Files
junhong_cmp_fiber/pkg/wechat/payment_v2.go
huang 5b283285a2
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m11s
fix: 修复微信支付容器内 mkdir 权限错误,新增 wechat_v2 支付渠道
- NewPaymentAppFromConfig 加 Log.Stdout=true,禁止 PowerWeChat SDK 在容器
  工作目录创建 wechat/ 日志文件夹(Permission denied 根因)
- 新增 ProviderTypeWechatV2 = wechat_v2 常量,与 wechat(v3) 独立区分
- 新增 PaymentV2Service:直连统一下单 v2 接口,XML + MD5 签名
- newPaymentProvider 按 ProviderType 分支路由到对应版本
2026-03-31 12:12:13 +08:00

204 lines
5.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package wechat
import (
"context"
"crypto/md5"
"encoding/xml"
"fmt"
"io"
"net/http"
"sort"
"strings"
"time"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"go.uber.org/zap"
)
const wechatPayV2UnifiedOrderURL = "https://api.mch.weixin.qq.com/pay/unifiedorder"
// PaymentV2Service 微信支付 v2 服务
// 适用于仅配置 APIv2Key 的商户(无需 v3 证书序列号)
type PaymentV2Service struct {
appID string
mchID string
apiKey string
notifyURL string
logger *zap.Logger
}
// NewPaymentV2Service 创建微信支付 v2 服务
func NewPaymentV2Service(appID, mchID, apiKey, notifyURL string, logger *zap.Logger) *PaymentV2Service {
return &PaymentV2Service{
appID: appID,
mchID: mchID,
apiKey: apiKey,
notifyURL: notifyURL,
logger: logger,
}
}
// v2UnifiedOrderRequest 统一下单请求v2 XML 格式)
type v2UnifiedOrderRequest struct {
XMLName xml.Name `xml:"xml"`
AppID string `xml:"appid"`
MchID string `xml:"mch_id"`
NonceStr string `xml:"nonce_str"`
Sign string `xml:"sign"`
Body string `xml:"body"`
OutTradeNo string `xml:"out_trade_no"`
TotalFee int `xml:"total_fee"`
SpbillIP string `xml:"spbill_create_ip"`
NotifyURL string `xml:"notify_url"`
TradeType string `xml:"trade_type"`
OpenID string `xml:"openid"`
}
// v2UnifiedOrderResponse 统一下单响应v2 XML 格式)
type v2UnifiedOrderResponse struct {
XMLName xml.Name `xml:"xml"`
ReturnCode string `xml:"return_code"`
ReturnMsg string `xml:"return_msg"`
ResultCode string `xml:"result_code"`
ErrCode string `xml:"err_code"`
ErrCodeDes string `xml:"err_code_des"`
PrepayID string `xml:"prepay_id"`
}
// CreateJSAPIOrder 创建 v2 JSAPI 支付订单
func (s *PaymentV2Service) CreateJSAPIOrder(ctx context.Context, orderNo, description, openID string, amount int) (*JSAPIPayResult, error) {
if orderNo == "" || openID == "" || amount <= 0 {
return nil, errors.New(errors.CodeInvalidParam, "订单号、OpenID 和金额不能为空")
}
nonceStr := v2GenerateNonceStr()
params := map[string]string{
"appid": s.appID,
"mch_id": s.mchID,
"nonce_str": nonceStr,
"body": description,
"out_trade_no": orderNo,
"total_fee": fmt.Sprintf("%d", amount),
"spbill_create_ip": "127.0.0.1",
"notify_url": s.notifyURL,
"trade_type": "JSAPI",
"openid": openID,
}
params["sign"] = v2SignMD5(params, s.apiKey)
reqBody := &v2UnifiedOrderRequest{
AppID: params["appid"],
MchID: params["mch_id"],
NonceStr: params["nonce_str"],
Sign: params["sign"],
Body: params["body"],
OutTradeNo: params["out_trade_no"],
TotalFee: amount,
SpbillIP: params["spbill_create_ip"],
NotifyURL: params["notify_url"],
TradeType: params["trade_type"],
OpenID: params["openid"],
}
xmlBytes, err := xml.Marshal(reqBody)
if err != nil {
return nil, errors.Wrap(errors.CodeWechatPayFailed, err, "序列化统一下单请求失败")
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, wechatPayV2UnifiedOrderURL, strings.NewReader(string(xmlBytes)))
if err != nil {
return nil, errors.Wrap(errors.CodeWechatPayFailed, err, "构建统一下单请求失败")
}
httpReq.Header.Set("Content-Type", "application/xml")
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
s.logger.Error("调用微信 v2 统一下单接口失败", zap.String("order_no", orderNo), zap.Error(err))
return nil, errors.Wrap(errors.CodeWechatPayFailed, err, "调用统一下单接口失败")
}
defer httpResp.Body.Close()
respBytes, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, errors.Wrap(errors.CodeWechatPayFailed, err, "读取统一下单响应失败")
}
var resp v2UnifiedOrderResponse
if err = xml.Unmarshal(respBytes, &resp); err != nil {
return nil, errors.Wrap(errors.CodeWechatPayFailed, err, "解析统一下单响应失败")
}
if resp.ReturnCode != "SUCCESS" {
s.logger.Error("微信 v2 统一下单通信失败",
zap.String("order_no", orderNo),
zap.String("return_msg", resp.ReturnMsg),
)
return nil, errors.New(errors.CodeWechatPayFailed, "统一下单通信失败: "+resp.ReturnMsg)
}
if resp.ResultCode != "SUCCESS" {
s.logger.Error("微信 v2 统一下单业务失败",
zap.String("order_no", orderNo),
zap.String("err_code", resp.ErrCode),
zap.String("err_code_des", resp.ErrCodeDes),
)
return nil, errors.New(errors.CodeWechatPayFailed, "统一下单业务失败: "+resp.ErrCodeDes)
}
payConfig := s.buildJSAPIPayConfig(resp.PrepayID, nonceStr)
s.logger.Info("创建 v2 JSAPI 支付订单成功",
zap.String("order_no", orderNo),
zap.String("prepay_id", resp.PrepayID),
)
return &JSAPIPayResult{
PrepayID: resp.PrepayID,
PayConfig: payConfig,
}, nil
}
// buildJSAPIPayConfig 构建 JSAPI 唤起支付所需的签名参数
func (s *PaymentV2Service) buildJSAPIPayConfig(prepayID, nonceStr string) map[string]string {
timestamp := fmt.Sprintf("%d", time.Now().Unix())
params := map[string]string{
"appId": s.appID,
"timeStamp": timestamp,
"nonceStr": nonceStr,
"package": "prepay_id=" + prepayID,
"signType": "MD5",
}
params["paySign"] = v2SignMD5(params, s.apiKey)
return params
}
// v2SignMD5 对参数按字典序排序后拼接 key计算 MD5 大写签名
func v2SignMD5(params map[string]string, apiKey string) string {
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
var sb strings.Builder
for _, k := range keys {
v := params[k]
if v == "" {
continue
}
sb.WriteString(k)
sb.WriteString("=")
sb.WriteString(v)
sb.WriteString("&")
}
sb.WriteString("key=")
sb.WriteString(apiKey)
hash := md5.Sum([]byte(sb.String()))
return strings.ToUpper(fmt.Sprintf("%x", hash))
}
func v2GenerateNonceStr() string {
return fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d", time.Now().UnixNano()))))
}