敏实报表
This commit is contained in:
1 parent
96f495d555
commit
42dbce5e8f
21 files changed
+1314
-146
No files matched your search
+33
@@ -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;
|
||||
}
|
||||
+41
@@ -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<Long> 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;
|
||||
}
|
||||
+13
-13
@@ -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;
|
||||
}
|
||||
+61
@@ -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<CanteenReportPersonEntity> {
|
||||
|
||||
@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;
|
||||
}
|
||||
+59
-28
@@ -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 金额(元):兼容数字单元格与文本单元格。
|
||||
* <p>字段上指定本 converter 后,按单元格实际类型解析,不依赖 supportExcelTypeKey 匹配。
|
||||
* Excel 金额(元):兼容数字 / 文本 / 公式缓存值(EasyExcel 4 走 ReadConverterContext)。
|
||||
*/
|
||||
public class ExcelYuanConverter implements Converter<BigDecimal> {
|
||||
|
||||
@@ -22,39 +23,30 @@ public class ExcelYuanConverter implements Converter<BigDecimal> {
|
||||
|
||||
@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<BigDecimal> 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<BigDecimal> {
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -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<String> INCOME_TYPES = Collections.unmodifiableList(Arrays.asList(
|
||||
INCOME_SWIPE, INCOME_ALIPAY, INCOME_BOX, INCOME_OTHER, INCOME_PROFIT_ADJ, INCOME_INVOICE, INCOME_BAD_DEBT
|
||||
));
|
||||
|
||||
+16
-4
@@ -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)) {
|
||||
|
||||
+9
-4
@@ -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<String> 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<String> orgs = cn.hutool.core.util.StrUtil.isBlank(orgLevel2s)
|
||||
? java.util.List.of()
|
||||
: cn.hutool.core.util.StrUtil.splitTrim(orgLevel2s, ',');
|
||||
|
||||
+91
@@ -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<CanteenReportPersonExcel> rows = CanteenReportExcelUtils.readSheet(file,
|
||||
CanteenReportPersonExcel.class, "刷卡明细");
|
||||
return personService.importExcel(rows, replaceAll);
|
||||
}
|
||||
|
||||
@ResponseExcel
|
||||
@Operation(summary = "导出")
|
||||
@GetMapping("/export")
|
||||
public List<CanteenReportPersonExcel> export() {
|
||||
return personService.exportExcel();
|
||||
}
|
||||
}
|
||||
+22
@@ -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
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ public interface CanteenReportFaceDetailMapper extends BaseMapper<CanteenReportF
|
||||
@Param("importLogId") Long importLogId);
|
||||
|
||||
/**
|
||||
* 按人+维度汇总;B扫C 等归入支付宝,其余归入员工刷卡。金额用订单金额。
|
||||
* 按人+维度汇总;刷脸等→员工刷卡,B扫C等扫码保持支付方式原样。金额用订单金额。
|
||||
* 传入 importLogId 时只按批次汇总,忽略日期段。
|
||||
* orgLevel2s 非空时只汇总这些二级组织。
|
||||
*/
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.bsd.pjcloud.canteen.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenReportPersonEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface CanteenReportPersonMapper extends BaseMapper<CanteenReportPersonEntity> {
|
||||
|
||||
int deletePhysicallyAll();
|
||||
|
||||
int insertBatch(@Param("list") List<CanteenReportPersonEntity> list);
|
||||
}
|
||||
+6
-9
@@ -15,18 +15,15 @@ public interface CanteenReportFaceImportService extends IService<CanteenReportIm
|
||||
|
||||
/**
|
||||
* 流式导入人脸 Excel → 逐笔明细表(不直接写刷卡明细)。
|
||||
*
|
||||
* @param orgLevel2s 非空时只入库这些二级组织(如嘉兴地区、湖州地区),其它行跳过
|
||||
*/
|
||||
R importFaceExcel(MultipartFile file, String bizMonth, String dateStart, String dateEnd, boolean replaceRange);
|
||||
R importFaceExcel(MultipartFile file, String bizMonth, String dateStart, String dateEnd, boolean replaceRange,
|
||||
List<String> 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<String> orgLevel2s);
|
||||
|
||||
+31
@@ -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<CanteenReportPersonEntity> {
|
||||
|
||||
Page<CanteenReportPersonEntity> pageList(Page page, String empNo, String empName, String costCenter);
|
||||
|
||||
R savePerson(CanteenReportPersonEntity entity);
|
||||
|
||||
R updatePerson(CanteenReportPersonEntity entity);
|
||||
|
||||
/**
|
||||
* @param replaceAll true=全量覆盖;false=新增导入(同工号则更新)
|
||||
*/
|
||||
R importExcel(List<CanteenReportPersonExcel> rows, boolean replaceAll);
|
||||
|
||||
List<CanteenReportPersonExcel> exportExcel();
|
||||
|
||||
R clearAll();
|
||||
|
||||
/** empNo → costCenter(空核对成本也占位) */
|
||||
Map<String, String> loadCostCenterMap();
|
||||
}
|
||||
+6
@@ -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<CanteenReportSwipeEn
|
||||
Map<String, Object> 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<CanteenReportSwipeExcel> rows, String bizMonth, boolean replaceMonth, BindingResult bindingResult);
|
||||
|
||||
List<CanteenReportSwipeExcel> exportExcel(String bizMonth);
|
||||
|
||||
+81
-81
@@ -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<CanteenRepor
|
||||
private final CanteenReportFaceDetailMapper faceDetailMapper;
|
||||
private final CanteenReportSwipeMapper swipeMapper;
|
||||
private final CanteenReportSettleParkMapper settleParkMapper;
|
||||
private final CanteenReportRosterService rosterService;
|
||||
private final RemoteFileService remoteFileService;
|
||||
private final CanteenReportPersonService personService;
|
||||
|
||||
@Override
|
||||
public Page<CanteenReportImportLogEntity> pageLogs(Page page, String bizMonth, String status) {
|
||||
@@ -130,7 +129,7 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
|
||||
|
||||
@Override
|
||||
public R importFaceExcel(MultipartFile file, String bizMonth, String dateStart, String dateEnd,
|
||||
boolean replaceRange) {
|
||||
boolean replaceRange, List<String> orgLevel2s) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
return R.failed("请上传 Excel 文件");
|
||||
}
|
||||
@@ -144,6 +143,7 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
|
||||
String operator = currentUser();
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
List<String> orgs = normalizeOrgLevel2s(orgLevel2s);
|
||||
|
||||
CanteenReportImportLogEntity logEntity = new CanteenReportImportLogEntity();
|
||||
logEntity.setId(IdWorker.getId());
|
||||
@@ -182,11 +182,11 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
|
||||
final String ds = dateStart;
|
||||
final String de = dateEnd;
|
||||
final boolean replace = replaceRange;
|
||||
final String fileName = originalName;
|
||||
final List<String> 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<CanteenRepor
|
||||
worker.start();
|
||||
|
||||
String eta = estimateImportMinutes(fileBytes);
|
||||
return R.ok(logEntity, "已提交导入,后台正在处理。请在下方导入列表查看状态(导入中→成功/失败)。" + eta
|
||||
String orgTip = CollUtil.isEmpty(orgs) ? "全部二级组织" : ("仅 " + String.join("、", orgs));
|
||||
return R.ok(logEntity, "已提交导入(" + orgTip + "),后台正在处理。请在下方导入列表查看状态。" + eta
|
||||
+ "完成后请再点「统计汇总到刷卡明细」。");
|
||||
}
|
||||
|
||||
@@ -225,10 +226,10 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
|
||||
return String.format("当前文件约 %.0f MB,预计约 %d~%d 分钟。", mb, low, high);
|
||||
}
|
||||
|
||||
private void doFaceImportAsync(File temp, String originalName, CanteenReportImportLogEntity logEntity,
|
||||
String bizMonth, String dateStart, String dateEnd, boolean replaceRange, String operator, Long tenantId,
|
||||
long t0) throws Exception {
|
||||
archiveExcel(temp, originalName, logEntity);
|
||||
private void doFaceImportAsync(File temp, CanteenReportImportLogEntity logEntity, String bizMonth,
|
||||
String dateStart, String dateEnd, boolean replaceRange, List<String> 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<CanteenRepor
|
||||
}
|
||||
|
||||
FaceDetailAccumulator acc = new FaceDetailAccumulator(logEntity.getId(), bizMonth, operator, tenantId,
|
||||
LocalDateTime.now(), faceDetailMapper, BATCH_SIZE);
|
||||
LocalDateTime.now(), faceDetailMapper, BATCH_SIZE, orgLevel2s);
|
||||
try (InputStream in = new BufferedInputStream(new FileInputStream(temp), 1 << 20)) {
|
||||
EasyExcel.read(in, CanteenReportFaceExcel.class, acc)
|
||||
.sheet()
|
||||
@@ -270,7 +271,12 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
|
||||
logEntity.setTotalRows(acc.getTotalRows());
|
||||
logEntity.setSuccessRows(acc.getSuccessRows());
|
||||
logEntity.setSkipRows(acc.getSkipRows());
|
||||
logEntity.setSkipSummary(acc.buildSkipSummary());
|
||||
String skip = acc.buildSkipSummary();
|
||||
if (CollUtil.isNotEmpty(orgLevel2s)) {
|
||||
String orgPrefix = "导入地区过滤:" + String.join("、", orgLevel2s) + "\n";
|
||||
skip = StrUtil.isBlank(skip) ? orgPrefix : orgPrefix + skip;
|
||||
}
|
||||
logEntity.setSkipSummary(skip);
|
||||
logEntity.setStatus("SUCCESS");
|
||||
logEntity.setAggStatus("NONE");
|
||||
logEntity.setCostMs(System.currentTimeMillis() - t0);
|
||||
@@ -354,11 +360,11 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
|
||||
}
|
||||
|
||||
Map<String, String> settleParkMap = loadSettleParkMap();
|
||||
Map<String, String> hrScopeMap = rosterService.loadHrScopeMap(month);
|
||||
Map<String, String> costCenterMap = personService.loadCostCenterMap();
|
||||
List<CanteenReportSwipeEntity> saveList = new ArrayList<>(aggRows.size());
|
||||
Map<String, CanteenReportRosterMissVO> 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<CanteenRepor
|
||||
e.setSettleCounter(emptyToNull(row.getDeviceName()));
|
||||
e.setConsumeAmount(row.getAmount());
|
||||
e.setIncomeType(row.getIncomeType());
|
||||
// 扫码客单(B扫C 等)无工号:不匹配人员,只配厂区;收入类别保持支付方式原样,后续可批量改成「支付宝」等
|
||||
boolean skipPerson = CanteenReportFaceMapper.isScanOrGuestPay(row.getIncomeType());
|
||||
String empNo = StrUtil.trim(row.getEmpNo());
|
||||
String costCenter = null;
|
||||
String missReason = null;
|
||||
if (StrUtil.isBlank(empNo)) {
|
||||
missReason = "无工号";
|
||||
}
|
||||
else if (!hrScopeMap.containsKey(empNo)) {
|
||||
missReason = "花名册无此工号";
|
||||
}
|
||||
else if (StrUtil.isBlank(hrScopeMap.get(empNo))) {
|
||||
missReason = "花名册无人事范围";
|
||||
}
|
||||
else {
|
||||
costCenter = hrScopeMap.get(empNo);
|
||||
}
|
||||
if (missReason != null) {
|
||||
unmatchedRoster++;
|
||||
costCenter = CanteenReportFaceMapper.mapCostCenter(row.getLegalEntity());
|
||||
e.setRemarkTag(CanteenReportConstants.REMARK_ROSTER_MISS);
|
||||
String key = StrUtil.isNotBlank(empNo) ? empNo
|
||||
: ("名:" + StrUtil.blankToDefault(row.getEmpName(), "") + "|法:"
|
||||
+ StrUtil.blankToDefault(row.getLegalEntity(), ""));
|
||||
CanteenReportRosterMissVO miss = missByKey.get(key);
|
||||
if (miss == null) {
|
||||
miss = new CanteenReportRosterMissVO();
|
||||
miss.setEmpNo(emptyToNull(empNo));
|
||||
miss.setEmpName(emptyToNull(row.getEmpName()));
|
||||
miss.setLegalEntity(emptyToNull(row.getLegalEntity()));
|
||||
miss.setCostCenter(costCenter);
|
||||
miss.setReason(missReason);
|
||||
miss.setRowCount(1);
|
||||
missByKey.put(key, miss);
|
||||
if (!skipPerson) {
|
||||
String missReason = null;
|
||||
if (StrUtil.isBlank(empNo)) {
|
||||
missReason = "无工号";
|
||||
}
|
||||
else if (!costCenterMap.containsKey(empNo)) {
|
||||
missReason = "人员信息无此工号";
|
||||
}
|
||||
else if (StrUtil.isBlank(costCenterMap.get(empNo))) {
|
||||
missReason = "人员信息无核对成本";
|
||||
}
|
||||
else {
|
||||
miss.setRowCount(miss.getRowCount() + 1);
|
||||
costCenter = costCenterMap.get(empNo);
|
||||
}
|
||||
if (missReason != null) {
|
||||
unmatchedPerson++;
|
||||
costCenter = CanteenReportFaceMapper.mapCostCenter(row.getLegalEntity());
|
||||
e.setRemarkTag(CanteenReportConstants.REMARK_PERSON_MISS);
|
||||
String key = StrUtil.isNotBlank(empNo) ? empNo
|
||||
: ("名:" + StrUtil.blankToDefault(row.getEmpName(), "") + "|法:"
|
||||
+ StrUtil.blankToDefault(row.getLegalEntity(), ""));
|
||||
CanteenReportRosterMissVO miss = missByKey.get(key);
|
||||
if (miss == null) {
|
||||
miss = new CanteenReportRosterMissVO();
|
||||
miss.setEmpNo(emptyToNull(empNo));
|
||||
miss.setEmpName(emptyToNull(row.getEmpName()));
|
||||
miss.setLegalEntity(emptyToNull(row.getLegalEntity()));
|
||||
miss.setCostCenter(costCenter);
|
||||
miss.setReason(missReason);
|
||||
miss.setRowCount(1);
|
||||
missByKey.put(key, miss);
|
||||
}
|
||||
else {
|
||||
miss.setRowCount(miss.getRowCount() + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
e.setCostCenter(costCenter);
|
||||
@@ -467,22 +477,19 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
|
||||
|
||||
long cost = System.currentTimeMillis() - t0;
|
||||
StringBuilder tip = new StringBuilder();
|
||||
if (hrScopeMap.isEmpty()) {
|
||||
tip.append(";当月花名册为空,核对成本已回退法律归属映射,请先导入花名册");
|
||||
if (costCenterMap.isEmpty() && unmatchedPerson > 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<CanteenRepor
|
||||
return StrUtil.isBlank(s) ? null : s.trim();
|
||||
}
|
||||
|
||||
private void archiveExcel(File temp, String originalName, CanteenReportImportLogEntity logEntity) {
|
||||
try (InputStream in = new FileInputStream(temp)) {
|
||||
MultipartFile mf = new MockMultipartFile("file", originalName,
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", in);
|
||||
R upload = remoteFileService.innerUpload(mf, "canteen-report-face", null, "canteen-report-face");
|
||||
if (upload != null && upload.getData() instanceof Map) {
|
||||
Map<?, ?> 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<CanteenRepor
|
||||
private final LocalDateTime now;
|
||||
private final CanteenReportFaceDetailMapper faceDetailMapper;
|
||||
private final int batchSize;
|
||||
/** 非空时只入库这些二级组织 */
|
||||
private final Set<String> orgLevel2Filter;
|
||||
|
||||
private final List<CanteenReportFaceDetailEntity> buffer = new ArrayList<>();
|
||||
private final Map<String, Integer> skipReasonCount = new LinkedHashMap<>();
|
||||
@@ -606,7 +592,8 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
|
||||
private String resolvedBizMonth;
|
||||
|
||||
FaceDetailAccumulator(Long importLogId, String bizMonthParam, String operator, Long tenantId,
|
||||
LocalDateTime now, CanteenReportFaceDetailMapper faceDetailMapper, int batchSize) {
|
||||
LocalDateTime now, CanteenReportFaceDetailMapper faceDetailMapper, int batchSize,
|
||||
List<String> orgLevel2s) {
|
||||
this.importLogId = importLogId;
|
||||
this.bizMonthParam = bizMonthParam;
|
||||
this.operator = operator;
|
||||
@@ -614,6 +601,12 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
|
||||
this.now = now;
|
||||
this.faceDetailMapper = faceDetailMapper;
|
||||
this.batchSize = batchSize;
|
||||
if (CollUtil.isEmpty(orgLevel2s)) {
|
||||
this.orgLevel2Filter = null;
|
||||
}
|
||||
else {
|
||||
this.orgLevel2Filter = new HashSet<>(orgLevel2s);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -629,6 +622,13 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
|
||||
recordSkip(excelRow, "无法确定报表归属月(请填写归属月,或保证时间列可解析)", data);
|
||||
return;
|
||||
}
|
||||
if (orgLevel2Filter != null) {
|
||||
String org2 = StrUtil.trim(e.getOrgLevel2());
|
||||
if (StrUtil.isBlank(org2) || !orgLevel2Filter.contains(org2)) {
|
||||
recordSkip(excelRow, "二级组织不在导入范围", data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 有归属月即可入库;无业务日仍保留(汇总按月不依赖单日)
|
||||
if (StrUtil.isNotBlank(e.getBizDate())) {
|
||||
trackDate(e.getBizDate());
|
||||
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
package com.bsd.pjcloud.canteen.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
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.CanteenReportPersonExcel;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenReportPersonEntity;
|
||||
import com.bsd.pjcloud.canteen.mapper.CanteenReportPersonMapper;
|
||||
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 org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CanteenReportPersonServiceImpl extends ServiceImpl<CanteenReportPersonMapper, CanteenReportPersonEntity>
|
||||
implements CanteenReportPersonService {
|
||||
|
||||
private static final int BATCH = 800;
|
||||
|
||||
@Override
|
||||
public Page<CanteenReportPersonEntity> pageList(Page page, String empNo, String empName, String costCenter) {
|
||||
return page(page, Wrappers.<CanteenReportPersonEntity>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<CanteenReportPersonExcel> rows, boolean replaceAll) {
|
||||
if (CollUtil.isEmpty(rows)) {
|
||||
return R.failed("导入数据为空");
|
||||
}
|
||||
Map<String, CanteenReportPersonExcel> 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<CanteenReportPersonEntity> saveList = new ArrayList<>(byEmp.size());
|
||||
for (Map.Entry<String, CanteenReportPersonExcel> en : byEmp.entrySet()) {
|
||||
saveList.add(toEntity(en.getValue(), en.getKey(), null, operator, tenantId, now, true));
|
||||
}
|
||||
for (List<CanteenReportPersonEntity> chunk : CollUtil.split(saveList, BATCH)) {
|
||||
baseMapper.insertBatch(chunk);
|
||||
}
|
||||
return R.ok(null, String.format("全量覆盖导入成功:入库 %d 人,跳过无工号 %d 行", saveList.size(), skipBlank));
|
||||
}
|
||||
|
||||
Map<String, CanteenReportPersonEntity> existMap = list(Wrappers.<CanteenReportPersonEntity>lambdaQuery()
|
||||
.in(CanteenReportPersonEntity::getEmpNo, byEmp.keySet())).stream()
|
||||
.collect(Collectors.toMap(CanteenReportPersonEntity::getEmpNo, Function.identity(), (a, b) -> a));
|
||||
|
||||
List<CanteenReportPersonEntity> insertList = new ArrayList<>();
|
||||
List<CanteenReportPersonEntity> updateList = new ArrayList<>();
|
||||
for (Map.Entry<String, CanteenReportPersonExcel> 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<CanteenReportPersonEntity> 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<CanteenReportPersonExcel> exportExcel() {
|
||||
List<CanteenReportPersonEntity> list = list(Wrappers.<CanteenReportPersonEntity>lambdaQuery()
|
||||
.orderByAsc(CanteenReportPersonEntity::getEmpNo));
|
||||
List<CanteenReportPersonExcel> 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<String, String> loadCostCenterMap() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
List<CanteenReportPersonEntity> list = list(Wrappers.<CanteenReportPersonEntity>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.<CanteenReportPersonEntity>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;
|
||||
}
|
||||
}
|
||||
+142
-1
@@ -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<CanteenReportSwip
|
||||
qw.eq(StrUtil.isNotBlank(source), "source", source);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R saveOrUpdateRow(CanteenReportSwipeEntity entity) {
|
||||
if (entity == null) {
|
||||
return R.failed("参数为空");
|
||||
}
|
||||
if (StrUtil.isBlank(entity.getBizMonth())) {
|
||||
return R.failed("请填写业务月");
|
||||
}
|
||||
entity.setBizMonth(entity.getBizMonth().trim());
|
||||
if (StrUtil.isBlank(entity.getIncomeType())) {
|
||||
return R.failed("请填写收入类别");
|
||||
}
|
||||
entity.setIncomeType(entity.getIncomeType().trim());
|
||||
if (entity.getConsumeAmount() == null) {
|
||||
return R.failed("请填写消费金额");
|
||||
}
|
||||
boolean allowNeg = CanteenReportConstants.OTHER_INCOME_TYPES.contains(entity.getIncomeType())
|
||||
|| CanteenReportFaceMapper.isScanOrGuestPay(entity.getIncomeType());
|
||||
if (entity.getConsumeAmount().compareTo(BigDecimal.ZERO) < 0 && !allowNeg
|
||||
&& CanteenReportConstants.INCOME_SWIPE.equals(entity.getIncomeType())) {
|
||||
return R.failed("员工刷卡不允许负数金额");
|
||||
}
|
||||
if (StrUtil.isBlank(entity.getPark())) {
|
||||
return R.failed("请填写厂区");
|
||||
}
|
||||
entity.setPark(entity.getPark().trim());
|
||||
if (!CanteenReportConstants.PARKS.contains(entity.getPark())
|
||||
&& !CanteenReportConstants.SHARE_PARKS.contains(entity.getPark())) {
|
||||
return R.failed("厂区不在枚举内:" + entity.getPark());
|
||||
}
|
||||
trimSwipeFields(entity);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String user = currentUser();
|
||||
if (entity.getId() == null) {
|
||||
entity.setId(IdWorker.getId());
|
||||
if (StrUtil.isBlank(entity.getSource())) {
|
||||
entity.setSource("MANUAL");
|
||||
}
|
||||
entity.setCreateBy(user);
|
||||
entity.setUpdateBy(user);
|
||||
entity.setCreateTime(now);
|
||||
entity.setUpdateTime(now);
|
||||
entity.setDelFlag(CommonConstants.STATUS_NORMAL);
|
||||
entity.setTenantId(TenantContextHolder.getTenantId());
|
||||
baseMapper.insert(entity);
|
||||
return R.ok(entity, "新增成功");
|
||||
}
|
||||
entity.setUpdateBy(user);
|
||||
entity.setUpdateTime(now);
|
||||
updateById(entity);
|
||||
return R.ok(entity, "保存成功");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R batchUpdate(CanteenReportSwipeBatchUpdateDTO dto) {
|
||||
if (dto == null || CollUtil.isEmpty(dto.getIds())) {
|
||||
return R.failed("请选择要修改的数据");
|
||||
}
|
||||
boolean any = Boolean.TRUE.equals(dto.getUpdateIncomeType()) || Boolean.TRUE.equals(dto.getUpdatePark())
|
||||
|| Boolean.TRUE.equals(dto.getUpdateCostCenter()) || Boolean.TRUE.equals(dto.getUpdateLegalEntity());
|
||||
if (!any) {
|
||||
return R.failed("请至少勾选一项要修改的字段");
|
||||
}
|
||||
if (Boolean.TRUE.equals(dto.getUpdateIncomeType()) && StrUtil.isBlank(dto.getIncomeType())) {
|
||||
return R.failed("请填写收入类别");
|
||||
}
|
||||
if (Boolean.TRUE.equals(dto.getUpdatePark())) {
|
||||
if (StrUtil.isBlank(dto.getPark())) {
|
||||
return R.failed("请填写厂区");
|
||||
}
|
||||
String park = dto.getPark().trim();
|
||||
if (!CanteenReportConstants.PARKS.contains(park) && !CanteenReportConstants.SHARE_PARKS.contains(park)) {
|
||||
return R.failed("厂区不在枚举内:" + park);
|
||||
}
|
||||
dto.setPark(park);
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String user = currentUser();
|
||||
int ok = 0;
|
||||
for (Long id : dto.getIds()) {
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
CanteenReportSwipeEntity e = getById(id);
|
||||
if (e == null) {
|
||||
continue;
|
||||
}
|
||||
if (Boolean.TRUE.equals(dto.getUpdateIncomeType())) {
|
||||
e.setIncomeType(StrUtil.trim(dto.getIncomeType()));
|
||||
}
|
||||
if (Boolean.TRUE.equals(dto.getUpdatePark())) {
|
||||
e.setPark(dto.getPark());
|
||||
}
|
||||
if (Boolean.TRUE.equals(dto.getUpdateCostCenter())) {
|
||||
e.setCostCenter(StrUtil.trim(dto.getCostCenter()));
|
||||
}
|
||||
if (Boolean.TRUE.equals(dto.getUpdateLegalEntity())) {
|
||||
e.setLegalEntity(StrUtil.trim(dto.getLegalEntity()));
|
||||
}
|
||||
e.setUpdateBy(user);
|
||||
e.setUpdateTime(now);
|
||||
updateById(e);
|
||||
ok++;
|
||||
}
|
||||
return R.ok(null, "已批量修改 " + ok + " 条");
|
||||
}
|
||||
|
||||
private static void trimSwipeFields(CanteenReportSwipeEntity e) {
|
||||
e.setBizDate(StrUtil.trim(e.getBizDate()));
|
||||
e.setEmpNo(StrUtil.trim(e.getEmpNo()));
|
||||
e.setEmpName(StrUtil.trim(e.getEmpName()));
|
||||
e.setLegalEntity(StrUtil.trim(e.getLegalEntity()));
|
||||
e.setDeptName(StrUtil.trim(e.getDeptName()));
|
||||
e.setConsumeArea(StrUtil.trim(e.getConsumeArea()));
|
||||
e.setSettleCounter(StrUtil.trim(e.getSettleCounter()));
|
||||
e.setCostCenter(StrUtil.trim(e.getCostCenter()));
|
||||
e.setCostCategory(StrUtil.trim(e.getCostCategory()));
|
||||
e.setRemarkTag(StrUtil.trim(e.getRemarkTag()));
|
||||
}
|
||||
|
||||
private static String currentUser() {
|
||||
try {
|
||||
if (SecurityUtils.getUser() != null) {
|
||||
return SecurityUtils.getUser().getUsername();
|
||||
}
|
||||
}
|
||||
catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R importExcel(List<CanteenReportSwipeExcel> rows, String bizMonth, boolean replaceMonth,
|
||||
@@ -151,7 +289,10 @@ public class CanteenReportSwipeServiceImpl extends ServiceImpl<CanteenReportSwip
|
||||
for (List<CanteenReportSwipeEntity> 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<CanteenReportSwipeEntity> list) {
|
||||
|
||||
+360
-2
@@ -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 工作簿)。
|
||||
* <p>
|
||||
* 兼容 WPS/Excel 另存的 CSV/TSV(常见 GBK):避免「整行塞进第一列 + 中文乱码」。
|
||||
*/
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class CanteenReportExcelUtils {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> List<T> readSheet(MultipartFile file, Class<T> 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<T> rows = readDelimitedText(bytes, clazz, charset, delimiter);
|
||||
expandTabJoinedRows(rows);
|
||||
return rows;
|
||||
}
|
||||
|
||||
// 刷卡明细:POI 按列序读,避免 EasyExcel 金额转换丢数
|
||||
if (clazz == com.bsd.pjcloud.canteen.dto.report.CanteenReportSwipeExcel.class) {
|
||||
return (List<T>) 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<T>) listener.getList();
|
||||
List<T> rows = (List<T>) listener.getList();
|
||||
expandTabJoinedRows(rows);
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷卡明细专用:POI + DataFormatter 按列序读取(金额在第 7 列 index=6)。
|
||||
*/
|
||||
public static List<com.bsd.pjcloud.canteen.dto.report.CanteenReportSwipeExcel> readSwipeByPoi(byte[] bytes,
|
||||
String preferredSheetName) throws Exception {
|
||||
List<com.bsd.pjcloud.canteen.dto.report.CanteenReportSwipeExcel> 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 <T> List<T> readDelimitedText(byte[] bytes, Class<T> 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<Integer, Field> indexFields = excelIndexFields(clazz);
|
||||
List<T> 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<Integer, Field> 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<String> 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<Integer, Field> excelIndexFields(Class<?> clazz) {
|
||||
Map<Integer, Field> 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 <T> void expandTabJoinedRows(List<T> rows) {
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Class<?> clazz = rows.get(0).getClass();
|
||||
Map<Integer, Field> 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<Integer, Field> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+10
-2
@@ -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
|
||||
</select>
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.bsd.pjcloud.canteen.mapper.CanteenReportPersonMapper">
|
||||
|
||||
<delete id="deletePhysicallyAll">
|
||||
DELETE FROM canteen_report_person
|
||||
</delete>
|
||||
|
||||
<insert id="insertBatch">
|
||||
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
|
||||
<foreach collection="list" item="e" separator=",">
|
||||
(
|
||||
#{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}
|
||||
)
|
||||
</foreach>
|
||||
</insert>
|
||||
</mapper>
|
||||
Reference in new issue
Block a user