package com.yunhai.flowcenter.controller.api; import com.yunhai.flowcenter.entity.FlwInstance; import com.yunhai.flowcenter.entity.FlwTask; import com.yunhai.flowcenter.model.FlowModels; import com.yunhai.flowcenter.service.FlowAccessGuard; import com.yunhai.flowcenter.service.FlowDiagramService; import com.yunhai.flowcenter.service.FlowInstanceService; import com.yunhai.flowcenter.service.FlowQueryService; import com.yunhai.flowcenter.service.FlowTaskService; import com.yunhai.platform.apistd.BizException; import com.yunhai.platform.utils.exception.ErrorCode; import com.yunhai.platform.utils.model.BasePageResult; import com.yunhai.platform.utils.model.BoaUser; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; 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.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Map; /** * 用户面(/api/flow/**,CAS 会话):我的办件/待办/已办/办件详情/办理/撤回/催办。 * 一切审批入口的收敛点(概览 §8.5 不变式 2):业务系统内嵌页、消息跳转最终都调这套 API。 */ @RestController @RequestMapping("/api/flow") @RequiredArgsConstructor public class FlowUserController { private final FlowQueryService queryService; private final FlowTaskService taskService; private final FlowInstanceService instanceService; private final FlowAccessGuard accessGuard; private final FlowDiagramService diagramService; private final com.yunhai.flowcenter.service.DiagramImageService imageService; /** 我提交的办件 */ @GetMapping("/my") public BasePageResult my(BoaUser user, @RequestParam(value = "status", required = false) Integer status, @RequestParam(value = "page", required = false) Integer page, @RequestParam(value = "size", required = false) Integer size) { return queryService.pageMy(requireSchId(user), user.getId(), status, page, size); } /** 我的待办(主路径,走 assignee 索引) */ @GetMapping("/todo") public BasePageResult todo(BoaUser user, @RequestParam(value = "page", required = false) Integer page, @RequestParam(value = "size", required = false) Integer size) { return queryService.pageTodo(requireSchId(user), user.getId(), page, size); } /** 我的已办 */ @GetMapping("/done") public BasePageResult done(BoaUser user, @RequestParam(value = "page", required = false) Integer page, @RequestParam(value = "size", required = false) Integer size) { return queryService.pageDone(requireSchId(user), user.getId(), page, size); } /** 办件详情(提交人/办理人/本校管理员可见) */ @GetMapping("/instance/{id}") public FlowModels.InstanceDetail detail(BoaUser user, @PathVariable("id") Long id) { FlwInstance instance = instanceService.requireInstance(requireSchId(user), id); accessGuard.requireInstanceVisible(instance, user); return queryService.detail(requireSchId(user), id, user.getId()); } /** 任务定位办件详情(办理页 /task/{taskId} 数据源;可见性走办件详情同一守卫) */ @GetMapping("/task/{id}/detail") public FlowModels.InstanceDetail taskDetail(BoaUser user, @PathVariable("id") Long id) { Long schId = requireSchId(user); FlwTask task = taskService.requireTask(schId, id); FlwInstance instance = instanceService.requireInstance(schId, task.getInstanceId()); accessGuard.requireInstanceVisible(instance, user); return queryService.detail(schId, instance.getId(), user.getId()); } /** 同意(opinion 可选;多出口节点必须带 edgeName=所选出口名,单出口可省)——一切审批入口的收敛点 */ @PostMapping("/task/{id}/approve") public void approve(BoaUser user, @PathVariable("id") Long id, @RequestBody(required = false) Map body) { String opinion = body == null ? null : body.get("opinion"); String edgeName = body == null ? null : body.get("edgeName"); taskService.handle(requireSchId(user), id, true, opinion, user.getId(), edgeName); } /** 拒绝(opinion 必填) */ @PostMapping("/task/{id}/reject") public void reject(BoaUser user, @PathVariable("id") Long id, @RequestBody(required = false) Map body) { String opinion = body == null ? null : body.get("opinion"); taskService.handle(requireSchId(user), id, false, opinion, user.getId()); } /** 退回(targetNodeId 可选=缺省回最近已办节点;opinion 可选) */ @PostMapping("/task/{id}/returnBack") public void returnBack(BoaUser user, @PathVariable("id") Long id, @RequestBody(required = false) Map body) { String targetNodeId = body == null ? null : body.get("targetNodeId"); String opinion = body == null ? null : body.get("opinion"); taskService.returnBack(requireSchId(user), id, targetNodeId, opinion, user.getId()); } /** 移交(toUserId 必填;reason 可选) */ @PostMapping("/task/{id}/transfer") public void transfer(BoaUser user, @PathVariable("id") Long id, @RequestBody Map body) { Integer toUserId = body.get("toUserId") == null ? null : Integer.valueOf(String.valueOf(body.get("toUserId"))); Object reason = body.get("reason"); taskService.transfer(requireSchId(user), id, toUserId, reason == null ? null : String.valueOf(reason), user.getId()); } /** 撤回(提交人;仅尚无任务被办理时) */ @PostMapping("/instance/{id}/withdraw") public void withdraw(BoaUser user, @PathVariable("id") Long id) { FlwInstance instance = instanceService.requireInstance(requireSchId(user), id); if (!instance.getSubmitterId().equals(user.getId())) { throw new BizException(ErrorCode.FORBIDDEN, "仅提交人本人可撤回"); } instanceService.withdraw(requireSchId(user), id); } /** 催办(提交人/管理员;对全部待办任务各催一次,msgNo 幂等) */ @PostMapping("/instance/{id}/urge") public void urge(BoaUser user, @PathVariable("id") Long id) { instanceService.urgeInstance(requireSchId(user), id, user.getId()); } /** * 流程示意图(方案 §3.1):flowGuid=最高已发布版(登录+本校); * instanceId=办件 def 快照版,可见性与办件详情同口径(requireInstanceVisible)。 */ @GetMapping("/diagram") public FlowModels.DiagramVo diagram(BoaUser user, @RequestParam(value = "flowGuid", required = false) String flowGuid, @RequestParam(value = "instanceId", required = false) Long instanceId, @RequestParam(value = "mode", required = false) String mode) { Long schId = requireSchId(user); if (flowGuid != null && !flowGuid.trim().isEmpty()) { return diagramService.byGuid(schId, flowGuid.trim(), mode); } if (instanceId != null) { FlwInstance instance = instanceService.requireInstance(schId, instanceId); accessGuard.requireInstanceVisible(instance, user); return diagramService.byInstance(instance, mode); } throw new BizException(ErrorCode.PARAM_ERROR, "缺少必要参数:flowGuid 或 instanceId"); } /** * 流程源码(2026-09-18,图示页"源码"抽屉/复制用):可见性与 {@link #diagram} 同口径—— * flowGuid=最高已发布版;instanceId=该办件实际运行的 def 快照(先过 requireInstanceVisible)。 */ @GetMapping("/def/source") public FlowModels.DefSourceVo defSource(BoaUser user, @RequestParam(value = "flowGuid", required = false) String flowGuid, @RequestParam(value = "instanceId", required = false) Long instanceId) { Long schId = requireSchId(user); if (flowGuid != null && !flowGuid.trim().isEmpty()) { return diagramService.sourceByGuid(schId, flowGuid.trim()); } if (instanceId != null) { FlwInstance instance = instanceService.requireInstance(schId, instanceId); accessGuard.requireInstanceVisible(instance, user); return diagramService.sourceByInstance(instance); } throw new BizException(ErrorCode.PARAM_ERROR, "缺少必要参数:flowGuid 或 instanceId"); } /** * 流程图图片流(CAS 会话版"CAS 验证码式嵌入",2026-09-16):流程中心自有页面 <img src> * 直嵌(办件详情页内嵌运行时状态图),免跳 /diagram 全屏页。可见性与 /diagram 同口径。 * 外部系统(img 带不了会话)走 /oapi/flow/diagram/url 换 token + /open/flow/diagram/img。 */ @GetMapping("/diagram.png") public ResponseEntity diagramPng(BoaUser user, @RequestParam(value = "flowGuid", required = false) String flowGuid, @RequestParam(value = "instanceId", required = false) Long instanceId, @RequestParam(value = "mode", required = false) String mode) { FlowModels.DiagramVo vo = diagram(user, flowGuid, instanceId, mode); return ResponseEntity.ok() .contentType(MediaType.IMAGE_PNG) .cacheControl(org.springframework.http.CacheControl.noStore()) .body(imageService.renderPng(vo)); } private Long requireSchId(BoaUser user) { if (user == null) { throw new BizException(ErrorCode.FORBIDDEN, "未登录"); } if (user.getCurrentRole() == null || user.getCurrentRole().getSchId() == null) { throw new BizException(ErrorCode.PARAM_ERROR, "当前身份无学校归属(区域账号无流程业务)"); } return Long.valueOf(user.getCurrentRole().getSchId()); } }