Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/AuditDataAggregateController.java
T
QiufengandClaude Opus 4.8 5e51dc3f56 SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。

== 此快照内容 ==
- 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091)
- 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static)
- 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB)
- 交接文档 go.md + go-code-reference/endpoints/entities/database.md
- 多代理建设脚本 .claude/wf-*.js

== 状态 ==
- 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板)
- 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用
- W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成)
- 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456

== 排除(gitignore, 可再生) ==
node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui)
完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules)

时间戳: 20260615-191517

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:19:15 +08:00

607 lines
30 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.kaidi.oa.web;
import com.kaidi.oa.common.ApiResp;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.domain.ComplianceSelfCheckTask;
import com.kaidi.oa.domain.Contract;
import com.kaidi.oa.domain.ExpenseClaim;
import com.kaidi.oa.domain.FixedAsset;
import com.kaidi.oa.domain.LitigationCase;
import com.kaidi.oa.domain.MfgSalesOrder;
import com.kaidi.oa.domain.Payment;
import com.kaidi.oa.repository.BankReconciliationRepository;
import com.kaidi.oa.repository.ComplianceChecklistRepository;
import com.kaidi.oa.repository.ComplianceSelfCheckTaskRepository;
import com.kaidi.oa.repository.ContractRepository;
import com.kaidi.oa.repository.ExpenseClaimRepository;
import com.kaidi.oa.repository.FixedAssetRepository;
import com.kaidi.oa.repository.LitigationCaseRepository;
import com.kaidi.oa.repository.MfgSalesOrderRepository;
import com.kaidi.oa.repository.PaymentRepository;
import com.kaidi.oa.repository.VoucherRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 审计跨模块数据聚合接口(内控部·审计监察部·第11模块)。
*
* 解决审计缺口:「无系统化审计只读聚合视图(需要从财务凭证/采购订单/合同/生产工单/人事薪酬/银行流水等
* 模块统一取数);结算中心银行流水/票据台账无审计专用只读端点;各业务部门内控自评问卷无专项数据流向审计」。
*
* 全部端点仅读,无写副作用,路径 /api/oa/audit-data-aggregate
* 纳入 SENSITIVE_READ_PREFIXES(含财务/银行/合同/合规敏感数据,需 ADMIN/APPROVER)。
*
* 端点列表:
* - GET /finance-overview 财务总览(付款/合同/报销/凭证统计,含金额趋势);
* - GET /contract-list 合同台账只读列表(可按被审计单位/期间过滤);
* - GET /payment-list 付款记录只读列表(金额/状态/收款方,可按期间过滤);
* - GET /expense-list 费用报销只读列表(部门/类别,可按期间过滤);
* - GET /bank-statement 银行流水/对账台账只读清单;
* - GET /fixed-asset-risk 固定资产风险清单(已报废/闲置/超龄);
* - GET /legal-exposure 法务风险敞口(诉讼案件+合规处罚+黑名单供应商);
* - GET /compliance-checklist 内控自评问卷汇总(各部门自评状态/通过率);
* - GET /vendor-concentration 供应商集中度分析(付款金额 top-10 收款方占比);
* - GET /payee-anomaly 收款方异常线索(同收款方不同银行账号/大额临近签批限额)。
*/
@RestController
@RequestMapping("/api/oa/audit-data-aggregate")
public class AuditDataAggregateController {
private final ContractRepository contractRepo;
private final PaymentRepository paymentRepo;
private final ExpenseClaimRepository expenseRepo;
private final FixedAssetRepository fixedAssetRepo;
private final VoucherRepository voucherRepo;
private final BankReconciliationRepository bankReconRepo;
private final LitigationCaseRepository litigationRepo;
private final ComplianceChecklistRepository complianceRepo;
private final MfgSalesOrderRepository mfgSalesOrderRepo;
private final ComplianceSelfCheckTaskRepository selfCheckTaskRepo;
public AuditDataAggregateController(ContractRepository contractRepo,
PaymentRepository paymentRepo,
ExpenseClaimRepository expenseRepo,
FixedAssetRepository fixedAssetRepo,
VoucherRepository voucherRepo,
BankReconciliationRepository bankReconRepo,
LitigationCaseRepository litigationRepo,
ComplianceChecklistRepository complianceRepo,
MfgSalesOrderRepository mfgSalesOrderRepo,
ComplianceSelfCheckTaskRepository selfCheckTaskRepo) {
this.contractRepo = contractRepo;
this.paymentRepo = paymentRepo;
this.expenseRepo = expenseRepo;
this.fixedAssetRepo = fixedAssetRepo;
this.voucherRepo = voucherRepo;
this.bankReconRepo = bankReconRepo;
this.litigationRepo = litigationRepo;
this.complianceRepo = complianceRepo;
this.mfgSalesOrderRepo = mfgSalesOrderRepo;
this.selfCheckTaskRepo = selfCheckTaskRepo;
}
/**
* 财务总览:付款/合同/报销统计金额合计,供审计底稿取数参考。
*/
@GetMapping("/finance-overview")
public ApiResp<Map<String, Object>> financeOverview() {
List<Payment> payments = paymentRepo.findAll();
List<Contract> contracts = contractRepo.findAll();
List<ExpenseClaim> expenses = expenseRepo.findAll();
BigDecimal totalPayment = payments.stream()
.map(p -> Money.nz(p.getAmount()))
.reduce(BigDecimal.ZERO, Money::add);
BigDecimal approvedPayment = payments.stream()
.filter(p -> "已付款".equals(p.getStatus()) || "已审批".equals(p.getStatus()))
.map(p -> Money.nz(p.getAmount()))
.reduce(BigDecimal.ZERO, Money::add);
BigDecimal totalContractAmount = contracts.stream()
.map(c -> Money.nz(c.getAmount()))
.reduce(BigDecimal.ZERO, Money::add);
BigDecimal totalPaidAmount = contracts.stream()
.map(c -> Money.nz(c.getPaidAmount()))
.reduce(BigDecimal.ZERO, Money::add);
BigDecimal totalExpense = expenses.stream()
.filter(e -> "已报销".equals(e.getStatus()) || "已审批".equals(e.getStatus()))
.map(e -> Money.nz(e.getAmount()))
.reduce(BigDecimal.ZERO, Money::add);
// 付款按月聚合趋势。
Map<String, BigDecimal> paymentByMonth = new LinkedHashMap<>();
for (Payment p : payments) {
String month = p.getPayDate() == null ? "未知"
: p.getPayDate().substring(0, Math.min(7, p.getPayDate().length()));
paymentByMonth.merge(month, Money.nz(p.getAmount()), Money::add);
}
List<Map<String, Object>> trend = paymentByMonth.entrySet().stream()
.map(e -> Map.<String, Object>of("month", e.getKey(), "amount", e.getValue().doubleValue()))
.toList();
return ApiResp.ok(Map.of(
"totalPayments", payments.size(),
"totalPaymentAmount", totalPayment.doubleValue(),
"approvedPaymentAmount", approvedPayment.doubleValue(),
"totalContracts", contracts.size(),
"totalContractAmount", totalContractAmount.doubleValue(),
"totalPaidAmount", totalPaidAmount.doubleValue(),
"totalExpenseClaims", expenses.size(),
"totalExpenseAmount", totalExpense.doubleValue(),
"paymentTrend", trend
));
}
/**
* 合同台账只读列表(审计专用视图,可按被审计单位/合同类型过滤)。
*/
@GetMapping("/contract-list")
public ApiResp<List<Map<String, Object>>> contractList(
@RequestParam(required = false) String auditee,
@RequestParam(required = false) String type,
@RequestParam(defaultValue = "200") int limit) {
List<Contract> all = contractRepo.findAll();
return ApiResp.ok(all.stream()
.filter(c -> auditee == null || auditee.isBlank()
|| (c.getPartyA() != null && c.getPartyA().contains(auditee))
|| (c.getPartyB() != null && c.getPartyB().contains(auditee)))
.filter(c -> type == null || type.isBlank()
|| type.equals(c.getType()))
.limit(limit)
.map(c -> {
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", c.getId());
m.put("name", c.getName());
m.put("type", c.getType());
m.put("partyA", c.getPartyA());
m.put("partyB", c.getPartyB());
m.put("amount", Money.nz(c.getAmount()).doubleValue());
m.put("paidAmount", Money.nz(c.getPaidAmount()).doubleValue());
m.put("status", c.getStatus());
m.put("signDate", c.getSignDate());
return m;
})
.toList());
}
/**
* 付款记录只读列表(可按收款方/期间/金额下限过滤,默认最近200条)。
*/
@GetMapping("/payment-list")
public ApiResp<List<Map<String, Object>>> paymentList(
@RequestParam(required = false) String payee,
@RequestParam(required = false) String fromDate,
@RequestParam(required = false) String toDate,
@RequestParam(defaultValue = "0") double minAmount,
@RequestParam(defaultValue = "200") int limit) {
BigDecimal minAmtBD = Money.of(minAmount);
List<Payment> all = paymentRepo.findAll();
return ApiResp.ok(all.stream()
.filter(p -> payee == null || payee.isBlank()
|| (p.getPayeeName() != null && p.getPayeeName().contains(payee)))
.filter(p -> fromDate == null || fromDate.isBlank()
|| (p.getPayDate() != null && p.getPayDate().compareTo(fromDate) >= 0))
.filter(p -> toDate == null || toDate.isBlank()
|| (p.getPayDate() != null && p.getPayDate().compareTo(toDate) <= 0))
.filter(p -> Money.nz(p.getAmount()).compareTo(minAmtBD) >= 0)
.limit(limit)
.map(p -> {
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", p.getId());
m.put("code", p.getCode());
m.put("subject", p.getSubject());
m.put("payeeName", p.getPayeeName());
m.put("amount", Money.nz(p.getAmount()).doubleValue());
m.put("payType", p.getPayType());
m.put("status", p.getStatus());
m.put("payDate", p.getPayDate());
return m;
})
.toList());
}
/**
* 费用报销只读列表(按部门/类别/期间过滤)。
*/
@GetMapping("/expense-list")
public ApiResp<List<Map<String, Object>>> expenseList(
@RequestParam(required = false) String dept,
@RequestParam(required = false) String category,
@RequestParam(defaultValue = "200") int limit) {
List<ExpenseClaim> all = expenseRepo.findAll();
return ApiResp.ok(all.stream()
.filter(e -> dept == null || dept.isBlank()
|| dept.equals(e.getDept()))
.filter(e -> category == null || category.isBlank()
|| category.equals(e.getCategory()))
.limit(limit)
.map(e -> {
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", e.getId());
m.put("code", e.getCode());
m.put("applicant", e.getApplicant());
m.put("dept", e.getDept());
m.put("category", e.getCategory());
m.put("amount", Money.nz(e.getAmount()).doubleValue());
m.put("status", e.getStatus());
m.put("expenseDate", e.getExpenseDate());
m.put("summary", e.getSummary());
return m;
})
.toList());
}
/**
* 银行流水/对账台账只读清单(审计专用银行流水取数口)。
*/
@GetMapping("/bank-statement")
public ApiResp<Object> bankStatement(@RequestParam(defaultValue = "200") int limit) {
var all = bankReconRepo.findAll();
var list = all.stream().limit(limit)
.map(b -> {
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", b.getId());
m.put("reconDate", b.getReconcileDate());
m.put("bankBalance", Money.nz(b.getBankBalance()).doubleValue());
m.put("bookBalance", Money.nz(b.getBookBalance()).doubleValue());
m.put("difference", Money.nz(b.getDiff()).doubleValue());
m.put("status", b.getStatus());
return m;
}).toList();
return ApiResp.ok(list);
}
/**
* 固定资产风险清单:报废/闲置/净值为负的资产(审计关注点)。
*/
@GetMapping("/fixed-asset-risk")
public ApiResp<List<Map<String, Object>>> fixedAssetRisk() {
List<FixedAsset> all = fixedAssetRepo.findAll();
return ApiResp.ok(all.stream()
.filter(fa -> "报废".equals(fa.getStatus()) || "闲置".equals(fa.getStatus())
|| Money.nz(fa.getNetValue()).compareTo(BigDecimal.ZERO) < 0)
.map(fa -> {
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", fa.getId());
m.put("name", fa.getName());
m.put("assetNo", fa.getAssetNo());
m.put("category", fa.getCategory());
m.put("status", fa.getStatus());
m.put("originalValue", Money.nz(fa.getOriginalValue()).doubleValue());
m.put("netValue", Money.nz(fa.getNetValue()).doubleValue());
m.put("dept", fa.getDepartment());
return m;
})
.toList());
}
/**
* 法务风险敞口:诉讼案件+合规自评不通过条目。
*/
@GetMapping("/legal-exposure")
public ApiResp<Map<String, Object>> legalExposure() {
List<LitigationCase> cases = litigationRepo.findAll();
long openCases = cases.stream().filter(c -> !"已结案".equals(c.getStatus())).count();
BigDecimal claimTotal = cases.stream()
.map(c -> Money.nz(c.getAmount()))
.reduce(BigDecimal.ZERO, Money::add);
List<Map<String, Object>> activeCases = cases.stream()
.filter(c -> !"已结案".equals(c.getStatus()))
.map(c -> {
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", c.getId());
m.put("caseNo", c.getCaseNo());
m.put("title", c.getCaseName());
m.put("status", c.getStatus());
m.put("claimAmount", Money.nz(c.getAmount()).doubleValue());
m.put("cause", c.getCause());
return m;
}).toList();
return ApiResp.ok(Map.of(
"totalCases", cases.size(),
"openCases", openCases,
"totalClaimAmount", claimTotal.doubleValue(),
"activeCases", activeCases));
}
/**
* 内控自评问卷汇总(各部门自评状态/通过率,联动 ComplianceChecklist)。
*/
@GetMapping("/compliance-checklist")
public ApiResp<Object> complianceChecklist() {
var all = complianceRepo.findAll();
// 按部门聚合完成率。
Map<String, List<com.kaidi.oa.domain.ComplianceChecklist>> byDept = new LinkedHashMap<>();
for (var item : all) {
String dept = item.getResponsibleDept() == null ? "未知" : item.getResponsibleDept();
byDept.computeIfAbsent(dept, k -> new ArrayList<>()).add(item);
}
List<Map<String, Object>> deptSummary = byDept.entrySet().stream().map(e -> {
long total = e.getValue().size();
long passed = e.getValue().stream()
.filter(checklist -> "已通过".equals(checklist.getStatus())
|| "通过".equals(checklist.getStatus())
|| "合规".equals(checklist.getStatus())).count();
double rate = total <= 0 ? 0 : Math.round(passed * 10000d / total) / 100d;
return Map.<String, Object>of("dept", e.getKey(), "total", total,
"passed", passed, "passRate", rate);
}).toList();
return ApiResp.ok(Map.of("total", all.size(), "byDept", deptSummary));
}
/**
* 供应商/收款方集中度分析:按付款金额聚合 top-10 收款方及其占比(供应商集中度审计)。
*/
@GetMapping("/vendor-concentration")
public ApiResp<Object> vendorConcentration() {
List<Payment> payments = paymentRepo.findAll();
Map<String, BigDecimal> byPayee = new LinkedHashMap<>();
BigDecimal total = BigDecimal.ZERO;
for (Payment p : payments) {
if (p.getPayeeName() == null || p.getPayeeName().isBlank()) continue;
byPayee.merge(p.getPayeeName(), Money.nz(p.getAmount()), Money::add);
total = Money.add(total, p.getAmount());
}
final BigDecimal totalFinal = total;
List<Map<String, Object>> top10 = byPayee.entrySet().stream()
.sorted((a, b) -> b.getValue().compareTo(a.getValue()))
.limit(10)
.map(e -> {
double pct = totalFinal.compareTo(BigDecimal.ZERO) == 0 ? 0
: Math.round(e.getValue().doubleValue() / totalFinal.doubleValue() * 10000d) / 100d;
return Map.<String, Object>of(
"payee", e.getKey(),
"amount", e.getValue().doubleValue(),
"percentage", pct);
}).toList();
double top10Pct = top10.stream()
.mapToDouble(m -> (Double) m.get("percentage")).sum();
return ApiResp.ok(Map.of("totalPaymentAmount", total.doubleValue(),
"top10", top10, "top10Concentration", Math.round(top10Pct * 100d) / 100d));
}
/**
* 收款方异常线索:大额付款(超 threshold 万元)中可疑的记录(供人工审查)。
*/
@GetMapping("/payee-anomaly")
public ApiResp<Object> payeeAnomaly(
@RequestParam(defaultValue = "100000") double threshold) {
BigDecimal thresholdBD = Money.of(threshold);
List<Payment> payments = paymentRepo.findAll();
List<Map<String, Object>> suspicious = payments.stream()
.filter(p -> Money.nz(p.getAmount()).compareTo(thresholdBD) >= 0)
.map(p -> {
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", p.getId());
m.put("code", p.getCode());
m.put("payeeName", p.getPayeeName());
m.put("amount", Money.nz(p.getAmount()).doubleValue());
m.put("subject", p.getSubject());
m.put("payDate", p.getPayDate());
m.put("status", p.getStatus());
return m;
})
.sorted((a, b) -> Double.compare((Double) b.get("amount"), (Double) a.get("amount")))
.toList();
return ApiResp.ok(Map.of(
"threshold", threshold,
"count", suspicious.size(),
"items", suspicious));
}
/**
* 销售/经营部 出库单(销售订单)专属只读聚合视图(审计缺口11)。
* 从制造管理中心销售订单取数,支持按客户/阶段/公司主体过滤,返回出库/发货明细清单。
*
* @param customer 客户名称(模糊过滤,空=全部)。
* @param salesStage 销售进度阶段(如「发货」/「已验收」,空=全部)。
* @param companySubject 公司主体(空=全部)。
* @param limit 最多返回条数(默认200)。
*/
@GetMapping("/sales-order-view")
public ApiResp<Map<String, Object>> salesOrderView(
@RequestParam(required = false) String customer,
@RequestParam(required = false) String salesStage,
@RequestParam(required = false) String companySubject,
@RequestParam(defaultValue = "200") int limit) {
List<MfgSalesOrder> all = mfgSalesOrderRepo.findAllByOrderByIdDesc();
List<MfgSalesOrder> filtered = all.stream()
.filter(o -> customer == null || customer.isBlank()
|| (o.getCustomer() != null && o.getCustomer().contains(customer)))
.filter(o -> salesStage == null || salesStage.isBlank()
|| salesStage.equals(o.getSalesStage()))
.filter(o -> companySubject == null || companySubject.isBlank()
|| companySubject.equals(o.getCompanySubject()))
.limit(limit)
.toList();
BigDecimal totalAmount = filtered.stream()
.map(o -> Money.nz(o.getAmount()))
.reduce(BigDecimal.ZERO, Money::add);
// 按阶段分组统计(方便审计看出库分布)。
Map<String, Long> byStage = new LinkedHashMap<>();
for (MfgSalesOrder o : filtered) {
String stage = o.getSalesStage() == null ? "未知" : o.getSalesStage();
byStage.merge(stage, 1L, Long::sum);
}
List<Map<String, Object>> rows = filtered.stream().map(o -> {
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", o.getId());
m.put("orderNo", o.getOrderNo());
m.put("projectCode", o.getProjectCode());
m.put("projectName", o.getProjectName());
m.put("customer", o.getCustomer());
m.put("equipType", o.getEquipType());
m.put("deliveryDate", o.getDeliveryDate());
m.put("amount", Money.nz(o.getAmount()).doubleValue());
m.put("salesStage", o.getSalesStage());
m.put("deliverySite", o.getDeliverySite());
m.put("contractNo", o.getContractNo());
m.put("owner", o.getOwner());
m.put("companySubject", o.getCompanySubject());
return m;
}).toList();
Map<String, Object> result = new LinkedHashMap<>();
result.put("count", rows.size());
result.put("totalAmount", totalAmount.doubleValue());
result.put("byStage", byStage);
result.put("note", "数据来源:制造管理中心·销售订单(含发货/出库/验收状态),只读审计视图");
result.put("items", rows);
return ApiResp.ok(result);
}
/**
* 票据台账(商业汇票/银票)专用审计只读端点(审计缺口11)。
* 从付款记录中过滤票据类付款类型(商业汇票/银行承兑汇票/商票/银票等),
* 返回票据台账清单+金额汇总,供审计人员取证。
*
* @param billType 票据类型关键词(如「汇票」/「承兑」,空=全部票据类付款)。
* @param fromDate 付款日期起(YYYY-MM-DD,空=不限)。
* @param toDate 付款日期止(YYYY-MM-DD,空=不限)。
* @param limit 最多返回条数(默认200)。
*/
@GetMapping("/bill-ledger")
public ApiResp<Map<String, Object>> billLedger(
@RequestParam(required = false) String billType,
@RequestParam(required = false) String fromDate,
@RequestParam(required = false) String toDate,
@RequestParam(defaultValue = "200") int limit) {
List<Payment> all = paymentRepo.findAll();
// 票据类付款类型关键词列表(覆盖常见票据名称)。
final java.util.Set<String> BILL_KEYWORDS = new java.util.LinkedHashSet<>(
java.util.Arrays.asList("汇票", "承兑", "票据", "商票", "银票", "银行承兑", "商业承兑"));
List<Payment> bills = all.stream()
.filter(p -> {
String pt = p.getPayType() == null ? "" : p.getPayType();
String subject = p.getSubject() == null ? "" : p.getSubject();
boolean isBill = BILL_KEYWORDS.stream().anyMatch(
kw -> pt.contains(kw) || subject.contains(kw));
if (!isBill) return false;
if (billType != null && !billType.isBlank()) {
isBill = pt.contains(billType) || subject.contains(billType);
}
return isBill;
})
.filter(p -> fromDate == null || fromDate.isBlank()
|| (p.getPayDate() != null && p.getPayDate().compareTo(fromDate) >= 0))
.filter(p -> toDate == null || toDate.isBlank()
|| (p.getPayDate() != null && p.getPayDate().compareTo(toDate) <= 0))
.limit(limit)
.toList();
BigDecimal totalBillAmount = bills.stream()
.map(p -> Money.nz(p.getAmount()))
.reduce(BigDecimal.ZERO, Money::add);
Map<String, BigDecimal> byType = new LinkedHashMap<>();
for (Payment p : bills) {
String pt = p.getPayType() == null ? "其他票据" : p.getPayType();
byType.merge(pt, Money.nz(p.getAmount()), Money::add);
}
Map<String, Double> byTypeDouble = new LinkedHashMap<>();
for (Map.Entry<String, BigDecimal> e : byType.entrySet()) {
byTypeDouble.put(e.getKey(), e.getValue().doubleValue());
}
List<Map<String, Object>> rows = bills.stream().map(p -> {
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", p.getId());
m.put("code", p.getCode());
m.put("payeeName", p.getPayeeName());
m.put("amount", Money.nz(p.getAmount()).doubleValue());
m.put("payType", p.getPayType());
m.put("subject", p.getSubject());
m.put("payDate", p.getPayDate());
m.put("status", p.getStatus());
return m;
}).toList();
Map<String, Object> result = new LinkedHashMap<>();
result.put("count", rows.size());
result.put("totalBillAmount", totalBillAmount.doubleValue());
result.put("byBillType", byTypeDouble);
result.put("note", "从付款记录中聚合含「汇票/承兑/票据/商票/银票」关键词的记录,作为票据台账审计只读视图");
result.put("items", rows);
return ApiResp.ok(result);
}
/**
* 内控自评答卷明细(按部门)——供审计人员取证(审计缺口11)。
* 当前 /compliance-checklist 仅返回状态统计,本端点返回各部门自评答卷填报详情,
* 包含每条任务的填报结果(selfCheckResult)、提交日期、复核意见等。
*
* @param dept 责任部门(精确或空=全部)。
* @param status 任务状态(空=全部)。
* @param limit 最多返回条数(默认200)。
*/
@GetMapping("/self-check-task-detail")
public ApiResp<Map<String, Object>> selfCheckTaskDetail(
@RequestParam(required = false) String dept,
@RequestParam(required = false) String status,
@RequestParam(defaultValue = "200") int limit) {
List<ComplianceSelfCheckTask> all = selfCheckTaskRepo.findAll();
List<ComplianceSelfCheckTask> filtered = all.stream()
.filter(t -> dept == null || dept.isBlank()
|| dept.equals(t.getResponsibleDept()))
.filter(t -> status == null || status.isBlank()
|| status.equals(t.getStatus()))
.limit(limit)
.toList();
// 按部门聚合状态分布(用于审计取证汇总)。
Map<String, Map<String, Long>> deptStatusMap = new LinkedHashMap<>();
for (ComplianceSelfCheckTask t : filtered) {
String d = t.getResponsibleDept() == null ? "未知" : t.getResponsibleDept();
String st = t.getStatus() == null ? "未知" : t.getStatus();
deptStatusMap.computeIfAbsent(d, k -> new LinkedHashMap<>())
.merge(st, 1L, Long::sum);
}
List<Map<String, Object>> rows = filtered.stream().map(t -> {
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", t.getId());
m.put("taskNo", t.getTaskNo());
m.put("checklistTitle", t.getChecklistTitle());
m.put("period", t.getPeriod());
m.put("responsibleDept", t.getResponsibleDept());
m.put("assignee", t.getAssignee());
m.put("dueDate", t.getDueDate());
m.put("status", t.getStatus());
m.put("selfCheckResult", t.getSelfCheckResult());
m.put("submitDate", t.getSubmitDate());
m.put("reviewer", t.getReviewer());
m.put("reviewComment", t.getReviewComment());
m.put("reviewDate", t.getReviewDate());
return m;
}).toList();
long submittedCount = filtered.stream()
.filter(t -> !"待填报".equals(t.getStatus()) && !"填报中".equals(t.getStatus())).count();
long closedCount = filtered.stream()
.filter(t -> "已闭环".equals(t.getStatus())).count();
Map<String, Object> result = new LinkedHashMap<>();
result.put("count", rows.size());
result.put("submittedCount", submittedCount);
result.put("closedCount", closedCount);
result.put("deptStatusSummary", deptStatusMap);
result.put("note", "内控自评答卷明细(含填报结果/复核意见),供审计人员按部门取证");
result.put("items", rows);
return ApiResp.ok(result);
}
}