|
|
@@ -17,12 +17,9 @@ import org.springframework.stereotype.Service;
|
|
|
import org.springframework.web.client.RestTemplate;
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
|
-import java.util.ArrayList;
|
|
|
-import java.util.Collections;
|
|
|
import java.util.Date;
|
|
|
import java.util.HashMap;
|
|
|
import java.util.LinkedHashMap;
|
|
|
-import java.util.List;
|
|
|
import java.util.Map;
|
|
|
|
|
|
/**
|
|
|
@@ -36,9 +33,6 @@ public class VirtualPayService {
|
|
|
/** 虚拟支付下单场景标识(wx.requestVirtualPayment 固定 URI 前缀) */
|
|
|
private static final String URI_REQUEST_VIRTUAL_PAYMENT = "requestVirtualPayment";
|
|
|
|
|
|
- /** 道具直购模式(微信虚拟支付官方固定值) */
|
|
|
- private static final String MODE_SHORT_SERIES_GOODS = "short_series_goods";
|
|
|
-
|
|
|
/** 签名算法标识 */
|
|
|
private static final String SIGN_TYPE_HMAC_SHA256 = "HMAC-SHA256";
|
|
|
|
|
|
@@ -65,9 +59,6 @@ public class VirtualPayService {
|
|
|
|
|
|
private final RestTemplate restTemplate = new RestTemplate(); // query_order HTTP 调用
|
|
|
|
|
|
- @Value("${wechat.appid}")
|
|
|
- private String appid;
|
|
|
-
|
|
|
/** 虚拟支付 AppKey(商户后台基本配置获取,现网/沙箱分开),paySig 签名密钥 */
|
|
|
@Value("${wechat.appkey}")
|
|
|
private String appKey;
|
|
|
@@ -76,9 +67,9 @@ public class VirtualPayService {
|
|
|
@Value("${wechat.sandbox-appkey:}")
|
|
|
private String sandboxAppKey;
|
|
|
|
|
|
- /** 虚拟支付商户号:占位,当前复用 v3 商户号,联调时替换为虚拟支付商户号 */
|
|
|
- @Value("${wechat.mch-id}")
|
|
|
- private String mchId;
|
|
|
+ /** 虚拟支付应用ID(mp-支付基础配置中的 offerId,全应用唯一;productId 才是道具ID) */
|
|
|
+ @Value("${wechat.offer-id}")
|
|
|
+ private String offerId;
|
|
|
|
|
|
/** 虚拟支付环境:0=现网 1=沙箱 */
|
|
|
@Value("${wechat.virtual-pay-env:0}")
|
|
|
@@ -93,43 +84,42 @@ public class VirtualPayService {
|
|
|
/**
|
|
|
* 生成虚拟支付下单参数(道具直购,双签名:paySig + signature)。
|
|
|
*
|
|
|
- * @param config 道具映射(offerId 取微信平台道具 ID,即 product_id)
|
|
|
- * @param outTradeNo 业务订单号(前缀 A/ORD/SUB 决定发货推送路由)
|
|
|
- * @param totalFee 金额(分,服务端计算传入,防篡改)
|
|
|
- * @param productInfo 道具名称描述
|
|
|
- * @param attach 透传业务信息(goodsType:bizKey,便于对账)
|
|
|
+ * <p>orderInfo 按官方 wx.requestVirtualPayment signData 结构组装(camelCase,字段集固定):
|
|
|
+ * offerId/buyQuantity/env/currencyType/productId/goodsPrice/outTradeNo/attach。
|
|
|
+ * offerId = 支付应用ID(mp-支付基础配置,全应用唯一,非道具ID);productId = 道具ID(member1314 等)。
|
|
|
+ *
|
|
|
+ * @param config 道具映射(productId 取微信平台道具 ID)
|
|
|
+ * @param outTradeNo 业务订单号(前缀 A/ORD/SUB 决定发货推送路由,8-32 字符)
|
|
|
+ * @param totalFee 金额(分,服务端计算传入,防篡改;同时作为 goodsPrice 供官方校验后台道具价)
|
|
|
+ * @param productInfo 道具名称描述(仅日志使用,signData 不含此字段)
|
|
|
+ * @param attach 透传业务信息(goodsType:bizKey,发货推送时透传,便于对账)
|
|
|
* @param sessionKey 用户登录会话密钥(signature 身份签名用,直接字符串编码不 base64 解码)
|
|
|
*/
|
|
|
public VirtualPayParamsDTO generatePayParams(VirtualGoodsConfig config, String outTradeNo,
|
|
|
Integer totalFee, String productInfo,
|
|
|
String attach, String sessionKey) {
|
|
|
- // 1. orderInfo 组装(官方签名规则:signData 不得包含 platform 字段,否则报 -15005)
|
|
|
+ // 1. orderInfo 按官方 signData 结构组装(LinkedHashMap 保序;后端 fastjson 与前端 JSON.stringify 均保插入序)
|
|
|
Map<String, Object> orderInfo = new LinkedHashMap<>();
|
|
|
- orderInfo.put("mch_id", mchId);
|
|
|
- orderInfo.put("appid", appid);
|
|
|
- orderInfo.put("out_trade_no", outTradeNo);
|
|
|
- orderInfo.put("total_fee", totalFee);
|
|
|
- orderInfo.put("product_info", productInfo);
|
|
|
+ orderInfo.put("offerId", offerId); // 支付应用ID(mp-支付基础配置)
|
|
|
+ orderInfo.put("buyQuantity", 1); // 道具直购单次购买数量=1
|
|
|
+ orderInfo.put("env", env); // 0=现网 1=沙箱
|
|
|
+ orderInfo.put("currencyType", "CNY");
|
|
|
+ orderInfo.put("productId", config.getProductId()); // 道具ID(member1314 等)
|
|
|
+ orderInfo.put("goodsPrice", totalFee); // 道具单价(分),官方校验与后台道具价一致
|
|
|
+ orderInfo.put("outTradeNo", outTradeNo);
|
|
|
orderInfo.put("attach", attach);
|
|
|
- orderInfo.put("mode", MODE_SHORT_SERIES_GOODS);
|
|
|
- orderInfo.put("offerId", config.getProductId());
|
|
|
|
|
|
- // 2. extInfo 空对象
|
|
|
+ // 2. extInfo 空对象(官方扩展字段,当前不使用;不进签名串)
|
|
|
Map<String, Object> extInfo = new HashMap<>();
|
|
|
|
|
|
- // 3. signData = orderInfo + extInfo 按官方规则拼接(key 字典序、key=value、& 连接,不含 platform)
|
|
|
- // ⚠️ 精确拼接串按设计文档 §5.2 与官方虚拟支付规范推导,联调对照官方 Python 签名脚本验证
|
|
|
- // (文档示例输出 c37809f27c...)
|
|
|
- String signData = buildSignData(orderInfo, extInfo);
|
|
|
-
|
|
|
- // 4. paySig = hmac_sha256(appKey, "requestVirtualPayment&" + signData)
|
|
|
+ // 3. signData = orderInfo 的 JSON 串(官方签名原文,与前端 JSON.stringify(orderInfo) 逐字节一致)
|
|
|
+ // paySig = hmac_sha256(appKey, "requestVirtualPayment&" + signData)
|
|
|
+ // signature = hmac_sha256(sessionKey, signData)(sessionKey 直接字符串编码,不 base64 解码)
|
|
|
+ String signData = JSON.toJSONString(orderInfo);
|
|
|
String paySig = HmacSignUtil.hmacSha256Hex(effectiveAppKey(), URI_REQUEST_VIRTUAL_PAYMENT + "&" + signData);
|
|
|
+ String signature = HmacSignUtil.hmacSha256Hex(sessionKey, signData);
|
|
|
|
|
|
- // 5. signature = hmac_sha256(sessionKey, <官方定义的拼接串>),sessionKey 直接字符串编码
|
|
|
- // ⚠️ 拼接规则联调时对照官方文档确认
|
|
|
- String signature = HmacSignUtil.hmacSha256Hex(sessionKey, buildSignData(orderInfo, null));
|
|
|
-
|
|
|
- // 6. 组装 DTO
|
|
|
+ // 4. 组装 DTO
|
|
|
VirtualPayParamsDTO dto = new VirtualPayParamsDTO();
|
|
|
dto.setOrderInfo(orderInfo);
|
|
|
dto.setExtInfo(extInfo);
|
|
|
@@ -138,8 +128,8 @@ public class VirtualPayService {
|
|
|
dto.setEnv(env);
|
|
|
dto.setSignType(SIGN_TYPE_HMAC_SHA256);
|
|
|
|
|
|
- log.info("生成虚拟支付参数: outTradeNo={}, env={}, offerId={}, totalFee={}",
|
|
|
- outTradeNo, env, config.getProductId(), totalFee);
|
|
|
+ log.info("生成虚拟支付参数: outTradeNo={}, env={}, offerId={}, productId={}, goodsPrice={}, productInfo={}",
|
|
|
+ outTradeNo, env, offerId, config.getProductId(), totalFee, productInfo);
|
|
|
return dto;
|
|
|
}
|
|
|
|
|
|
@@ -400,7 +390,7 @@ public class VirtualPayService {
|
|
|
/**
|
|
|
* 构建 query_order 的 pay_sig(package-private,便于单测断言)。
|
|
|
* 官方规则:hmac_sha256(appKey, uri + '&' + post_body),uri=/xpay/query_order,
|
|
|
- * post_body 为原始 JSON body 串(与 generatePayParams 的 buildSignData 字典序不同)。
|
|
|
+ * post_body 为原始 JSON body 串(与 requestVirtualPayment 的 signData 同样是对 JSON 串签名)。
|
|
|
*/
|
|
|
String buildQueryOrderPaySig(String postBody) {
|
|
|
return HmacSignUtil.hmacSha256Hex(effectiveAppKey(), "/xpay/query_order&" + postBody);
|
|
|
@@ -422,37 +412,6 @@ public class VirtualPayService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 按官方规则拼接签名数据串:key 按字典序排序,以 key=value 形式用 & 连接。
|
|
|
- * 官方规则要求 signData 不得包含 platform 字段(否则 -15005),此处统一剔除。
|
|
|
- * extInfo 为 null 时仅拼 orderInfo。
|
|
|
- */
|
|
|
- String buildSignData(Map<String, Object> orderInfo, Map<String, Object> extInfo) {
|
|
|
- Map<String, Object> merged = new LinkedHashMap<>(orderInfo);
|
|
|
- if (extInfo != null) {
|
|
|
- merged.putAll(extInfo);
|
|
|
- }
|
|
|
- merged.remove("platform");
|
|
|
-
|
|
|
- List<String> keys = new ArrayList<>(merged.keySet());
|
|
|
- Collections.sort(keys);
|
|
|
-
|
|
|
- StringBuilder sb = new StringBuilder();
|
|
|
- boolean first = true;
|
|
|
- for (String key : keys) {
|
|
|
- Object value = merged.get(key);
|
|
|
- if (value == null) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- if (!first) {
|
|
|
- sb.append("&");
|
|
|
- }
|
|
|
- sb.append(key).append("=").append(value);
|
|
|
- first = false;
|
|
|
- }
|
|
|
- return sb.toString();
|
|
|
- }
|
|
|
-
|
|
|
/** 从 payload 按多个候选 key(含大小写不敏感兜底)取字符串值 */
|
|
|
private String getStringIgnoreCase(Map<String, Object> payload, String... keys) {
|
|
|
if (payload == null) {
|