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

212 lines
8.5 KiB
Java

package com.kaidi.oa.web;
import com.kaidi.oa.common.ApiException;
import com.kaidi.oa.common.ApiResp;
import com.kaidi.oa.common.NotFoundException;
import com.kaidi.oa.domain.BidTenderCollab;
import com.kaidi.oa.repository.BidRepository;
import com.kaidi.oa.repository.BidTenderCollabRepository;
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.PatchMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.util.List;
/**
* 标书协作工单管理(BidTenderCollab)。
*
* 办事处向总部提报本地价格/资质材料,协助编制标书;总部审阅后标记"资料完备"。
* collabType: 本地价格提供 / 资质材料提供 / 踏勘报告 / 综合协助
* status: 待提交 / 已提交 / 总部审阅中 / 资料完备 / 已归档
*/
@RestController
@RequestMapping("/api/oa/bid-tender-collabs")
public class BidTenderCollabController {
private final BidTenderCollabRepository collabRepo;
private final BidRepository bidRepo;
public BidTenderCollabController(BidTenderCollabRepository collabRepo,
BidRepository bidRepo) {
this.collabRepo = collabRepo;
this.bidRepo = bidRepo;
}
/** 合法状态集合(避免客户端写入非法状态)。 */
private static final List<String> VALID_STATUSES = List.of(
"待提交", "已提交", "总部审阅中", "资料完备", "已归档");
/** 合法协作类型集合。 */
private static final List<String> VALID_TYPES = List.of(
"本地价格提供", "资质材料提供", "踏勘报告", "综合协助");
/** 状态迁移合法路径(from → allowed nexts)。 */
private static boolean isValidTransition(String from, String to) {
if (from == null) return true;
return switch (from) {
case "待提交" -> List.of("已提交").contains(to);
case "已提交" -> List.of("总部审阅中", "待提交").contains(to);
case "总部审阅中" -> List.of("资料完备", "已提交").contains(to);
case "资料完备" -> List.of("已归档").contains(to);
default -> false;
};
}
@GetMapping
public ApiResp<List<BidTenderCollab>> list(
@RequestParam(required = false) String status,
@RequestParam(required = false) String branchName,
@RequestParam(required = false) String submitter,
@RequestParam(required = false) Long bidId) {
List<BidTenderCollab> result;
if (bidId != null) {
result = collabRepo.findByBidId(bidId);
} else if (status != null && !status.isBlank()) {
result = collabRepo.findByStatus(status);
} else if (branchName != null && !branchName.isBlank()) {
result = collabRepo.findByBranchName(branchName);
} else if (submitter != null && !submitter.isBlank()) {
result = collabRepo.findBySubmitter(submitter);
} else {
result = collabRepo.findAll();
}
return ApiResp.ok(result);
}
@GetMapping("/{id}")
public ApiResp<BidTenderCollab> get(@PathVariable Long id) {
return ApiResp.ok(collabRepo.findById(id)
.orElseThrow(() -> new NotFoundException("标书协作工单不存在: " + id)));
}
public record CreateCollabRequest(
Long bidId,
String collabType,
String subject,
String branchName,
String submitter,
String hqReviewer,
String materialSummary,
Long storedFileId,
String dueDate,
String status) {
}
@PostMapping
@Transactional
public ApiResp<BidTenderCollab> create(@RequestBody CreateCollabRequest req) {
if (req.subject() == null || req.subject().isBlank()) {
throw new ApiException(400, "协作主题(subject)不能为空");
}
if (req.branchName() == null || req.branchName().isBlank()) {
throw new ApiException(400, "提报办事处(branchName)不能为空");
}
if (req.collabType() != null && !VALID_TYPES.contains(req.collabType())) {
throw new ApiException(400, "collabType 非法:" + req.collabType());
}
String initStatus = (req.status() != null && !req.status().isBlank()) ? req.status() : "待提交";
if (!VALID_STATUSES.contains(initStatus)) {
throw new ApiException(400, "status 非法:" + initStatus);
}
// 若关联了 bid,冗余写入项目名称供展示。
String bidProjectName = null;
if (req.bidId() != null) {
bidProjectName = bidRepo.findById(req.bidId())
.map(b -> b.getProjectName())
.orElse(null);
}
BidTenderCollab c = new BidTenderCollab();
c.setBidId(req.bidId());
c.setCollabType(req.collabType());
c.setSubject(req.subject());
c.setBranchName(req.branchName());
c.setSubmitter(req.submitter());
c.setHqReviewer(req.hqReviewer());
c.setMaterialSummary(req.materialSummary());
c.setStoredFileId(req.storedFileId());
c.setDueDate(req.dueDate());
c.setStatus(initStatus);
c.setBidProjectName(bidProjectName);
c.setCreatedAt(Instant.now());
c.setUpdatedAt(Instant.now());
return ApiResp.ok(collabRepo.save(c));
}
public record UpdateCollabRequest(
String collabType,
String subject,
String branchName,
String submitter,
String hqReviewer,
String materialSummary,
Long storedFileId,
String dueDate,
String status,
String reviewNote) {
}
/**
* PATCH /{id} — 部分更新;状态迁移校验合法路径。
* 总部回填审阅意见(reviewNote)时同步推进状态到"资料完备"。
*/
@PatchMapping("/{id}")
@Transactional
public ApiResp<BidTenderCollab> update(@PathVariable Long id,
@RequestBody UpdateCollabRequest req) {
BidTenderCollab c = collabRepo.findById(id)
.orElseThrow(() -> new NotFoundException("标书协作工单不存在: " + id));
if (req.collabType() != null) {
if (!VALID_TYPES.contains(req.collabType())) {
throw new ApiException(400, "collabType 非法:" + req.collabType());
}
c.setCollabType(req.collabType());
}
if (req.subject() != null) {
if (req.subject().isBlank()) throw new ApiException(400, "协作主题不能为空");
c.setSubject(req.subject());
}
if (req.branchName() != null) c.setBranchName(req.branchName());
if (req.submitter() != null) c.setSubmitter(req.submitter());
if (req.hqReviewer() != null) c.setHqReviewer(req.hqReviewer());
if (req.materialSummary() != null) c.setMaterialSummary(req.materialSummary());
if (req.storedFileId() != null) c.setStoredFileId(req.storedFileId());
if (req.dueDate() != null) c.setDueDate(req.dueDate());
if (req.reviewNote() != null) c.setReviewNote(req.reviewNote());
if (req.status() != null && !req.status().equals(c.getStatus())) {
if (!VALID_STATUSES.contains(req.status())) {
throw new ApiException(400, "status 非法:" + req.status());
}
if (!isValidTransition(c.getStatus(), req.status())) {
throw new ApiException(400,
"非法状态迁移:" + c.getStatus() + " → " + req.status());
}
c.setStatus(req.status());
}
c.setUpdatedAt(Instant.now());
return ApiResp.ok(collabRepo.save(c));
}
@DeleteMapping("/{id}")
@Transactional
public ApiResp<Void> delete(@PathVariable Long id) {
if (!collabRepo.existsById(id)) {
throw new NotFoundException("标书协作工单不存在: " + id);
}
collabRepo.deleteById(id);
return ApiResp.ok(null);
}
}