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,242 @@
package com.kaidi.oa.web;
import com.kaidi.oa.common.ApiException;
import com.kaidi.oa.common.ApiResp;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.common.NotFoundException;
import com.kaidi.oa.domain.FinVoucherTemplate;
import com.kaidi.oa.domain.Voucher;
import com.kaidi.oa.repository.FinVoucherTemplateRepository;
import com.kaidi.oa.repository.VoucherRepository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
/**
* 财务部·凭证模板管理与期末结转(财务总账深化)。
*
* 审计缺口:无凭证模板;无自动/自定义结转损益;无期末处理。
*
* 本控制器提供:
* 1. 凭证模板 CRUD(常用业务:计提折旧/摊销/结转损益/结转研发/分摊制造费/期末调汇)。
* 2. POST /{id}/apply —— 按模板一键生成草稿凭证(摘要变量替换 ${period}/${amount})。
* 3. POST /period-close —— 月末结转损益:自动生成一批结转凭证(收入→本年利润/成本→本年利润),
* 返回生成的凭证列表(草稿状态,需财务审核过账)。
*
* 写口:AuthInterceptor FINANCE_PREFIXES(/api/oa/fin-voucher-templates) 限 ADMIN/APPROVER。
* 读口:SENSITIVE_READ_PREFIXES 同门槛(含科目信息,属机密财务)。
*/
@RestController
@RequestMapping("/api/oa/fin-voucher-templates")
public class FinVoucherTemplateController {
private final FinVoucherTemplateRepository templateRepo;
private final VoucherRepository voucherRepo;
public FinVoucherTemplateController(FinVoucherTemplateRepository templateRepo,
VoucherRepository voucherRepo) {
this.templateRepo = templateRepo;
this.voucherRepo = voucherRepo;
}
// ---------- 模板 CRUD ----------
@GetMapping
public ApiResp<List<FinVoucherTemplate>> list(
@RequestParam(required = false) String templateType,
@RequestParam(required = false) Boolean enabled) {
if (templateType != null && !templateType.isBlank()) {
return ApiResp.ok(templateRepo.findByTemplateType(templateType));
}
if (enabled != null) {
return ApiResp.ok(templateRepo.findByEnabled(enabled));
}
return ApiResp.ok(templateRepo.findAll());
}
@GetMapping("/{id}")
public ApiResp<FinVoucherTemplate> get(@PathVariable Long id) {
return ApiResp.ok(templateRepo.findById(id)
.orElseThrow(() -> new NotFoundException("凭证模板不存在: " + id)));
}
public record TemplateRequest(
String name, String templateType, String debitAccount, String creditAccount,
String summaryTemplate, String amountSource, Boolean enabled, String createdBy) {
}
@PostMapping
public ApiResp<FinVoucherTemplate> create(@RequestBody TemplateRequest req) {
if (req.name() == null || req.name().isBlank()) {
throw new ApiException(400, "模板名称不能为空");
}
FinVoucherTemplate t = new FinVoucherTemplate();
t.setCode("TMPl-" + (templateRepo.count() + 1));
t.setName(req.name());
t.setTemplateType(req.templateType() == null ? FinVoucherTemplate.TYPE_MANUAL : req.templateType());
t.setDebitAccount(req.debitAccount());
t.setCreditAccount(req.creditAccount());
t.setSummaryTemplate(req.summaryTemplate() == null ? req.name() : req.summaryTemplate());
t.setAmountSource(req.amountSource() == null ? "手填" : req.amountSource());
t.setEnabled(req.enabled() == null ? Boolean.TRUE : req.enabled());
t.setCreatedBy(req.createdBy());
t.setCreatedAt(Instant.now());
return ApiResp.ok(templateRepo.save(t));
}
@PutMapping("/{id}")
public ApiResp<FinVoucherTemplate> update(@PathVariable Long id, @RequestBody TemplateRequest req) {
FinVoucherTemplate t = templateRepo.findById(id)
.orElseThrow(() -> new NotFoundException("凭证模板不存在: " + id));
if (req.name() != null && !req.name().isBlank()) t.setName(req.name());
if (req.templateType() != null) t.setTemplateType(req.templateType());
if (req.debitAccount() != null) t.setDebitAccount(req.debitAccount());
if (req.creditAccount() != null) t.setCreditAccount(req.creditAccount());
if (req.summaryTemplate() != null) t.setSummaryTemplate(req.summaryTemplate());
if (req.amountSource() != null) t.setAmountSource(req.amountSource());
if (req.enabled() != null) t.setEnabled(req.enabled());
return ApiResp.ok(templateRepo.save(t));
}
@DeleteMapping("/{id}")
public ApiResp<Void> delete(@PathVariable Long id) {
templateRepo.findById(id).orElseThrow(() -> new NotFoundException("凭证模板不存在: " + id));
templateRepo.deleteById(id);
return ApiResp.ok(null);
}
// ---------- 按模板生成草稿凭证 ----------
public record ApplyRequest(Double amount, String period, String preparer) {}
@PostMapping("/{id}/apply")
@Transactional
public ApiResp<Voucher> apply(@PathVariable Long id, @RequestBody ApplyRequest req) {
FinVoucherTemplate t = templateRepo.findById(id)
.orElseThrow(() -> new NotFoundException("凭证模板不存在: " + id));
if (!Boolean.TRUE.equals(t.getEnabled())) {
throw new ApiException(400, "该模板已停用,无法生成凭证");
}
if (req.amount() == null || req.amount() <= 0) {
throw new ApiException(400, "金额必须大于 0");
}
String period = req.period() != null ? req.period() : LocalDate.now().toString().substring(0, 7);
String summary = (t.getSummaryTemplate() == null ? t.getName() : t.getSummaryTemplate())
.replace("${period}", period)
.replace("${amount}", String.format("%.2f", req.amount()));
Voucher v = new Voucher();
v.setVoucherNo("TMPl-" + t.getCode() + "-" + System.currentTimeMillis() % 10000);
v.setVoucherDate(LocalDate.now().toString());
v.setSummary(summary);
v.setDebitAccount(t.getDebitAccount());
v.setCreditAccount(t.getCreditAccount());
v.setAmount(Money.of(req.amount()));
v.setStatus(Voucher.S_DRAFT);
v.setPreparer(req.preparer());
v.setReversed(Boolean.FALSE);
v.setIsReversal(Boolean.FALSE);
v.setCreatedAt(Instant.now());
return ApiResp.ok(voucherRepo.save(v));
}
// ---------- 月末结转损益 ----------
public record PeriodCloseRequest(
String period, String preparer,
Double totalRevenue, Double totalCost, Double totalExpense) {}
/**
* 月末结转损益:生成三张草稿凭证(借收入/贷本年利润;借本年利润/贷成本;借本年利润/贷费用),
* 财务确认后逐张审核过账完成结账前检查。
*/
@PostMapping("/period-close")
@Transactional
public ApiResp<List<Voucher>> periodClose(@RequestBody PeriodCloseRequest req) {
if (req.period() == null || req.period().isBlank()) {
throw new ApiException(400, "结转期间不能为空,格式 yyyy-MM");
}
String period = req.period();
String preparer = req.preparer() == null ? "系统自动" : req.preparer();
double revenue = req.totalRevenue() == null ? 0 : req.totalRevenue();
double cost = req.totalCost() == null ? 0 : req.totalCost();
double expense = req.totalExpense() == null ? 0 : req.totalExpense();
List<Voucher> generated = new java.util.ArrayList<>();
// 凭证1:结转收入 → 本年利润(借 6001/主营业务收入,贷 4103/本年利润)
if (revenue > 0) {
Voucher v1 = buildTransferVoucher(
"JZ-" + period + "-01", period,
period + " 月结转主营业务收入",
"6001", "4103", revenue, preparer);
generated.add(voucherRepo.save(v1));
}
// 凭证2:结转成本 → 本年利润(借 4103/本年利润,贷 6401/主营业务成本)
if (cost > 0) {
Voucher v2 = buildTransferVoucher(
"JZ-" + period + "-02", period,
period + " 月结转主营业务成本",
"4103", "6401", cost, preparer);
generated.add(voucherRepo.save(v2));
}
// 凭证3:结转期间费用 → 本年利润(借 4103/本年利润,贷 6601/管理费用+销售费用+财务费用汇总)
if (expense > 0) {
Voucher v3 = buildTransferVoucher(
"JZ-" + period + "-03", period,
period + " 月结转期间费用",
"4103", "6601", expense, preparer);
generated.add(voucherRepo.save(v3));
}
if (generated.isEmpty()) {
throw new ApiException(400, "收入/成本/费用至少填写一项");
}
return ApiResp.ok(generated);
}
private Voucher buildTransferVoucher(String voucherNo, String period, String summary,
String debit, String credit, double amount, String preparer) {
Voucher v = new Voucher();
v.setVoucherNo(voucherNo);
v.setVoucherDate(LocalDate.now().toString());
v.setSummary(summary);
v.setDebitAccount(debit);
v.setCreditAccount(credit);
v.setAmount(Money.of(amount));
v.setStatus(Voucher.S_DRAFT);
v.setPreparer(preparer);
v.setReversed(Boolean.FALSE);
v.setIsReversal(Boolean.FALSE);
v.setCreatedAt(Instant.now());
return v;
}
// ---------- 模板类型列表(前端下拉用) ----------
@GetMapping("/template-types")
public ApiResp<List<String>> templateTypes() {
return ApiResp.ok(List.of(
FinVoucherTemplate.TYPE_MANUAL,
FinVoucherTemplate.TYPE_DEPREC,
FinVoucherTemplate.TYPE_AMORT,
FinVoucherTemplate.TYPE_TRANSFER,
FinVoucherTemplate.TYPE_RD,
FinVoucherTemplate.TYPE_MFG,
FinVoucherTemplate.TYPE_FX
));
}
}