package cc.mrbird.febs.pay.service;
import cc.mrbird.febs.common.enumerates.OrderStatusEnum;
import cc.mrbird.febs.common.utils.RedisUtils;
import cc.mrbird.febs.mall.controller.dependentStation.constant.OrderConstants;
import cc.mrbird.febs.mall.entity.DataDictionaryCustom;
import cc.mrbird.febs.mall.entity.MallOrderInfo;
import cc.mrbird.febs.mall.mapper.DataDictionaryCustomMapper;
import cc.mrbird.febs.mall.mapper.MallOrderInfoMapper;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Date;
import java.util.Map;
import java.util.TreeMap;
/**
* LWPAY 支付服务
*
* 处理 LWPAY 代收(创建支付订单)和回调通知
*
* LWPAY API 基础说明:
* - 签名算法:参数按 ASCII 字典序排序 → key=value 拼接 → 末尾追加 &key={secret} → MD5 → 大写
* - 代收接口:POST /Pay_Index (application/x-www-form-urlencoded)
* - 支付回调:application/x-www-form-urlencoded,收到后需响应 "OK"
*
* @author auto-generated
*/
@Slf4j
@Service
public class LwPayService {
@Resource
private MallOrderInfoMapper mallOrderInfoMapper;
@Resource
private DataDictionaryCustomMapper dataDictionaryCustomMapper;
@Resource
private RedisUtils redisUtils;
/** LWPAY 配置字典 type */
private static final String DICT_TYPE = "LWPAY_CONFIG";
/** LWPAY API 基础地址 */
private static final String LWPAY_BASE_URL = "https://lwpay.live";
/**
* 参与签名的字段白名单
* 签名只包含这 7 个业务必传字段,pay_productname / pay_attach / pay_md5sign 不参与签名
*/
private static final String[] SIGN_FIELD_KEYS = {
"pay_memberid", "pay_orderid", "pay_applydate",
"pay_bankcode", "pay_notifyurl", "pay_callbackurl", "pay_amount"
};
// ==================== 代收接口 ====================
/**
* 代收下单 → 获取 LWPAY 支付跳转 URL
*
* @param order 订单实体
* @param bankCode 银行编码(如 968 对应 USDT-TRC20)
* @param network 网络链(如 TRX,仅 bankCode=968 时需要)
* @return LWPAY 支付页面 URL
*/
public String createPayment(MallOrderInfo order, String bankCode, String network) {
String memberId = getConfigValue("MEMBER_ID");
String secretKey = getConfigValue("SECRET_KEY");
if (StrUtil.isBlank(memberId) || StrUtil.isBlank(secretKey)) {
throw new RuntimeException("LWPAY 商户号/秘钥未配置");
}
TreeMap params = new TreeMap<>();
params.put("pay_memberid", memberId);
params.put("pay_orderid", order.getOrderNo());
params.put("pay_applydate", cn.hutool.core.date.DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
params.put("pay_bankcode", bankCode);
params.put("pay_notifyurl", getNotifyUrl());
params.put("pay_callbackurl", getReturnUrl());
params.put("pay_amount", order.getAmount().toPlainString());
params.put("pay_productname", "商品订单-" + order.getOrderNo());
// 通道 968 需要 network 参数
if ("968".equals(bankCode) && StrUtil.isNotBlank(network)) {
params.put("pay_attach", "network:" + network);
}
// 签名:仅使用白名单中的 7 个业务必传字段
TreeMap signParams = new TreeMap<>();
for (String key : SIGN_FIELD_KEYS) {
String val = params.get(key);
if (StrUtil.isNotBlank(val)) {
signParams.put(key, val);
}
}
String sign = generateSign(signParams, secretKey);
params.put("pay_md5sign", sign);
log.info("LWPAY 代收请求: memberId={}, orderNo={}, amount={}, bankCode={}",
memberId, order.getOrderNo(), order.getAmount(), bankCode);
// 发送 POST 请求
String response = postForm(LWPAY_BASE_URL + "/Pay_Index", params);
log.info("LWPAY 代收响应: {}", response);
if (StrUtil.isBlank(response)) {
throw new RuntimeException("LWPAY 响应为空");
}
// 解析响应 JSON: {"status": "1", "msg": "success", "payurl": "https://..."}
com.alibaba.fastjson.JSONObject json = com.alibaba.fastjson.JSONObject.parseObject(response);
if (!"1".equals(json.getString("status"))) {
throw new RuntimeException("LWPAY 代收失败: " + json.getString("msg"));
}
return json.getString("payurl");
}
// ==================== 回调处理 ====================
/**
* 处理 LWPAY 支付结果通知
*
* @param params 回调参数 Map
* @return true-处理成功, false-处理失败
*/
public boolean handleCallback(Map params) {
String secretKey = getConfigValue("SECRET_KEY");
if (StrUtil.isBlank(secretKey)) {
log.error("LWPAY 秘钥未配置,无法验签");
return false;
}
// 1. 验证签名
if (!verifySign(params, secretKey)) {
log.error("LWPAY 回调签名验证失败: params={}", params);
return false;
}
String returncode = params.get("returncode");
String orderId = params.get("orderid");
String amount = params.get("amount");
String transactionId = params.get("transaction_id");
String refCode = params.get("refCode");
log.info("LWPAY 回调: orderNo={}, returncode={}, refCode={}, amount={}, txid={}",
orderId, returncode, refCode, amount, transactionId);
// 2. 判断支付状态:"00" 且 refCode="1" 表示支付成功
if (!"00".equals(returncode) || !"1".equals(refCode)) {
log.warn("LWPAY 支付未成功: orderNo={}, returncode={}, refCode={}", orderId, returncode, refCode);
return true; // 非成功状态也返回 true,避免 LWPAY 重试
}
// 3. 查询订单
MallOrderInfo order = mallOrderInfoMapper.selectOne(
Wrappers.lambdaQuery(MallOrderInfo.class)
.eq(MallOrderInfo::getOrderNo, orderId)
);
if (order == null) {
log.error("LWPAY 回调订单不存在: orderNo={}", orderId);
return false;
}
// 4. 幂等检查(避免重复回调)
if (OrderStatusEnum.WAIT_PAY.getValue() != order.getStatus()) {
log.info("LWPAY 订单已处理: orderNo={}, status={}", orderId, order.getStatus());
return true;
}
// 5. 更新订单状态为待发货
mallOrderInfoMapper.update(
null,
Wrappers.lambdaUpdate(MallOrderInfo.class)
.set(MallOrderInfo::getStatus, OrderStatusEnum.WAIT_SHIPPING.getValue())
.set(MallOrderInfo::getTradeHash, transactionId)
.set(MallOrderInfo::getPayTime, new Date())
.set(MallOrderInfo::getPayMethod, "LWPAY支付")
.set(MallOrderInfo::getPayResult, "1")
.eq(MallOrderInfo::getId, order.getId())
);
// 清理 Redis 中的金额匹配 key(如果有)
String amountKey = OrderConstants.TRC20_ORDER_KEY + order.getAmount();
redisUtils.del(amountKey);
log.info("LWPAY 支付成功: orderNo={}, txid={}, amount={}", orderId, transactionId, amount);
return true;
}
// ==================== 签名工具 ====================
/**
* 生成 MD5 签名(大写)
*
* 1. 参数按 ASCII 字典序排序
* 2. key=value&key=value 拼接
* 3. 末尾追加 &key={secretKey}
* 4. MD5 → 大写
*/
public String generateSign(TreeMap params, String secretKey) {
String signStr = buildSignString(params) + "&key=" + secretKey;
log.info("LWPAY 待签名字符串: {}", signStr);
return SecureUtil.md5(signStr).toUpperCase();
}
/**
* 验证回调签名
*/
public boolean verifySign(Map params, String secretKey) {
String receivedSign = params.get("sign");
if (StrUtil.isBlank(receivedSign)) {
return false;
}
// 剔除 sign 字段本身,构建签名字符串
TreeMap signParams = new TreeMap<>();
for (Map.Entry entry : params.entrySet()) {
if (!"sign".equals(entry.getKey()) && StrUtil.isNotBlank(entry.getValue())) {
signParams.put(entry.getKey(), entry.getValue());
}
}
String expectedSign = generateSign(signParams, secretKey);
boolean valid = expectedSign.equalsIgnoreCase(receivedSign);
if (!valid) {
log.warn("LWPAY 签名不匹配: expected={}, received={}", expectedSign, receivedSign);
}
return valid;
}
/**
* 将参数拼接为签名字符串(不含 &key=)
* key1=value1&key2=value2&...
*/
private String buildSignString(TreeMap params) {
StringBuilder sb = new StringBuilder();
for (Map.Entry entry : params.entrySet()) {
if (StrUtil.isNotBlank(entry.getValue())) {
if (sb.length() > 0) {
sb.append("&");
}
sb.append(entry.getKey()).append("=").append(entry.getValue());
}
}
return sb.toString();
}
// ==================== 工具方法 ====================
/**
* 从数据字典获取 LWPAY 配置
*/
public String getConfigValue(String code) {
DataDictionaryCustom dict = dataDictionaryCustomMapper.selectDicDataByTypeAndCode(DICT_TYPE, code);
return dict != null ? dict.getValue() : null;
}
/**
* 获取回调通知 URL
*/
private String getNotifyUrl() {
return getConfigValue("NOTIFY_URL");
}
/**
* 获取页面跳转 URL
*/
private String getReturnUrl() {
return getConfigValue("RETURN_URL");
}
/**
* POST application/x-www-form-urlencoded 请求
*/
private String postForm(String url, Map params) {
try {
// 转换为 String[] 格式(OkHttpUtil2.doPost 接口要求)
Map body = new java.util.HashMap<>();
for (Map.Entry entry : params.entrySet()) {
body.put(entry.getKey(), new String[]{entry.getValue()});
}
byte[] bytes = cc.mrbird.febs.mall.controller.dependentStation.utils.OkHttpUtil2
.doPost(url, null, body, "application/json");
if (bytes != null) {
return new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
}
return null;
} catch (Exception e) {
log.error("LWPAY POST 请求异常: url={}", url, e);
return null;
}
}
}