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

371 lines
18 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.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));
}
}