@@ -30,12 +30,14 @@ import com.wechat.pay.java.service.payments.jsapi.model.*;
import com.wechat.pay.java.service.payments.nativepay.NativePayService ;
// Native支付的类将使用完全限定名避免冲突
import com.wechat.pay.java.service.payments.model.Transaction ;
import lombok.Data ;
import org.springframework.beans.factory.annotation.Value ;
import org.springframework.stereotype.Service ;
import org.springframework.util.CollectionUtils ;
import javax.annotation.Resource ;
import java.math.BigDecimal ;
import java.math.RoundingMode ;
import java.time.LocalDateTime ;
import java.util.* ;
import java.util.concurrent.TimeUnit ;
@@ -86,6 +88,165 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
private ShopExpressService shopExpressService ;
private static final long USER_ORDER_STATS_CACHE_SECONDS = 60L ;
private static final long WECHAT_PREPAY_SNAPSHOT_TTL_MINUTES = 30L ;
private static final String WECHAT_PREPAY_SNAPSHOT_KEY_PREFIX = " wxpay:prepay:snapshot: " ;
@Data
private static class WechatPrepaySnapshot {
private String outTradeNo ;
private String appid ;
private String mchid ;
private String openid ;
private Integer total ; // 分
private String description ;
private String notifyUrl ;
private String attach ;
private String wechatPayType ;
}
private static String trimTrailingSlashes ( String url ) {
if ( url = = null ) {
return null ;
}
String u = url . trim ( ) ;
while ( u . endsWith ( " / " ) ) {
u = u . substring ( 0 , u . length ( ) - 1 ) ;
}
return u ;
}
private static String ensureTenantSuffix ( String baseUrl , Integer tenantId ) {
if ( StrUtil . isBlank ( baseUrl ) | | tenantId = = null ) {
return baseUrl ;
}
String base = trimTrailingSlashes ( baseUrl ) ;
String suffix = " / " + tenantId ;
if ( base . endsWith ( suffix ) ) {
return base ;
}
return base + suffix ;
}
private String prepaySnapshotKey ( Payment payment , String outTradeNo ) {
return WECHAT_PREPAY_SNAPSHOT_KEY_PREFIX + payment . getMchId ( ) + " : " + outTradeNo ;
}
private WechatPrepaySnapshot getPrepaySnapshot ( Payment payment , String outTradeNo ) {
try {
return redisUtil . get ( prepaySnapshotKey ( payment , outTradeNo ) , WechatPrepaySnapshot . class ) ;
} catch ( Exception e ) {
// 缓存不可用时不影响支付主流程
log . warn ( " 读取微信预下单快照失败 - outTradeNo={}, mchId={} " , outTradeNo , payment ! = null ? payment . getMchId ( ) : null , e ) ;
return null ;
}
}
private void savePrepaySnapshot ( Payment payment , WechatPrepaySnapshot snapshot ) {
if ( payment = = null | | snapshot = = null | | StrUtil . isBlank ( snapshot . getOutTradeNo ( ) ) ) {
return ;
}
try {
redisUtil . set ( prepaySnapshotKey ( payment , snapshot . getOutTradeNo ( ) ) , snapshot , WECHAT_PREPAY_SNAPSHOT_TTL_MINUTES , TimeUnit . MINUTES ) ;
} catch ( Exception e ) {
log . warn ( " 保存微信预下单快照失败 - outTradeNo={}, mchId={} " , snapshot . getOutTradeNo ( ) , payment . getMchId ( ) , e ) ;
}
}
private static boolean isIdempotencyParamMismatch ( Throwable t ) {
Throwable cur = t ;
while ( cur ! = null ) {
if ( cur instanceof ServiceException ) {
ServiceException se = ( ServiceException ) cur ;
String code = se . getErrorCode ( ) ;
String msg = se . getErrorMessage ( ) ;
String body = se . getResponseBody ( ) ;
if ( " INVALID_REQUEST " . equals ( code ) & & ( ( msg ! = null & & msg . contains ( " 请求重入 " ) ) | | ( body ! = null & & body . contains ( " 请求重入 " ) ) ) ) {
return true ;
}
}
String m = cur . getMessage ( ) ;
if ( m ! = null & & m . contains ( " INVALID_REQUEST " ) & & m . contains ( " 请求重入 " ) ) {
return true ;
}
cur = cur . getCause ( ) ;
}
return false ;
}
private static Integer toFen ( BigDecimal amountYuan ) {
if ( amountYuan = = null ) {
return null ;
}
// 微信支付金额字段使用整数分,这里按两位小数四舍五入再转分
BigDecimal fen = amountYuan . setScale ( 2 , RoundingMode . HALF_UP ) . movePointRight ( 2 ) ;
return fen . intValueExact ( ) ;
}
private String defaultShopOrderNotifyUrl ( Integer tenantId ) {
String base = trimTrailingSlashes ( config . getServerUrl ( ) ) ;
if ( StrUtil . isBlank ( base ) | | tenantId = = null ) {
return null ;
}
return base + " /shop/shop-order/notify/ " + tenantId ;
}
private String legacySystemWxPayNotifyUrl ( Integer tenantId ) {
String base = trimTrailingSlashes ( config . getServerUrl ( ) ) ;
if ( StrUtil . isBlank ( base ) | | tenantId = = null ) {
return null ;
}
return base + " /system/wx-pay/notify/ " + tenantId ;
}
private String legacyShopWxPayNotifyUrl ( Integer tenantId ) {
// 旧代码曾使用 /api/shop/wx-pay/notify/{tenantId}
if ( tenantId = = null ) {
return null ;
}
return " http://jimei-api.natapp1.cc/api/shop/wx-pay/notify/ " + tenantId ;
}
private String devShopOrderNotifyUrl ( Integer tenantId ) {
if ( tenantId = = null ) {
return null ;
}
return " http://jimei-api.natapp1.cc/api/shop/shop-order/notify/ " + tenantId ;
}
private List < String > buildNotifyUrlCandidates ( ShopOrder order , Payment payment , WechatPrepaySnapshot snapshot ) {
LinkedHashSet < String > urls = new LinkedHashSet < > ( ) ;
if ( snapshot ! = null & & StrUtil . isNotBlank ( snapshot . getNotifyUrl ( ) ) ) {
urls . add ( trimTrailingSlashes ( snapshot . getNotifyUrl ( ) ) ) ;
}
if ( payment ! = null & & StrUtil . isNotBlank ( payment . getNotifyUrl ( ) ) ) {
urls . add ( ensureTenantSuffix ( payment . getNotifyUrl ( ) , order . getTenantId ( ) ) ) ;
}
// 新默认回调(本项目已实现)
String shopOrderNotify = defaultShopOrderNotifyUrl ( order . getTenantId ( ) ) ;
if ( StrUtil . isNotBlank ( shopOrderNotify ) ) {
urls . add ( shopOrderNotify ) ;
}
// 兼容历史回调地址(用于已创建订单的“重新支付”重入校验)
String legacySystem = legacySystemWxPayNotifyUrl ( order . getTenantId ( ) ) ;
if ( StrUtil . isNotBlank ( legacySystem ) ) {
urls . add ( legacySystem ) ;
}
if ( " dev " . equals ( active ) ) {
String devNotify = devShopOrderNotifyUrl ( order . getTenantId ( ) ) ;
if ( StrUtil . isNotBlank ( devNotify ) ) {
urls . add ( devNotify ) ;
}
String devLegacy = legacyShopWxPayNotifyUrl ( order . getTenantId ( ) ) ;
if ( StrUtil . isNotBlank ( devLegacy ) ) {
urls . add ( devLegacy ) ;
}
}
return new ArrayList < > ( urls ) ;
}
@Override
@@ -321,13 +482,15 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
Config wxPayConfig = getWxPayConfig ( order ) ;
NativePayService nativeService = new NativePayService . Builder ( ) . config ( wxPayConfig ) . build ( ) ;
// 订单金额(转换为分)
BigDecimal decimal = order . getTotalPrice ( ) ;
final BigDecimal multiply = decimal . multiply ( new BigDecimal ( 100 ) ) ;
Integer money = multiply . intValue ( ) ;
// 订单金额(转换为分)- 优先使用 payPrice, 避免与 JSAPI 金额字段不一致导致重入校验失败
BigDecimal payAmount = order . getPayPrice ( ) ! = null ? order . getPayPrice ( ) : order . getTotalPrice ( ) ;
Integer money = toFen ( payAmount ) ;
if ( money = = null ) {
throw new RuntimeException ( " 订单金额为null " ) ;
}
// 测试环境使用1分钱
if ( active . equals ( " dev " ) ) {
if ( " dev " . equals ( active ) ) {
money = 1 ;
}
@@ -354,26 +517,67 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
System . out . println ( " 商户号(MchId): " + payment . getMchId ( ) ) ;
System . out . println ( " 应用ID(AppId): " + payment . getAppId ( ) ) ;
// 设置回调地址
String notifyUrl = config . getServerUrl ( ) + " /system/wx-pay/notify/ " + order . getTenantId ( ) ;
if ( active . equals ( " dev " ) ) {
notifyUrl = " http://jimei-api.natapp1.cc/api/shop/wx-pay/notify/ " + order . getTenantId ( ) ;
String outTradeNo = order . getOrderNo ( ) ;
WechatPrepaySnapshot snapshot = getPrepaySnapshot ( payment , outTradeNo ) ;
if ( snapshot = = null ) {
snapshot = new WechatPrepaySnapshot ( ) ;
snapshot . setOutTradeNo ( outTradeNo ) ;
snapshot . setWechatPayType ( WechatPayType . NATIVE ) ;
snapshot . setAppid ( payment . getAppId ( ) ) ;
snapshot . setMchid ( payment . getMchId ( ) ) ;
snapshot . setAttach ( order . getTenantId ( ) . toString ( ) ) ;
snapshot . setTotal ( money ) ;
snapshot . setDescription ( description ) ;
// notifyUrl 在下面根据重试结果回填
} else {
// 使用快照中的关键字段,确保重入时参数一致
if ( snapshot . getTotal ( ) ! = null ) {
amount . setTotal ( snapshot . getTotal ( ) ) ;
}
if ( StrUtil . isNotBlank ( paymen t. getNotifyUrl ( ) ) ) {
notifyUrl = paymen t . g etNotifyUrl ( ) . concat ( " / " ) . concat ( order . getTenantId ( ) . toString ( ) ) ;
if ( StrUtil . isNotBlank ( snapsho t. getDescription ( ) ) ) {
reques t . s etDescription ( snapshot . getDescription ( ) ) ;
}
if ( StrUtil . isNotBlank ( snapshot . getAttach ( ) ) ) {
request . setAttach ( snapshot . getAttach ( ) ) ;
}
if ( StrUtil . isNotBlank ( snapshot . getAppid ( ) ) ) {
request . setAppid ( snapshot . getAppid ( ) ) ;
}
if ( StrUtil . isNotBlank ( snapshot . getMchid ( ) ) ) {
request . setMchid ( snapshot . getMchid ( ) ) ;
}
}
// 开发环境固定使用1分钱( 与历史行为保持一致)
if ( " dev " . equals ( active ) ) {
amount . setTotal ( 1 ) ;
request . setAmount ( amount ) ;
snapshot . setTotal ( 1 ) ;
}
// 回调地址:优先用快照;若重入提示参数不一致,则尝试历史回调地址
Exception last = null ;
String notifyUrlUsed = null ;
for ( String notifyUrl : buildNotifyUrlCandidates ( order , payment , snapshot ) ) {
if ( StrUtil . isBlank ( notifyUrl ) ) {
continue ;
}
request . setNotifyUrl ( notifyUrl ) ;
try {
System . out . println ( " === 发起Native支付请求 === " ) ;
System . out . println ( " 请求参数: " + request ) ;
// 调用Native支付API
com . wechat . pay . java . service . payments . nativepay . model . PrepayResponse response = nativeService . prepay ( request ) ;
notifyUrlUsed = notifyUrl ;
System . out . println ( " === Native支付响应成功 === " ) ;
System . out . println ( " 二维码URL: " + response . getCodeUrl ( ) ) ;
// 构建返回数据
snapshot . setNotifyUrl ( notifyUrlUsed ) ;
snapshot . setTotal ( amount . getTotal ( ) ) ;
snapshot . setDescription ( request . getDescription ( ) ) ;
savePrepaySnapshot ( payment , snapshot ) ;
final HashMap < String , String > orderInfo = new HashMap < > ( ) ;
orderInfo . put ( " provider " , " wxpay " ) ;
orderInfo . put ( " codeUrl " , response . getCodeUrl ( ) ) ; // Native支付返回二维码URL
@@ -382,6 +586,18 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
orderInfo . put ( " wechatPayType " , WechatPayType . NATIVE ) ;
return orderInfo ;
} catch ( Exception e ) {
last = e ;
if ( ! isIdempotencyParamMismatch ( e ) ) {
throw e ;
}
log . warn ( " Native预下单重入参数不一致, 尝试切换notifyUrl重试 - outTradeNo={}, notifyUrl={} " , outTradeNo , notifyUrl , e ) ;
}
}
if ( last ! = null ) {
throw last ;
}
throw new RuntimeException ( " 创建Native支付订单失败: notifyUrl为空 " ) ;
}
/**
@@ -399,10 +615,12 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
JsapiServiceExtension service = getWxService ( order ) ;
System . out . println ( " 微信支付服务构建完成 " ) ;
// 订单金额
BigDecimal decimal = order . getPayPrice ( ) ;
final BigDecimal multiply = decimal . multiply ( new BigDecimal ( 100 ) ) ;
Integer money = multiply . intValue ( ) ;
// 订单金额(分)
BigDecimal payAmount = order . getPayPrice ( ) ! = null ? order . getPayPrice ( ) : order . getTotalPrice ( ) ;
Integer money = toFen ( payAmount ) ;
if ( money = = null ) {
throw new RuntimeException ( " 订单金额为null " ) ;
}
System . out . println ( " === 构建支付请求参数 === " ) ;
@@ -419,7 +637,8 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
request . setMchid ( payment . getMchId ( ) ) ;
// 微信支付description字段限制127字节, 需要截断处理
String d escription = com . gxwebsoft . common . core . utils . WechatPayUtils . processDescription ( order . getComments ( ) ) ;
String rawD escription = StrUtil . isNotBlank ( order . getComments ( ) ) ? order . getComments ( ) : " 订单支付 " ;
String description = com . gxwebsoft . common . core . utils . WechatPayUtils . processDescription ( rawDescription ) ;
System . out . println ( " 设置描述: " + description ) ;
request . setDescription ( description ) ;
@@ -433,26 +652,76 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
System . out . println ( " 设置用户OpenID: " + order . getOpenid ( ) ) ;
payer . setOpenid ( order . getOpenid ( ) ) ;
request . setPayer ( payer ) ;
request . setNotifyUrl ( config . getServerUrl ( ) + " /system/wx-pay/notify/ " + order . getTenantId ( ) ) ; // 默认回调地址
// 测试环境
if ( active . equals ( " dev " ) ) {
String outTradeNo = order . getOrderNo ( ) ;
WechatPrepaySnapshot snapshot = getPrepaySnapshot ( payment , outTradeNo ) ;
if ( snapshot = = null ) {
snapshot = new WechatPrepaySnapshot ( ) ;
snapshot . setOutTradeNo ( outTradeNo ) ;
snapshot . setWechatPayType ( WechatPayType . JSAPI ) ;
snapshot . setAppid ( payment . getAppId ( ) ) ;
snapshot . setMchid ( payment . getMchId ( ) ) ;
snapshot . setOpenid ( order . getOpenid ( ) ) ;
snapshot . setAttach ( order . getTenantId ( ) . toString ( ) ) ;
snapshot . setTotal ( amount . getTotal ( ) ) ;
snapshot . setDescription ( request . getDescription ( ) ) ;
// notifyUrl 在下面根据重试结果回填
} else {
// 使用快照中的关键字段,确保重入时参数一致
if ( snapshot . getTotal ( ) ! = null ) {
amount . setTotal ( snapshot . getTotal ( ) ) ;
request . setAmount ( amount ) ;
}
if ( StrUtil . isNotBlank ( snapshot . getDescription ( ) ) ) {
request . setDescription ( snapshot . getDescription ( ) ) ;
}
if ( StrUtil . isNotBlank ( snapshot . getAttach ( ) ) ) {
request . setAttach ( snapshot . getAttach ( ) ) ;
}
if ( StrUtil . isNotBlank ( snapshot . getOpenid ( ) ) ) {
payer . setOpenid ( snapshot . getOpenid ( ) ) ;
request . setPayer ( payer ) ;
}
if ( StrUtil . isNotBlank ( snapshot . getAppid ( ) ) ) {
request . setAppid ( snapshot . getAppid ( ) ) ;
}
if ( StrUtil . isNotBlank ( snapshot . getMchid ( ) ) ) {
request . setMchid ( snapshot . getMchid ( ) ) ;
}
}
// 测试环境使用1分钱( 快照优先)
if ( " dev " . equals ( active ) ) {
amount . setTotal ( 1 ) ;
request . setAmount ( amount ) ;
request . setNotifyUrl ( " http://jimei-api.natapp1.cc/api/shop/wx-pay/notify/ " + order . g etTenantId ( ) ) ; // 默认回调地址
snapshot . s etTotal ( 1 ) ;
}
// 后台配置的回调地址
if ( StrUtil . isNotBlank ( payment . getNotifyUrl ( ) ) ) {
request . setNotifyUrl ( payment . getNotifyUrl ( ) . concat ( " / " ) . concat ( order . getTenantId ( ) . toString ( ) ) ) ;
System . out . println ( " 后台配置的回调地址 = " + request . getNotifyUrl ( ) ) ;
Exception last = null ;
String notifyUrlUsed = null ;
for ( String notifyUrl : buildNotifyUrlCandidates ( order , payment , snapshot ) ) {
if ( StrUtil . isBlank ( notifyUrl ) ) {
continue ;
}
request . setNotifyUrl ( notifyUrl ) ;
try {
System . out . println ( " === 发起微信支付请求 === " ) ;
System . out . println ( " 请求参数: " + request ) ;
PrepayWithRequestPaymentResponse response = service . prepayWithRequestPayment ( request ) ;
notifyUrlUsed = notifyUrl ;
System . out . println ( " === 微信支付响应成功 === " ) ;
System . out . println ( " 预支付ID: " + response . getPackageVal ( ) ) ;
snapshot . setNotifyUrl ( notifyUrlUsed ) ;
snapshot . setTotal ( request . getAmount ( ) . getTotal ( ) ) ;
snapshot . setDescription ( request . getDescription ( ) ) ;
snapshot . setOpenid ( request . getPayer ( ) ! = null ? request . getPayer ( ) . getOpenid ( ) : null ) ;
snapshot . setAppid ( request . getAppid ( ) ) ;
snapshot . setMchid ( request . getMchid ( ) ) ;
snapshot . setAttach ( request . getAttach ( ) ) ;
savePrepaySnapshot ( payment , snapshot ) ;
final HashMap < String , String > orderInfo = new HashMap < > ( ) ;
orderInfo . put ( " provider " , " wxpay " ) ;
orderInfo . put ( " timeStamp " , response . getTimeStamp ( ) ) ;
@@ -464,6 +733,18 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
orderInfo . put ( " payType " , WechatPayType . JSAPI ) ;
orderInfo . put ( " wechatPayType " , WechatPayType . JSAPI ) ;
return orderInfo ;
} catch ( Exception e ) {
last = e ;
if ( ! isIdempotencyParamMismatch ( e ) ) {
throw e ;
}
log . warn ( " JSAPI预下单重入参数不一致, 尝试切换notifyUrl重试 - outTradeNo={}, notifyUrl={} " , outTradeNo , notifyUrl , e ) ;
}
}
if ( last ! = null ) {
throw last ;
}
throw new RuntimeException ( " 创建JSAPI支付订单失败: notifyUrl为空 " ) ;
}
@Override