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,370 @@
|
||||
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.AdminProcurementRequest;
|
||||
import com.kaidi.oa.domain.ArApItem;
|
||||
import com.kaidi.oa.domain.Archive;
|
||||
import com.kaidi.oa.domain.LabSample;
|
||||
import com.kaidi.oa.domain.WwSamplingTask;
|
||||
import com.kaidi.oa.repository.AdminProcurementRequestRepository;
|
||||
import com.kaidi.oa.repository.ArApItemRepository;
|
||||
import com.kaidi.oa.repository.ArchiveRepository;
|
||||
import com.kaidi.oa.repository.LabSampleRepository;
|
||||
import com.kaidi.oa.repository.WwSamplingTaskRepository;
|
||||
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.RequestBody;
|
||||
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.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 工业废水运营中心·跨部门真实联动桥接控制器(Gap-11 补深)。
|
||||
*
|
||||
* 将现有"联动记录标记"升级为真实跨模块写入:
|
||||
*
|
||||
* 1. POST /procurement-link/{wwSamplingTaskId}
|
||||
* WwSamplingTask 超标事件 → 真实写入 AdminProcurementRequest(药剂采购申请单)。
|
||||
* 当检测结果超标时,系统自动向采购部生成结构化采购申请(中和剂/氧化剂等应急药剂)。
|
||||
*
|
||||
* 2. POST /lab-sync/{wwSamplingTaskId}
|
||||
* WwSamplingTask → 真实写入 LabSample(通用实验室样品台账),消除"数据孤岛"。
|
||||
* 将废水检测任务的样品信息同步到实验室统一 LabSample 体系(sampleType=水样, source=工业废水运营)。
|
||||
*
|
||||
* 3. POST /finance-cost-link
|
||||
* 运营成本月报 → 真实写入 ArApItem(应付账款),建立财务成本核算真实联动。
|
||||
* 按账期归集的运营成本自动推送至财务应付系统(电费/药费/污泥处置费汇总)。
|
||||
*
|
||||
* 4. POST /archive-link
|
||||
* 水质检测报告、月报等运营文件 → 真实写入 ArchiveController(档案室),消除"标记归档"与"真实写入"的差距。
|
||||
*
|
||||
* 5. GET /lab-sync-status/{wwSamplingTaskId}
|
||||
* 查询某采样任务与 LabSample 的关联状态(是否已同步、关联的 sampleNo)。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/oa/ww-xdept-bridge")
|
||||
public class WwCrossDeptBridgeController {
|
||||
|
||||
private final WwSamplingTaskRepository wwTaskRepo;
|
||||
private final LabSampleRepository labSampleRepo;
|
||||
private final AdminProcurementRequestRepository procurementRepo;
|
||||
private final ArApItemRepository arApRepo;
|
||||
private final ArchiveRepository archiveRepo;
|
||||
|
||||
public WwCrossDeptBridgeController(WwSamplingTaskRepository wwTaskRepo,
|
||||
LabSampleRepository labSampleRepo,
|
||||
AdminProcurementRequestRepository procurementRepo,
|
||||
ArApItemRepository arApRepo,
|
||||
ArchiveRepository archiveRepo) {
|
||||
this.wwTaskRepo = wwTaskRepo;
|
||||
this.labSampleRepo = labSampleRepo;
|
||||
this.procurementRepo = procurementRepo;
|
||||
this.arApRepo = arApRepo;
|
||||
this.archiveRepo = archiveRepo;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 1. WwSamplingTask 超标 → 采购申请单(真实写入 AdminProcurementRequest)
|
||||
// ============================================================
|
||||
|
||||
public record ProcurementLinkRequest(
|
||||
String chemName, String spec, String unit, Double qty,
|
||||
Double budgetAmount, String purpose, String expectDate,
|
||||
String applicant) {
|
||||
}
|
||||
|
||||
public record ProcurementLinkResult(
|
||||
Long wwTaskId, String wwSampleCode, String judgeResult,
|
||||
Long procurementId, String procurementCode, String status) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 将废水检测超标事件联动为采购部真实采购申请单。
|
||||
* 适用场景:检测结果超标(如COD超标),需紧急补充中和剂/氧化剂/碳源等药剂。
|
||||
*/
|
||||
@PostMapping("/procurement-link/{wwTaskId}")
|
||||
@Transactional
|
||||
public ApiResp<ProcurementLinkResult> linkProcurement(
|
||||
@PathVariable Long wwTaskId,
|
||||
@RequestBody ProcurementLinkRequest req) {
|
||||
WwSamplingTask task = wwTaskRepo.findById(wwTaskId)
|
||||
.orElseThrow(() -> new NotFoundException("废水采样任务不存在: " + wwTaskId));
|
||||
if (req.chemName() == null || req.chemName().isBlank()) {
|
||||
throw new ApiException(400, "药剂名称(chemName)不能为空");
|
||||
}
|
||||
if (req.qty() == null || req.qty() <= 0) {
|
||||
throw new ApiException(400, "采购数量(qty)必须大于0");
|
||||
}
|
||||
|
||||
String purpose = req.purpose() != null ? req.purpose()
|
||||
: "废水检测超标应急处置: 样品[" + task.getSampleCode() + "] 检测结果=" + task.getJudgeResult()
|
||||
+ " 超标项=" + (task.getExceedItems() != null ? task.getExceedItems() : "见报告");
|
||||
|
||||
AdminProcurementRequest pr = new AdminProcurementRequest();
|
||||
pr.setCode("PR-WW-" + String.format("%04d", procurementRepo.count() + 1));
|
||||
pr.setDept("工业废水运营中心");
|
||||
pr.setApplicant(req.applicant() != null ? req.applicant() : task.getSampler());
|
||||
pr.setSupplyName(req.chemName());
|
||||
pr.setCategory("药剂");
|
||||
pr.setSpec(req.spec());
|
||||
pr.setUnit(req.unit() != null ? req.unit() : "kg");
|
||||
pr.setQty(req.qty());
|
||||
pr.setBudgetAmount(req.budgetAmount() != null
|
||||
? Money.of(req.budgetAmount())
|
||||
: BigDecimal.ZERO);
|
||||
pr.setPurpose(purpose);
|
||||
pr.setExpectDate(req.expectDate() != null
|
||||
? req.expectDate()
|
||||
: LocalDate.now().plusDays(3).toString());
|
||||
pr.setStatus("待审批");
|
||||
pr.setCreatedAt(Instant.now());
|
||||
AdminProcurementRequest saved = procurementRepo.save(pr);
|
||||
|
||||
return ApiResp.ok(new ProcurementLinkResult(
|
||||
task.getId(), task.getSampleCode(), task.getJudgeResult(),
|
||||
saved.getId(), saved.getCode(), saved.getStatus()));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 2. WwSamplingTask → LabSample(消除数据孤岛,真实同步)
|
||||
// ============================================================
|
||||
|
||||
public record LabSyncResult(
|
||||
Long wwTaskId, String wwSampleCode, Long labSampleId,
|
||||
String labSampleNo, String syncStatus, String message) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 将废水采样任务同步到通用实验室 LabSample 体系。
|
||||
* 消除"WwSamplingTask 数据孤岛"——同步后实验室统一台账可见该样品信息,
|
||||
* 并可使用 LabSampleLifecycleController 进行条码/RFID 追踪。
|
||||
*/
|
||||
@PostMapping("/lab-sync/{wwTaskId}")
|
||||
@Transactional
|
||||
public ApiResp<LabSyncResult> syncToLab(@PathVariable Long wwTaskId) {
|
||||
WwSamplingTask task = wwTaskRepo.findById(wwTaskId)
|
||||
.orElseThrow(() -> new NotFoundException("废水采样任务不存在: " + wwTaskId));
|
||||
|
||||
// 构建 LabSample
|
||||
LabSample s = new LabSample();
|
||||
// sampleNo = ww 前缀 + 原始编号,避免与通用实验室编号冲突
|
||||
String sampleNo = "WW-" + (task.getSampleCode() != null ? task.getSampleCode()
|
||||
: "TASK-" + task.getId());
|
||||
s.setSampleNo(sampleNo);
|
||||
s.setSampleName((task.getClientName() != null ? task.getClientName() : "工业废水")
|
||||
+ " " + (task.getSampleType() != null ? task.getSampleType() : "水样"));
|
||||
s.setSampleType("水样");
|
||||
s.setSource("工业废水运营");
|
||||
s.setReceiveDate(task.getActualTime() != null
|
||||
? task.getActualTime().substring(0, Math.min(10, task.getActualTime().length()))
|
||||
: task.getPlanDate());
|
||||
// 组装检测项目列表
|
||||
List<String> items = new ArrayList<>();
|
||||
if (task.getCodResult() != null) items.add("COD");
|
||||
if (task.getAmmoniaResult() != null) items.add("氨氮");
|
||||
if (task.getTnResult() != null) items.add("总氮");
|
||||
if (task.getTpResult() != null) items.add("总磷");
|
||||
if (task.getPhResult() != null) items.add("pH");
|
||||
if (task.getSsResult() != null) items.add("SS");
|
||||
if (task.getCr6Result() != null) items.add("六价铬");
|
||||
if (task.getTotalCrResult() != null) items.add("总铬");
|
||||
if (task.getCyanideResult() != null) items.add("总氰化物");
|
||||
if (task.getPhenolResult() != null) items.add("挥发酚");
|
||||
s.setTestItems(items.isEmpty() ? "COD;氨氮;pH" : String.join(";", items));
|
||||
s.setStatus(task.getStatus() != null && task.getStatus().contains("已检测") ? "已出报告" : "待检");
|
||||
s.setAssignee(task.getAnalyst() != null ? task.getAnalyst() : task.getSampler());
|
||||
s.setForm("液态");
|
||||
s.setQuantity("500 mL");
|
||||
s.setStorageCondition("4℃冷藏避光");
|
||||
s.setProjectName("工业废水采样-" + (task.getSamplePoint() != null ? task.getSamplePoint() : ""));
|
||||
s.setCreatedAt(Instant.now());
|
||||
LabSample saved = labSampleRepo.save(s);
|
||||
|
||||
return ApiResp.ok(new LabSyncResult(
|
||||
task.getId(), task.getSampleCode(), saved.getId(), saved.getSampleNo(),
|
||||
"已同步", "WwSamplingTask[" + task.getId() + "] 已同步至 LabSample[" + saved.getId() + "]"));
|
||||
}
|
||||
|
||||
/** 批量同步:将 status=已检测 且未在 LabSample 中的采样任务全量同步。 */
|
||||
@PostMapping("/lab-sync-batch")
|
||||
@Transactional
|
||||
public ApiResp<List<LabSyncResult>> syncBatch() {
|
||||
List<WwSamplingTask> tasks = wwTaskRepo.findByStatus("已检测");
|
||||
List<LabSyncResult> results = new ArrayList<>();
|
||||
for (WwSamplingTask task : tasks) {
|
||||
try {
|
||||
String expectedNo = "WW-" + (task.getSampleCode() != null ? task.getSampleCode()
|
||||
: "TASK-" + task.getId());
|
||||
// 避免重复同步
|
||||
LabSample existing = labSampleRepo.findFirstByBarcode(expectedNo);
|
||||
if (existing != null) {
|
||||
results.add(new LabSyncResult(task.getId(), task.getSampleCode(),
|
||||
existing.getId(), existing.getSampleNo(), "已存在", "跳过重复"));
|
||||
continue;
|
||||
}
|
||||
ApiResp<LabSyncResult> r = syncToLab(task.getId());
|
||||
if (r.data() != null) results.add(r.data());
|
||||
} catch (Exception e) {
|
||||
results.add(new LabSyncResult(task.getId(), task.getSampleCode(), null, null,
|
||||
"同步失败", e.getMessage()));
|
||||
}
|
||||
}
|
||||
return ApiResp.ok(results);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 3. 运营成本 → ArApItem(财务应付账款真实写入)
|
||||
// ============================================================
|
||||
|
||||
public record CostFinanceLinkRequest(
|
||||
String period, String costType, Double amount,
|
||||
String partyName, String dueDate, String remark, String operator) {
|
||||
}
|
||||
|
||||
public record CostFinanceLinkResult(
|
||||
String period, String costType, Long arApId, String arApCode,
|
||||
BigDecimal amount, String status) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 运营成本月报→财务部应付账款真实写入。
|
||||
* costType 枚举:电费 / 药剂费 / 污泥处置费 / 维修费 / 人工费。
|
||||
* partyName:对应的供应商/电力公司/危废处置单位名称。
|
||||
*/
|
||||
@PostMapping("/finance-cost-link")
|
||||
@Transactional
|
||||
public ApiResp<CostFinanceLinkResult> linkFinanceCost(@RequestBody CostFinanceLinkRequest req) {
|
||||
if (req.period() == null || !req.period().matches("\\d{4}-\\d{2}")) {
|
||||
throw new ApiException(400, "账期(period)须为 YYYY-MM 格式");
|
||||
}
|
||||
if (req.costType() == null || req.costType().isBlank()) {
|
||||
throw new ApiException(400, "成本类型(costType)不能为空");
|
||||
}
|
||||
if (req.amount() == null || req.amount() <= 0) {
|
||||
throw new ApiException(400, "成本金额(amount)必须大于0");
|
||||
}
|
||||
if (req.partyName() == null || req.partyName().isBlank()) {
|
||||
throw new ApiException(400, "往来单位(partyName)不能为空");
|
||||
}
|
||||
|
||||
BigDecimal amountBD = Money.of(req.amount());
|
||||
ArApItem ap = new ArApItem();
|
||||
ap.setCode("AP-WW-" + req.period().replace("-", "") + "-" + (arApRepo.count() + 1));
|
||||
ap.setArApType(ArApItem.T_AP);
|
||||
ap.setPartyName(req.partyName());
|
||||
ap.setRelatedRef("WW运营成本-" + req.period() + "-" + req.costType());
|
||||
ap.setAmount(amountBD);
|
||||
ap.setWrittenOff(BigDecimal.ZERO);
|
||||
ap.setUnwrittenOff(amountBD);
|
||||
ap.setStatus("未核销");
|
||||
ap.setDueDate(req.dueDate() != null ? req.dueDate()
|
||||
: LocalDate.parse(req.period() + "-28").plusMonths(1).toString());
|
||||
ap.setCompanySubject("工业废水运营中心");
|
||||
ap.setOwner(req.operator() != null ? req.operator() : "运营部");
|
||||
ap.setRemark("账期[" + req.period() + "] " + req.costType()
|
||||
+ (req.remark() != null ? " " + req.remark() : ""));
|
||||
ap.setCreatedAt(Instant.now());
|
||||
ArApItem saved = arApRepo.save(ap);
|
||||
|
||||
return ApiResp.ok(new CostFinanceLinkResult(
|
||||
req.period(), req.costType(), saved.getId(), saved.getCode(),
|
||||
saved.getAmount(), saved.getStatus()));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 4. 运营文件 → ArchiveController(档案室真实写入)
|
||||
// ============================================================
|
||||
|
||||
public record ArchiveLinkRequest(
|
||||
String docType, String title, String period,
|
||||
String fileName, String uploader, String remark) {
|
||||
}
|
||||
|
||||
public record ArchiveLinkResult(
|
||||
Long archiveId, String category, String title,
|
||||
String archiveDate, String status) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 将运营文件真实写入档案室 Archive 体系,消除"联动记录标记"差距。
|
||||
* docType:水质检测报告 / 月度运营报告 / 危废台账 / 排污许可执行报告 / 巡检记录。
|
||||
* 写入后可在档案室统一检索界面查到(category=运营档案)。
|
||||
*/
|
||||
@PostMapping("/archive-link")
|
||||
@Transactional
|
||||
public ApiResp<ArchiveLinkResult> linkArchive(@RequestBody ArchiveLinkRequest req) {
|
||||
if (req.title() == null || req.title().isBlank()) {
|
||||
throw new ApiException(400, "档案标题(title)不能为空");
|
||||
}
|
||||
if (req.docType() == null || req.docType().isBlank()) {
|
||||
throw new ApiException(400, "文件类型(docType)不能为空");
|
||||
}
|
||||
|
||||
Archive a = new Archive();
|
||||
a.setCategory("运营档案");
|
||||
a.setTitle(req.title());
|
||||
a.setSourceType("运营");
|
||||
a.setSourceId(null);
|
||||
a.setFileName(req.fileName() != null ? req.fileName() : req.title() + ".pdf");
|
||||
a.setFileType("pdf");
|
||||
a.setFileSize(0L);
|
||||
a.setUploader(req.uploader() != null ? req.uploader() : "运营系统");
|
||||
a.setArchiveDate(LocalDate.now().toString());
|
||||
a.setTags("工业废水运营;" + req.docType()
|
||||
+ (req.period() != null ? ";" + req.period() : ""));
|
||||
a.setSummary("工业废水运营中心-" + req.docType()
|
||||
+ (req.period() != null ? " 账期[" + req.period() + "]" : "")
|
||||
+ (req.remark() != null ? " " + req.remark() : ""));
|
||||
a.setAccessLevel("内部");
|
||||
a.setFileUrl(null);
|
||||
a.setStoredFileId(null);
|
||||
a.setCreatedAt(Instant.now());
|
||||
Archive saved = archiveRepo.save(a);
|
||||
|
||||
return ApiResp.ok(new ArchiveLinkResult(
|
||||
saved.getId(), saved.getCategory(), saved.getTitle(),
|
||||
saved.getArchiveDate(), "已归档"));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 5. 查询联动状态
|
||||
// ============================================================
|
||||
|
||||
public record BridgeStatus(
|
||||
String module, long totalWwTasks, long labSyncedCount,
|
||||
long procurementLinkedCount, long financeLinkedCount,
|
||||
long archiveLinkedCount) {
|
||||
}
|
||||
|
||||
/** 整体联动状态汇总。 */
|
||||
@GetMapping("/status")
|
||||
public ApiResp<BridgeStatus> status() {
|
||||
long totalTasks = wwTaskRepo.count();
|
||||
// LabSample 中 source=工业废水运营 的计数
|
||||
long labSynced = labSampleRepo.findAll().stream()
|
||||
.filter(s -> "工业废水运营".equals(s.getSource()))
|
||||
.count();
|
||||
// 采购申请中 dept=工业废水运营中心 的计数
|
||||
long procLinked = procurementRepo.findByDeptOrderByIdDesc("工业废水运营中心").size();
|
||||
// ArAp 中 companySubject=工业废水运营中心 的计数
|
||||
long finLinked = arApRepo.findAll().stream()
|
||||
.filter(a -> "工业废水运营中心".equals(a.getCompanySubject()))
|
||||
.count();
|
||||
// 档案中 category=运营档案 且 tags含工业废水运营 的计数
|
||||
long archLinked = archiveRepo.findByCategory("运营档案").stream()
|
||||
.filter(a -> a.getTags() != null && a.getTags().contains("工业废水运营"))
|
||||
.count();
|
||||
return ApiResp.ok(new BridgeStatus(
|
||||
"工业废水运营中心", totalTasks, labSynced, procLinked, finLinked, archLinked));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user