feat(oapi): 办件进度/流程图token出图/托管撤回 + CAS直嵌示意图(两会话产物合入)
build / build-push (push) Successful in 1m29s

This commit is contained in:
chenxing
2026-09-17 01:03:57 +08:00
parent b8441065d0
commit 6afc7d7b92
10 changed files with 978 additions and 1 deletions
@@ -13,6 +13,8 @@ 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;
@@ -37,6 +39,7 @@ public class FlowUserController {
private final FlowInstanceService instanceService;
private final FlowAccessGuard accessGuard;
private final FlowDiagramService diagramService;
private final com.yunhai.flowcenter.service.DiagramImageService imageService;
/** 我提交的办件 */
@GetMapping("/my")
@@ -155,6 +158,23 @@ public class FlowUserController {
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, "未登录");
@@ -21,7 +21,12 @@ import java.util.List;
* - GET /oapi/flow/{flowGuid}?schId= → data={flowGuid,name,version,nodeSummary}
* - POST /oapi/flow/apply → data={instanceId,duplicated}
* - GET /oapi/flow/instance?bizType=&bizId=&schId= → data={status,version,instanceId,nodeSummary,traceJson,…}
* - POST /oapi/flow/instance/{instanceId}/withdraw?schId=&submitterId= → data=null(提交人复核后撤回)
* - GET /oapi/flow/progress?instanceId=|bizType=&bizId=&schId= → data={status,steps:[…]}
* - GET /oapi/flow/diagram/url?instanceId=|flowGuid=&mode=&schId= → data={imgUrl,expiresIn}
* - POST /oapi/flow/callback/reg?schId= → data={secret}
* 刻意不提供审批动作(同意/拒绝/退回/移交/催办):那是"人"的决策,必须带登录身份走 CAS 用户面,
* 应用间签名不得代签——谁办理谁负责,审计链不能断在应用上。
*/
@RestController
@RequestMapping("/oapi/flow")
@@ -54,6 +59,42 @@ public class OapiFlowController {
return flowService.instanceByBiz(schId, bizType, bizId);
}
/**
* 办件进度(结构化轨迹,SDK FlowProgress 契约):instanceId 或 bizType/bizId 定位。
* 消费方渲染时间线(发起申请→各节点办理人行),SDK com.yunhai.platform.openapi.flow.model 同构。
*/
@GetMapping("/progress")
public OapiModels.ProgressVo progress(@RequestParam(value = "instanceId", required = false) Long instanceId,
@RequestParam(value = "bizType", required = false) String bizType,
@RequestParam(value = "bizId", required = false) String bizId,
@RequestParam("schId") Long schId) {
return flowService.progress(schId, instanceId, bizType, bizId);
}
/**
* 流程图换图(token 模式,"CAS 验证码式嵌入"):img 标签带不了签名,本端点在 app-sign 保护下
* 签发短时 token,返回可直接放 {@code <img src>} 的免签路径 {@code /open/flow/diagram/img?token=}。
* 口径:instanceId=办件运行时状态图;flowGuid=最高已发布版定义图。
*/
@GetMapping("/diagram/url")
public OapiModels.DiagramUrlVo diagramUrl(@RequestParam(value = "instanceId", required = false) Long instanceId,
@RequestParam(value = "flowGuid", required = false) String flowGuid,
@RequestParam(value = "mode", required = false) String mode,
@RequestParam("schId") Long schId) {
return flowService.diagramUrl(schId, instanceId, flowGuid, mode);
}
/**
* 撤回(提交人本人;仅尚无任务被办理时)。submitterId 由业务应用按其办件归属传入,
* 本端与办件提交人复核——应用签名不替代"谁在撤"的身份证明。
*/
@PostMapping("/instance/{instanceId}/withdraw")
public void withdraw(@RequestParam("schId") Long schId,
@RequestParam("submitterId") Integer submitterId,
@PathVariable("instanceId") Long instanceId) {
flowService.withdraw(schId, instanceId, submitterId);
}
@PostMapping("/callback/reg")
public OapiModels.CallbackRegResultVo registerCallback(
@RequestParam("schId") Long schId,
@@ -0,0 +1,78 @@
package com.yunhai.flowcenter.controller.open;
import com.yunhai.flowcenter.entity.FlwInstance;
import com.yunhai.flowcenter.model.FlowModels;
import com.yunhai.flowcenter.repository.FlwDefRepository;
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
import com.yunhai.flowcenter.service.DiagramImageService;
import com.yunhai.flowcenter.service.DiagramTokenService;
import com.yunhai.flowcenter.service.FlowDiagramService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 免签图片流端点(/open/flow/**,用户拍板 2026-09-16,"CAS 验证码式"嵌入)。
* <p>
* 故意不落 /api/**(CAS 会话)也不落 /oapi/**(app-sign):&lt;img src&gt; 两者都带不了鉴权头;
* 免签资格由 10 分钟短时 token 承担(DiagramTokenService,token 由 app-sign 的
* GET /oapi/flow/diagram/url 签发)。token 分发点之外,本端点不接受任何业务参数直接定位——
* 免签面收窄到"只认 token",定位与可见性全在签发侧(OapiFlowService.diagramUrl)完成。
*/
@Slf4j
@RestController
@RequestMapping("/open/flow")
@RequiredArgsConstructor
public class OpenDiagramImageController {
private final DiagramTokenService tokenService;
private final DiagramImageService imageService;
private final FlwInstanceRepository instanceRepository;
private final FlwDefRepository defRepository;
private final FlowDiagramService diagramService;
@GetMapping("/diagram/img")
public ResponseEntity<byte[]> diagramImg(@RequestParam("token") String token) {
DiagramTokenService.TokenEntry entry = tokenService.resolve(token);
if (entry == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.contentType(MediaType.TEXT_PLAIN)
.body("流程图已过期,请刷新页面重新获取".getBytes());
}
// 办件口径:节点带运行时状态(token 已收 schId,出图前再二次比对防漂移)
if (entry.getInstanceId() != null) {
FlwInstance instance = instanceRepository.findById(entry.getInstanceId()).orElse(null);
if (instance == null || !entry.getSchId().equals(instance.getSchId())) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.contentType(MediaType.TEXT_PLAIN)
.body("办件不存在".getBytes());
}
return png(diagramService.byInstance(instance, entry.getMode()));
}
// 定义口径(flowGuid 预览)
return defRepository.findById(entry.getDefId())
.map(def -> entry.getSchId().equals(def.getSchId())
? png(diagramService.build(def, null, entry.getMode()))
: notFound("流程不存在"))
.orElseGet(() -> notFound("流程不存在"));
}
private ResponseEntity<byte[]> png(FlowModels.DiagramVo vo) {
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_PNG)
.cacheControl(org.springframework.http.CacheControl.noStore())
.body(imageService.renderPng(vo));
}
private ResponseEntity<byte[]> notFound(String msg) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.contentType(MediaType.TEXT_PLAIN)
.body(msg.getBytes());
}
}