build / build-push (push) Successful in 1m30s
- 已办去重: 同一办件一行 + doneCount 累计次数(FlwTaskRepository 查询去重)
- 发起/撤回/重提留痕事件行(状态码 6/7, 无需 DDL); /oapi/flow/progress steps 含撤回/重提步骤
- 管理端新增启用回路: POST /admin/map/enable/{id}、POST /admin/def/enable/{id}; GET /api/flow/def/source 源码查询
- 修复 findEnabledByOrdId 未过滤 status 的隐性缺陷; 新增 DeptLabelSupport 科室标签支撑
- 测试: FlowEngineIT 撤回留痕/已办去重断言、OapiFlowContractIT 撤回步骤断言(94 tests 全过)
- 本地验签修复: 本地 UserBase 无 context-path, yh.open.services 显式覆盖服务目录枚举兜底的 /userbase basePath, 否则出站签名与服务端剥离不对齐 → GetUserInfo 等全部「验签失败」; 生产经 Caddy 挂 /userbase 时删除 services 覆盖
208 lines
11 KiB
Java
208 lines
11 KiB
Java
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<FlowModels.InstanceItem> 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<FlowModels.TaskItem> 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<FlowModels.TaskItem> 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<String, String> 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<String, String> 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<String, String> 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<String, Object> 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<byte[]> 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());
|
||
}
|
||
}
|