feat(flow): 接入新流程体系——SDK withdraw/progress/diagramUrl:撤回后端中转+同源流程图端点+Trace 结构化 steps(治前端直调外溢)
build / build-push (push) Failing after 33s

This commit is contained in:
chenxing
2026-09-17 01:10:36 +08:00
parent b3c5326163
commit 08e9531b5f
6 changed files with 299 additions and 0 deletions
@@ -66,6 +66,21 @@ public class NoticeFlowController {
return noticeFlowService.buildTrace(serid, user);
}
/**
* 撤回审核(2026-09-17 治理:原先前端直调 flow-center 用户面,现后端经 SDK 托管件中转;
* 仅创建人本人 + 审批中,撤回后状态由 flow.withdrawn 回调推进)。
*/
@PostMapping("Withdraw")
public void withdraw(@RequestParam("serid") Integer serid, BoaUser user) {
noticeFlowService.withdrawNotice(serid, user);
}
/** 流程示意图(PNG 流,同源直嵌 img;可见性与 Trace 同口径,token 不落前端) */
@GetMapping(value = "Diagram", produces = org.springframework.http.MediaType.IMAGE_PNG_VALUE)
public byte[] diagram(@RequestParam("serid") Integer serid, BoaUser user) {
return noticeFlowService.diagramImage(serid, user);
}
private Integer currentSchId(BoaUser user) {
if (user == null || user.getCurrentRole() == null) {
return null;
@@ -7,6 +7,8 @@ import com.yunhai.platform.openapi.flow.model.FlowApplyResult;
import com.yunhai.platform.openapi.flow.model.FlowInstance;
import com.yunhai.platform.openapi.flow.model.FlowCallbackRegBody;
import com.yunhai.platform.openapi.flow.model.FlowCallbackRegResult;
import com.yunhai.platform.openapi.flow.model.FlowDiagramUrl;
import com.yunhai.platform.openapi.flow.model.FlowProgress;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -61,4 +63,18 @@ public final class FlowModels {
@JsonIgnoreProperties(ignoreUnknown = true)
public static class CallbackRegResultVo extends FlowCallbackRegResult {
}
/** 办件进度(GET /oapi/flow/progress;steps[0] 固定"发起申请",2026-09-17 接入) */
@Data
@EqualsAndHashCode(callSuper = true)
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ProgressVo extends FlowProgress {
}
/** 流程示意图换图地址(GET /oapi/flow/diagram/url;imgUrl 为 flow-center 免签相对路径) */
@Data
@EqualsAndHashCode(callSuper = true)
@JsonIgnoreProperties(ignoreUnknown = true)
public static class DiagramUrlVo extends FlowDiagramUrl {
}
}
@@ -116,6 +116,87 @@ public class FlowCenterClient implements FlowClient {
FlowModels.CallbackRegResultVo.class);
}
@Override
public void withdraw(Integer schId, Long instanceId, Integer submitterId) {
requireEnabled();
if (directoryResolved) {
com.yunhai.platform.openapi.flow.YschFlowOpenApi.withdraw(schId, instanceId, submitterId);
return;
}
Map<String, String> query = new HashMap<>();
query.put("schId", String.valueOf(schId));
query.put("submitterId", String.valueOf(submitterId));
// 无请求体:对端 POST 无 @RequestBody,query 传参即可(body 传 null 即零长度请求体)
openCall.post(target(), "/oapi/flow/instance/" + instanceId + "/withdraw", query, null, Void.class);
}
@Override
public FlowModels.ProgressVo progress(Integer schId, Long instanceId) {
requireEnabled();
if (directoryResolved) {
return com.yunhai.platform.openapi.flow.YschFlowOpenApi.progress(
instanceId, BIZ_TYPE, null, schId, FlowModels.ProgressVo.class);
}
Map<String, String> query = new HashMap<>();
query.put("instanceId", String.valueOf(instanceId));
query.put("schId", String.valueOf(schId));
return openCall.get(target(), "/oapi/flow/progress", query, FlowModels.ProgressVo.class);
}
/** 免签出图端点(flow-center OpenDiagramImageController,相对其 context-path) */
private static final String DIAGRAM_IMG_PATH = "/open/flow/diagram/img";
@Override
public byte[] diagramImage(Integer schId, Long instanceId) {
requireEnabled();
String imgUrl;
if (directoryResolved) {
FlowModels.DiagramUrlVo dto = com.yunhai.platform.openapi.flow.YschFlowOpenApi.diagramUrl(
instanceId, null, null, schId, FlowModels.DiagramUrlVo.class);
imgUrl = dto == null ? null : dto.getImgUrl();
} else {
Map<String, String> query = new HashMap<>();
query.put("instanceId", String.valueOf(instanceId));
query.put("schId", String.valueOf(schId));
FlowModels.DiagramUrlVo dto =
openCall.get(target(), "/oapi/flow/diagram/url", query, FlowModels.DiagramUrlVo.class);
imgUrl = dto == null ? null : dto.getImgUrl();
}
String token = tokenOf(imgUrl);
if (token == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "流程中心未返回有效的示意图地址");
}
Map<String, String> imgQuery = new HashMap<>();
imgQuery.put("token", token);
byte[] bytes = directoryResolved
? com.yunhai.platform.openapi.flow.YschFlowOpenApi.raw().download(DIAGRAM_IMG_PATH, imgQuery)
: openCall.download(target(), DIAGRAM_IMG_PATH, imgQuery);
if (bytes == null || bytes.length == 0) {
throw new BizException(ErrorCode.PARAM_ERROR, "流程示意图为空(流程可能未发布节点)");
}
return bytes;
}
/**
* 从换图地址中取 token(形如 {@code /open/flow/diagram/img?token=xxx})。
* 包可见:契约测试冻结"token 只在后端流转、前端拿不到"的取用形态。
*/
static String tokenOf(String imgUrl) {
if (imgUrl == null || imgUrl.isEmpty()) {
return null;
}
int q = imgUrl.indexOf("?token=");
if (q < 0) {
return null;
}
String token = imgUrl.substring(q + "?token=".length());
int amp = token.indexOf('&');
if (amp >= 0) {
token = token.substring(0, amp);
}
return token.isEmpty() ? null : token;
}
private TargetSpec target() {
// base-path 空:本地/容器裸地址直连(两级模型 §2.2 对齐不变式的现行约定)
return TargetSpec.of(properties.getBaseUrl(), "");
@@ -21,4 +21,16 @@ public interface FlowClient {
/** 回调注册(下发 secret;重复注册会轮换 secret——仅在 reg 缺失时调用) */
FlowModels.CallbackRegResultVo registerCallback(Integer schId, FlowModels.CallbackRegBody body);
/**
* 撤回(托管件,2026-09-17 接入):对端按 submitterId 与办件提交人逐字复核,
* 应用签名只证明"哪个应用在调"不证明"谁在撤"——防业务应用越权代撤他人办件。
*/
void withdraw(Integer schId, Long instanceId, Integer submitterId);
/** 办件进度(结构化 steps,instanceId 定位;oapi 只校验 schId,调用方先做可见性守卫) */
FlowModels.ProgressVo progress(Integer schId, Long instanceId);
/** 流程示意图字节(token 换图两步都在本方法内,浏览器只拿同源 PNG,token 不落前端) */
byte[] diagramImage(Integer schId, Long instanceId);
}
@@ -235,6 +235,15 @@ public class NoticeFlowService {
result.put("finishTime", instance.getFinishTime());
result.put("trace", parseJson(instance.getTraceJson()));
}
if (notice.getFlowInstanceId() != null) {
// 结构化步骤(steps[0] 固定"发起申请",同节点多任务每人一行)——
// 2026-09-17 接入 SDK progress,详情页时间线数据源;失败降级不阻断
FlowModels.ProgressVo progress =
flowClient.progress(notice.getSchId(), notice.getFlowInstanceId());
if (progress != null) {
result.put("steps", progress.getSteps());
}
}
} catch (Exception e) {
// 进度查询失败不阻断详情:展示本地镜像态即可(流程中心停机时详情仍可用)
log.warn("[notice-flow] 进度查询失败:notice={} {}", noticeId, e.toString());
@@ -243,6 +252,48 @@ public class NoticeFlowService {
return result;
}
/**
* 撤回审核(2026-09-17 治理:原先前端直调 flow-center 用户面——跨应用 CAS 票据 + CORS
* 双重外溢,且"谁能撤"落在前端)。现经 SDK 托管件后端中转:①本人(publisherAccount)
* = 权威授权;②审批中(10) 且已有实例;③对端再按 submitterId 复核一次。
* 撤回后镜像状态由 flow.withdrawn 回调推进(40),本方法不改本地状态——单一写入口。
*/
public void withdrawNotice(Integer noticeId, BoaUser user) {
Notice notice = requireNotice(noticeId);
if (user == null || !StringUtils.hasText(notice.getPublisherAccount())
|| !notice.getPublisherAccount().equals(user.getAccount())) {
throw new BizException(ErrorCode.FORBIDDEN, "仅通知创建人可撤回审核");
}
Integer audit = notice.getAuditStatus();
if (audit == null || audit != FlowAuditStatusEnum.APPROVING.getCode()) {
throw new BizException(ErrorCode.PARAM_ERROR, "仅审批中的通知可撤回");
}
if (notice.getFlowInstanceId() == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "该通知尚未进入流程,无需撤回");
}
flowClient.withdraw(notice.getSchId(), notice.getFlowInstanceId(), user.getId());
log.info("[notice-flow] 撤回审核:notice={} instance={} user={}",
noticeId, notice.getFlowInstanceId(), user.getId());
}
/**
* 流程示意图(PNG 流,同源直嵌 {@code <img>}):可见性与 Trace 同口径(未发布仅三类人)。
* token 与字节都在服务端流转,前端不再需要知道 flow-center 地址。
*/
public byte[] diagramImage(Integer noticeId, BoaUser user) {
Notice notice = requireNotice(noticeId);
if (notice.getStatus() == null || notice.getStatus() != 1) {
noticeFlowGuard.requireUnpublishedVisible(notice, user);
}
if (!StringUtils.hasText(notice.getFlowGuid()) || notice.getAuditStatus() == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "该通知未接入审核流程");
}
if (notice.getFlowInstanceId() == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "该通知尚未进入流程,无流程图");
}
return flowClient.diagramImage(notice.getSchId(), notice.getFlowInstanceId());
}
/** 编辑页"审核流程"选择器数据源(未启用返回空列表由前端隐藏) */
public List<FlowModels.FlowSummaryVo> options(Integer schId) {
if (!properties.isEnabled()) {