Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
da69e2e2
authored
Oct 22, 2025
by
Jony.L
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
新支付初步修改
parent
ff391fee
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
512 additions
and
0 deletions
+512
-0
computility-module-pay/src/main/java/com/luhu/computility/module/pay/framework/pay/core/client/impl/wpgj/WpgjCryptoUtils.java
+241
-0
computility-module-pay/src/main/java/com/luhu/computility/module/pay/framework/pay/core/client/impl/wpgj/WpgjPayClient.java
+200
-0
computility-module-pay/src/main/java/com/luhu/computility/module/pay/framework/pay/core/client/impl/wpgj/WpgjPayClientConfig.java
+71
-0
No files found.
computility-module-pay/src/main/java/com/luhu/computility/module/pay/framework/pay/core/client/impl/wpgj/WpgjCryptoUtils.java
0 → 100644
View file @
da69e2e2
package
com
.
luhu
.
computility
.
module
.
pay
.
framework
.
pay
.
core
.
client
.
impl
.
wpgj
;
import
cn.hutool.core.codec.Base64
;
import
cn.hutool.core.util.RandomUtil
;
import
cn.hutool.core.util.StrUtil
;
import
cn.hutool.crypto.Mode
;
import
cn.hutool.crypto.Padding
;
import
cn.hutool.crypto.symmetric.AES
;
import
cn.hutool.json.JSONObject
;
import
cn.hutool.json.JSONUtil
;
import
lombok.extern.slf4j.Slf4j
;
import
javax.crypto.Cipher
;
import
java.nio.charset.StandardCharsets
;
import
java.security.KeyFactory
;
import
java.security.PrivateKey
;
import
java.security.PublicKey
;
import
java.security.spec.PKCS8EncodedKeySpec
;
import
java.security.spec.X509EncodedKeySpec
;
import
java.time.LocalDateTime
;
import
java.time.format.DateTimeFormatter
;
/**
* 旺铺聚合支付加解密工具类
*
* 支持RSA+AES混合加密
* - AES加密请求数据
* - RSA加密AES密钥
*
* @author generated
*/
@Slf4j
public
class
WpgjCryptoUtils
{
private
static
final
String
AES_ALGORITHM
=
"AES"
;
private
static
final
String
RSA_ALGORITHM
=
"RSA"
;
private
static
final
String
RSA_TRANSFORMATION
=
"RSA/ECB/PKCS1Padding"
;
/**
* 生成随机AES密钥(16位)
*
* @return AES密钥
*/
public
static
String
generateAESKey
()
{
return
RandomUtil
.
randomString
(
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
,
16
);
}
/**
* AES加密
*
* @param data 明文数据
* @param aesKey AES密钥
* @return Base64编码的密文
*/
public
static
String
aesEncrypt
(
String
data
,
String
aesKey
)
{
try
{
AES
aes
=
new
AES
(
Mode
.
ECB
,
Padding
.
PKCS5Padding
,
aesKey
.
getBytes
(
StandardCharsets
.
UTF_8
));
byte
[]
encrypted
=
aes
.
encrypt
(
data
.
getBytes
(
StandardCharsets
.
UTF_8
));
return
Base64
.
encode
(
encrypted
);
}
catch
(
Exception
e
)
{
log
.
error
(
"[aesEncrypt] AES加密失败: {}"
,
e
.
getMessage
(),
e
);
throw
new
RuntimeException
(
"AES加密失败"
,
e
);
}
}
/**
* AES解密
*
* @param encryptedData Base64编码的密文
* @param aesKey AES密钥
* @return 明文数据
*/
public
static
String
aesDecrypt
(
String
encryptedData
,
String
aesKey
)
{
try
{
AES
aes
=
new
AES
(
Mode
.
ECB
,
Padding
.
PKCS5Padding
,
aesKey
.
getBytes
(
StandardCharsets
.
UTF_8
));
byte
[]
encrypted
=
Base64
.
decode
(
encryptedData
);
byte
[]
decrypted
=
aes
.
decrypt
(
encrypted
);
return
new
String
(
decrypted
,
StandardCharsets
.
UTF_8
);
}
catch
(
Exception
e
)
{
log
.
error
(
"[aesDecrypt] AES解密失败: {}"
,
e
.
getMessage
(),
e
);
throw
new
RuntimeException
(
"AES解密失败"
,
e
);
}
}
/**
* RSA公钥加密
*
* @param data 要加密的数据
* @param publicKey RSA公钥
* @return Base64编码的密文
*/
public
static
String
rsaEncrypt
(
String
data
,
String
publicKey
)
{
try
{
// 移除公钥格式
String
publicKeyStr
=
publicKey
.
replace
(
"-----BEGIN PUBLIC KEY-----"
,
""
)
.
replace
(
"-----END PUBLIC KEY-----"
,
""
)
.
replaceAll
(
"\\s"
,
""
);
byte
[]
keyBytes
=
Base64
.
decode
(
publicKeyStr
);
X509EncodedKeySpec
spec
=
new
X509EncodedKeySpec
(
keyBytes
);
KeyFactory
keyFactory
=
KeyFactory
.
getInstance
(
RSA_ALGORITHM
);
PublicKey
pubKey
=
keyFactory
.
generatePublic
(
spec
);
Cipher
cipher
=
Cipher
.
getInstance
(
RSA_TRANSFORMATION
);
cipher
.
init
(
Cipher
.
ENCRYPT_MODE
,
pubKey
);
byte
[]
encrypted
=
cipher
.
doFinal
(
data
.
getBytes
(
StandardCharsets
.
UTF_8
));
return
Base64
.
encode
(
encrypted
);
}
catch
(
Exception
e
)
{
log
.
error
(
"[rsaEncrypt] RSA加密失败: {}"
,
e
.
getMessage
(),
e
);
throw
new
RuntimeException
(
"RSA加密失败"
,
e
);
}
}
/**
* RSA私钥解密
*
* @param encryptedData Base64编码的密文
* @param privateKey RSA私钥
* @return 明文数据
*/
public
static
String
rsaDecrypt
(
String
encryptedData
,
String
privateKey
)
{
try
{
// 移除私钥格式
String
privateKeyStr
=
privateKey
.
replace
(
"-----BEGIN PRIVATE KEY-----"
,
""
)
.
replace
(
"-----END PRIVATE KEY-----"
,
""
)
.
replaceAll
(
"\\s"
,
""
);
byte
[]
keyBytes
=
Base64
.
decode
(
privateKeyStr
);
PKCS8EncodedKeySpec
spec
=
new
PKCS8EncodedKeySpec
(
keyBytes
);
KeyFactory
keyFactory
=
KeyFactory
.
getInstance
(
RSA_ALGORITHM
);
PrivateKey
privKey
=
keyFactory
.
generatePrivate
(
spec
);
Cipher
cipher
=
Cipher
.
getInstance
(
RSA_TRANSFORMATION
);
cipher
.
init
(
Cipher
.
DECRYPT_MODE
,
privKey
);
byte
[]
encrypted
=
Base64
.
decode
(
encryptedData
);
byte
[]
decrypted
=
cipher
.
doFinal
(
encrypted
);
return
new
String
(
decrypted
,
StandardCharsets
.
UTF_8
);
}
catch
(
Exception
e
)
{
log
.
error
(
"[rsaDecrypt] RSA解密失败: {}"
,
e
.
getMessage
(),
e
);
throw
new
RuntimeException
(
"RSA解密失败"
,
e
);
}
}
/**
* 构建请求数据
*
* @param data 业务数据
* @param organizNo 机构号
* @return 完整的请求数据
*/
public
static
String
buildRequestData
(
JSONObject
data
,
String
organizNo
)
{
JSONObject
request
=
new
JSONObject
();
request
.
set
(
"serialNo"
,
generateSerialNo
());
request
.
set
(
"version"
,
"1.0"
);
request
.
set
(
"timestamp"
,
generateTimestamp
());
request
.
set
(
"data"
,
data
);
request
.
set
(
"signature"
,
null
);
request
.
set
(
"extras"
,
null
);
request
.
set
(
"organizNo"
,
organizNo
);
return
JSONUtil
.
toJsonStr
(
request
);
}
/**
* 加密请求数据
*
* @param requestData 请求数据
* @param publicKey 旺铺公钥
* @return 加密后的请求数据
*/
public
static
String
encryptRequest
(
String
requestData
,
String
publicKey
)
{
// 生成随机AES密钥
String
aesKey
=
generateAESKey
();
// AES加密请求数据
JSONObject
requestJson
=
JSONUtil
.
parseObj
(
requestData
);
String
dataToEncrypt
=
JSONUtil
.
toJsonStr
(
requestJson
.
get
(
"data"
));
String
encryptedData
=
aesEncrypt
(
dataToEncrypt
,
aesKey
);
// RSA加密AES密钥
String
encryptedAesKey
=
rsaEncrypt
(
aesKey
,
publicKey
);
// 组装最终请求数据
requestJson
.
set
(
"data"
,
encryptedData
);
requestJson
.
set
(
"signature"
,
encryptedAesKey
);
return
JSONUtil
.
toJsonStr
(
requestJson
);
}
/**
* 解密响应数据
*
* @param responseData 响应数据
* @param privateKey 机构私钥
* @return 解密后的业务数据
*/
public
static
String
decryptResponse
(
String
responseData
,
String
privateKey
)
{
try
{
JSONObject
responseJson
=
JSONUtil
.
parseObj
(
responseData
);
// 获取加密的AES密钥
String
encryptedAesKey
=
responseJson
.
getStr
(
"signature"
);
if
(
StrUtil
.
isBlank
(
encryptedAesKey
))
{
throw
new
RuntimeException
(
"响应中缺少加密的AES密钥"
);
}
// RSA解密得到AES密钥
String
aesKey
=
rsaDecrypt
(
encryptedAesKey
,
privateKey
);
// 获取加密的业务数据
String
encryptedData
=
responseJson
.
getStr
(
"data"
);
if
(
StrUtil
.
isBlank
(
encryptedData
))
{
throw
new
RuntimeException
(
"响应中缺少加密的业务数据"
);
}
// AES解密得到业务数据
return
aesDecrypt
(
encryptedData
,
aesKey
);
}
catch
(
Exception
e
)
{
log
.
error
(
"[decryptResponse] 解密响应数据失败: {}"
,
e
.
getMessage
(),
e
);
throw
new
RuntimeException
(
"解密响应数据失败"
,
e
);
}
}
/**
* 生成序列号
*/
private
static
String
generateSerialNo
()
{
return
System
.
currentTimeMillis
()
+
RandomUtil
.
randomNumbers
(
6
);
}
/**
* 生成时间戳
*/
private
static
String
generateTimestamp
()
{
return
LocalDateTime
.
now
().
format
(
DateTimeFormatter
.
ofPattern
(
"yyyyMMddHHmmssSSS"
));
}
}
\ No newline at end of file
computility-module-pay/src/main/java/com/luhu/computility/module/pay/framework/pay/core/client/impl/wpgj/WpgjPayClient.java
0 → 100644
View file @
da69e2e2
package
com
.
luhu
.
computility
.
module
.
pay
.
framework
.
pay
.
core
.
client
.
impl
.
wpgj
;
import
cn.hutool.core.util.StrUtil
;
import
cn.hutool.http.HttpResponse
;
import
cn.hutool.http.HttpUtil
;
import
cn.hutool.json.JSONObject
;
import
cn.hutool.json.JSONUtil
;
import
com.luhu.computility.module.pay.enums.PayChannelEnum
;
import
com.luhu.computility.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO
;
import
com.luhu.computility.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO
;
import
com.luhu.computility.module.pay.framework.pay.core.client.impl.AbstractPayClient
;
import
com.luhu.computility.module.pay.enums.order.PayOrderStatusEnum
;
import
com.luhu.computility.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum
;
import
lombok.extern.slf4j.Slf4j
;
import
java.math.BigDecimal
;
import
java.time.LocalDateTime
;
import
java.time.format.DateTimeFormatter
;
import
java.util.HashMap
;
import
java.util.Map
;
/**
* 旺铺聚合支付的 PayClient 实现类
*
* @author generated
*/
@Slf4j
public
class
WpgjPayClient
extends
AbstractPayClient
<
WpgjPayClientConfig
>
{
public
WpgjPayClient
(
Long
channelId
,
String
channelCode
,
WpgjPayClientConfig
config
)
{
super
(
channelId
,
PayChannelEnum
.
WX_NATIVE
.
getCode
(),
config
);
}
@Override
protected
void
doInit
()
{
// 旺铺支付不需要特殊初始化
}
@Override
protected
PayOrderRespDTO
doUnifiedOrder
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
Throwable
{
try
{
// 构建业务数据
JSONObject
businessData
=
buildBusinessData
(
reqDTO
);
// 构建完整请求数据
String
requestData
=
WpgjCryptoUtils
.
buildRequestData
(
businessData
,
config
.
getOrganizNo
());
// 加密请求数据
String
encryptedRequest
=
WpgjCryptoUtils
.
encryptRequest
(
requestData
,
config
.
getPublicKey
());
// 发送HTTP请求
String
apiUrl
=
config
.
getApiUrl
()
+
"/industrial/payment/dynamic"
;
HttpResponse
response
=
HttpUtil
.
createPost
(
apiUrl
)
.
body
(
encryptedRequest
)
.
contentType
(
"application/json"
)
.
timeout
(
30000
)
.
execute
();
if
(!
response
.
isOk
())
{
log
.
error
(
"[doUnifiedOrder] 旺铺支付请求失败: status={}, body={}"
,
response
.
getStatus
(),
response
.
body
());
return
PayOrderRespDTO
.
closedOf
(
"HTTP_ERROR"
,
"请求失败: "
+
response
.
getStatus
(),
reqDTO
.
getOutTradeNo
(),
response
.
body
());
}
// 解析响应
String
responseBody
=
response
.
body
();
JSONObject
responseJson
=
JSONUtil
.
parseObj
(
responseBody
);
// 检查响应状态
String
code
=
responseJson
.
getStr
(
"code"
);
if
(!
"0000"
.
equals
(
code
))
{
String
msg
=
responseJson
.
getStr
(
"msg"
,
"未知错误"
);
log
.
error
(
"[doUnifiedOrder] 旺铺支付返回错误: code={}, msg={}"
,
code
,
msg
);
return
PayOrderRespDTO
.
closedOf
(
code
,
msg
,
reqDTO
.
getOutTradeNo
(),
responseBody
);
}
// 解密响应数据获取payUrl
String
decryptedData
=
WpgjCryptoUtils
.
decryptResponse
(
responseBody
,
config
.
getPrivateKey
());
JSONObject
dataJson
=
JSONUtil
.
parseObj
(
decryptedData
);
String
payUrl
=
dataJson
.
getStr
(
"payUrl"
);
if
(
StrUtil
.
isBlank
(
payUrl
))
{
log
.
error
(
"[doUnifiedOrder] 旺铺支付响应中缺少payUrl"
);
return
PayOrderRespDTO
.
closedOf
(
"MISSING_PAY_URL"
,
"响应中缺少支付链接"
,
reqDTO
.
getOutTradeNo
(),
responseBody
);
}
// 返回成功结果
Map
<
String
,
Object
>
rawData
=
new
HashMap
<>();
rawData
.
put
(
"serialNo"
,
responseJson
.
getStr
(
"serialNo"
));
rawData
.
put
(
"payUrl"
,
payUrl
);
rawData
.
put
(
"response"
,
responseBody
);
return
PayOrderRespDTO
.
waitingOf
(
PayOrderDisplayModeEnum
.
QR_CODE
.
getMode
(),
payUrl
,
reqDTO
.
getOutTradeNo
(),
rawData
);
}
catch
(
Exception
e
)
{
log
.
error
(
"[doUnifiedOrder] 旺铺支付统一下单异常"
,
e
);
return
PayOrderRespDTO
.
closedOf
(
"SYSTEM_ERROR"
,
e
.
getMessage
(),
reqDTO
.
getOutTradeNo
(),
e
.
getMessage
());
}
}
@Override
protected
PayOrderRespDTO
doParseOrderNotify
(
Map
<
String
,
String
>
params
,
String
body
,
Map
<
String
,
String
>
headers
)
throws
Throwable
{
try
{
// 解密回调数据
String
decryptedData
=
WpgjCryptoUtils
.
decryptResponse
(
body
,
config
.
getPrivateKey
());
JSONObject
dataJson
=
JSONUtil
.
parseObj
(
decryptedData
);
// 获取订单状态和相关信息
String
orderId
=
dataJson
.
getStr
(
"order_id"
);
String
orderStatus
=
dataJson
.
getStr
(
"order_status"
);
String
tradeNo
=
dataJson
.
getStr
(
"trade_no"
);
String
tradeTime
=
dataJson
.
getStr
(
"trade_time"
);
// 判断支付状态
Integer
status
;
if
(
"1"
.
equals
(
orderStatus
))
{
status
=
PayOrderStatusEnum
.
SUCCESS
.
getStatus
();
}
else
{
status
=
PayOrderStatusEnum
.
CLOSED
.
getStatus
();
}
// 解析交易时间
LocalDateTime
successTime
=
null
;
if
(
StrUtil
.
isNotBlank
(
tradeTime
))
{
try
{
successTime
=
LocalDateTime
.
parse
(
tradeTime
,
DateTimeFormatter
.
ofPattern
(
"yyyy-MM-dd HH:mm:ss"
));
}
catch
(
Exception
e
)
{
log
.
warn
(
"[doParseOrderNotify] 解析交易时间失败: {}"
,
tradeTime
,
e
);
}
}
// 构建原始数据
Map
<
String
,
Object
>
rawData
=
new
HashMap
<>();
rawData
.
put
(
"decryptedData"
,
decryptedData
);
rawData
.
put
(
"rawBody"
,
body
);
rawData
.
put
(
"dataJson"
,
dataJson
);
return
PayOrderRespDTO
.
of
(
status
,
tradeNo
,
null
,
successTime
,
orderId
,
rawData
);
}
catch
(
Exception
e
)
{
log
.
error
(
"[doParseOrderNotify] 解析旺铺支付回调异常"
,
e
);
throw
e
;
}
}
@Override
protected
PayOrderRespDTO
doGetOrder
(
String
outTradeNo
)
throws
Throwable
{
// 旺铺支付暂时不支持主动查询订单状态
// 可以根据需要实现订单查询接口
log
.
warn
(
"[doGetOrder] 旺铺支付暂不支持订单查询: {}"
,
outTradeNo
);
return
null
;
}
/**
* 构建业务数据
*
* @param reqDTO 统一下单请求
* @return 业务数据
*/
private
JSONObject
buildBusinessData
(
PayOrderUnifiedReqDTO
reqDTO
)
{
JSONObject
data
=
new
JSONObject
();
// 必填字段
data
.
set
(
"organiz_no"
,
config
.
getOrganizNo
());
data
.
set
(
"mer_no"
,
config
.
getMerNo
());
data
.
set
(
"mer_code"
,
config
.
getMerCode
());
data
.
set
(
"term_code"
,
config
.
getTermCode
());
// 金额转换:分转元
BigDecimal
amountYuan
=
new
BigDecimal
(
reqDTO
.
getPrice
()).
divide
(
new
BigDecimal
(
100
),
2
,
BigDecimal
.
ROUND_HALF_UP
);
data
.
set
(
"order_amt"
,
amountYuan
.
toString
());
// 订单信息
data
.
set
(
"mer_order_id"
,
reqDTO
.
getOutTradeNo
());
data
.
set
(
"notifyurl"
,
reqDTO
.
getNotifyUrl
());
// 可选字段
if
(
StrUtil
.
isNotBlank
(
reqDTO
.
getReturnUrl
()))
{
data
.
set
(
"return_url"
,
reqDTO
.
getReturnUrl
());
}
// 设置默认关闭时间30分钟
data
.
set
(
"close_time"
,
"30"
);
// 设置订单标题
String
subject
=
reqDTO
.
getSubject
();
if
(
StrUtil
.
isBlank
(
subject
))
{
subject
=
"商品支付"
;
}
data
.
set
(
"order_title"
,
subject
);
return
data
;
}
}
\ No newline at end of file
computility-module-pay/src/main/java/com/luhu/computility/module/pay/framework/pay/core/client/impl/wpgj/WpgjPayClientConfig.java
0 → 100644
View file @
da69e2e2
package
com
.
luhu
.
computility
.
module
.
pay
.
framework
.
pay
.
core
.
client
.
impl
.
wpgj
;
import
com.luhu.computility.framework.common.util.validation.ValidationUtils
;
import
com.luhu.computility.module.pay.framework.pay.core.client.PayClientConfig
;
import
lombok.Data
;
import
javax.validation.ConstraintValidator
;
import
javax.validation.constraints.NotBlank
;
/**
* 旺铺聚合支付的 PayClientConfig 实现类
*
* @author generated
*/
@Data
public
class
WpgjPayClientConfig
implements
PayClientConfig
{
/**
* 合作机构渠道号
* 测试环境:105549
*/
@NotBlank
(
message
=
"合作机构渠道号不能为空"
)
private
String
organizNo
;
/**
* 旺铺内部商户号,进件入网后返回
* 测试环境:99911325651RE1R
*/
@NotBlank
(
message
=
"旺铺内部商户号不能为空"
)
private
String
merNo
;
/**
* 商户号,进件入网后返回
* 测试环境:K20241200111267
*/
@NotBlank
(
message
=
"商户号不能为空"
)
private
String
merCode
;
/**
* 终端号,进件入网后返回
* 测试环境:1011215692596
*/
@NotBlank
(
message
=
"终端号不能为空"
)
private
String
termCode
;
/**
* 旺铺公钥,用于加密AES密钥
*/
@NotBlank
(
message
=
"旺铺公钥不能为空"
)
private
String
publicKey
;
/**
* 机构私钥,用于解密回调数据中的AES密钥
*/
@NotBlank
(
message
=
"机构私钥不能为空"
)
private
String
privateKey
;
/**
* API地址
* 测试环境:https://stg5-qr.wpgjcs.com
*/
@NotBlank
(
message
=
"API地址不能为空"
)
private
String
apiUrl
;
@Override
public
void
validate
(
ConstraintValidator
validator
)
{
ValidationUtils
.
validate
(
validator
,
this
);
}
}
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment