敏实报表

This commit is contained in:
zjf committed 2026-08-22 23:19:41 +08:00
1 parent cb4aaef486
commit aca18e8a6b
13 files changed
+650

No files matched your search

@@ -0,0 +1,26 @@
package com.bsd.pjcloud.canteen.dto.report;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "供应商分摊保存")
public class CanteenReportSupplierAllocSaveDTO {
@Schema(description = "业务月")
private String bizMonth;
@Schema(description = "分配单元")
private List<Cell> cells = new ArrayList<>();
@Data
public static class Cell {
private String costCenter;
private String supplier;
private BigDecimal amount;
}
}
@@ -0,0 +1,50 @@
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;
@Data
@TableName("canteen_report_supplier_alloc")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "食堂报表-供应商分摊手工分配")
public class CanteenReportSupplierAllocEntity extends Model<CanteenReportSupplierAllocEntity> {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@Schema(description = "业务月份 yyyy-MM")
private String bizMonth;
@Schema(description = "成本中心")
private String costCenter;
@Schema(description = "供应商")
private String supplier;
@Schema(description = "分配金额(元)")
private BigDecimal amount;
@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;
}
@@ -58,6 +58,11 @@ public final class CanteenReportConstants {
"食材", "能耗", "外协"
));
/** 供应商分摊右侧表:最末手填列(无固定开票额,参与行差额) */
public static final List<String> SUPPLIER_ALLOC_MANUAL_COLS = Collections.unmodifiableList(Arrays.asList(
"招待费"
));
public static final List<String> COST_CENTERS = Collections.unmodifiableList(Arrays.asList(
"嘉兴敏华",
"嘉兴敏惠-铝件",
@@ -0,0 +1,25 @@
package com.bsd.pjcloud.canteen.vo.report;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.Map;
@Data
@Schema(description = "供应商分摊-分配行")
public class CanteenReportSupplierAllocRowVO {
@Schema(description = "成本中心")
private String costCenter;
@Schema(description = "开票小计(来自左侧成本分摊开票金额)")
private BigDecimal invoiceSubtotal = BigDecimal.ZERO;
@Schema(description = "各供应商分配金额")
private Map<String, BigDecimal> supplierAmounts = new LinkedHashMap<>();
@Schema(description = "差额=开票小计-各供应商之和")
private BigDecimal diff = BigDecimal.ZERO;
}
@@ -0,0 +1,42 @@
package com.bsd.pjcloud.canteen.vo.report;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Data
@Schema(description = "供应商分摊右侧表")
public class CanteenReportSupplierAllocTableVO {
@Schema(description = "供应商列顺序")
private List<String> suppliers = new ArrayList<>();
@Schema(description = "右侧手填列(招待费等,无固定开票上限)")
private List<String> manualColumns = new ArrayList<>();
@Schema(description = "供应商固定开票合计(食材能耗汇总)")
private Map<String, BigDecimal> supplierTotals = new LinkedHashMap<>();
@Schema(description = "供应商开票总计")
private BigDecimal grandTotal = BigDecimal.ZERO;
@Schema(description = "成本中心开票小计合计")
private BigDecimal invoiceGrandTotal = BigDecimal.ZERO;
@Schema(description = "分配行")
private List<CanteenReportSupplierAllocRowVO> rows = new ArrayList<>();
@Schema(description = "小计行(各列合计)")
private CanteenReportSupplierAllocRowVO subtotalRow;
@Schema(description = "检查行:列合计-固定开票,应≤0防止填超")
private Map<String, BigDecimal> checkRow = new LinkedHashMap<>();
@Schema(description = "检查-开票小计列:已分配合计-开票小计合计")
private BigDecimal checkInvoiceSubtotal = BigDecimal.ZERO;
}
@@ -3,7 +3,9 @@ package com.bsd.pjcloud.canteen.controller;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportBusinessExportExcel;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportIncomeExportExcel;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportShareExportExcel;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportSupplierAllocSaveDTO;
import com.bsd.pjcloud.canteen.service.CanteenReportCalcService;
import com.bsd.pjcloud.canteen.service.CanteenReportSupplierAllocService;
import com.bsd.pjcloud.canteen.vo.report.CanteenReportBusinessRowVO;
import com.bsd.pjcloud.canteen.vo.report.CanteenReportIncomeRowVO;
import com.bsd.pjcloud.canteen.vo.report.CanteenReportShareRowVO;
@@ -15,6 +17,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@@ -32,6 +36,7 @@ import java.util.Map;
public class CanteenReportController {
private final CanteenReportCalcService calcService;
private final CanteenReportSupplierAllocService supplierAllocService;
@Operation(summary = "汇总校验")
@GetMapping("/summary")
@@ -57,6 +62,18 @@ public class CanteenReportController {
return R.ok(calcService.supplierShareAudit(bizMonth));
}
@Operation(summary = "供应商分摊-右侧开票分配表")
@GetMapping("/supplierShare/alloc")
public R supplierShareAlloc(@RequestParam String bizMonth) {
return R.ok(supplierAllocService.buildTable(bizMonth));
}
@Operation(summary = "保存供应商分摊开票分配")
@PutMapping("/supplierShare/alloc")
public R saveSupplierShareAlloc(@RequestBody CanteenReportSupplierAllocSaveDTO dto) {
return supplierAllocService.saveAlloc(dto);
}
@Operation(summary = "经营报表")
@GetMapping("/business")
public R business(@RequestParam String bizMonth) {
@@ -39,6 +39,13 @@ public class CanteenReportFaceImportController {
return R.ok(faceImportService.getById(id));
}
@SysLog("删除人脸消费导入记录")
@Operation(summary = "删除导入日志及该批明细(含曾按该批写入的 FACE 刷卡行)")
@DeleteMapping("/log")
public R removeLog(@RequestBody Long[] ids) {
return faceImportService.deleteImportLogs(java.util.Arrays.asList(ids));
}
@Operation(summary = "人脸逐笔明细分页")
@GetMapping("/detail/page")
public R detailPage(@ParameterObject Page page,
@@ -0,0 +1,12 @@
package com.bsd.pjcloud.canteen.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bsd.pjcloud.canteen.entity.CanteenReportSupplierAllocEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface CanteenReportSupplierAllocMapper extends BaseMapper<CanteenReportSupplierAllocEntity> {
int deletePhysicallyByBizMonth(@Param("bizMonth") String bizMonth);
}
@@ -38,4 +38,7 @@ public interface CanteenReportFaceImportService extends IService<CanteenReportIm
String deviceName, String empName, String phone, List<String> orgLevel2s);
List<CanteenReportOrgLevel2VO> listOrgLevel2(String bizMonth, Long importLogId);
/** 删除导入日志及该批人脸明细;若曾按该批汇总,同步删除关联 FACE 刷卡行 */
R deleteImportLogs(List<Long> ids);
}
@@ -0,0 +1,14 @@
package com.bsd.pjcloud.canteen.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportSupplierAllocSaveDTO;
import com.bsd.pjcloud.canteen.entity.CanteenReportSupplierAllocEntity;
import com.bsd.pjcloud.canteen.vo.report.CanteenReportSupplierAllocTableVO;
import com.bsd.pjcloud.common.core.util.R;
public interface CanteenReportSupplierAllocService extends IService<CanteenReportSupplierAllocEntity> {
CanteenReportSupplierAllocTableVO buildTable(String bizMonth);
R saveAlloc(CanteenReportSupplierAllocSaveDTO dto);
}
@@ -75,6 +75,38 @@ public class CanteenReportFaceImportServiceImpl extends ServiceImpl<CanteenRepor
.orderByDesc(CanteenReportImportLogEntity::getCreateTime));
}
@Override
@org.springframework.transaction.annotation.Transactional(rollbackFor = Exception.class)
public R deleteImportLogs(List<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return R.failed("请选择要删除的导入记录");
}
int logCount = 0;
int detailRows = 0;
int swipeRows = 0;
for (Long id : ids) {
if (id == null) {
continue;
}
CanteenReportImportLogEntity logEntity = getById(id);
if (logEntity == null) {
continue;
}
if ("RUNNING".equals(logEntity.getStatus())) {
return R.failed(String.format("「%s」仍在导入中,请完成或失败后再删除", StrUtil.blankToDefault(logEntity.getFileName(), "导入任务")));
}
detailRows += faceDetailMapper.deletePhysicallyByImportLogId(id);
swipeRows += swipeMapper.deletePhysicallyByImportLogId(id);
removeById(id);
logCount++;
}
if (logCount == 0) {
return R.failed("未找到可删除的导入记录");
}
return R.ok(null, String.format("已删除 %d 条导入记录;人脸明细 %d 行,关联 FACE 刷卡 %d 行", logCount, detailRows,
swipeRows));
}
@Override
public Page<CanteenReportFaceDetailEntity> pageDetails(Page page, String bizMonth, Long importLogId, String empNo,
String payMethod, String deviceName, String empName, String phone, List<String> orgLevel2s) {
@@ -0,0 +1,408 @@
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.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.canteen.dto.report.CanteenReportSupplierAllocSaveDTO;
import com.bsd.pjcloud.canteen.entity.CanteenReportFoodEnergyEntity;
import com.bsd.pjcloud.canteen.entity.CanteenReportSupplierAllocEntity;
import com.bsd.pjcloud.canteen.mapper.CanteenReportSupplierAllocMapper;
import com.bsd.pjcloud.canteen.report.CanteenReportConstants;
import com.bsd.pjcloud.canteen.service.CanteenReportCalcService;
import com.bsd.pjcloud.canteen.service.CanteenReportFoodEnergyService;
import com.bsd.pjcloud.canteen.service.CanteenReportSupplierAllocService;
import com.bsd.pjcloud.canteen.vo.report.CanteenReportShareRowVO;
import com.bsd.pjcloud.canteen.vo.report.CanteenReportSupplierAllocRowVO;
import com.bsd.pjcloud.canteen.vo.report.CanteenReportSupplierAllocTableVO;
import com.bsd.pjcloud.common.core.util.R;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class CanteenReportSupplierAllocServiceImpl
extends ServiceImpl<CanteenReportSupplierAllocMapper, CanteenReportSupplierAllocEntity>
implements CanteenReportSupplierAllocService {
private static final int SCALE = 2;
private final CanteenReportCalcService calcService;
private final CanteenReportFoodEnergyService foodEnergyService;
@Override
public CanteenReportSupplierAllocTableVO buildTable(String bizMonth) {
Map<String, BigDecimal> supplierTotals = loadSupplierTotals(bizMonth);
List<String> suppliers = supplierTotals.keySet().stream()
.sorted(Comparator.comparing((String s) -> supplierTotals.get(s)).reversed()
.thenComparing(String::compareTo))
.collect(Collectors.toList());
List<String> manualCols = new ArrayList<>(CanteenReportConstants.SUPPLIER_ALLOC_MANUAL_COLS);
List<String> allCols = new ArrayList<>(suppliers);
allCols.addAll(manualCols);
BigDecimal grandTotal = supplierTotals.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add)
.setScale(SCALE, RoundingMode.HALF_UP);
Map<String, BigDecimal> saved = loadSavedMap(bizMonth);
List<CanteenReportShareRowVO> shareRows = calcService.supplierShareReport(bizMonth);
List<CanteenReportSupplierAllocRowVO> rows = new ArrayList<>();
Map<String, BigDecimal> colSums = new LinkedHashMap<>();
for (String col : allCols) {
colSums.put(col, BigDecimal.ZERO);
}
BigDecimal invoiceGrand = BigDecimal.ZERO;
BigDecimal allocGrand = BigDecimal.ZERO;
for (CanteenReportShareRowVO share : shareRows) {
String center = share.getCostCenter();
if (isMetaRow(center)) {
continue;
}
CanteenReportSupplierAllocRowVO row = buildRow(center, nz(share.getInvoiceAmount()), allCols, saved);
rows.add(row);
invoiceGrand = invoiceGrand.add(row.getInvoiceSubtotal());
for (Map.Entry<String, BigDecimal> e : row.getSupplierAmounts().entrySet()) {
colSums.merge(e.getKey(), nz(e.getValue()), BigDecimal::add);
allocGrand = allocGrand.add(nz(e.getValue()));
}
}
CanteenReportSupplierAllocRowVO subtotalRow = new CanteenReportSupplierAllocRowVO();
subtotalRow.setCostCenter("小计");
subtotalRow.setInvoiceSubtotal(invoiceGrand.setScale(SCALE, RoundingMode.HALF_UP));
Map<String, BigDecimal> subMap = new LinkedHashMap<>();
for (String col : allCols) {
subMap.put(col, nz(colSums.get(col)).setScale(SCALE, RoundingMode.HALF_UP));
}
subtotalRow.setSupplierAmounts(subMap);
subtotalRow.setDiff(invoiceGrand.subtract(allocGrand).setScale(SCALE, RoundingMode.HALF_UP));
Map<String, BigDecimal> checkRow = new LinkedHashMap<>();
for (String sup : suppliers) {
BigDecimal sum = nz(colSums.get(sup));
BigDecimal fixed = nz(supplierTotals.get(sup));
checkRow.put(sup, sum.subtract(fixed).setScale(SCALE, RoundingMode.HALF_UP));
}
CanteenReportSupplierAllocTableVO vo = new CanteenReportSupplierAllocTableVO();
vo.setSuppliers(suppliers);
vo.setManualColumns(manualCols);
vo.setSupplierTotals(roundMap(supplierTotals));
vo.setGrandTotal(grandTotal);
vo.setInvoiceGrandTotal(invoiceGrand.setScale(SCALE, RoundingMode.HALF_UP));
vo.setRows(rows);
vo.setSubtotalRow(subtotalRow);
vo.setCheckRow(checkRow);
vo.setCheckInvoiceSubtotal(allocGrand.subtract(invoiceGrand).setScale(SCALE, RoundingMode.HALF_UP));
return vo;
}
@Override
@Transactional(rollbackFor = Exception.class)
public R saveAlloc(CanteenReportSupplierAllocSaveDTO dto) {
if (dto == null || StrUtil.isBlank(dto.getBizMonth())) {
return R.failed("业务月不能为空");
}
String bizMonth = dto.getBizMonth().trim();
// 逻辑删除无法释放 uk(biz_month,cost_center,supplier),保存前须物理清空该月
baseMapper.deletePhysicallyByBizMonth(bizMonth);
if (CollUtil.isEmpty(dto.getCells())) {
return R.ok(null, "已清空该月供应商分摊");
}
Map<String, CanteenReportSupplierAllocEntity> dedup = new LinkedHashMap<>();
for (CanteenReportSupplierAllocSaveDTO.Cell cell : dto.getCells()) {
if (cell == null || StrUtil.isBlank(cell.getCostCenter()) || StrUtil.isBlank(cell.getSupplier())) {
continue;
}
String center = cell.getCostCenter().trim();
String supplier = cell.getSupplier().trim();
if (isMetaRow(center)) {
continue;
}
BigDecimal amt = nz(cell.getAmount()).setScale(SCALE, RoundingMode.HALF_UP);
if (amt.compareTo(BigDecimal.ZERO) == 0) {
continue;
}
String key = center + "|" + supplier;
CanteenReportSupplierAllocEntity entity = new CanteenReportSupplierAllocEntity();
entity.setBizMonth(bizMonth);
entity.setCostCenter(center);
entity.setSupplier(supplier);
entity.setAmount(amt);
dedup.put(key, entity);
}
List<CanteenReportSupplierAllocEntity> batch = new ArrayList<>(dedup.values());
if (!batch.isEmpty()) {
saveBatch(batch, 500);
}
CanteenReportSupplierAllocTableVO table = buildTable(bizMonth);
boolean over = table.getCheckRow().values().stream().anyMatch(v -> nz(v).compareTo(BigDecimal.ZERO) > 0);
String msg = over ? "已保存;部分供应商分配合计超过固定开票额,请查看检查行" : "保存成功";
return R.ok(table, msg);
}
private Map<String, BigDecimal> loadSupplierTotals(String bizMonth) {
List<CanteenReportFoodEnergyEntity> foods = foodEnergyService.list(
Wrappers.<CanteenReportFoodEnergyEntity>lambdaQuery()
.eq(CanteenReportFoodEnergyEntity::getBizMonth, bizMonth));
Map<String, BigDecimal> totals = new LinkedHashMap<>();
for (CanteenReportFoodEnergyEntity f : foods) {
String classify = StrUtil.blankToDefault(f.getClassify(), "食材").trim();
if (!CanteenReportConstants.SHARE_FOOD_CLASSIFY.contains(classify)) {
continue;
}
String supplier = StrUtil.blankToDefault(f.getSupplier(), "未知供应商").trim();
totals.merge(supplier, nz(f.getSettleAmount()), BigDecimal::add);
}
return totals;
}
private Map<String, BigDecimal> loadSavedMap(String bizMonth) {
List<CanteenReportSupplierAllocEntity> list = list(
Wrappers.<CanteenReportSupplierAllocEntity>lambdaQuery()
.eq(CanteenReportSupplierAllocEntity::getBizMonth, bizMonth));
Map<String, BigDecimal> map = new HashMap<>();
for (CanteenReportSupplierAllocEntity e : list) {
map.put(e.getCostCenter() + "|" + e.getSupplier(), nz(e.getAmount()));
}
return map;
}
private CanteenReportSupplierAllocRowVO buildRow(String center, BigDecimal invoiceSubtotal, List<String> columns,
Map<String, BigDecimal> saved) {
CanteenReportSupplierAllocRowVO row = new CanteenReportSupplierAllocRowVO();
row.setCostCenter(center);
row.setInvoiceSubtotal(invoiceSubtotal.setScale(SCALE, RoundingMode.HALF_UP));
Map<String, BigDecimal> amounts = new LinkedHashMap<>();
BigDecimal rowSum = BigDecimal.ZERO;
for (String col : columns) {
BigDecimal amt = nz(saved.get(center + "|" + col)).setScale(SCALE, RoundingMode.HALF_UP);
amounts.put(col, amt);
rowSum = rowSum.add(amt);
}
row.setSupplierAmounts(amounts);
row.setDiff(invoiceSubtotal.subtract(rowSum).setScale(SCALE, RoundingMode.HALF_UP));
return row;
}
private static boolean isMetaRow(String center) {
return "小计".equals(center) || "开票调整(全局)".equals(center)
|| (center != null && center.startsWith("检查"));
}
private static BigDecimal nz(BigDecimal v) {
return v == null ? BigDecimal.ZERO : v;
}
private static Map<String, BigDecimal> roundMap(Map<String, BigDecimal> src) {
Map<String, BigDecimal> out = new LinkedHashMap<>();
for (Map.Entry<String, BigDecimal> e : src.entrySet()) {
out.put(e.getKey(), nz(e.getValue()).setScale(SCALE, RoundingMode.HALF_UP));
}
return out;
}
}
@@ -0,0 +1,9 @@
<?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.CanteenReportSupplierAllocMapper">
<delete id="deletePhysicallyByBizMonth">
DELETE FROM canteen_report_supplier_alloc WHERE biz_month = #{bizMonth}
</delete>
</mapper>