恢复点(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>
607 lines
30 KiB
Java
607 lines
30 KiB
Java
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);
|
||
}
|
||
}
|