rk
15 小时以前 ab9cd2c82bd64de8e33510db1d1e78a5b3b4de70
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
package com.doumee.config.wx;
 
import com.doumee.core.constants.ResponseStatus;
import com.doumee.core.exception.BusinessException;
import com.doumee.core.utils.ID;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse;
import com.wechat.pay.java.service.refund.model.AmountReq;
import com.wechat.pay.java.service.refund.model.CreateRequest;
import com.wechat.pay.java.service.refund.model.Refund;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import java.util.HashMap;
import java.util.Map;
 
/**
 * 微信支付V3版本服务(JSAPI下单 + 退款 + 回调验签)
 *
 * @author rk
 * @date 2026/04/20
 */
@Service
@Slf4j
public class WxPayV3Service {
 
    @Autowired
    private WxPayProperties wxPayProperties;
 
    /**
     * JSAPI下单,返回前端调起支付所需的参数
     *
     * @param outTradeNo  商户订单号
     * @param description 商品描述
     * @param totalCents  金额(分)
     * @param openid      用户openid
     * @param notifyUrl   支付回调地址
     * @param attach      附加数据(回调时原样返回,用于路由不同业务)
     * @return 前端调起支付参数(appId, timeStamp, nonceStr, package, signType, paySign)
     */
    public Map<String, String> createOrder(String outTradeNo, String description,
                                            Long totalCents, String openid,
                                            String notifyUrl, String attach) {
        if (WxMiniConfig.v3JsapiService == null) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "微信支付V3未初始化");
        }
        try {
            PrepayRequest request = new PrepayRequest();
            request.setAppid(wxPayProperties.getAppId());
            request.setMchid(wxPayProperties.getMchId());
            request.setDescription(description);
            request.setOutTradeNo(outTradeNo);
            request.setNotifyUrl(notifyUrl);
            if (attach != null && !attach.isEmpty()) {
                request.setAttach(attach);
            }
 
            com.wechat.pay.java.service.payments.jsapi.model.Amount amount =
                    new com.wechat.pay.java.service.payments.jsapi.model.Amount();
            amount.setTotal(1);//totalCents.intValue());
            amount.setCurrency("CNY");
            request.setAmount(amount);
 
            com.wechat.pay.java.service.payments.jsapi.model.Payer payer =
                    new com.wechat.pay.java.service.payments.jsapi.model.Payer();
            payer.setOpenid(openid);
            request.setPayer(payer);
 
            PrepayWithRequestPaymentResponse response =
                    WxMiniConfig.v3JsapiService.prepayWithRequestPayment(request);
 
            Map<String, String> result = new HashMap<>();
            result.put("appId", response.getAppId());
            result.put("timeStamp", response.getTimeStamp());
            result.put("nonceStr", response.getNonceStr());
            result.put("package", response.getPackageVal());
            result.put("signType", response.getSignType());
            result.put("paySign", response.getPaySign());
            return result;
        } catch (Exception e) {
            log.error("微信支付V3下单失败: {}", e.getMessage(), e);
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "支付调起失败:" + e.getMessage());
        }
    }
 
    /**
     * V3退款
     *
     * @param outTradeNo   商户订单号
     * @param totalCents   原订单金额(分)
     * @param refundCents  退款金额(分)
     * @param reason       退款原因
     * @param notifyUrl    退款回调地址
     * @return Refund 退款结果(包含 outRefundNo、status 等)
     */
    public Refund refund(String outTradeNo, Long totalCents, Long refundCents,
                         String reason, String notifyUrl) {
        if (WxMiniConfig.v3RefundService == null) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "微信支付V3未初始化");
        }
        try {
            String outRefundNo = ID.nextGUID();
 
            CreateRequest request = new CreateRequest();
            request.setOutTradeNo(outTradeNo);
            request.setOutRefundNo(outRefundNo);
            request.setReason(reason);
            request.setNotifyUrl(notifyUrl);
 
            AmountReq amount = new AmountReq();
            amount.setRefund(1L);//refundCents);
            amount.setTotal(1L);//totalCents);
            amount.setCurrency("CNY");
            request.setAmount(amount);
 
            Refund result = WxMiniConfig.v3RefundService.create(request);
            log.info("微信支付V3退款申请结果, outTradeNo={}, outRefundNo={}, status={}",
                    outTradeNo, outRefundNo, result.getStatus());
            return result;
        } catch (Exception e) {
            log.error("微信支付V3退款失败: {}", e.getMessage(), e);
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "退款失败:" + e.getMessage());
        }
    }
 
    /**
     * 解析V3支付回调通知
     *
     * @param serialNumber  证书序列号(Wechatpay-Serial header)
     * @param timestamp     时间戳(Wechatpay-Timestamp header)
     * @param nonce         随机串(Wechatpay-Nonce header)
     * @param signature     签名(Wechatpay-Signature header)
     * @param body          请求体JSON
     * @return 解析后的Transaction对象
     */
    public com.wechat.pay.java.service.payments.model.Transaction parsePayNotify(
            String serialNumber, String timestamp, String nonce,
            String signature, String body) {
        try {
            RequestParam requestParam = new RequestParam.Builder()
                    .serialNumber(serialNumber)
                    .timestamp(timestamp)
                    .nonce(nonce)
                    .signature(signature)
                    .body(body)
                    .build();
 
            return WxMiniConfig.v3NotificationParser.parse(requestParam,
                    com.wechat.pay.java.service.payments.model.Transaction.class);
        } catch (Exception e) {
            log.error("微信支付V3回调验签失败: {}", e.getMessage(), e);
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "回调验签失败");
        }
    }
 
    /**
     * 解析V3退款回调通知
     *
     * @param serialNumber  证书序列号
     * @param timestamp     时间戳
     * @param nonce         随机串
     * @param signature     签名
     * @param body          请求体JSON
     * @return 解析后的RefundNotification对象
     */
    public com.wechat.pay.java.service.refund.model.RefundNotification parseRefundNotify(
            String serialNumber, String timestamp, String nonce,
            String signature, String body) {
        try {
            RequestParam requestParam = new RequestParam.Builder()
                    .serialNumber(serialNumber)
                    .timestamp(timestamp)
                    .nonce(nonce)
                    .signature(signature)
                    .body(body)
                    .build();
            return WxMiniConfig.v3NotificationParser.parse(requestParam,
                    com.wechat.pay.java.service.refund.model.RefundNotification.class);
        } catch (Exception e) {
            log.error("微信支付V3退款回调验签失败: {}", e.getMessage(), e);
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "退款回调验签失败");
        }
    }
}