From 10a1a391561ca1e85599ea272fd209e8bd57584d Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Mon, 22 Jun 2026 17:33:09 +0800
Subject: [PATCH] feat(mall): 集成 LWPAY 支付功能

---
 src/main/java/cc/mrbird/febs/mall/controller/dependentStation/TokenviewWebhookController.java |   29 +++
 src/main/java/cc/mrbird/febs/mall/controller/dependentStation/ApiMallOrderController.java     |   90 +++++++++++
 src/main/java/cc/mrbird/febs/mall/dto/LwPayCreateOrderDto.java                                |   31 +++
 src/main/java/cc/mrbird/febs/pay/service/LwPayService.java                                    |  281 +++++++++++++++++++++++++++++++++++
 src/main/java/cc/mrbird/febs/common/configure/WebMvcConfigure.java                            |    2 
 sql/xc_mall_pay_config.sql                                                                    |   30 +++
 6 files changed, 458 insertions(+), 5 deletions(-)

diff --git a/sql/xc_mall_pay_config.sql b/sql/xc_mall_pay_config.sql
new file mode 100644
index 0000000..ec91d39
--- /dev/null
+++ b/sql/xc_mall_pay_config.sql
@@ -0,0 +1,30 @@
+-- ============================================================
+-- 支付 & Webhook 配置数据
+-- 插入 data_dictionary_custom 表
+-- ============================================================
+
+-- ----------------------------
+-- 1. LWPAY 支付配置
+-- ----------------------------
+INSERT INTO data_dictionary_custom (REVISION, CREATED_BY, CREATED_TIME, UPDATED_BY, UPDATED_TIME, type, code, value, description)
+VALUES
+(1, 'system', NOW(), 'system', NOW(), 'LWPAY_CONFIG', 'MEMBER_ID',   '请替换为商户号',      'LWPAY商户号'),
+(1, 'system', NOW(), 'system', NOW(), 'LWPAY_CONFIG', 'SECRET_KEY',  '请替换为签名密钥',     'LWPAY签名密钥'),
+(1, 'system', NOW(), 'system', NOW(), 'LWPAY_CONFIG', 'NOTIFY_URL',  'https://your-domain/api/order/lwPayNotify',  'LWPAY异步回调地址'),
+(1, 'system', NOW(), 'system', NOW(), 'LWPAY_CONFIG', 'RETURN_URL',  'https://your-domain/api/order/lwPayReturn',  'LWPAY支付完成跳转地址');
+
+-- ----------------------------
+-- 2. Tokenview Webhook 配置(签名密钥)
+-- ----------------------------
+INSERT INTO data_dictionary_custom (REVISION, CREATED_BY, CREATED_TIME, UPDATED_BY, UPDATED_TIME, type, code, value, description)
+VALUES
+(1, 'system', NOW(), 'system', NOW(), 'TOKENVIEW_CONFIG', 'WEBHOOK_SECRET', 'dd12521274e434115df5c4277755839766349007fb57936d9d5be0a7a4f0e42f', 'Tokenview Webhook HMAC-SHA256 签名密钥'),
+(1, 'system', NOW(), 'system', NOW(), 'TOKENVIEW_CONFIG', 'TRC20_USDT_ADDRESS', '请替换为TRC20收款地址', 'TRC20 USDT 收款地址');
+
+-- ----------------------------
+-- 说明:
+-- 1. MEMBER_ID / SECRET_KEY:从 LWPAY 商户后台获取
+-- 2. NOTIFY_URL / RETURN_URL:替换 your-domain 为实际域名
+-- 3. WEBHOOK_SECRET:需与 Tokenview 后台配置的 Secret Key 一致
+-- 4. TRC20_USDT_ADDRESS:系统接收 USDT 的 TRC20 钱包地址(同时用于 SALES_SERVICE/TRC_ADDRESS)
+-- ============================================================
diff --git a/src/main/java/cc/mrbird/febs/common/configure/WebMvcConfigure.java b/src/main/java/cc/mrbird/febs/common/configure/WebMvcConfigure.java
index d7d19e8..b11da8f 100644
--- a/src/main/java/cc/mrbird/febs/common/configure/WebMvcConfigure.java
+++ b/src/main/java/cc/mrbird/febs/common/configure/WebMvcConfigure.java
@@ -37,6 +37,8 @@
         registration.excludePathPatterns("/api/fuPayReturn/payment/callback");
         registration.excludePathPatterns("/api/fuPay/notify");
         registration.excludePathPatterns("/api/tokenview/**");
+        registration.excludePathPatterns("/api/order/lwPayNotify");
+        registration.excludePathPatterns("/api/order/lwPayReturn");
 
         // 添加Swagger UI相关路径
         registration.excludePathPatterns("/api/swagger-ui.html");
diff --git a/src/main/java/cc/mrbird/febs/mall/controller/dependentStation/ApiMallOrderController.java b/src/main/java/cc/mrbird/febs/mall/controller/dependentStation/ApiMallOrderController.java
index 2417866..6eb09d3 100644
--- a/src/main/java/cc/mrbird/febs/mall/controller/dependentStation/ApiMallOrderController.java
+++ b/src/main/java/cc/mrbird/febs/mall/controller/dependentStation/ApiMallOrderController.java
@@ -4,11 +4,13 @@
 import cc.mrbird.febs.common.entity.FebsResponse;
 import cc.mrbird.febs.common.entity.LimitType;
 import cc.mrbird.febs.mall.dto.*;
+import cc.mrbird.febs.mall.entity.MallOrderInfo;
 import cc.mrbird.febs.mall.service.IApiMallOrderInfoService;
 import cc.mrbird.febs.mall.vo.ApiOrderPayVo;
 import cc.mrbird.febs.mall.vo.OrderDetailVo;
 import cc.mrbird.febs.mall.vo.OrderListVo;
 import cc.mrbird.febs.pay.service.IXcxPayService;
+import cc.mrbird.febs.pay.service.LwPayService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiResponse;
@@ -18,6 +20,8 @@
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
+import javax.servlet.http.HttpServletRequest;
+import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -35,6 +39,7 @@
 
     private final IApiMallOrderInfoService mallOrderInfoService;
     private final IXcxPayService iXcxPayService;
+    private final LwPayService lwPayService;
 
     @ApiOperation(value = "创建订单--验证是否允许创建", notes = "创建订单--验证是否允许创建")
     @PostMapping(value = "/createOrderVerify")
@@ -164,4 +169,89 @@
         return new FebsResponse().success().data(iXcxPayService.getTemplateId());
     }
 
+    // ==================== LWPAY 支付 ====================
+
+    /**
+     * 创建订单并通过 LWPAY 支付
+     * <p>
+     * 流程:创建订单 → 调 LWPAY 代收接口 → 返回支付 URL
+     * <p>
+     * 参数说明:
+     * - bankCode: 银行编码,968=USDT-TRC20
+     * - network:  网络链,TRX/ETH/MATIC/BSC/SOL/ARBEVM(bankCode=968 时必填)
+     */
+    @ApiOperation(value = "创建订单-LWPAY支付", notes = "创建订单并返回LWPAY支付URL")
+    @PostMapping(value = "/createOrderByLwPay")
+    @Limit(key = "createOrderByLwPay", period = 1, count = 1, name = "LWPAY下单", prefix = "limit", limitType = LimitType.IP)
+    public FebsResponse createOrderByLwPay(@RequestBody @Validated LwPayCreateOrderDto lwPayDto) {
+        // 1. 创建订单
+        AddOrderDto addOrderDto = lwPayDto.getOrder();
+        Long orderId = mallOrderInfoService.createOrder(addOrderDto);
+
+        // 2. 获取订单详情
+        MallOrderInfo order = mallOrderInfoService.getById(orderId);
+        if (order == null) {
+            return new FebsResponse().fail().message("订单创建失败");
+        }
+
+        // 3. 调用 LWPAY 代收接口
+        try {
+            String payUrl = lwPayService.createPayment(
+                    order,
+                    lwPayDto.getBankCode(),
+                    lwPayDto.getNetwork()
+            );
+
+            Map<String, Object> result = new HashMap<>();
+            result.put("orderNo", order.getOrderNo());
+            result.put("amount", order.getAmount());
+            result.put("payUrl", payUrl);
+            return new FebsResponse().success().data(result).message("下单成功");
+        } catch (Exception e) {
+            log.error("LWPAY 代收下单失败: orderId={}", orderId, e);
+            return new FebsResponse().fail().message("支付通道异常: " + e.getMessage());
+        }
+    }
+
+    /**
+     * LWPAY 支付结果通知(服务端回调)
+     * <p>
+     * LWPAY 在支付完成后会 POST 通知到此地址。
+     * 需在 LWPAY 商户后台配置 pay_notifyurl 为此地址。
+     * <p>
+     * 注意:收到后必须响应 "OK",LWPAY 才会停止重试。
+     */
+    @ApiOperation(value = "LWPAY 支付回调", notes = "接收LWPAY异步通知")
+    @PostMapping(value = "/lwPayNotify")
+    public String lwPayNotify(HttpServletRequest request) {
+        // 解析 form 参数
+        Map<String, String> params = new HashMap<>();
+        Enumeration<String> paramNames = request.getParameterNames();
+        while (paramNames.hasMoreElements()) {
+            String name = paramNames.nextElement();
+            params.put(name, request.getParameter(name));
+        }
+
+        log.info("LWPAY 回调参数: {}", params);
+
+        boolean success = lwPayService.handleCallback(params);
+        return success ? "OK" : "FAIL";
+    }
+
+    /**
+     * LWPAY 支付结果页面跳转
+     * <p>
+     * 用户在 LWPAY 支付页面完成后跳回此地址。
+     * 需在 LWPAY 商户后台配置 pay_callbackurl 为此地址。
+     */
+    @ApiOperation(value = "LWPAY 支付跳转", notes = "LWPAY支付完成后的页面跳转")
+    @GetMapping(value = "/lwPayReturn")
+    public String lwPayReturn(HttpServletRequest request) {
+        // 简单跳转到前端结果页
+        String orderId = request.getParameter("orderid");
+        String returncode = request.getParameter("returncode");
+        log.info("LWPAY 页面跳转: orderNo={}, returncode={}", orderId, returncode);
+        return "redirect:/pages/payResult?orderNo=" + orderId + "&code=" + returncode;
+    }
+
 }
diff --git a/src/main/java/cc/mrbird/febs/mall/controller/dependentStation/TokenviewWebhookController.java b/src/main/java/cc/mrbird/febs/mall/controller/dependentStation/TokenviewWebhookController.java
index 6335745..39a862e 100644
--- a/src/main/java/cc/mrbird/febs/mall/controller/dependentStation/TokenviewWebhookController.java
+++ b/src/main/java/cc/mrbird/febs/mall/controller/dependentStation/TokenviewWebhookController.java
@@ -1,5 +1,7 @@
 package cc.mrbird.febs.mall.controller.dependentStation;
 
+import cc.mrbird.febs.mall.entity.DataDictionaryCustom;
+import cc.mrbird.febs.mall.mapper.DataDictionaryCustomMapper;
 import cn.hutool.core.util.StrUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.TypeReference;
@@ -32,12 +34,28 @@
     @Resource
     private TokenviewWebhookService tokenviewWebhookService;
 
+    @Resource
+    private DataDictionaryCustomMapper dataDictionaryCustomMapper;
+
+    /** 默认 Webhook 签名密钥(数据库未配置时回退使用) */
+    private static final String DEFAULT_WEBHOOK_SECRET = "dd12521274e434115df5c4277755839766349007fb57936d9d5be0a7a4f0e42f";
+
+    /** Webhook 配置字典 type */
+    private static final String TOKENVIEW_DICT_TYPE = "TOKENVIEW_CONFIG";
+    private static final String TOKENVIEW_DICT_CODE = "WEBHOOK_SECRET";
+
     /**
-     * Webhook 签名密钥(HMAC-SHA256)
-     * 需与 Tokenview 后台配置的 Secret Key 保持一致
-     * TODO: 移至配置文件
+     * 获取 Webhook 签名密钥(HMAC-SHA256)
+     * 优先从数据库 data_dictionary_custom 读取,未配置则回退默认值
      */
-    private static final String WEBHOOK_SECRET = "dd12521274e434115df5c4277755839766349007fb57936d9d5be0a7a4f0e42f";
+    private String getWebhookSecret() {
+        DataDictionaryCustom dict = dataDictionaryCustomMapper.selectDicDataByTypeAndCode(
+                TOKENVIEW_DICT_TYPE, TOKENVIEW_DICT_CODE);
+        if (dict != null && StrUtil.isNotBlank(dict.getValue())) {
+            return dict.getValue();
+        }
+        return DEFAULT_WEBHOOK_SECRET;
+    }
 
     /**
      * 接收 Tokenview 地址监控推送
@@ -147,9 +165,10 @@
      */
     private boolean verifySignature(String payload, String signature) {
         try {
+            String secret = getWebhookSecret();
             Mac sha256Hmac = Mac.getInstance("HmacSHA256");
             SecretKeySpec secretKey = new SecretKeySpec(
-                    WEBHOOK_SECRET.getBytes(StandardCharsets.UTF_8),
+                    secret.getBytes(StandardCharsets.UTF_8),
                     "HmacSHA256"
             );
             sha256Hmac.init(secretKey);
diff --git a/src/main/java/cc/mrbird/febs/mall/dto/LwPayCreateOrderDto.java b/src/main/java/cc/mrbird/febs/mall/dto/LwPayCreateOrderDto.java
new file mode 100644
index 0000000..70832ec
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/mall/dto/LwPayCreateOrderDto.java
@@ -0,0 +1,31 @@
+package cc.mrbird.febs.mall.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+/**
+ * LWPAY 创建订单请求 DTO
+ *
+ * @author auto-generated
+ */
+@Data
+@ApiModel(value = "LwPayCreateOrderDto", description = "LWPAY支付下单参数")
+public class LwPayCreateOrderDto {
+
+    @Valid
+    @NotNull(message = "订单信息不能为空")
+    @ApiModelProperty(value = "订单信息", required = true)
+    private AddOrderDto order;
+
+    @NotBlank(message = "银行编码不能为空")
+    @ApiModelProperty(value = "银行编码:968=USDT-TRC20", example = "968", required = true)
+    private String bankCode;
+
+    @ApiModelProperty(value = "网络链(bankCode=968时必填):TRX/ETH/MATIC/BSC/SOL/ARBEVM", example = "TRX")
+    private String network;
+}
diff --git a/src/main/java/cc/mrbird/febs/pay/service/LwPayService.java b/src/main/java/cc/mrbird/febs/pay/service/LwPayService.java
new file mode 100644
index 0000000..512eef2
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/pay/service/LwPayService.java
@@ -0,0 +1,281 @@
+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 支付服务
+ * <p>
+ * 处理 LWPAY 代收(创建支付订单)和回调通知
+ * <p>
+ * 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://www.lwpay.com";
+
+    // ==================== 代收接口 ====================
+
+    /**
+     * 代收下单 → 获取 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<String, String> 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);
+        }
+
+        // 生成签名
+        String sign = generateSign(params, 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<String, String> 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 签名(大写)
+     * <p>
+     * 1. 参数按 ASCII 字典序排序
+     * 2. key=value&key=value 拼接
+     * 3. 末尾追加 &key={secretKey}
+     * 4. MD5 → 大写
+     */
+    public String generateSign(TreeMap<String, String> params, String secretKey) {
+        String signStr = buildSignString(params) + "&key=" + secretKey;
+        log.debug("LWPAY 待签名字符串: {}", signStr);
+        return SecureUtil.md5(signStr).toUpperCase();
+    }
+
+    /**
+     * 验证回调签名
+     */
+    public boolean verifySign(Map<String, String> params, String secretKey) {
+        String receivedSign = params.get("sign");
+        if (StrUtil.isBlank(receivedSign)) {
+            return false;
+        }
+
+        // 剔除 sign 字段本身,构建签名字符串
+        TreeMap<String, String> signParams = new TreeMap<>();
+        for (Map.Entry<String, String> 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<String, String> params) {
+        StringBuilder sb = new StringBuilder();
+        for (Map.Entry<String, String> 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<String, String> params) {
+        try {
+            // 转换为 String[] 格式(OkHttpUtil2.doPost 接口要求)
+            Map<String, String[]> body = new java.util.HashMap<>();
+            for (Map.Entry<String, String> 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;
+        }
+    }
+}

--
Gitblit v1.9.1