|
@@ -5,6 +5,7 @@ import com.payment.platform.common.exception.BusinessException;
|
|
|
import com.payment.platform.common.response.PageResult;
|
|
import com.payment.platform.common.response.PageResult;
|
|
|
import com.payment.platform.common.response.Result;
|
|
import com.payment.platform.common.response.Result;
|
|
|
import com.payment.platform.core.security.LoginUser;
|
|
import com.payment.platform.core.security.LoginUser;
|
|
|
|
|
+import com.payment.platform.module.payment.leshouying.client.LeshouyingApiException;
|
|
|
import com.payment.platform.module.payment.leshouying.dto.LsyAreaItemDTO;
|
|
import com.payment.platform.module.payment.leshouying.dto.LsyAreaItemDTO;
|
|
|
import com.payment.platform.module.payment.leshouying.dto.LsyCommitMerchantInfoDTO;
|
|
import com.payment.platform.module.payment.leshouying.dto.LsyCommitMerchantInfoDTO;
|
|
|
import com.payment.platform.module.payment.leshouying.dto.LsyMccItemDTO;
|
|
import com.payment.platform.module.payment.leshouying.dto.LsyMccItemDTO;
|
|
@@ -13,6 +14,7 @@ import com.payment.platform.module.payment.leshouying.entity.LsyMerchantEntity;
|
|
|
import com.payment.platform.module.payment.leshouying.service.LsyMerchantService;
|
|
import com.payment.platform.module.payment.leshouying.service.LsyMerchantService;
|
|
|
import com.payment.platform.module.payment.leshouying.service.LsyOnboardingService;
|
|
import com.payment.platform.module.payment.leshouying.service.LsyOnboardingService;
|
|
|
import com.payment.platform.module.payment.leshouying.util.LsyLogSanitizer;
|
|
import com.payment.platform.module.payment.leshouying.util.LsyLogSanitizer;
|
|
|
|
|
+import jakarta.servlet.http.HttpServletRequest;
|
|
|
import lombok.Data;
|
|
import lombok.Data;
|
|
|
import lombok.EqualsAndHashCode;
|
|
import lombok.EqualsAndHashCode;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.RequiredArgsConstructor;
|
|
@@ -20,6 +22,7 @@ import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.security.access.prepost.PreAuthorize;
|
|
import org.springframework.security.access.prepost.PreAuthorize;
|
|
|
import org.springframework.security.core.Authentication;
|
|
import org.springframework.security.core.Authentication;
|
|
|
import org.springframework.security.core.context.SecurityContextHolder;
|
|
import org.springframework.security.core.context.SecurityContextHolder;
|
|
|
|
|
+import org.springframework.web.bind.annotation.ExceptionHandler;
|
|
|
import org.springframework.web.bind.annotation.GetMapping;
|
|
import org.springframework.web.bind.annotation.GetMapping;
|
|
|
import org.springframework.web.bind.annotation.PostMapping;
|
|
import org.springframework.web.bind.annotation.PostMapping;
|
|
|
import org.springframework.web.bind.annotation.RequestBody;
|
|
import org.springframework.web.bind.annotation.RequestBody;
|
|
@@ -510,6 +513,59 @@ public class LeshouyingOnboardingController {
|
|
|
return Result.ok(onboardingService.mccList(level, parentId, StrUtil.blankToDefault(merchantType, null)));
|
|
return Result.ok(onboardingService.mccList(level, parentId, StrUtil.blankToDefault(merchantType, null)));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ──────────────── 走上游失败时的出口(R-7b) ────────────────
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 本控制器任一走上游的端点抛出 {@link LeshouyingApiException} 时的出口。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>【为什么这条出口必须存在】</b>全仓唯一的 advice({@code GlobalExceptionHandler})
|
|
|
|
|
+ * 五个分支里**没有** {@link LeshouyingApiException} ⇒ 它落兜底的 {@code Exception} 分支,
|
|
|
|
|
+ * 得到 HTTP 500 + 业务码 500「服务器内部错误」—— 零信息,而 500 的语义就是「重试」。
|
|
|
|
|
+ * 对 {@code /create} 来说重试一次就是**再建一个上游商户**(上游建号没有幂等键);
|
|
|
|
|
+ * {@code /commit} 与 {@code /upload-image} 同理,都是一次无法撤销的重复副作用。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>★ <b>为什么挂在控制器上、而不是加进 {@code GlobalExceptionHandler}</b>:那个 advice 是
|
|
|
|
|
+ * **全平台**的,而这条口径只对进件这条链成立 —— 同 {@code LeshouyingOpenController} 当年把三个
|
|
|
|
|
+ * {@code @ExceptionHandler} 挂在自己身上的两条理由(那里还记着「改用 {@code @ControllerAdvice}
|
|
|
|
|
+ * + {@code assignableTypes} 的话,多个 advice 之间只按 {@code @Order} 排序、不按匹配度,
|
|
|
|
|
+ * 是个静默的先后手问题」)。{@code ExceptionHandlerExceptionResolver} 先查 handler 自身的
|
|
|
|
|
+ * 处理器、再查 advice 缓存 ⇒ 本方法对**本控制器**的优先级是确定的。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>★★ <b>两句文案,判据取 HTTP 语义而不是路径名单。</b>本控制器六个端点里五个走上游:
|
|
|
|
|
+ * {@code /create} 建号、{@code /commit} 提交、{@code /upload-image} 上传、
|
|
|
|
|
+ * {@code /areas} 与 {@code /mcc} 取参考数据(只有 {@code /page} 只读本地库)。
|
|
|
|
|
+ * 前三者是**写** —— 超时 / 连接中断时我们根本不知道上游那一次调用有没有落地,
|
|
|
|
|
+ * 所以文案不能断言「没成功」,只能给防重试的指令;方向是不对称的:多谨慎一次只是让管理员
|
|
|
|
|
+ * 多查一遍,乐观一次就是多一个上游商户。后两者是**读** —— GET 被定义为 safe
|
|
|
|
|
+ * (重放不产生副作用),重试是安全的,拿写腿那句会把一次「刷新下拉框」升级成一次人工工单。
|
|
|
|
|
+ * 若改用「哪些路径是写」的名单去枚举,将来往本控制器加端点就会漏 ——
|
|
|
|
|
+ * 而漏的方向恰好是「新写端点拿到可重试文案」,正是本方法要防的那件事。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>★ <b>应答里零上游痕迹</b>(Global Constraint 1):{@code e.getMessage()} 逐字形如
|
|
|
|
|
+ * 「乐收赢接口失败[HTTP_504]: …」—— 既有上游名号又有上游返回码 ⇒ 它**只进日志**,
|
|
|
|
|
+ * 一个字都不进应答。这也正是不能照抄 {@code LeshouyingOpenController} 那句
|
|
|
|
|
+ * {@code Result.of(500, "处理未成功,请稍后重试")} 的原因:那句话对读端点成立、对写端点有害。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>★ <b>回 HTTP 200 + 业务码 502,不是 HTTP 502</b>:本仓业务失败的既有出口就是
|
|
|
|
|
+ * HTTP 200 + 业务码({@code BusinessException} 那一支没有 {@code @ResponseStatus}),
|
|
|
|
|
+ * 而调用方(管理端 axios 拦截器)是按业务码取 {@code msg} 展示的 —— 回 HTTP 502
|
|
|
|
|
+ * 反而会让这句防重试文案到不了操作员眼前。业务码取 502 而不是 500:500 与兜底分支同码,
|
|
|
|
|
+ * 测不出「是不是真的换了出口」;而 502 在本模块已有一条既有含义({@code LsyOnboardingService}
|
|
|
|
|
+ * 里三处「上游成功了、但回的东西不能用」)。
|
|
|
|
|
+ */
|
|
|
|
|
+ @ExceptionHandler(LeshouyingApiException.class)
|
|
|
|
|
+ public Result<?> handleUpstreamFailure(LeshouyingApiException e, HttpServletRequest request) {
|
|
|
|
|
+ // message 已由 LeshouyingApiException 自己 sanitize(压平行终止符 + 逐个字段封顶)⇒ 进日志安全。
|
|
|
|
|
+ log.error("进件走上游调用失败 [{} {}]: {}", request.getMethod(), request.getRequestURI(),
|
|
|
|
|
+ e.getMessage(), e);
|
|
|
|
|
+ if ("GET".equals(request.getMethod())) {
|
|
|
|
|
+ // 读端点:重放无副作用 ⇒ 可以照既有「可重试」口径。
|
|
|
|
|
+ return Result.of(502, "数据获取未成功,请稍后重试");
|
|
|
|
|
+ }
|
|
|
|
|
+ // 写端点:上游那一次调用**可能已经生效** ⇒ 只能给防重试的指令。
|
|
|
|
|
+ return Result.of(502, "请求未能完成,无法确认是否已经生效(不得当作失败直接重试,请联系管理员核实)");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// ──────────────── 服务端决定的值:只来自认证上下文 / 平台配置,不接受请求体指定 ────────────────
|
|
// ──────────────── 服务端决定的值:只来自认证上下文 / 平台配置,不接受请求体指定 ────────────────
|
|
|
|
|
|
|
|
/**
|
|
/**
|