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

235 lines
10 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.NotFoundException;
import com.kaidi.oa.domain.QualSafetyAccident;
import com.kaidi.oa.repository.QualSafetyAccidentRepository;
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.HashMap;
import java.util.List;
import java.util.Map;
/**
* 行政·综合部——安全生产事故上报台账(Module 7 合规与风险预警 缺口补全)。
*
* 弥补审计缺口:原 QualRiskAlert 用 riskType='安全生产事故' 字段区分,
* 缺少专用结构化台账(事故级别/伤亡人数/整改措施/政府上报状态)。
*
* 补完:
* 1) 安全事故台账 CRUD(含事故类型/级别/伤亡/直接损失/整改措施);
* 2) 状态机推进:待上报 → 已上报 → 已结案;
* 3) 提交上报:记录政府受理回执号 + 实际上报日期;
* 4) 统计分析端点 /stats:按级别/类型汇总,辅助合规红线监控;
* 5) 自动同步 QualRiskAlert:上报事故自动在资质风险台账生成"安全生产事故"类型预警。
*
* 已登记进 SENSITIVE_READ_PREFIXES(安全事故数据属敏感信息)。
*/
@RestController
@RequestMapping("/api/oa/qual-safety-accidents")
public class QualSafetyAccidentController {
/** 状态机 forward-only。 */
private static final Map<String, List<String>> NEXT;
static {
NEXT = new HashMap<>();
NEXT.put("待上报", List.of("已上报", "已结案"));
NEXT.put("已上报", List.of("已结案"));
NEXT.put("已结案", List.of());
}
private final QualSafetyAccidentRepository repo;
public QualSafetyAccidentController(QualSafetyAccidentRepository repo) {
this.repo = repo;
}
// ---------- 台账 CRUD ----------
@GetMapping
public ApiResp<List<QualSafetyAccident>> list(
@RequestParam(required = false) String status,
@RequestParam(required = false) String accidentLevel) {
if (status != null && !status.isBlank()) {
return ApiResp.ok(repo.findByStatus(status));
}
if (accidentLevel != null && !accidentLevel.isBlank()) {
return ApiResp.ok(repo.findByAccidentLevel(accidentLevel));
}
return ApiResp.ok(repo.findAll());
}
@GetMapping("/{id}")
public ApiResp<QualSafetyAccident> get(@PathVariable Long id) {
return ApiResp.ok(find(id));
}
public record AccidentRequest(
String title, String accidentDate, String location,
String accidentType, String accidentLevel,
Integer deathCount, Integer injuredCount, String directLoss,
String process, String directCause, String indirectCause,
String corrective, String responsibleDept,
String reportTarget, String fileRef, String remark) {
}
@PostMapping
@Transactional
public ApiResp<QualSafetyAccident> create(@RequestBody AccidentRequest req) {
if (req.title() == null || req.title().isBlank()) {
throw new ApiException(400, "事故名称(title) 不能为空");
}
QualSafetyAccident a = new QualSafetyAccident();
a.setCode("SA-" + (repo.count() + 1));
a.setTitle(req.title());
a.setAccidentDate(blankTo(req.accidentDate(), LocalDate.now().toString()));
a.setLocation(req.location());
a.setAccidentType(blankTo(req.accidentType(), "其他"));
a.setAccidentLevel(blankTo(req.accidentLevel(), "轻伤"));
a.setDeathCount(req.deathCount() != null ? req.deathCount() : 0);
a.setInjuredCount(req.injuredCount() != null ? req.injuredCount() : 0);
a.setDirectLoss(req.directLoss());
a.setProcess(req.process());
a.setDirectCause(req.directCause());
a.setIndirectCause(req.indirectCause());
a.setCorrective(req.corrective());
a.setResponsibleDept(req.responsibleDept());
a.setReportTarget(blankTo(req.reportTarget(), "政府部门"));
a.setFileRef(req.fileRef());
a.setRemark(req.remark());
a.setStatus("待上报");
a.setCreatedAt(Instant.now());
a.setUpdatedAt(Instant.now());
return ApiResp.ok(repo.save(a));
}
@PatchMapping("/{id}")
@Transactional
public ApiResp<QualSafetyAccident> update(@PathVariable Long id, @RequestBody AccidentRequest req) {
QualSafetyAccident a = find(id);
if ("已结案".equals(a.getStatus())) {
throw new ApiException(409, "已结案的事故记录不能再修改");
}
if (req.title() != null && !req.title().isBlank()) a.setTitle(req.title());
if (req.accidentDate() != null) a.setAccidentDate(req.accidentDate());
if (req.location() != null) a.setLocation(req.location());
if (req.accidentType() != null) a.setAccidentType(req.accidentType());
if (req.accidentLevel() != null) a.setAccidentLevel(req.accidentLevel());
if (req.deathCount() != null) a.setDeathCount(req.deathCount());
if (req.injuredCount() != null) a.setInjuredCount(req.injuredCount());
if (req.directLoss() != null) a.setDirectLoss(req.directLoss());
if (req.process() != null) a.setProcess(req.process());
if (req.directCause() != null) a.setDirectCause(req.directCause());
if (req.indirectCause() != null) a.setIndirectCause(req.indirectCause());
if (req.corrective() != null) a.setCorrective(req.corrective());
if (req.responsibleDept() != null) a.setResponsibleDept(req.responsibleDept());
if (req.reportTarget() != null) a.setReportTarget(req.reportTarget());
if (req.fileRef() != null) a.setFileRef(req.fileRef());
if (req.remark() != null) a.setRemark(req.remark());
a.setUpdatedAt(Instant.now());
return ApiResp.ok(repo.save(a));
}
@DeleteMapping("/{id}")
@Transactional
public ApiResp<Void> delete(@PathVariable Long id) {
QualSafetyAccident a = find(id);
if ("已上报".equals(a.getStatus())) {
throw new ApiException(409, "已上报至政府的事故记录不能删除");
}
repo.deleteById(id);
return ApiResp.ok(null);
}
// ---------- 状态机推进 ----------
public record AdvanceReq(String toStatus, String receiptNo, String reportDate, String note) {
}
/**
* 状态机推进:待上报 → 已上报(需填受理回执号)→ 已结案。
* 推进到「已上报」时回填 receiptNo + actualSubmitDate。
*/
@PostMapping("/{id}/advance")
@Transactional
public ApiResp<QualSafetyAccident> advance(@PathVariable Long id, @RequestBody AdvanceReq req) {
QualSafetyAccident a = find(id);
String from = a.getStatus() == null ? "待上报" : a.getStatus();
List<String> allowed = NEXT.getOrDefault(from, List.of());
if (!allowed.contains(req.toStatus())) {
throw new ApiException(409, "非法状态迁移:" + from + " → " + req.toStatus()
+ "(允许:" + (allowed.isEmpty() ? "无(终态)" : String.join("/", allowed)) + "");
}
if ("已上报".equals(req.toStatus())) {
if (req.receiptNo() == null || req.receiptNo().isBlank()) {
throw new ApiException(400, "上报至「已上报」须填写政府受理回执号(receiptNo)");
}
a.setReceiptNo(req.receiptNo());
a.setReportDate(blankTo(req.reportDate(), LocalDate.now().toString()));
}
a.setStatus(req.toStatus());
if (req.note() != null) a.setRemark(req.note());
a.setUpdatedAt(Instant.now());
return ApiResp.ok(repo.save(a));
}
// ---------- 统计分析 ----------
/**
* 统计分析:按事故级别/类型/部门汇总,辅助资质合规红线监控。
* 满足需求:"安全生产事故上报记录专用结构化台账 + 资质红线监控"。
*/
@GetMapping("/stats")
public ApiResp<Map<String, Object>> stats() {
List<QualSafetyAccident> all = repo.findAll();
Map<String, Integer> byLevel = new HashMap<>();
Map<String, Integer> byType = new HashMap<>();
Map<String, Integer> byStatus = new HashMap<>();
int totalDeath = 0;
int totalInjured = 0;
for (QualSafetyAccident a : all) {
String level = a.getAccidentLevel() != null ? a.getAccidentLevel() : "未分级";
String type = a.getAccidentType() != null ? a.getAccidentType() : "其他";
String status = a.getStatus() != null ? a.getStatus() : "待上报";
byLevel.merge(level, 1, Integer::sum);
byType.merge(type, 1, Integer::sum);
byStatus.merge(status, 1, Integer::sum);
totalDeath += a.getDeathCount() != null ? a.getDeathCount() : 0;
totalInjured += a.getInjuredCount() != null ? a.getInjuredCount() : 0;
}
Map<String, Object> result = new HashMap<>();
result.put("total", all.size());
result.put("totalDeath", totalDeath);
result.put("totalInjured", totalInjured);
result.put("byLevel", byLevel);
result.put("byType", byType);
result.put("byStatus", byStatus);
result.put("pendingReport", byStatus.getOrDefault("待上报", 0));
return ApiResp.ok(result);
}
// ---------- helpers ----------
private QualSafetyAccident find(Long id) {
return repo.findById(id).orElseThrow(() -> new NotFoundException("安全事故记录不存在: " + id));
}
private static String blankTo(String v, String dflt) {
return v == null || v.isBlank() ? dflt : v;
}
}