diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportBoxExcel.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportBoxExcel.java index a14a58914..070e319dc 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportBoxExcel.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportBoxExcel.java @@ -1,53 +1,41 @@ -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; -} +package com.bsd.pjcloud.canteen.dto.report; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 包厢台账(汇总)行数据。 + *

+ * 导入已改为 {@code CanteenReportBoxExcelBinder} 按规范化表头名称映射, + * 不再依赖 EasyExcel {@code @ExcelProperty}(多值 value 会被当成多级表头而非别名)。 + */ +@Data +public class CanteenReportBoxExcel { + + private String monthText; + + private String bizDate; + + private String costCenter; + + private String costCenterCode; + + private String empNo; + + private String empName; + + private BigDecimal foodAmount; + + private BigDecimal drinkAmount; + + private BigDecimal totalAmount; + + private String legalEntity; + + private String remark1; + + private String remark2; + + private String park; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportFaceExcel.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportFaceExcel.java index 4c8a2e06e..dd3b5ed4b 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportFaceExcel.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportFaceExcel.java @@ -4,138 +4,140 @@ import com.alibaba.excel.annotation.ExcelProperty; import lombok.Data; /** - * 人脸消费系统导出 Excel,列序与样表一致(共 43 列)。 + * 人脸消费系统导出 Excel。 + *

+ * 按表头名称匹配(不按固定列序号),避免不同月份导出增删/调列后字段错位。 * 明细入库原样落库;汇总到刷卡明细时再做字段映射。 */ @Data public class CanteenReportFaceExcel { - @ExcelProperty(index = 0) + @ExcelProperty({ "订单号" }) private String orderNo; - @ExcelProperty(index = 1) + @ExcelProperty({ "第三方订单号" }) private String thirdOrderNo; - @ExcelProperty(index = 2) + @ExcelProperty({ "创建时间" }) private String createTime; - @ExcelProperty(index = 3) + @ExcelProperty({ "支付时间" }) private String payTime; - @ExcelProperty(index = 4) + @ExcelProperty({ "扣款时间" }) private String deductTime; - @ExcelProperty(index = 5) + @ExcelProperty({ "一级组织" }) private String orgLevel1; - @ExcelProperty(index = 6) + @ExcelProperty({ "二级组织" }) private String orgLevel2; - @ExcelProperty(index = 7) + @ExcelProperty({ "三级组织" }) private String orgLevel3; - @ExcelProperty(index = 8) + @ExcelProperty({ "食堂" }) private String canteen; - @ExcelProperty(index = 9) + @ExcelProperty({ "食堂区域" }) private String canteenArea; - @ExcelProperty(index = 10) + @ExcelProperty({ "消费点" }) private String consumePoint; - @ExcelProperty(index = 11) + @ExcelProperty({ "姓名" }) private String empName; - @ExcelProperty(index = 12) + @ExcelProperty({ "手机号", "手机", "电话" }) private String phone; - @ExcelProperty(index = 13) + @ExcelProperty({ "法律归属" }) private String legalEntity; - @ExcelProperty(index = 14) + @ExcelProperty({ "人员编号", "工号", "员工编号" }) private String empNo; - @ExcelProperty(index = 15) + @ExcelProperty({ "卡号" }) private String cardNo; - @ExcelProperty(index = 16) + @ExcelProperty({ "分组" }) private String groupName; - @ExcelProperty(index = 17) + @ExcelProperty({ "部门" }) private String deptName; - @ExcelProperty(index = 18) + @ExcelProperty({ "支付方式" }) private String payMethod; - @ExcelProperty(index = 19) + @ExcelProperty({ "支付类型" }) private String payType; - @ExcelProperty(index = 20) + @ExcelProperty({ "订单金额" }) private String orderAmount; - @ExcelProperty(index = 21) + @ExcelProperty({ "动账金额" }) private String changeAmount; - @ExcelProperty(index = 22) + @ExcelProperty({ "储值动账" }) private String storedChange; - @ExcelProperty(index = 23) + @ExcelProperty({ "补贴动账" }) private String subsidyChange; - @ExcelProperty(index = 24) + @ExcelProperty({ "赠送动账" }) private String giftChange; - @ExcelProperty(index = 25) + @ExcelProperty({ "储值钱包余额" }) private String storedBalance; - @ExcelProperty(index = 26) + @ExcelProperty({ "补贴钱包余额" }) private String subsidyBalance; - @ExcelProperty(index = 27) + @ExcelProperty({ "赠送钱包余额" }) private String giftBalance; - @ExcelProperty(index = 28) + @ExcelProperty({ "动账组织" }) private String changeOrg; - @ExcelProperty(index = 29) + @ExcelProperty({ "优惠" }) private String discount; - @ExcelProperty(index = 30) + @ExcelProperty({ "优惠金额" }) private String discountAmount; - @ExcelProperty(index = 31) + @ExcelProperty({ "优惠类型" }) private String discountType; - @ExcelProperty(index = 32) + @ExcelProperty({ "券类型" }) private String couponType; - @ExcelProperty(index = 33) + @ExcelProperty({ "抵扣金额" }) private String deductAmount; - @ExcelProperty(index = 34) + @ExcelProperty({ "手续费" }) private String feeAmount; - @ExcelProperty(index = 35) + @ExcelProperty({ "餐补" }) private String mealSubsidy; - @ExcelProperty(index = 36) + @ExcelProperty({ "消费类型" }) private String consumeType; - @ExcelProperty(index = 37) + @ExcelProperty({ "餐段" }) private String mealSegment; - @ExcelProperty(index = 38) + @ExcelProperty({ "交易设备", "结算台", "设备名称" }) private String deviceName; - @ExcelProperty(index = 39) + @ExcelProperty({ "设备状态" }) private String deviceStatus; - @ExcelProperty(index = 40) + @ExcelProperty({ "备注" }) private String remark; - @ExcelProperty(index = 41) + @ExcelProperty({ "操作员" }) private String operatorName; - @ExcelProperty(index = 42) + @ExcelProperty({ "访客餐支付方式" }) private String visitorPayMethod; } diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenReportPersonEntity.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenReportPersonEntity.java index 3f39de2cd..36c2c330f 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenReportPersonEntity.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenReportPersonEntity.java @@ -17,6 +17,9 @@ public class CanteenReportPersonEntity extends Model @TableId(type = IdType.ASSIGN_ID) private Long id; + @Schema(description = "适用月份 yyyy-MM") + private String bizMonth; + @Schema(description = "工号") private String empNo; diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/report/CanteenReportBoxExcelBinder.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/report/CanteenReportBoxExcelBinder.java new file mode 100644 index 000000000..9228a7b79 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/report/CanteenReportBoxExcelBinder.java @@ -0,0 +1,136 @@ +package com.bsd.pjcloud.canteen.report; + +import cn.hutool.core.util.StrUtil; +import com.bsd.pjcloud.canteen.dto.report.CanteenReportBoxExcel; +import com.bsd.pjcloud.canteen.excel.ExcelYuanConverter; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; + +/** + * 包厢台账 Excel:按规范化表头名称映射(不依赖列序号 / EasyExcel 多值注解)。 + *

+ * 注意:EasyExcel 的 {@code @ExcelProperty({"A","B"})} 表示多级表头,不是别名; + * 样表常见表头含括号/空格,必须自行规范化匹配。 + */ +public final class CanteenReportBoxExcelBinder { + + private static final Map ALIAS_TO_FIELD; + + private static final Map> FIELD_SETTERS; + + static { + Map alias = new HashMap<>(); + putAliases(alias, "monthText", "月份", "业务月"); + putAliases(alias, "bizDate", "日期", "业务日", "消费日期"); + putAliases(alias, "costCenter", "成本中心名称(核对)", "成本中心名称", "成本中心", "核对成本", "核对成本(开票公司)"); + putAliases(alias, "costCenterCode", "成本中心代码", "成本中心编码"); + putAliases(alias, "empNo", "签单人工号", "工号", "人员编号", "员工编号"); + putAliases(alias, "empName", "签单人", "姓名", "员工姓名"); + putAliases(alias, "foodAmount", "食品", "食品金额"); + putAliases(alias, "drinkAmount", "酒水", "酒水金额"); + putAliases(alias, "totalAmount", "总计", "总金额", "金额", "消费金额"); + putAliases(alias, "legalEntity", "费用承担公司(核对)", "费用承担公司", "法律归属", "法人公司"); + putAliases(alias, "remark1", "备注1", "备注"); + putAliases(alias, "remark2", "备注2"); + putAliases(alias, "park", "园区", "厂区"); + ALIAS_TO_FIELD = Collections.unmodifiableMap(alias); + + Map> setters = new HashMap<>(); + setters.put("monthText", CanteenReportBoxExcel::setMonthText); + setters.put("bizDate", CanteenReportBoxExcel::setBizDate); + setters.put("costCenter", CanteenReportBoxExcel::setCostCenter); + setters.put("costCenterCode", CanteenReportBoxExcel::setCostCenterCode); + setters.put("empNo", CanteenReportBoxExcel::setEmpNo); + setters.put("empName", CanteenReportBoxExcel::setEmpName); + setters.put("foodAmount", (row, v) -> row.setFoodAmount(parseAmount(v))); + setters.put("drinkAmount", (row, v) -> row.setDrinkAmount(parseAmount(v))); + setters.put("totalAmount", (row, v) -> row.setTotalAmount(parseAmount(v))); + setters.put("legalEntity", CanteenReportBoxExcel::setLegalEntity); + setters.put("remark1", CanteenReportBoxExcel::setRemark1); + setters.put("remark2", CanteenReportBoxExcel::setRemark2); + setters.put("park", CanteenReportBoxExcel::setPark); + FIELD_SETTERS = Collections.unmodifiableMap(setters); + } + + private CanteenReportBoxExcelBinder() { + } + + private static void putAliases(Map alias, String field, String... names) { + for (String n : names) { + alias.put(normalizeHead(n), field); + } + } + + /** 去空白/全角空格等,便于表头匹配 */ + public static String normalizeHead(String head) { + if (head == null) { + return ""; + } + String s = head.replace('\u00A0', ' ').replace(' ', ' ').trim(); + s = s.replaceAll("\\s+", ""); + return s; + } + + public static Map buildColumnFieldMap(Map rawHeadByIndex, + Set outMatchedFields) { + Map colField = new LinkedHashMap<>(); + if (rawHeadByIndex == null) { + return colField; + } + for (Map.Entry e : rawHeadByIndex.entrySet()) { + String field = ALIAS_TO_FIELD.get(normalizeHead(e.getValue())); + if (field == null) { + continue; + } + colField.put(e.getKey(), field); + if (outMatchedFields != null) { + outMatchedFields.add(field); + } + } + return colField; + } + + public static boolean hasField(Set matchedFields, String... fields) { + if (matchedFields == null || matchedFields.isEmpty()) { + return false; + } + for (String f : fields) { + if (matchedFields.contains(f)) { + return true; + } + } + return false; + } + + public static CanteenReportBoxExcel bind(Map colFieldMap, Map row) { + CanteenReportBoxExcel excel = new CanteenReportBoxExcel(); + if (colFieldMap == null || row == null) { + return excel; + } + for (Map.Entry e : colFieldMap.entrySet()) { + BiConsumer setter = FIELD_SETTERS.get(e.getValue()); + if (setter == null) { + continue; + } + boolean preferInteger = "empNo".equals(e.getValue()) || "costCenterCode".equals(e.getValue()); + String val = CanteenReportFaceExcelBinder.cellToPlainString(row.get(e.getKey()), preferInteger); + if (val != null) { + setter.accept(excel, val); + } + } + return excel; + } + + private static BigDecimal parseAmount(String raw) { + if (StrUtil.isBlank(raw)) { + return null; + } + return ExcelYuanConverter.parseYuan(raw); + } +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/report/CanteenReportConstants.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/report/CanteenReportConstants.java index 2251105bc..6ceca1b10 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/report/CanteenReportConstants.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/report/CanteenReportConstants.java @@ -36,6 +36,12 @@ public final class CanteenReportConstants { /** 刷卡备注:人员信息未命中,核对成本已回退法律归属 */ public static final String REMARK_PERSON_MISS = "未命中人员信息"; + /** 人脸汇总匹配方式:按当月人员信息(工号→核对成本) */ + public static final String MATCH_MODE_PERSON = "PERSON"; + + /** 人脸汇总匹配方式:按当月花名册(工号→人事范围/核对成本) */ + public static final String MATCH_MODE_ROSTER = "ROSTER"; + public static final List INCOME_TYPES = Collections.unmodifiableList(Arrays.asList( INCOME_SWIPE, INCOME_ALIPAY, INCOME_BOX, INCOME_OTHER, INCOME_PROFIT_ADJ, INCOME_INVOICE, INCOME_BAD_DEBT )); diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/report/CanteenReportFaceExcelBinder.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/report/CanteenReportFaceExcelBinder.java new file mode 100644 index 000000000..535fc2bbb --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/report/CanteenReportFaceExcelBinder.java @@ -0,0 +1,238 @@ +package com.bsd.pjcloud.canteen.report; + +import cn.hutool.core.util.StrUtil; +import com.bsd.pjcloud.canteen.dto.report.CanteenReportFaceExcel; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.BiConsumer; + +/** + * 人脸 Excel:按规范化表头名称映射到字段(不依赖列序号 / EasyExcel 注解精确匹配)。 + *

+ * 处理空格、全角空格、常见别名;数字工号避免科学计数法。 + */ +public final class CanteenReportFaceExcelBinder { + + /** 规范化表头 → 字段名 */ + private static final Map ALIAS_TO_FIELD; + + private static final Map> FIELD_SETTERS; + + static { + Map alias = new HashMap<>(); + putAliases(alias, "orderNo", "订单号"); + putAliases(alias, "thirdOrderNo", "第三方订单号"); + putAliases(alias, "createTime", "创建时间"); + putAliases(alias, "payTime", "支付时间"); + putAliases(alias, "deductTime", "扣款时间"); + putAliases(alias, "orgLevel1", "一级组织"); + putAliases(alias, "orgLevel2", "二级组织"); + putAliases(alias, "orgLevel3", "三级组织"); + putAliases(alias, "canteen", "食堂"); + putAliases(alias, "canteenArea", "食堂区域"); + putAliases(alias, "consumePoint", "消费点"); + putAliases(alias, "empName", "姓名"); + putAliases(alias, "phone", "手机号", "手机", "电话", "联系电话"); + putAliases(alias, "legalEntity", "法律归属"); + putAliases(alias, "empNo", "人员编号", "工号", "员工编号", "人员工号", "员工号"); + putAliases(alias, "cardNo", "卡号"); + putAliases(alias, "groupName", "分组", "员工分组"); + putAliases(alias, "deptName", "部门"); + putAliases(alias, "payMethod", "支付方式"); + putAliases(alias, "payType", "支付类型"); + putAliases(alias, "orderAmount", "订单金额"); + putAliases(alias, "changeAmount", "动账金额"); + putAliases(alias, "storedChange", "储值动账"); + putAliases(alias, "subsidyChange", "补贴动账"); + putAliases(alias, "giftChange", "赠送动账"); + putAliases(alias, "storedBalance", "储值钱包余额"); + putAliases(alias, "subsidyBalance", "补贴钱包余额"); + putAliases(alias, "giftBalance", "赠送钱包余额"); + putAliases(alias, "changeOrg", "动账组织"); + putAliases(alias, "discount", "优惠"); + putAliases(alias, "discountAmount", "优惠金额"); + putAliases(alias, "discountType", "优惠类型"); + putAliases(alias, "couponType", "券类型"); + putAliases(alias, "deductAmount", "抵扣金额"); + putAliases(alias, "feeAmount", "手续费"); + putAliases(alias, "mealSubsidy", "餐补"); + putAliases(alias, "consumeType", "消费类型"); + putAliases(alias, "mealSegment", "餐段"); + putAliases(alias, "deviceName", "交易设备", "结算台", "设备名称", "设备"); + putAliases(alias, "deviceStatus", "设备状态"); + putAliases(alias, "remark", "备注"); + putAliases(alias, "operatorName", "操作员"); + putAliases(alias, "visitorPayMethod", "访客餐支付方式"); + ALIAS_TO_FIELD = Collections.unmodifiableMap(alias); + + Map> setters = new HashMap<>(); + setters.put("orderNo", CanteenReportFaceExcel::setOrderNo); + setters.put("thirdOrderNo", CanteenReportFaceExcel::setThirdOrderNo); + setters.put("createTime", CanteenReportFaceExcel::setCreateTime); + setters.put("payTime", CanteenReportFaceExcel::setPayTime); + setters.put("deductTime", CanteenReportFaceExcel::setDeductTime); + setters.put("orgLevel1", CanteenReportFaceExcel::setOrgLevel1); + setters.put("orgLevel2", CanteenReportFaceExcel::setOrgLevel2); + setters.put("orgLevel3", CanteenReportFaceExcel::setOrgLevel3); + setters.put("canteen", CanteenReportFaceExcel::setCanteen); + setters.put("canteenArea", CanteenReportFaceExcel::setCanteenArea); + setters.put("consumePoint", CanteenReportFaceExcel::setConsumePoint); + setters.put("empName", CanteenReportFaceExcel::setEmpName); + setters.put("phone", CanteenReportFaceExcel::setPhone); + setters.put("legalEntity", CanteenReportFaceExcel::setLegalEntity); + setters.put("empNo", CanteenReportFaceExcel::setEmpNo); + setters.put("cardNo", CanteenReportFaceExcel::setCardNo); + setters.put("groupName", CanteenReportFaceExcel::setGroupName); + setters.put("deptName", CanteenReportFaceExcel::setDeptName); + setters.put("payMethod", CanteenReportFaceExcel::setPayMethod); + setters.put("payType", CanteenReportFaceExcel::setPayType); + setters.put("orderAmount", CanteenReportFaceExcel::setOrderAmount); + setters.put("changeAmount", CanteenReportFaceExcel::setChangeAmount); + setters.put("storedChange", CanteenReportFaceExcel::setStoredChange); + setters.put("subsidyChange", CanteenReportFaceExcel::setSubsidyChange); + setters.put("giftChange", CanteenReportFaceExcel::setGiftChange); + setters.put("storedBalance", CanteenReportFaceExcel::setStoredBalance); + setters.put("subsidyBalance", CanteenReportFaceExcel::setSubsidyBalance); + setters.put("giftBalance", CanteenReportFaceExcel::setGiftBalance); + setters.put("changeOrg", CanteenReportFaceExcel::setChangeOrg); + setters.put("discount", CanteenReportFaceExcel::setDiscount); + setters.put("discountAmount", CanteenReportFaceExcel::setDiscountAmount); + setters.put("discountType", CanteenReportFaceExcel::setDiscountType); + setters.put("couponType", CanteenReportFaceExcel::setCouponType); + setters.put("deductAmount", CanteenReportFaceExcel::setDeductAmount); + setters.put("feeAmount", CanteenReportFaceExcel::setFeeAmount); + setters.put("mealSubsidy", CanteenReportFaceExcel::setMealSubsidy); + setters.put("consumeType", CanteenReportFaceExcel::setConsumeType); + setters.put("mealSegment", CanteenReportFaceExcel::setMealSegment); + setters.put("deviceName", CanteenReportFaceExcel::setDeviceName); + setters.put("deviceStatus", CanteenReportFaceExcel::setDeviceStatus); + setters.put("remark", CanteenReportFaceExcel::setRemark); + setters.put("operatorName", CanteenReportFaceExcel::setOperatorName); + setters.put("visitorPayMethod", CanteenReportFaceExcel::setVisitorPayMethod); + FIELD_SETTERS = Collections.unmodifiableMap(setters); + } + + private CanteenReportFaceExcelBinder() { + } + + private static void putAliases(Map alias, String field, String... names) { + for (String n : names) { + alias.put(normalizeHead(n), field); + } + } + + /** 去空白/全角空格等,便于表头匹配 */ + public static String normalizeHead(String head) { + if (head == null) { + return ""; + } + String s = head.replace('\u00A0', ' ').replace(' ', ' ').trim(); + // 去掉所有空白,兼容「人员 编号」「人员编号\n」 + s = s.replaceAll("\\s+", ""); + return s; + } + + /** + * 根据原始表头行建立:列下标 → 字段名 + * + * @return 列下标到字段;同时 outMatchedFields 填入已命中字段名 + */ + public static Map buildColumnFieldMap(Map rawHeadByIndex, + java.util.Set outMatchedFields) { + Map colField = new LinkedHashMap<>(); + if (rawHeadByIndex == null) { + return colField; + } + for (Map.Entry e : rawHeadByIndex.entrySet()) { + String field = ALIAS_TO_FIELD.get(normalizeHead(e.getValue())); + if (field == null) { + continue; + } + colField.put(e.getKey(), field); + if (outMatchedFields != null) { + outMatchedFields.add(field); + } + } + return colField; + } + + public static boolean hasField(java.util.Set matchedFields, String... fields) { + if (matchedFields == null || matchedFields.isEmpty()) { + return false; + } + for (String f : fields) { + if (matchedFields.contains(f)) { + return true; + } + } + return false; + } + + public static CanteenReportFaceExcel bind(Map colFieldMap, Map row) { + CanteenReportFaceExcel excel = new CanteenReportFaceExcel(); + if (colFieldMap == null || row == null) { + return excel; + } + for (Map.Entry e : colFieldMap.entrySet()) { + BiConsumer setter = FIELD_SETTERS.get(e.getValue()); + if (setter == null) { + continue; + } + String val = cellToPlainString(row.get(e.getKey()), "empNo".equals(e.getValue()) || "phone".equals(e.getValue()) + || "cardNo".equals(e.getValue()) || "orderNo".equals(e.getValue())); + if (val != null) { + setter.accept(excel, val); + } + } + return excel; + } + + /** + * 单元格转字符串;对工号/手机号等尽量保留整数形式,避免 1.0093664E7。 + */ + public static String cellToPlainString(Object raw, boolean preferInteger) { + if (raw == null) { + return null; + } + if (raw instanceof Number) { + return numberToPlain((Number) raw, preferInteger); + } + String s = String.valueOf(raw).trim(); + if (s.isEmpty() || "null".equalsIgnoreCase(s)) { + return null; + } + if (preferInteger && s.matches("[-+]?\\d+(\\.\\d+)?([Ee][-+]?\\d+)?")) { + try { + return numberToPlain(new BigDecimal(s), true); + } + catch (Exception ignored) { + return s; + } + } + return s; + } + + private static String numberToPlain(Number n, boolean preferInteger) { + BigDecimal bd = n instanceof BigDecimal ? (BigDecimal) n : new BigDecimal(n.toString()); + if (preferInteger) { + try { + bd = bd.stripTrailingZeros(); + if (bd.scale() <= 0) { + return bd.toBigInteger().toString(); + } + } + catch (Exception ignored) { + // fall through + } + } + return bd.stripTrailingZeros().toPlainString(); + } + + public static String blankToNull(String s) { + return StrUtil.isBlank(s) ? null : s.trim(); + } +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportBoxImportController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportBoxImportController.java index 730aa2122..dce50594f 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportBoxImportController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportBoxImportController.java @@ -28,4 +28,12 @@ public class CanteenReportBoxImportController { @RequestParam(defaultValue = "true") boolean replaceMonth) { return boxImportService.importExcel(file, bizMonth, replaceMonth); } + + @SysLog("清空包厢刷卡") + @Operation(summary = "按月清空包厢刷卡(source=BOX);可传 park 只清某厂区") + @DeleteMapping("/clear") + public R clear(@RequestParam String bizMonth, + @RequestParam(required = false) String park) { + return boxImportService.clearByMonthPark(bizMonth, park); + } } diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportController.java index e8a96d2cf..d69df8142 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportController.java @@ -80,6 +80,16 @@ public class CanteenReportController { return R.ok(calcService.businessReport(bizMonth)); } + @Operation(summary = "当月动态厂区列(来自刷卡/食材实际数据)") + @GetMapping("/parks") + public R parks(@RequestParam String bizMonth) { + List parks = calcService.resolveParkColumns(bizMonth); + Map map = new java.util.LinkedHashMap<>(); + map.put("parks", parks); + map.put("shareParks", parks); + return R.ok(map); + } + @ResponseExcel @Operation(summary = "导出收入合计") @GetMapping("/export/income") diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportFaceImportController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportFaceImportController.java index eec4756fd..2d0a878a1 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportFaceImportController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportFaceImportController.java @@ -113,14 +113,16 @@ public class CanteenReportFaceImportController { } @SysLog("人脸明细汇总到刷卡明细") - @Operation(summary = "统计汇总写入刷卡明细(支付方式原样作收入类别;B扫C等不自动归支付宝)") + @Operation(summary = "统计汇总写入刷卡明细;matchMode=PERSON|ROSTER;excludeParks 排除厂区(逗号分隔)") @PostMapping("/aggregate") public R aggregate(@RequestParam(required = false) String bizMonth, @RequestParam(required = false) String dateStart, @RequestParam(required = false) String dateEnd, @RequestParam(required = false) String importLogId, @RequestParam(defaultValue = "true") boolean replaceFace, - @RequestParam(required = false) String orgLevel2s) { + @RequestParam(required = false) String orgLevel2s, + @RequestParam(defaultValue = "PERSON") String matchMode, + @RequestParam(required = false) String excludeParks) { Long logId = null; if (cn.hutool.core.util.StrUtil.isNotBlank(importLogId)) { try { @@ -134,7 +136,11 @@ public class CanteenReportFaceImportController { java.util.List 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); + java.util.List exclude = cn.hutool.core.util.StrUtil.isBlank(excludeParks) + ? java.util.List.of() + : cn.hutool.core.util.StrUtil.splitTrim(excludeParks, ','); + return faceImportService.aggregateToSwipe(bizMonth, dateStart, dateEnd, logId, replaceFace, orgs, matchMode, + exclude); } @Operation(summary = "明细中的二级组织列表(汇总筛选用)") diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportFoodEnergyController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportFoodEnergyController.java index 4132604c9..fb4e5d4ff 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportFoodEnergyController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportFoodEnergyController.java @@ -63,9 +63,11 @@ public class CanteenReportFoodEnergyController { @SysLog("按月清空食材能耗") @DeleteMapping("/month/{bizMonth}") - public R clearMonth(@PathVariable String bizMonth) { + public R clearMonth(@PathVariable String bizMonth, + @RequestParam(required = false) String park) { return R.ok(foodEnergyService.remove(Wrappers.lambdaQuery() - .eq(CanteenReportFoodEnergyEntity::getBizMonth, bizMonth))); + .eq(CanteenReportFoodEnergyEntity::getBizMonth, bizMonth) + .eq(cn.hutool.core.util.StrUtil.isNotBlank(park), CanteenReportFoodEnergyEntity::getPark, park))); } @Operation(summary = "导入Excel(自动识别收发存汇总表 / 食堂报表「食材能耗」sheet)") diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportPersonController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportPersonController.java index 45a88c83e..e7c79ecec 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportPersonController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportPersonController.java @@ -32,10 +32,11 @@ public class CanteenReportPersonController { @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 costCenter) { - return R.ok(personService.pageList(page, empNo, empName, costCenter)); + return R.ok(personService.pageList(page, bizMonth, empNo, empName, costCenter)); } @Operation(summary = "详情") @@ -72,20 +73,28 @@ public class CanteenReportPersonController { return personService.clearAll(); } + @SysLog("按月清空人员信息") + @Operation(summary = "按适用月清空") + @DeleteMapping("/month") + public R clearByMonth(@RequestParam String bizMonth) { + return personService.clearByMonth(bizMonth); + } + @SysLog("导入人员信息") - @Operation(summary = "导入Excel:replaceAll=true 全量覆盖;false 新增导入(同工号更新)") + @Operation(summary = "导入Excel:需传 bizMonth;replaceMonth=true 覆盖当月;false 同月同工号更新") @PostMapping("/import") public R importExcel(@RequestParam("file") MultipartFile file, - @RequestParam(defaultValue = "true") boolean replaceAll) throws Exception { + @RequestParam String bizMonth, + @RequestParam(defaultValue = "true") boolean replaceMonth) throws Exception { List rows = CanteenReportExcelUtils.readSheet(file, CanteenReportPersonExcel.class, "刷卡明细"); - return personService.importExcel(rows, replaceAll); + return personService.importExcel(rows, bizMonth, replaceMonth); } @ResponseExcel @Operation(summary = "导出") @GetMapping("/export") - public List export() { - return personService.exportExcel(); + public List export(@RequestParam(required = false) String bizMonth) { + return personService.exportExcel(bizMonth); } } diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportSettleParkController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportSettleParkController.java index cbb1b6649..c680c4138 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportSettleParkController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportSettleParkController.java @@ -2,7 +2,6 @@ package com.bsd.pjcloud.canteen.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.canteen.entity.CanteenReportSettleParkEntity; -import com.bsd.pjcloud.canteen.report.CanteenReportConstants; import com.bsd.pjcloud.canteen.service.CanteenReportSettleParkService; import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.common.log.annotation.SysLog; @@ -34,10 +33,10 @@ public class CanteenReportSettleParkController { return R.ok(settleParkService.pageList(page, settleCounter, park)); } - @Operation(summary = "厂区枚举") + @Operation(summary = "厂区枚举(配置表+推荐名,可自由扩展)") @GetMapping("/parks") public R parks() { - return R.ok(CanteenReportConstants.PARKS); + return R.ok(settleParkService.listConfiguredParks()); } @Operation(summary = "详情") diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenReportFaceDetailMapper.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenReportFaceDetailMapper.java index 23ef847a2..5e98bb63a 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenReportFaceDetailMapper.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenReportFaceDetailMapper.java @@ -14,14 +14,16 @@ public interface CanteenReportFaceDetailMapper extends BaseMapper list); - int deletePhysicallyByImportLogId(@Param("importLogId") Long importLogId); + /** 分批物理删除,配合 limit 避免 OceanBase query_timeout */ + int deletePhysicallyByImportLogId(@Param("importLogId") Long importLogId, @Param("limit") int limit); int deletePhysicallyByDateRange(@Param("bizMonth") String bizMonth, - @Param("dateStart") String dateStart, @Param("dateEnd") String dateEnd); + @Param("dateStart") String dateStart, @Param("dateEnd") String dateEnd, + @Param("limit") int limit); int deletePhysicallyDateRangeExceptLog(@Param("bizMonth") String bizMonth, @Param("dateStart") String dateStart, @Param("dateEnd") String dateEnd, - @Param("importLogId") Long importLogId); + @Param("importLogId") Long importLogId, @Param("limit") int limit); /** * 按人+维度汇总;刷脸等→员工刷卡,B扫C等扫码保持支付方式原样。金额用订单金额。 diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenReportPersonMapper.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenReportPersonMapper.java index 73c56e9c9..015d222c4 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenReportPersonMapper.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenReportPersonMapper.java @@ -12,5 +12,7 @@ public interface CanteenReportPersonMapper extends BaseMapper list); } diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenReportSwipeMapper.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenReportSwipeMapper.java index 60368a857..f23f785d8 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenReportSwipeMapper.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenReportSwipeMapper.java @@ -12,22 +12,33 @@ public interface CanteenReportSwipeMapper extends BaseMapper excludeParks, @Param("limit") int limit); int insertBatch(@Param("list") List list); } diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportBoxImportService.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportBoxImportService.java index 013d59548..a44de504f 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportBoxImportService.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportBoxImportService.java @@ -9,4 +9,7 @@ import org.springframework.web.multipart.MultipartFile; public interface CanteenReportBoxImportService { R importExcel(MultipartFile file, String bizMonth, boolean replaceMonth); + + /** 清空某月包厢刷卡;park 非空则只清该厂区 */ + R clearByMonthPark(String bizMonth, String park); } diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportCalcService.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportCalcService.java index 549bb9155..8c5a923d9 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportCalcService.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportCalcService.java @@ -23,4 +23,7 @@ public interface CanteenReportCalcService { List businessReport(String bizMonth); Map summary(String bizMonth); + + /** 当月刷卡/食材实际出现的厂区列(动态) */ + List resolveParkColumns(String bizMonth); } diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportFaceImportService.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportFaceImportService.java index eff5b15e1..d2d90f4f1 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportFaceImportService.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportFaceImportService.java @@ -23,10 +23,12 @@ public interface CanteenReportFaceImportService extends IService orgLevel2s); + List orgLevel2s, String matchMode, List excludeParks); Page pageLogs(Page page, String bizMonth, String status); diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportPersonService.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportPersonService.java index 27d83e1d4..e5b3ee7cc 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportPersonService.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportPersonService.java @@ -11,21 +11,27 @@ import java.util.Map; public interface CanteenReportPersonService extends IService { - Page pageList(Page page, String empNo, String empName, String costCenter); + Page pageList(Page page, String bizMonth, String empNo, String empName, + String costCenter); R savePerson(CanteenReportPersonEntity entity); R updatePerson(CanteenReportPersonEntity entity); /** - * @param replaceAll true=全量覆盖;false=新增导入(同工号则更新) + * 按适用月导入。 + * + * @param bizMonth 适用月 yyyy-MM + * @param replaceMonth true=先清空该月再入库;false=同月同工号更新 */ - R importExcel(List rows, boolean replaceAll); + R importExcel(List rows, String bizMonth, boolean replaceMonth); - List exportExcel(); + List exportExcel(String bizMonth); R clearAll(); - /** empNo → costCenter(空核对成本也占位) */ - Map loadCostCenterMap(); + R clearByMonth(String bizMonth); + + /** 指定适用月:empNo → costCenter(空核对成本也占位) */ + Map loadCostCenterMap(String bizMonth); } diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportSettleParkService.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportSettleParkService.java index 0e37a7cab..5882284f6 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportSettleParkService.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportSettleParkService.java @@ -10,4 +10,7 @@ public interface CanteenReportSettleParkService extends IService pageList(Page page, String settleCounter, String park); R saveOrUpdateRow(CanteenReportSettleParkEntity entity); + + /** 配置过的厂区 + 推荐常量(可自由扩展) */ + java.util.List listConfiguredParks(); } diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportBoxImportServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportBoxImportServiceImpl.java index 27bc57b7d..589847d89 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportBoxImportServiceImpl.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportBoxImportServiceImpl.java @@ -3,12 +3,13 @@ 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.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; 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.CanteenReportBoxExcelBinder; import com.bsd.pjcloud.canteen.report.CanteenReportConstants; import com.bsd.pjcloud.canteen.service.CanteenReportBoxImportService; import com.bsd.pjcloud.common.core.constant.CommonConstants; @@ -23,7 +24,11 @@ import org.springframework.web.multipart.MultipartFile; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Set; @Service @RequiredArgsConstructor @@ -31,9 +36,33 @@ public class CanteenReportBoxImportServiceImpl implements CanteenReportBoxImport private static final String SOURCE_BOX = "BOX"; private static final int BATCH = 800; + private static final int DELETE_BATCH = 3000; + private static final int SKIP_SAMPLE_LIMIT = 50; private final CanteenReportSwipeMapper swipeMapper; + private int deleteBoxByMonthPark(String bizMonth, String park) { + int total = 0; + int n; + do { + n = swipeMapper.deletePhysicallyBySourceBizMonthPark(SOURCE_BOX, bizMonth, park, DELETE_BATCH); + total += n; + } + while (n >= DELETE_BATCH); + return total; + } + + private int deleteBoxByMonth(String bizMonth) { + int total = 0; + int n; + do { + n = swipeMapper.deletePhysicallyBySourceAndBizMonth(SOURCE_BOX, bizMonth, DELETE_BATCH); + total += n; + } + while (n >= DELETE_BATCH); + return total; + } + @Override @Transactional(rollbackFor = Exception.class) public R importExcel(MultipartFile file, String bizMonth, boolean replaceMonth) { @@ -44,13 +73,15 @@ public class CanteenReportBoxImportServiceImpl implements CanteenReportBoxImport return R.failed("请传入业务月份 bizMonth"); } try { - List rows = EasyExcel.read(file.getInputStream()) - .head(CanteenReportBoxExcel.class) - .registerConverter(new ExcelYuanConverter()) - .registerConverter(new ExcelYuanStringConverter()) + BoxRowAccumulator acc = new BoxRowAccumulator(); + EasyExcel.read(file.getInputStream(), acc) .sheet() .headRowNumber(1) - .doReadSync(); + .doRead(); + if (StrUtil.isNotBlank(acc.headError)) { + return R.failed(acc.headError); + } + List rows = acc.rows; if (CollUtil.isEmpty(rows)) { return R.failed("导入数据为空"); } @@ -58,16 +89,27 @@ public class CanteenReportBoxImportServiceImpl implements CanteenReportBoxImport Long tenantId = TenantContextHolder.getTenantId(); LocalDateTime now = LocalDateTime.now(); List saveList = new ArrayList<>(); + Map skipReasonCount = new LinkedHashMap<>(); + List skipSamples = new ArrayList<>(); int skip = 0; - for (CanteenReportBoxExcel row : rows) { + for (int i = 0; i < rows.size(); i++) { + CanteenReportBoxExcel row = rows.get(i); + int excelRow = i + 2; // 表头第1行,数据从第2行起 + if (isBlankRow(row)) { + skip++; + recordSkip(skipReasonCount, skipSamples, excelRow, "空行", row); + continue; + } BigDecimal amount = resolveAmount(row); if (amount == null || amount.compareTo(BigDecimal.ZERO) == 0) { skip++; + recordSkip(skipReasonCount, skipSamples, excelRow, "无金额或金额为0(总计/食品+酒水)", row); continue; } if (StrUtil.isBlank(row.getCostCenter()) && StrUtil.isBlank(row.getEmpNo()) && StrUtil.isBlank(row.getPark())) { skip++; + recordSkip(skipReasonCount, skipSamples, excelRow, "成本中心、签单人工号、园区均为空", row); continue; } CanteenReportSwipeEntity e = new CanteenReportSwipeEntity(); @@ -95,22 +137,181 @@ public class CanteenReportBoxImportServiceImpl implements CanteenReportBoxImport saveList.add(e); } if (CollUtil.isEmpty(saveList)) { - return R.failed("无有效包厢金额可入库"); + Map emptyData = buildSkipPayload(0, skip, skipReasonCount, skipSamples); + return R.failed(emptyData, "无有效包厢金额可入库。" + buildSkipSummary(skip, skipReasonCount, skipSamples)); } if (replaceMonth) { - swipeMapper.deletePhysicallyBySourceAndBizMonth(SOURCE_BOX, bizMonth.trim()); + deleteBoxByMonth(bizMonth.trim()); } for (List chunk : CollUtil.split(saveList, BATCH)) { swipeMapper.insertBatch(chunk); } - return R.ok(null, String.format("包厢导入成功:写入刷卡 %d 条(收入类别=包厢收入),跳过 %d 条", - saveList.size(), skip)); + Map data = buildSkipPayload(saveList.size(), skip, skipReasonCount, skipSamples); + String msg = String.format("包厢导入成功:写入刷卡 %d 条(收入类别=包厢收入),跳过 %d 条", + saveList.size(), skip); + if (skip > 0) { + msg = msg + "。可查看跳过明细。"; + } + return R.ok(data, msg); } catch (Exception e) { return R.failed("导入失败: " + e.getMessage()); } } + @Override + @Transactional(rollbackFor = Exception.class) + public R clearByMonthPark(String bizMonth, String park) { + if (StrUtil.isBlank(bizMonth)) { + return R.failed("请传入业务月份 bizMonth"); + } + String month = bizMonth.trim(); + String parkTrim = StrUtil.trim(park); + int n = deleteBoxByMonthPark(month, StrUtil.isBlank(parkTrim) ? null : parkTrim); + if (StrUtil.isNotBlank(parkTrim)) { + return R.ok(null, String.format("已清空 %s 厂区「%s」包厢刷卡 %d 条", month, parkTrim, n)); + } + return R.ok(null, String.format("已清空 %s 全部包厢刷卡 %d 条", month, n)); + } + + /** + * 按表头名称读包厢明细(Map 行),关键列缺失时直接报错。 + */ + static final class BoxRowAccumulator extends AnalysisEventListener> { + + private final List rows = new ArrayList<>(); + private Map colFieldMap = Map.of(); + private String headError; + + @Override + public void invokeHeadMap(Map headMap, AnalysisContext context) { + if (headMap == null || headMap.isEmpty()) { + headError = "未读到表头,请确认第一行是列名"; + return; + } + Set matched = new HashSet<>(); + this.colFieldMap = CanteenReportBoxExcelBinder.buildColumnFieldMap(headMap, matched); + List missing = new ArrayList<>(); + if (!CanteenReportBoxExcelBinder.hasField(matched, "costCenter")) { + missing.add("成本中心名称(核对)"); + } + if (!CanteenReportBoxExcelBinder.hasField(matched, "totalAmount", "foodAmount", "drinkAmount")) { + missing.add("总计/食品/酒水"); + } + if (!CanteenReportBoxExcelBinder.hasField(matched, "park")) { + missing.add("园区"); + } + if (!missing.isEmpty()) { + List rawHeads = new ArrayList<>(); + for (String h : headMap.values()) { + if (StrUtil.isNotBlank(h)) { + rawHeads.add(h.trim()); + } + } + headError = "表头缺少关键列(按名称匹配):" + String.join("、", missing) + + "。当前表头示例:" + String.join(" | ", rawHeads.subList(0, Math.min(12, rawHeads.size()))) + + (rawHeads.size() > 12 ? " …" : "") + + "。请导入「包厢台账」明细 Sheet(含成本中心/签单人/总计/园区)。"; + } + } + + @Override + public void invoke(Map row, AnalysisContext context) { + if (StrUtil.isNotBlank(headError)) { + return; + } + rows.add(CanteenReportBoxExcelBinder.bind(colFieldMap, row)); + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + // no-op:行已全部收集到 rows + } + } + + private static boolean isBlankRow(CanteenReportBoxExcel row) { + if (row == null) { + return true; + } + return StrUtil.isBlank(row.getBizDate()) + && StrUtil.isBlank(row.getCostCenter()) + && StrUtil.isBlank(row.getEmpNo()) + && StrUtil.isBlank(row.getEmpName()) + && StrUtil.isBlank(row.getPark()) + && StrUtil.isBlank(row.getLegalEntity()) + && (row.getTotalAmount() == null || row.getTotalAmount().compareTo(BigDecimal.ZERO) == 0) + && (row.getFoodAmount() == null || row.getFoodAmount().compareTo(BigDecimal.ZERO) == 0) + && (row.getDrinkAmount() == null || row.getDrinkAmount().compareTo(BigDecimal.ZERO) == 0); + } + + private static void recordSkip(Map reasonCount, List samples, int excelRow, + String reason, CanteenReportBoxExcel row) { + reasonCount.merge(reason, 1, Integer::sum); + if (samples.size() >= SKIP_SAMPLE_LIMIT) { + return; + } + samples.add("Excel第" + excelRow + "行:" + reason + ";" + briefRow(row)); + } + + private static String briefRow(CanteenReportBoxExcel row) { + if (row == null) { + return ""; + } + List parts = new ArrayList<>(); + if (StrUtil.isNotBlank(row.getBizDate())) { + parts.add("日期=" + row.getBizDate()); + } + if (StrUtil.isNotBlank(row.getCostCenter())) { + parts.add("成本中心=" + row.getCostCenter()); + } + if (StrUtil.isNotBlank(row.getEmpNo())) { + parts.add("工号=" + row.getEmpNo()); + } + if (StrUtil.isNotBlank(row.getEmpName())) { + parts.add("签单人=" + row.getEmpName()); + } + if (StrUtil.isNotBlank(row.getPark())) { + parts.add("园区=" + row.getPark()); + } + if (row.getTotalAmount() != null) { + parts.add("总计=" + row.getTotalAmount()); + } + return parts.isEmpty() ? "(无有效字段)" : String.join(",", parts); + } + + private static String buildSkipSummary(int skip, Map reasonCount, List samples) { + if (skip <= 0) { + return ""; + } + StringBuilder sb = new StringBuilder(); + sb.append("共跳过 ").append(skip).append(" 行。\n"); + sb.append("【分类统计】\n"); + for (Map.Entry e : reasonCount.entrySet()) { + sb.append("- ").append(e.getKey()).append(":").append(e.getValue()).append(" 行\n"); + } + if (CollUtil.isNotEmpty(samples)) { + sb.append("【样例(最多").append(SKIP_SAMPLE_LIMIT).append("条)】\n"); + for (String s : samples) { + sb.append(s).append("\n"); + } + if (skip > samples.size()) { + sb.append("…其余 ").append(skip - samples.size()).append(" 行略\n"); + } + } + return sb.toString(); + } + + private static Map buildSkipPayload(int written, int skip, Map reasonCount, + List samples) { + Map data = new LinkedHashMap<>(); + data.put("written", written); + data.put("skip", skip); + data.put("skipReasons", reasonCount); + data.put("skipSamples", samples); + data.put("skipSummary", buildSkipSummary(skip, reasonCount, samples)); + return data; + } + private static BigDecimal resolveAmount(CanteenReportBoxExcel row) { if (row.getTotalAmount() != null) { return row.getTotalAmount(); diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportCalcServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportCalcServiceImpl.java index fb1dded30..c89496449 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportCalcServiceImpl.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportCalcServiceImpl.java @@ -22,15 +22,19 @@ import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * 报表计算演示实现: * 收入合计 = 刷卡明细按成本中心×厂区汇总; * 供应商分摊 = 权重 × (厂区食材池 − 厂区包厢 − 厂区其他收入),权重=中心员工刷卡/厂区员工刷卡; * 经营报表 = 收入(万元) + 支出(食材=分摊−能耗−外协;能耗/外协按食材能耗分类;人工按人工列) + 毛利分析。 + * 厂区列按当月刷卡/食材实际出现的厂区动态生成(有数据才出列)。 */ @Service @RequiredArgsConstructor @@ -43,8 +47,64 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { private final CanteenReportFoodEnergyService foodEnergyService; private final CanteenReportManualAdjustService adjustService; + /** 单次计算内的动态厂区列 */ + private final ThreadLocal> parkColsHolder = new ThreadLocal<>(); + + private List parkCols() { + List cols = parkColsHolder.get(); + return cols != null ? cols : List.of(); + } + + private void beginParkCols(String bizMonth) { + parkColsHolder.set(resolveParkColumns(bizMonth)); + } + + private void endParkCols() { + parkColsHolder.remove(); + } + + /** + * 当月刷卡 + 食材能耗中出现的非空厂区;优先按常量推荐顺序,其余按名称排序。 + */ + @Override + public List resolveParkColumns(String bizMonth) { + Set found = new LinkedHashSet<>(); + if (StrUtil.isNotBlank(bizMonth)) { + for (CanteenReportSwipeEntity s : loadSwipes(bizMonth)) { + if (StrUtil.isNotBlank(s.getPark())) { + found.add(s.getPark().trim()); + } + } + for (CanteenReportFoodEnergyEntity f : loadFoods(bizMonth)) { + if (StrUtil.isNotBlank(f.getPark())) { + found.add(f.getPark().trim()); + } + } + } + List ordered = new ArrayList<>(); + for (String p : CanteenReportConstants.PARKS) { + if (found.remove(p)) { + ordered.add(p); + } + } + List rest = new ArrayList<>(found); + Collections.sort(rest); + ordered.addAll(rest); + return ordered; + } + @Override public List incomeReport(String bizMonth) { + beginParkCols(bizMonth); + try { + return incomeReport0(bizMonth); + } + finally { + endParkCols(); + } + } + + private List incomeReport0(String bizMonth) { List swipes = loadSwipes(bizMonth); // key: incomeType|costCenter|park -> amount Map agg = new HashMap<>(); @@ -74,6 +134,16 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { @Override public List supplierShareReport(String bizMonth) { + beginParkCols(bizMonth); + try { + return supplierShareReport0(bizMonth); + } + finally { + endParkCols(); + } + } + + private List supplierShareReport0(String bizMonth) { List swipes = loadSwipes(bizMonth); List foods = loadFoods(bizMonth); List adjusts = loadAdjusts(bizMonth); @@ -110,7 +180,7 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { // 可分摊 = 厂区食材池 − 厂区包厢收入 − 厂区其他收入(与样表一致) Map parkAllocable = new LinkedHashMap<>(); - for (String park : CanteenReportConstants.SHARE_PARKS) { + for (String park : parkCols()) { parkAllocable.put(park, nz(parkFoodPool.get(park)).subtract(nz(parkBoxOther.get(park)))); } @@ -145,7 +215,7 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { List rows = new ArrayList<>(); Map shareParkTotal = new LinkedHashMap<>(); - for (String park : CanteenReportConstants.SHARE_PARKS) { + for (String park : parkCols()) { shareParkTotal.put(park, BigDecimal.ZERO); } BigDecimal totalSub = BigDecimal.ZERO; @@ -157,7 +227,7 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { CanteenReportShareRowVO row = new CanteenReportShareRowVO(); row.setCostCenter(center); BigDecimal sub = BigDecimal.ZERO; - for (String park : CanteenReportConstants.SHARE_PARKS) { + for (String park : parkCols()) { BigDecimal allocable = nz(parkAllocable.get(park)); BigDecimal parkTotal = nz(parkSwipeTotal.get(park)); BigDecimal centerAmt = nz(centerParkSwipe.get(center + "|" + park)); @@ -186,7 +256,7 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { if (globalAdjust.compareTo(BigDecimal.ZERO) != 0) { CanteenReportShareRowVO globalRow = new CanteenReportShareRowVO(); globalRow.setCostCenter("开票调整(全局)"); - for (String park : CanteenReportConstants.SHARE_PARKS) { + for (String park : parkCols()) { globalRow.getParkAmounts().put(park, BigDecimal.ZERO); } globalRow.setSubtotal(BigDecimal.ZERO); @@ -201,7 +271,7 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { // 小计行 CanteenReportShareRowVO subtotalRow = new CanteenReportShareRowVO(); subtotalRow.setCostCenter("小计"); - for (String park : CanteenReportConstants.SHARE_PARKS) { + for (String park : parkCols()) { subtotalRow.getParkAmounts().put(park, nz(shareParkTotal.get(park)).setScale(SCALE, RoundingMode.HALF_UP)); } subtotalRow.setSubtotal(totalSub.setScale(SCALE, RoundingMode.HALF_UP)); @@ -214,6 +284,16 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { @Override public CanteenReportShareAuditVO supplierShareAudit(String bizMonth) { + beginParkCols(bizMonth); + try { + return supplierShareAudit0(bizMonth); + } + finally { + endParkCols(); + } + } + + private CanteenReportShareAuditVO supplierShareAudit0(String bizMonth) { List foods = loadFoods(bizMonth); List swipes = loadSwipes(bizMonth); Map parkFoodPool = new HashMap<>(); @@ -241,7 +321,7 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { } } Map parkAllocable = new LinkedHashMap<>(); - for (String park : CanteenReportConstants.SHARE_PARKS) { + for (String park : parkCols()) { parkAllocable.put(park, nz(parkFoodPool.get(park)).subtract(nz(parkBoxOther.get(park)))); } @@ -256,7 +336,7 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { List weightRows = new ArrayList<>(); Map shareParkTotal = new LinkedHashMap<>(); Map weightParkTotal = new LinkedHashMap<>(); - for (String park : CanteenReportConstants.SHARE_PARKS) { + for (String park : parkCols()) { shareParkTotal.put(park, BigDecimal.ZERO); weightParkTotal.put(park, BigDecimal.ZERO); } @@ -264,7 +344,7 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { CanteenReportShareWeightRowVO row = new CanteenReportShareWeightRowVO(); row.setCostCenter(center); boolean any = false; - for (String park : CanteenReportConstants.SHARE_PARKS) { + for (String park : parkCols()) { BigDecimal parkTotal = nz(parkSwipeTotal.get(park)); BigDecimal centerAmt = nz(centerParkSwipe.get(center + "|" + park)).setScale(SCALE, RoundingMode.HALF_UP); BigDecimal weight = BigDecimal.ZERO; @@ -291,7 +371,7 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { } CanteenReportShareWeightRowVO totalRow = new CanteenReportShareWeightRowVO(); totalRow.setCostCenter("合计(权重应≈1)"); - for (String park : CanteenReportConstants.SHARE_PARKS) { + for (String park : parkCols()) { 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)); @@ -299,7 +379,7 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { weightRows.add(totalRow); List parkChecks = new ArrayList<>(); - for (String park : CanteenReportConstants.SHARE_PARKS) { + for (String park : parkCols()) { 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); @@ -322,8 +402,18 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { @Override public List businessReport(String bizMonth) { + beginParkCols(bizMonth); + try { + return businessReport0(bizMonth); + } + finally { + endParkCols(); + } + } + + private List businessReport0(String bizMonth) { List swipes = loadSwipes(bizMonth); - List shares = supplierShareReport(bizMonth); + List shares = supplierShareReport0(bizMonth); List foods = loadFoods(bizMonth); Map> incomeByTypePark = new HashMap<>(); @@ -377,7 +467,7 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { } } Map foodOnlyYuan = emptyParkMap(); - for (String park : CanteenReportConstants.PARKS) { + for (String park : parkCols()) { foodOnlyYuan.put(park, nz(shareExpenseYuan.get(park)).subtract(nz(splitFromShareYuan.get(park)))); } @@ -418,20 +508,28 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { @Override public Map summary(String bizMonth) { - List swipes = loadSwipes(bizMonth); - List foods = loadFoods(bizMonth); - BigDecimal swipeSum = swipes.stream().map(s -> nz(s.getConsumeAmount())).reduce(BigDecimal.ZERO, BigDecimal::add); - BigDecimal foodSum = foods.stream().map(f -> nz(f.getSettleAmount())).reduce(BigDecimal.ZERO, BigDecimal::add); - Map map = new LinkedHashMap<>(); - map.put("bizMonth", bizMonth); - map.put("swipeCount", swipes.size()); - 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; + beginParkCols(bizMonth); + try { + List swipes = loadSwipes(bizMonth); + List foods = loadFoods(bizMonth); + BigDecimal swipeSum = swipes.stream().map(s -> nz(s.getConsumeAmount())).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal foodSum = foods.stream().map(f -> nz(f.getSettleAmount())).reduce(BigDecimal.ZERO, BigDecimal::add); + Map map = new LinkedHashMap<>(); + map.put("bizMonth", bizMonth); + map.put("swipeCount", swipes.size()); + map.put("swipeAmount", swipeSum); + map.put("foodCount", foods.size()); + map.put("foodAmount", foodSum); + CanteenReportShareAuditVO audit = supplierShareAudit0(bizMonth); + map.put("shareParkChecks", audit.getParkChecks()); + map.put("shareWeightRows", audit.getWeightRows()); + map.put("parks", new ArrayList<>(parkCols())); + map.put("shareParks", new ArrayList<>(parkCols())); + return map; + } + finally { + endParkCols(); + } } private static boolean isShareMetaRow(String costCenter) { @@ -468,7 +566,7 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { CanteenReportIncomeRowVO row = emptyIncomeRow(incomeType, center); BigDecimal sub = BigDecimal.ZERO; boolean any = false; - for (String park : CanteenReportConstants.PARKS) { + for (String park : parkCols()) { BigDecimal amt = nz(agg.get(incomeType + "|" + center + "|" + park)).setScale(SCALE, RoundingMode.HALF_UP); row.getParkAmounts().put(park, amt); sub = sub.add(amt); @@ -502,20 +600,20 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { return v == null ? BigDecimal.ZERO : v; } - private static CanteenReportIncomeRowVO emptyIncomeRow(String group, String center) { + private CanteenReportIncomeRowVO emptyIncomeRow(String group, String center) { CanteenReportIncomeRowVO row = new CanteenReportIncomeRowVO(); row.setGroupName(group); row.setCostCenter(center); - for (String park : CanteenReportConstants.PARKS) { + for (String park : parkCols()) { row.getParkAmounts().put(park, BigDecimal.ZERO); } row.setSubtotal(BigDecimal.ZERO); return row; } - private static Map emptyParkMap() { + private Map emptyParkMap() { Map map = new LinkedHashMap<>(); - for (String park : CanteenReportConstants.PARKS) { + for (String park : parkCols()) { map.put(park, BigDecimal.ZERO); } return map; @@ -530,12 +628,12 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { } } - private static CanteenReportBusinessRowVO toWanRow(String type, String category, Map yuanByPark) { + private CanteenReportBusinessRowVO toWanRow(String type, String category, Map yuanByPark) { CanteenReportBusinessRowVO row = new CanteenReportBusinessRowVO(); row.setType(type); row.setCategory(category); BigDecimal sub = BigDecimal.ZERO; - for (String park : CanteenReportConstants.PARKS) { + for (String park : parkCols()) { BigDecimal wan = nz(yuanByPark.get(park)).divide(WAN, SCALE, RoundingMode.HALF_UP); row.getParkAmounts().put(park, wan); sub = sub.add(wan); @@ -544,7 +642,7 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { return row; } - private static CanteenReportBusinessRowVO sumRows(String type, String category, List rows) { + private CanteenReportBusinessRowVO sumRows(String type, String category, List rows) { CanteenReportBusinessRowVO row = new CanteenReportBusinessRowVO(); row.setType(type); row.setCategory(category); @@ -573,13 +671,13 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { return row; } - private static CanteenReportBusinessRowVO subtractRow(String type, String category, + private CanteenReportBusinessRowVO subtractRow(String type, String category, CanteenReportBusinessRowVO a, CanteenReportBusinessRowVO b) { CanteenReportBusinessRowVO row = new CanteenReportBusinessRowVO(); row.setType(type); row.setCategory(category); BigDecimal sub = BigDecimal.ZERO; - for (String park : CanteenReportConstants.PARKS) { + for (String park : parkCols()) { BigDecimal v = nz(a.getParkAmounts().get(park)).subtract(nz(b.getParkAmounts().get(park))) .setScale(SCALE, RoundingMode.HALF_UP); row.getParkAmounts().put(park, v); @@ -589,12 +687,12 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { return row; } - private static CanteenReportBusinessRowVO negateRow(String type, String category, CanteenReportBusinessRowVO src) { + private CanteenReportBusinessRowVO negateRow(String type, String category, CanteenReportBusinessRowVO src) { CanteenReportBusinessRowVO row = new CanteenReportBusinessRowVO(); row.setType(type); row.setCategory(category); BigDecimal sub = BigDecimal.ZERO; - for (String park : CanteenReportConstants.PARKS) { + for (String park : parkCols()) { BigDecimal v = nz(src.getParkAmounts().get(park)).negate().setScale(SCALE, RoundingMode.HALF_UP); row.getParkAmounts().put(park, v); sub = sub.add(v); @@ -603,12 +701,12 @@ public class CanteenReportCalcServiceImpl implements CanteenReportCalcService { return row; } - private static CanteenReportBusinessRowVO ratioRow(String type, String category, + private CanteenReportBusinessRowVO ratioRow(String type, String category, CanteenReportBusinessRowVO num, CanteenReportBusinessRowVO den) { CanteenReportBusinessRowVO row = new CanteenReportBusinessRowVO(); row.setType(type); row.setCategory(category); - for (String park : CanteenReportConstants.PARKS) { + for (String park : parkCols()) { BigDecimal d = nz(den.getParkAmounts().get(park)); BigDecimal n = nz(num.getParkAmounts().get(park)); BigDecimal r = d.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportFaceImportServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportFaceImportServiceImpl.java index dd78aa6b5..53bd55f58 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportFaceImportServiceImpl.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportFaceImportServiceImpl.java @@ -5,7 +5,7 @@ import cn.hutool.core.io.FileUtil; 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.alibaba.excel.event.AnalysisEventListener; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.baomidou.mybatisplus.core.toolkit.Wrappers; @@ -24,9 +24,11 @@ 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.CanteenReportFaceExcelBinder; import com.bsd.pjcloud.canteen.report.CanteenReportFaceMapper; import com.bsd.pjcloud.canteen.service.CanteenReportFaceImportService; import com.bsd.pjcloud.canteen.service.CanteenReportPersonService; +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; @@ -61,17 +63,87 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl= DELETE_BATCH); + return total; + } + + private int deleteFaceDetailsByDateRange(String bizMonth, String dateStart, String dateEnd) { + int total = 0; + int n; + do { + n = faceDetailMapper.deletePhysicallyByDateRange(bizMonth, dateStart, dateEnd, DELETE_BATCH); + total += n; + } + while (n >= DELETE_BATCH); + return total; + } + + private int deleteFaceDetailsDateRangeExceptLog(String bizMonth, String dateStart, String dateEnd, Long importLogId) { + int total = 0; + int n; + do { + n = faceDetailMapper.deletePhysicallyDateRangeExceptLog(bizMonth, dateStart, dateEnd, importLogId, DELETE_BATCH); + total += n; + } + while (n >= DELETE_BATCH); + return total; + } + + private int deleteSwipeByImportLogId(Long importLogId) { + int total = 0; + int n; + do { + n = swipeMapper.deletePhysicallyByImportLogId(importLogId, DELETE_BATCH); + total += n; + } + while (n >= DELETE_BATCH); + return total; + } + + private int deleteSwipeFaceByBizMonth(String bizMonth) { + int total = 0; + int n; + do { + n = swipeMapper.deletePhysicallyFaceByBizMonth(bizMonth, DELETE_BATCH); + total += n; + } + while (n >= DELETE_BATCH); + return total; + } + + private int deleteSwipeFaceByBizMonthExceptParks(String bizMonth, List excludeParks) { + int total = 0; + int n; + do { + n = swipeMapper.deletePhysicallyFaceByBizMonthExceptParks(bizMonth, excludeParks, DELETE_BATCH); + total += n; + } + while (n >= DELETE_BATCH); + return total; + } @Override public Page pageLogs(Page page, String bizMonth, String status) { return page(page, Wrappers.lambdaQuery() .eq(StrUtil.isNotBlank(bizMonth), CanteenReportImportLogEntity::getBizMonth, bizMonth) .eq(StrUtil.isNotBlank(status), CanteenReportImportLogEntity::getStatus, status) + .orderByDesc(CanteenReportImportLogEntity::getBizMonth) .orderByDesc(CanteenReportImportLogEntity::getCreateTime)); } @@ -95,8 +167,8 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl orgLevel2s) { + boolean replaceFace, List orgLevel2s, String matchMode, List excludeParks) { if (StrUtil.isBlank(bizMonth) && importLogId == null) { return R.failed("请传入业务月份 bizMonth,或指定某一批导入日志"); } @@ -391,12 +465,28 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl settleParkMap = loadSettleParkMap(); - Map costCenterMap = personService.loadCostCenterMap(); + // 月份对月份:人员信息 / 花名册均按汇总归属月加载 + Map costCenterMap = useRoster + ? rosterService.loadHrScopeMap(month) + : personService.loadCostCenterMap(month); + Set excludeParkSet = new HashSet<>(); + if (CollUtil.isNotEmpty(excludeParks)) { + for (String p : excludeParks) { + if (StrUtil.isNotBlank(p)) { + excludeParkSet.add(p.trim()); + } + } + } List saveList = new ArrayList<>(aggRows.size()); Map missByKey = new LinkedHashMap<>(); int unmatchedPark = 0; int unmatchedPerson = 0; + int excludedParkRows = 0; for (CanteenReportFaceAggRow row : aggRows) { if (row.getAmount() == null || row.getAmount().compareTo(BigDecimal.ZERO) == 0) { continue; @@ -423,10 +513,10 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl 0 + ? String.format("汇总后可写入金额均为 0 或均被排除厂区过滤(排除 %d 条),未写入刷卡明细", excludedParkRows) + : "汇总后可写入金额均为 0,未写入刷卡明细"); } if (replaceFace) { if (importLogId != null) { - swipeMapper.deletePhysicallyByImportLogId(importLogId); + deleteSwipeByImportLogId(importLogId); + } + else if (CollUtil.isNotEmpty(excludeParkSet)) { + // 排除厂区:保留这些厂区已有 FACE,只覆盖其余 + deleteSwipeFaceByBizMonthExceptParks(month, new ArrayList<>(excludeParkSet)); } else { - swipeMapper.deletePhysicallyFaceByBizMonth(month); + deleteSwipeFaceByBizMonth(month); } } @@ -509,16 +609,21 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl 0) { - tip.append(";人员信息为空,员工刷卡核对成本已回退法律归属映射(B扫C等扫码客单不匹配人员),请先导入人员信息"); + tip.append(String.format(";当月%s为空,员工刷卡核对成本已回退法律归属映射(B扫C等扫码客单不匹配人员),请先导入当月%s", + matchLabel, matchLabel)); } else if (unmatchedPerson > 0) { - tip.append(String.format(";有 %d 条员工刷卡、%d 人未命中人员信息,已回退法律归属(B扫C等保持支付方式原样,可批量改为支付宝);刷卡明细备注「未命中人员信息」可筛选", - unmatchedPerson, missByKey.size())); + tip.append(String.format(";有 %d 条员工刷卡、%d 人未命中%s,已回退法律归属(B扫C等保持支付方式原样,可批量改为支付宝);刷卡明细备注「%s」可筛选", + unmatchedPerson, missByKey.size(), matchLabel, missRemark)); } if (unmatchedPark > 0) { tip.append(String.format(";有 %d 条结算台未配置厂区,请到「结算台厂区」维护后再重新汇总", unmatchedPark)); } + if (excludedParkRows > 0) { + tip.append(String.format(";已排除厂区数据 %d 条(保留原 FACE 不覆盖)", excludedParkRows)); + } CanteenReportAggResultVO result = new CanteenReportAggResultVO(); result.setSwipeRows(saveList.size()); result.setUnmatchedRows(unmatchedPerson); @@ -599,7 +704,7 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl { + static final class FaceDetailAccumulator extends AnalysisEventListener> { private static final int SKIP_SAMPLE_LIMIT = 80; @@ -622,6 +727,9 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl colFieldMap = Map.of(); FaceDetailAccumulator(Long importLogId, String bizMonthParam, String operator, Long tenantId, LocalDateTime now, CanteenReportFaceDetailMapper faceDetailMapper, int batchSize, @@ -641,10 +749,54 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl headMap, AnalysisContext context) { + if (headMap == null || headMap.isEmpty()) { + headError = "未读到表头,请确认第一行是列名"; + return; + } + Set matched = new HashSet<>(); + this.colFieldMap = CanteenReportFaceExcelBinder.buildColumnFieldMap(headMap, matched); + List missing = new ArrayList<>(); + if (!CanteenReportFaceExcelBinder.hasField(matched, "empNo")) { + missing.add("人员编号"); + } + if (!CanteenReportFaceExcelBinder.hasField(matched, "empName")) { + missing.add("姓名"); + } + if (!CanteenReportFaceExcelBinder.hasField(matched, "orderAmount")) { + missing.add("订单金额"); + } + if (!CanteenReportFaceExcelBinder.hasField(matched, "deviceName")) { + missing.add("交易设备"); + } + if (!CanteenReportFaceExcelBinder.hasField(matched, "orgLevel2")) { + missing.add("二级组织"); + } + if (!missing.isEmpty()) { + List rawHeads = new ArrayList<>(); + for (String h : headMap.values()) { + if (StrUtil.isNotBlank(h)) { + rawHeads.add(h.trim()); + } + } + headError = "表头缺少关键列(按名称匹配):" + String.join("、", missing) + + "。当前表头示例:" + String.join(" | ", rawHeads.subList(0, Math.min(12, rawHeads.size()))) + + (rawHeads.size() > 12 ? " …" : ""); + } + } + + @Override + public void invoke(Map row, AnalysisContext context) { + if (StrUtil.isNotBlank(headError)) { + return; + } totalRows++; int excelRow = context.readRowHolder() != null ? context.readRowHolder().getRowIndex() + 1 : totalRows + 1; + CanteenReportFaceExcel data = CanteenReportFaceExcelBinder.bind(colFieldMap, row); CanteenReportFaceDetailEntity e = CanteenReportFaceMapper.toDetail(data, bizMonthParam, importLogId); if (e == null) { recordSkip(excelRow, "空行(无订单号/姓名/人员编号/金额/时间)", data); @@ -787,5 +939,9 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl pageList(Page page, String bizMonth) { return page(page, Wrappers.lambdaQuery() .eq(StrUtil.isNotBlank(bizMonth), CanteenReportManualAdjustEntity::getBizMonth, bizMonth) + .orderByDesc(CanteenReportManualAdjustEntity::getBizMonth) .orderByDesc(CanteenReportManualAdjustEntity::getId)); } diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportPersonServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportPersonServiceImpl.java index 27b3ed93a..252c69219 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportPersonServiceImpl.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportPersonServiceImpl.java @@ -36,11 +36,14 @@ public class CanteenReportPersonServiceImpl extends ServiceImpl pageList(Page page, String empNo, String empName, String costCenter) { + public Page pageList(Page page, String bizMonth, String empNo, String empName, + String costCenter) { return page(page, Wrappers.lambdaQuery() + .eq(StrUtil.isNotBlank(bizMonth), CanteenReportPersonEntity::getBizMonth, bizMonth) .like(StrUtil.isNotBlank(empNo), CanteenReportPersonEntity::getEmpNo, empNo) .like(StrUtil.isNotBlank(empName), CanteenReportPersonEntity::getEmpName, empName) .like(StrUtil.isNotBlank(costCenter), CanteenReportPersonEntity::getCostCenter, costCenter) + .orderByDesc(CanteenReportPersonEntity::getBizMonth) .orderByAsc(CanteenReportPersonEntity::getEmpNo)); } @@ -73,10 +76,14 @@ public class CanteenReportPersonServiceImpl extends ServiceImpl rows, boolean replaceAll) { + public R importExcel(List rows, String bizMonth, boolean replaceMonth) { if (CollUtil.isEmpty(rows)) { return R.failed("导入数据为空"); } + String month = StrUtil.trim(bizMonth); + if (StrUtil.isBlank(month)) { + return R.failed("请选择适用月份"); + } Map byEmp = new LinkedHashMap<>(); int skipBlank = 0; for (CanteenReportPersonExcel row : rows) { @@ -91,7 +98,6 @@ public class CanteenReportPersonServiceImpl extends ServiceImpl saveList = new ArrayList<>(byEmp.size()); for (Map.Entry en : byEmp.entrySet()) { - saveList.add(toEntity(en.getValue(), en.getKey(), null, operator, tenantId, now, true)); + saveList.add(toEntity(en.getValue(), month, en.getKey(), null, operator, tenantId, now, true)); } for (List chunk : CollUtil.split(saveList, BATCH)) { baseMapper.insertBatch(chunk); } - return R.ok(null, String.format("全量覆盖导入成功:入库 %d 人,跳过无工号 %d 行", saveList.size(), skipBlank)); + return R.ok(null, String.format("覆盖 %s 导入成功:入库 %d 人,跳过无工号 %d 行", month, saveList.size(), skipBlank)); } Map existMap = list(Wrappers.lambdaQuery() + .eq(CanteenReportPersonEntity::getBizMonth, month) .in(CanteenReportPersonEntity::getEmpNo, byEmp.keySet())).stream() .collect(Collectors.toMap(CanteenReportPersonEntity::getEmpNo, Function.identity(), (a, b) -> a)); @@ -128,11 +135,11 @@ public class CanteenReportPersonServiceImpl extends ServiceImpl en : byEmp.entrySet()) { CanteenReportPersonEntity exist = existMap.get(en.getKey()); if (exist == null) { - insertList.add(toEntity(en.getValue(), en.getKey(), null, operator, tenantId, now, true)); + insertList.add(toEntity(en.getValue(), month, en.getKey(), null, operator, tenantId, now, true)); } else { - CanteenReportPersonEntity upd = toEntity(en.getValue(), en.getKey(), exist.getId(), operator, tenantId, - now, false); + CanteenReportPersonEntity upd = toEntity(en.getValue(), month, en.getKey(), exist.getId(), operator, + tenantId, now, false); upd.setCreateBy(exist.getCreateBy()); upd.setCreateTime(exist.getCreateTime()); upd.setDelFlag(exist.getDelFlag()); @@ -146,13 +153,15 @@ public class CanteenReportPersonServiceImpl extends ServiceImpl exportExcel() { + public List exportExcel(String bizMonth) { List list = list(Wrappers.lambdaQuery() + .eq(StrUtil.isNotBlank(bizMonth), CanteenReportPersonEntity::getBizMonth, bizMonth) + .orderByDesc(CanteenReportPersonEntity::getBizMonth) .orderByAsc(CanteenReportPersonEntity::getEmpNo)); List result = new ArrayList<>(list.size()); for (CanteenReportPersonEntity e : list) { @@ -171,9 +180,24 @@ public class CanteenReportPersonServiceImpl extends ServiceImpl loadCostCenterMap() { + @Transactional(rollbackFor = Exception.class) + public R clearByMonth(String bizMonth) { + String month = StrUtil.trim(bizMonth); + if (StrUtil.isBlank(month)) { + return R.failed("请选择适用月份"); + } + baseMapper.deletePhysicallyByBizMonth(month); + return R.ok(null, "已清空 " + month + " 人员信息"); + } + + @Override + public Map loadCostCenterMap(String bizMonth) { Map map = new HashMap<>(); + if (StrUtil.isBlank(bizMonth)) { + return map; + } List list = list(Wrappers.lambdaQuery() + .eq(CanteenReportPersonEntity::getBizMonth, bizMonth.trim()) .select(CanteenReportPersonEntity::getEmpNo, CanteenReportPersonEntity::getCostCenter)); for (CanteenReportPersonEntity e : list) { if (StrUtil.isBlank(e.getEmpNo())) { @@ -186,10 +210,11 @@ public class CanteenReportPersonServiceImpl extends ServiceImpllambdaQuery() + .eq(CanteenReportPersonEntity::getBizMonth, entity.getBizMonth()) .eq(CanteenReportPersonEntity::getEmpNo, entity.getEmpNo()) .ne(excludeId != null, CanteenReportPersonEntity::getId, excludeId)); if (cnt > 0) { - return R.failed("工号已存在:" + entity.getEmpNo()); + return R.failed(String.format("该月工号已存在:%s / %s", entity.getBizMonth(), entity.getEmpNo())); } return null; } diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportRosterServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportRosterServiceImpl.java index 18d2df464..6fcd08f8b 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportRosterServiceImpl.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportRosterServiceImpl.java @@ -41,6 +41,7 @@ public class CanteenReportRosterServiceImpl extends ServiceImpl implements CanteenReportSettleParkService { @@ -31,10 +33,6 @@ public class CanteenReportSettleParkServiceImpl extends ServiceImpllambdaQuery() .eq(CanteenReportSettleParkEntity::getSettleCounter, entity.getSettleCounter()) .ne(entity.getId() != null, CanteenReportSettleParkEntity::getId, entity.getId())); @@ -44,4 +42,23 @@ public class CanteenReportSettleParkServiceImpl extends ServiceImpl listConfiguredParks() { + List list = list(Wrappers.lambdaQuery() + .select(CanteenReportSettleParkEntity::getPark) + .isNotNull(CanteenReportSettleParkEntity::getPark) + .ne(CanteenReportSettleParkEntity::getPark, "") + .orderByAsc(CanteenReportSettleParkEntity::getPark)); + java.util.LinkedHashSet set = new java.util.LinkedHashSet<>(); + for (String p : CanteenReportConstants.PARKS) { + set.add(p); + } + for (CanteenReportSettleParkEntity e : list) { + if (StrUtil.isNotBlank(e.getPark())) { + set.add(e.getPark().trim()); + } + } + return new java.util.ArrayList<>(set); + } } diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportSwipeServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportSwipeServiceImpl.java index 0a088007d..d66f45be6 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportSwipeServiceImpl.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportSwipeServiceImpl.java @@ -58,6 +58,7 @@ public class CanteenReportSwipeServiceImpl extends ServiceImpl + - DELETE FROM canteen_report_face_detail WHERE import_log_id = #{importLogId} + DELETE /*+ QUERY_TIMEOUT(120000000) */ FROM canteen_report_face_detail + WHERE import_log_id = #{importLogId} + LIMIT #{limit} - DELETE FROM canteen_report_face_detail + DELETE /*+ QUERY_TIMEOUT(120000000) */ FROM canteen_report_face_detail WHERE biz_month = #{bizMonth} AND biz_date >= #{dateStart} AND biz_date <= #{dateEnd} + LIMIT #{limit} - DELETE FROM canteen_report_face_detail + DELETE /*+ QUERY_TIMEOUT(120000000) */ FROM canteen_report_face_detail WHERE biz_month = #{bizMonth} AND biz_date >= #{dateStart} AND biz_date <= #{dateEnd} AND (import_log_id IS NULL OR import_log_id <> #{importLogId}) + LIMIT #{limit}