From 42dbce5e8f4901a11f2cd4d247b904157f34ac2b Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Thu, 20 Aug 2026 17:32:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=8F=E5=AE=9E=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/report/CanteenReportPersonExcel.java | 33 ++ .../CanteenReportSwipeBatchUpdateDTO.java | 41 ++ .../dto/report/CanteenReportSwipeExcel.java | 26 +- .../entity/CanteenReportPersonEntity.java | 61 +++ .../canteen/excel/ExcelYuanConverter.java | 87 +++-- .../report/CanteenReportConstants.java | 5 +- .../report/CanteenReportFaceMapper.java | 20 +- .../CanteenReportFaceImportController.java | 13 +- .../CanteenReportPersonController.java | 91 +++++ .../CanteenReportSwipeController.java | 22 ++ .../mapper/CanteenReportFaceDetailMapper.java | 2 +- .../mapper/CanteenReportPersonMapper.java | 16 + .../CanteenReportFaceImportService.java | 15 +- .../service/CanteenReportPersonService.java | 31 ++ .../service/CanteenReportSwipeService.java | 6 + .../CanteenReportFaceImportServiceImpl.java | 162 ++++---- .../impl/CanteenReportPersonServiceImpl.java | 289 ++++++++++++++ .../impl/CanteenReportSwipeServiceImpl.java | 143 ++++++- .../canteen/util/CanteenReportExcelUtils.java | 362 +++++++++++++++++- .../mapper/CanteenReportFaceDetailMapper.xml | 12 +- .../mapper/CanteenReportPersonMapper.xml | 23 ++ 21 files changed, 1314 insertions(+), 146 deletions(-) create mode 100644 pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportPersonExcel.java create mode 100644 pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportSwipeBatchUpdateDTO.java create mode 100644 pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenReportPersonEntity.java create mode 100644 pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportPersonController.java create mode 100644 pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenReportPersonMapper.java create mode 100644 pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportPersonService.java create mode 100644 pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenReportPersonServiceImpl.java create mode 100644 pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenReportPersonMapper.xml diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportPersonExcel.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportPersonExcel.java new file mode 100644 index 000000000..18d2e5136 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportPersonExcel.java @@ -0,0 +1,33 @@ +package com.bsd.pjcloud.canteen.dto.report; + +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +/** + * 人员信息导入:与「刷卡明细」sheet 列序一致,按工号去重取核对成本。 + */ +@Data +public class CanteenReportPersonExcel { + + /** 仅按列序,避免表头名匹配异常 */ + @ExcelProperty(index = 0) + private String empNo; + + @ExcelProperty(index = 1) + private String empName; + + @ExcelProperty(index = 2) + private String legalEntity; + + @ExcelProperty(index = 3) + private String deptName; + + @ExcelProperty(index = 8) + private String costCenter; + + @ExcelProperty(index = 9) + private String costCategory; + + @ExcelProperty(index = 10) + private String park; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportSwipeBatchUpdateDTO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportSwipeBatchUpdateDTO.java new file mode 100644 index 000000000..9a98bd507 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportSwipeBatchUpdateDTO.java @@ -0,0 +1,41 @@ +package com.bsd.pjcloud.canteen.dto.report; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * 刷卡明细批量修改:仅更新勾选/传入的字段。 + */ +@Data +@Schema(description = "刷卡明细批量修改") +public class CanteenReportSwipeBatchUpdateDTO { + + @Schema(description = "主键列表") + private List ids; + + @Schema(description = "是否修改收入类别") + private Boolean updateIncomeType; + + @Schema(description = "收入类别") + private String incomeType; + + @Schema(description = "是否修改厂区") + private Boolean updatePark; + + @Schema(description = "厂区") + private String park; + + @Schema(description = "是否修改核对成本") + private Boolean updateCostCenter; + + @Schema(description = "核对成本") + private String costCenter; + + @Schema(description = "是否修改法律归属") + private Boolean updateLegalEntity; + + @Schema(description = "法律归属") + private String legalEntity; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportSwipeExcel.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportSwipeExcel.java index ce14ea4ff..42e65ebf2 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportSwipeExcel.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/report/CanteenReportSwipeExcel.java @@ -6,44 +6,44 @@ import lombok.Data; import java.math.BigDecimal; /** - * 刷卡明细导入:按列序对齐原表「刷卡明细」sheet,兼容整本工作簿。 + * 刷卡明细导入:仅按列序对齐「刷卡明细」sheet(避免表头文案差异导致金额列错位为空)。 */ @Data public class CanteenReportSwipeExcel { - @ExcelProperty(index = 0, value = "工号") + @ExcelProperty(index = 0) private String empNo; - @ExcelProperty(index = 1, value = "姓名") + @ExcelProperty(index = 1) private String empName; - @ExcelProperty(index = 2, value = "法律归属") + @ExcelProperty(index = 2) private String legalEntity; - @ExcelProperty(index = 3, value = "部门") + @ExcelProperty(index = 3) private String deptName; - @ExcelProperty(index = 4, value = "消费区域") + @ExcelProperty(index = 4) private String consumeArea; - @ExcelProperty(index = 5, value = "结算台") + @ExcelProperty(index = 5) private String settleCounter; - @ExcelProperty(index = 6, value = "消费金额", converter = com.bsd.pjcloud.canteen.excel.ExcelYuanConverter.class) + @ExcelProperty(index = 6, converter = com.bsd.pjcloud.canteen.excel.ExcelYuanConverter.class) private BigDecimal consumeAmount; - @ExcelProperty(index = 7, value = "收入类别") + @ExcelProperty(index = 7) private String incomeType; - @ExcelProperty(index = 8, value = "核对成本(开票公司)") + @ExcelProperty(index = 8) private String costCenter; - @ExcelProperty(index = 9, value = "成本分类") + @ExcelProperty(index = 9) private String costCategory; - @ExcelProperty(index = 10, value = "厂区") + @ExcelProperty(index = 10) private String park; - @ExcelProperty(index = 11, value = "备注标识") + @ExcelProperty(index = 11) private String remarkTag; } 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 new file mode 100644 index 000000000..3f39de2cd --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenReportPersonEntity.java @@ -0,0 +1,61 @@ +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_person") +@EqualsAndHashCode(callSuper = true) +@Schema(description = "食堂报表-人员信息") +public class CanteenReportPersonEntity extends Model { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + @Schema(description = "工号") + private String empNo; + + @Schema(description = "姓名") + private String empName; + + @Schema(description = "法律归属") + private String legalEntity; + + @Schema(description = "部门") + private String deptName; + + @Schema(description = "核对成本(开票公司)") + private String costCenter; + + @Schema(description = "成本分类") + private String costCategory; + + @Schema(description = "厂区") + private String park; + + @Schema(description = "备注") + private String remark; + + @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; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/excel/ExcelYuanConverter.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/excel/ExcelYuanConverter.java index d62e7d894..168eb7a0a 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/excel/ExcelYuanConverter.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/excel/ExcelYuanConverter.java @@ -1,6 +1,8 @@ package com.bsd.pjcloud.canteen.excel; import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.converters.ReadConverterContext; +import com.alibaba.excel.converters.WriteConverterContext; import com.alibaba.excel.enums.CellDataTypeEnum; import com.alibaba.excel.metadata.GlobalConfiguration; import com.alibaba.excel.metadata.data.ReadCellData; @@ -10,8 +12,7 @@ import com.alibaba.excel.metadata.property.ExcelContentProperty; import java.math.BigDecimal; /** - * Excel 金额(元):兼容数字单元格与文本单元格。 - *

字段上指定本 converter 后,按单元格实际类型解析,不依赖 supportExcelTypeKey 匹配。 + * Excel 金额(元):兼容数字 / 文本 / 公式缓存值(EasyExcel 4 走 ReadConverterContext)。 */ public class ExcelYuanConverter implements Converter { @@ -22,39 +23,30 @@ public class ExcelYuanConverter implements Converter { @Override public CellDataTypeEnum supportExcelTypeKey() { - // 注册表用;字段注解指定本类时仍会直接调用 convertToJavaData return CellDataTypeEnum.NUMBER; } + @Override + public BigDecimal convertToJavaData(ReadConverterContext context) { + if (context == null || context.getReadCellData() == null) { + return null; + } + return parse(context.getReadCellData()); + } + @Override public BigDecimal convertToJavaData(ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { - if (cellData == null || cellData.getType() == null || cellData.getType() == CellDataTypeEnum.EMPTY) { - return null; - } - if (cellData.getType() == CellDataTypeEnum.NUMBER && cellData.getNumberValue() != null) { - return cellData.getNumberValue(); - } - if (cellData.getType() == CellDataTypeEnum.BOOLEAN && cellData.getBooleanValue() != null) { - return cellData.getBooleanValue() ? BigDecimal.ONE : BigDecimal.ZERO; - } - String raw = cellData.getStringValue(); - if ((raw == null || raw.trim().isEmpty()) && cellData.getNumberValue() != null) { - return cellData.getNumberValue(); - } - if (raw == null || raw.trim().isEmpty()) { - return null; - } - String n = raw.trim().replace(",", "").replace("¥", "").replace("¥", "").replace("元", ""); - if (n.isEmpty()) { - return null; - } - try { - return new BigDecimal(n); - } - catch (NumberFormatException ex) { - return null; + return parse(cellData); + } + + @Override + public WriteCellData convertToExcelData(WriteConverterContext context) { + BigDecimal value = context == null ? null : context.getValue(); + if (value == null) { + return new WriteCellData<>(""); } + return new WriteCellData<>(value.stripTrailingZeros().toPlainString()); } @Override @@ -65,4 +57,43 @@ public class ExcelYuanConverter implements Converter { } return new WriteCellData<>(value.stripTrailingZeros().toPlainString()); } + + public static BigDecimal parseYuan(String raw) { + if (raw == null) { + return null; + } + String n = raw.trim() + .replace(",", "") + .replace(",", "") + .replace("¥", "") + .replace("¥", "") + .replace("元", "") + .replace(" ", "") + .replace("\u00A0", ""); + if (n.isEmpty() || "-".equals(n) || "--".equals(n) || "/".equals(n) || "#N/A".equalsIgnoreCase(n)) { + return null; + } + if (n.startsWith("=")) { + n = n.substring(1).trim(); + } + try { + return new BigDecimal(n); + } + catch (NumberFormatException ex) { + return null; + } + } + + private static BigDecimal parse(ReadCellData cellData) { + if (cellData == null) { + return null; + } + if (cellData.getNumberValue() != null) { + return cellData.getNumberValue(); + } + if (cellData.getType() == CellDataTypeEnum.BOOLEAN && cellData.getBooleanValue() != null) { + return cellData.getBooleanValue() ? BigDecimal.ONE : BigDecimal.ZERO; + } + return parseYuan(cellData.getStringValue()); + } } 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 7a8b5c6a4..7440a5529 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 @@ -30,9 +30,12 @@ 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 String REMARK_PERSON_MISS = "未命中人员信息"; + 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/CanteenReportFaceMapper.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/report/CanteenReportFaceMapper.java index 788aad421..127ab917e 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/report/CanteenReportFaceMapper.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/report/CanteenReportFaceMapper.java @@ -117,15 +117,27 @@ public final class CanteenReportFaceMapper { return e; } - /** 汇总用:B扫C 等 → 支付宝;其余 → 员工刷卡 */ + /** + * 汇总用收入类别: + * - B扫C/收钱吧/支付宝/微信/扫码 → 保持支付方式原样(不自动归「支付宝」) + * - 刷脸支付等其余 → 「员工刷卡」 + */ public static String mapIncomeType(String payMethod, String payType) { - String m = nvl(payMethod) + " " + nvl(payType); - if (containsAny(m, "B扫C", "收钱吧", "支付宝", "微信", "扫码")) { - return CanteenReportConstants.INCOME_ALIPAY; + String raw = !isBlank(payMethod) ? payMethod.trim() : (!isBlank(payType) ? payType.trim() : ""); + if (isScanOrGuestPay(raw) || isScanOrGuestPay(nvl(payMethod) + " " + nvl(payType))) { + return isBlank(raw) ? "未知支付方式" : raw; } return CanteenReportConstants.INCOME_SWIPE; } + /** B扫C/收钱吧等扫码客单:汇总时不匹配人员信息 */ + public static boolean isScanOrGuestPay(String incomeOrPay) { + if (isBlank(incomeOrPay)) { + return false; + } + return containsAny(incomeOrPay, "B扫C", "收钱吧", "支付宝", "微信", "扫码"); + } + /** 汇总用:法律归属 → 核对成本 */ public static String mapCostCenter(String legalEntity) { if (isBlank(legalEntity)) { 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 7d788ccbd..2086fd33a 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 @@ -91,18 +91,22 @@ public class CanteenReportFaceImportController { } @SysLog("人脸消费Excel导入明细") - @Operation(summary = "流式导入人脸Excel到逐笔明细表") + @Operation(summary = "流式导入人脸Excel到逐笔明细表;orgLevel2s 非空时只入库所选二级组织") @PostMapping("/import") public R importExcel(@RequestParam("file") MultipartFile file, @RequestParam(required = false) String bizMonth, @RequestParam(required = false) String dateStart, @RequestParam(required = false) String dateEnd, - @RequestParam(defaultValue = "true") boolean replaceRange) { - return faceImportService.importFaceExcel(file, bizMonth, dateStart, dateEnd, replaceRange); + @RequestParam(defaultValue = "true") boolean replaceRange, + @RequestParam(required = false) String orgLevel2s) { + java.util.List orgs = cn.hutool.core.util.StrUtil.isBlank(orgLevel2s) + ? java.util.List.of() + : cn.hutool.core.util.StrUtil.splitTrim(orgLevel2s, ','); + return faceImportService.importFaceExcel(file, bizMonth, dateStart, dateEnd, replaceRange, orgs); } @SysLog("人脸明细汇总到刷卡明细") - @Operation(summary = "统计汇总写入刷卡明细(B扫C→支付宝)") + @Operation(summary = "统计汇总写入刷卡明细(支付方式原样作收入类别;B扫C等不自动归支付宝)") @PostMapping("/aggregate") public R aggregate(@RequestParam(required = false) String bizMonth, @RequestParam(required = false) String dateStart, @@ -119,6 +123,7 @@ public class CanteenReportFaceImportController { return R.failed("importLogId 非法: " + importLogId); } } + // 兼容旧参数;地区已在导入时过滤,汇总默认全量 java.util.List orgs = cn.hutool.core.util.StrUtil.isBlank(orgLevel2s) ? java.util.List.of() : cn.hutool.core.util.StrUtil.splitTrim(orgLevel2s, ','); 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 new file mode 100644 index 000000000..45a88c83e --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportPersonController.java @@ -0,0 +1,91 @@ +package com.bsd.pjcloud.canteen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.bsd.pjcloud.canteen.dto.report.CanteenReportPersonExcel; +import com.bsd.pjcloud.canteen.entity.CanteenReportPersonEntity; +import com.bsd.pjcloud.canteen.service.CanteenReportPersonService; +import com.bsd.pjcloud.canteen.util.CanteenReportExcelUtils; +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.common.excel.annotation.ResponseExcel; +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; + +import java.util.Arrays; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/canteenReport/person") +@Tag(description = "canteenReportPerson", name = "食堂报表-人员信息(演示)") +@SecurityRequirement(name = HttpHeaders.AUTHORIZATION) +public class CanteenReportPersonController { + + private final CanteenReportPersonService personService; + + @Operation(summary = "分页") + @GetMapping("/page") + public R page(@ParameterObject Page page, + @RequestParam(required = false) String empNo, + @RequestParam(required = false) String empName, + @RequestParam(required = false) String costCenter) { + return R.ok(personService.pageList(page, empNo, empName, costCenter)); + } + + @Operation(summary = "详情") + @GetMapping("/{id}") + public R get(@PathVariable Long id) { + return R.ok(personService.getById(id)); + } + + @SysLog("新增人员信息") + @Operation(summary = "新增") + @PostMapping + public R add(@RequestBody CanteenReportPersonEntity entity) { + return personService.savePerson(entity); + } + + @SysLog("修改人员信息") + @Operation(summary = "修改") + @PutMapping + public R update(@RequestBody CanteenReportPersonEntity entity) { + return personService.updatePerson(entity); + } + + @SysLog("删除人员信息") + @Operation(summary = "删除") + @DeleteMapping + public R remove(@RequestBody Long[] ids) { + return R.ok(personService.removeBatchByIds(Arrays.asList(ids))); + } + + @SysLog("清空全部人员信息") + @Operation(summary = "清空全部") + @DeleteMapping("/all") + public R clearAll() { + return personService.clearAll(); + } + + @SysLog("导入人员信息") + @Operation(summary = "导入Excel:replaceAll=true 全量覆盖;false 新增导入(同工号更新)") + @PostMapping("/import") + public R importExcel(@RequestParam("file") MultipartFile file, + @RequestParam(defaultValue = "true") boolean replaceAll) throws Exception { + List rows = CanteenReportExcelUtils.readSheet(file, + CanteenReportPersonExcel.class, "刷卡明细"); + return personService.importExcel(rows, replaceAll); + } + + @ResponseExcel + @Operation(summary = "导出") + @GetMapping("/export") + public List export() { + return personService.exportExcel(); + } +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportSwipeController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportSwipeController.java index 32b6c25ee..ccd5a2f19 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportSwipeController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenReportSwipeController.java @@ -2,6 +2,7 @@ package com.bsd.pjcloud.canteen.controller; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.bsd.pjcloud.canteen.dto.report.CanteenReportSwipeBatchUpdateDTO; import com.bsd.pjcloud.canteen.dto.report.CanteenReportSwipeExcel; import com.bsd.pjcloud.canteen.entity.CanteenReportSwipeEntity; import com.bsd.pjcloud.canteen.service.CanteenReportSwipeService; @@ -65,6 +66,27 @@ public class CanteenReportSwipeController { return R.ok(swipeService.getById(id)); } + @SysLog("新增刷卡明细") + @Operation(summary = "新增") + @PostMapping + public R add(@RequestBody CanteenReportSwipeEntity entity) { + return swipeService.saveOrUpdateRow(entity); + } + + @SysLog("修改刷卡明细") + @Operation(summary = "修改") + @PutMapping + public R update(@RequestBody CanteenReportSwipeEntity entity) { + return swipeService.saveOrUpdateRow(entity); + } + + @SysLog("批量修改刷卡明细") + @Operation(summary = "批量修改收入类别/厂区/核对成本/法律归属") + @PutMapping("/batch") + public R batchUpdate(@RequestBody CanteenReportSwipeBatchUpdateDTO dto) { + return swipeService.batchUpdate(dto); + } + @SysLog("删除刷卡明细") @Operation(summary = "删除") @DeleteMapping 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 c457998a5..23ef847a2 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 @@ -24,7 +24,7 @@ public interface CanteenReportFaceDetailMapper extends BaseMapper { + + int deletePhysicallyAll(); + + int insertBatch(@Param("list") List list); +} 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 ccce683c9..1b8857ec3 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 @@ -15,18 +15,15 @@ public interface CanteenReportFaceImportService extends IService orgLevel2s); /** - * 将明细汇总写入刷卡明细。B扫C→支付宝,其余→员工刷卡;厂区按结算台配置。 - * - * @param bizMonth 业务月 - * @param dateStart 可选日期段 - * @param dateEnd 可选日期段 - * @param importLogId 可选,仅汇总某一批导入 - * @param replaceFace 是否先清空该月已有 FACE 汇总刷卡行 - * @param orgLevel2s 可选二级组织,非空时只汇总这些组织(如嘉兴地区、湖州地区) + * 将明细汇总写入刷卡明细。刷脸等→员工刷卡;B扫C等扫码保持支付方式原样(不自动归支付宝,可批量改); + * 扫码客单不匹配人员只配厂区;员工刷卡用工号匹配核对成本。厂区按结算台。入库时已按地区过滤,汇总默认全量。 */ R aggregateToSwipe(String bizMonth, String dateStart, String dateEnd, Long importLogId, boolean replaceFace, List orgLevel2s); 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 new file mode 100644 index 000000000..27d83e1d4 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportPersonService.java @@ -0,0 +1,31 @@ +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.CanteenReportPersonExcel; +import com.bsd.pjcloud.canteen.entity.CanteenReportPersonEntity; +import com.bsd.pjcloud.common.core.util.R; + +import java.util.List; +import java.util.Map; + +public interface CanteenReportPersonService extends IService { + + Page pageList(Page page, String empNo, String empName, String costCenter); + + R savePerson(CanteenReportPersonEntity entity); + + R updatePerson(CanteenReportPersonEntity entity); + + /** + * @param replaceAll true=全量覆盖;false=新增导入(同工号则更新) + */ + R importExcel(List rows, boolean replaceAll); + + List exportExcel(); + + R clearAll(); + + /** empNo → costCenter(空核对成本也占位) */ + Map loadCostCenterMap(); +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportSwipeService.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportSwipeService.java index 3afdbe8d6..56af7a880 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportSwipeService.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenReportSwipeService.java @@ -2,6 +2,7 @@ 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.CanteenReportSwipeBatchUpdateDTO; import com.bsd.pjcloud.canteen.dto.report.CanteenReportSwipeExcel; import com.bsd.pjcloud.canteen.entity.CanteenReportSwipeEntity; import com.bsd.pjcloud.common.core.util.R; @@ -20,6 +21,11 @@ public interface CanteenReportSwipeService extends IService sumAmount(String bizMonth, String park, String incomeType, String costCenter, String empNo, Long importLogId, String remarkTag, String source); + R saveOrUpdateRow(CanteenReportSwipeEntity entity); + + /** 批量修改收入类别 / 厂区 / 核对成本 / 法律归属 */ + R batchUpdate(CanteenReportSwipeBatchUpdateDTO dto); + R importExcel(List rows, String bizMonth, boolean replaceMonth, BindingResult bindingResult); List exportExcel(String bizMonth); 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 3dba9cb28..d851f9c73 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 @@ -11,7 +11,6 @@ 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; @@ -27,14 +26,13 @@ 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.canteen.service.CanteenReportPersonService; 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 lombok.extern.slf4j.Slf4j; -import org.springframework.mock.web.MockMultipartFile; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -47,9 +45,11 @@ import java.nio.file.Files; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; /** * 人脸消费:Excel → 逐笔明细表;再统计汇总 → 刷卡明细。 @@ -65,8 +65,7 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl pageLogs(Page page, String bizMonth, String status) { @@ -130,7 +129,7 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl orgLevel2s) { if (file == null || file.isEmpty()) { return R.failed("请上传 Excel 文件"); } @@ -144,6 +143,7 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl orgs = normalizeOrgLevel2s(orgLevel2s); CanteenReportImportLogEntity logEntity = new CanteenReportImportLogEntity(); logEntity.setId(IdWorker.getId()); @@ -182,11 +182,11 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl orgFilter = orgs; Thread worker = new Thread(() -> { TenantContextHolder.setTenantId(tenantId); try { - doFaceImportAsync(tempFile, fileName, logEntity, month, ds, de, replace, operator, tenantId, t0); + doFaceImportAsync(tempFile, logEntity, month, ds, de, replace, orgFilter, operator, tenantId, t0); } catch (Throwable e) { log.error("人脸明细异步导入失败 logId={}", logEntity.getId(), e); @@ -210,7 +210,8 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl orgLevel2s, String operator, + Long tenantId, long t0) throws Exception { + // 暂不存档 Excel,加快导入;列表也不再提供 Excel 下载 boolean preReplace = replaceRange && StrUtil.isNotBlank(bizMonth) && StrUtil.isNotBlank(dateStart) && StrUtil.isNotBlank(dateEnd); @@ -237,7 +238,7 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl settleParkMap = loadSettleParkMap(); - Map hrScopeMap = rosterService.loadHrScopeMap(month); + Map costCenterMap = personService.loadCostCenterMap(); List saveList = new ArrayList<>(aggRows.size()); Map missByKey = new LinkedHashMap<>(); int unmatchedPark = 0; - int unmatchedRoster = 0; + int unmatchedPerson = 0; for (CanteenReportFaceAggRow row : aggRows) { if (row.getAmount() == null || row.getAmount().compareTo(BigDecimal.ZERO) == 0) { continue; @@ -375,41 +381,45 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl 0) { + tip.append(";人员信息为空,员工刷卡核对成本已回退法律归属映射(B扫C等扫码客单不匹配人员),请先导入人员信息"); } - else if (unmatchedRoster > 0) { - tip.append(String.format(";有 %d 条、%d 人未命中花名册,已回退法律归属,刷卡明细备注「未命中花名册」可筛选", - unmatchedRoster, missByKey.size())); + else if (unmatchedPerson > 0) { + tip.append(String.format(";有 %d 条员工刷卡、%d 人未命中人员信息,已回退法律归属(B扫C等保持支付方式原样,可批量改为支付宝);刷卡明细备注「未命中人员信息」可筛选", + unmatchedPerson, 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.setUnmatchedRows(unmatchedPerson); result.setUnmatchedPeople(missByKey.size()); result.setUnmatched(new ArrayList<>(missByKey.values())); return R.ok(result, String.format("汇总成功:写入刷卡明细 %d 条,耗时 %d 秒%s", @@ -530,29 +537,6 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl data = (Map) upload.getData(); - Object url = data.get("url"); - Object fn = data.get("fileName"); - if (url != null) { - logEntity.setFileUrl(String.valueOf(url)); - } - if (fn != null) { - logEntity.setOssFileName(String.valueOf(fn)); - } - updateById(logEntity); - } - } - catch (Exception e) { - log.warn("人脸 Excel 存档失败,继续导入: {}", e.getMessage()); - } - } - private void failLog(CanteenReportImportLogEntity logEntity, String msg, long t0) { logEntity.setStatus("FAILED"); logEntity.setErrorMsg(msg); @@ -594,6 +578,8 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl orgLevel2Filter; private final List buffer = new ArrayList<>(); private final Map skipReasonCount = new LinkedHashMap<>(); @@ -606,7 +592,8 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl orgLevel2s) { this.importLogId = importLogId; this.bizMonthParam = bizMonthParam; this.operator = operator; @@ -614,6 +601,12 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl(orgLevel2s); + } } @Override @@ -629,6 +622,13 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl + implements CanteenReportPersonService { + + private static final int BATCH = 800; + + @Override + public Page pageList(Page page, String empNo, String empName, String costCenter) { + return page(page, Wrappers.lambdaQuery() + .like(StrUtil.isNotBlank(empNo), CanteenReportPersonEntity::getEmpNo, empNo) + .like(StrUtil.isNotBlank(empName), CanteenReportPersonEntity::getEmpName, empName) + .like(StrUtil.isNotBlank(costCenter), CanteenReportPersonEntity::getCostCenter, costCenter) + .orderByAsc(CanteenReportPersonEntity::getEmpNo)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R savePerson(CanteenReportPersonEntity entity) { + R check = validatePerson(entity, null); + if (check != null) { + return check; + } + fillMeta(entity, true); + save(entity); + return R.ok(null, "新增成功"); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R updatePerson(CanteenReportPersonEntity entity) { + if (entity.getId() == null) { + return R.failed("缺少主键"); + } + R check = validatePerson(entity, entity.getId()); + if (check != null) { + return check; + } + fillMeta(entity, false); + updateById(entity); + return R.ok(null, "修改成功"); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R importExcel(List rows, boolean replaceAll) { + if (CollUtil.isEmpty(rows)) { + return R.failed("导入数据为空"); + } + Map byEmp = new LinkedHashMap<>(); + int skipBlank = 0; + for (CanteenReportPersonExcel row : rows) { + String empNo = normalizeEmpNo(row.getEmpNo()); + if (empNo == null) { + skipBlank++; + continue; + } + row.setEmpNo(empNo); + CanteenReportPersonExcel old = byEmp.get(empNo); + if (old == null) { + byEmp.put(empNo, row); + continue; + } + // 同工号多行:优先保留有核对成本的 + if (StrUtil.isBlank(old.getCostCenter()) && StrUtil.isNotBlank(row.getCostCenter())) { + byEmp.put(empNo, row); + } + else if (StrUtil.isNotBlank(row.getCostCenter())) { + byEmp.put(empNo, row); + } + } + if (byEmp.isEmpty()) { + return R.failed(String.format("有效数据为空:共 %d 行均无工号,已全部剔除", skipBlank)); + } + + String operator = currentUser(); + Long tenantId = TenantContextHolder.getTenantId(); + LocalDateTime now = LocalDateTime.now(); + + if (replaceAll) { + baseMapper.deletePhysicallyAll(); + List saveList = new ArrayList<>(byEmp.size()); + for (Map.Entry en : byEmp.entrySet()) { + saveList.add(toEntity(en.getValue(), 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)); + } + + Map existMap = list(Wrappers.lambdaQuery() + .in(CanteenReportPersonEntity::getEmpNo, byEmp.keySet())).stream() + .collect(Collectors.toMap(CanteenReportPersonEntity::getEmpNo, Function.identity(), (a, b) -> a)); + + List insertList = new ArrayList<>(); + List updateList = new ArrayList<>(); + for (Map.Entry en : byEmp.entrySet()) { + CanteenReportPersonEntity exist = existMap.get(en.getKey()); + if (exist == null) { + insertList.add(toEntity(en.getValue(), en.getKey(), null, operator, tenantId, now, true)); + } + else { + CanteenReportPersonEntity upd = toEntity(en.getValue(), en.getKey(), exist.getId(), operator, tenantId, + now, false); + upd.setCreateBy(exist.getCreateBy()); + upd.setCreateTime(exist.getCreateTime()); + upd.setDelFlag(exist.getDelFlag()); + upd.setTenantId(exist.getTenantId()); + updateList.add(upd); + } + } + for (List chunk : CollUtil.split(insertList, BATCH)) { + baseMapper.insertBatch(chunk); + } + for (CanteenReportPersonEntity e : updateList) { + updateById(e); + } + return R.ok(null, String.format("新增导入成功:新增 %d 人,更新 %d 人,跳过无工号 %d 行", + insertList.size(), updateList.size(), skipBlank)); + } + + @Override + public List exportExcel() { + List list = list(Wrappers.lambdaQuery() + .orderByAsc(CanteenReportPersonEntity::getEmpNo)); + List result = new ArrayList<>(list.size()); + for (CanteenReportPersonEntity e : list) { + CanteenReportPersonExcel x = new CanteenReportPersonExcel(); + BeanUtils.copyProperties(e, x); + result.add(x); + } + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R clearAll() { + baseMapper.deletePhysicallyAll(); + return R.ok(null, "已清空全部人员信息"); + } + + @Override + public Map loadCostCenterMap() { + Map map = new HashMap<>(); + List list = list(Wrappers.lambdaQuery() + .select(CanteenReportPersonEntity::getEmpNo, CanteenReportPersonEntity::getCostCenter)); + for (CanteenReportPersonEntity e : list) { + if (StrUtil.isBlank(e.getEmpNo())) { + continue; + } + String empNo = e.getEmpNo().trim(); + String cc = StrUtil.trim(e.getCostCenter()); + map.putIfAbsent(empNo, cc == null ? "" : cc); + } + return map; + } + + private CanteenReportPersonEntity toEntity(CanteenReportPersonExcel row, String empNo, Long id, String operator, + Long tenantId, LocalDateTime now, boolean isNew) { + CanteenReportPersonEntity e = new CanteenReportPersonEntity(); + e.setId(id != null ? id : IdWorker.getId()); + e.setEmpNo(empNo); + e.setEmpName(StrUtil.trim(row.getEmpName())); + e.setLegalEntity(StrUtil.trim(row.getLegalEntity())); + e.setDeptName(StrUtil.trim(row.getDeptName())); + e.setCostCenter(StrUtil.trim(row.getCostCenter())); + e.setCostCategory(StrUtil.trim(row.getCostCategory())); + e.setPark(StrUtil.trim(row.getPark())); + e.setUpdateBy(operator); + e.setUpdateTime(now); + if (isNew) { + e.setCreateBy(operator); + e.setCreateTime(now); + e.setDelFlag(CommonConstants.STATUS_NORMAL); + e.setTenantId(tenantId); + } + return e; + } + + /** 新增/编辑:工号、姓名、成本分类、厂区必填 */ + private R validatePerson(CanteenReportPersonEntity entity, Long excludeId) { + if (entity == null) { + return R.failed("参数为空"); + } + String empNo = normalizeEmpNo(entity.getEmpNo()); + if (empNo == null) { + return R.failed("请填写有效工号"); + } + entity.setEmpNo(empNo); + if (StrUtil.isBlank(entity.getEmpName())) { + return R.failed("请填写姓名"); + } + if (StrUtil.isBlank(entity.getCostCategory())) { + return R.failed("请填写成本分类"); + } + if (StrUtil.isBlank(entity.getPark())) { + return R.failed("请填写厂区"); + } + entity.setEmpName(entity.getEmpName().trim()); + entity.setCostCategory(entity.getCostCategory().trim()); + entity.setPark(entity.getPark().trim()); + long cnt = count(Wrappers.lambdaQuery() + .eq(CanteenReportPersonEntity::getEmpNo, entity.getEmpNo()) + .ne(excludeId != null, CanteenReportPersonEntity::getId, excludeId)); + if (cnt > 0) { + return R.failed("工号已存在:" + entity.getEmpNo()); + } + return null; + } + + private static String normalizeEmpNo(String raw) { + if (raw == null) { + return null; + } + String empNo = raw.trim(); + if (empNo.isEmpty()) { + return null; + } + if (empNo.matches("^\\d+\\.0+$")) { + empNo = empNo.substring(0, empNo.indexOf('.')); + } + String lower = empNo.toLowerCase(); + if ("-".equals(empNo) || "--".equals(empNo) || "/".equals(empNo) || "\\".equals(empNo) + || "无".equals(empNo) || "无工号".equals(empNo) || "null".equals(lower) || "n/a".equals(lower) + || "#n/a".equals(lower) || "#".equals(empNo)) { + return null; + } + return empNo; + } + + private void fillMeta(CanteenReportPersonEntity entity, boolean isNew) { + LocalDateTime now = LocalDateTime.now(); + String user = currentUser(); + if (isNew) { + if (entity.getId() == null) { + entity.setId(IdWorker.getId()); + } + entity.setCreateBy(user); + entity.setCreateTime(now); + entity.setDelFlag(CommonConstants.STATUS_NORMAL); + entity.setTenantId(TenantContextHolder.getTenantId()); + } + entity.setUpdateBy(user); + entity.setUpdateTime(now); + } + + private static String currentUser() { + try { + if (SecurityUtils.getUser() != null) { + return SecurityUtils.getUser().getUsername(); + } + } + catch (Exception ignored) { + // ignore + } + return null; + } +} 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 72e88bfd1..0b49dc0b0 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 @@ -7,10 +7,12 @@ 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.CanteenReportSwipeBatchUpdateDTO; import com.bsd.pjcloud.canteen.dto.report.CanteenReportSwipeExcel; 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.report.CanteenReportFaceMapper; import com.bsd.pjcloud.canteen.service.CanteenReportSwipeService; import com.bsd.pjcloud.common.core.constant.CommonConstants; import com.bsd.pjcloud.common.core.util.R; @@ -88,6 +90,142 @@ public class CanteenReportSwipeServiceImpl extends ServiceImpl rows, String bizMonth, boolean replaceMonth, @@ -151,7 +289,10 @@ public class CanteenReportSwipeServiceImpl extends ServiceImpl chunk : CollUtil.split(saveList, BATCH_SIZE)) { baseMapper.insertBatch(chunk); } - return R.ok(null, "导入成功,共 " + saveList.size() + " 条"); + long withAmt = saveList.stream() + .filter(e -> e.getConsumeAmount() != null && e.getConsumeAmount().compareTo(BigDecimal.ZERO) != 0) + .count(); + return R.ok(null, String.format("导入成功,共 %d 条,其中金额非 0 共 %d 条", saveList.size(), withAmt)); } private void fillInsertMeta(List list) { diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/util/CanteenReportExcelUtils.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/util/CanteenReportExcelUtils.java index df0160a80..ead538bc1 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/util/CanteenReportExcelUtils.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/util/CanteenReportExcelUtils.java @@ -2,6 +2,7 @@ package com.bsd.pjcloud.canteen.util; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.ExcelReader; +import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.read.metadata.ReadSheet; import com.bsd.pjcloud.canteen.excel.ExcelYuanConverter; import com.bsd.pjcloud.canteen.excel.ExcelYuanStringConverter; @@ -12,6 +13,7 @@ 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 lombok.extern.slf4j.Slf4j; import org.apache.poi.ss.usermodel.DataFormatter; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; @@ -20,17 +22,43 @@ import org.apache.poi.ss.usermodel.WorkbookFactory; import org.springframework.web.multipart.MultipartFile; import java.io.ByteArrayInputStream; +import java.lang.reflect.Field; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** - * 报表演示:按 sheet 名读取 Excel(支持整本多 sheet 工作簿) + * 报表演示:按 sheet 名读取 Excel(支持整本多 sheet 工作簿)。 + *

+ * 兼容 WPS/Excel 另存的 CSV/TSV(常见 GBK):避免「整行塞进第一列 + 中文乱码」。 */ +@Slf4j @UtilityClass public class CanteenReportExcelUtils { @SuppressWarnings("unchecked") public static List readSheet(MultipartFile file, Class clazz, String preferredSheetName) throws Exception { byte[] bytes = file.getBytes(); + String filename = file.getOriginalFilename(); + + if (isTextTable(filename, bytes)) { + Charset charset = detectTextCharset(bytes); + char delimiter = detectDelimiter(bytes, charset); + log.info("报表导入按文本表解析 file={} charset={} delimiter={}", filename, charset.name(), + delimiter == '\t' ? "TAB" : String.valueOf(delimiter)); + List rows = readDelimitedText(bytes, clazz, charset, delimiter); + expandTabJoinedRows(rows); + return rows; + } + + // 刷卡明细:POI 按列序读,避免 EasyExcel 金额转换丢数 + if (clazz == com.bsd.pjcloud.canteen.dto.report.CanteenReportSwipeExcel.class) { + return (List) readSwipeByPoi(bytes, preferredSheetName); + } + int sheetNo = resolveSheetNo(bytes, preferredSheetName); DefaultAnalysisEventListener listener = new DefaultAnalysisEventListener(); EasyExcel.read(new ByteArrayInputStream(bytes), clazz, listener) @@ -44,7 +72,109 @@ public class CanteenReportExcelUtils { .sheet(sheetNo) .headRowNumber(1) .doRead(); - return (List) listener.getList(); + List rows = (List) listener.getList(); + expandTabJoinedRows(rows); + return rows; + } + + /** + * 刷卡明细专用:POI + DataFormatter 按列序读取(金额在第 7 列 index=6)。 + */ + public static List readSwipeByPoi(byte[] bytes, + String preferredSheetName) throws Exception { + List result = new ArrayList<>(); + try (Workbook wb = WorkbookFactory.create(new ByteArrayInputStream(bytes))) { + Sheet sheet = resolveSheet(wb, preferredSheetName); + if (sheet == null) { + return result; + } + DataFormatter fmt = new DataFormatter(); + int last = sheet.getLastRowNum(); + int amountNonNull = 0; + for (int r = 1; r <= last; r++) { + Row row = sheet.getRow(r); + if (row == null) { + continue; + } + com.bsd.pjcloud.canteen.dto.report.CanteenReportSwipeExcel x = new com.bsd.pjcloud.canteen.dto.report.CanteenReportSwipeExcel(); + x.setEmpNo(cellStr(fmt, row, 0)); + x.setEmpName(cellStr(fmt, row, 1)); + x.setLegalEntity(cellStr(fmt, row, 2)); + x.setDeptName(cellStr(fmt, row, 3)); + x.setConsumeArea(cellStr(fmt, row, 4)); + x.setSettleCounter(cellStr(fmt, row, 5)); + java.math.BigDecimal amt = ExcelYuanConverter.parseYuan(cellStr(fmt, row, 6)); + // 数字单元格 DataFormatter 偶发空串时,再直接取 numeric + if (amt == null && row.getCell(6) != null) { + try { + if (row.getCell(6).getCellType() == org.apache.poi.ss.usermodel.CellType.NUMERIC + || row.getCell(6).getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) { + amt = java.math.BigDecimal.valueOf(row.getCell(6).getNumericCellValue()); + } + } + catch (Exception ignored) { + // ignore + } + } + x.setConsumeAmount(amt); + x.setIncomeType(cellStr(fmt, row, 7)); + x.setCostCenter(cellStr(fmt, row, 8)); + x.setCostCategory(cellStr(fmt, row, 9)); + x.setPark(cellStr(fmt, row, 10)); + x.setRemarkTag(cellStr(fmt, row, 11)); + if (isBlankRow(x)) { + continue; + } + if (amt != null) { + amountNonNull++; + } + result.add(x); + } + log.info("刷卡明细 POI 读取完成 sheet={} rows={} amountNonNull={}", sheet.getSheetName(), result.size(), + amountNonNull); + } + return result; + } + + private static Sheet resolveSheet(Workbook wb, String preferredSheetName) { + if (wb == null || wb.getNumberOfSheets() <= 0) { + return null; + } + if (preferredSheetName != null && !preferredSheetName.isEmpty()) { + for (int i = 0; i < wb.getNumberOfSheets(); i++) { + Sheet s = wb.getSheetAt(i); + if (s != null && preferredSheetName.equals(s.getSheetName())) { + return s; + } + } + for (int i = 0; i < wb.getNumberOfSheets(); i++) { + Sheet s = wb.getSheetAt(i); + if (s != null && s.getSheetName() != null && s.getSheetName().contains(preferredSheetName)) { + return s; + } + } + } + return wb.getSheetAt(0); + } + + private static String cellStr(DataFormatter fmt, Row row, int col) { + if (row == null) { + return null; + } + String v = fmt.formatCellValue(row.getCell(col)); + if (v == null) { + return null; + } + v = v.trim(); + return v.isEmpty() ? null : v; + } + + private static boolean isBlankRow(com.bsd.pjcloud.canteen.dto.report.CanteenReportSwipeExcel x) { + return (x.getEmpNo() == null || x.getEmpNo().isBlank()) + && x.getConsumeAmount() == null + && (x.getIncomeType() == null || x.getIncomeType().isBlank()) + && (x.getEmpName() == null || x.getEmpName().isBlank()) + && (x.getCostCenter() == null || x.getCostCenter().isBlank()); } /** @@ -111,4 +241,232 @@ public class CanteenReportExcelUtils { } return 0; } + + /** 扩展名或内容像 CSV/TSV(非 xlsx/xls 二进制) */ + private static boolean isTextTable(String filename, byte[] bytes) { + if (filename != null) { + String lower = filename.toLowerCase(); + if (lower.endsWith(".csv") || lower.endsWith(".tsv") || lower.endsWith(".txt")) { + return true; + } + } + if (bytes == null || bytes.length < 4) { + return false; + } + // xlsx / zip + if (bytes[0] == 'P' && bytes[1] == 'K') { + return false; + } + // ole2 xls + if ((bytes[0] & 0xFF) == 0xD0 && (bytes[1] & 0xFF) == 0xCF && (bytes[2] & 0xFF) == 0x11 + && (bytes[3] & 0xFF) == 0xE0) { + return false; + } + // 明文且首行含分隔符 + 表头关键字 + Charset cs = detectTextCharset(bytes); + String head = peekHead(bytes, cs, 800); + boolean hasSep = head.indexOf('\t') >= 0 || countChar(head, ',') >= 3; + boolean hasHint = head.contains("工号") || head.contains("姓名") || head.contains("法律归属") + || head.contains("核对成本"); + return hasSep && hasHint; + } + + private static Charset detectTextCharset(byte[] bytes) { + if (bytes.length >= 3 && (bytes[0] & 0xFF) == 0xEF && (bytes[1] & 0xFF) == 0xBB && (bytes[2] & 0xFF) == 0xBF) { + return StandardCharsets.UTF_8; + } + Charset gbk = Charset.forName("GB18030"); + String asUtf8 = new String(bytes, StandardCharsets.UTF_8); + String asGbk = new String(bytes, gbk); + int utfScore = textScore(asUtf8); + int gbkScore = textScore(asGbk); + // UTF-8 误读 GBK 常出现大量 + if (asUtf8.indexOf('\uFFFD') >= 0) { + return gbk; + } + return gbkScore > utfScore ? gbk : StandardCharsets.UTF_8; + } + + private static int textScore(String s) { + int score = 0; + if (s.contains("工号")) { + score += 5; + } + if (s.contains("姓名")) { + score += 5; + } + if (s.contains("法律归属")) { + score += 5; + } + if (s.contains("核对成本")) { + score += 5; + } + if (s.contains("厂区")) { + score += 2; + } + if (s.indexOf('\uFFFD') >= 0) { + score -= 20; + } + return score; + } + + private static char detectDelimiter(byte[] bytes, Charset charset) { + String head = peekHead(bytes, charset, 1200); + String firstLine = head.split("\\r?\\n", 2)[0]; + int tabs = countChar(firstLine, '\t'); + int commas = countChar(firstLine, ','); + return tabs >= commas ? '\t' : ','; + } + + private static String peekHead(byte[] bytes, Charset charset, int maxChars) { + String all = new String(bytes, 0, Math.min(bytes.length, maxChars * 3), charset); + return all.length() > maxChars ? all.substring(0, maxChars) : all; + } + + private static int countChar(String s, char c) { + int n = 0; + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == c) { + n++; + } + } + return n; + } + + private static List readDelimitedText(byte[] bytes, Class clazz, Charset charset, char delimiter) + throws Exception { + String text = new String(bytes, charset); + if (text.startsWith("\uFEFF")) { + text = text.substring(1); + } + String[] lines = text.split("\\r?\\n"); + if (lines.length <= 1) { + return new ArrayList<>(); + } + Map indexFields = excelIndexFields(clazz); + List result = new ArrayList<>(Math.max(16, lines.length - 1)); + for (int i = 1; i < lines.length; i++) { + String line = lines[i]; + if (line == null || line.isBlank()) { + continue; + } + String[] cols = splitLine(line, delimiter); + T row = clazz.getDeclaredConstructor().newInstance(); + for (Map.Entry en : indexFields.entrySet()) { + int idx = en.getKey(); + if (idx < 0 || idx >= cols.length) { + continue; + } + Field f = en.getValue(); + String v = cols[idx]; + if (v != null) { + v = v.trim(); + if (v.startsWith("\"") && v.endsWith("\"") && v.length() >= 2) { + v = v.substring(1, v.length() - 1).replace("\"\"", "\""); + } + if (v.isEmpty()) { + v = null; + } + } + if (f.getType() == String.class) { + f.set(row, v); + } + else if (f.getType() == java.math.BigDecimal.class) { + f.set(row, ExcelYuanConverter.parseYuan(v)); + } + } + result.add(row); + } + return result; + } + + private static String[] splitLine(String line, char delimiter) { + if (delimiter == '\t') { + return line.split("\t", -1); + } + // 简易 CSV:支持引号包裹 + List parts = new ArrayList<>(); + StringBuilder cur = new StringBuilder(); + boolean inQuote = false; + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + if (c == '"') { + inQuote = !inQuote; + cur.append(c); + continue; + } + if (c == delimiter && !inQuote) { + parts.add(cur.toString()); + cur.setLength(0); + continue; + } + cur.append(c); + } + parts.add(cur.toString()); + return parts.toArray(new String[0]); + } + + private static Map excelIndexFields(Class clazz) { + Map map = new LinkedHashMap<>(); + for (Field f : clazz.getDeclaredFields()) { + ExcelProperty prop = f.getAnnotation(ExcelProperty.class); + if (prop == null) { + continue; + } + int idx = prop.index(); + if (idx < 0) { + continue; + } + f.setAccessible(true); + map.put(idx, f); + } + return map; + } + + /** + * 防御:若第 0 列实际是「整行 Tab 粘在一起」,按列序拆开写回带 @ExcelProperty(index) 的字段。 + */ + @SuppressWarnings("unchecked") + private static void expandTabJoinedRows(List rows) { + if (rows == null || rows.isEmpty()) { + return; + } + Class clazz = rows.get(0).getClass(); + Map indexFields = excelIndexFields(clazz); + Field col0 = indexFields.get(0); + if (col0 == null || col0.getType() != String.class) { + return; + } + int fixed = 0; + for (T row : rows) { + try { + String first = (String) col0.get(row); + if (first == null || first.indexOf('\t') < 0) { + continue; + } + String[] cols = first.split("\t", -1); + if (cols.length < 2) { + continue; + } + for (Map.Entry en : indexFields.entrySet()) { + int idx = en.getKey(); + Field f = en.getValue(); + if (f.getType() != String.class) { + continue; + } + String v = idx < cols.length ? cols[idx] : null; + f.set(row, v == null ? null : v.trim()); + } + fixed++; + } + catch (Exception ignored) { + // ignore single row + } + } + if (fixed > 0) { + log.warn("报表导入检测到 {} 行「整行粘在第一列」,已按 Tab 拆列;建议改用真正的 .xlsx 或按 GBK/UTF-8 分隔正确的 CSV", + fixed); + } + } + } diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenReportFaceDetailMapper.xml b/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenReportFaceDetailMapper.xml index 87a34f74f..840a04407 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenReportFaceDetailMapper.xml +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenReportFaceDetailMapper.xml @@ -65,9 +65,13 @@ OR IFNULL(pay_method, '') LIKE '%收钱吧%' OR IFNULL(pay_method, '') LIKE '%支付宝%' OR IFNULL(pay_method, '') LIKE '%微信%' + OR IFNULL(pay_method, '') LIKE '%扫码%' OR IFNULL(pay_type, '') LIKE '%收钱吧%' OR IFNULL(pay_type, '') LIKE '%支付宝%' - THEN '支付宝' + OR IFNULL(pay_type, '') LIKE '%B扫C%' + OR IFNULL(pay_type, '') LIKE '%微信%' + OR IFNULL(pay_type, '') LIKE '%扫码%' + THEN IFNULL(NULLIF(TRIM(pay_method), ''), IFNULL(NULLIF(TRIM(pay_type), ''), '未知支付方式')) ELSE '员工刷卡' END AS incomeType, SUM(IFNULL(order_amount, 0)) AS amount @@ -104,9 +108,13 @@ OR IFNULL(pay_method, '') LIKE '%收钱吧%' OR IFNULL(pay_method, '') LIKE '%支付宝%' OR IFNULL(pay_method, '') LIKE '%微信%' + OR IFNULL(pay_method, '') LIKE '%扫码%' OR IFNULL(pay_type, '') LIKE '%收钱吧%' OR IFNULL(pay_type, '') LIKE '%支付宝%' - THEN '支付宝' + OR IFNULL(pay_type, '') LIKE '%B扫C%' + OR IFNULL(pay_type, '') LIKE '%微信%' + OR IFNULL(pay_type, '') LIKE '%扫码%' + THEN IFNULL(NULLIF(TRIM(pay_method), ''), IFNULL(NULLIF(TRIM(pay_type), ''), '未知支付方式')) ELSE '员工刷卡' END diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenReportPersonMapper.xml b/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenReportPersonMapper.xml new file mode 100644 index 000000000..0e0d75aa6 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenReportPersonMapper.xml @@ -0,0 +1,23 @@ + + + + + + DELETE FROM canteen_report_person + + + + INSERT INTO canteen_report_person ( + id, emp_no, emp_name, legal_entity, dept_name, + cost_center, cost_category, park, remark, + create_by, create_time, update_by, update_time, del_flag, tenant_id + ) VALUES + + ( + #{e.id}, #{e.empNo}, #{e.empName}, #{e.legalEntity}, #{e.deptName}, + #{e.costCenter}, #{e.costCategory}, #{e.park}, #{e.remark}, + #{e.createBy}, #{e.createTime}, #{e.updateBy}, #{e.updateTime}, #{e.delFlag}, #{e.tenantId} + ) + + +