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":
*
* - {@link #issueContext} 失败=意图不能进入填报,直接拒(没有可信上下文就没有可信建件);
* - {@link #fetchReceipt} 失败=确认动作不做,办件不形成(收据是唯一的答卷事实源);
* - {@link #reportApplied} 失败不回滚本地投影:中心没收到确认只是"对方还不知道",
* 台账里留 PENDING 由重报扫描追平;
* - {@link #verifyGrants} 失败=消费端暂缓写入(撤权竞态口径),而不是按"还有效"放行。
*
*/
@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();
}
}