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,376 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.domain.CostHrDeptLaborRate;
import com.kaidi.oa.domain.CostPurchaseOrder;
import com.kaidi.oa.domain.CostReductionItem;
import com.kaidi.oa.domain.StandardCost;
import com.kaidi.oa.repository.CostHrDeptLaborRateRepository;
import com.kaidi.oa.repository.CostPurchaseOrderRepository;
import com.kaidi.oa.repository.CostReductionItemRepository;
import com.kaidi.oa.repository.StandardCostRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
/**
* 成本控制部演示种子数据(W7 补 PARTIAL 缺口)。
*
* <p>补齐以下三条 0 数据缺口:
* <ol>
* <li><b>CostPurchaseOrder</b>(采购成本控制 §5):6 条跨状态/价格状态的演示采购订单,
* 含正常价格/超标准价/严重超价,让价格预警看板、节约额汇总、价格趋势折线、
* 计价引擎 valuation-engine/calc 全部活体可见。</li>
* <li><b>CostHrDeptLaborRate</b>HR 工时费率 §8):5 条各部门标准工时费率,
* 消除 /cost-hr-labor-rates 活体返回 null data 的缺口。</li>
* <li><b>CostReductionItem</b>(降本增效跟踪 §6):5 条跨状态演示项目,
* 含"已完成"项目自动算降本额/达成率,让降本汇总/成本趋势活体可见。</li>
* </ol>
*
* 幂等:各表 count() > 0 即跳过,重启不重复插入。
* @Order(9) 在 DataSeeder(@Order 1) 之后独立运行。
*/
@Component
@Order(9)
public class CostCtrlDemoSeeder implements ApplicationRunner {
private final CostPurchaseOrderRepository poRepo;
private final CostHrDeptLaborRateRepository rateRepo;
private final CostReductionItemRepository reductionRepo;
private final StandardCostRepository stdCostRepo;
public CostCtrlDemoSeeder(CostPurchaseOrderRepository poRepo,
CostHrDeptLaborRateRepository rateRepo,
CostReductionItemRepository reductionRepo,
StandardCostRepository stdCostRepo) {
this.poRepo = poRepo;
this.rateRepo = rateRepo;
this.reductionRepo = reductionRepo;
this.stdCostRepo = stdCostRepo;
}
@Override
@Transactional
public void run(ApplicationArguments args) {
seedPurchaseOrders();
seedHrLaborRates();
seedCostReductions();
seedStandardCosts();
}
// ===================================================================
// 1. 采购订单种子(6 条,跨状态 + 跨价格状态)
// ===================================================================
private void seedPurchaseOrders() {
if (poRepo.count() > 0) return;
// 订单1:钢材-正常价格-已完成(用于计价引擎 FIFO/移动平均历史数据)
po("PO-2025-0001", "Q235B 钢材", "钢板 δ=10mm", "",
"华钢物资有限公司", 50.0,
new BigDecimal("5980.00"), new BigDecimal("6100.00"), new BigDecimal("5950.00"),
"凯迪生物质锅炉项目", "原材料", "2025-03-05", "2025-03-15",
new BigDecimal("200.00"), "张采购", "钢材主结构采购,执行中标价", "已完成", "正常");
// 订单2:螺旋输送机-正常-执行中
po("PO-2025-0002", "螺旋输送机叶片", "叶径 φ320×L6000", "",
"河南龙达机械", 10.0,
new BigDecimal("3800.00"), new BigDecimal("3900.00"), new BigDecimal("3750.00"),
"凯迪水处理工程", "设备", "2025-04-10", "2025-04-30",
new BigDecimal("500.00"), "李采购", "批量采购享9.7折", "执行中", "正常");
// 订单3:无机絮凝剂-超标准价-待审批(超标5.8%,需成控审批)
po("PO-2025-0003", "聚合氯化铝(PAC", "含量30%工业级", "",
"江苏清河水处理材料", 20.0,
new BigDecimal("2180.00"), new BigDecimal("2060.00"), new BigDecimal("2100.00"),
"凯迪环保项目A", "原材料", "2025-05-02", "2025-05-20",
new BigDecimal("300.00"), "王采购", "供应商因原料涨价上调单价,超标5.8%需成控审批", "待审批", "超标准价");
// 订单4:变频器-严重超价-草稿(超标12.4%,严重预警)
po("PO-2025-0004", "ABB 变频器 ACS550", "37kW 380V 三相", "",
"上海ABB电气代理", 3.0,
new BigDecimal("18900.00"), new BigDecimal("16800.00"), new BigDecimal("17200.00"),
"凯迪技改项目", "设备", "2025-05-15", "2025-06-15",
new BigDecimal("0.00"), "赵采购", "原进口零件涨价,代理商上调12.4%,需专项评审", "草稿", "严重超价");
// 订单5:聚乙烯管道-超历史均价-已批准(超历史均价但未超标准价)
po("PO-2025-0005", "PE100给水管 DN200", "SDR17 PN10 6m/根", "",
"浙江联塑管道", 200.0,
new BigDecimal("385.00"), new BigDecimal("400.00"), new BigDecimal("370.00"),
"凯迪水处理工程", "原材料", "2025-06-01", "2025-06-20",
new BigDecimal("800.00"), "孙采购", "管道价格较上次均价上涨4.1%,已评审通过", "已批准", "超历史均价");
// 订单6:钢材二期-正常-已完成(与订单1同物料,构成历史均价计算基础)
po("PO-2025-0006", "Q235B 钢材", "钢板 δ=8mm", "",
"华钢物资有限公司", 30.0,
new BigDecimal("5860.00"), new BigDecimal("6100.00"), new BigDecimal("5950.00"),
"凯迪水处理工程", "原材料", "2025-04-20", "2025-05-05",
new BigDecimal("150.00"), "张采购", "二期追加采购,与一期同供应商协商优惠价", "已完成", "正常");
}
private void po(String code, String materialName, String spec, String unit,
String supplierName, double qty,
BigDecimal unitPrice, BigDecimal standardPrice, BigDecimal historyAvgPrice,
String projectName, String category,
String applyDate, String deliveryDate,
BigDecimal freightCost, String applicant, String note,
String status, String priceStatus) {
CostPurchaseOrder o = new CostPurchaseOrder();
o.setCode(code);
o.setMaterialName(materialName);
o.setSpec(spec);
o.setUnit(unit);
o.setSupplierName(supplierName);
o.setQty(qty);
o.setUnitPrice(unitPrice);
o.setStandardPrice(standardPrice);
o.setHistoryAvgPrice(historyAvgPrice);
o.setLastPrice(historyAvgPrice);
o.setProjectName(projectName);
o.setCategory(category);
o.setApplyDate(applyDate);
o.setDeliveryDate(deliveryDate);
o.setFreightCost(freightCost);
o.setApplicant(applicant);
o.setNote(note);
o.setStatus(status);
o.setPriceStatus(priceStatus);
// totalAmount = qty * unitPrice
BigDecimal total = unitPrice.multiply(BigDecimal.valueOf(qty)).setScale(2, RoundingMode.HALF_UP);
o.setTotalAmount(total);
// landedCost = total + freightCost
o.setLandedCost(Money.add(total, freightCost));
// stdPriceVarianceRate = (unitPrice - standardPrice) / standardPrice * 100
if (standardPrice.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal rate = Money.sub(unitPrice, standardPrice)
.divide(standardPrice, 4, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100))
.setScale(2, RoundingMode.HALF_UP);
o.setStdPriceVarianceRate(rate);
} else {
o.setStdPriceVarianceRate(BigDecimal.ZERO);
}
o.setCreatedAt(Instant.now().minus(30, ChronoUnit.DAYS));
poRepo.save(o);
}
// ===================================================================
// 2. HR 工时费率种子(5 条,覆盖主要生产/管理部门)
// ===================================================================
private void seedHrLaborRates() {
if (rateRepo.count() > 0) return;
// 生产部(通用费率):月薪约 8000,168工时基准 → ≈47.62元/h
hrRate("生产部", null, new BigDecimal("47.62"), "2025-01", null,
"合同推算", "含五险一金分摊,月薪约8000元/168工时", "有效", "张会计");
// 生产部·焊工(技能岗溢价)
hrRate("生产部", "焊工", new BigDecimal("62.50"), "2025-01", null,
"HR录入", "焊工资质岗位工时费率,含技能溢价", "有效", "张会计");
// 成本控制部(管理部门):月薪约12000 → ≈71.43元/h
hrRate("成本控制部", null, new BigDecimal("71.43"), "2025-01", null,
"合同推算", "管理部门月薪约12000元/168工时基准", "有效", "张会计");
// 水处理运营中心(运营岗):月薪约9000 → ≈53.57元/h
hrRate("水处理运营中心", null, new BigDecimal("53.57"), "2025-01", null,
"合同推算", "运营岗标准费率,含夜班补贴折算", "有效", "李主管");
// 研发中心(高技能):月薪约15000 → ≈89.29元/h
hrRate("研发中心", null, new BigDecimal("89.29"), "2026-01", null,
"HR录入", "研发人员按岗位价值定价,含专利奖励折算", "有效", "王人事");
// 历史停用费率(2024年度旧标准,已停用,保留做趋势对比)
hrRate("生产部", null, new BigDecimal("42.86"), "2024-01", "2024-12",
"合同推算", "2024年度旧标准,已停用", "已停用", "张会计");
}
private void hrRate(String dept, String position, BigDecimal ratePerHour,
String effectiveFrom, String effectiveTo,
String source, String remark, String status, String createdBy) {
CostHrDeptLaborRate r = new CostHrDeptLaborRate();
r.setDept(dept);
r.setPosition(position);
r.setRatePerHour(ratePerHour);
r.setEffectiveFrom(effectiveFrom);
r.setEffectiveTo(effectiveTo);
r.setSource(source);
r.setRemark(remark);
r.setStatus(status);
r.setCreatedBy(createdBy);
r.setCreatedAt(Instant.now().minus(180, ChronoUnit.DAYS));
r.setUpdatedAt(Instant.now().minus(30, ChronoUnit.DAYS));
rateRepo.save(r);
}
// ===================================================================
// 3. 降本增效种子(5 条,跨状态 + 含已完成自动算降本)
// ===================================================================
private void seedCostReductions() {
if (reductionRepo.count() > 0) return;
// 项目1:集中采购钢材-已完成(节约58万,达成率96.7%)
reduction("CR-2025-0001", "钢材集中采购框架合同降本",
"集中采购", "Q235B钢材", "全线工程项目",
"成本控制部", "李成控", "2025-01-01", "2025-03-31", "2025-03-28",
new BigDecimal("6100.00"), new BigDecimal("600000.00"),
new BigDecimal("5800.00"), 200.0,
"已完成", "与华钢物资签订年度框架协议,锁定6%折扣,实际节约580000元",
"通过集中采购框架合同,将钢材单价由6100元/吨降至5800元/吨,采购200吨节约60000元");
// 项目2:替代物料-实施中
reduction("CR-2025-0002", "絮凝剂替代:PAC切换PAFC节本",
"替代物料", "聚合氯化铝(PAC", "凯迪环保项目A",
"成本控制部", "王成控", "2025-04-01", "2025-07-31", null,
new BigDecimal("2180.00"), new BigDecimal("180000.00"),
BigDecimal.ZERO, 0.0,
"实施中", null,
"将传统PAC切换至聚合氯化铝铁(PAFC),同等除浊效果单价降低约15%,预计节约18万元");
// 项目3:工艺优化-立项阶段
reduction("CR-2025-0003", "焊接工艺优化降低耗材损耗",
"工艺优化", "焊丝/焊条", "凯迪生物质锅炉项目",
"生产部", "赵工长", "2025-06-01", "2025-08-31", null,
new BigDecimal("85.00"), new BigDecimal("50000.00"),
BigDecimal.ZERO, 0.0,
"立项", null,
"引入MAG焊工艺替换手工弧焊,焊丝消耗率预期下降25%,年节约耗材成本5万元");
// 项目4:供应商谈判-已完成(超额达成122%)
reduction("CR-2026-0001", "变频器批量采购价格谈判",
"供应商谈判", "ABB变频器ACS550", "凯迪技改项目",
"成本控制部", "李成控", "2026-01-15", "2026-03-31", "2026-03-15",
new BigDecimal("18900.00"), new BigDecimal("50000.00"),
new BigDecimal("16500.00"), 3.0,
"已完成", "批量采购3台+延伸服务合同,实际降价12.7%,超额完成目标",
"与ABB授权代理商重新谈判,以批量+服务捆绑换取更低单价,节约72000元");
// 项目5:减少浪费-实施中
reduction("CR-2026-0002", "PE管道施工剩余料回收利用",
"减少浪费", "PE100给水管", "凯迪水处理工程",
"工程项目部", "孙工程师", "2026-03-01", "2026-06-30", null,
new BigDecimal("385.00"), new BigDecimal("30000.00"),
BigDecimal.ZERO, 0.0,
"实施中", null,
"建立施工剩余料统一回收再利用机制,减少20%的采购冗余,预期年节约3万元");
}
private void reduction(String code, String name,
String measureType, String targetItem, String projectName,
String department, String owner,
String planStartDate, String planEndDate, String actualEndDate,
BigDecimal baseCost, BigDecimal targetSaving,
BigDecimal actualCost, double implementQty,
String status, String completionNote, String description) {
CostReductionItem item = new CostReductionItem();
item.setCode(code);
item.setName(name);
item.setMeasureType(measureType);
item.setTargetItem(targetItem);
item.setProjectName(projectName);
item.setDepartment(department);
item.setOwner(owner);
item.setPlanStartDate(planStartDate);
item.setPlanEndDate(planEndDate);
item.setActualEndDate(actualEndDate);
item.setBaseCost(baseCost);
item.setTargetSaving(targetSaving);
item.setStatus(status);
item.setDescription(description);
item.setCompletionNote(completionNote);
item.setCreatedAt(Instant.now().minus(60, ChronoUnit.DAYS));
// 若已完成,自动计算实际降本金额 = (baseCost - actualCost) * implementQty
if ("已完成".equals(status) && implementQty > 0 && actualCost.compareTo(BigDecimal.ZERO) > 0) {
item.setActualCost(actualCost);
item.setImplementQty(implementQty);
BigDecimal actualSaving = Money.sub(baseCost, actualCost)
.multiply(BigDecimal.valueOf(implementQty))
.setScale(2, RoundingMode.HALF_UP);
item.setActualSaving(actualSaving);
// achievementRate = actualSaving / targetSaving * 100
if (targetSaving.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal rate = actualSaving
.divide(targetSaving, 4, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100))
.setScale(1, RoundingMode.HALF_UP);
item.setAchievementRate(rate);
}
} else {
item.setActualCost(BigDecimal.ZERO);
item.setActualSaving(BigDecimal.ZERO);
item.setAchievementRate(BigDecimal.ZERO);
}
reductionRepo.save(item);
}
// ===================================================================
// 4. 标准成本种子(6 条,跨期间跨产品,支持产品成本计算表期间对比)
// ===================================================================
private void seedStandardCosts() {
if (stdCostRepo.count() > 0) return;
// 凯迪生物质锅炉 - 2025-03(环比基准期)
stdCost("KD-BOILER-001", "凯迪生物质蒸汽锅炉(6T/H",
new BigDecimal("388500.00"), new BigDecimal("52500.00"), new BigDecimal("31200.00"),
new BigDecimal("415800.00"), "2025-03", "生物质肥料制造中心",
"2025年Q1标准成本,含主体钢材+配套件料费");
// 凯迪生物质锅炉 - 2025-06(环比+1季度,体现价格变动)
stdCost("KD-BOILER-001", "凯迪生物质蒸汽锅炉(6T/H",
new BigDecimal("398200.00"), new BigDecimal("54600.00"), new BigDecimal("32800.00"),
new BigDecimal("420800.00"), "2025-06", "生物质肥料制造中心",
"2025年Q2标准成本,钢材涨价导致料费上升2.5%");
// 凯迪生物质锅炉 - 2026-03(同比对比)
stdCost("KD-BOILER-001", "凯迪生物质蒸汽锅炉(6T/H",
new BigDecimal("393500.00"), new BigDecimal("56700.00"), new BigDecimal("33500.00"),
new BigDecimal("417200.00"), "2026-03", "生物质肥料制造中心",
"2026年Q1标准成本,人工费率调整后");
// 水处理一体化设备 - 2025-03
stdCost("KD-WTP-200", "凯迪一体化净水设备(200T/H",
new BigDecimal("156800.00"), new BigDecimal("28900.00"), new BigDecimal("18200.00"),
new BigDecimal("172500.00"), "2025-03", "水处理运营中心",
"水处理设备2025年基础标准成本");
// 水处理一体化设备 - 2025-06(实际成本超支)
stdCost("KD-WTP-200", "凯迪一体化净水设备(200T/H",
new BigDecimal("163400.00"), new BigDecimal("29800.00"), new BigDecimal("19100.00"),
new BigDecimal("183200.00"), "2025-06", "水处理运营中心",
"Q2实际成本超支4.7%,主要因泵件价格上涨");
// 污泥压滤机 - 2026-01(新产品首次建标)
stdCost("KD-SLD-100", "凯迪带式污泥压滤机",
new BigDecimal("89300.00"), new BigDecimal("18600.00"), new BigDecimal("11400.00"),
new BigDecimal("98200.00"), "2026-01", "水处理运营中心",
"新品首次建标,参考行业均价 + 自制成本分析");
}
private void stdCost(String productCode, String productName,
BigDecimal materialCost, BigDecimal laborCost, BigDecimal overheadCost,
BigDecimal actualCost, String period, String costCenter, String note) {
StandardCost sc = new StandardCost();
sc.setProductCode(productCode);
sc.setProductName(productName);
sc.setMaterialCost(materialCost);
sc.setLaborCost(laborCost);
sc.setOverheadCost(overheadCost);
BigDecimal total = Money.add(Money.add(materialCost, laborCost), overheadCost);
sc.setTotalStandard(total);
sc.setActualCost(actualCost);
sc.setVariance(Money.sub(actualCost, total));
sc.setPeriod(period);
sc.setCostCenter(costCenter);
sc.setNote(note);
sc.setCreatedAt(Instant.now().minus(90, ChronoUnit.DAYS));
stdCostRepo.save(sc);
}
}