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

265 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.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-denyADMIN/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;
}
}
}