This commit is contained in:
dzc committed 2026-08-17 14:31:56 +08:00
commit 195756e80a
24 files changed
+1701 -13

No files matched your search

@@ -0,0 +1,31 @@
package com.bsd.pjcloud.canteen.dto.report;
import lombok.Data;
import java.math.BigDecimal;
/**
* 人脸明细按维度汇总后的中间行(再映射厂区/成本中心写入刷卡明细)
*/
@Data
public class CanteenReportFaceAggRow {
private String empNo;
private String empName;
private String legalEntity;
private String deptName;
/** 三级组织 → 消费区域 */
private String orgLevel3;
/** 交易设备 → 结算台 */
private String deviceName;
/** 员工刷卡 / 支付宝 */
private String incomeType;
private BigDecimal amount;
}
@@ -0,0 +1,141 @@
package com.bsd.pjcloud.canteen.dto.report;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
/**
* 人脸消费系统导出 Excel,列序与样表一致(共 43 列)。
* 明细入库原样落库;汇总到刷卡明细时再做字段映射。
*/
@Data
public class CanteenReportFaceExcel {
@ExcelProperty(index = 0)
private String orderNo;
@ExcelProperty(index = 1)
private String thirdOrderNo;
@ExcelProperty(index = 2)
private String createTime;
@ExcelProperty(index = 3)
private String payTime;
@ExcelProperty(index = 4)
private String deductTime;
@ExcelProperty(index = 5)
private String orgLevel1;
@ExcelProperty(index = 6)
private String orgLevel2;
@ExcelProperty(index = 7)
private String orgLevel3;
@ExcelProperty(index = 8)
private String canteen;
@ExcelProperty(index = 9)
private String canteenArea;
@ExcelProperty(index = 10)
private String consumePoint;
@ExcelProperty(index = 11)
private String empName;
@ExcelProperty(index = 12)
private String phone;
@ExcelProperty(index = 13)
private String legalEntity;
@ExcelProperty(index = 14)
private String empNo;
@ExcelProperty(index = 15)
private String cardNo;
@ExcelProperty(index = 16)
private String groupName;
@ExcelProperty(index = 17)
private String deptName;
@ExcelProperty(index = 18)
private String payMethod;
@ExcelProperty(index = 19)
private String payType;
@ExcelProperty(index = 20)
private String orderAmount;
@ExcelProperty(index = 21)
private String changeAmount;
@ExcelProperty(index = 22)
private String storedChange;
@ExcelProperty(index = 23)
private String subsidyChange;
@ExcelProperty(index = 24)
private String giftChange;
@ExcelProperty(index = 25)
private String storedBalance;
@ExcelProperty(index = 26)
private String subsidyBalance;
@ExcelProperty(index = 27)
private String giftBalance;
@ExcelProperty(index = 28)
private String changeOrg;
@ExcelProperty(index = 29)
private String discount;
@ExcelProperty(index = 30)
private String discountAmount;
@ExcelProperty(index = 31)
private String discountType;
@ExcelProperty(index = 32)
private String couponType;
@ExcelProperty(index = 33)
private String deductAmount;
@ExcelProperty(index = 34)
private String feeAmount;
@ExcelProperty(index = 35)
private String mealSubsidy;
@ExcelProperty(index = 36)
private String consumeType;
@ExcelProperty(index = 37)
private String mealSegment;
@ExcelProperty(index = 38)
private String deviceName;
@ExcelProperty(index = 39)
private String deviceStatus;
@ExcelProperty(index = 40)
private String remark;
@ExcelProperty(index = 41)
private String operatorName;
@ExcelProperty(index = 42)
private String visitorPayMethod;
}
@@ -0,0 +1,178 @@
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.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 人脸消费逐笔明细:列与导出 Excel 一致(另含 biz_month/biz_date 便于分段与汇总)。
*/
@Data
@TableName("canteen_report_face_detail")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "食堂报表-人脸消费逐笔明细(Excel原列)")
public class CanteenReportFaceDetailEntity extends Model<CanteenReportFaceDetailEntity> {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long importLogId;
@Schema(description = "报表归属月")
private String bizMonth;
@Schema(description = "业务日(由扣款/支付时间解析)")
private String bizDate;
@Schema(description = "订单号")
private String orderNo;
@Schema(description = "第三方订单号")
private String thirdOrderNo;
@Schema(description = "创建时间")
private String orderCreateTime;
@Schema(description = "支付时间")
private String payTime;
@Schema(description = "扣款时间")
private String deductTime;
@Schema(description = "一级组织")
private String orgLevel1;
@Schema(description = "二级组织")
private String orgLevel2;
@Schema(description = "三级组织")
private String orgLevel3;
@Schema(description = "食堂")
private String canteen;
@Schema(description = "食堂区域")
private String canteenArea;
@Schema(description = "消费点")
private String consumePoint;
@Schema(description = "姓名")
private String empName;
@Schema(description = "手机号")
private String phone;
@Schema(description = "法律归属")
private String legalEntity;
@Schema(description = "人员编号")
private String empNo;
@Schema(description = "卡号")
private String cardNo;
@Schema(description = "分组")
private String groupName;
@Schema(description = "部门")
private String deptName;
@Schema(description = "支付方式")
private String payMethod;
@Schema(description = "支付类型")
private String payType;
@Schema(description = "订单金额")
private BigDecimal orderAmount;
@Schema(description = "动账金额")
private BigDecimal changeAmount;
@Schema(description = "储值动账")
private BigDecimal storedChange;
@Schema(description = "补贴动账")
private BigDecimal subsidyChange;
@Schema(description = "赠送动账")
private BigDecimal giftChange;
@Schema(description = "储值钱包余额")
private BigDecimal storedBalance;
@Schema(description = "补贴钱包余额")
private BigDecimal subsidyBalance;
@Schema(description = "赠送钱包余额")
private BigDecimal giftBalance;
@Schema(description = "动账组织")
private String changeOrg;
@Schema(description = "优惠")
private String discount;
@Schema(description = "优惠金额")
private BigDecimal discountAmount;
@Schema(description = "优惠类型")
private String discountType;
@Schema(description = "券类型")
private String couponType;
@Schema(description = "抵扣金额")
private BigDecimal deductAmount;
@Schema(description = "手续费")
private BigDecimal feeAmount;
@Schema(description = "餐补")
private BigDecimal mealSubsidy;
@Schema(description = "消费类型")
private String consumeType;
@Schema(description = "餐段")
private String mealSegment;
@Schema(description = "交易设备")
private String deviceName;
@Schema(description = "设备状态")
private String deviceStatus;
@Schema(description = "备注")
private String remark;
@Schema(description = "操作员")
private String operatorName;
@Schema(description = "访客餐支付方式")
private String visitorPayMethod;
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
@TableField(fill = FieldFill.INSERT)
private String delFlag;
private Long tenantId;
}
@@ -0,0 +1,89 @@
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_import_log")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "食堂报表-导入日志(演示)")
public class CanteenReportImportLogEntity extends Model<CanteenReportImportLogEntity> {
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private Long id;
@Schema(description = "业务月份 yyyy-MM")
private String bizMonth;
@Schema(description = "数据起日 yyyy-MM-dd")
private String dateStart;
@Schema(description = "数据止日 yyyy-MM-dd")
private String dateEnd;
@Schema(description = "原始文件名")
private String fileName;
@Schema(description = "已存档 Excel 访问路径")
private String fileUrl;
@Schema(description = "OSS 文件名")
private String ossFileName;
@Schema(description = "来源类型 FACE")
private String sourceType;
@Schema(description = "Excel 有效行数")
private Integer totalRows;
@Schema(description = "成功入库行数")
private Integer successRows;
@Schema(description = "跳过行数")
private Integer skipRows;
@Schema(description = "汇总写入刷卡明细行数")
private Integer aggRows;
@Schema(description = "NONE/DONE/FAILED")
private String aggStatus;
@Schema(description = "最近汇总时间")
private LocalDateTime aggTime;
@Schema(description = "RUNNING/SUCCESS/FAILED")
private String status;
@Schema(description = "失败原因摘要")
private String errorMsg;
@Schema(description = "耗时毫秒")
private Long costMs;
@Schema(description = "操作人")
private String operator;
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
@TableField(fill = FieldFill.INSERT)
private String delFlag;
private Long tenantId;
}
@@ -0,0 +1,45 @@
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_settle_park")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "食堂报表-结算台厂区配置")
public class CanteenReportSettleParkEntity extends Model<CanteenReportSettleParkEntity> {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@Schema(description = "结算台/交易设备名称")
private String settleCounter;
@Schema(description = "厂区")
private String park;
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;
}
@@ -22,6 +22,9 @@ public class CanteenReportSwipeEntity extends Model<CanteenReportSwipeEntity> {
@Schema(description = "业务月份 yyyy-MM")
private String bizMonth;
@Schema(description = "业务日期 yyyy-MM-dd")
private String bizDate;
@Schema(description = "工号")
private String empNo;
@@ -58,6 +61,12 @@ public class CanteenReportSwipeEntity extends Model<CanteenReportSwipeEntity> {
@Schema(description = "备注标识")
private String remarkTag;
@Schema(description = "导入日志ID")
private Long importLogId;
@Schema(description = "来源 FACE/MANUAL/EXCEL")
private String source;
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建人")
private String createBy;
@@ -0,0 +1,231 @@
package com.bsd.pjcloud.canteen.report;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportFaceExcel;
import com.bsd.pjcloud.canteen.entity.CanteenReportFaceDetailEntity;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 人脸 Excel → 明细原样入库;汇总时再映射收入类别 / 成本中心。
*/
public final class CanteenReportFaceMapper {
private static final Pattern DATE_PATTERN = Pattern.compile("(20\\d{2})[-/](\\d{1,2})[-/](\\d{1,2})");
private static final Map<String, String> LEGAL_TO_CENTER = new LinkedHashMap<>();
static {
for (String c : CanteenReportConstants.COST_CENTERS) {
LEGAL_TO_CENTER.put(c, c);
LEGAL_TO_CENTER.put(c.replace('-', '_'), c);
LEGAL_TO_CENTER.put(c.replace('_', '-'), c);
}
LEGAL_TO_CENTER.put("嘉兴敏惠_铝件", "嘉兴敏惠-铝件");
LEGAL_TO_CENTER.put("嘉兴敏惠_塑件", "嘉兴敏惠-塑件");
LEGAL_TO_CENTER.put("嘉兴敏惠_塑件BU职能", "嘉兴敏惠-塑件BU职能");
LEGAL_TO_CENTER.put("嘉兴敏惠_塑件技术中心", "嘉兴敏惠-塑件技术中心");
LEGAL_TO_CENTER.put("嘉兴敏惠_塑件项目中心", "嘉兴敏惠-塑件项目中心");
LEGAL_TO_CENTER.put("嘉兴敏惠_研究院", "嘉兴敏惠-研究院");
LEGAL_TO_CENTER.put("嘉兴敏惠_地区共享", "嘉兴敏惠-地区共享");
LEGAL_TO_CENTER.put("嘉兴敏华_塑件工厂", "嘉兴敏华");
LEGAL_TO_CENTER.put("嘉兴信元_塑件开发中心", "嘉兴信元-塑件开发中心");
LEGAL_TO_CENTER.put("嘉兴信元_塑件模具厂", "嘉兴信元-塑件模具厂");
LEGAL_TO_CENTER.put("宁波敏实汽车零部件技术研发有限公司", "宁波敏实汽车零部件技术研发有限公司");
LEGAL_TO_CENTER.put("浙江敏能科技有限公司", "浙江敏能");
LEGAL_TO_CENTER.put("浙江敏盛汽车零部件有限公司", "浙江敏盛");
LEGAL_TO_CENTER.put("嘉兴敏胜汽车零部件有限公司", "嘉兴敏胜");
LEGAL_TO_CENTER.put("嘉兴敏实机械有限公司", "嘉兴敏实");
LEGAL_TO_CENTER.put("敏实海拉(嘉兴)汽车零部件有限公司", "敏实海拉");
LEGAL_TO_CENTER.put("嘉兴敏能汽车零部件有限公司", "嘉兴敏能");
}
private CanteenReportFaceMapper() {
}
/** Excel 原列写入明细表(不做刷卡字段映射) */
public static CanteenReportFaceDetailEntity toDetail(CanteenReportFaceExcel row, String bizMonth, Long importLogId) {
if (row == null) {
return null;
}
String bizDate = extractBizDate(row.getDeductTime(), row.getPayTime(), row.getCreateTime());
BigDecimal orderAmt = firstAmount(row.getOrderAmount(), row.getChangeAmount());
if (isBlank(row.getEmpNo()) && isBlank(row.getEmpName()) && isBlank(row.getOrderNo()) && orderAmt == null
&& isBlank(bizDate)) {
return null;
}
CanteenReportFaceDetailEntity e = new CanteenReportFaceDetailEntity();
e.setImportLogId(importLogId);
if (!isBlank(bizMonth)) {
e.setBizMonth(bizMonth.trim());
}
else if (bizDate != null && bizDate.length() >= 7) {
e.setBizMonth(bizDate.substring(0, 7));
}
e.setBizDate(bizDate);
e.setOrderNo(trim(row.getOrderNo()));
e.setThirdOrderNo(trim(row.getThirdOrderNo()));
e.setOrderCreateTime(trim(row.getCreateTime()));
e.setPayTime(trim(row.getPayTime()));
e.setDeductTime(trim(row.getDeductTime()));
e.setOrgLevel1(trim(row.getOrgLevel1()));
e.setOrgLevel2(trim(row.getOrgLevel2()));
e.setOrgLevel3(trim(row.getOrgLevel3()));
e.setCanteen(trim(row.getCanteen()));
e.setCanteenArea(trim(row.getCanteenArea()));
e.setConsumePoint(trim(row.getConsumePoint()));
e.setEmpName(trim(row.getEmpName()));
e.setPhone(trim(row.getPhone()));
e.setLegalEntity(trim(row.getLegalEntity()));
e.setEmpNo(trim(row.getEmpNo()));
e.setCardNo(trim(row.getCardNo()));
e.setGroupName(trim(row.getGroupName()));
e.setDeptName(trim(row.getDeptName()));
e.setPayMethod(trim(row.getPayMethod()));
e.setPayType(trim(row.getPayType()));
e.setOrderAmount(orderAmt == null ? BigDecimal.ZERO : orderAmt);
e.setChangeAmount(parseAmount(row.getChangeAmount()));
e.setStoredChange(parseAmount(row.getStoredChange()));
e.setSubsidyChange(parseAmount(row.getSubsidyChange()));
e.setGiftChange(parseAmount(row.getGiftChange()));
e.setStoredBalance(parseAmount(row.getStoredBalance()));
e.setSubsidyBalance(parseAmount(row.getSubsidyBalance()));
e.setGiftBalance(parseAmount(row.getGiftBalance()));
e.setChangeOrg(trim(row.getChangeOrg()));
e.setDiscount(trim(row.getDiscount()));
e.setDiscountAmount(parseAmount(row.getDiscountAmount()));
e.setDiscountType(trim(row.getDiscountType()));
e.setCouponType(trim(row.getCouponType()));
e.setDeductAmount(parseAmount(row.getDeductAmount()));
e.setFeeAmount(parseAmount(row.getFeeAmount()));
e.setMealSubsidy(parseAmount(row.getMealSubsidy()));
e.setConsumeType(trim(row.getConsumeType()));
e.setMealSegment(trim(row.getMealSegment()));
e.setDeviceName(trim(row.getDeviceName()));
e.setDeviceStatus(trim(row.getDeviceStatus()));
e.setRemark(trim(row.getRemark()));
e.setOperatorName(trim(row.getOperatorName()));
e.setVisitorPayMethod(trim(row.getVisitorPayMethod()));
return e;
}
/** 汇总用: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;
}
return CanteenReportConstants.INCOME_SWIPE;
}
/** 汇总用:法律归属 → 核对成本 */
public static String mapCostCenter(String legalEntity) {
if (isBlank(legalEntity)) {
return "未匹配成本中心";
}
String raw = legalEntity.trim();
if (LEGAL_TO_CENTER.containsKey(raw)) {
return LEGAL_TO_CENTER.get(raw);
}
String norm = raw.replace('_', '-');
if (LEGAL_TO_CENTER.containsKey(norm)) {
return LEGAL_TO_CENTER.get(norm);
}
String stripped = raw.replace("有限公司", "").trim();
for (String c : CanteenReportConstants.COST_CENTERS) {
if (raw.contains(c) || (!stripped.isEmpty() && c.contains(stripped))) {
return c;
}
}
String compactRaw = raw.replace("-", "").replace("_", "");
for (String c : CanteenReportConstants.COST_CENTERS) {
String compact = c.replace("-", "").replace("_", "");
if (compactRaw.contains(compact)) {
return c;
}
}
return raw;
}
public static String extractBizDate(String... candidates) {
for (String c : candidates) {
if (isBlank(c)) {
continue;
}
String s = c.trim();
Matcher m = DATE_PATTERN.matcher(s);
if (m.find()) {
try {
return LocalDate.of(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)),
Integer.parseInt(m.group(3))).toString();
}
catch (Exception ignored) {
// continue
}
}
try {
String head = s.length() >= 10 ? s.substring(0, 10).replace('/', '-') : s.replace('/', '-');
return LocalDate.parse(head, DateTimeFormatter.ISO_LOCAL_DATE).toString();
}
catch (DateTimeParseException ignored) {
// continue
}
}
return null;
}
public static BigDecimal firstAmount(String... vals) {
for (String v : vals) {
BigDecimal b = parseAmount(v);
if (b != null) {
return b;
}
}
return null;
}
public static BigDecimal parseAmount(String v) {
if (isBlank(v)) {
return null;
}
String s = v.trim().replace(",", "").replace("¥", "").replace("元", "");
try {
return new BigDecimal(s);
}
catch (Exception e) {
return null;
}
}
private static boolean containsAny(String text, String... keys) {
if (isBlank(text)) {
return false;
}
for (String k : keys) {
if (text.contains(k)) {
return true;
}
}
return false;
}
private static boolean isBlank(String s) {
return s == null || s.trim().isEmpty();
}
private static String trim(String s) {
return s == null ? null : s.trim();
}
private static String nvl(String s) {
return s == null ? "" : s;
}
}
@@ -0,0 +1,91 @@
package com.bsd.pjcloud.canteen.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.canteen.service.CanteenReportFaceImportService;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* 食堂报表-人脸消费导入(明细落库 + 汇总刷卡)
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/canteenReport/faceImport")
@Tag(description = "canteenReportFaceImport", name = "食堂报表-人脸消费导入(演示)")
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class CanteenReportFaceImportController {
private final CanteenReportFaceImportService faceImportService;
@Operation(summary = "导入日志分页")
@GetMapping("/log/page")
public R page(@ParameterObject Page page,
@RequestParam(required = false) String bizMonth,
@RequestParam(required = false) String status) {
return R.ok(faceImportService.pageLogs(page, bizMonth, status));
}
@Operation(summary = "导入日志详情")
@GetMapping("/log/{id}")
public R get(@PathVariable Long id) {
return R.ok(faceImportService.getById(id));
}
@Operation(summary = "人脸逐笔明细分页")
@GetMapping("/detail/page")
public R detailPage(@ParameterObject Page page,
@RequestParam(required = false) String bizMonth,
@RequestParam(required = false) String importLogId,
@RequestParam(required = false) String empNo,
@RequestParam(required = false) String payMethod) {
Long logId = null;
if (cn.hutool.core.util.StrUtil.isNotBlank(importLogId)) {
try {
logId = Long.parseLong(importLogId.trim());
}
catch (NumberFormatException e) {
return R.failed("importLogId 非法");
}
}
return R.ok(faceImportService.pageDetails(page, bizMonth, logId, empNo, payMethod));
}
@SysLog("人脸消费Excel导入明细")
@Operation(summary = "流式导入人脸Excel到逐笔明细表")
@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);
}
@SysLog("人脸明细汇总到刷卡明细")
@Operation(summary = "统计汇总写入刷卡明细(B扫C→支付宝)")
@PostMapping("/aggregate")
public R aggregate(@RequestParam(required = false) String bizMonth,
@RequestParam(required = false) String dateStart,
@RequestParam(required = false) String dateEnd,
@RequestParam(required = false) String importLogId,
@RequestParam(defaultValue = "true") boolean replaceFace) {
Long logId = null;
if (cn.hutool.core.util.StrUtil.isNotBlank(importLogId)) {
try {
logId = Long.parseLong(importLogId.trim());
}
catch (NumberFormatException e) {
return R.failed("importLogId 非法: " + importLogId);
}
}
return faceImportService.aggregateToSwipe(bizMonth, dateStart, dateEnd, logId, replaceFace);
}
}
@@ -0,0 +1,73 @@
package com.bsd.pjcloud.canteen.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.canteen.entity.CanteenReportSettleParkEntity;
import com.bsd.pjcloud.canteen.report.CanteenReportConstants;
import com.bsd.pjcloud.canteen.service.CanteenReportSettleParkService;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.log.annotation.SysLog;
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 java.util.Arrays;
import java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping("/canteenReport/settlePark")
@Tag(description = "canteenReportSettlePark", name = "食堂报表-结算台厂区配置(演示)")
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class CanteenReportSettleParkController {
private final CanteenReportSettleParkService settleParkService;
@Operation(summary = "分页")
@GetMapping("/page")
public R page(@ParameterObject Page page,
@RequestParam(required = false) String settleCounter,
@RequestParam(required = false) String park) {
return R.ok(settleParkService.pageList(page, settleCounter, park));
}
@Operation(summary = "厂区枚举")
@GetMapping("/parks")
public R parks() {
return R.ok(CanteenReportConstants.PARKS);
}
@Operation(summary = "详情")
@GetMapping("/{id}")
public R get(@PathVariable Long id) {
return R.ok(settleParkService.getById(id));
}
@SysLog("新增结算台厂区")
@PostMapping
public R add(@RequestBody CanteenReportSettleParkEntity entity) {
return settleParkService.saveOrUpdateRow(entity);
}
@SysLog("修改结算台厂区")
@PutMapping
public R edit(@RequestBody CanteenReportSettleParkEntity entity) {
return settleParkService.saveOrUpdateRow(entity);
}
@SysLog("删除结算台厂区")
@DeleteMapping
public R remove(@RequestBody Long[] ids) {
return R.ok(settleParkService.removeBatchByIds(Arrays.asList(ids)));
}
@Operation(summary = "全部列表(汇总用)")
@GetMapping("/list")
public R listAll() {
List<CanteenReportSettleParkEntity> list = settleParkService.list();
return R.ok(list);
}
}
@@ -37,8 +37,9 @@ public class CanteenReportSwipeController {
@RequestParam(required = false) String park,
@RequestParam(required = false) String incomeType,
@RequestParam(required = false) String costCenter,
@RequestParam(required = false) String empNo) {
return R.ok(swipeService.pageList(page, bizMonth, park, incomeType, costCenter, empNo));
@RequestParam(required = false) String empNo,
@RequestParam(required = false) Long importLogId) {
return R.ok(swipeService.pageList(page, bizMonth, park, incomeType, costCenter, empNo, importLogId));
}
@Operation(summary = "详情")
@@ -0,0 +1,34 @@
package com.bsd.pjcloud.canteen.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportFaceAggRow;
import com.bsd.pjcloud.canteen.entity.CanteenReportFaceDetailEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface CanteenReportFaceDetailMapper extends BaseMapper<CanteenReportFaceDetailEntity> {
int insertBatch(@Param("list") List<CanteenReportFaceDetailEntity> list);
int deletePhysicallyByImportLogId(@Param("importLogId") Long importLogId);
int deletePhysicallyByDateRange(@Param("bizMonth") String bizMonth,
@Param("dateStart") String dateStart, @Param("dateEnd") String dateEnd);
int deletePhysicallyDateRangeExceptLog(@Param("bizMonth") String bizMonth,
@Param("dateStart") String dateStart, @Param("dateEnd") String dateEnd,
@Param("importLogId") Long importLogId);
/**
* 按人+维度汇总;B扫C 等归入支付宝,其余归入员工刷卡。金额用订单金额。
* 传入 importLogId 时只按批次汇总,忽略日期段。
*/
List<CanteenReportFaceAggRow> selectAggByMonth(@Param("bizMonth") String bizMonth,
@Param("dateStart") String dateStart, @Param("dateEnd") String dateEnd,
@Param("importLogId") Long importLogId);
long countDetail(@Param("bizMonth") String bizMonth, @Param("importLogId") Long importLogId);
}
@@ -0,0 +1,9 @@
package com.bsd.pjcloud.canteen.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bsd.pjcloud.canteen.entity.CanteenReportImportLogEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CanteenReportImportLogMapper extends BaseMapper<CanteenReportImportLogEntity> {
}
@@ -0,0 +1,9 @@
package com.bsd.pjcloud.canteen.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bsd.pjcloud.canteen.entity.CanteenReportSettleParkEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CanteenReportSettleParkMapper extends BaseMapper<CanteenReportSettleParkEntity> {
}
@@ -12,5 +12,19 @@ public interface CanteenReportSwipeMapper extends BaseMapper<CanteenReportSwipeE
int deletePhysicallyByBizMonth(@Param("bizMonth") String bizMonth);
int deletePhysicallyFaceByBizMonth(@Param("bizMonth") String bizMonth);
/**
* 物理删除人脸来源在指定日期段的数据(用于半月分段覆盖导入)
*/
int deletePhysicallyFaceByDateRange(@Param("bizMonth") String bizMonth,
@Param("dateStart") String dateStart, @Param("dateEnd") String dateEnd);
int deletePhysicallyByImportLogId(@Param("importLogId") Long importLogId);
int deletePhysicallyFaceRangeExceptLog(@Param("bizMonth") String bizMonth,
@Param("dateStart") String dateStart, @Param("dateEnd") String dateEnd,
@Param("importLogId") Long importLogId);
int insertBatch(@Param("list") List<CanteenReportSwipeEntity> list);
}
@@ -0,0 +1,32 @@
package com.bsd.pjcloud.canteen.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.canteen.entity.CanteenReportFaceDetailEntity;
import com.bsd.pjcloud.canteen.entity.CanteenReportImportLogEntity;
import com.bsd.pjcloud.common.core.util.R;
import org.springframework.web.multipart.MultipartFile;
public interface CanteenReportFaceImportService extends IService<CanteenReportImportLogEntity> {
/**
* 流式导入人脸 Excel → 逐笔明细表(不直接写刷卡明细)。
*/
R importFaceExcel(MultipartFile file, String bizMonth, String dateStart, String dateEnd, boolean replaceRange);
/**
* 将明细汇总写入刷卡明细。B扫C→支付宝,其余→员工刷卡;厂区按结算台配置。
*
* @param bizMonth 业务月
* @param dateStart 可选日期段
* @param dateEnd 可选日期段
* @param importLogId 可选,仅汇总某一批导入
* @param replaceFace 是否先清空该月已有 FACE 汇总刷卡行
*/
R aggregateToSwipe(String bizMonth, String dateStart, String dateEnd, Long importLogId, boolean replaceFace);
Page<CanteenReportImportLogEntity> pageLogs(Page page, String bizMonth, String status);
Page<CanteenReportFaceDetailEntity> pageDetails(Page page, String bizMonth, Long importLogId, String empNo,
String payMethod);
}
@@ -0,0 +1,13 @@
package com.bsd.pjcloud.canteen.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.canteen.entity.CanteenReportSettleParkEntity;
import com.bsd.pjcloud.common.core.util.R;
public interface CanteenReportSettleParkService extends IService<CanteenReportSettleParkEntity> {
Page<CanteenReportSettleParkEntity> pageList(Page page, String settleCounter, String park);
R saveOrUpdateRow(CanteenReportSettleParkEntity entity);
}
@@ -11,7 +11,8 @@ import java.util.List;
public interface CanteenReportSwipeService extends IService<CanteenReportSwipeEntity> {
Page<CanteenReportSwipeEntity> pageList(Page page, String bizMonth, String park, String incomeType, String costCenter, String empNo);
Page<CanteenReportSwipeEntity> pageList(Page page, String bizMonth, String park, String incomeType, String costCenter,
String empNo, Long importLogId);
R importExcel(List<CanteenReportSwipeExcel> rows, String bizMonth, boolean replaceMonth, BindingResult bindingResult);
@@ -0,0 +1,495 @@
package com.bsd.pjcloud.canteen.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.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.CanteenReportFaceAggRow;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportFaceExcel;
import com.bsd.pjcloud.canteen.entity.CanteenReportFaceDetailEntity;
import com.bsd.pjcloud.canteen.entity.CanteenReportImportLogEntity;
import com.bsd.pjcloud.canteen.entity.CanteenReportSettleParkEntity;
import com.bsd.pjcloud.canteen.entity.CanteenReportSwipeEntity;
import com.bsd.pjcloud.canteen.mapper.CanteenReportFaceDetailMapper;
import com.bsd.pjcloud.canteen.mapper.CanteenReportImportLogMapper;
import com.bsd.pjcloud.canteen.mapper.CanteenReportSettleParkMapper;
import com.bsd.pjcloud.canteen.mapper.CanteenReportSwipeMapper;
import com.bsd.pjcloud.canteen.report.CanteenReportFaceMapper;
import com.bsd.pjcloud.canteen.service.CanteenReportFaceImportService;
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;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 人脸消费:Excel → 逐笔明细表;再统计汇总 → 刷卡明细。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenReportImportLogMapper, CanteenReportImportLogEntity>
implements CanteenReportFaceImportService {
private static final int BATCH_SIZE = 1500;
private final CanteenReportFaceDetailMapper faceDetailMapper;
private final CanteenReportSwipeMapper swipeMapper;
private final CanteenReportSettleParkMapper settleParkMapper;
private final RemoteFileService remoteFileService;
@Override
public Page<CanteenReportImportLogEntity> pageLogs(Page page, String bizMonth, String status) {
return page(page, Wrappers.<CanteenReportImportLogEntity>lambdaQuery()
.eq(StrUtil.isNotBlank(bizMonth), CanteenReportImportLogEntity::getBizMonth, bizMonth)
.eq(StrUtil.isNotBlank(status), CanteenReportImportLogEntity::getStatus, status)
.orderByDesc(CanteenReportImportLogEntity::getCreateTime));
}
@Override
public Page<CanteenReportFaceDetailEntity> pageDetails(Page page, String bizMonth, Long importLogId, String empNo,
String payMethod) {
return faceDetailMapper.selectPage(page, Wrappers.<CanteenReportFaceDetailEntity>lambdaQuery()
.eq(StrUtil.isNotBlank(bizMonth), CanteenReportFaceDetailEntity::getBizMonth, bizMonth)
.eq(importLogId != null, CanteenReportFaceDetailEntity::getImportLogId, importLogId)
.like(StrUtil.isNotBlank(empNo), CanteenReportFaceDetailEntity::getEmpNo, empNo)
.like(StrUtil.isNotBlank(payMethod), CanteenReportFaceDetailEntity::getPayMethod, payMethod)
.orderByDesc(CanteenReportFaceDetailEntity::getId));
}
@Override
public R importFaceExcel(MultipartFile file, String bizMonth, String dateStart, String dateEnd,
boolean replaceRange) {
if (file == null || file.isEmpty()) {
return R.failed("请上传 Excel 文件");
}
String originalName = file.getOriginalFilename();
if (StrUtil.isBlank(originalName) || (!originalName.endsWith(".xlsx") && !originalName.endsWith(".xls"))) {
return R.failed("仅支持 .xlsx / .xls");
}
long t0 = System.currentTimeMillis();
String operator = currentUser();
Long tenantId = TenantContextHolder.getTenantId();
LocalDateTime now = LocalDateTime.now();
CanteenReportImportLogEntity logEntity = new CanteenReportImportLogEntity();
logEntity.setId(IdWorker.getId());
logEntity.setBizMonth(StrUtil.trim(bizMonth));
logEntity.setDateStart(StrUtil.trim(dateStart));
logEntity.setDateEnd(StrUtil.trim(dateEnd));
logEntity.setFileName(originalName);
logEntity.setSourceType("FACE");
logEntity.setStatus("RUNNING");
logEntity.setAggStatus("NONE");
logEntity.setAggRows(0);
logEntity.setTotalRows(0);
logEntity.setSuccessRows(0);
logEntity.setSkipRows(0);
logEntity.setOperator(operator);
logEntity.setCreateBy(operator);
logEntity.setUpdateBy(operator);
logEntity.setCreateTime(now);
logEntity.setUpdateTime(now);
logEntity.setDelFlag(CommonConstants.STATUS_NORMAL);
logEntity.setTenantId(tenantId);
save(logEntity);
File temp = null;
try {
temp = Files.createTempFile("canteen-face-", ".xlsx").toFile();
file.transferTo(temp);
archiveExcel(temp, originalName, logEntity);
boolean preReplace = replaceRange && StrUtil.isNotBlank(bizMonth) && StrUtil.isNotBlank(dateStart)
&& StrUtil.isNotBlank(dateEnd);
if (preReplace) {
faceDetailMapper.deletePhysicallyByDateRange(bizMonth.trim(), dateStart.trim(), dateEnd.trim());
}
FaceDetailAccumulator acc = new FaceDetailAccumulator(logEntity.getId(), bizMonth, operator, tenantId,
now, faceDetailMapper, BATCH_SIZE);
try (InputStream in = new BufferedInputStream(new FileInputStream(temp), 1 << 20)) {
EasyExcel.read(in, CanteenReportFaceExcel.class, acc)
.sheet()
.headRowNumber(1)
.doRead();
}
acc.flush();
String resolvedMonth = StrUtil.blankToDefault(StrUtil.trim(bizMonth), acc.getResolvedBizMonth());
String resolvedStart = StrUtil.blankToDefault(StrUtil.trim(dateStart), acc.getMinDate());
String resolvedEnd = StrUtil.blankToDefault(StrUtil.trim(dateEnd), acc.getMaxDate());
if (StrUtil.isBlank(resolvedMonth) || StrUtil.isBlank(resolvedStart) || StrUtil.isBlank(resolvedEnd)) {
faceDetailMapper.deletePhysicallyByImportLogId(logEntity.getId());
failLog(logEntity, "无法确定业务月或日期段,请填写业务月与数据起止日期", t0);
return R.failed(logEntity.getErrorMsg());
}
if (replaceRange && !preReplace) {
faceDetailMapper.deletePhysicallyDateRangeExceptLog(resolvedMonth, resolvedStart, resolvedEnd,
logEntity.getId());
}
logEntity.setBizMonth(resolvedMonth);
logEntity.setDateStart(resolvedStart);
logEntity.setDateEnd(resolvedEnd);
logEntity.setTotalRows(acc.getTotalRows());
logEntity.setSuccessRows(acc.getSuccessRows());
logEntity.setSkipRows(acc.getSkipRows());
logEntity.setStatus("SUCCESS");
logEntity.setAggStatus("NONE");
logEntity.setCostMs(System.currentTimeMillis() - t0);
logEntity.setUpdateTime(LocalDateTime.now());
updateById(logEntity);
return R.ok(logEntity, String.format(
"明细导入成功:入库 %d 条,跳过 %d 条,日期 %s~%s,耗时 %d 秒。请点击「统计汇总到刷卡明细」生成报表数据。",
acc.getSuccessRows(), acc.getSkipRows(), resolvedStart, resolvedEnd,
(logEntity.getCostMs() + 999) / 1000));
}
catch (Exception e) {
log.error("人脸明细导入失败 logId={}", logEntity.getId(), e);
try {
faceDetailMapper.deletePhysicallyByImportLogId(logEntity.getId());
}
catch (Exception ignored) {
// ignore
}
failLog(logEntity, StrUtil.maxLength(StrUtil.blankToDefault(e.getMessage(), e.getClass().getSimpleName()), 900),
t0);
return R.failed("导入失败: " + e.getMessage());
}
finally {
if (temp != null && temp.exists()) {
FileUtil.del(temp);
}
}
}
@Override
public R aggregateToSwipe(String bizMonth, String dateStart, String dateEnd, Long importLogId,
boolean replaceFace) {
if (StrUtil.isBlank(bizMonth) && importLogId == null) {
return R.failed("请传入业务月份 bizMonth,或指定某一批导入日志");
}
long t0 = System.currentTimeMillis();
String operator = currentUser();
Long tenantId = TenantContextHolder.getTenantId();
LocalDateTime now = LocalDateTime.now();
String month = StrUtil.trim(bizMonth);
// 指定导入批次时:只用 importLogId,日期段不参与过滤(避免日志日期与明细 biz_date 不一致)
String useDateStart = importLogId != null ? null : StrUtil.trim(dateStart);
String useDateEnd = importLogId != null ? null : StrUtil.trim(dateEnd);
if (importLogId != null) {
CanteenReportImportLogEntity logEntity = getById(importLogId);
if (logEntity == null) {
return R.failed("导入日志不存在,可能是页面 ID 精度丢失,请刷新后重试,或使用上方「统计汇总到刷卡明细」按月汇总");
}
if (StrUtil.isBlank(month)) {
month = logEntity.getBizMonth();
}
long cnt = faceDetailMapper.countDetail(null, importLogId);
if (cnt <= 0) {
return R.failed(String.format(
"该批导入日志下没有明细(importLogId=%s)。请打开「明细」确认能否查到数据;若明细也是空的,需要重新导入 Excel",
importLogId));
}
}
else {
long cnt = faceDetailMapper.countDetail(month, null);
if (cnt <= 0) {
long any = faceDetailMapper.countDetail(null, null);
if (any <= 0) {
return R.failed("库中没有任何人脸明细,请先成功导入 Excel(导入日志里「明细入库」应大于 0)");
}
return R.failed(String.format(
"归属月 %s 下没有明细(库中其它月份共有 %d 条)。请核对导入日志的「业务月」,或改选正确归属月再汇总",
month, any));
}
}
List<CanteenReportFaceAggRow> aggRows = faceDetailMapper.selectAggByMonth(month, useDateStart, useDateEnd,
importLogId);
if (CollUtil.isEmpty(aggRows)) {
return R.failed("明细存在但汇总结果为空,请检查订单金额是否都为 0");
}
if (StrUtil.isBlank(month) && importLogId != null) {
CanteenReportImportLogEntity logEntity = getById(importLogId);
if (logEntity != null) {
month = logEntity.getBizMonth();
}
}
if (StrUtil.isBlank(month)) {
return R.failed("无法确定报表归属月");
}
Map<String, String> settleParkMap = loadSettleParkMap();
List<CanteenReportSwipeEntity> saveList = new ArrayList<>(aggRows.size());
int unmatchedPark = 0;
for (CanteenReportFaceAggRow row : aggRows) {
if (row.getAmount() == null || row.getAmount().compareTo(BigDecimal.ZERO) == 0) {
continue;
}
CanteenReportSwipeEntity e = new CanteenReportSwipeEntity();
e.setId(IdWorker.getId());
e.setBizMonth(month);
e.setBizDate(null);
e.setEmpNo(emptyToNull(row.getEmpNo()));
e.setEmpName(emptyToNull(row.getEmpName()));
e.setLegalEntity(emptyToNull(row.getLegalEntity()));
e.setDeptName(emptyToNull(row.getDeptName()));
e.setConsumeArea(emptyToNull(row.getOrgLevel3()));
e.setSettleCounter(emptyToNull(row.getDeviceName()));
e.setConsumeAmount(row.getAmount());
e.setIncomeType(row.getIncomeType());
e.setCostCenter(CanteenReportFaceMapper.mapCostCenter(row.getLegalEntity()));
String park = settleParkMap.get(StrUtil.trim(row.getDeviceName()));
if (StrUtil.isBlank(park) && StrUtil.isNotBlank(row.getDeviceName())) {
unmatchedPark++;
}
e.setPark(park);
e.setImportLogId(importLogId);
e.setSource("FACE");
e.setCreateBy(operator);
e.setUpdateBy(operator);
e.setCreateTime(now);
e.setUpdateTime(now);
e.setDelFlag(CommonConstants.STATUS_NORMAL);
e.setTenantId(tenantId);
saveList.add(e);
}
if (CollUtil.isEmpty(saveList)) {
return R.failed("汇总后可写入金额均为 0,未写入刷卡明细");
}
if (replaceFace) {
if (importLogId != null) {
swipeMapper.deletePhysicallyByImportLogId(importLogId);
}
else {
swipeMapper.deletePhysicallyFaceByBizMonth(month);
}
}
for (List<CanteenReportSwipeEntity> chunk : CollUtil.split(saveList, BATCH_SIZE)) {
swipeMapper.insertBatch(chunk);
}
if (importLogId != null) {
CanteenReportImportLogEntity logEntity = getById(importLogId);
if (logEntity != null) {
logEntity.setAggRows(saveList.size());
logEntity.setAggStatus("DONE");
logEntity.setAggTime(now);
logEntity.setUpdateTime(now);
updateById(logEntity);
}
}
else {
update(Wrappers.<CanteenReportImportLogEntity>lambdaUpdate()
.set(CanteenReportImportLogEntity::getAggStatus, "DONE")
.set(CanteenReportImportLogEntity::getAggTime, now)
.set(CanteenReportImportLogEntity::getAggRows, saveList.size())
.eq(CanteenReportImportLogEntity::getBizMonth, month)
.eq(CanteenReportImportLogEntity::getStatus, "SUCCESS"));
}
long cost = System.currentTimeMillis() - t0;
String tip = unmatchedPark > 0
? String.format(";有 %d 条结算台未配置厂区,请到「结算台厂区」维护后再重新汇总", unmatchedPark)
: "";
return R.ok(null, String.format("汇总成功:写入刷卡明细 %d 条,耗时 %d 秒%s", saveList.size(), (cost + 999) / 1000, tip));
}
private Map<String, String> loadSettleParkMap() {
List<CanteenReportSettleParkEntity> list = settleParkMapper.selectList(
Wrappers.<CanteenReportSettleParkEntity>lambdaQuery());
Map<String, String> map = new HashMap<>();
for (CanteenReportSettleParkEntity e : list) {
if (StrUtil.isNotBlank(e.getSettleCounter()) && StrUtil.isNotBlank(e.getPark())) {
map.put(e.getSettleCounter().trim(), e.getPark().trim());
}
}
return map;
}
private static String emptyToNull(String s) {
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);
logEntity.setCostMs(System.currentTimeMillis() - t0);
logEntity.setUpdateTime(LocalDateTime.now());
updateById(logEntity);
}
private static String currentUser() {
try {
if (SecurityUtils.getUser() != null) {
return SecurityUtils.getUser().getUsername();
}
}
catch (Exception ignored) {
// ignore
}
return null;
}
static final class FaceDetailAccumulator implements ReadListener<CanteenReportFaceExcel> {
private final Long importLogId;
private final String bizMonthParam;
private final String operator;
private final Long tenantId;
private final LocalDateTime now;
private final CanteenReportFaceDetailMapper faceDetailMapper;
private final int batchSize;
private final List<CanteenReportFaceDetailEntity> buffer = new ArrayList<>();
private int totalRows;
private int successRows;
private int skipRows;
private String minDate;
private String maxDate;
private String resolvedBizMonth;
FaceDetailAccumulator(Long importLogId, String bizMonthParam, String operator, Long tenantId,
LocalDateTime now, CanteenReportFaceDetailMapper faceDetailMapper, int batchSize) {
this.importLogId = importLogId;
this.bizMonthParam = bizMonthParam;
this.operator = operator;
this.tenantId = tenantId;
this.now = now;
this.faceDetailMapper = faceDetailMapper;
this.batchSize = batchSize;
}
@Override
public void invoke(CanteenReportFaceExcel data, AnalysisContext context) {
totalRows++;
CanteenReportFaceDetailEntity e = CanteenReportFaceMapper.toDetail(data, bizMonthParam, importLogId);
if (e == null || StrUtil.isBlank(e.getBizDate()) || StrUtil.isBlank(e.getBizMonth())) {
skipRows++;
return;
}
trackDate(e.getBizDate());
if (resolvedBizMonth == null) {
resolvedBizMonth = e.getBizMonth();
}
e.setId(IdWorker.getId());
e.setCreateBy(operator);
e.setUpdateBy(operator);
e.setCreateTime(now);
e.setUpdateTime(now);
e.setDelFlag(CommonConstants.STATUS_NORMAL);
e.setTenantId(tenantId);
buffer.add(e);
if (buffer.size() >= batchSize) {
flush();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
flush();
}
void flush() {
if (CollUtil.isEmpty(buffer)) {
return;
}
faceDetailMapper.insertBatch(buffer);
successRows += buffer.size();
buffer.clear();
}
private void trackDate(String d) {
if (StrUtil.isBlank(d)) {
return;
}
if (minDate == null || d.compareTo(minDate) < 0) {
minDate = d;
}
if (maxDate == null || d.compareTo(maxDate) > 0) {
maxDate = d;
}
}
int getTotalRows() {
return totalRows;
}
int getSuccessRows() {
return successRows;
}
int getSkipRows() {
return skipRows;
}
String getMinDate() {
return minDate;
}
String getMaxDate() {
return maxDate;
}
String getResolvedBizMonth() {
return resolvedBizMonth;
}
}
}
@@ -0,0 +1,47 @@
package com.bsd.pjcloud.canteen.service.impl;
import cn.hutool.core.util.StrUtil;
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.entity.CanteenReportSettleParkEntity;
import com.bsd.pjcloud.canteen.mapper.CanteenReportSettleParkMapper;
import com.bsd.pjcloud.canteen.report.CanteenReportConstants;
import com.bsd.pjcloud.canteen.service.CanteenReportSettleParkService;
import com.bsd.pjcloud.common.core.util.R;
import org.springframework.stereotype.Service;
@Service
public class CanteenReportSettleParkServiceImpl extends ServiceImpl<CanteenReportSettleParkMapper, CanteenReportSettleParkEntity>
implements CanteenReportSettleParkService {
@Override
public Page<CanteenReportSettleParkEntity> pageList(Page page, String settleCounter, String park) {
return page(page, Wrappers.<CanteenReportSettleParkEntity>lambdaQuery()
.like(StrUtil.isNotBlank(settleCounter), CanteenReportSettleParkEntity::getSettleCounter, settleCounter)
.eq(StrUtil.isNotBlank(park), CanteenReportSettleParkEntity::getPark, park)
.orderByAsc(CanteenReportSettleParkEntity::getPark)
.orderByAsc(CanteenReportSettleParkEntity::getSettleCounter));
}
@Override
public R saveOrUpdateRow(CanteenReportSettleParkEntity entity) {
if (entity == null || StrUtil.isBlank(entity.getSettleCounter()) || StrUtil.isBlank(entity.getPark())) {
return R.failed("结算台与厂区不能为空");
}
entity.setSettleCounter(entity.getSettleCounter().trim());
entity.setPark(entity.getPark().trim());
if (!CanteenReportConstants.PARKS.contains(entity.getPark())
&& !CanteenReportConstants.SHARE_PARKS.contains(entity.getPark())) {
return R.failed("厂区不在枚举内: " + entity.getPark());
}
long cnt = count(Wrappers.<CanteenReportSettleParkEntity>lambdaQuery()
.eq(CanteenReportSettleParkEntity::getSettleCounter, entity.getSettleCounter())
.ne(entity.getId() != null, CanteenReportSettleParkEntity::getId, entity.getId()));
if (cnt > 0) {
return R.failed("该结算台已配置厂区");
}
saveOrUpdate(entity);
return R.ok(entity);
}
}
@@ -39,13 +39,14 @@ public class CanteenReportSwipeServiceImpl extends ServiceImpl<CanteenReportSwip
@Override
public Page<CanteenReportSwipeEntity> pageList(Page page, String bizMonth, String park, String incomeType,
String costCenter, String empNo) {
String costCenter, String empNo, Long importLogId) {
return page(page, Wrappers.<CanteenReportSwipeEntity>lambdaQuery()
.eq(StrUtil.isNotBlank(bizMonth), CanteenReportSwipeEntity::getBizMonth, bizMonth)
.eq(StrUtil.isNotBlank(park), CanteenReportSwipeEntity::getPark, park)
.eq(StrUtil.isNotBlank(incomeType), CanteenReportSwipeEntity::getIncomeType, incomeType)
.like(StrUtil.isNotBlank(costCenter), CanteenReportSwipeEntity::getCostCenter, costCenter)
.like(StrUtil.isNotBlank(empNo), CanteenReportSwipeEntity::getEmpNo, empNo)
.eq(importLogId != null, CanteenReportSwipeEntity::getImportLogId, importLogId)
.orderByDesc(CanteenReportSwipeEntity::getId));
}
@@ -0,0 +1,118 @@
<?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.CanteenReportFaceDetailMapper">
<insert id="insertBatch">
INSERT INTO canteen_report_face_detail (
id, import_log_id, biz_month, biz_date,
order_no, third_order_no, order_create_time, pay_time, deduct_time,
org_level1, org_level2, org_level3, canteen, canteen_area, consume_point,
emp_name, phone, legal_entity, emp_no, card_no, group_name, dept_name,
pay_method, pay_type, order_amount, change_amount,
stored_change, subsidy_change, gift_change,
stored_balance, subsidy_balance, gift_balance,
change_org, discount, discount_amount, discount_type, coupon_type,
deduct_amount, fee_amount, meal_subsidy,
consume_type, meal_segment, device_name, device_status, remark, operator_name, visitor_pay_method,
create_by, create_time, update_by, update_time, del_flag, tenant_id
) VALUES
<foreach collection="list" item="e" separator=",">
(
#{e.id}, #{e.importLogId}, #{e.bizMonth}, #{e.bizDate},
#{e.orderNo}, #{e.thirdOrderNo}, #{e.orderCreateTime}, #{e.payTime}, #{e.deductTime},
#{e.orgLevel1}, #{e.orgLevel2}, #{e.orgLevel3}, #{e.canteen}, #{e.canteenArea}, #{e.consumePoint},
#{e.empName}, #{e.phone}, #{e.legalEntity}, #{e.empNo}, #{e.cardNo}, #{e.groupName}, #{e.deptName},
#{e.payMethod}, #{e.payType}, #{e.orderAmount}, #{e.changeAmount},
#{e.storedChange}, #{e.subsidyChange}, #{e.giftChange},
#{e.storedBalance}, #{e.subsidyBalance}, #{e.giftBalance},
#{e.changeOrg}, #{e.discount}, #{e.discountAmount}, #{e.discountType}, #{e.couponType},
#{e.deductAmount}, #{e.feeAmount}, #{e.mealSubsidy},
#{e.consumeType}, #{e.mealSegment}, #{e.deviceName}, #{e.deviceStatus}, #{e.remark}, #{e.operatorName}, #{e.visitorPayMethod},
#{e.createBy}, #{e.createTime}, #{e.updateBy}, #{e.updateTime}, #{e.delFlag}, #{e.tenantId}
)
</foreach>
</insert>
<delete id="deletePhysicallyByImportLogId">
DELETE FROM canteen_report_face_detail WHERE import_log_id = #{importLogId}
</delete>
<delete id="deletePhysicallyByDateRange">
DELETE FROM canteen_report_face_detail
WHERE biz_month = #{bizMonth}
AND biz_date &gt;= #{dateStart}
AND biz_date &lt;= #{dateEnd}
</delete>
<delete id="deletePhysicallyDateRangeExceptLog">
DELETE FROM canteen_report_face_detail
WHERE biz_month = #{bizMonth}
AND biz_date &gt;= #{dateStart}
AND biz_date &lt;= #{dateEnd}
AND (import_log_id IS NULL OR import_log_id &lt;&gt; #{importLogId})
</delete>
<select id="selectAggByMonth" resultType="com.bsd.pjcloud.canteen.dto.report.CanteenReportFaceAggRow">
SELECT
IFNULL(emp_no, '') AS empNo,
IFNULL(emp_name, '') AS empName,
IFNULL(legal_entity, '') AS legalEntity,
IFNULL(dept_name, '') AS deptName,
IFNULL(org_level3, '') AS orgLevel3,
IFNULL(device_name, '') AS deviceName,
CASE
WHEN IFNULL(pay_method, '') LIKE '%B扫C%'
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 '支付宝'
ELSE '员工刷卡'
END AS incomeType,
SUM(IFNULL(order_amount, 0)) AS amount
FROM canteen_report_face_detail
WHERE IFNULL(del_flag, '0') = '0'
<if test="importLogId != null">
AND import_log_id = #{importLogId}
</if>
<if test="importLogId == null and bizMonth != null and bizMonth != ''">
AND biz_month = #{bizMonth}
</if>
<!-- 有 importLogId 时不再按日期过滤,避免日志日期段与明细 biz_date 不一致导致查空 -->
<if test="importLogId == null and dateStart != null and dateStart != ''">
AND (biz_date IS NULL OR biz_date &gt;= #{dateStart})
</if>
<if test="importLogId == null and dateEnd != null and dateEnd != ''">
AND (biz_date IS NULL OR biz_date &lt;= #{dateEnd})
</if>
GROUP BY
IFNULL(emp_no, ''),
IFNULL(emp_name, ''),
IFNULL(legal_entity, ''),
IFNULL(dept_name, ''),
IFNULL(org_level3, ''),
IFNULL(device_name, ''),
CASE
WHEN IFNULL(pay_method, '') LIKE '%B扫C%'
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 '支付宝'
ELSE '员工刷卡'
END
</select>
<select id="countDetail" resultType="long">
SELECT COUNT(1) FROM canteen_report_face_detail
WHERE IFNULL(del_flag, '0') = '0'
<if test="importLogId != null">
AND import_log_id = #{importLogId}
</if>
<if test="bizMonth != null and bizMonth != ''">
AND biz_month = #{bizMonth}
</if>
</select>
</mapper>
@@ -6,18 +6,43 @@
DELETE FROM canteen_report_swipe WHERE biz_month = #{bizMonth}
</delete>
<delete id="deletePhysicallyFaceByBizMonth">
DELETE FROM canteen_report_swipe WHERE source = 'FACE' AND biz_month = #{bizMonth}
</delete>
<delete id="deletePhysicallyFaceByDateRange">
DELETE FROM canteen_report_swipe
WHERE source = 'FACE'
AND biz_month = #{bizMonth}
AND biz_date &gt;= #{dateStart}
AND biz_date &lt;= #{dateEnd}
</delete>
<delete id="deletePhysicallyByImportLogId">
DELETE FROM canteen_report_swipe WHERE import_log_id = #{importLogId}
</delete>
<delete id="deletePhysicallyFaceRangeExceptLog">
DELETE FROM canteen_report_swipe
WHERE source = 'FACE'
AND biz_month = #{bizMonth}
AND biz_date &gt;= #{dateStart}
AND biz_date &lt;= #{dateEnd}
AND (import_log_id IS NULL OR import_log_id &lt;&gt; #{importLogId})
</delete>
<insert id="insertBatch">
INSERT INTO canteen_report_swipe (
id, biz_month, emp_no, emp_name, legal_entity, dept_name,
id, biz_month, biz_date, emp_no, emp_name, legal_entity, dept_name,
consume_area, settle_counter, consume_amount, income_type,
cost_center, cost_category, park, remark_tag,
cost_center, cost_category, park, remark_tag, import_log_id, source,
create_by, create_time, update_by, update_time, del_flag, tenant_id
) VALUES
<foreach collection="list" item="e" separator=",">
(
#{e.id}, #{e.bizMonth}, #{e.empNo}, #{e.empName}, #{e.legalEntity}, #{e.deptName},
#{e.id}, #{e.bizMonth}, #{e.bizDate}, #{e.empNo}, #{e.empName}, #{e.legalEntity}, #{e.deptName},
#{e.consumeArea}, #{e.settleCounter}, #{e.consumeAmount}, #{e.incomeType},
#{e.costCenter}, #{e.costCategory}, #{e.park}, #{e.remarkTag},
#{e.costCenter}, #{e.costCategory}, #{e.park}, #{e.remarkTag}, #{e.importLogId}, #{e.source},
#{e.createBy}, #{e.createTime}, #{e.updateBy}, #{e.updateTime}, #{e.delFlag}, #{e.tenantId}
)
</foreach>
@@ -5,7 +5,7 @@ import cn.hutool.core.util.StrUtil;
import java.util.regex.Pattern;
/**
* 姓名/昵称:支持中文,或英文(单词间可空格,如 John Smith)
* 姓名/昵称:支持中文、英文、数字(单词间可空格,如 John Smith、张三1)
*/
public final class PersonNameUtils {
@@ -21,8 +21,9 @@ public final class PersonNameUtils {
private static final int MAX_LEN = 32;
/** 中文 / 英文 / 数字,单词间允许空格 */
private static final Pattern PERSON_NAME_PATTERN = Pattern.compile(
"^(?:[\\u4E00-\\u9FA5]{1,32}|[A-Za-z]+(?:\\s[A-Za-z]+){0,8}|[\\u4E00-\\u9FA5A-Za-z]+(?:\\s[\\u4E00-\\u9FA5A-Za-z]+){0,8})$");
"^[\\u4E00-\\u9FA5A-Za-z0-9]+(?:\\s[\\u4E00-\\u9FA5A-Za-z0-9]+){0,8}$");
private PersonNameUtils() {
}
@@ -72,7 +73,7 @@ public final class PersonNameUtils {
if (CODE_NAME_EMPTY.equals(code) || CODE_NICKNAME_EMPTY.equals(code)) {
return fieldLabel + "不能为空";
}
return fieldLabel + "仅支持中文,或英文(单词间可用空格,如 John Smith)";
return fieldLabel + "仅支持中文、英文、数字(单词间可用空格)";
}
}
@@ -357,7 +357,7 @@ public class SysUserImportExportServiceImpl implements SysUserImportExportServic
if (StrUtil.isBlank(excel.getName())) {
excel.setName(excel.getPhone() + "学生家长");
} else if (!PersonNameUtils.isValid(excel.getName())) {
errorMessages.add("姓名仅支持中文,或英文(单词间可用空格),且不能超过32个字符");
errorMessages.add("姓名仅支持中文、英文、数字(单词间可用空格),且不能超过32个字符");
}
if (StrUtil.isBlank(excel.getPhone())) {
errorMessages.add("手机号不能为空");
@@ -437,7 +437,7 @@ public class SysUserImportExportServiceImpl implements SysUserImportExportServic
for (TeacherUserExcelVO excel : teacherUserExcelList) {
Set<String> errorMessages = new LinkedHashSet<>();
if (!PersonNameUtils.isValid(excel.getName())) {
errorMessages.add("姓名仅支持中文,或英文(单词间可用空格),且不能超过32个字符");
errorMessages.add("姓名仅支持中文、英文、数字(单词间可用空格),且不能超过32个字符");
}
if (StrUtil.isBlank(excel.getTeacherNo())) {
errorMessages.add("学号不能为空");