feat(apply): 用户面撤回/进度/流程图后端中转 + 大厅HallController + flowEnabled 三态(SDK withdraw/progress/diagramUrl)
build / build-push (push) Successful in 1m11s
build / build-push (push) Successful in 1m11s
This commit is contained in:
@@ -186,6 +186,112 @@ public class FlowCenterClient implements FlowClient {
|
||||
return String.valueOf(secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回(托管件 POST /oapi/flow/instance/{id}/withdraw?schId=&submitterId=)。
|
||||
* 对端按 submitterId 与办件提交人复核:应用签名只证明"哪个应用在调",不证明"谁在撤"。
|
||||
*/
|
||||
@Override
|
||||
public void withdraw(Long schId, Long instanceId, Integer submitterId) {
|
||||
requireEnabled();
|
||||
Map<String, String> query = new HashMap<>();
|
||||
query.put("schId", String.valueOf(schId));
|
||||
query.put("submitterId", String.valueOf(submitterId));
|
||||
if (directoryResolved) {
|
||||
com.yunhai.platform.openapi.flow.YschFlowOpenApi.withdraw(schId, instanceId, submitterId);
|
||||
return;
|
||||
}
|
||||
openCall.post(flowSpec, "/oapi/flow/instance/" + instanceId + "/withdraw", query, null, Void.class);
|
||||
}
|
||||
|
||||
/** 办件进度(门面 progress / legacy GET;instanceId 优先,否则 bizType+bizId) */
|
||||
@Override
|
||||
public SvcModels.FlowProgressVo progress(Long schId, Long instanceId, String bizType, String bizId) {
|
||||
requireEnabled();
|
||||
if (directoryResolved) {
|
||||
return com.yunhai.platform.openapi.flow.YschFlowOpenApi.progress(
|
||||
instanceId, bizType, bizId, schId, SvcModels.FlowProgressVo.class);
|
||||
}
|
||||
Map<String, String> query = new HashMap<>();
|
||||
if (instanceId != null) {
|
||||
query.put("instanceId", String.valueOf(instanceId));
|
||||
} else {
|
||||
query.put("bizType", bizType);
|
||||
query.put("bizId", bizId);
|
||||
}
|
||||
query.put("schId", String.valueOf(schId));
|
||||
return openCall.get(flowSpec, "/oapi/flow/progress", query, SvcModels.FlowProgressVo.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 流程示意图字节:先换免签 token(/oapi/flow/diagram/url,走应用签名),再取图
|
||||
* (/open/flow/diagram/img,免签只认 token)。两步都在本方法内,token 不出后端。
|
||||
* <p>
|
||||
* 取图走 {@code raw()}(预绑 flowcenter 的泛化视图)/ ad-hoc spec 的 download:目标基址解析
|
||||
* 与签名由 SDK 统一承载,本类不拼 flow-center 地址——Caddy 没有 flow-center 的通用路由
|
||||
* (Projman 只生成 {ctx}/api/* 与前端静态段),故浏览器侧不可达,必须由后端中转运图。
|
||||
*/
|
||||
@Override
|
||||
public byte[] diagramImage(Long schId, Long instanceId, String flowGuid, String mode) {
|
||||
requireEnabled();
|
||||
String imgUrl;
|
||||
if (directoryResolved) {
|
||||
com.yunhai.platform.openapi.flow.model.FlowDiagramUrl dto =
|
||||
com.yunhai.platform.openapi.flow.YschFlowOpenApi.diagramUrl(
|
||||
instanceId, flowGuid, mode, schId, SvcModels.FlowDiagramVo.class);
|
||||
imgUrl = dto == null ? null : dto.getImgUrl();
|
||||
} else {
|
||||
Map<String, String> query = new HashMap<>();
|
||||
if (instanceId != null) {
|
||||
query.put("instanceId", String.valueOf(instanceId));
|
||||
}
|
||||
if (flowGuid != null && !flowGuid.isEmpty()) {
|
||||
query.put("flowGuid", flowGuid);
|
||||
}
|
||||
if (mode != null && !mode.isEmpty()) {
|
||||
query.put("mode", mode);
|
||||
}
|
||||
query.put("schId", String.valueOf(schId));
|
||||
SvcModels.FlowDiagramVo dto =
|
||||
openCall.get(flowSpec, "/oapi/flow/diagram/url", query, SvcModels.FlowDiagramVo.class);
|
||||
imgUrl = dto == null ? null : dto.getImgUrl();
|
||||
}
|
||||
String token = tokenOf(imgUrl);
|
||||
if (token == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "流程中心未返回有效的示意图地址");
|
||||
}
|
||||
Map<String, String> imgQuery = Collections.singletonMap("token", token);
|
||||
byte[] bytes = directoryResolved
|
||||
? com.yunhai.platform.openapi.flow.YschFlowOpenApi.raw().download(DIAGRAM_IMG_PATH, imgQuery)
|
||||
: openCall.download(flowSpec, DIAGRAM_IMG_PATH, imgQuery);
|
||||
if (bytes == null || bytes.length == 0) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "流程示意图为空(流程可能未发布节点)");
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/** 免签出图端点(flow-center OpenDiagramImageController,相对其 context-path) */
|
||||
private static final String DIAGRAM_IMG_PATH = "/open/flow/diagram/img";
|
||||
|
||||
/**
|
||||
* 从换图地址中取 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 回调 URL → 接收方 context-path:取 URL path 段去掉尾部 "/callback/flow"
|
||||
* (如 /svc-hall/callback/flow → /svc-hall;裸地址 /callback/flow → 空串)。
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.yunhai.svchall.entity.ServiceForm;
|
||||
import com.yunhai.svchall.entity.ServiceItem;
|
||||
import com.yunhai.svchall.enums.ApplyFlowStatusEnum;
|
||||
import com.yunhai.svchall.enums.ServiceItemStatusEnum;
|
||||
import com.yunhai.svchall.model.svc.SvcModels;
|
||||
import com.yunhai.svchall.model.svc.SvcModels.ApplyPageQuery;
|
||||
import com.yunhai.svchall.model.svc.SvcModels.ApplySubmitRequest;
|
||||
import com.yunhai.svchall.model.svc.SvcModels.ApplyVo;
|
||||
@@ -126,7 +127,7 @@ public class ServiceApplyService {
|
||||
apply = applyRepository.save(apply);
|
||||
|
||||
flowSubmitAfterCommit(apply.getId());
|
||||
return ApplyVo.of(apply);
|
||||
return stampFlowEnabled(ApplyVo.of(apply));
|
||||
}
|
||||
|
||||
/** 重试:PENDING 行复用同一 bizId 再次代发起(幂等);仅提交人本人可重试自己的办件(同步路径,无长事务) */
|
||||
@@ -140,7 +141,7 @@ public class ServiceApplyService {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "该办件不在待提交状态,无需重试");
|
||||
}
|
||||
doFlowApply(applyId, false);
|
||||
return applyRepository.findById(applyId).map(ApplyVo::of).orElse(ApplyVo.of(apply));
|
||||
return stampFlowEnabled(applyRepository.findById(applyId).map(ApplyVo::of).orElse(ApplyVo.of(apply)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,7 +165,84 @@ public class ServiceApplyService {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "该办件未绑定流程,不可重提");
|
||||
}
|
||||
doFlowApply(applyId, true);
|
||||
return applyRepository.findById(applyId).map(ApplyVo::of).orElse(ApplyVo.of(apply));
|
||||
return stampFlowEnabled(applyRepository.findById(applyId).map(ApplyVo::of).orElse(ApplyVo.of(apply)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回(用户面;2026-09-16 治理:原先前端直调 flow-center {@code /api/flow/instance/{id}/withdraw}
|
||||
* ——跨应用 CAS 票据 + CORS 双重外溢,且"谁能撤"落在前端)。现由本服务中转:
|
||||
* ①本应用内定位办件(同校 + 提交人本人 = 权威授权,也是列表自见规则);②状态须审批中(10) 且
|
||||
* 已有流程实例;③经 FlowClient→SDK 门面调 flow-center 托管件(对端再按 submitterId 复核一次)。
|
||||
* 撤回后的镜像状态由 flow.withdrawn 回调推进(40),本方法不改本地状态——单一写入口,
|
||||
* 避免"本地先改 + 回调后到"的版本比较歧义。
|
||||
*/
|
||||
public void withdraw(BoaUser user, Integer applyId) {
|
||||
ServiceApply apply = requireOwnApply(user, applyId, "仅可撤回自己的办件");
|
||||
if (apply.getFlowStatus() == null
|
||||
|| apply.getFlowStatus() != ApplyFlowStatusEnum.APPROVING.getCode()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "仅审批中的办件可撤回");
|
||||
}
|
||||
if (apply.getFlowInstanceId() == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "该办件尚未进入流程,无需撤回");
|
||||
}
|
||||
flowClient.withdraw(apply.getSchId(), apply.getFlowInstanceId(), apply.getSubmitterId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批进度(结构化轨迹):可见性走与详情同一个守卫(本人/管理员/办理人三分支),
|
||||
* 再按 instanceId 取流程中心进度——对端只校验 schId,故守卫必须在本地先做。
|
||||
* 未进入流程的办件返回空轨迹(前端渲染"待提交"提示),不报错。
|
||||
*/
|
||||
public SvcModels.FlowProgressVo progress(BoaUser user, Integer applyId) {
|
||||
ServiceApply apply = requireVisibleApply(user, applyId);
|
||||
if (apply.getFlowInstanceId() == null) {
|
||||
SvcModels.FlowProgressVo empty = new SvcModels.FlowProgressVo();
|
||||
empty.setInstanceId(null);
|
||||
empty.setBizType(FlowCallbackService.BIZ_TYPE);
|
||||
empty.setBizId(String.valueOf(apply.getId()));
|
||||
empty.setBizTitle(apply.getItemTitle());
|
||||
empty.setStatus(apply.getFlowStatus());
|
||||
empty.setSubmitterId(apply.getSubmitterId());
|
||||
empty.setSubmitterName(apply.getSubmitterName());
|
||||
empty.setSubmitTime(apply.getSubmitTime() == null ? null : apply.getSubmitTime().toString());
|
||||
empty.setSteps(new ArrayList<>());
|
||||
return empty;
|
||||
}
|
||||
return flowClient.progress(apply.getSchId(), apply.getFlowInstanceId(), null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 流程示意图(PNG 流):同守卫;instanceId 口径(带运行时节点状态)。
|
||||
* 出图两步(取免签 token → 取图字节)全部在本服务侧完成,浏览器只拿同源 {@code /api/**} 图片。
|
||||
*/
|
||||
public byte[] diagramImage(BoaUser user, Integer applyId, String mode) {
|
||||
ServiceApply apply = requireVisibleApply(user, applyId);
|
||||
if (apply.getFlowInstanceId() == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "该办件尚未进入流程,无流程图");
|
||||
}
|
||||
return flowClient.diagramImage(apply.getSchId(), apply.getFlowInstanceId(), null, mode);
|
||||
}
|
||||
|
||||
/** 本人办件(提交人校验;撤回/重提共用口径) */
|
||||
private ServiceApply requireOwnApply(BoaUser user, Integer applyId, String forbiddenMsg) {
|
||||
ServiceApply apply = applyRepository.findByIdAndSchId(applyId, userSchId(user))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在"));
|
||||
if (!user.getId().equals(apply.getSubmitterId())) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, forbiddenMsg);
|
||||
}
|
||||
return apply;
|
||||
}
|
||||
|
||||
/** 可见办件(提交人本人 / 管理员 / 办理人;非超管) */
|
||||
private ServiceApply requireVisibleApply(BoaUser user, Integer applyId) {
|
||||
if (isSuperAdmin(user)) {
|
||||
return applyRepository.findById(applyId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在"));
|
||||
}
|
||||
ServiceApply apply = applyRepository.findByIdAndSchId(applyId, userSchId(user))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在"));
|
||||
applyViewGuard.requireVisible(apply, user);
|
||||
return apply;
|
||||
}
|
||||
|
||||
/** 我的办件(提交人视角,flowStatus 可选筛选) */
|
||||
@@ -378,6 +456,16 @@ public class ServiceApplyService {
|
||||
|
||||
// ===== 内部:快照与工具 =====
|
||||
|
||||
/**
|
||||
* 提交类响应补"流程中心是否已接入"标志(BP-R4-1 误报根因,响应期字段不入库):
|
||||
* 代发起是 AFTER_COMMIT 异步,提交成功那一刻 flowStatus 必然是 0;只有结合本标志,
|
||||
* 前端才能区分"排队中(秒级翻转,等轮询)"与"压根没接入(不会翻转,需提示重试/联系人)"。
|
||||
*/
|
||||
private ApplyVo stampFlowEnabled(ApplyVo vo) {
|
||||
vo.setFlowEnabled(flowClient.enabled());
|
||||
return vo;
|
||||
}
|
||||
|
||||
/** 事项要点快照(= biz_snapshot 内容源;附件清单仅文件名——flow-center biz_snapshot 内容边界) */
|
||||
private String itemSnapshotJson(ServiceItem item) {
|
||||
return itemSnapshotJson(item, new ArrayList<>());
|
||||
|
||||
Reference in New Issue
Block a user