Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/RdExpenseTraceController.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

148 lines
6.8 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.common.NotFoundException;
import com.kaidi.oa.domain.RdExpense;
import com.kaidi.oa.domain.RdProject;
import com.kaidi.oa.repository.RdExpenseRepository;
import com.kaidi.oa.repository.RdProjectRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
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;
/**
* 研发费用归集·证据链(数据中心·自动取数)。按 rdProjectId 聚合该研发项目的全部研发费用,
* 列出每一笔费用 + 其来源单据引用(凭证号/发票/工时单 voucher),并按费用类型形成证据链汇总
* (项目 / 费用类型 / 金额合计 / 笔数),让「研发立项-费用-凭证」证据链一眼可查、可自动核对。
*
* 纯只读聚合:参考 {@code FundPoolController}/{@code EvidenceController} 跨表聚合不建实体,
* 金额一律走 {@link Money}BigDecimal)。该端点端出研发费用金额+凭证明细,属机密财务读,
* 仅 GET,读口须经 AuthInterceptor 敏感读门槛——见 sharedFileSnippets 将
* {@code /api/oa/rd-expense-traces} 登记进 SENSITIVE_READ_PREFIXES。
*/
@RestController
@RequestMapping("/api/oa/rd-expense-traces")
public class RdExpenseTraceController {
private final RdExpenseRepository rdExpenseRepo;
private final RdProjectRepository rdProjectRepo;
public RdExpenseTraceController(RdExpenseRepository rdExpenseRepo,
RdProjectRepository rdProjectRepo) {
this.rdExpenseRepo = rdExpenseRepo;
this.rdProjectRepo = rdProjectRepo;
}
/** 单笔费用的证据行(费用明细 + 来源单据引用)。 */
public record EvidenceLine(Long expenseId, String category, String description,
double amount, String occurDate, String voucher,
String status, String recorder) {
}
/** 按费用类型的汇总行(费用类型 / 金额合计 / 笔数)。 */
public record CategorySummary(String category, double amount, int count) {
}
/** 研发项目维度的项目引用。 */
public record ProjectRef(Long id, String name, String code) {
}
/** 一个研发项目的证据链:项目引用 + 逐笔证据行 + 按类型汇总 + 金额合计 + 总笔数。 */
public record ExpenseTrace(ProjectRef project, List<EvidenceLine> lines,
List<CategorySummary> byCategory, double totalAmount, int lineCount) {
}
/** 跨全部研发项目的证据链总览(看板用)。 */
public record TraceOverview(List<ExpenseTrace> traces, double totalAmount, int lineCount) {
}
private static final class CatAcc {
BigDecimal amount = BigDecimal.ZERO;
int count = 0;
}
/**
* 单个研发项目的费用证据链。
* GET /api/oa/rd-expense-traces/{rdProjectId}
*/
@GetMapping("/{rdProjectId}")
public ApiResp<ExpenseTrace> trace(@PathVariable Long rdProjectId) {
RdProject project = rdProjectRepo.findById(rdProjectId)
.orElseThrow(() -> new NotFoundException("研发项目不存在:" + rdProjectId));
ExpenseTrace trace = buildTrace(new ProjectRef(project.getId(), project.getName(),
project.getCode()), rdExpenseRepo.findByRdProjectId(rdProjectId));
return ApiResp.ok(trace);
}
/**
* 全部研发项目的费用证据链总览(看板)。逐项目聚合,附跨项目金额合计与总笔数。
* GET /api/oa/rd-expense-traces
*/
@GetMapping
public ApiResp<TraceOverview> overview() {
// 先把费用按项目分桶(插入序固定行位)。
Map<Long, List<RdExpense>> byProject = new LinkedHashMap<>();
for (RdExpense e : rdExpenseRepo.findAll()) {
byProject.computeIfAbsent(e.getRdProjectId(), k -> new ArrayList<>()).add(e);
}
// 项目名/编码以 RdProject 主档为准;费用上的 rdProjectName 仅作回退(项目已删时)。
Map<Long, RdProject> projects = new LinkedHashMap<>();
for (RdProject p : rdProjectRepo.findAll()) {
projects.put(p.getId(), p);
}
List<ExpenseTrace> traces = new ArrayList<>();
BigDecimal grandTotal = BigDecimal.ZERO;
int grandLines = 0;
for (Map.Entry<Long, List<RdExpense>> entry : byProject.entrySet()) {
Long pid = entry.getKey();
List<RdExpense> expenses = entry.getValue();
ProjectRef ref;
RdProject p = pid == null ? null : projects.get(pid);
if (p != null) {
ref = new ProjectRef(p.getId(), p.getName(), p.getCode());
} else {
String fallbackName = expenses.isEmpty() ? null : expenses.get(0).getRdProjectName();
ref = new ProjectRef(pid, fallbackName, null);
}
ExpenseTrace trace = buildTrace(ref, expenses);
traces.add(trace);
grandTotal = Money.add(grandTotal, BigDecimal.valueOf(trace.totalAmount()));
grandLines += trace.lineCount();
}
return ApiResp.ok(new TraceOverview(traces, grandTotal.doubleValue(), grandLines));
}
/** 把一组费用折叠成一个项目的证据链(逐行 + 按类型汇总 + 合计)。 */
private ExpenseTrace buildTrace(ProjectRef ref, List<RdExpense> expenses) {
List<EvidenceLine> lines = new ArrayList<>();
Map<String, CatAcc> byCategory = new LinkedHashMap<>();
BigDecimal total = BigDecimal.ZERO;
for (RdExpense e : expenses) {
lines.add(new EvidenceLine(e.getId(), e.getCategory(), e.getDescription(),
Money.nz(e.getAmount()).doubleValue(), e.getOccurDate(), e.getVoucher(),
e.getStatus(), e.getRecorder()));
String cat = e.getCategory() == null || e.getCategory().isBlank() ? "未分类" : e.getCategory();
CatAcc acc = byCategory.computeIfAbsent(cat, k -> new CatAcc());
acc.amount = Money.add(acc.amount, e.getAmount());
acc.count++;
total = Money.add(total, e.getAmount());
}
List<CategorySummary> summaries = new ArrayList<>();
for (Map.Entry<String, CatAcc> c : byCategory.entrySet()) {
summaries.add(new CategorySummary(c.getKey(), c.getValue().amount.doubleValue(),
c.getValue().count));
}
return new ExpenseTrace(ref, lines, summaries, total.doubleValue(), lines.size());
}
}