Files
flow-center-service/src/main/java/com/yunhai/flowcenter/controller/api/FlowUserController.java
T
chenxing 9cc0b06c29
build / build-push (push) Successful in 1m30s
feat(flow): 工作台与轨迹优化 + 管理端启用回路 + 本地验签配置修复
- 已办去重: 同一办件一行 + 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 覆盖
2026-09-18 16:56:01 +08:00

208 lines
11 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.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):流程中心自有页面 &lt;img src&gt;
* 直嵌(办件详情页内嵌运行时状态图),免跳 /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());
}
}