feat(embed): 新增办件进度嵌入页签发端点与免签数据端点
build / build-push (push) Successful in 10m1s

This commit is contained in:
chenxing
2026-10-03 16:42:21 +08:00
parent 1d2f4a0cb2
commit d28b5d1161
6 changed files with 251 additions and 0 deletions
@@ -25,6 +25,7 @@ import java.util.List;
* - 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}
* - GET /oapi/flow/embed/url?instanceId=|bizType=&bizId=&mode=&schId= → data={pageUrl,expiresIn}
* - POST /oapi/flow/callback/reg?schId= → data={secret}
* 应用内审批(2026-09 第三步,原"刻意不提供审批动作"立场由该方案修订:登录身份由调用方
* 应用按其 CAS 会话保证并以 operatorId 传入,本端按任务办理人逐字复核,审计链不断):
@@ -89,6 +90,20 @@ public class OapiFlowController {
return flowService.diagramUrl(schId, instanceId, flowGuid, mode);
}
/**
* 办件进度嵌入页换址(iframe 场景,token 模式与换图同构):pageUrl 为 flow-center 前端
* 绝对地址,子应用直接放 {@code <iframe src>}。用户级可见性(本人/办理人/管理员)由
* 子应用后端在调本端点前完成——oapi 层只认 schId,应用签名不替代"谁能看"。
*/
@GetMapping("/embed/url")
public OapiModels.EmbedUrlVo embedUrl(@RequestParam(value = "instanceId", required = false) Long instanceId,
@RequestParam(value = "bizType", required = false) String bizType,
@RequestParam(value = "bizId", required = false) String bizId,
@RequestParam(value = "mode", required = false) String mode,
@RequestParam("schId") Long schId) {
return flowService.embedUrl(schId, instanceId, bizType, bizId, mode);
}
/**
* 撤回(提交人本人;仅尚无任务被办理时)。submitterId 由业务应用按其办件归属传入,
* 本端与办件提交人复核——应用签名不替代"谁在撤"的身份证明。
@@ -0,0 +1,64 @@
package com.yunhai.flowcenter.controller.open;
import com.yunhai.flowcenter.entity.FlwInstance;
import com.yunhai.flowcenter.model.FlowModels;
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
import com.yunhai.flowcenter.service.DiagramTokenService;
import com.yunhai.flowcenter.service.FlowQueryService;
import lombok.RequiredArgsConstructor;
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/embed/**,与图片流 /open/flow/diagram/img 同纪律)。
* <p>
* {@code <iframe src>} 进不了 /api(CAS)也进不了 /oapi(app-sign):免签资格由 10 分钟
* 短时 token 承担(与换图共用 DiagramTokenService 同一 token 池,token 由 app-sign 的
* GET /oapi/flow/embed/url 签发)。token 分发点之外,本端点不接受任何业务参数直接定位——
* 免签面收窄到"只认 token",定位与可见性全在签发侧(OapiFlowService.embedUrl)完成。
* 出参复用管理面办件详情 InstanceDetail 整体(viewerId=null,本人待办字段自然为空),
* 不新造 VO——嵌入页信息量与管理面一致,单点维护。
*/
@RestController
@RequestMapping("/open/flow/embed")
@RequiredArgsConstructor
public class OpenEmbedController {
private final DiagramTokenService tokenService;
private final FlowQueryService flowQueryService;
private final FlwInstanceRepository instanceRepository;
@GetMapping("/data")
public ResponseEntity<?> embedData(@RequestParam(value = "token", required = false) String token) {
DiagramTokenService.TokenEntry entry = tokenService.resolve(token);
if (entry == null) {
return notFound("链接已过期,请重新打开");
}
// 办件口径收窄:defId 口径的换图 token(flowGuid 预览)没有办件,不在此消费
if (entry.getInstanceId() == null) {
return notFound("链接已过期,请重新打开");
}
// token 已收 schId,出数据前再二次比对防漂移(同图片流口径,token 无法跨校探数据)
FlwInstance instance = instanceRepository.findById(entry.getInstanceId()).orElse(null);
if (instance == null || !entry.getSchId().equals(instance.getSchId())) {
return notFound("办件不存在");
}
FlowModels.InstanceDetail detail =
flowQueryService.detail(entry.getSchId(), entry.getInstanceId(), null);
return ResponseEntity.ok()
.cacheControl(org.springframework.http.CacheControl.noStore())
.body(detail);
}
private ResponseEntity<String> notFound(String msg) {
// 显式 UTF-8:text/plain 缺 charset 时浏览器/客户端按 ISO-8859-1 解码中文必乱码
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.contentType(new MediaType("text", "plain", java.nio.charset.StandardCharsets.UTF_8))
.body(msg);
}
}
@@ -146,6 +146,15 @@ public final class OapiModels {
private long expiresIn;
}
/** 办件进度嵌入页换址响应(pageUrl 可直接放 <iframe src>,SDK FlowEmbedUrl 同构) */
@Data
public static class EmbedUrlVo {
/** 嵌入页绝对地址(flow-center 前端基址 + hash 路由 + 短时 token) */
private String pageUrl;
/** 相对 token 有效期(秒) */
private long expiresIn;
}
/** 待办任务行(tasks/todo 元素):审批列表页数据源,带办件定位与快照 */
@Data
public static class TodoTaskVo {
@@ -61,6 +61,14 @@ public class OapiFlowService {
@Value("${flow-center.submitter-school-check.enabled:true}")
private boolean submitterCheckEnabled;
/**
* 办件进度嵌入页前端基址(flow-center 自身的对外前端地址,本方案新增的第一个前端地址配置:
* 此前 UserMsg jumpPath 均为裸相对路径由接收方解析)。未配置即调签发 → fail-fast 业务异常,
* 与 callback-url 同纪律——宁可不发也不发一个打不开的地址。
*/
@Value("${flow-center.embed.front-url:}")
private String embedFrontUrl;
private final FlwDefRepository defRepository;
private final FlwInstanceRepository instanceRepository;
private final FlwTaskRepository taskRepository;
@@ -324,6 +332,43 @@ public class OapiFlowService {
return vo;
}
/**
* 办件进度嵌入页换址(token 模式,与换图同构):同一 token 两处消费——嵌入页数据
* /open/flow/embed/data 与流程图 /open/flow/diagram/img 均免签认领该 token。定位二选一:
* instanceId(与办件直接复核)/ bizType+bizId(对账口径,办件不存在与 progress 同文案)。
* 用户级可见性(本人/办理人/管理员)由子应用后端在调本端点前完成——oapi 层只认 schId。
*/
public OapiModels.EmbedUrlVo embedUrl(Long schId, Long instanceId, String bizType, String bizId, String mode) {
// 配置缺失优先于办件定位:fail-fast 纪律要的是"无论如何都不发打不开的地址"
if (embedFrontUrl == null || embedFrontUrl.trim().isEmpty()) {
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "嵌入页地址未配置:flow-center.embed.front-url");
}
Long resolvedInstanceId;
if (instanceId != null) {
resolvedInstanceId = requireInstance(schId, instanceId).getId();
} else {
if (bizType == null || bizId == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "缺少必要参数:instanceId 或 bizType/bizId");
}
OapiModels.InstanceVo vo = instanceByBiz(schId, bizType, bizId);
if (vo == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "办件不存在或不可见");
}
resolvedInstanceId = vo.getInstanceId();
}
// mode 与 DiagramTokenService 内部缺省同值("auto"),URL 里显式带上仅为前端展示冗余
String normMode = mode == null || mode.trim().isEmpty() ? "auto" : mode.trim();
String token = tokenService.issue(schId, resolvedInstanceId, null, normMode);
String base = embedFrontUrl.trim();
if (!base.endsWith("/")) {
base = base + "/";
}
OapiModels.EmbedUrlVo out = new OapiModels.EmbedUrlVo();
out.setPageUrl(base + "#/embed/instance?token=" + token + "&mode=" + normMode);
out.setExpiresIn(DiagramTokenService.TTL_MS / 1000);
return out;
}
/** 进度/换图共用的办件定位(schId 收敛在次) */
private FlwInstance requireInstance(Long schId, Long instanceId) {
FlwInstance instance = instanceRepository.findById(instanceId)