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:
@@ -0,0 +1,147 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user