Files
svc-hall-service/src/main/java/com/yunhai/svchall/service/impl/FcSubmissionClient.java
T
chenxing 391281582a
build / build-push (push) Successful in 1m21s
t1~t3+精修: 事项FC绑定/可信建件(意图+投影+恢复)/答卷对账Worker
含10-22精修: 对账有限重试(retry_count/指数退避/work_version防旧覆盖)+接缝身份(source-app-key空拒收)+应急对账闸让位FlowApplyRecoveryService+同attempt指令不重开终态。SQL未执行。
2026-10-22 10:03:07 +08:00

150 lines
7.7 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.yunhai.svchall.service.impl;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.openapi.core.YschOpenCall;
import com.yunhai.platform.utils.exception.ErrorCode;
import com.yunhai.svchall.model.FcApplyModels.AppliedReport;
import com.yunhai.svchall.model.FcApplyModels.AppliedReportView;
import com.yunhai.svchall.model.FcApplyModels.ContextIssueRequest;
import com.yunhai.svchall.model.FcApplyModels.ContextIssueView;
import com.yunhai.svchall.model.FcApplyModels.ContextReceiptView;
import com.yunhai.svchall.model.FcApplyModels.GrantSnapshotView;
import com.yunhai.svchall.model.FcApplyModels.GrantVerifyRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
/**
* 表单中心可信建件客户端(answer-delivery-v2 §6/§7,TASK_2 A.2/A.4/B.6)。
*
* <p>与 {@link FormCenterCatalogClient}(TASK_1 的目录/绑定核验)同一条出站范式:
* 命名目标 {@code form-center} 经 {@link YschOpenCall} 走服务目录三级兜底,
* 身份=本应用 appKey(starter 签名拦截器注入),<b>appKey/secret 绝不进浏览器</b>。
* 本类只增加端点,不复用也不修改 TASK_1 那个客户端的职责范围。</p>
*
* <p>四条出站调用各有明确的失败语义,不搞"统一 catch 后返回 null":</p>
* <ul>
* <li>{@link #issueContext} 失败=意图不能进入填报,直接拒(没有可信上下文就没有可信建件);</li>
* <li>{@link #fetchReceipt} 失败=确认动作不做,办件不形成(收据是唯一的答卷事实源);</li>
* <li>{@link #reportApplied} 失败<b>不回滚</b>本地投影:中心没收到确认只是"对方还不知道",
* 台账里留 PENDING 由重报扫描追平;</li>
* <li>{@link #verifyGrants} 失败=消费端<b>暂缓写入</b>(撤权竞态口径),而不是按"还有效"放行。</li>
* </ul>
*/
@Slf4j
@Component
public class FcSubmissionClient {
/** 服务目录 key(同 TASK_1 客户端,一个目标不拆两个名字) */
static final String TARGET = "form-center";
static final String CONTEXT_PATH = "/oapi/form/submission-context";
static final String DELIVERY_RECEIPT_PATH = "/oapi/form/answer-delivery/receipt";
static final String GRANT_VERIFY_PATH = "/oapi/form/answer-delivery/grants/verify";
private final YschOpenCall openCall;
public FcSubmissionClient(YschOpenCall openCall) {
this.openCall = openCall;
}
/** 签发/复用可信提交上下文(同意图重试回到同一上下文,不新建第二业务意图) */
public ContextIssueView issueContext(ContextIssueRequest request) {
requireRequest(request);
try {
ContextIssueView data = openCall.post(TARGET, CONTEXT_PATH, Collections.emptyMap(),
request, ContextIssueView.class);
if (data == null || blank(data.getContextToken())) {
throw new BizException(ErrorCode.PARAM_ERROR, "表单中心提交上下文返回异常(缺少 contextToken)");
}
if (!request.getIntentId().equals(data.getIntentId())) {
throw new BizException(ErrorCode.PARAM_ERROR,
"表单中心返回的意图键与请求不符(请求 " + request.getIntentId()
+ ",返回 " + data.getIntentId() + ")");
}
if (blank(data.getEmbedOrigin())) {
// 中心锁定的可嵌入 origin 是 postMessage 的唯一目标;缺了就只能拒,不能自己拼一个
throw new BizException(ErrorCode.PARAM_ERROR, "表单中心未返回可嵌入 origin,已拒绝打开填报页");
}
return data;
} catch (BizException e) {
throw e;
} catch (RuntimeException e) {
log.warn("[svc-hall] 提交上下文签发失败 intentId={}:{}", request.getIntentId(), e.toString());
throw new BizException(ErrorCode.PARAM_ERROR,
"表单中心提交上下文不可达(网络/授权未配置):" + e.getMessage());
}
}
/** 读取服务端收据(确认动作的唯一数据源;浏览器 success 只是"可以去读"的信号) */
public ContextReceiptView fetchReceipt(String fcIntentId) {
if (blank(fcIntentId)) {
throw new BizException(ErrorCode.PARAM_ERROR, "fcIntentId 必填");
}
try {
ContextReceiptView data = openCall.get(TARGET, CONTEXT_PATH + "/" + fcIntentId + "/receipt",
Collections.emptyMap(), ContextReceiptView.class);
if (data == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "表单中心收据返回空 data");
}
return data;
} catch (BizException e) {
throw e;
} catch (RuntimeException e) {
log.warn("[svc-hall] 收据读取失败 intentId={}:{}", fcIntentId, e.toString());
throw new BizException(ErrorCode.PARAM_ERROR,
"表单中心收据不可达(网络/授权未配置),本次不建件:" + e.getMessage());
}
}
/** 上报本地应用结果(consumerApplied);失败只记台账,不回滚已落库的投影 */
public AppliedReportView reportApplied(AppliedReport report) {
try {
return openCall.post(TARGET, DELIVERY_RECEIPT_PATH, Collections.emptyMap(),
report, AppliedReportView.class);
} catch (RuntimeException e) {
log.warn("[svc-hall] 投递回执上报失败 eventId={}:{}", report == null ? null : report.getEventId(),
e.toString());
return null;
}
}
/** 批量授权核验(消费端写投影前的闸门;不可用即暂缓,不按"还有效"放行) */
public List<GrantSnapshotView> verifyGrants(Long consumerSchoolId, List<String> formUuids) {
GrantVerifyRequest body = new GrantVerifyRequest();
body.setConsumerSchoolId(consumerSchoolId);
body.setFormUuids(formUuids == null ? Collections.emptyList() : formUuids);
try {
GrantSnapshotView[] data = openCall.post(TARGET, GRANT_VERIFY_PATH, Collections.emptyMap(),
body, GrantSnapshotView[].class);
if (data == null) {
throw new IllegalStateException("grants/verify 返回空 data");
}
return java.util.Arrays.asList(data);
} catch (RuntimeException e) {
log.warn("[svc-hall] 授权核验失败 school={}:{}", consumerSchoolId, e.toString());
throw new BizException(ErrorCode.PARAM_ERROR, "表单中心授权核验不可达,本次暂缓写入");
}
}
private static void requireRequest(ContextIssueRequest request) {
if (request == null || blank(request.getIntentId())) {
throw new BizException(ErrorCode.PARAM_ERROR, "intentId 必填(本应用服务端生成的业务意图键)");
}
if (request.getConsumerSchoolId() == null || request.getConsumerSchoolId() <= 0) {
throw new BizException(ErrorCode.PARAM_ERROR, "consumerSchoolId 必须是真实学校");
}
if (blank(request.getFormUuid()) || request.getFormVersion() == null || request.getFormVersion() <= 0) {
throw new BizException(ErrorCode.PARAM_ERROR, "表单坐标必须成对且版本确切");
}
if (request.getSubmitterId() == null || request.getSubmitterId() <= 0) {
throw new BizException(ErrorCode.PARAM_ERROR, "submitterId 必填(会话用户,不接受浏览器传入)");
}
}
private static boolean blank(String raw) {
return raw == null || raw.trim().isEmpty();
}
}