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,236 @@
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.SupervisionProject;
import com.kaidi.oa.domain.SvAccident;
import com.kaidi.oa.repository.SupervisionProjectRepository;
import com.kaidi.oa.repository.SvAccidentRepository;
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.time.LocalDate;
import java.util.List;
/**
* 工程监理部 · 安全事故快速上报(补 Gap 6SafetyCheckController 无 accident/伤亡专属流程)。
*
* 完整流程:
* 快速上报(POST /)→ 已上报
* 启动应急预案(POST /{id}/activate-emergency)→ 应急处置中
* 移交调查(POST /{id}/investigate)→ 调查中
* 归档整改措施(POST /{id}/archive)→ 整改归档
* 确认闭环(POST /{id}/close)→ 已闭环
*
* 资源路径 /api/oa/sv-accidents。
*/
@RestController
@RequestMapping("/api/oa/sv-accidents")
public class SvAccidentController {
private static final List<String> ACCIDENT_TYPES = List.of("伤亡事故", "财产损失", "险肇事故", "环境污染");
private static final List<String> LEVELS = List.of("一般事故", "较大事故", "重大事故", "特别重大事故");
private final SvAccidentRepository accidentRepo;
private final SupervisionProjectRepository projectRepo;
public SvAccidentController(SvAccidentRepository accidentRepo, SupervisionProjectRepository projectRepo) {
this.accidentRepo = accidentRepo;
this.projectRepo = projectRepo;
}
@GetMapping
public ApiResp<List<SvAccident>> list(
@RequestParam(required = false) Long projectId,
@RequestParam(required = false) String status,
@RequestParam(required = false) String level) {
if (projectId != null) {
return ApiResp.ok(accidentRepo.findBySupervisionProjectIdOrderByIdDesc(projectId));
}
if (status != null && !status.isBlank()) {
return ApiResp.ok(accidentRepo.findByStatus(status));
}
if (level != null && !level.isBlank()) {
return ApiResp.ok(accidentRepo.findByLevel(level));
}
return ApiResp.ok(accidentRepo.findAll());
}
@GetMapping("/{id}")
public ApiResp<SvAccident> get(@PathVariable Long id) {
return ApiResp.ok(find(id));
}
public record ReportRequest(
Long supervisionProjectId, String occurTime, String location,
String accidentType, String level, String casualtyInfo,
String preliminaryCause, String reporter, String attachmentRefs) {
}
/**
* 快速上报安全事故:填基本信息 + 伤亡情况 + 初步原因,状态置「已上报」。
* 重大/特别重大事故自动标记预警(level 字段)。
*/
@PostMapping
@Transactional
public ApiResp<SvAccident> report(@RequestBody ReportRequest req) {
if (req.supervisionProjectId() == null) {
throw new ApiException(400, "所属监理项目(supervisionProjectId) 不能为空");
}
SupervisionProject parent = projectRepo.findById(req.supervisionProjectId())
.orElseThrow(() -> new NotFoundException("supervision project not found: " + req.supervisionProjectId()));
if (req.occurTime() == null || req.occurTime().isBlank()) {
throw new ApiException(400, "事故发生时间(occurTime) 不能为空");
}
if (req.accidentType() != null && !req.accidentType().isBlank()
&& !ACCIDENT_TYPES.contains(req.accidentType())) {
throw new ApiException(400, "事故类型无效,允许:" + ACCIDENT_TYPES);
}
if (req.level() != null && !req.level().isBlank() && !LEVELS.contains(req.level())) {
throw new ApiException(400, "事故等级无效,允许:" + LEVELS);
}
SvAccident a = new SvAccident();
a.setSupervisionProjectId(parent.getId());
a.setProjectCode(parent.getCode());
a.setAccidentNo("SGT-" + (accidentRepo.count() + 1));
a.setOccurTime(req.occurTime());
a.setLocation(req.location());
a.setAccidentType(req.accidentType() == null || req.accidentType().isBlank()
? "伤亡事故" : req.accidentType());
a.setLevel(req.level() == null || req.level().isBlank() ? "一般事故" : req.level());
a.setCasualtyInfo(req.casualtyInfo());
a.setPreliminaryCause(req.preliminaryCause());
a.setReporter(req.reporter());
a.setReportTime(Instant.now());
a.setEmergencyActivated(false);
a.setAttachmentRefs(req.attachmentRefs());
a.setStatus("已上报");
a.setCreatedAt(Instant.now());
return ApiResp.ok(accidentRepo.save(a));
}
@PatchMapping("/{id}")
public ApiResp<SvAccident> update(@PathVariable Long id, @RequestBody ReportRequest req) {
SvAccident a = find(id);
if ("已闭环".equals(a.getStatus())) {
throw new ApiException(409, "已闭环的事故记录不可修改");
}
if (req.occurTime() != null && !req.occurTime().isBlank()) a.setOccurTime(req.occurTime());
if (req.location() != null) a.setLocation(req.location());
if (req.accidentType() != null && !req.accidentType().isBlank()) {
if (!ACCIDENT_TYPES.contains(req.accidentType())) throw new ApiException(400, "事故类型无效:" + ACCIDENT_TYPES);
a.setAccidentType(req.accidentType());
}
if (req.level() != null && !req.level().isBlank()) {
if (!LEVELS.contains(req.level())) throw new ApiException(400, "事故等级无效:" + LEVELS);
a.setLevel(req.level());
}
if (req.casualtyInfo() != null) a.setCasualtyInfo(req.casualtyInfo());
if (req.preliminaryCause() != null) a.setPreliminaryCause(req.preliminaryCause());
if (req.attachmentRefs() != null) a.setAttachmentRefs(req.attachmentRefs());
return ApiResp.ok(accidentRepo.save(a));
}
@DeleteMapping("/{id}")
public ApiResp<Void> delete(@PathVariable Long id) {
SvAccident a = find(id);
if (!"已上报".equals(a.getStatus())) {
throw new ApiException(409, "仅「已上报」状态的事故记录可删除(撤回)");
}
accidentRepo.deleteById(id);
return ApiResp.ok(null);
}
// ---------- 业务动作 ----------
public record EmergencyRequest(String emergencyPlanRef, String emergencyDesc) {
}
/**
* 启动应急预案:已上报 → 应急处置中。
* 记录应急预案引用(关联 SvPlan.planType=监理应急预案)。
*/
@PostMapping("/{id}/activate-emergency")
@Transactional
public ApiResp<SvAccident> activateEmergency(@PathVariable Long id, @RequestBody EmergencyRequest req) {
SvAccident a = find(id);
if (!"已上报".equals(a.getStatus())) {
throw new ApiException(409, "仅「已上报」状态可启动应急,当前:" + a.getStatus());
}
if (Boolean.TRUE.equals(a.getEmergencyActivated())) {
throw new ApiException(409, "应急预案已启动,请勿重复");
}
a.setEmergencyActivated(true);
a.setEmergencyPlanRef(req.emergencyPlanRef());
a.setEmergencyDesc(req.emergencyDesc());
a.setStatus("应急处置中");
return ApiResp.ok(accidentRepo.save(a));
}
public record InvestigateRequest(String investigationResult) {
}
/** 移交/进入事故调查:应急处置中 → 调查中。 */
@PostMapping("/{id}/investigate")
@Transactional
public ApiResp<SvAccident> investigate(@PathVariable Long id, @RequestBody InvestigateRequest req) {
SvAccident a = find(id);
if (!"应急处置中".equals(a.getStatus())) {
throw new ApiException(409, "请先启动应急预案,再进入调查阶段");
}
if (req.investigationResult() != null) a.setInvestigationResult(req.investigationResult());
a.setStatus("调查中");
return ApiResp.ok(accidentRepo.save(a));
}
public record ArchiveRequest(String correctiveMeasures, String correctiveDate, String investigationResult) {
}
/** 归档整改措施:调查中 → 整改归档。 */
@PostMapping("/{id}/archive")
@Transactional
public ApiResp<SvAccident> archive(@PathVariable Long id, @RequestBody ArchiveRequest req) {
SvAccident a = find(id);
if (!"调查中".equals(a.getStatus())) {
throw new ApiException(409, "请先进入调查阶段,再归档整改措施");
}
if (req.correctiveMeasures() == null || req.correctiveMeasures().isBlank()) {
throw new ApiException(400, "整改措施(correctiveMeasures) 不能为空");
}
if (req.investigationResult() != null && !req.investigationResult().isBlank()) {
a.setInvestigationResult(req.investigationResult());
}
a.setCorrectiveMeasures(req.correctiveMeasures());
a.setCorrectiveDate(req.correctiveDate() == null || req.correctiveDate().isBlank()
? LocalDate.now().toString() : req.correctiveDate());
a.setStatus("整改归档");
return ApiResp.ok(accidentRepo.save(a));
}
/** 确认闭环:整改归档 → 已闭环。 */
@PostMapping("/{id}/close")
@Transactional
public ApiResp<SvAccident> close(@PathVariable Long id) {
SvAccident a = find(id);
if (!"整改归档".equals(a.getStatus())) {
throw new ApiException(409, "请先完成调查归档,再闭环");
}
a.setStatus("已闭环");
return ApiResp.ok(accidentRepo.save(a));
}
private SvAccident find(Long id) {
return accidentRepo.findById(id)
.orElseThrow(() -> new NotFoundException("sv-accident not found: " + id));
}
}