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,252 @@
|
||||
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.FertPkgMaterialLedger;
|
||||
import com.kaidi.oa.repository.FertPkgMaterialLedgerRepository;
|
||||
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.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 生物质肥料制造中心·包装物独立台账(需求 §6,审计 PARTIAL[low])。
|
||||
*
|
||||
* 补全缺口:包装物库(编织袋/标签/吨袋)独立台账实体,与成品批次明细关联管理,库位管理。
|
||||
*
|
||||
* 端点:
|
||||
* <ul>
|
||||
* <li>GET /fert-pkg-material-ledger:列表(可按 status/packSpec/keyword 筛选);</li>
|
||||
* <li>POST /fert-pkg-material-ledger:建档(pkgCode 唯一约束 → 409);</li>
|
||||
* <li>PUT /fert-pkg-material-ledger/{id}:更新描述字段;</li>
|
||||
* <li>DELETE /fert-pkg-material-ledger/{id}:删除;</li>
|
||||
* <li>POST /fert-pkg-material-ledger/{id}/inbound:入库(增 qty,记录入库日期);</li>
|
||||
* <li>POST /fert-pkg-material-ledger/{id}/issue:领用出库(减 qty,绑定成品批次);</li>
|
||||
* <li>POST /fert-pkg-material-ledger/{id}/return:退库;</li>
|
||||
* <li>GET /fert-pkg-material-ledger/low-stock:低库存预警(qty <= safetyStock);</li>
|
||||
* <li>GET /fert-pkg-material-ledger/summary:各包装规格汇总统计;</li>
|
||||
* </ul>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/oa/fert-pkg-material-ledger")
|
||||
public class FertPkgMaterialLedgerController {
|
||||
|
||||
private final FertPkgMaterialLedgerRepository repo;
|
||||
|
||||
public FertPkgMaterialLedgerController(FertPkgMaterialLedgerRepository repo) {
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
// -------- 列表 --------
|
||||
|
||||
@GetMapping
|
||||
public ApiResp<List<FertPkgMaterialLedger>> list(
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) String packSpec,
|
||||
@RequestParam(required = false) String keyword) {
|
||||
List<FertPkgMaterialLedger> all;
|
||||
if (keyword != null && !keyword.isBlank()) {
|
||||
all = repo.findByPkgNameContaining(keyword.trim());
|
||||
} else if (packSpec != null && !packSpec.isBlank()) {
|
||||
all = repo.findByPackSpec(packSpec.trim());
|
||||
} else if (status != null && !status.isBlank()) {
|
||||
all = repo.findByStatus(status.trim());
|
||||
} else {
|
||||
all = repo.findAll();
|
||||
}
|
||||
return ApiResp.ok(all);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ApiResp<FertPkgMaterialLedger> get(@PathVariable Long id) {
|
||||
return ApiResp.ok(find(id));
|
||||
}
|
||||
|
||||
// -------- 建档/更新/删除 --------
|
||||
|
||||
public record PkgRequest(
|
||||
String pkgCode, String pkgName, String spec, String bin, String unit,
|
||||
Double qty, Double safetyStock, Double unitCost, String supplier,
|
||||
String packSpec, String remark) {
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Transactional
|
||||
public ApiResp<FertPkgMaterialLedger> create(@RequestBody PkgRequest req) {
|
||||
if (req.pkgName() == null || req.pkgName().isBlank()) {
|
||||
throw new ApiException(400, "包装物名称(pkgName) 不能为空");
|
||||
}
|
||||
String code = (req.pkgCode() == null || req.pkgCode().isBlank())
|
||||
? "PKG-" + (repo.count() + 1) : req.pkgCode().trim();
|
||||
if (repo.findByPkgCode(code).isPresent()) {
|
||||
throw new ApiException(409, "包装物编码已存在:" + code);
|
||||
}
|
||||
FertPkgMaterialLedger e = new FertPkgMaterialLedger();
|
||||
e.setPkgCode(code);
|
||||
e.setPkgName(req.pkgName().trim());
|
||||
e.setSpec(req.spec());
|
||||
e.setWarehouse("包装物库");
|
||||
e.setBin(req.bin() == null || req.bin().isBlank() ? "A货架-01" : req.bin().trim());
|
||||
e.setUnit(req.unit() == null || req.unit().isBlank() ? "条" : req.unit().trim());
|
||||
BigDecimal qty = Money.of(req.qty());
|
||||
e.setQty(qty);
|
||||
e.setSafetyStock(Money.of(req.safetyStock()));
|
||||
e.setUnitCost(Money.of(req.unitCost()));
|
||||
e.setSupplier(req.supplier());
|
||||
e.setPackSpec(req.packSpec());
|
||||
e.setLastInboundDate(LocalDate.now().toString());
|
||||
e.setLastMoveType(qty.signum() > 0 ? "入库" : null);
|
||||
e.setStatus(evalStatus(qty, Money.of(req.safetyStock())));
|
||||
e.setRemark(req.remark());
|
||||
e.setCreatedAt(Instant.now());
|
||||
return ApiResp.ok(repo.save(e));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@Transactional
|
||||
public ApiResp<FertPkgMaterialLedger> update(@PathVariable Long id, @RequestBody PkgRequest req) {
|
||||
FertPkgMaterialLedger e = find(id);
|
||||
if (req.pkgName() != null && !req.pkgName().isBlank()) e.setPkgName(req.pkgName().trim());
|
||||
if (req.spec() != null) e.setSpec(req.spec());
|
||||
if (req.bin() != null && !req.bin().isBlank()) e.setBin(req.bin().trim());
|
||||
if (req.unit() != null && !req.unit().isBlank()) e.setUnit(req.unit().trim());
|
||||
if (req.safetyStock() != null) e.setSafetyStock(Money.of(req.safetyStock()));
|
||||
if (req.unitCost() != null) e.setUnitCost(Money.of(req.unitCost()));
|
||||
if (req.supplier() != null) e.setSupplier(req.supplier());
|
||||
if (req.packSpec() != null) e.setPackSpec(req.packSpec());
|
||||
if (req.remark() != null) e.setRemark(req.remark());
|
||||
e.setStatus(evalStatus(e.getQty(), e.getSafetyStock()));
|
||||
return ApiResp.ok(repo.save(e));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Transactional
|
||||
public ApiResp<Void> delete(@PathVariable Long id) {
|
||||
repo.delete(find(id));
|
||||
return ApiResp.ok();
|
||||
}
|
||||
|
||||
// -------- 出入库操作 --------
|
||||
|
||||
public record MoveRequest(Double qty, String linkedBatchNo, String operator, String remark) {
|
||||
}
|
||||
|
||||
/** 入库:增 qty,记录入库日期。 */
|
||||
@PostMapping("/{id}/inbound")
|
||||
@Transactional
|
||||
public ApiResp<FertPkgMaterialLedger> inbound(@PathVariable Long id, @RequestBody MoveRequest req) {
|
||||
FertPkgMaterialLedger e = find(id);
|
||||
BigDecimal q = requirePositive(req.qty());
|
||||
e.setQty(Money.of(e.getQty().add(q)));
|
||||
e.setLastInboundDate(LocalDate.now().toString());
|
||||
e.setLastMoveType("入库");
|
||||
e.setStatus(evalStatus(e.getQty(), e.getSafetyStock()));
|
||||
return ApiResp.ok(repo.save(e));
|
||||
}
|
||||
|
||||
/** 领用出库:减 qty,绑定成品批次号(补全与成品明细关联管理缺口)。 */
|
||||
@PostMapping("/{id}/issue")
|
||||
@Transactional
|
||||
public ApiResp<FertPkgMaterialLedger> issue(@PathVariable Long id, @RequestBody MoveRequest req) {
|
||||
FertPkgMaterialLedger e = find(id);
|
||||
BigDecimal q = requirePositive(req.qty());
|
||||
if (q.compareTo(e.getQty()) > 0) {
|
||||
throw new ApiException(400, "领用数量(" + q + ")超过库存(" + e.getQty() + ")");
|
||||
}
|
||||
e.setQty(Money.of(e.getQty().subtract(q)));
|
||||
if (req.linkedBatchNo() != null && !req.linkedBatchNo().isBlank()) {
|
||||
e.setLinkedBatchNo(req.linkedBatchNo().trim());
|
||||
}
|
||||
e.setLastMoveType("领用");
|
||||
e.setStatus(evalStatus(e.getQty(), e.getSafetyStock()));
|
||||
return ApiResp.ok(repo.save(e));
|
||||
}
|
||||
|
||||
/** 退库:增 qty,清除成品批次关联。 */
|
||||
@PostMapping("/{id}/return")
|
||||
@Transactional
|
||||
public ApiResp<FertPkgMaterialLedger> returnToStock(@PathVariable Long id, @RequestBody MoveRequest req) {
|
||||
FertPkgMaterialLedger e = find(id);
|
||||
BigDecimal q = requirePositive(req.qty());
|
||||
e.setQty(Money.of(e.getQty().add(q)));
|
||||
e.setLastMoveType("退库");
|
||||
e.setStatus(evalStatus(e.getQty(), e.getSafetyStock()));
|
||||
return ApiResp.ok(repo.save(e));
|
||||
}
|
||||
|
||||
// -------- 低库存预警 --------
|
||||
|
||||
public record LowStockRow(Long id, String pkgCode, String pkgName, String spec, String packSpec,
|
||||
BigDecimal qty, BigDecimal safetyStock, String unit, String bin) {
|
||||
}
|
||||
|
||||
@GetMapping("/low-stock")
|
||||
public ApiResp<List<LowStockRow>> lowStock() {
|
||||
List<LowStockRow> out = new ArrayList<>();
|
||||
for (FertPkgMaterialLedger e : repo.findAll()) {
|
||||
if (e.getSafetyStock() != null && e.getSafetyStock().signum() > 0
|
||||
&& e.getQty().compareTo(e.getSafetyStock()) <= 0) {
|
||||
out.add(new LowStockRow(e.getId(), e.getPkgCode(), e.getPkgName(),
|
||||
e.getSpec(), e.getPackSpec(), e.getQty(), e.getSafetyStock(),
|
||||
e.getUnit(), e.getBin()));
|
||||
}
|
||||
}
|
||||
return ApiResp.ok(out);
|
||||
}
|
||||
|
||||
// -------- 规格汇总统计 --------
|
||||
|
||||
public record PkgSummaryRow(String packSpec, BigDecimal totalQty, int itemCount, BigDecimal totalValue) {
|
||||
}
|
||||
|
||||
@GetMapping("/summary")
|
||||
public ApiResp<List<PkgSummaryRow>> summary() {
|
||||
java.util.Map<String, BigDecimal[]> map = new java.util.LinkedHashMap<>();
|
||||
for (FertPkgMaterialLedger e : repo.findAll()) {
|
||||
String key = e.getPackSpec() == null ? "未指定" : e.getPackSpec();
|
||||
BigDecimal[] arr = map.computeIfAbsent(key, k -> new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO});
|
||||
arr[0] = arr[0].add(e.getQty());
|
||||
arr[1] = arr[1].add(BigDecimal.ONE);
|
||||
BigDecimal val = e.getQty().multiply(e.getUnitCost() == null ? BigDecimal.ZERO : e.getUnitCost());
|
||||
arr[2] = arr[2].add(val);
|
||||
}
|
||||
List<PkgSummaryRow> out = new ArrayList<>();
|
||||
for (java.util.Map.Entry<String, BigDecimal[]> entry : map.entrySet()) {
|
||||
out.add(new PkgSummaryRow(entry.getKey(), Money.of(entry.getValue()[0]),
|
||||
entry.getValue()[1].intValue(), Money.of(entry.getValue()[2])));
|
||||
}
|
||||
return ApiResp.ok(out);
|
||||
}
|
||||
|
||||
// -------- helpers --------
|
||||
|
||||
private FertPkgMaterialLedger find(Long id) {
|
||||
return repo.findById(id).orElseThrow(() -> new NotFoundException("包装物台账记录不存在:" + id));
|
||||
}
|
||||
|
||||
private static BigDecimal requirePositive(Double v) {
|
||||
BigDecimal q = Money.of(v);
|
||||
if (q.signum() <= 0) throw new ApiException(400, "数量必须大于 0");
|
||||
return q;
|
||||
}
|
||||
|
||||
private static String evalStatus(BigDecimal qty, BigDecimal safety) {
|
||||
if (qty == null || qty.signum() <= 0) return "已耗尽";
|
||||
if (safety != null && safety.signum() > 0 && qty.compareTo(safety) <= 0) return "低库存";
|
||||
return "在库";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user