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.LegalConsult; import com.kaidi.oa.domain.LegalKnowledge; import com.kaidi.oa.repository.LegalConsultRepository; import com.kaidi.oa.repository.LegalKnowledgeRepository; 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.ArrayList; import java.util.List; /** * 法务合规·法律咨询与内部服务。业务部门提交法律咨询工单,法务分派承办人、回复法律意见、留档闭环。 * 工单 CRUD + 分派(assign,状态→处理中) + 答复(reply,回填法律意见并置已答复) + 关闭。 * * 含部门法律意见等内部信息,读口已登记进 SENSITIVE_READ_PREFIXES;写口受 default-deny 保护。 */ @RestController @RequestMapping("/api/oa/legal-consults") public class LegalConsultController { private final LegalConsultRepository repo; private final LegalKnowledgeRepository knowledgeRepo; public LegalConsultController(LegalConsultRepository repo, LegalKnowledgeRepository knowledgeRepo) { this.repo = repo; this.knowledgeRepo = knowledgeRepo; } @GetMapping public ApiResp> list(@RequestParam(required = false) String status, @RequestParam(required = false) String assignee) { if (status != null && !status.isBlank()) { return ApiResp.ok(repo.findByStatus(status)); } if (assignee != null && !assignee.isBlank()) { return ApiResp.ok(repo.findByAssignee(assignee)); } return ApiResp.ok(repo.findAll()); } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { return ApiResp.ok(repo.findById(id) .orElseThrow(() -> new NotFoundException("legal consult not found: " + id))); } public record ConsultRequest( String code, String subject, String detail, String requestDept, String requester, String urgency, String assignee, String legalOpinion, String status) { } @PostMapping public ApiResp create(@RequestBody ConsultRequest req) { if (req.subject() == null || req.subject().isBlank()) { throw new ApiException(400, "咨询事项(subject) 不能为空"); } LegalConsult c = new LegalConsult(); c.setCode(req.code() == null || req.code().isBlank() ? "FLZX-" + (repo.count() + 1) : req.code()); c.setSubject(req.subject()); c.setDetail(req.detail()); c.setRequestDept(req.requestDept()); c.setRequester(req.requester()); c.setUrgency(req.urgency() == null || req.urgency().isBlank() ? "普通" : req.urgency()); c.setAssignee(req.assignee()); c.setLegalOpinion(req.legalOpinion()); c.setStatus(req.status() == null || req.status().isBlank() ? (req.assignee() == null || req.assignee().isBlank() ? "待受理" : "处理中") : req.status()); c.setCreatedAt(Instant.now()); return ApiResp.ok(repo.save(c)); } @PatchMapping("/{id}") public ApiResp update(@PathVariable Long id, @RequestBody ConsultRequest req) { LegalConsult c = repo.findById(id) .orElseThrow(() -> new NotFoundException("legal consult not found: " + id)); if (req.subject() != null && !req.subject().isBlank()) c.setSubject(req.subject()); if (req.detail() != null) c.setDetail(req.detail()); if (req.requestDept() != null) c.setRequestDept(req.requestDept()); if (req.requester() != null) c.setRequester(req.requester()); if (req.urgency() != null && !req.urgency().isBlank()) c.setUrgency(req.urgency()); if (req.assignee() != null) c.setAssignee(req.assignee()); if (req.legalOpinion() != null) c.setLegalOpinion(req.legalOpinion()); if (req.status() != null && !req.status().isBlank()) c.setStatus(req.status()); return ApiResp.ok(repo.save(c)); } @DeleteMapping("/{id}") public ApiResp delete(@PathVariable Long id) { if (!repo.existsById(id)) { throw new NotFoundException("legal consult not found: " + id); } repo.deleteById(id); return ApiResp.ok(null); } // ---------- 分派承办法务 ---------- public record AssignRequest(String assignee) { } /** * 分派工单给承办法务:回填 assignee,状态从「待受理」推进到「处理中」。 */ @PostMapping("/{id}/assign") public ApiResp assign(@PathVariable Long id, @RequestBody AssignRequest req) { LegalConsult c = repo.findById(id) .orElseThrow(() -> new NotFoundException("legal consult not found: " + id)); if (req.assignee() == null || req.assignee().isBlank()) { throw new ApiException(400, "承办法务(assignee) 不能为空"); } c.setAssignee(req.assignee()); if ("待受理".equals(c.getStatus()) || c.getStatus() == null || c.getStatus().isBlank()) { c.setStatus("处理中"); } return ApiResp.ok(repo.save(c)); } // ---------- 答复法律意见 ---------- public record ReplyRequest(String legalOpinion) { } /** * 答复法律意见:回填法律意见与答复日,状态置「已答复」。 */ @PostMapping("/{id}/reply") public ApiResp reply(@PathVariable Long id, @RequestBody ReplyRequest req) { LegalConsult c = repo.findById(id) .orElseThrow(() -> new NotFoundException("legal consult not found: " + id)); if (req.legalOpinion() == null || req.legalOpinion().isBlank()) { throw new ApiException(400, "法律意见(legalOpinion) 不能为空"); } c.setLegalOpinion(req.legalOpinion()); c.setStatus("已答复"); c.setRepliedDate(LocalDate.now().toString()); return ApiResp.ok(repo.save(c)); } // ---------- 知识库自动解答(常见问题 FAQ 检索) ---------- public record AutoAnswerResult(boolean matched, List hits) { } public record KnowledgeHit(Long id, String title, String category, String bizScene, String contentPreview) { } /** * 常见问题知识库自动解答:将咨询事项 subject + keyword 在法务知识库中做模糊匹配, * 返回最多 5 条相关知识条目,业务部门可先查阅后再决定是否正式提交工单。 * 无法匹配时返回 matched=false,提示需人工受理。 */ @GetMapping("/auto-answer") public ApiResp autoAnswer(@RequestParam String q) { if (q == null || q.isBlank()) { throw new ApiException(400, "检索词(q) 不能为空"); } String kw = q.trim(); // 标题命中 + 关键词命中,去重合并,最多返回 5 条 List byTitle = knowledgeRepo.findByTitleContaining(kw); List byKw = knowledgeRepo.findByKeywordsContaining(kw); List merged = new ArrayList<>(byTitle); for (LegalKnowledge k : byKw) { if (merged.stream().noneMatch(m -> m.getId().equals(k.getId()))) { merged.add(k); } } List hits = merged.stream().limit(5).map(k -> new KnowledgeHit( k.getId(), k.getTitle(), k.getCategory(), k.getBizScene(), k.getContent() == null ? "" : (k.getContent().length() > 120 ? k.getContent().substring(0, 120) + "…" : k.getContent()) )).toList(); return ApiResp.ok(new AutoAnswerResult(!hits.isEmpty(), hits)); } }