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.math.BigDecimal; import java.math.RoundingMode; 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", getBRLAmount(order.getAmount())); 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"); } private String getBRLAmount(BigDecimal amount){ DataDictionaryCustom dataDictionaryCustom = dataDictionaryCustomMapper.selectDicDataByTypeAndCode( "MONEY_CHANGE", "BRL" ); BigDecimal rate = new BigDecimal("5.18"); if (dataDictionaryCustom != null){ rate = new BigDecimal(dataDictionaryCustom.getValue()); } return amount.multiply( rate).setScale(2, RoundingMode.DOWN).toString(); } // ==================== 回调处理 ==================== /** * 处理 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; } } }