build / build-push (push) Successful in 1m21s
含10-22精修: 对账有限重试(retry_count/指数退避/work_version防旧覆盖)+接缝身份(source-app-key空拒收)+应急对账闸让位FlowApplyRecoveryService+同attempt指令不重开终态。SQL未执行。
150 lines
7.7 KiB
Java
150 lines
7.7 KiB
Java
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();
|
||
}
|
||
}
|