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)。 * *

与 {@link FormCenterCatalogClient}(TASK_1 的目录/绑定核验)同一条出站范式: * 命名目标 {@code form-center} 经 {@link YschOpenCall} 走服务目录三级兜底, * 身份=本应用 appKey(starter 签名拦截器注入),appKey/secret 绝不进浏览器。 * 本类只增加端点,不复用也不修改 TASK_1 那个客户端的职责范围。

* *

四条出站调用各有明确的失败语义,不搞"统一 catch 后返回 null":

* */ @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 verifyGrants(Long consumerSchoolId, List 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(); } }