敏实报表

This commit is contained in:
zjf committed 2026-08-18 18:13:07 +08:00
1 parent db277500de
commit 94f39deb2c
41 files changed
+1793 -124

No files matched your search

@@ -0,0 +1,18 @@
package com.bsd.pjcloud.canteen.dto.report;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class CanteenReportAggResultVO {
private Integer swipeRows;
private Integer unmatchedRows;
private Integer unmatchedPeople;
private List<CanteenReportRosterMissVO> unmatched = new ArrayList<>();
}
@@ -0,0 +1,53 @@
package com.bsd.pjcloud.canteen.dto.report;
import com.alibaba.excel.annotation.ExcelProperty;
import com.bsd.pjcloud.canteen.excel.ExcelYuanConverter;
import lombok.Data;
import java.math.BigDecimal;
/**
* 包厢台账(汇总)Excel,列序对齐样表。
*/
@Data
public class CanteenReportBoxExcel {
@ExcelProperty(index = 0)
private String monthText;
@ExcelProperty(index = 1)
private String bizDate;
@ExcelProperty(index = 5)
private String costCenter;
@ExcelProperty(index = 6)
private String costCenterCode;
@ExcelProperty(index = 8)
private String empNo;
@ExcelProperty(index = 9)
private String empName;
@ExcelProperty(index = 10, converter = ExcelYuanConverter.class)
private BigDecimal foodAmount;
@ExcelProperty(index = 11, converter = ExcelYuanConverter.class)
private BigDecimal drinkAmount;
@ExcelProperty(index = 12, converter = ExcelYuanConverter.class)
private BigDecimal totalAmount;
@ExcelProperty(index = 14)
private String legalEntity;
@ExcelProperty(index = 15)
private String remark1;
@ExcelProperty(index = 16)
private String remark2;
@ExcelProperty(index = 17)
private String park;
}
@@ -1,6 +1,7 @@
package com.bsd.pjcloud.canteen.dto.report;
import com.alibaba.excel.annotation.ExcelProperty;
import com.bsd.pjcloud.canteen.excel.ExcelYuanConverter;
import lombok.Data;
import java.math.BigDecimal;
@@ -35,28 +36,28 @@ public class CanteenReportFoodEnergyExcel {
@ExcelProperty(index = 7, value = "单位")
private String unit;
@ExcelProperty(index = 8, value = "数量")
@ExcelProperty(index = 8, value = "数量", converter = ExcelYuanConverter.class)
private BigDecimal qty;
@ExcelProperty(index = 9, value = "单价")
@ExcelProperty(index = 9, value = "单价", converter = ExcelYuanConverter.class)
private BigDecimal unitPrice;
@ExcelProperty(index = 10, value = "定价")
@ExcelProperty(index = 10, value = "定价", converter = ExcelYuanConverter.class)
private BigDecimal listPrice;
@ExcelProperty(index = 11, value = "差异")
@ExcelProperty(index = 11, value = "差异", converter = ExcelYuanConverter.class)
private BigDecimal priceDiff;
@ExcelProperty(index = 12, value = "结算单价")
@ExcelProperty(index = 12, value = "结算单价", converter = ExcelYuanConverter.class)
private BigDecimal settleUnitPrice;
@ExcelProperty(index = 13, value = "结算总金额")
@ExcelProperty(index = 13, value = "结算总金额", converter = ExcelYuanConverter.class)
private BigDecimal settleAmount;
@ExcelProperty(index = 14, value = "金额")
@ExcelProperty(index = 14, value = "金额", converter = ExcelYuanConverter.class)
private BigDecimal amount;
@ExcelProperty(index = 15, value = "金额差异")
@ExcelProperty(index = 15, value = "金额差异", converter = ExcelYuanConverter.class)
private BigDecimal amountDiff;
@ExcelProperty(index = 16, value = "食堂仓库")
@@ -0,0 +1,11 @@
package com.bsd.pjcloud.canteen.dto.report;
import lombok.Data;
@Data
public class CanteenReportOrgLevel2VO {
private String name;
private Long cnt;
}
@@ -0,0 +1,72 @@
package com.bsd.pjcloud.canteen.dto.report;
import com.alibaba.excel.annotation.ExcelProperty;
import com.bsd.pjcloud.canteen.excel.ExcelYuanConverter;
import lombok.Data;
import java.math.BigDecimal;
/**
* 食堂收发存报表(汇总)→ 映射为食材能耗。
* 列序对齐:2026年7月份食堂收发存报表(汇总).xlsx Sheet1
*/
@Data
public class CanteenReportReceiptStockExcel {
@ExcelProperty(index = 0)
private String bizDate;
@ExcelProperty(index = 1)
private String receiptNo;
@ExcelProperty(index = 2)
private String supplier;
@ExcelProperty(index = 3)
private String productCode;
@ExcelProperty(index = 4)
private String category;
@ExcelProperty(index = 5)
private String productName;
@ExcelProperty(index = 6)
private String brand;
@ExcelProperty(index = 7)
private String spec;
@ExcelProperty(index = 8)
private String unit;
@ExcelProperty(index = 9, converter = ExcelYuanConverter.class)
private BigDecimal qty;
@ExcelProperty(index = 10, converter = ExcelYuanConverter.class)
private BigDecimal unitPrice;
@ExcelProperty(index = 11, converter = ExcelYuanConverter.class)
private BigDecimal listPrice;
@ExcelProperty(index = 12, converter = ExcelYuanConverter.class)
private BigDecimal priceDiff;
@ExcelProperty(index = 13, converter = ExcelYuanConverter.class)
private BigDecimal settleUnitPrice;
@ExcelProperty(index = 14, converter = ExcelYuanConverter.class)
private BigDecimal settleAmount;
@ExcelProperty(index = 15, converter = ExcelYuanConverter.class)
private BigDecimal amount;
@ExcelProperty(index = 16, converter = ExcelYuanConverter.class)
private BigDecimal amountDiff;
@ExcelProperty(index = 17)
private String warehouse;
@ExcelProperty(index = 21)
private String park;
}
@@ -0,0 +1,59 @@
package com.bsd.pjcloud.canteen.dto.report;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
/**
* 花名册 Excel(列序对齐 202607花名册.xlsx)
*/
@Data
public class CanteenReportRosterExcel {
@ExcelProperty(index = 0)
private String empNo;
@ExcelProperty(index = 1)
private String empName;
@ExcelProperty(index = 2)
private String empGroup;
@ExcelProperty(index = 3)
private String deptCxo1;
@ExcelProperty(index = 4)
private String deptCxo2;
@ExcelProperty(index = 5)
private String deptCxo3;
@ExcelProperty(index = 6)
private String jobTitle;
@ExcelProperty(index = 7)
private String jobNature;
@ExcelProperty(index = 8)
private String hrScope;
@ExcelProperty(index = 9)
private String legalCompany;
@ExcelProperty(index = 10)
private String rdFlag;
@ExcelProperty(index = 11)
private String financeCode;
@ExcelProperty(index = 12)
private String financeName;
@ExcelProperty(index = 13)
private String erpCostCode;
@ExcelProperty(index = 14)
private String erpCostName;
@ExcelProperty(index = 15)
private String orgTag;
}
@@ -0,0 +1,25 @@
package com.bsd.pjcloud.canteen.dto.report;
import lombok.Data;
/**
* 汇总时未命中花名册人事范围的人员(已回退法律归属)
*/
@Data
public class CanteenReportRosterMissVO {
private String empNo;
private String empName;
private String legalEntity;
/** 回退后的核对成本 */
private String costCenter;
/** 无工号 / 花名册无此工号 / 花名册无人事范围 */
private String reason;
/** 汇总刷卡行数 */
private Integer rowCount;
}
@@ -29,7 +29,7 @@ public class CanteenReportSwipeExcel {
@ExcelProperty(index = 5, value = "结算台")
private String settleCounter;
@ExcelProperty(index = 6, value = "消费金额")
@ExcelProperty(index = 6, value = "消费金额", converter = com.bsd.pjcloud.canteen.excel.ExcelYuanConverter.class)
private BigDecimal consumeAmount;
@ExcelProperty(index = 7, value = "收入类别")
@@ -48,6 +48,9 @@ public class CanteenReportImportLogEntity extends Model<CanteenReportImportLogEn
@Schema(description = "跳过行数")
private Integer skipRows;
@Schema(description = "跳过原因摘要")
private String skipSummary;
@Schema(description = "汇总写入刷卡明细行数")
private Integer aggRows;
@@ -0,0 +1,75 @@
package com.bsd.pjcloud.canteen.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@TableName("canteen_report_roster")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "食堂报表-花名册")
public class CanteenReportRosterEntity extends Model<CanteenReportRosterEntity> {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@Schema(description = "适用月份 yyyy-MM")
private String bizMonth;
@Schema(description = "全球工号")
private String empNo;
@Schema(description = "员工姓名")
private String empName;
private String empGroup;
private String deptCxo1;
private String deptCxo2;
private String deptCxo3;
private String jobTitle;
private String jobNature;
@Schema(description = "人事范围→核对成本")
private String hrScope;
private String legalCompany;
private String rdFlag;
private String financeCode;
private String financeName;
private String erpCostCode;
private String erpCostName;
private String orgTag;
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
@TableField(fill = FieldFill.INSERT)
private String delFlag;
private Long tenantId;
}
@@ -0,0 +1,40 @@
package com.bsd.pjcloud.canteen.excel;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import java.math.BigDecimal;
/**
* 覆盖 EasyExcel 默认 {@code BigDecimalStringConverter}:文本「杯」等非数字返回 null,不抛异常。
*/
public class ExcelYuanStringConverter implements Converter<BigDecimal> {
private final ExcelYuanConverter delegate = new ExcelYuanConverter();
@Override
public Class<BigDecimal> supportJavaTypeKey() {
return BigDecimal.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public BigDecimal convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
return delegate.convertToJavaData(cellData, contentProperty, globalConfiguration);
}
@Override
public WriteCellData<?> convertToExcelData(BigDecimal value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
return delegate.convertToExcelData(value, contentProperty, globalConfiguration);
}
}
@@ -30,6 +30,9 @@ public final class CanteenReportConstants {
public static final String INCOME_OTHER = "其他收入";
public static final String INCOME_BOX = "包厢收入";
/** 刷卡备注:花名册未命中,核对成本已回退法律归属 */
public static final String REMARK_ROSTER_MISS = "未命中花名册";
public static final List<String> INCOME_TYPES = Collections.unmodifiableList(Arrays.asList(
INCOME_SWIPE, INCOME_ALIPAY, INCOME_BOX, INCOME_OTHER, INCOME_PROFIT_ADJ, INCOME_INVOICE, INCOME_BAD_DEBT
));
@@ -171,6 +171,19 @@ public final class CanteenReportFaceMapper {
// continue
}
}
// Excel 序列日期(如 45854 或 45854.375)
if (s.matches("\\d{4,5}(\\.\\d+)?")) {
try {
double serial = Double.parseDouble(s);
if (serial > 20000 && serial < 80000) {
LocalDate base = LocalDate.of(1899, 12, 30);
return base.plusDays((long) serial).toString();
}
}
catch (Exception ignored) {
// continue
}
}
try {
String head = s.length() >= 10 ? s.substring(0, 10).replace('/', '-') : s.replace('/', '-');
return LocalDate.parse(head, DateTimeFormatter.ISO_LOCAL_DATE).toString();
@@ -0,0 +1,18 @@
package com.bsd.pjcloud.canteen.vo.report;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "供应商分摊核对:厂区食材池 + 权重明细")
public class CanteenReportShareAuditVO {
@Schema(description = "各厂区食材池 / 可分摊 / 分摊合计")
private List<CanteenReportShareParkCheckVO> parkChecks = new ArrayList<>();
@Schema(description = "各成本中心在各厂区的权重与员工刷卡")
private List<CanteenReportShareWeightRowVO> weightRows = new ArrayList<>();
}
@@ -0,0 +1,32 @@
package com.bsd.pjcloud.canteen.vo.report;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Schema(description = "供应商分摊-厂区核对(元)")
public class CanteenReportShareParkCheckVO {
@Schema(description = "厂区")
private String park;
@Schema(description = "食材池:食材能耗结算总金额,分类∈食材/能耗/外协")
private BigDecimal foodPool = BigDecimal.ZERO;
@Schema(description = "厂区包厢+其他收入(刷卡)")
private BigDecimal boxOther = BigDecimal.ZERO;
@Schema(description = "该厂区员工刷卡合计(权重分母)")
private BigDecimal swipeTotal = BigDecimal.ZERO;
@Schema(description = "可分摊 = 食材池 − 包厢其他")
private BigDecimal allocable = BigDecimal.ZERO;
@Schema(description = "各成本中心分摊合计")
private BigDecimal shareTotal = BigDecimal.ZERO;
@Schema(description = "差额 = 分摊合计 − 可分摊,接近 0 即核对通过")
private BigDecimal diff = BigDecimal.ZERO;
}
@@ -0,0 +1,25 @@
package com.bsd.pjcloud.canteen.vo.report;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.Map;
@Data
@Schema(description = "供应商分摊-权重行:该公司该厂区员工刷卡 / 厂区员工刷卡合计")
public class CanteenReportShareWeightRowVO {
@Schema(description = "成本中心")
private String costCenter;
@Schema(description = "该公司各厂区员工刷卡(元)")
private Map<String, BigDecimal> swipeAmounts = new LinkedHashMap<>();
@Schema(description = "权重 0~1,分摊金额 = 权重 × 该厂区可分摊")
private Map<String, BigDecimal> weights = new LinkedHashMap<>();
@Schema(description = "分摊金额(元)= 权重 × 可分摊")
private Map<String, BigDecimal> shareAmounts = new LinkedHashMap<>();
}
@@ -0,0 +1,31 @@
package com.bsd.pjcloud.canteen.controller;
import com.bsd.pjcloud.canteen.service.CanteenReportBoxImportService;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequiredArgsConstructor
@RequestMapping("/canteenReport/boxImport")
@Tag(description = "canteenReportBoxImport", name = "食堂报表-包厢台账导入(演示)")
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class CanteenReportBoxImportController {
private final CanteenReportBoxImportService boxImportService;
@SysLog("导入包厢台账")
@Operation(summary = "导入包厢台账Excel → 刷卡明细(包厢收入)")
@PostMapping("/import")
public R importExcel(@RequestParam("file") MultipartFile file,
@RequestParam String bizMonth,
@RequestParam(defaultValue = "true") boolean replaceMonth) {
return boxImportService.importExcel(file, bizMonth, replaceMonth);
}
}
@@ -51,6 +51,12 @@ public class CanteenReportController {
return R.ok(calcService.supplierShareReport(bizMonth));
}
@Operation(summary = "供应商分摊-厂区核对与权重明细")
@GetMapping("/supplierShare/parkCheck")
public R supplierShareParkCheck(@RequestParam String bizMonth) {
return R.ok(calcService.supplierShareAudit(bizMonth));
}
@Operation(summary = "经营报表")
@GetMapping("/business")
public R business(@RequestParam String bizMonth) {
@@ -45,7 +45,11 @@ public class CanteenReportFaceImportController {
@RequestParam(required = false) String bizMonth,
@RequestParam(required = false) String importLogId,
@RequestParam(required = false) String empNo,
@RequestParam(required = false) String payMethod) {
@RequestParam(required = false) String payMethod,
@RequestParam(required = false) String deviceName,
@RequestParam(required = false) String empName,
@RequestParam(required = false) String phone,
@RequestParam(required = false) String orgLevel2s) {
Long logId = null;
if (cn.hutool.core.util.StrUtil.isNotBlank(importLogId)) {
try {
@@ -55,7 +59,35 @@ public class CanteenReportFaceImportController {
return R.failed("importLogId 非法");
}
}
return R.ok(faceImportService.pageDetails(page, bizMonth, logId, empNo, payMethod));
java.util.List<String> orgs = cn.hutool.core.util.StrUtil.isBlank(orgLevel2s)
? java.util.List.of()
: cn.hutool.core.util.StrUtil.splitTrim(orgLevel2s, ',');
return R.ok(faceImportService.pageDetails(page, bizMonth, logId, empNo, payMethod, deviceName, empName, phone, orgs));
}
@Operation(summary = "人脸明细按筛选条件汇总订单金额(元)")
@GetMapping("/detail/sum")
public R detailSum(@RequestParam(required = false) String bizMonth,
@RequestParam(required = false) String importLogId,
@RequestParam(required = false) String empNo,
@RequestParam(required = false) String payMethod,
@RequestParam(required = false) String deviceName,
@RequestParam(required = false) String empName,
@RequestParam(required = false) String phone,
@RequestParam(required = false) String orgLevel2s) {
Long logId = null;
if (cn.hutool.core.util.StrUtil.isNotBlank(importLogId)) {
try {
logId = Long.parseLong(importLogId.trim());
}
catch (NumberFormatException e) {
return R.failed("importLogId 非法");
}
}
java.util.List<String> orgs = cn.hutool.core.util.StrUtil.isBlank(orgLevel2s)
? java.util.List.of()
: cn.hutool.core.util.StrUtil.splitTrim(orgLevel2s, ',');
return R.ok(faceImportService.sumDetails(bizMonth, logId, empNo, payMethod, deviceName, empName, phone, orgs));
}
@SysLog("人脸消费Excel导入明细")
@@ -76,7 +108,8 @@ public class CanteenReportFaceImportController {
@RequestParam(required = false) String dateStart,
@RequestParam(required = false) String dateEnd,
@RequestParam(required = false) String importLogId,
@RequestParam(defaultValue = "true") boolean replaceFace) {
@RequestParam(defaultValue = "true") boolean replaceFace,
@RequestParam(required = false) String orgLevel2s) {
Long logId = null;
if (cn.hutool.core.util.StrUtil.isNotBlank(importLogId)) {
try {
@@ -86,6 +119,25 @@ public class CanteenReportFaceImportController {
return R.failed("importLogId 非法: " + importLogId);
}
}
return faceImportService.aggregateToSwipe(bizMonth, dateStart, dateEnd, logId, replaceFace);
java.util.List<String> orgs = cn.hutool.core.util.StrUtil.isBlank(orgLevel2s)
? java.util.List.of()
: cn.hutool.core.util.StrUtil.splitTrim(orgLevel2s, ',');
return faceImportService.aggregateToSwipe(bizMonth, dateStart, dateEnd, logId, replaceFace, orgs);
}
@Operation(summary = "明细中的二级组织列表(汇总筛选用)")
@GetMapping("/orgLevel2")
public R listOrgLevel2(@RequestParam(required = false) String bizMonth,
@RequestParam(required = false) String importLogId) {
Long logId = null;
if (cn.hutool.core.util.StrUtil.isNotBlank(importLogId)) {
try {
logId = Long.parseLong(importLogId.trim());
}
catch (NumberFormatException e) {
return R.failed("importLogId 非法");
}
}
return R.ok(faceImportService.listOrgLevel2(bizMonth, logId));
}
}
@@ -43,6 +43,18 @@ public class CanteenReportFoodEnergyController {
return R.ok(foodEnergyService.getById(id));
}
@SysLog("新增食材能耗")
@PostMapping
public R save(@RequestBody CanteenReportFoodEnergyEntity entity) {
return foodEnergyService.saveOrUpdateRow(entity);
}
@SysLog("修改食材能耗")
@PutMapping
public R update(@RequestBody CanteenReportFoodEnergyEntity entity) {
return foodEnergyService.saveOrUpdateRow(entity);
}
@SysLog("删除食材能耗")
@DeleteMapping
public R remove(@RequestBody Long[] ids) {
@@ -56,14 +68,25 @@ public class CanteenReportFoodEnergyController {
.eq(CanteenReportFoodEnergyEntity::getBizMonth, bizMonth)));
}
@Operation(summary = "导入Excel(自动识别「食材能耗」sheet,可传整本报表)")
@Operation(summary = "导入Excel(自动识别收发存汇总表 / 食堂报表「食材能耗」sheet)")
@PostMapping("/import")
public R importExcel(@RequestParam("file") org.springframework.web.multipart.MultipartFile file,
@RequestParam String bizMonth,
@RequestParam(defaultValue = "true") boolean replaceMonth) throws Exception {
List<CanteenReportFoodEnergyExcel> rows = com.bsd.pjcloud.canteen.util.CanteenReportExcelUtils
.readSheet(file, CanteenReportFoodEnergyExcel.class, "食材能耗");
return foodEnergyService.importExcel(rows, bizMonth, replaceMonth, null);
@RequestParam(defaultValue = "true") boolean replaceMonth) {
try {
if (com.bsd.pjcloud.canteen.util.CanteenReportExcelUtils.isReceiptStockLayout(file)) {
return foodEnergyService.importReceiptStock(file, bizMonth, replaceMonth);
}
List<CanteenReportFoodEnergyExcel> rows = com.bsd.pjcloud.canteen.util.CanteenReportExcelUtils
.readSheet(file, CanteenReportFoodEnergyExcel.class, "食材能耗");
return foodEnergyService.importExcel(rows, bizMonth, replaceMonth, null);
}
catch (com.alibaba.excel.exception.ExcelDataConvertException e) {
return R.failed("Excel 列对不上。请上传收发存汇总表,或整本食堂报表(含「食材能耗」sheet)");
}
catch (Exception e) {
return R.failed("导入失败: " + e.getMessage());
}
}
@ResponseExcel
@@ -0,0 +1,43 @@
package com.bsd.pjcloud.canteen.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.canteen.service.CanteenReportRosterService;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequiredArgsConstructor
@RequestMapping("/canteenReport/roster")
@Tag(description = "canteenReportRoster", name = "食堂报表-花名册(演示)")
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class CanteenReportRosterController {
private final CanteenReportRosterService rosterService;
@Operation(summary = "分页")
@GetMapping("/page")
public R page(@ParameterObject Page page,
@RequestParam(required = false) String bizMonth,
@RequestParam(required = false) String empNo,
@RequestParam(required = false) String empName,
@RequestParam(required = false) String hrScope) {
return R.ok(rosterService.pageList(page, bizMonth, empNo, empName, hrScope));
}
@SysLog("导入花名册")
@Operation(summary = "导入花名册Excel")
@PostMapping("/import")
public R importExcel(@RequestParam("file") MultipartFile file,
@RequestParam String bizMonth,
@RequestParam(defaultValue = "true") boolean replaceMonth) {
return rosterService.importExcel(file, bizMonth, replaceMonth);
}
}
@@ -38,8 +38,25 @@ public class CanteenReportSwipeController {
@RequestParam(required = false) String incomeType,
@RequestParam(required = false) String costCenter,
@RequestParam(required = false) String empNo,
@RequestParam(required = false) Long importLogId) {
return R.ok(swipeService.pageList(page, bizMonth, park, incomeType, costCenter, empNo, importLogId));
@RequestParam(required = false) Long importLogId,
@RequestParam(required = false) String remarkTag,
@RequestParam(required = false) String source) {
return R.ok(swipeService.pageList(page, bizMonth, park, incomeType, costCenter, empNo, importLogId, remarkTag,
source));
}
@Operation(summary = "按当前筛选条件汇总消费金额(元)")
@GetMapping("/sum")
public R sum(@RequestParam(required = false) String bizMonth,
@RequestParam(required = false) String park,
@RequestParam(required = false) String incomeType,
@RequestParam(required = false) String costCenter,
@RequestParam(required = false) String empNo,
@RequestParam(required = false) Long importLogId,
@RequestParam(required = false) String remarkTag,
@RequestParam(required = false) String source) {
return R.ok(swipeService.sumAmount(bizMonth, park, incomeType, costCenter, empNo, importLogId, remarkTag,
source));
}
@Operation(summary = "详情")
@@ -2,6 +2,7 @@ package com.bsd.pjcloud.canteen.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportFaceAggRow;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportOrgLevel2VO;
import com.bsd.pjcloud.canteen.entity.CanteenReportFaceDetailEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -25,10 +26,16 @@ public interface CanteenReportFaceDetailMapper extends BaseMapper<CanteenReportF
/**
* 按人+维度汇总;B扫C 等归入支付宝,其余归入员工刷卡。金额用订单金额。
* 传入 importLogId 时只按批次汇总,忽略日期段。
* orgLevel2s 非空时只汇总这些二级组织。
*/
List<CanteenReportFaceAggRow> selectAggByMonth(@Param("bizMonth") String bizMonth,
@Param("dateStart") String dateStart, @Param("dateEnd") String dateEnd,
@Param("importLogId") Long importLogId);
@Param("importLogId") Long importLogId,
@Param("orgLevel2s") List<String> orgLevel2s);
long countDetail(@Param("bizMonth") String bizMonth, @Param("importLogId") Long importLogId);
long countDetail(@Param("bizMonth") String bizMonth, @Param("importLogId") Long importLogId,
@Param("orgLevel2s") List<String> orgLevel2s);
List<CanteenReportOrgLevel2VO> listOrgLevel2(@Param("bizMonth") String bizMonth,
@Param("importLogId") Long importLogId);
}
@@ -0,0 +1,16 @@
package com.bsd.pjcloud.canteen.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bsd.pjcloud.canteen.entity.CanteenReportRosterEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface CanteenReportRosterMapper extends BaseMapper<CanteenReportRosterEntity> {
int deletePhysicallyByBizMonth(@Param("bizMonth") String bizMonth);
int insertBatch(@Param("list") List<CanteenReportRosterEntity> list);
}
@@ -26,5 +26,8 @@ public interface CanteenReportSwipeMapper extends BaseMapper<CanteenReportSwipeE
@Param("dateStart") String dateStart, @Param("dateEnd") String dateEnd,
@Param("importLogId") Long importLogId);
/** 按来源物理删除某月刷卡(如 BOX 包厢) */
int deletePhysicallyBySourceAndBizMonth(@Param("source") String source, @Param("bizMonth") String bizMonth);
int insertBatch(@Param("list") List<CanteenReportSwipeEntity> list);
}
@@ -0,0 +1,12 @@
package com.bsd.pjcloud.canteen.service;
import com.bsd.pjcloud.common.core.util.R;
import org.springframework.web.multipart.MultipartFile;
/**
* 包厢台账 → 刷卡明细(收入类别=包厢收入,source=BOX)
*/
public interface CanteenReportBoxImportService {
R importExcel(MultipartFile file, String bizMonth, boolean replaceMonth);
}
@@ -2,6 +2,7 @@ package com.bsd.pjcloud.canteen.service;
import com.bsd.pjcloud.canteen.vo.report.CanteenReportBusinessRowVO;
import com.bsd.pjcloud.canteen.vo.report.CanteenReportIncomeRowVO;
import com.bsd.pjcloud.canteen.vo.report.CanteenReportShareAuditVO;
import com.bsd.pjcloud.canteen.vo.report.CanteenReportShareRowVO;
import java.util.List;
@@ -16,6 +17,9 @@ public interface CanteenReportCalcService {
List<CanteenReportShareRowVO> supplierShareReport(String bizMonth);
/** 厂区核对 + 权重明细 */
CanteenReportShareAuditVO supplierShareAudit(String bizMonth);
List<CanteenReportBusinessRowVO> businessReport(String bizMonth);
Map<String, Object> summary(String bizMonth);
@@ -2,11 +2,15 @@ package com.bsd.pjcloud.canteen.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportOrgLevel2VO;
import com.bsd.pjcloud.canteen.entity.CanteenReportFaceDetailEntity;
import com.bsd.pjcloud.canteen.entity.CanteenReportImportLogEntity;
import com.bsd.pjcloud.common.core.util.R;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map;
public interface CanteenReportFaceImportService extends IService<CanteenReportImportLogEntity> {
/**
@@ -22,11 +26,19 @@ public interface CanteenReportFaceImportService extends IService<CanteenReportIm
* @param dateEnd 可选日期段
* @param importLogId 可选,仅汇总某一批导入
* @param replaceFace 是否先清空该月已有 FACE 汇总刷卡行
* @param orgLevel2s 可选二级组织,非空时只汇总这些组织(如嘉兴地区、湖州地区)
*/
R aggregateToSwipe(String bizMonth, String dateStart, String dateEnd, Long importLogId, boolean replaceFace);
R aggregateToSwipe(String bizMonth, String dateStart, String dateEnd, Long importLogId, boolean replaceFace,
List<String> orgLevel2s);
Page<CanteenReportImportLogEntity> pageLogs(Page page, String bizMonth, String status);
Page<CanteenReportFaceDetailEntity> pageDetails(Page page, String bizMonth, Long importLogId, String empNo,
String payMethod);
String payMethod, String deviceName, String empName, String phone, List<String> orgLevel2s);
/** 与明细分页相同条件的订单金额合计(元) */
Map<String, Object> sumDetails(String bizMonth, Long importLogId, String empNo, String payMethod,
String deviceName, String empName, String phone, List<String> orgLevel2s);
List<CanteenReportOrgLevel2VO> listOrgLevel2(String bizMonth, Long importLogId);
}
@@ -6,6 +6,7 @@ import com.bsd.pjcloud.canteen.dto.report.CanteenReportFoodEnergyExcel;
import com.bsd.pjcloud.canteen.entity.CanteenReportFoodEnergyEntity;
import com.bsd.pjcloud.common.core.util.R;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@@ -13,7 +14,13 @@ public interface CanteenReportFoodEnergyService extends IService<CanteenReportFo
Page<CanteenReportFoodEnergyEntity> pageList(Page page, String bizMonth, String park, String supplier);
/** 手工新增/修改一行(外协、能耗、耗材、食材等) */
R saveOrUpdateRow(CanteenReportFoodEnergyEntity entity);
R importExcel(List<CanteenReportFoodEnergyExcel> rows, String bizMonth, boolean replaceMonth, BindingResult bindingResult);
/** 导入「食堂收发存报表(汇总)」列布局(由 /import 自动识别调用) */
R importReceiptStock(MultipartFile file, String bizMonth, boolean replaceMonth);
List<CanteenReportFoodEnergyExcel> exportExcel(String bizMonth);
}
@@ -0,0 +1,19 @@
package com.bsd.pjcloud.canteen.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.canteen.entity.CanteenReportRosterEntity;
import com.bsd.pjcloud.common.core.util.R;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
public interface CanteenReportRosterService extends IService<CanteenReportRosterEntity> {
Page<CanteenReportRosterEntity> pageList(Page page, String bizMonth, String empNo, String empName, String hrScope);
R importExcel(MultipartFile file, String bizMonth, boolean replaceMonth);
/** empNo -> 人事范围;人在花名册但人事范围为空时值为空串(key 仍存在) */
Map<String, String> loadHrScopeMap(String bizMonth);
}
@@ -7,12 +7,18 @@ import com.bsd.pjcloud.canteen.entity.CanteenReportSwipeEntity;
import com.bsd.pjcloud.common.core.util.R;
import org.springframework.validation.BindingResult;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
public interface CanteenReportSwipeService extends IService<CanteenReportSwipeEntity> {
Page<CanteenReportSwipeEntity> pageList(Page page, String bizMonth, String park, String incomeType, String costCenter,
String empNo, Long importLogId);
String empNo, Long importLogId, String remarkTag, String source);
/** 与分页相同条件的消费金额合计(元),含 bySource 拆分 */
Map<String, Object> sumAmount(String bizMonth, String park, String incomeType, String costCenter,
String empNo, Long importLogId, String remarkTag, String source);
R importExcel(List<CanteenReportSwipeExcel> rows, String bizMonth, boolean replaceMonth, BindingResult bindingResult);
@@ -0,0 +1,139 @@
package com.bsd.pjcloud.canteen.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.bsd.pjcloud.canteen.excel.ExcelYuanConverter;
import com.bsd.pjcloud.canteen.excel.ExcelYuanStringConverter;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportBoxExcel;
import com.bsd.pjcloud.canteen.entity.CanteenReportSwipeEntity;
import com.bsd.pjcloud.canteen.mapper.CanteenReportSwipeMapper;
import com.bsd.pjcloud.canteen.report.CanteenReportConstants;
import com.bsd.pjcloud.canteen.service.CanteenReportBoxImportService;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.data.tenant.TenantContextHolder;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class CanteenReportBoxImportServiceImpl implements CanteenReportBoxImportService {
private static final String SOURCE_BOX = "BOX";
private static final int BATCH = 800;
private final CanteenReportSwipeMapper swipeMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public R importExcel(MultipartFile file, String bizMonth, boolean replaceMonth) {
if (file == null || file.isEmpty()) {
return R.failed("请上传包厢台账 Excel");
}
if (StrUtil.isBlank(bizMonth)) {
return R.failed("请传入业务月份 bizMonth");
}
try {
List<CanteenReportBoxExcel> rows = EasyExcel.read(file.getInputStream())
.head(CanteenReportBoxExcel.class)
.registerConverter(new ExcelYuanConverter())
.registerConverter(new ExcelYuanStringConverter())
.sheet()
.headRowNumber(1)
.doReadSync();
if (CollUtil.isEmpty(rows)) {
return R.failed("导入数据为空");
}
String operator = currentUser();
Long tenantId = TenantContextHolder.getTenantId();
LocalDateTime now = LocalDateTime.now();
List<CanteenReportSwipeEntity> saveList = new ArrayList<>();
int skip = 0;
for (CanteenReportBoxExcel row : rows) {
BigDecimal amount = resolveAmount(row);
if (amount == null || amount.compareTo(BigDecimal.ZERO) == 0) {
skip++;
continue;
}
if (StrUtil.isBlank(row.getCostCenter()) && StrUtil.isBlank(row.getEmpNo())
&& StrUtil.isBlank(row.getPark())) {
skip++;
continue;
}
CanteenReportSwipeEntity e = new CanteenReportSwipeEntity();
e.setId(IdWorker.getId());
e.setBizMonth(bizMonth.trim());
e.setBizDate(blankToNull(row.getBizDate()));
e.setEmpNo(blankToNull(row.getEmpNo()));
e.setEmpName(blankToNull(row.getEmpName()));
e.setLegalEntity(blankToNull(row.getLegalEntity()));
e.setConsumeAmount(amount);
e.setIncomeType(CanteenReportConstants.INCOME_BOX);
e.setCostCenter(StrUtil.blankToDefault(blankToNull(row.getCostCenter()), "未匹配成本中心"));
e.setPark(blankToNull(row.getPark()));
String remark = StrUtil.join(" ",
StrUtil.blankToDefault(row.getRemark1(), ""),
StrUtil.blankToDefault(row.getRemark2(), "")).trim();
e.setRemarkTag(StrUtil.isBlank(remark) ? "包厢台账" : remark);
e.setSource(SOURCE_BOX);
e.setCreateBy(operator);
e.setUpdateBy(operator);
e.setCreateTime(now);
e.setUpdateTime(now);
e.setDelFlag(CommonConstants.STATUS_NORMAL);
e.setTenantId(tenantId);
saveList.add(e);
}
if (CollUtil.isEmpty(saveList)) {
return R.failed("无有效包厢金额可入库");
}
if (replaceMonth) {
swipeMapper.deletePhysicallyBySourceAndBizMonth(SOURCE_BOX, bizMonth.trim());
}
for (List<CanteenReportSwipeEntity> chunk : CollUtil.split(saveList, BATCH)) {
swipeMapper.insertBatch(chunk);
}
return R.ok(null, String.format("包厢导入成功:写入刷卡 %d 条(收入类别=包厢收入),跳过 %d 条",
saveList.size(), skip));
}
catch (Exception e) {
return R.failed("导入失败: " + e.getMessage());
}
}
private static BigDecimal resolveAmount(CanteenReportBoxExcel row) {
if (row.getTotalAmount() != null) {
return row.getTotalAmount();
}
BigDecimal food = row.getFoodAmount() == null ? BigDecimal.ZERO : row.getFoodAmount();
BigDecimal drink = row.getDrinkAmount() == null ? BigDecimal.ZERO : row.getDrinkAmount();
BigDecimal sum = food.add(drink);
return sum.compareTo(BigDecimal.ZERO) == 0 ? null : sum;
}
private static String blankToNull(String s) {
return StrUtil.isBlank(s) ? null : s.trim();
}
private static String currentUser() {
try {
if (SecurityUtils.getUser() != null) {
return SecurityUtils.getUser().getUsername();
}
}
catch (Exception ignored) {
// ignore
}
return null;
}
}
@@ -12,7 +12,10 @@ import com.bsd.pjcloud.canteen.service.CanteenReportManualAdjustService;
import com.bsd.pjcloud.canteen.service.CanteenReportSwipeService;
import com.bsd.pjcloud.canteen.vo.report.CanteenReportBusinessRowVO;
import com.bsd.pjcloud.canteen.vo.report.CanteenReportIncomeRowVO;
import com.bsd.pjcloud.canteen.vo.report.CanteenReportShareAuditVO;
import com.bsd.pjcloud.canteen.vo.report.CanteenReportShareParkCheckVO;
import com.bsd.pjcloud.canteen.vo.report.CanteenReportShareRowVO;
import com.bsd.pjcloud.canteen.vo.report.CanteenReportShareWeightRowVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -27,7 +30,7 @@ import java.util.Map;
* 报表计算演示实现:
* 收入合计 = 刷卡明细按成本中心×厂区汇总;
* 供应商分摊 = 权重 × (厂区食材池 − 厂区包厢 − 厂区其他收入),权重=中心员工刷卡/厂区员工刷卡;
* 经营报表 = 收入(万元) + 食材支出(分摊/10000) + 毛利分析。
* 经营报表 = 收入(万元) + 支出(食材=分摊−能耗−外协;能耗/外协按食材能耗分类;人工按人工列) + 毛利分析。
*/
@Service
@RequiredArgsConstructor
@@ -209,10 +212,119 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService {
return rows;
}
@Override
public CanteenReportShareAuditVO supplierShareAudit(String bizMonth) {
List<CanteenReportFoodEnergyEntity> foods = loadFoods(bizMonth);
List<CanteenReportSwipeEntity> swipes = loadSwipes(bizMonth);
Map<String, BigDecimal> parkFoodPool = new HashMap<>();
for (CanteenReportFoodEnergyEntity f : foods) {
String classify = StrUtil.blankToDefault(f.getClassify(), "食材").trim();
if (!CanteenReportConstants.SHARE_FOOD_CLASSIFY.contains(classify)) {
continue;
}
parkFoodPool.merge(StrUtil.blankToDefault(f.getPark(), ""), nz(f.getSettleAmount()), BigDecimal::add);
}
Map<String, BigDecimal> parkBoxOther = new HashMap<>();
Map<String, BigDecimal> parkSwipeTotal = new HashMap<>();
Map<String, BigDecimal> centerParkSwipe = new HashMap<>();
for (CanteenReportSwipeEntity s : swipes) {
String park = StrUtil.blankToDefault(s.getPark(), "");
String center = StrUtil.blankToDefault(s.getCostCenter(), "未匹配成本中心");
String type = StrUtil.blankToDefault(s.getIncomeType(), "");
BigDecimal amt = nz(s.getConsumeAmount());
if (CanteenReportConstants.INCOME_SWIPE.equals(type)) {
parkSwipeTotal.merge(park, amt, BigDecimal::add);
centerParkSwipe.merge(center + "|" + park, amt, BigDecimal::add);
}
if (CanteenReportConstants.SHARE_BOX_OTHER_TYPES.contains(type)) {
parkBoxOther.merge(park, amt, BigDecimal::add);
}
}
Map<String, BigDecimal> parkAllocable = new LinkedHashMap<>();
for (String park : CanteenReportConstants.SHARE_PARKS) {
parkAllocable.put(park, nz(parkFoodPool.get(park)).subtract(nz(parkBoxOther.get(park))));
}
List<String> centers = new ArrayList<>(CanteenReportConstants.COST_CENTERS);
for (String k : centerParkSwipe.keySet()) {
String c = k.substring(0, k.indexOf('|'));
if (!centers.contains(c)) {
centers.add(c);
}
}
List<CanteenReportShareWeightRowVO> weightRows = new ArrayList<>();
Map<String, BigDecimal> shareParkTotal = new LinkedHashMap<>();
Map<String, BigDecimal> weightParkTotal = new LinkedHashMap<>();
for (String park : CanteenReportConstants.SHARE_PARKS) {
shareParkTotal.put(park, BigDecimal.ZERO);
weightParkTotal.put(park, BigDecimal.ZERO);
}
for (String center : centers) {
CanteenReportShareWeightRowVO row = new CanteenReportShareWeightRowVO();
row.setCostCenter(center);
boolean any = false;
for (String park : CanteenReportConstants.SHARE_PARKS) {
BigDecimal parkTotal = nz(parkSwipeTotal.get(park));
BigDecimal centerAmt = nz(centerParkSwipe.get(center + "|" + park)).setScale(SCALE, RoundingMode.HALF_UP);
BigDecimal weight = BigDecimal.ZERO;
BigDecimal share = BigDecimal.ZERO;
BigDecimal allocable = nz(parkAllocable.get(park));
if (parkTotal.compareTo(BigDecimal.ZERO) > 0) {
weight = centerAmt.divide(parkTotal, 8, RoundingMode.HALF_UP);
if (allocable.compareTo(BigDecimal.ZERO) != 0) {
share = weight.multiply(allocable).setScale(SCALE, RoundingMode.HALF_UP);
}
}
row.getSwipeAmounts().put(park, centerAmt);
row.getWeights().put(park, weight);
row.getShareAmounts().put(park, share);
shareParkTotal.merge(park, share, BigDecimal::add);
weightParkTotal.merge(park, weight, BigDecimal::add);
if (centerAmt.compareTo(BigDecimal.ZERO) != 0 || share.compareTo(BigDecimal.ZERO) != 0) {
any = true;
}
}
if (any) {
weightRows.add(row);
}
}
CanteenReportShareWeightRowVO totalRow = new CanteenReportShareWeightRowVO();
totalRow.setCostCenter("合计(权重应≈1)");
for (String park : CanteenReportConstants.SHARE_PARKS) {
totalRow.getSwipeAmounts().put(park, nz(parkSwipeTotal.get(park)).setScale(SCALE, RoundingMode.HALF_UP));
totalRow.getWeights().put(park, nz(weightParkTotal.get(park)).setScale(8, RoundingMode.HALF_UP));
totalRow.getShareAmounts().put(park, nz(shareParkTotal.get(park)).setScale(SCALE, RoundingMode.HALF_UP));
}
weightRows.add(totalRow);
List<CanteenReportShareParkCheckVO> parkChecks = new ArrayList<>();
for (String park : CanteenReportConstants.SHARE_PARKS) {
BigDecimal foodPool = nz(parkFoodPool.get(park)).setScale(SCALE, RoundingMode.HALF_UP);
BigDecimal boxOther = nz(parkBoxOther.get(park)).setScale(SCALE, RoundingMode.HALF_UP);
BigDecimal allocable = foodPool.subtract(boxOther).setScale(SCALE, RoundingMode.HALF_UP);
BigDecimal shareTotal = nz(shareParkTotal.get(park)).setScale(SCALE, RoundingMode.HALF_UP);
CanteenReportShareParkCheckVO vo = new CanteenReportShareParkCheckVO();
vo.setPark(park);
vo.setFoodPool(foodPool);
vo.setBoxOther(boxOther);
vo.setAllocable(allocable);
vo.setSwipeTotal(nz(parkSwipeTotal.get(park)).setScale(SCALE, RoundingMode.HALF_UP));
vo.setShareTotal(shareTotal);
vo.setDiff(shareTotal.subtract(allocable).setScale(SCALE, RoundingMode.HALF_UP));
parkChecks.add(vo);
}
CanteenReportShareAuditVO audit = new CanteenReportShareAuditVO();
audit.setParkChecks(parkChecks);
audit.setWeightRows(weightRows);
return audit;
}
@Override
public List<CanteenReportBusinessRowVO> businessReport(String bizMonth) {
List<CanteenReportSwipeEntity> swipes = loadSwipes(bizMonth);
List<CanteenReportShareRowVO> shares = supplierShareReport(bizMonth);
List<CanteenReportFoodEnergyEntity> foods = loadFoods(bizMonth);
Map<String, Map<String, BigDecimal>> incomeByTypePark = new HashMap<>();
for (String t : CanteenReportConstants.INCOME_TYPES) {
@@ -236,22 +348,45 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService {
incomeSub = sumRows("收入(万元)", "小计", rows);
rows.add(incomeSub);
// 食材支出 = 分摊表各厂区成本中心分摊之和 / 10000
Map<String, BigDecimal> foodExpenseYuan = emptyParkMap();
// 分摊合计(元)含食材+能耗+外协;经营报表拆开展示时,食材行 = 分摊 − 能耗 − 外协,避免重复
Map<String, BigDecimal> shareExpenseYuan = emptyParkMap();
for (CanteenReportShareRowVO share : shares) {
if (isShareMetaRow(share.getCostCenter())) {
continue;
}
for (Map.Entry<String, BigDecimal> e : share.getParkAmounts().entrySet()) {
foodExpenseYuan.merge(e.getKey(), nz(e.getValue()), BigDecimal::add);
shareExpenseYuan.merge(e.getKey(), nz(e.getValue()), BigDecimal::add);
}
}
CanteenReportBusinessRowVO foodRow = toWanRow("支出(万元)", "食材", foodExpenseYuan);
rows.add(foodRow);
rows.add(toWanRow("支出(万元)", "能耗", emptyParkMap()));
rows.add(toWanRow("支出(万元)", "人工", emptyParkMap()));
rows.add(toWanRow("支出(万元)", "外协", emptyParkMap()));
CanteenReportBusinessRowVO expenseSub = cloneAs("支出(万元)", "小计", foodRow);
Map<String, BigDecimal> energyYuan = emptyParkMap();
Map<String, BigDecimal> outsourceYuan = emptyParkMap();
Map<String, BigDecimal> laborYuan = emptyParkMap();
Map<String, BigDecimal> splitFromShareYuan = emptyParkMap();
for (CanteenReportFoodEnergyEntity f : foods) {
String park = StrUtil.blankToDefault(f.getPark(), "");
String classify = StrUtil.blankToDefault(f.getClassify(), "食材").trim();
BigDecimal settle = nz(f.getSettleAmount());
laborYuan.merge(park, nz(f.getLaborAmount()), BigDecimal::add);
if ("能耗".equals(classify)) {
energyYuan.merge(park, settle, BigDecimal::add);
splitFromShareYuan.merge(park, settle, BigDecimal::add);
}
else if ("外协".equals(classify)) {
outsourceYuan.merge(park, settle, BigDecimal::add);
splitFromShareYuan.merge(park, settle, BigDecimal::add);
}
}
Map<String, BigDecimal> foodOnlyYuan = emptyParkMap();
for (String park : CanteenReportConstants.PARKS) {
foodOnlyYuan.put(park, nz(shareExpenseYuan.get(park)).subtract(nz(splitFromShareYuan.get(park))));
}
int expenseFrom = rows.size();
rows.add(toWanRow("支出(万元)", "食材", foodOnlyYuan));
rows.add(toWanRow("支出(万元)", "能耗", energyYuan));
rows.add(toWanRow("支出(万元)", "人工", laborYuan));
rows.add(toWanRow("支出(万元)", "外协", outsourceYuan));
CanteenReportBusinessRowVO expenseSub = sumRows("支出(万元)", "小计", rows.subList(expenseFrom, rows.size()));
rows.add(expenseSub);
// 毛利
@@ -293,6 +428,9 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService {
map.put("swipeAmount", swipeSum);
map.put("foodCount", foods.size());
map.put("foodAmount", foodSum);
CanteenReportShareAuditVO audit = supplierShareAudit(bizMonth);
map.put("shareParkChecks", audit.getParkChecks());
map.put("shareWeightRows", audit.getWeightRows());
return map;
}
@@ -6,13 +6,16 @@ import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.admin.api.feign.RemoteFileService;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportAggResultVO;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportFaceAggRow;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportFaceExcel;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportRosterMissVO;
import com.bsd.pjcloud.canteen.entity.CanteenReportFaceDetailEntity;
import com.bsd.pjcloud.canteen.entity.CanteenReportImportLogEntity;
import com.bsd.pjcloud.canteen.entity.CanteenReportSettleParkEntity;
@@ -21,8 +24,10 @@ import com.bsd.pjcloud.canteen.mapper.CanteenReportFaceDetailMapper;
import com.bsd.pjcloud.canteen.mapper.CanteenReportImportLogMapper;
import com.bsd.pjcloud.canteen.mapper.CanteenReportSettleParkMapper;
import com.bsd.pjcloud.canteen.mapper.CanteenReportSwipeMapper;
import com.bsd.pjcloud.canteen.report.CanteenReportConstants;
import com.bsd.pjcloud.canteen.report.CanteenReportFaceMapper;
import com.bsd.pjcloud.canteen.service.CanteenReportFaceImportService;
import com.bsd.pjcloud.canteen.service.CanteenReportRosterService;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.data.tenant.TenantContextHolder;
@@ -42,6 +47,7 @@ import java.nio.file.Files;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -59,6 +65,7 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
private final CanteenReportFaceDetailMapper faceDetailMapper;
private final CanteenReportSwipeMapper swipeMapper;
private final CanteenReportSettleParkMapper settleParkMapper;
private final CanteenReportRosterService rosterService;
private final RemoteFileService remoteFileService;
@Override
@@ -71,15 +78,56 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
@Override
public Page<CanteenReportFaceDetailEntity> pageDetails(Page page, String bizMonth, Long importLogId, String empNo,
String payMethod) {
String payMethod, String deviceName, String empName, String phone, List<String> orgLevel2s) {
List<String> orgs = normalizeOrgLevel2s(orgLevel2s);
return faceDetailMapper.selectPage(page, Wrappers.<CanteenReportFaceDetailEntity>lambdaQuery()
.eq(StrUtil.isNotBlank(bizMonth), CanteenReportFaceDetailEntity::getBizMonth, bizMonth)
.eq(importLogId != null, CanteenReportFaceDetailEntity::getImportLogId, importLogId)
.like(StrUtil.isNotBlank(empNo), CanteenReportFaceDetailEntity::getEmpNo, empNo)
.like(StrUtil.isNotBlank(payMethod), CanteenReportFaceDetailEntity::getPayMethod, payMethod)
.like(StrUtil.isNotBlank(deviceName), CanteenReportFaceDetailEntity::getDeviceName, deviceName)
.like(StrUtil.isNotBlank(empName), CanteenReportFaceDetailEntity::getEmpName, empName)
.like(StrUtil.isNotBlank(phone), CanteenReportFaceDetailEntity::getPhone, phone)
.in(CollUtil.isNotEmpty(orgs), CanteenReportFaceDetailEntity::getOrgLevel2, orgs)
.orderByDesc(CanteenReportFaceDetailEntity::getId));
}
@Override
public Map<String, Object> sumDetails(String bizMonth, Long importLogId, String empNo, String payMethod,
String deviceName, String empName, String phone, List<String> orgLevel2s) {
List<String> orgs = normalizeOrgLevel2s(orgLevel2s);
QueryWrapper<CanteenReportFaceDetailEntity> qw = Wrappers.query();
qw.select("IFNULL(SUM(order_amount),0) AS totalAmount", "COUNT(1) AS totalCount");
qw.eq(StrUtil.isNotBlank(bizMonth), "biz_month", bizMonth);
qw.eq(importLogId != null, "import_log_id", importLogId);
qw.like(StrUtil.isNotBlank(empNo), "emp_no", empNo);
qw.like(StrUtil.isNotBlank(payMethod), "pay_method", payMethod);
qw.like(StrUtil.isNotBlank(deviceName), "device_name", deviceName);
qw.like(StrUtil.isNotBlank(empName), "emp_name", empName);
qw.like(StrUtil.isNotBlank(phone), "phone", phone);
qw.in(CollUtil.isNotEmpty(orgs), "org_level2", orgs);
List<Map<String, Object>> list = faceDetailMapper.selectMaps(qw);
if (CollUtil.isEmpty(list) || list.get(0) == null) {
Map<String, Object> empty = new HashMap<>();
empty.put("totalAmount", BigDecimal.ZERO);
empty.put("totalCount", 0);
return empty;
}
Map<String, Object> raw = list.get(0);
Map<String, Object> out = new HashMap<>();
Object amt = raw.get("totalAmount");
if (amt == null) {
amt = raw.get("TOTALAMOUNT");
}
Object cnt = raw.get("totalCount");
if (cnt == null) {
cnt = raw.get("TOTALCOUNT");
}
out.put("totalAmount", amt != null ? amt : BigDecimal.ZERO);
out.put("totalCount", cnt != null ? cnt : 0);
return out;
}
@Override
public R importFaceExcel(MultipartFile file, String bizMonth, String dateStart, String dateEnd,
boolean replaceRange) {
@@ -91,6 +139,7 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
return R.failed("仅支持 .xlsx / .xls");
}
long fileBytes = file.getSize();
long t0 = System.currentTimeMillis();
String operator = currentUser();
Long tenantId = TenantContextHolder.getTenantId();
@@ -118,81 +167,122 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
logEntity.setTenantId(tenantId);
save(logEntity);
File temp = null;
File temp;
try {
temp = Files.createTempFile("canteen-face-", ".xlsx").toFile();
file.transferTo(temp);
archiveExcel(temp, originalName, logEntity);
boolean preReplace = replaceRange && StrUtil.isNotBlank(bizMonth) && StrUtil.isNotBlank(dateStart)
&& StrUtil.isNotBlank(dateEnd);
if (preReplace) {
faceDetailMapper.deletePhysicallyByDateRange(bizMonth.trim(), dateStart.trim(), dateEnd.trim());
}
FaceDetailAccumulator acc = new FaceDetailAccumulator(logEntity.getId(), bizMonth, operator, tenantId,
now, faceDetailMapper, BATCH_SIZE);
try (InputStream in = new BufferedInputStream(new FileInputStream(temp), 1 << 20)) {
EasyExcel.read(in, CanteenReportFaceExcel.class, acc)
.sheet()
.headRowNumber(1)
.doRead();
}
acc.flush();
String resolvedMonth = StrUtil.blankToDefault(StrUtil.trim(bizMonth), acc.getResolvedBizMonth());
String resolvedStart = StrUtil.blankToDefault(StrUtil.trim(dateStart), acc.getMinDate());
String resolvedEnd = StrUtil.blankToDefault(StrUtil.trim(dateEnd), acc.getMaxDate());
if (StrUtil.isBlank(resolvedMonth) || StrUtil.isBlank(resolvedStart) || StrUtil.isBlank(resolvedEnd)) {
faceDetailMapper.deletePhysicallyByImportLogId(logEntity.getId());
failLog(logEntity, "无法确定业务月或日期段,请填写业务月与数据起止日期", t0);
return R.failed(logEntity.getErrorMsg());
}
if (replaceRange && !preReplace) {
faceDetailMapper.deletePhysicallyDateRangeExceptLog(resolvedMonth, resolvedStart, resolvedEnd,
logEntity.getId());
}
logEntity.setBizMonth(resolvedMonth);
logEntity.setDateStart(resolvedStart);
logEntity.setDateEnd(resolvedEnd);
logEntity.setTotalRows(acc.getTotalRows());
logEntity.setSuccessRows(acc.getSuccessRows());
logEntity.setSkipRows(acc.getSkipRows());
logEntity.setStatus("SUCCESS");
logEntity.setAggStatus("NONE");
logEntity.setCostMs(System.currentTimeMillis() - t0);
logEntity.setUpdateTime(LocalDateTime.now());
updateById(logEntity);
return R.ok(logEntity, String.format(
"明细导入成功:入库 %d 条,跳过 %d 条,日期 %s~%s,耗时 %d 秒。请点击「统计汇总到刷卡明细」生成报表数据。",
acc.getSuccessRows(), acc.getSkipRows(), resolvedStart, resolvedEnd,
(logEntity.getCostMs() + 999) / 1000));
}
catch (Exception e) {
log.error("人脸明细导入失败 logId={}", logEntity.getId(), e);
failLog(logEntity, "接收上传文件失败: " + e.getMessage(), t0);
return R.failed("接收上传文件失败: " + e.getMessage());
}
final File tempFile = temp;
final String month = bizMonth;
final String ds = dateStart;
final String de = dateEnd;
final boolean replace = replaceRange;
final String fileName = originalName;
Thread worker = new Thread(() -> {
TenantContextHolder.setTenantId(tenantId);
try {
faceDetailMapper.deletePhysicallyByImportLogId(logEntity.getId());
doFaceImportAsync(tempFile, fileName, logEntity, month, ds, de, replace, operator, tenantId, t0);
}
catch (Exception ignored) {
// ignore
catch (Throwable e) {
log.error("人脸明细异步导入失败 logId={}", logEntity.getId(), e);
try {
faceDetailMapper.deletePhysicallyByImportLogId(logEntity.getId());
}
catch (Exception ignored) {
// ignore
}
failLog(logEntity,
StrUtil.maxLength(StrUtil.blankToDefault(e.getMessage(), e.getClass().getSimpleName()), 900), t0);
}
failLog(logEntity, StrUtil.maxLength(StrUtil.blankToDefault(e.getMessage(), e.getClass().getSimpleName()), 900),
t0);
return R.failed("导入失败: " + e.getMessage());
finally {
TenantContextHolder.clear();
if (tempFile.exists()) {
FileUtil.del(tempFile);
}
}
}, "canteen-face-import-" + logEntity.getId());
worker.setDaemon(false);
worker.start();
String eta = estimateImportMinutes(fileBytes);
return R.ok(logEntity, "已提交导入,后台正在处理。请在下方导入列表查看状态(导入中→成功/失败)。" + eta
+ "完成后请再点「统计汇总到刷卡明细」。");
}
/** 按文件大小粗估耗时(经验值,供前端提示) */
private static String estimateImportMinutes(long fileBytes) {
double mb = fileBytes / (1024.0 * 1024.0);
if (mb < 5) {
return "预计约 1 分钟内完成。";
}
finally {
if (temp != null && temp.exists()) {
FileUtil.del(temp);
}
int low = Math.max(2, (int) Math.ceil(mb / 20));
int high = Math.max(low + 1, (int) Math.ceil(mb / 12));
return String.format("当前文件约 %.0f MB,预计约 %d~%d 分钟。", mb, low, high);
}
private void doFaceImportAsync(File temp, String originalName, CanteenReportImportLogEntity logEntity,
String bizMonth, String dateStart, String dateEnd, boolean replaceRange, String operator, Long tenantId,
long t0) throws Exception {
archiveExcel(temp, originalName, logEntity);
boolean preReplace = replaceRange && StrUtil.isNotBlank(bizMonth) && StrUtil.isNotBlank(dateStart)
&& StrUtil.isNotBlank(dateEnd);
if (preReplace) {
faceDetailMapper.deletePhysicallyByDateRange(bizMonth.trim(), dateStart.trim(), dateEnd.trim());
}
FaceDetailAccumulator acc = new FaceDetailAccumulator(logEntity.getId(), bizMonth, operator, tenantId,
LocalDateTime.now(), faceDetailMapper, BATCH_SIZE);
try (InputStream in = new BufferedInputStream(new FileInputStream(temp), 1 << 20)) {
EasyExcel.read(in, CanteenReportFaceExcel.class, acc)
.sheet()
.headRowNumber(1)
.doRead();
}
acc.flush();
String resolvedMonth = StrUtil.blankToDefault(StrUtil.trim(bizMonth), acc.getResolvedBizMonth());
String resolvedStart = StrUtil.blankToDefault(StrUtil.trim(dateStart), acc.getMinDate());
String resolvedEnd = StrUtil.blankToDefault(StrUtil.trim(dateEnd), acc.getMaxDate());
if (StrUtil.isBlank(resolvedMonth)) {
faceDetailMapper.deletePhysicallyByImportLogId(logEntity.getId());
failLog(logEntity, "无法确定报表归属月,请填写报表归属月", t0);
return;
}
if (StrUtil.isBlank(resolvedStart) || StrUtil.isBlank(resolvedEnd)) {
resolvedStart = resolvedMonth + "-01";
resolvedEnd = lastDayOfMonth(resolvedMonth);
}
if (replaceRange && !preReplace) {
faceDetailMapper.deletePhysicallyDateRangeExceptLog(resolvedMonth, resolvedStart, resolvedEnd,
logEntity.getId());
}
logEntity.setBizMonth(resolvedMonth);
logEntity.setDateStart(resolvedStart);
logEntity.setDateEnd(resolvedEnd);
logEntity.setTotalRows(acc.getTotalRows());
logEntity.setSuccessRows(acc.getSuccessRows());
logEntity.setSkipRows(acc.getSkipRows());
logEntity.setSkipSummary(acc.buildSkipSummary());
logEntity.setStatus("SUCCESS");
logEntity.setAggStatus("NONE");
logEntity.setCostMs(System.currentTimeMillis() - t0);
logEntity.setUpdateTime(LocalDateTime.now());
updateById(logEntity);
log.info("人脸明细异步导入完成 logId={} success={} skip={} costMs={}", logEntity.getId(),
acc.getSuccessRows(), acc.getSkipRows(), logEntity.getCostMs());
}
@Override
public R aggregateToSwipe(String bizMonth, String dateStart, String dateEnd, Long importLogId,
boolean replaceFace) {
boolean replaceFace, List<String> orgLevel2s) {
if (StrUtil.isBlank(bizMonth) && importLogId == null) {
return R.failed("请传入业务月份 bizMonth,或指定某一批导入日志");
}
@@ -205,6 +295,7 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
// 指定导入批次时:只用 importLogId,日期段不参与过滤(避免日志日期与明细 biz_date 不一致)
String useDateStart = importLogId != null ? null : StrUtil.trim(dateStart);
String useDateEnd = importLogId != null ? null : StrUtil.trim(dateEnd);
List<String> orgs = normalizeOrgLevel2s(orgLevel2s);
if (importLogId != null) {
CanteenReportImportLogEntity logEntity = getById(importLogId);
@@ -214,17 +305,23 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
if (StrUtil.isBlank(month)) {
month = logEntity.getBizMonth();
}
long cnt = faceDetailMapper.countDetail(null, importLogId);
long cnt = faceDetailMapper.countDetail(null, importLogId, null);
if (cnt <= 0) {
return R.failed(String.format(
"该批导入日志下没有明细(importLogId=%s)。请打开「明细」确认能否查到数据;若明细也是空的,需要重新导入 Excel",
importLogId));
}
if (CollUtil.isNotEmpty(orgs)) {
long filtered = faceDetailMapper.countDetail(null, importLogId, orgs);
if (filtered <= 0) {
return R.failed("所选二级组织下没有明细,请改选「嘉兴地区 / 湖州地区」等后再汇总");
}
}
}
else {
long cnt = faceDetailMapper.countDetail(month, null);
long cnt = faceDetailMapper.countDetail(month, null, null);
if (cnt <= 0) {
long any = faceDetailMapper.countDetail(null, null);
long any = faceDetailMapper.countDetail(null, null, null);
if (any <= 0) {
return R.failed("库中没有任何人脸明细,请先成功导入 Excel(导入日志里「明细入库」应大于 0)");
}
@@ -232,12 +329,18 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
"归属月 %s 下没有明细(库中其它月份共有 %d 条)。请核对导入日志的「业务月」,或改选正确归属月再汇总",
month, any));
}
if (CollUtil.isNotEmpty(orgs)) {
long filtered = faceDetailMapper.countDetail(month, null, orgs);
if (filtered <= 0) {
return R.failed("所选二级组织下没有明细,请核对筛选后再汇总");
}
}
}
List<CanteenReportFaceAggRow> aggRows = faceDetailMapper.selectAggByMonth(month, useDateStart, useDateEnd,
importLogId);
importLogId, orgs);
if (CollUtil.isEmpty(aggRows)) {
return R.failed("明细存在但汇总结果为空,请检查订单金额是否都为 0");
return R.failed("明细存在但汇总结果为空,请检查订单金额是否都为 0,或所选二级组织是否有数据");
}
if (StrUtil.isBlank(month) && importLogId != null) {
@@ -251,8 +354,11 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
}
Map<String, String> settleParkMap = loadSettleParkMap();
Map<String, String> hrScopeMap = rosterService.loadHrScopeMap(month);
List<CanteenReportSwipeEntity> saveList = new ArrayList<>(aggRows.size());
Map<String, CanteenReportRosterMissVO> missByKey = new LinkedHashMap<>();
int unmatchedPark = 0;
int unmatchedRoster = 0;
for (CanteenReportFaceAggRow row : aggRows) {
if (row.getAmount() == null || row.getAmount().compareTo(BigDecimal.ZERO) == 0) {
continue;
@@ -269,7 +375,44 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
e.setSettleCounter(emptyToNull(row.getDeviceName()));
e.setConsumeAmount(row.getAmount());
e.setIncomeType(row.getIncomeType());
e.setCostCenter(CanteenReportFaceMapper.mapCostCenter(row.getLegalEntity()));
String empNo = StrUtil.trim(row.getEmpNo());
String costCenter = null;
String missReason = null;
if (StrUtil.isBlank(empNo)) {
missReason = "无工号";
}
else if (!hrScopeMap.containsKey(empNo)) {
missReason = "花名册无此工号";
}
else if (StrUtil.isBlank(hrScopeMap.get(empNo))) {
missReason = "花名册无人事范围";
}
else {
costCenter = hrScopeMap.get(empNo);
}
if (missReason != null) {
unmatchedRoster++;
costCenter = CanteenReportFaceMapper.mapCostCenter(row.getLegalEntity());
e.setRemarkTag(CanteenReportConstants.REMARK_ROSTER_MISS);
String key = StrUtil.isNotBlank(empNo) ? empNo
: ("名:" + StrUtil.blankToDefault(row.getEmpName(), "") + "|法:"
+ StrUtil.blankToDefault(row.getLegalEntity(), ""));
CanteenReportRosterMissVO miss = missByKey.get(key);
if (miss == null) {
miss = new CanteenReportRosterMissVO();
miss.setEmpNo(emptyToNull(empNo));
miss.setEmpName(emptyToNull(row.getEmpName()));
miss.setLegalEntity(emptyToNull(row.getLegalEntity()));
miss.setCostCenter(costCenter);
miss.setReason(missReason);
miss.setRowCount(1);
missByKey.put(key, miss);
}
else {
miss.setRowCount(miss.getRowCount() + 1);
}
}
e.setCostCenter(costCenter);
String park = settleParkMap.get(StrUtil.trim(row.getDeviceName()));
if (StrUtil.isBlank(park) && StrUtil.isNotBlank(row.getDeviceName())) {
unmatchedPark++;
@@ -323,10 +466,52 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
}
long cost = System.currentTimeMillis() - t0;
String tip = unmatchedPark > 0
? String.format(";有 %d 条结算台未配置厂区,请到「结算台厂区」维护后再重新汇总", unmatchedPark)
: "";
return R.ok(null, String.format("汇总成功:写入刷卡明细 %d 条,耗时 %d 秒%s", saveList.size(), (cost + 999) / 1000, tip));
StringBuilder tip = new StringBuilder();
if (hrScopeMap.isEmpty()) {
tip.append(";当月花名册为空,核对成本已回退法律归属映射,请先导入花名册");
}
else if (unmatchedRoster > 0) {
tip.append(String.format(";有 %d 条、%d 人未命中花名册,已回退法律归属,刷卡明细备注「未命中花名册」可筛选",
unmatchedRoster, missByKey.size()));
}
if (unmatchedPark > 0) {
tip.append(String.format(";有 %d 条结算台未配置厂区,请到「结算台厂区」维护后再重新汇总", unmatchedPark));
}
if (CollUtil.isNotEmpty(orgs)) {
tip.append(";已按二级组织筛选:").append(String.join("、", orgs));
}
CanteenReportAggResultVO result = new CanteenReportAggResultVO();
result.setSwipeRows(saveList.size());
result.setUnmatchedRows(unmatchedRoster);
result.setUnmatchedPeople(missByKey.size());
result.setUnmatched(new ArrayList<>(missByKey.values()));
return R.ok(result, String.format("汇总成功:写入刷卡明细 %d 条,耗时 %d 秒%s",
saveList.size(), (cost + 999) / 1000, tip));
}
@Override
public List<com.bsd.pjcloud.canteen.dto.report.CanteenReportOrgLevel2VO> listOrgLevel2(String bizMonth,
Long importLogId) {
return faceDetailMapper.listOrgLevel2(StrUtil.trim(bizMonth), importLogId);
}
private static List<String> normalizeOrgLevel2s(List<String> orgLevel2s) {
if (CollUtil.isEmpty(orgLevel2s)) {
return List.of();
}
List<String> list = new ArrayList<>();
for (String s : orgLevel2s) {
if (StrUtil.isBlank(s)) {
continue;
}
for (String part : s.split(",")) {
String t = StrUtil.trim(part);
if (StrUtil.isNotBlank(t) && !list.contains(t)) {
list.add(t);
}
}
}
return list;
}
private Map<String, String> loadSettleParkMap() {
@@ -376,6 +561,16 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
updateById(logEntity);
}
private static String lastDayOfMonth(String yyyyMm) {
try {
java.time.YearMonth ym = java.time.YearMonth.parse(yyyyMm);
return ym.atEndOfMonth().toString();
}
catch (Exception e) {
return yyyyMm + "-28";
}
}
private static String currentUser() {
try {
if (SecurityUtils.getUser() != null) {
@@ -390,6 +585,8 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
static final class FaceDetailAccumulator implements ReadListener<CanteenReportFaceExcel> {
private static final int SKIP_SAMPLE_LIMIT = 80;
private final Long importLogId;
private final String bizMonthParam;
private final String operator;
@@ -399,6 +596,8 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
private final int batchSize;
private final List<CanteenReportFaceDetailEntity> buffer = new ArrayList<>();
private final Map<String, Integer> skipReasonCount = new LinkedHashMap<>();
private final List<String> skipSamples = new ArrayList<>();
private int totalRows;
private int successRows;
private int skipRows;
@@ -420,12 +619,20 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
@Override
public void invoke(CanteenReportFaceExcel data, AnalysisContext context) {
totalRows++;
int excelRow = context.readRowHolder() != null ? context.readRowHolder().getRowIndex() + 1 : totalRows + 1;
CanteenReportFaceDetailEntity e = CanteenReportFaceMapper.toDetail(data, bizMonthParam, importLogId);
if (e == null || StrUtil.isBlank(e.getBizDate()) || StrUtil.isBlank(e.getBizMonth())) {
skipRows++;
if (e == null) {
recordSkip(excelRow, "空行(无订单号/姓名/人员编号/金额/时间)", data);
return;
}
trackDate(e.getBizDate());
if (StrUtil.isBlank(e.getBizMonth())) {
recordSkip(excelRow, "无法确定报表归属月(请填写归属月,或保证时间列可解析)", data);
return;
}
// 有归属月即可入库;无业务日仍保留(汇总按月不依赖单日)
if (StrUtil.isNotBlank(e.getBizDate())) {
trackDate(e.getBizDate());
}
if (resolvedBizMonth == null) {
resolvedBizMonth = e.getBizMonth();
}
@@ -442,6 +649,63 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
}
}
private void recordSkip(int excelRow, String reason, CanteenReportFaceExcel data) {
skipRows++;
skipReasonCount.merge(reason, 1, Integer::sum);
if (skipSamples.size() < SKIP_SAMPLE_LIMIT) {
String hint = briefRow(data);
skipSamples.add("Excel第" + excelRow + "行:" + reason + (StrUtil.isNotBlank(hint) ? ";" + hint : ""));
}
}
private static String briefRow(CanteenReportFaceExcel data) {
if (data == null) {
return "";
}
List<String> parts = new ArrayList<>();
if (StrUtil.isNotBlank(data.getOrderNo())) {
parts.add("订单号=" + data.getOrderNo());
}
if (StrUtil.isNotBlank(data.getEmpNo())) {
parts.add("人员编号=" + data.getEmpNo());
}
if (StrUtil.isNotBlank(data.getEmpName())) {
parts.add("姓名=" + data.getEmpName());
}
if (StrUtil.isNotBlank(data.getDeductTime())) {
parts.add("扣款时间=" + data.getDeductTime());
}
else if (StrUtil.isNotBlank(data.getPayTime())) {
parts.add("支付时间=" + data.getPayTime());
}
else if (StrUtil.isNotBlank(data.getCreateTime())) {
parts.add("创建时间=" + data.getCreateTime());
}
return String.join(",", parts);
}
String buildSkipSummary() {
if (skipRows <= 0) {
return null;
}
StringBuilder sb = new StringBuilder();
sb.append("共跳过 ").append(skipRows).append(" 行。\n");
sb.append("【分类统计】\n");
for (Map.Entry<String, Integer> e : skipReasonCount.entrySet()) {
sb.append("- ").append(e.getKey()).append(":").append(e.getValue()).append(" 行\n");
}
if (CollUtil.isNotEmpty(skipSamples)) {
sb.append("【样例(最多").append(SKIP_SAMPLE_LIMIT).append("条)】\n");
for (String s : skipSamples) {
sb.append(s).append("\n");
}
if (skipRows > skipSamples.size()) {
sb.append("…其余 ").append(skipRows - skipSamples.size()).append(" 行略\n");
}
}
return sb.toString();
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
flush();
@@ -2,12 +2,16 @@ package com.bsd.pjcloud.canteen.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportFoodEnergyExcel;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportReceiptStockExcel;
import com.bsd.pjcloud.canteen.entity.CanteenReportFoodEnergyEntity;
import com.bsd.pjcloud.canteen.excel.ExcelYuanConverter;
import com.bsd.pjcloud.canteen.excel.ExcelYuanStringConverter;
import com.bsd.pjcloud.canteen.mapper.CanteenReportFoodEnergyMapper;
import com.bsd.pjcloud.canteen.service.CanteenReportFoodEnergyService;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
@@ -20,10 +24,12 @@ import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -45,6 +51,41 @@ public class CanteenReportFoodEnergyServiceImpl
.orderByDesc(CanteenReportFoodEnergyEntity::getId));
}
@Override
@Transactional(rollbackFor = Exception.class)
public R saveOrUpdateRow(CanteenReportFoodEnergyEntity entity) {
if (entity == null) {
return R.failed("参数为空");
}
if (StrUtil.isBlank(entity.getBizMonth())) {
return R.failed("请填写业务月");
}
if (StrUtil.isBlank(entity.getSupplier())) {
return R.failed("请填写供应商(或费用归属说明,如「嘉兴前台提供」)");
}
if (StrUtil.isBlank(entity.getClassify())) {
entity.setClassify(mapClassify(entity.getCategory(), entity.getProductName()));
}
fillCalculatedAmounts(entity);
applyClassifyAmounts(entity);
if (entity.getId() == null) {
fillInsertMeta(Collections.singletonList(entity));
baseMapper.insert(entity);
return R.ok(entity, "新增成功");
}
entity.setUpdateTime(LocalDateTime.now());
try {
if (SecurityUtils.getUser() != null) {
entity.setUpdateBy(SecurityUtils.getUser().getUsername());
}
}
catch (Exception ignored) {
// ignore
}
updateById(entity);
return R.ok(entity, "保存成功");
}
@Override
@Transactional(rollbackFor = Exception.class)
public R importExcel(List<CanteenReportFoodEnergyExcel> rows, String bizMonth, boolean replaceMonth,
@@ -79,19 +120,11 @@ public class CanteenReportFoodEnergyServiceImpl
CanteenReportFoodEnergyEntity e = new CanteenReportFoodEnergyEntity();
BeanUtils.copyProperties(row, e);
e.setBizMonth(bizMonth);
if (StrUtil.isBlank(e.getClassify())) {
e.setClassify(mapClassify(e.getCategory(), e.getProductName()));
}
fillCalculatedAmounts(e);
if (e.getEnergyAmount() == null) {
e.setEnergyAmount(BigDecimal.ZERO);
}
if (e.getLaborAmount() == null) {
e.setLaborAmount(BigDecimal.ZERO);
}
if (e.getConsumableAmount() == null) {
e.setConsumableAmount(BigDecimal.ZERO);
}
if (e.getOutsourceAmount() == null) {
e.setOutsourceAmount(BigDecimal.ZERO);
}
applyClassifyAmounts(e);
saveList.add(e);
}
if (CollUtil.isNotEmpty(errorMessageList)) {
@@ -109,6 +142,114 @@ public class CanteenReportFoodEnergyServiceImpl
return R.ok(null, "导入成功,共 " + saveList.size() + " 条");
}
@Override
@Transactional(rollbackFor = Exception.class)
public R importReceiptStock(MultipartFile file, String bizMonth, boolean replaceMonth) {
if (file == null || file.isEmpty()) {
return R.failed("请上传收发存 Excel");
}
if (StrUtil.isBlank(bizMonth)) {
return R.failed("请传入业务月份 bizMonth,格式 yyyy-MM");
}
try {
List<CanteenReportReceiptStockExcel> rows = EasyExcel.read(file.getInputStream())
.head(CanteenReportReceiptStockExcel.class)
.registerConverter(new ExcelYuanConverter())
.registerConverter(new ExcelYuanStringConverter())
.sheet()
.headRowNumber(1)
.doReadSync();
if (CollUtil.isEmpty(rows)) {
return R.failed("导入数据为空");
}
List<CanteenReportFoodEnergyEntity> saveList = new ArrayList<>(Math.min(rows.size(), 32768));
int skip = 0;
for (CanteenReportReceiptStockExcel row : rows) {
if (StrUtil.isBlank(row.getSupplier()) && row.getSettleAmount() == null && row.getAmount() == null) {
skip++;
continue;
}
if (StrUtil.isBlank(row.getSupplier())) {
skip++;
continue;
}
CanteenReportFoodEnergyEntity e = new CanteenReportFoodEnergyEntity();
e.setBizMonth(bizMonth.trim());
e.setBizDate(trimToNull(row.getBizDate()));
e.setSupplier(row.getSupplier().trim());
e.setReceiptNo(trimToNull(row.getReceiptNo()));
e.setCategory(trimToNull(row.getCategory()));
e.setProductCode(trimToNull(row.getProductCode()));
e.setProductName(trimToNull(row.getProductName()));
e.setSpec(trimToNull(row.getSpec()));
e.setUnit(trimToNull(row.getUnit()));
e.setQty(row.getQty());
e.setUnitPrice(row.getUnitPrice());
e.setListPrice(row.getListPrice());
e.setPriceDiff(row.getPriceDiff());
e.setSettleUnitPrice(row.getSettleUnitPrice());
e.setSettleAmount(row.getSettleAmount());
e.setAmount(row.getAmount());
e.setAmountDiff(row.getAmountDiff());
e.setWarehouse(trimToNull(row.getWarehouse()));
e.setPark(trimToNull(row.getPark()));
e.setClassify(mapClassify(row.getCategory(), row.getProductName()));
fillCalculatedAmounts(e);
applyClassifyAmounts(e);
saveList.add(e);
}
if (CollUtil.isEmpty(saveList)) {
return R.failed("无有效数据可入库(需有供应商)");
}
if (replaceMonth) {
baseMapper.deletePhysicallyByBizMonth(bizMonth.trim());
}
fillInsertMeta(saveList);
for (List<CanteenReportFoodEnergyEntity> chunk : CollUtil.split(saveList, BATCH_SIZE)) {
baseMapper.insertBatch(chunk);
}
return R.ok(null, String.format("收发存导入成功:入库 %d 条,跳过 %d 条", saveList.size(), skip));
}
catch (Exception e) {
if (e instanceof com.alibaba.excel.exception.ExcelDataConvertException) {
return R.failed("Excel 金额/数量列含非数字,请检查列是否错位: " + e.getMessage());
}
return R.failed("导入失败: " + e.getMessage());
}
}
/** 类别/品名含能耗、水电等 → 能耗;含外协 → 外协;含耗材 → 耗材;默认食材 */
private static String mapClassify(String category, String productName) {
String t = (StrUtil.blankToDefault(category, "") + " " + StrUtil.blankToDefault(productName, "")).trim();
if (StrUtil.containsAny(t, "能耗", "水电", "电费", "水费", "燃气", "能源")) {
return "能耗";
}
if (StrUtil.contains(t, "外协")) {
return "外协";
}
if (StrUtil.contains(t, "耗材")) {
return "耗材";
}
return "食材";
}
/** 按分类把结算金额落到能耗/外协/耗材列 */
private static void applyClassifyAmounts(CanteenReportFoodEnergyEntity e) {
String c = StrUtil.blankToDefault(e.getClassify(), "食材").trim();
e.setClassify(c);
BigDecimal settle = nz(e.getSettleAmount());
e.setEnergyAmount("能耗".equals(c) ? settle : BigDecimal.ZERO);
e.setOutsourceAmount("外协".equals(c) ? settle : BigDecimal.ZERO);
e.setConsumableAmount("耗材".equals(c) ? settle : BigDecimal.ZERO);
if (e.getLaborAmount() == null) {
e.setLaborAmount(BigDecimal.ZERO);
}
}
private static String trimToNull(String s) {
return StrUtil.isBlank(s) ? null : s.trim();
}
private void fillInsertMeta(List<CanteenReportFoodEnergyEntity> list) {
LocalDateTime now = LocalDateTime.now();
String user = null;
@@ -150,7 +291,14 @@ public class CanteenReportFoodEnergyServiceImpl
e.setSettleAmount(nz(e.getSettleUnitPrice()).multiply(qty).setScale(2, java.math.RoundingMode.HALF_UP));
}
if (e.getAmount() == null) {
e.setAmount(unitPrice.multiply(qty).setScale(2, java.math.RoundingMode.HALF_UP));
// 手工补外协/能耗等往往只填结算总金额,金额默认与之相同
if (qty.compareTo(BigDecimal.ZERO) == 0 && unitPrice.compareTo(BigDecimal.ZERO) == 0
&& e.getSettleAmount() != null) {
e.setAmount(e.getSettleAmount());
}
else {
e.setAmount(unitPrice.multiply(qty).setScale(2, java.math.RoundingMode.HALF_UP));
}
}
if (e.getAmountDiff() == null) {
e.setAmountDiff(nz(e.getAmount()).subtract(nz(e.getSettleAmount())));
@@ -0,0 +1,129 @@
package com.bsd.pjcloud.canteen.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportRosterExcel;
import com.bsd.pjcloud.canteen.entity.CanteenReportRosterEntity;
import com.bsd.pjcloud.canteen.mapper.CanteenReportRosterMapper;
import com.bsd.pjcloud.canteen.service.CanteenReportRosterService;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.data.tenant.TenantContextHolder;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class CanteenReportRosterServiceImpl extends ServiceImpl<CanteenReportRosterMapper, CanteenReportRosterEntity>
implements CanteenReportRosterService {
private static final int BATCH = 1000;
@Override
public Page<CanteenReportRosterEntity> pageList(Page page, String bizMonth, String empNo, String empName,
String hrScope) {
return page(page, Wrappers.<CanteenReportRosterEntity>lambdaQuery()
.eq(StrUtil.isNotBlank(bizMonth), CanteenReportRosterEntity::getBizMonth, bizMonth)
.like(StrUtil.isNotBlank(empNo), CanteenReportRosterEntity::getEmpNo, empNo)
.like(StrUtil.isNotBlank(empName), CanteenReportRosterEntity::getEmpName, empName)
.like(StrUtil.isNotBlank(hrScope), CanteenReportRosterEntity::getHrScope, hrScope)
.orderByAsc(CanteenReportRosterEntity::getEmpNo));
}
@Override
public R importExcel(MultipartFile file, String bizMonth, boolean replaceMonth) {
if (file == null || file.isEmpty()) {
return R.failed("请上传花名册 Excel");
}
if (StrUtil.isBlank(bizMonth)) {
return R.failed("请传入适用月份 bizMonth");
}
try {
List<CanteenReportRosterExcel> rows = EasyExcel.read(file.getInputStream())
.head(CanteenReportRosterExcel.class)
.sheet()
.headRowNumber(1)
.doReadSync();
if (CollUtil.isEmpty(rows)) {
return R.failed("导入数据为空");
}
String operator = currentUser();
Long tenantId = TenantContextHolder.getTenantId();
LocalDateTime now = LocalDateTime.now();
List<CanteenReportRosterEntity> saveList = new ArrayList<>(Math.min(rows.size(), 32768));
int skip = 0;
for (CanteenReportRosterExcel row : rows) {
if (StrUtil.isBlank(row.getEmpNo())) {
skip++;
continue;
}
CanteenReportRosterEntity e = new CanteenReportRosterEntity();
BeanUtils.copyProperties(row, e);
e.setId(IdWorker.getId());
e.setBizMonth(bizMonth.trim());
e.setEmpNo(row.getEmpNo().trim());
e.setCreateBy(operator);
e.setUpdateBy(operator);
e.setCreateTime(now);
e.setUpdateTime(now);
e.setDelFlag(CommonConstants.STATUS_NORMAL);
e.setTenantId(tenantId);
saveList.add(e);
}
if (replaceMonth) {
baseMapper.deletePhysicallyByBizMonth(bizMonth.trim());
}
for (List<CanteenReportRosterEntity> chunk : CollUtil.split(saveList, BATCH)) {
baseMapper.insertBatch(chunk);
}
return R.ok(null, String.format("花名册导入成功:入库 %d 条,跳过 %d 条", saveList.size(), skip));
}
catch (Exception e) {
return R.failed("导入失败: " + e.getMessage());
}
}
@Override
public Map<String, String> loadHrScopeMap(String bizMonth) {
Map<String, String> map = new HashMap<>();
List<CanteenReportRosterEntity> list = list(Wrappers.<CanteenReportRosterEntity>lambdaQuery()
.eq(StrUtil.isNotBlank(bizMonth), CanteenReportRosterEntity::getBizMonth, bizMonth)
.select(CanteenReportRosterEntity::getEmpNo, CanteenReportRosterEntity::getHrScope));
for (CanteenReportRosterEntity e : list) {
if (StrUtil.isBlank(e.getEmpNo())) {
continue;
}
String empNo = e.getEmpNo().trim();
String hr = StrUtil.trim(e.getHrScope());
// 空人事范围也占位,便于区分「不在花名册」和「在册但无人事范围」
map.putIfAbsent(empNo, hr == null ? "" : hr);
}
return map;
}
private static String currentUser() {
try {
if (SecurityUtils.getUser() != null) {
return SecurityUtils.getUser().getUsername();
}
}
catch (Exception ignored) {
// ignore
}
return null;
}
}
@@ -2,6 +2,7 @@ package com.bsd.pjcloud.canteen.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -25,8 +26,10 @@ import org.springframework.validation.BindingResult;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Service
@@ -39,7 +42,7 @@ public class CanteenReportSwipeServiceImpl extends ServiceImpl<CanteenReportSwip
@Override
public Page<CanteenReportSwipeEntity> pageList(Page page, String bizMonth, String park, String incomeType,
String costCenter, String empNo, Long importLogId) {
String costCenter, String empNo, Long importLogId, String remarkTag, String source) {
return page(page, Wrappers.<CanteenReportSwipeEntity>lambdaQuery()
.eq(StrUtil.isNotBlank(bizMonth), CanteenReportSwipeEntity::getBizMonth, bizMonth)
.eq(StrUtil.isNotBlank(park), CanteenReportSwipeEntity::getPark, park)
@@ -47,9 +50,44 @@ public class CanteenReportSwipeServiceImpl extends ServiceImpl<CanteenReportSwip
.like(StrUtil.isNotBlank(costCenter), CanteenReportSwipeEntity::getCostCenter, costCenter)
.like(StrUtil.isNotBlank(empNo), CanteenReportSwipeEntity::getEmpNo, empNo)
.eq(importLogId != null, CanteenReportSwipeEntity::getImportLogId, importLogId)
.eq(StrUtil.isNotBlank(remarkTag), CanteenReportSwipeEntity::getRemarkTag, remarkTag)
.eq(StrUtil.isNotBlank(source), CanteenReportSwipeEntity::getSource, source)
.orderByDesc(CanteenReportSwipeEntity::getId));
}
@Override
public Map<String, Object> sumAmount(String bizMonth, String park, String incomeType, String costCenter,
String empNo, Long importLogId, String remarkTag, String source) {
QueryWrapper<CanteenReportSwipeEntity> qw = Wrappers.query();
qw.select("IFNULL(SUM(consume_amount),0) AS totalAmount", "COUNT(1) AS totalCount");
applySumFilters(qw, bizMonth, park, incomeType, costCenter, empNo, importLogId, remarkTag, source);
Map<String, Object> map = getMap(qw);
if (map == null) {
map = new HashMap<>();
map.put("totalAmount", BigDecimal.ZERO);
map.put("totalCount", 0);
}
QueryWrapper<CanteenReportSwipeEntity> bySourceQw = Wrappers.query();
bySourceQw.select("IFNULL(source,'') AS source", "IFNULL(SUM(consume_amount),0) AS totalAmount",
"COUNT(1) AS totalCount");
applySumFilters(bySourceQw, bizMonth, park, incomeType, costCenter, empNo, importLogId, remarkTag, source);
bySourceQw.groupBy("source");
map.put("bySource", listMaps(bySourceQw));
return map;
}
private void applySumFilters(QueryWrapper<CanteenReportSwipeEntity> qw, String bizMonth, String park,
String incomeType, String costCenter, String empNo, Long importLogId, String remarkTag, String source) {
qw.eq(StrUtil.isNotBlank(bizMonth), "biz_month", bizMonth);
qw.eq(StrUtil.isNotBlank(park), "park", park);
qw.eq(StrUtil.isNotBlank(incomeType), "income_type", incomeType);
qw.like(StrUtil.isNotBlank(costCenter), "cost_center", costCenter);
qw.like(StrUtil.isNotBlank(empNo), "emp_no", empNo);
qw.eq(importLogId != null, "import_log_id", importLogId);
qw.eq(StrUtil.isNotBlank(remarkTag), "remark_tag", remarkTag);
qw.eq(StrUtil.isNotBlank(source), "source", source);
}
@Override
@Transactional(rollbackFor = Exception.class)
public R importExcel(List<CanteenReportSwipeExcel> rows, String bizMonth, boolean replaceMonth,
@@ -95,6 +133,7 @@ public class CanteenReportSwipeServiceImpl extends ServiceImpl<CanteenReportSwip
CanteenReportSwipeEntity e = new CanteenReportSwipeEntity();
BeanUtils.copyProperties(row, e);
e.setBizMonth(bizMonth);
e.setSource("EXCEL");
if (e.getConsumeAmount() == null) {
e.setConsumeAmount(BigDecimal.ZERO);
}
@@ -3,6 +3,8 @@ package com.bsd.pjcloud.canteen.util;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.bsd.pjcloud.canteen.excel.ExcelYuanConverter;
import com.bsd.pjcloud.canteen.excel.ExcelYuanStringConverter;
import com.bsd.pjcloud.common.excel.converters.LocalDateStringConverter;
import com.bsd.pjcloud.common.excel.converters.LocalDateTimeStringConverter;
import com.bsd.pjcloud.common.excel.converters.LocalTimeStringConverter;
@@ -10,6 +12,11 @@ import com.bsd.pjcloud.common.excel.converters.LongStringConverter;
import com.bsd.pjcloud.common.excel.converters.StringArrayConverter;
import com.bsd.pjcloud.common.excel.handler.DefaultAnalysisEventListener;
import lombok.experimental.UtilityClass;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
@@ -32,12 +39,55 @@ public class CanteenReportExcelUtils {
.registerConverter(LocalDateTimeStringConverter.INSTANCE)
.registerConverter(LongStringConverter.INSTANCE)
.registerConverter(StringArrayConverter.INSTANCE)
.registerConverter(new ExcelYuanConverter())
.registerConverter(new ExcelYuanStringConverter())
.sheet(sheetNo)
.headRowNumber(1)
.doRead();
return (List<T>) listener.getList();
}
/**
* 收发存汇总表:第2列是「收货单号」,且有「品牌」;与「食材能耗」sheet(第2列是供应商)区分。
*/
public static boolean isReceiptStockLayout(MultipartFile file) {
if (file == null || file.isEmpty()) {
return false;
}
String name = file.getOriginalFilename();
if (name != null && name.contains("收发存")) {
return true;
}
try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
Sheet sheet = wb.getNumberOfSheets() > 0 ? wb.getSheetAt(0) : null;
if (sheet == null) {
return false;
}
Row row = sheet.getRow(0);
if (row == null) {
return false;
}
DataFormatter fmt = new DataFormatter();
StringBuilder headers = new StringBuilder();
int last = Math.min(row.getLastCellNum(), 24);
for (int i = 0; i < last; i++) {
String h = fmt.formatCellValue(row.getCell(i));
if (h != null) {
headers.append(h.trim());
}
}
String all = headers.toString();
if (all.contains("收货单号") && (all.contains("品牌") || all.contains("结算金额"))) {
return true;
}
String col1 = fmt.formatCellValue(row.getCell(1));
return col1 != null && col1.contains("收货单号");
}
catch (Exception e) {
return false;
}
}
private static int resolveSheetNo(byte[] bytes, String preferredSheetName) {
if (preferredSheetName == null || preferredSheetName.isEmpty()) {
return 0;
@@ -86,6 +86,12 @@
<if test="importLogId == null and dateEnd != null and dateEnd != ''">
AND (biz_date IS NULL OR biz_date &lt;= #{dateEnd})
</if>
<if test="orgLevel2s != null and orgLevel2s.size() &gt; 0">
AND org_level2 IN
<foreach collection="orgLevel2s" item="org" open="(" separator="," close=")">
#{org}
</foreach>
</if>
GROUP BY
IFNULL(emp_no, ''),
IFNULL(emp_name, ''),
@@ -114,5 +120,26 @@
<if test="bizMonth != null and bizMonth != ''">
AND biz_month = #{bizMonth}
</if>
<if test="orgLevel2s != null and orgLevel2s.size() &gt; 0">
AND org_level2 IN
<foreach collection="orgLevel2s" item="org" open="(" separator="," close=")">
#{org}
</foreach>
</if>
</select>
<select id="listOrgLevel2" resultType="com.bsd.pjcloud.canteen.dto.report.CanteenReportOrgLevel2VO">
SELECT IFNULL(org_level2, '') AS name, COUNT(1) AS cnt
FROM canteen_report_face_detail
WHERE IFNULL(del_flag, '0') = '0'
AND org_level2 IS NOT NULL AND TRIM(org_level2) &lt;&gt; ''
<if test="importLogId != null">
AND import_log_id = #{importLogId}
</if>
<if test="importLogId == null and bizMonth != null and bizMonth != ''">
AND biz_month = #{bizMonth}
</if>
GROUP BY org_level2
ORDER BY cnt DESC
</select>
</mapper>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bsd.pjcloud.canteen.mapper.CanteenReportRosterMapper">
<delete id="deletePhysicallyByBizMonth">
DELETE FROM canteen_report_roster WHERE biz_month = #{bizMonth}
</delete>
<insert id="insertBatch">
INSERT INTO canteen_report_roster (
id, biz_month, emp_no, emp_name, emp_group, dept_cxo1, dept_cxo2, dept_cxo3,
job_title, job_nature, hr_scope, legal_company, rd_flag, finance_code, finance_name,
erp_cost_code, erp_cost_name, org_tag,
create_by, create_time, update_by, update_time, del_flag, tenant_id
) VALUES
<foreach collection="list" item="e" separator=",">
(
#{e.id}, #{e.bizMonth}, #{e.empNo}, #{e.empName}, #{e.empGroup}, #{e.deptCxo1}, #{e.deptCxo2}, #{e.deptCxo3},
#{e.jobTitle}, #{e.jobNature}, #{e.hrScope}, #{e.legalCompany}, #{e.rdFlag}, #{e.financeCode}, #{e.financeName},
#{e.erpCostCode}, #{e.erpCostName}, #{e.orgTag},
#{e.createBy}, #{e.createTime}, #{e.updateBy}, #{e.updateTime}, #{e.delFlag}, #{e.tenantId}
)
</foreach>
</insert>
</mapper>
@@ -31,6 +31,10 @@
AND (import_log_id IS NULL OR import_log_id &lt;&gt; #{importLogId})
</delete>
<delete id="deletePhysicallyBySourceAndBizMonth">
DELETE FROM canteen_report_swipe WHERE source = #{source} AND biz_month = #{bizMonth}
</delete>
<insert id="insertBatch">
INSERT INTO canteen_report_swipe (
id, biz_month, biz_date, emp_no, emp_name, legal_entity, dept_name,