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

174 lines
8.0 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.ApiException;
import com.kaidi.oa.common.ApiResp;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.common.NotFoundException;
import com.kaidi.oa.domain.Budget;
import com.kaidi.oa.domain.RdGrantDisbursement;
import com.kaidi.oa.domain.Voucher;
import com.kaidi.oa.repository.BudgetRepository;
import com.kaidi.oa.repository.RdGrantDisbursementRepository;
import com.kaidi.oa.repository.VoucherRepository;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.transaction.annotation.Transactional;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.util.List;
/**
* 政府资金到账·财务入账与项目预算提取联动(创新研发中心·申报服务部,需求 §3 资金拨付跟踪)。
*
* 补 PARTIAL(med):原到账仅生成机构服务费付款单,未联动「财务凭证入账」「项目支出预算提取」。
* 本控制器以「已到账」的 {@link RdGrantDisbursement} 为源,提供两条到账后联动动作(加法式,不改原控制器):
* <ul>
* <li>/{grantId}/post-voucher:生成「借 银行存款 / 贷 营业外收入-政府补助」的政府资金到账记账凭证
* source=rd_grant),通知财务入账;同源幂等(已生成则 409);</li>
* <li>/{grantId}/extract-budget:按到账资金提取一条项目支出预算(element=政府补助资金,budgetAmount=到账额),
* 供后续合作单位费用/项目支出从该预算列支;同源幂等。</li>
* </ul>
* 金额一律 {@link Money}BigDecimal);级联写带 {@link Transactional}。
*/
@RestController
@RequestMapping("/api/oa/decl-grant-ledger")
public class DeclGrantLedgerController {
private static final String ST_RECEIVED = "已到账";
private static final String ST_SETTLED = "已分配";
private static final String SRC_GRANT_VOUCHER = "rd_grant_receipt";
private static final String SRC_GRANT_BUDGET = "政府补助资金";
private final RdGrantDisbursementRepository grantRepo;
private final VoucherRepository voucherRepo;
private final BudgetRepository budgetRepo;
private final CurrentUserResolver currentUser;
public DeclGrantLedgerController(RdGrantDisbursementRepository grantRepo,
VoucherRepository voucherRepo,
BudgetRepository budgetRepo,
CurrentUserResolver currentUser) {
this.grantRepo = grantRepo;
this.voucherRepo = voucherRepo;
this.budgetRepo = budgetRepo;
this.currentUser = currentUser;
}
/** 某笔资金的入账与预算提取联动状态(前端判断按钮可用性)。 */
public record LedgerStatus(Long grantId, String projectName, double receivedAmount,
boolean vouchered, Long voucherId, String voucherNo,
boolean budgetExtracted, Long budgetId) {
}
@GetMapping("/{grantId}/status")
public ApiResp<LedgerStatus> status(@PathVariable Long grantId) {
RdGrantDisbursement g = loadGrant(grantId);
Voucher v = findVoucher(grantId);
Budget b = findBudget(grantId);
return ApiResp.ok(new LedgerStatus(g.getId(), g.getProjectName(),
Money.nz(g.getReceivedAmount()).doubleValue(),
v != null, v == null ? null : v.getId(), v == null ? null : v.getVoucherNo(),
b != null, b == null ? null : b.getId()));
}
/** 生成政府资金到账记账凭证:借 银行存款 / 贷 营业外收入-政府补助。要求资金已到账、同源未入账。 */
@PostMapping("/{grantId}/post-voucher")
@Transactional
public ApiResp<Voucher> postVoucher(@PathVariable Long grantId, HttpServletRequest request) {
RdGrantDisbursement g = loadGrant(grantId);
requireReceived(g);
if (findVoucher(grantId) != null) {
throw new ApiException(409, "该笔资金已生成到账入账凭证,请勿重复");
}
BigDecimal amount = Money.nz(g.getReceivedAmount());
if (Money.lte0(amount)) {
throw new ApiException(400, "到账金额为 0,无法入账");
}
Voucher v = new Voucher();
v.setVoucherNo("PZ-ZJ" + g.getId());
v.setVoucherDate(LocalDate.now().toString());
v.setSummary("收到政府资助资金到账:" + g.getProjectName()
+ (g.getAuthority() == null ? "" : "" + g.getAuthority() + ""));
v.setDebitAccount("银行存款");
v.setCreditAccount("营业外收入-政府补助");
v.setAmount(amount);
v.setStatus("待审核");
v.setPreparer(currentUser.resolveLabel(request));
v.setSourceType(SRC_GRANT_VOUCHER);
v.setSourceId(g.getId());
v.setCreatedAt(Instant.now());
return ApiResp.ok(voucherRepo.save(v));
}
/** 按到账资金提取项目支出预算(element=政府补助资金)。要求资金已到账、同源未提取。 */
@PostMapping("/{grantId}/extract-budget")
@Transactional
public ApiResp<Budget> extractBudget(@PathVariable Long grantId, HttpServletRequest request) {
RdGrantDisbursement g = loadGrant(grantId);
requireReceived(g);
if (findBudget(grantId) != null) {
throw new ApiException(409, "该笔资金已提取项目支出预算,请勿重复");
}
BigDecimal amount = Money.nz(g.getReceivedAmount());
if (Money.lte0(amount)) {
throw new ApiException(400, "到账金额为 0,无法提取预算");
}
Budget b = new Budget();
b.setName("政府补助专项预算·" + g.getProjectName());
b.setProjectId(g.getPolicyApplicationId());
b.setCompanySubject(g.getCompanySubject());
b.setElement(SRC_GRANT_BUDGET);
b.setYear(String.valueOf(LocalDate.now().getYear()));
b.setPeriod("年度");
b.setBudgetAmount(amount);
b.setActualAmount(Money.ZERO);
b.setOwner(g.getOwner() == null || g.getOwner().isBlank()
? currentUser.resolveLabel(request) : g.getOwner());
b.setStatus("执行中");
b.setCreatedAt(Instant.now());
// 复用 name 末尾标注来源 grant id,便于同源幂等检测(避免改动共享 Budget 实体加新列)。
b.setName(b.getName() + " #G" + g.getId());
return ApiResp.ok(budgetRepo.save(b));
}
// ---------- helpers ----------
private RdGrantDisbursement loadGrant(Long id) {
return grantRepo.findById(id)
.orElseThrow(() -> new NotFoundException("grant disbursement not found: " + id));
}
private void requireReceived(RdGrantDisbursement g) {
if (!ST_RECEIVED.equals(g.getStatus()) && !ST_SETTLED.equals(g.getStatus())) {
throw new ApiException(400, "资金尚未全额到账,不能入账/提取预算(当前:" + g.getStatus() + "");
}
}
/** 同源凭证检测:source=rd_grant_receipt 且 sourceId=grantId。 */
private Voucher findVoucher(Long grantId) {
for (Voucher v : voucherRepo.findAll()) {
if (SRC_GRANT_VOUCHER.equals(v.getSourceType()) && grantId.equals(v.getSourceId())) {
return v;
}
}
return null;
}
/** 同源预算检测:element=政府补助资金 且 name 末尾标注 #G<grantId>。 */
private Budget findBudget(Long grantId) {
String tag = "#G" + grantId;
for (Budget b : budgetRepo.findByElement(SRC_GRANT_BUDGET)) {
if (b.getName() != null && b.getName().endsWith(tag)) {
return b;
}
}
return null;
}
}