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.AuditNotice; import com.kaidi.oa.domain.AuditProject; import com.kaidi.oa.repository.AuditNoticeRepository; import com.kaidi.oa.repository.AuditProjectRepository; 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.time.Year; import java.util.List; /** * 审计通知书(内控部·审计监察部·审计项目实施管理)。 * * 解决审计缺口:「缺审计通知签收」——在线生成审计通知书,发送至被审计单位,被审计单位在线签收确认。 * * 状态机 + 跨实体级联(@Transactional): * - POST / 生成审计通知书(noticeNo 自动编号 AN-yyyy-序号)→ 待签收; * - PATCH /{id}/sign 被审计单位在线签收确认 → 已签收(回写 signedAt/signedBy); * - PATCH /{id}/recall 撤回通知(待签收 → 已撤回); * - DELETE /{id} 删除(仅待签收/已撤回可删除)。 * * 联动:签收后自动将关联审计项目状态推进至「实施中」(若当前为「计划」)。 */ @RestController @RequestMapping("/api/oa/audit-notices") public class AuditNoticeController { private final AuditNoticeRepository noticeRepo; private final AuditProjectRepository projectRepo; public AuditNoticeController(AuditNoticeRepository noticeRepo, AuditProjectRepository projectRepo) { this.noticeRepo = noticeRepo; this.projectRepo = projectRepo; } @GetMapping public ApiResp> list(@RequestParam(required = false) Long projectId, @RequestParam(required = false) String status) { if (projectId != null) { return ApiResp.ok(noticeRepo.findByProjectId(projectId)); } if (status != null && !status.isBlank()) { return ApiResp.ok(noticeRepo.findByStatus(status)); } return ApiResp.ok(noticeRepo.findByOrderByIdDesc()); } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { return ApiResp.ok(load(id)); } public record CreateRequest( Long projectId, String purpose, String plannedStartDate, String issuer, String issueDate) { } /** 生成审计通知书(状态 待签收)。 */ @PostMapping @Transactional public ApiResp create(@RequestBody CreateRequest req) { if (req.projectId() == null) { throw new ApiException(400, "请选择关联审计项目"); } AuditProject project = projectRepo.findById(req.projectId()) .orElseThrow(() -> new ApiException(400, "审计项目不存在: " + req.projectId())); AuditNotice n = new AuditNotice(); n.setNoticeNo(nextNoticeNo()); n.setProjectId(project.getId()); n.setProjectName(project.getName()); n.setAuditee(project.getAuditee()); n.setAuditPeriod(project.getPeriod()); n.setPurpose(req.purpose()); n.setPlannedStartDate(req.plannedStartDate()); n.setIssuer(req.issuer()); n.setIssueDate(req.issueDate() != null && !req.issueDate().isBlank() ? req.issueDate() : LocalDate.now().toString()); n.setStatus("待签收"); n.setCreatedAt(Instant.now()); return ApiResp.ok(noticeRepo.save(n)); } public record SignRequest(String signedBy, String signNote) { } /** * 被审计单位在线签收确认 → 已签收。 * 联动:若关联审计项目当前为「计划」,自动推进至「实施中」。 */ @PatchMapping("/{id}/sign") @Transactional public ApiResp sign(@PathVariable Long id, @RequestBody SignRequest req) { AuditNotice n = load(id); if ("已签收".equals(n.getStatus())) { throw new ApiException(409, "该审计通知已签收,请勿重复操作"); } if ("已撤回".equals(n.getStatus())) { throw new ApiException(409, "该审计通知已撤回,无法签收"); } if (req.signedBy() == null || req.signedBy().isBlank()) { throw new ApiException(400, "签收人不能为空"); } n.setSignedBy(req.signedBy()); n.setSignedAt(Instant.now()); n.setSignNote(req.signNote()); n.setStatus("已签收"); // 联动推进项目状态(计划 → 实施中)。 if (n.getProjectId() != null) { projectRepo.findById(n.getProjectId()).ifPresent(p -> { if ("计划".equals(p.getStatus())) { p.setStatus("实施中"); projectRepo.save(p); } }); } return ApiResp.ok(noticeRepo.save(n)); } public record RecallRequest(String reason) { } /** 撤回通知(仅待签收状态可撤回)。 */ @PatchMapping("/{id}/recall") public ApiResp recall(@PathVariable Long id, @RequestBody RecallRequest req) { AuditNotice n = load(id); if (!"待签收".equals(n.getStatus())) { throw new ApiException(409, "只有待签收状态的通知书可以撤回"); } n.setStatus("已撤回"); if (req.reason() != null) { n.setSignNote(req.reason()); } return ApiResp.ok(noticeRepo.save(n)); } /** 删除(仅待签收/已撤回可删)。 */ @DeleteMapping("/{id}") public ApiResp delete(@PathVariable Long id) { AuditNotice n = load(id); if ("已签收".equals(n.getStatus())) { throw new ApiException(409, "已签收的审计通知不能删除"); } noticeRepo.deleteById(id); return ApiResp.ok(null); } private AuditNotice load(Long id) { return noticeRepo.findById(id) .orElseThrow(() -> new NotFoundException("审计通知书不存在: " + id)); } /** 生成通知书编号 AN-yyyy-序号。 */ private String nextNoticeNo() { String year = String.valueOf(Year.now().getValue()); long seq = noticeRepo.count() + 1; return String.format("AN-%s-%03d", year, seq); } }