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>
This commit is contained in:
Qiufeng
2026-06-15 19:19:15 +08:00
co-authored by Claude Opus 4.8
commit 5e51dc3f56
10584 changed files with 2501339 additions and 0 deletions
@@ -0,0 +1,606 @@
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);
}
}