|
|
@@ -41,6 +41,8 @@ public class UnifiedClient {
|
|
|
private static final String PATH_REFUND_ORDER = "/har-pay/api/refund/refundOrder";
|
|
|
private static final String PATH_QUERY_REFUND = "/har-pay/api/refund/query";
|
|
|
private static final String PATH_CHANNEL_USER_JUMP = "/har-pay/api/channelUserId/jump";
|
|
|
+ /** 渠道用户 ID 跳转固定走服务商模式(业务裁定,不读商户配置、不接受调用方覆盖)。 */
|
|
|
+ private static final String CHANNEL_USER_IF_CODE = "fuioupay";
|
|
|
|
|
|
private static final DateTimeFormatter COMPACT_14 = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
|
|
|
|
|
@@ -86,7 +88,7 @@ public class UnifiedClient {
|
|
|
* <p>签名对<b>编码前</b>的值计算;query 值按 UTF-8 URL 编码;
|
|
|
* {@code appSecret} 只参与签名,绝不出现在 URL 里。
|
|
|
*/
|
|
|
- public String buildChannelUserJumpUrl(UnifiedMerchantEntity cfg, String ifCode,
|
|
|
+ public String buildChannelUserJumpUrl(UnifiedMerchantEntity cfg,
|
|
|
String redirectUrl, String extParam) {
|
|
|
Map<String, Object> params = new LinkedHashMap<>();
|
|
|
params.put("version", VERSION);
|
|
|
@@ -94,7 +96,7 @@ public class UnifiedClient {
|
|
|
params.put("reqTime", reqTime(props, OffsetDateTime.now()));
|
|
|
putIfPresent(params, "mchNo", cfg.getMchNo());
|
|
|
putIfPresent(params, "appId", cfg.getAppId());
|
|
|
- putIfPresent(params, "ifCode", ifCode);
|
|
|
+ params.put("ifCode", CHANNEL_USER_IF_CODE);
|
|
|
putIfPresent(params, "redirectUrl", redirectUrl);
|
|
|
putIfPresent(params, "extParam", extParam);
|
|
|
attachSign(params, cfg);
|
|
|
@@ -208,15 +210,8 @@ public class UnifiedClient {
|
|
|
throw new UnifiedApiException(UnifiedApiException.CODE_TRANSPORT, "上游响应不是 JSON 对象");
|
|
|
}
|
|
|
|
|
|
- // data 可能是 JSON 字符串:必须先解包再验签(verifyData 对非对象按空参数集处理)
|
|
|
- JsonNode data = UnifiedResponseParser.data(root);
|
|
|
- String sign = textOrNull(root.get("sign"));
|
|
|
- if (!UnifiedSigner.verifyData(data, sign, cfg.getAppSecret())) {
|
|
|
- log.error("[统一支付] {} 响应验签失败 mchOrderNo={} mchRefundNo={}",
|
|
|
- api, mchOrderNo, mchRefundNo);
|
|
|
- throw new UnifiedApiException(UnifiedApiException.CODE_SIGN, "上游响应验签失败");
|
|
|
- }
|
|
|
-
|
|
|
+ // 先判 code 再验签:上游业务错误可以带非空 data 却不带 sign,先验签会把业务错误
|
|
|
+ // 伪装成「响应验签失败」(SDK 的 isSuccess 也是先判 code)。
|
|
|
JsonNode codeNode = root.get("code");
|
|
|
if (codeNode == null || codeNode.isNull()) {
|
|
|
log.error("[统一支付] {} 响应缺少 code mchOrderNo={} mchRefundNo={}",
|
|
|
@@ -230,6 +225,15 @@ public class UnifiedClient {
|
|
|
api, mchOrderNo, mchRefundNo, code, UnifiedLogSanitizer.flatten(msg));
|
|
|
throw new UnifiedApiException(code, msg);
|
|
|
}
|
|
|
+
|
|
|
+ // data 可能是 JSON 字符串:必须先解包再验签(verifyData 对非对象按空参数集处理)
|
|
|
+ JsonNode data = UnifiedResponseParser.data(root);
|
|
|
+ String sign = textOrNull(root.get("sign"));
|
|
|
+ if (!UnifiedSigner.verifyData(data, sign, cfg.getAppSecret())) {
|
|
|
+ log.error("[统一支付] {} 响应验签失败 mchOrderNo={} mchRefundNo={}",
|
|
|
+ api, mchOrderNo, mchRefundNo);
|
|
|
+ throw new UnifiedApiException(UnifiedApiException.CODE_SIGN, "上游响应验签失败");
|
|
|
+ }
|
|
|
return data;
|
|
|
}
|
|
|
|