Administrator
6 days ago e55697ed07d7a99df3e64fd1624346a64a71dd20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
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://lwpay.live";
 
    // ==================== 代收接口 ====================
 
    /**
     * 代收下单 → 获取 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.info("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;
        }
    }
}