feat(flow): 回调链路迁平台 flow-callback starter + 表单级流程绑定
build / build-push (push) Failing after 8s

- 回调迁 starter(2026-09-20):删本地实现 FlowCallbackService/Controller/
  FilterConfiguration 及旧测试,X-Flw 验签/幂等/乱序门由 starter 承担,
  业务映射入 SvcFlowCallbackHandler(biz-type svc_item);端点仍 /callback/flow,
  存量注册 callback_url 不变、secret 零轮换,镜像表 flow_callback_reg
  由 sql/20260920_flow_callback_starter_migration.sql 迁移
- 表单级流程绑定:ServiceForm 增 flowGuid,事项未绑流程时回退取绑定表单
  最新版本(effectiveFlowGuid),绑定校验/在线办理口径同步
- FlowReconcileJob/FlowCenterClient/OpenApiAuthEnforceConfiguration 等
  SDK 适配微调;补 .gitignore(target/、日志)
- 全量 mvn test 本地绿(含新增 SvcFlowCallbackHandlerTest/
  ServiceItemEffectiveFlowTest)
This commit is contained in:
chenxing
2026-09-21 09:37:33 +08:00
parent 27e19923fb
commit b08d076f0b
26 changed files with 568 additions and 1052 deletions
@@ -81,13 +81,8 @@ public class ServiceItemService {
@Value("${svchall.item.dept-group-type:7}")
private int deptGroupType = 7;
/** 本应用对外可达基址(回调注册 callbackUrl 前缀;projman 注入,本地默认 dev) */
@Value("${svchall.public-base-url:http://localhost:8086}")
private String callbackPublicBase;
private final ServiceItemRepository itemRepository;
private final com.yunhai.svchall.repository.ServiceFormRepository formRepository;
private final com.yunhai.svchall.repository.ServiceFlowRegRepository flowRegRepository;
private final SiteDirectory siteDirectory;
private final ServiceDictService dictService;
private final ServiceFormService formService;
@@ -95,6 +90,7 @@ public class ServiceItemService {
private final SiteGuard siteGuard;
private final DeptSource deptSource;
private final FlowClient flowClient;
private final com.yunhai.platform.flowcallback.FlowCallbackRegistrar callbackRegistrar;
private final TransactionTemplate transactionTemplate;
// ===== 管理端 =====
@@ -206,44 +202,50 @@ public class ServiceItemService {
item = itemRepository.save(item);
attachmentService.bindGuide(site.getSchId(), item.getId(), req.getAttachmentIds());
ensureFlowReg(site, item.getFlowGuid());
// 注册以生效流程为准(表单级绑定:事项自身为空时用表单的)
ensureFlowReg(site, effectiveFlowGuid(site.getSchId(), item.getFlowGuid(), item.getFormCode()));
return item.getId();
}
/**
* 流程绑定联动(V2.0 B.2.6 + 契约冻结防轮换):事项绑 flow_guid 时确保<b>有效</b>回调注册存在
* (reg 下发 secret 落 service_flow_reg)。仅 reg 缺失/失效(secret 空或 status≠0)时才注册——
* flow-center 重复注册会轮换 secret,导致已存密钥验签失配;失效注册原地更新同一行拿回新密钥。
* 流程绑定联动(V2.0 B.2.6 + 契约冻结防轮换;2026-09-20 迁 starter
* {@link com.yunhai.platform.flowcallback.FlowCallbackRegistrar}):绑定 flowGuid 时确保
* <b>有效</b>回调注册存在(reg 下发 secret 落 starter 镜像表 flow_callback_reg)。
* 仅镜像行缺失/失效时才注册——flow-center 重复注册会轮换 secret,导致已存密钥验签失配。
* best-effort:注册失败记 warn 不阻断保存,下次保存重试。包级可见以便单测直测。
*/
void ensureFlowReg(SiteInfo site, String flowGuid) {
if (flowGuid == null || !flowClient.enabled()) {
return;
}
com.yunhai.svchall.entity.ServiceFlowReg reg = flowRegRepository
.findBySchIdAndFlowGuid(site.getSchId(), flowGuid).orElse(null);
if (reg != null && reg.getStatus() != null && reg.getStatus() == 0
&& reg.getSecret() != null && !reg.getSecret().isEmpty()) {
return;
}
String callbackUrl = callbackPublicBase + "/callback/flow";
try {
String secret = flowClient.registerCallback(site.getSchId(), flowGuid, callbackUrl, null);
if (reg == null) {
reg = new com.yunhai.svchall.entity.ServiceFlowReg();
reg.setSchId(site.getSchId());
reg.setFlowGuid(flowGuid);
}
reg.setCallbackUrl(callbackUrl);
reg.setSecret(secret);
reg.setStatus(0);
flowRegRepository.save(reg);
callbackRegistrar.ensureRegistered(
site.getSchId() == null ? null : site.getSchId().intValue(), flowGuid);
} catch (Exception e) {
log.warn("[svc-hall] 回调注册失败(下次保存重试)siteCode={} flowGuid={}:{}",
site.getSiteCode(), flowGuid, e.getMessage());
}
}
/**
* 生效流程解析(表单级绑定,2026-09-20):事项自身绑定优先,为空回退绑定表单最新版本的
* flow_guid。返回 null=两侧都没绑(不可在线办理)。
*/
String effectiveFlowGuid(Long schId, String itemFlowGuid, String formCode) {
if (itemFlowGuid != null && !itemFlowGuid.isEmpty()) {
return itemFlowGuid;
}
if (formCode == null || formCode.isEmpty()) {
return null;
}
return formRepository
.findBySchIdAndCodeOrderByVersionDesc(schId, formCode).stream()
.findFirst()
.map(com.yunhai.svchall.entity.ServiceForm::getFlowGuid)
.filter(guid -> guid != null && !guid.isEmpty())
.orElse(null);
}
@Transactional
public void adminPublish(BoaUser user, String siteCode, Integer id) {
SiteInfo site = siteGuard.requireSiteManager(user, siteCode);
@@ -657,7 +659,8 @@ public class ServiceItemService {
}
}
/** 绑定校验:表单必须存在(含草稿——允许先绑后发布,发起按已发布版本解析);流程按 FlowClient 可用性校验;onlineFlag=1 ⇒ flowGuid 非空 */
/** 绑定校验:表单必须存在(含草稿——允许先绑后发布,发起按已发布版本解析);
* 流程按 FlowClient 可用性校验;onlineFlag=1 ⇒ 生效流程(事项自身或绑定表单的)非空 */
private void validateBindings(SiteInfo site, SvcItemSaveRequest req) {
String formCode = emptyToNull(req.getFormCode());
if (formCode != null && !formRepository.existsBySchIdAndCode(site.getSchId(), formCode)) {
@@ -670,9 +673,9 @@ public class ServiceItemService {
}
}
boolean online = req.getOnlineFlag() == null || req.getOnlineFlag() != 0;
if (online && flowGuid == null) {
if (online && effectiveFlowGuid(site.getSchId(), flowGuid, formCode) == null) {
throw new BizException(ErrorCode.PARAM_ERROR,
"可在线办理的事项必须绑定流程(flowGuid);线下事项请关闭\"可在线办理\"开关");
"可在线办理的事项必须绑定流程(自身或所选表单二选一);线下事项请关闭\"可在线办理\"开关");
}
}