恢复点(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>
235 lines
10 KiB
Java
235 lines
10 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.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;
|
||
}
|
||
}
|