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,155 @@
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.SvPersonalCert;
import com.kaidi.oa.repository.SvPersonalCertRepository;
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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
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.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.Instant;
import java.util.List;
import java.util.stream.Collectors;
/**
* 工程监理部 · 监理人员执业资格证书台账(补足 Gap 1:监理方自身执业证有效期预警)。
*
* CRUD + 到期预警查询(GET /expiring?days=30):
* 返回有效期在当前 + days 天内的证书列表,方便总监提前处理续期。
* 状态自动判定:GET /expiring 同时自动将已过期证书 status 更新为「已过期」。
*
* 资源路径 /api/oa/sv-personal-certs。
*/
@RestController
@RequestMapping("/api/oa/sv-personal-certs")
public class SvPersonalCertController {
private final SvPersonalCertRepository certRepo;
public SvPersonalCertController(SvPersonalCertRepository certRepo) {
this.certRepo = certRepo;
}
@GetMapping
public ApiResp<List<SvPersonalCert>> list(
@RequestParam(required = false) Long projectId,
@RequestParam(required = false) String status,
@RequestParam(required = false) String personName) {
if (projectId != null) {
return ApiResp.ok(certRepo.findBySupervisionProjectId(projectId));
}
if (status != null && !status.isBlank()) {
return ApiResp.ok(certRepo.findByStatus(status));
}
if (personName != null && !personName.isBlank()) {
return ApiResp.ok(certRepo.findByPersonName(personName));
}
return ApiResp.ok(certRepo.findAll());
}
@GetMapping("/{id}")
public ApiResp<SvPersonalCert> get(@PathVariable Long id) {
return ApiResp.ok(find(id));
}
/**
* 到期预警查询:返回未来 days 天内到期的证书(默认 30 天),
* 并同步把已过期的自动标记为「已过期」。
*/
@GetMapping("/expiring")
@Transactional
public ApiResp<List<SvPersonalCert>> expiring(@RequestParam(defaultValue = "30") int days) {
String today = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
String deadline = LocalDate.now().plusDays(days).format(DateTimeFormatter.ISO_LOCAL_DATE);
// 自动更新已过期证书状态
certRepo.findByExpiryDateLessThanEqual(today).stream()
.filter(c -> !"已过期".equals(c.getStatus()) && !"已注销".equals(c.getStatus()))
.forEach(c -> { c.setStatus("已过期"); certRepo.save(c); });
// 返回 today <= expiryDate <= deadline 的有效证书(即将到期)
List<SvPersonalCert> result = certRepo.findByExpiryDateLessThanEqual(deadline).stream()
.filter(c -> c.getExpiryDate() != null && c.getExpiryDate().compareTo(today) >= 0)
.collect(Collectors.toList());
return ApiResp.ok(result);
}
public record CertRequest(
Long supervisionProjectId, String personName, String roleType, String certType,
String certNum, String issuingAuth, String issueDate, String expiryDate,
String status, String remark) {
}
@PostMapping
public ApiResp<SvPersonalCert> create(@RequestBody CertRequest req) {
if (req.personName() == null || req.personName().isBlank()) {
throw new ApiException(400, "人员姓名(personName) 不能为空");
}
if (req.expiryDate() == null || req.expiryDate().isBlank()) {
throw new ApiException(400, "有效期至(expiryDate) 不能为空");
}
SvPersonalCert c = new SvPersonalCert();
c.setSupervisionProjectId(req.supervisionProjectId());
c.setPersonName(req.personName());
c.setRoleType(req.roleType());
c.setCertType(req.certType() == null || req.certType().isBlank() ? "注册监理工程师" : req.certType());
c.setCertNum(req.certNum());
c.setIssuingAuth(req.issuingAuth());
c.setIssueDate(req.issueDate());
c.setExpiryDate(req.expiryDate());
// 自动判断有效/已过期
String today = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
String statusVal = req.status() != null && !req.status().isBlank() ? req.status()
: (req.expiryDate().compareTo(today) < 0 ? "已过期" : "有效");
c.setStatus(statusVal);
c.setRemark(req.remark());
c.setCreatedAt(Instant.now());
return ApiResp.ok(certRepo.save(c));
}
@PutMapping("/{id}")
public ApiResp<SvPersonalCert> update(@PathVariable Long id, @RequestBody CertRequest req) {
SvPersonalCert c = find(id);
if (req.supervisionProjectId() != null) c.setSupervisionProjectId(req.supervisionProjectId());
if (req.personName() != null && !req.personName().isBlank()) c.setPersonName(req.personName());
if (req.roleType() != null) c.setRoleType(req.roleType());
if (req.certType() != null && !req.certType().isBlank()) c.setCertType(req.certType());
if (req.certNum() != null) c.setCertNum(req.certNum());
if (req.issuingAuth() != null) c.setIssuingAuth(req.issuingAuth());
if (req.issueDate() != null) c.setIssueDate(req.issueDate());
if (req.expiryDate() != null && !req.expiryDate().isBlank()) {
c.setExpiryDate(req.expiryDate());
// 重算状态
String today = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
if (!"已注销".equals(c.getStatus())) {
c.setStatus(req.expiryDate().compareTo(today) < 0 ? "已过期" : "有效");
}
}
if (req.status() != null && !req.status().isBlank()) c.setStatus(req.status());
if (req.remark() != null) c.setRemark(req.remark());
return ApiResp.ok(certRepo.save(c));
}
@DeleteMapping("/{id}")
public ApiResp<Void> delete(@PathVariable Long id) {
if (!certRepo.existsById(id)) {
throw new NotFoundException("sv-personal-cert not found: " + id);
}
certRepo.deleteById(id);
return ApiResp.ok(null);
}
private SvPersonalCert find(Long id) {
return certRepo.findById(id)
.orElseThrow(() -> new NotFoundException("sv-personal-cert not found: " + id));
}
}