t1~t3+精修: 事项FC绑定/可信建件(意图+投影+恢复)/答卷对账Worker
build / build-push (push) Successful in 1m21s

含10-22精修: 对账有限重试(retry_count/指数退避/work_version防旧覆盖)+接缝身份(source-app-key空拒收)+应急对账闸让位FlowApplyRecoveryService+同attempt指令不重开终态。SQL未执行。
This commit is contained in:
chenxing
2026-10-22 10:03:07 +08:00
parent 036bdb1f7c
commit 391281582a
45 changed files with 8222 additions and 6 deletions
@@ -94,6 +94,22 @@ public class ServiceItemService {
private final com.yunhai.platform.flowcallback.FlowCallbackRegistrar callbackRegistrar;
private final TransactionTemplate transactionTemplate;
/**
* 表单中心目录/核验客户端(v3/TASK_1)。
*
* <p><b>用 setter 而不是加进 {@code @RequiredArgsConstructor} 的构造参数</b>:本类那 11 参构造
* 已被 {@code ServiceItemEffectiveFlowTest}/{@code ServiceItemFlowRegTest} 逐位钉住,而那两份测试
* 不在本任务的可改清单里——多一个入参会让它们直接编译不过。绑定核验必须在
* {@link #adminSave} 的事务内发生(远端失败=整次保存回滚,不留半个绑定),所以客户端装配在这里,
* 而不是挪到控制器入口。</p>
*/
private FormCenterCatalogClient formCenterClient;
@org.springframework.beans.factory.annotation.Autowired
void setFormCenterClient(FormCenterCatalogClient formCenterClient) {
this.formCenterClient = formCenterClient;
}
// ===== 管理端 =====
public Map<String, Object> adminPage(BoaUser user, SvcItemPageQuery query) {
@@ -196,7 +212,15 @@ public class ServiceItemService {
item.setNoticeCategoryId(req.getNoticeCategoryId());
item.setRuleCategoryId(req.getRuleCategoryId());
item.setCoverImage(toRelativeIfLocal(req.getCoverImage()));
item.setFormCode(emptyToNull(req.getFormCode()));
// 本地 form_code 停写(v3/TASK_1):原样回传=保留历史绑定(不清空存量行),改值即拒;
// 新表单一律走表单中心坐标,且已在 validateBindings 里经远端核验
String legacyFormCode = emptyToNull(req.getFormCode());
if (legacyFormCode != null && !legacyFormCode.equals(item.getFormCode())) {
throw new BizException(ErrorCode.PARAM_ERROR,
"本地表单定义已停用:不能在事项上写新的 form_code,请到表单中心建表后选择表单");
}
item.setFcFormUuid(trimToNull(req.getFcFormUuid()));
item.setFcFormVersion(req.getFcFormVersion());
item.setFlowGuid(emptyToNull(req.getFlowGuid()));
item.setApplyUrl(applyUrl);
// 服务承诺(P1-1):空串归一为 null,避免库里出现 "" 与 null 两种空值
@@ -740,12 +764,28 @@ public class ServiceItemService {
}
/** 绑定校验:表单必须存在(含草稿——允许先绑后发布,发起按已发布版本解析);
* 流程按 FlowClient 可用性校验;onlineFlag=1 ⇒ 生效流程(事项自身或绑定表单的)非空 */
* 流程按 FlowClient 可用性校验;onlineFlag=1 ⇒ 生效流程(事项自身或绑定表单的)非空。
* <br>v3/TASK_1 追加:表单中心绑定必须<b>成对</b>提交,且必须在写库前经远端核验
* (核验失败=抛错=整次 {@code adminSave} 回滚,不存在"uuid 落了、版本没落"的半个绑定)。
* 这里刻意不做"没有表单中心坐标时回落本地 form_code"的第二套口径:本地轨只读不写。 */
private void validateBindings(SiteInfo site, SvcItemSaveRequest req) {
String formCode = emptyToNull(req.getFormCode());
if (formCode != null && !formRepository.existsBySchIdAndCode(site.getSchId(), formCode)) {
throw new BizException(ErrorCode.PARAM_ERROR, "办理表单不存在:" + formCode);
}
String fcFormUuid = trimToNull(req.getFcFormUuid());
Integer fcFormVersion = req.getFcFormVersion();
if ((fcFormUuid == null) != (fcFormVersion == null)) {
throw new BizException(ErrorCode.PARAM_ERROR,
"表单中心绑定必须成对提交(fcFormUuid 与 fcFormVersion 同传或都不传;"
+ "当前 uuid=" + fcFormUuid + ",version=" + fcFormVersion + ")");
}
if (fcFormUuid != null) {
if (formCenterClient == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "表单中心客户端未装配,无法核验绑定坐标");
}
formCenterClient.requireBindable(site.getSchId(), fcFormUuid, fcFormVersion);
}
String flowGuid = emptyToNull(req.getFlowGuid());
if (flowGuid != null && flowClient.enabled()) {
if (flowClient.getFlow(site.getSchId(), flowGuid) == null) {
@@ -889,6 +929,15 @@ public class ServiceItemService {
return StringUtils.hasText(value) ? value : null;
}
/**
* 跨服务坐标用(表单中心 uuid):去空白后入库,空串归 null。
* {@link #emptyToNull} 不去空白——历史列里"首尾带空格的 code"从来不是问题,
* 但 uuid 要和中心逐字等值比对,留着空格会让核验通过后存进一个查不回的值。
*/
private static String trimToNull(String value) {
return StringUtils.hasText(value) ? value.trim() : null;
}
/** 封面:本应用上传的相对路径原样入库;外部完整 URL 截取 /upload/ 相对段(与附件同口径);折叠历史叠加段自愈 */
private static String toRelativeIfLocal(String coverImage) {
if (!StringUtils.hasText(coverImage)) {