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:
@@ -0,0 +1,264 @@
|
||||
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.BranchLicense;
|
||||
import com.kaidi.oa.repository.BranchLicenseRepository;
|
||||
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.time.format.DateTimeParseException;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分公司证照专项台账(分公司模块·行政与资产管理 §6)。
|
||||
*
|
||||
* 审计缺口补完:
|
||||
* ① 营业执照号、经营范围、注册资本、登记机关等专项字段;
|
||||
* ② 年检到期日 + 年检周期提醒(annualInspectDue,<=30天触发临检,已过期触发逾期);
|
||||
* ③ 有效期状态机(有效/临期/已过期),读时自动刷新;
|
||||
* ④ GET /alerts/annual-inspect 年检分级预警端点,驱动到期提醒。
|
||||
*
|
||||
* 写口 default-deny(ADMIN/APPROVER)保护;含机密信息,读口属受限资源。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/oa/branch-licenses")
|
||||
public class BranchLicenseController {
|
||||
|
||||
private final BranchLicenseRepository repo;
|
||||
|
||||
public BranchLicenseController(BranchLicenseRepository repo) {
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
// ---------- CRUD ----------
|
||||
|
||||
@GetMapping
|
||||
public ApiResp<List<BranchLicense>> list(@RequestParam(required = false) String org,
|
||||
@RequestParam(required = false) String licenseType,
|
||||
@RequestParam(required = false) String status) {
|
||||
List<BranchLicense> all;
|
||||
if (org != null && !org.isBlank()) {
|
||||
all = repo.findByOrg(org);
|
||||
} else if (licenseType != null && !licenseType.isBlank()) {
|
||||
all = repo.findByLicenseType(licenseType);
|
||||
} else {
|
||||
all = repo.findAll();
|
||||
}
|
||||
for (BranchLicense lic : all) {
|
||||
refreshExpiryView(lic);
|
||||
}
|
||||
if (status != null && !status.isBlank()) {
|
||||
List<BranchLicense> filtered = new ArrayList<>();
|
||||
for (BranchLicense lic : all) {
|
||||
if (status.equals(lic.getStatus())) {
|
||||
filtered.add(lic);
|
||||
}
|
||||
}
|
||||
return ApiResp.ok(filtered);
|
||||
}
|
||||
return ApiResp.ok(all);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ApiResp<BranchLicense> get(@PathVariable Long id) {
|
||||
BranchLicense lic = repo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("branch license not found: " + id));
|
||||
refreshExpiryView(lic);
|
||||
return ApiResp.ok(lic);
|
||||
}
|
||||
|
||||
public record LicenseRequest(
|
||||
String org, String licenseType, String name, String regNo,
|
||||
String bizScope, String registeredCapital, String regAuthority,
|
||||
String issueDate, String expiryDate, String annualInspectDue,
|
||||
String remark, String owner) {
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ApiResp<BranchLicense> create(@RequestBody LicenseRequest req) {
|
||||
if (req.name() == null || req.name().isBlank()) {
|
||||
throw new ApiException(400, "证照名称必填");
|
||||
}
|
||||
BranchLicense lic = new BranchLicense();
|
||||
lic.setOrg(req.org());
|
||||
lic.setLicenseType(req.licenseType() == null || req.licenseType().isBlank() ? "营业执照" : req.licenseType());
|
||||
lic.setName(req.name());
|
||||
lic.setRegNo(req.regNo());
|
||||
lic.setBizScope(req.bizScope());
|
||||
lic.setRegisteredCapital(req.registeredCapital());
|
||||
lic.setRegAuthority(req.regAuthority());
|
||||
lic.setIssueDate(req.issueDate());
|
||||
lic.setExpiryDate(req.expiryDate());
|
||||
lic.setAnnualInspectDue(req.annualInspectDue());
|
||||
lic.setRemark(req.remark());
|
||||
lic.setOwner(req.owner());
|
||||
lic.setCreatedAt(Instant.now());
|
||||
applyExpiryState(lic);
|
||||
return ApiResp.ok(repo.save(lic));
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}")
|
||||
public ApiResp<BranchLicense> update(@PathVariable Long id, @RequestBody LicenseRequest req) {
|
||||
BranchLicense lic = repo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("branch license not found: " + id));
|
||||
if (req.org() != null) lic.setOrg(req.org());
|
||||
if (req.licenseType() != null && !req.licenseType().isBlank()) lic.setLicenseType(req.licenseType());
|
||||
if (req.name() != null) {
|
||||
if (req.name().isBlank()) throw new ApiException(400, "证照名称不能为空");
|
||||
lic.setName(req.name());
|
||||
}
|
||||
if (req.regNo() != null) lic.setRegNo(req.regNo());
|
||||
if (req.bizScope() != null) lic.setBizScope(req.bizScope());
|
||||
if (req.registeredCapital() != null) lic.setRegisteredCapital(req.registeredCapital());
|
||||
if (req.regAuthority() != null) lic.setRegAuthority(req.regAuthority());
|
||||
if (req.issueDate() != null) lic.setIssueDate(req.issueDate());
|
||||
if (req.expiryDate() != null) lic.setExpiryDate(req.expiryDate());
|
||||
if (req.annualInspectDue() != null) lic.setAnnualInspectDue(req.annualInspectDue());
|
||||
if (req.remark() != null) lic.setRemark(req.remark());
|
||||
if (req.owner() != null) lic.setOwner(req.owner());
|
||||
applyExpiryState(lic);
|
||||
return ApiResp.ok(repo.save(lic));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResp<Void> delete(@PathVariable Long id) {
|
||||
if (!repo.existsById(id)) {
|
||||
throw new NotFoundException("branch license not found: " + id);
|
||||
}
|
||||
repo.deleteById(id);
|
||||
return ApiResp.ok(null);
|
||||
}
|
||||
|
||||
// ---------- 年检分级预警端点 ----------
|
||||
|
||||
public record AnnualInspectAlert(
|
||||
Long id, String org, String licenseType, String name, String regNo,
|
||||
String annualInspectDue, long daysToInspect, String level) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 年检周期提醒(需求 §6「有效期提醒/年检周期提醒」)。
|
||||
* level: 逾期(已过年检截止) / 紧急(<=30天) / 临近(<=60天) / 关注(<=days参数)。
|
||||
*/
|
||||
@GetMapping("/alerts/annual-inspect")
|
||||
public ApiResp<List<AnnualInspectAlert>> annualInspectAlerts(
|
||||
@RequestParam(required = false, defaultValue = "60") int days) {
|
||||
LocalDate today = LocalDate.now();
|
||||
List<AnnualInspectAlert> out = new ArrayList<>();
|
||||
for (BranchLicense lic : repo.findAll()) {
|
||||
LocalDate due = parseDateOrNull(lic.getAnnualInspectDue());
|
||||
if (due == null) {
|
||||
continue;
|
||||
}
|
||||
long d = ChronoUnit.DAYS.between(today, due);
|
||||
if (d > days) {
|
||||
continue;
|
||||
}
|
||||
String level = d < 0 ? "逾期" : d <= 30 ? "紧急" : d <= 60 ? "临近" : "关注";
|
||||
out.add(new AnnualInspectAlert(lic.getId(), lic.getOrg(), lic.getLicenseType(),
|
||||
lic.getName(), lic.getRegNo(), lic.getAnnualInspectDue(), d, level));
|
||||
}
|
||||
out.sort((a, b) -> Long.compare(a.daysToInspect(), b.daysToInspect()));
|
||||
return ApiResp.ok(out);
|
||||
}
|
||||
|
||||
// ---------- 有效期预警端点 ----------
|
||||
|
||||
public record ExpiryAlert(
|
||||
Long id, String org, String licenseType, String name,
|
||||
String expiryDate, long daysToExpiry, String level) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 有效期到期预警(与年检提醒分开,对应营业执照有效期/资质证书年检后续期)。
|
||||
*/
|
||||
@GetMapping("/alerts/expiry")
|
||||
public ApiResp<List<ExpiryAlert>> expiryAlerts(
|
||||
@RequestParam(required = false, defaultValue = "90") int days) {
|
||||
LocalDate today = LocalDate.now();
|
||||
List<ExpiryAlert> out = new ArrayList<>();
|
||||
for (BranchLicense lic : repo.findAll()) {
|
||||
LocalDate exp = parseDateOrNull(lic.getExpiryDate());
|
||||
if (exp == null) {
|
||||
continue;
|
||||
}
|
||||
long d = ChronoUnit.DAYS.between(today, exp);
|
||||
if (d > days) {
|
||||
continue;
|
||||
}
|
||||
String level = d < 0 ? "逾期" : d <= 30 ? "紧急" : d <= 60 ? "临近" : "关注";
|
||||
out.add(new ExpiryAlert(lic.getId(), lic.getOrg(), lic.getLicenseType(),
|
||||
lic.getName(), lic.getExpiryDate(), d, level));
|
||||
}
|
||||
out.sort((a, b) -> Long.compare(a.daysToExpiry(), b.daysToExpiry()));
|
||||
return ApiResp.ok(out);
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
/**
|
||||
* 有效期状态机(读时刷新,不落库):有效 / 临期(<=60天) / 已过期。
|
||||
* 年检状态按 annualInspectDue 独立刷新:正常 / 临检(<=30天) / 逾期 / 免检(空)。
|
||||
*/
|
||||
private void applyExpiryState(BranchLicense lic) {
|
||||
LocalDate today = LocalDate.now();
|
||||
|
||||
// 有效期状态
|
||||
LocalDate exp = parseDateOrNull(lic.getExpiryDate());
|
||||
if (exp == null) {
|
||||
lic.setStatus("有效");
|
||||
} else {
|
||||
long d = ChronoUnit.DAYS.between(today, exp);
|
||||
if (d < 0) {
|
||||
lic.setStatus("已过期");
|
||||
} else if (d <= 60) {
|
||||
lic.setStatus("临期");
|
||||
} else {
|
||||
lic.setStatus("有效");
|
||||
}
|
||||
}
|
||||
|
||||
// 年检状态
|
||||
LocalDate inspectDue = parseDateOrNull(lic.getAnnualInspectDue());
|
||||
if (inspectDue == null) {
|
||||
lic.setAnnualInspectStatus("免检");
|
||||
} else {
|
||||
long d = ChronoUnit.DAYS.between(today, inspectDue);
|
||||
if (d < 0) {
|
||||
lic.setAnnualInspectStatus("逾期");
|
||||
} else if (d <= 30) {
|
||||
lic.setAnnualInspectStatus("临检");
|
||||
} else {
|
||||
lic.setAnnualInspectStatus("正常");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshExpiryView(BranchLicense lic) {
|
||||
applyExpiryState(lic);
|
||||
}
|
||||
|
||||
private static LocalDate parseDateOrNull(String s) {
|
||||
if (s == null || s.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(s.trim().substring(0, Math.min(10, s.trim().length())));
|
||||
} catch (DateTimeParseException | IndexOutOfBoundsException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user