temp
This commit is contained in:
293
pkg/core/payment/alipayService.go
Normal file
293
pkg/core/payment/alipayService.go
Normal file
@@ -0,0 +1,293 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
"tyc-server/pkg/lzkit/lzUtils"
|
||||
|
||||
"github.com/smartwalle/alipay/v3"
|
||||
)
|
||||
|
||||
// AlipayConfig 支付宝配置
|
||||
type AlipayConfig struct {
|
||||
AppID string `json:"appId"` // 支付宝应用ID
|
||||
PrivateKey string `json:"privateKey"` // 应用私钥
|
||||
AlipayPublicKey string `json:"alipayPublicKey"` // 支付宝公钥
|
||||
IsProduction bool `json:"isProduction"` // 是否生产环境
|
||||
NotifyURL string `json:"notifyUrl"` // 支付结果通知地址
|
||||
ReturnURL string `json:"returnUrl"` // 支付完成跳转地址
|
||||
}
|
||||
|
||||
// AliPayService 支付宝支付服务
|
||||
type AliPayService struct {
|
||||
config AlipayConfig
|
||||
alipayClient *alipay.Client
|
||||
}
|
||||
|
||||
// NewAliPayService 创建支付宝支付服务
|
||||
func NewAliPayService(c AlipayConfig) (*AliPayService, error) {
|
||||
client, err := alipay.New(c.AppID, c.PrivateKey, c.IsProduction)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建支付宝客户端失败: %w", err)
|
||||
}
|
||||
|
||||
if err := client.LoadAliPayPublicKey(c.AlipayPublicKey); err != nil {
|
||||
return nil, fmt.Errorf("加载支付宝公钥失败: %w", err)
|
||||
}
|
||||
|
||||
return &AliPayService{
|
||||
config: c,
|
||||
alipayClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateOrder 创建支付宝支付订单
|
||||
func (a *AliPayService) CreateOrder(ctx context.Context, req *AlipayCreateOrderRequest) (*AlipayCreateOrderResponse, error) {
|
||||
if err := a.validateCreateOrderRequest(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.NotifyURL == "" {
|
||||
req.NotifyURL = a.config.NotifyURL
|
||||
}
|
||||
if req.ReturnURL == "" {
|
||||
req.ReturnURL = a.config.ReturnURL
|
||||
}
|
||||
if req.OutTradeNo == "" {
|
||||
req.OutTradeNo = a.GenerateOutTradeNo()
|
||||
}
|
||||
var payParams string
|
||||
var err error
|
||||
|
||||
switch req.Platform {
|
||||
case PlatformApp:
|
||||
payParams, err = a.createAppOrder(req)
|
||||
case PlatformH5:
|
||||
payParams, err = a.createH5Order(req)
|
||||
default:
|
||||
return nil, errors.New("不支持的支付平台")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AlipayCreateOrderResponse{
|
||||
OrderNo: req.OutTradeNo,
|
||||
PayParams: payParams,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// createAppOrder 创建APP支付订单
|
||||
func (a *AliPayService) createAppOrder(req *AlipayCreateOrderRequest) (string, error) {
|
||||
p := alipay.TradeAppPay{
|
||||
Trade: alipay.Trade{
|
||||
Subject: req.Subject,
|
||||
OutTradeNo: req.OutTradeNo,
|
||||
TotalAmount: lzUtils.ToAlipayAmount(req.Amount),
|
||||
ProductCode: "QUICK_MSECURITY_PAY",
|
||||
NotifyURL: req.NotifyURL,
|
||||
},
|
||||
}
|
||||
|
||||
payStr, err := a.alipayClient.TradeAppPay(p)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建支付宝APP支付订单失败: %w", err)
|
||||
}
|
||||
|
||||
return payStr, nil
|
||||
}
|
||||
|
||||
// createH5Order 创建H5支付订单
|
||||
func (a *AliPayService) createH5Order(req *AlipayCreateOrderRequest) (string, error) {
|
||||
if req.ReturnURL == "" {
|
||||
return "", errors.New("H5支付必须提供ReturnURL")
|
||||
}
|
||||
|
||||
p := alipay.TradeWapPay{
|
||||
Trade: alipay.Trade{
|
||||
Subject: req.Subject,
|
||||
OutTradeNo: req.OutTradeNo,
|
||||
TotalAmount: lzUtils.ToAlipayAmount(req.Amount),
|
||||
ProductCode: "QUICK_WAP_PAY",
|
||||
NotifyURL: req.NotifyURL,
|
||||
ReturnURL: req.ReturnURL,
|
||||
},
|
||||
}
|
||||
|
||||
payUrl, err := a.alipayClient.TradeWapPay(p)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建支付宝H5支付订单失败: %w", err)
|
||||
}
|
||||
|
||||
return payUrl.String(), nil
|
||||
}
|
||||
|
||||
// Refund 申请退款
|
||||
func (a *AliPayService) Refund(ctx context.Context, req *AlipayRefundRequest) (*AlipayRefundResponse, error) {
|
||||
if err := a.validateRefundRequest(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.RefundNo == "" {
|
||||
req.RefundNo = a.GenerateRefundNo()
|
||||
}
|
||||
|
||||
refund := alipay.TradeRefund{
|
||||
OutTradeNo: req.OrderNo,
|
||||
RefundAmount: a.ToAlipayAmount(req.RefundAmount),
|
||||
OutRequestNo: req.RefundNo,
|
||||
}
|
||||
|
||||
resp, err := a.alipayClient.TradeRefund(ctx, refund)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("申请支付宝退款失败: %w", err)
|
||||
}
|
||||
|
||||
if !resp.IsSuccess() {
|
||||
return nil, fmt.Errorf("申请支付宝退款失败: %s", resp.SubMsg)
|
||||
}
|
||||
|
||||
return &AlipayRefundResponse{
|
||||
OrderNo: req.OrderNo,
|
||||
RefundNo: req.RefundNo,
|
||||
RefundID: resp.TradeNo,
|
||||
Status: StatusSuccess,
|
||||
RefundTime: time.Now(),
|
||||
RefundAmount: alipayamountToFloat(resp.RefundFee),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// QueryOrder 查询订单
|
||||
func (a *AliPayService) QueryOrder(ctx context.Context, req *QueryOrderRequest) (*QueryOrderResponse, error) {
|
||||
if err := a.validateQueryRequest(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := alipay.TradeQuery{
|
||||
OutTradeNo: req.OrderNo,
|
||||
}
|
||||
|
||||
resp, err := a.alipayClient.TradeQuery(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询支付宝订单失败: %w", err)
|
||||
}
|
||||
|
||||
if !resp.IsSuccess() {
|
||||
return nil, fmt.Errorf("查询支付宝订单失败: %s", resp.SubMsg)
|
||||
}
|
||||
|
||||
amount, _ := strconv.ParseFloat(resp.TotalAmount, 64)
|
||||
|
||||
return &QueryOrderResponse{
|
||||
OrderNo: resp.OutTradeNo,
|
||||
TransactionID: resp.TradeNo,
|
||||
Status: a.convertTradeStatus(resp.TradeStatus),
|
||||
Amount: amount,
|
||||
PayTime: a.parseAlipayTime(resp.SendPayDate),
|
||||
PayMethod: MethodAlipay,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HandlePaymentNotification 处理支付结果通知
|
||||
func (a *AliPayService) HandlePaymentNotification(req *http.Request) (*QueryOrderResponse, error) {
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, fmt.Errorf("解析支付宝通知失败: %w", err)
|
||||
}
|
||||
|
||||
notification, err := a.alipayClient.DecodeNotification(req.Form)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("验证支付宝通知签名失败: %w", err)
|
||||
}
|
||||
|
||||
amount := alipayamountToFloat(notification.TotalAmount)
|
||||
|
||||
return &QueryOrderResponse{
|
||||
OrderNo: notification.OutTradeNo,
|
||||
TransactionID: notification.TradeNo,
|
||||
Status: a.convertTradeStatus(alipay.TradeStatus(notification.TradeStatus)),
|
||||
Amount: amount,
|
||||
PayTime: a.parseAlipayTime(notification.GmtPayment),
|
||||
PayMethod: MethodAlipay,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// validateCreateOrderRequest 验证创建订单请求参数
|
||||
func (a *AliPayService) validateCreateOrderRequest(req *AlipayCreateOrderRequest) error {
|
||||
if req.Amount <= 0 {
|
||||
return errors.New("支付金额必须大于0")
|
||||
}
|
||||
if req.Subject == "" {
|
||||
return errors.New("商品描述不能为空")
|
||||
}
|
||||
if req.Platform == "" {
|
||||
return errors.New("支付平台不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateRefundRequest 验证退款请求参数
|
||||
func (a *AliPayService) validateRefundRequest(req *AlipayRefundRequest) error {
|
||||
if req.OrderNo == "" {
|
||||
return errors.New("商户订单号不能为空")
|
||||
}
|
||||
if req.RefundAmount <= 0 {
|
||||
return errors.New("退款金额必须大于0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateQueryRequest 验证查询请求参数
|
||||
func (a *AliPayService) validateQueryRequest(req *QueryOrderRequest) error {
|
||||
if req.OrderNo == "" && req.TransactionID == "" {
|
||||
return errors.New("商户订单号和交易号不能同时为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertTradeStatus 转换支付宝交易状态
|
||||
func (a *AliPayService) convertTradeStatus(status alipay.TradeStatus) PaymentStatus {
|
||||
switch status {
|
||||
case alipay.TradeStatusWaitBuyerPay:
|
||||
return StatusPending
|
||||
case alipay.TradeStatusSuccess:
|
||||
return StatusSuccess
|
||||
case alipay.TradeStatusClosed:
|
||||
return StatusClosed
|
||||
case alipay.TradeStatusFinished:
|
||||
return StatusSuccess
|
||||
default:
|
||||
return StatusFailed
|
||||
}
|
||||
}
|
||||
|
||||
// parseAlipayTime 解析支付宝时间字符串
|
||||
func (a *AliPayService) parseAlipayTime(timeStr string) time.Time {
|
||||
t, err := time.Parse("2006-01-02 15:04:05", timeStr)
|
||||
if err != nil {
|
||||
return time.Now()
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// GenerateOutTradeNo 生成商户订单号
|
||||
func (a *AliPayService) GenerateOutTradeNo() string {
|
||||
return GenerateOrderNo(AlipayPrefix)
|
||||
}
|
||||
|
||||
// GenerateRefundNo 生成退款订单号
|
||||
func (a *AliPayService) GenerateRefundNo() string {
|
||||
return GenerateOrderNo(fmt.Sprintf("%s_%s", RefundPrefix, AlipayPrefix))
|
||||
}
|
||||
|
||||
// ToAlipayAmount 将金额从元转换为支付宝支付 SDK 需要的字符串格式,保留两位小数
|
||||
func (a *AliPayService) ToAlipayAmount(amount float64) string {
|
||||
// 格式化为字符串,保留两位小数
|
||||
return fmt.Sprintf("%.2f", amount)
|
||||
}
|
||||
func alipayamountToFloat(amount string) float64 {
|
||||
amountFloat, _ := strconv.ParseFloat(amount, 64)
|
||||
return amountFloat
|
||||
}
|
||||
341
pkg/core/payment/applepayService.go
Normal file
341
pkg/core/payment/applepayService.go
Normal file
@@ -0,0 +1,341 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
// ApplePayConfig 苹果支付配置
|
||||
type ApplePayConfig struct {
|
||||
BundleID string `json:"bundleId"` // 应用 Bundle ID
|
||||
KeyID string `json:"keyId"` // 密钥 ID
|
||||
TeamID string `json:"teamId"` // 团队 ID
|
||||
PrivateKey string `json:"privateKey"` // 私钥内容
|
||||
IsProduction bool `json:"isProduction"` // 是否生产环境
|
||||
NotifyURL string `json:"notifyUrl"` // 支付结果通知地址
|
||||
RefundNotifyURL string `json:"refundNotifyUrl"` // 退款结果通知地址
|
||||
SharedSecret string `json:"sharedSecret"` // 共享密钥
|
||||
Extra map[string]string `json:"extra"` // 额外配置
|
||||
}
|
||||
|
||||
// ApplePayService 苹果支付服务
|
||||
type ApplePayService struct {
|
||||
config ApplePayConfig
|
||||
}
|
||||
|
||||
// NewApplePayService 创建苹果支付服务
|
||||
func NewApplePayService(c ApplePayConfig) (*ApplePayService, error) {
|
||||
if c.BundleID == "" {
|
||||
return nil, errors.New("Bundle ID 不能为空")
|
||||
}
|
||||
if c.KeyID == "" {
|
||||
return nil, errors.New("Key ID 不能为空")
|
||||
}
|
||||
if c.TeamID == "" {
|
||||
return nil, errors.New("Team ID 不能为空")
|
||||
}
|
||||
if c.PrivateKey == "" {
|
||||
return nil, errors.New("私钥不能为空")
|
||||
}
|
||||
|
||||
return &ApplePayService{
|
||||
config: c,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateOrder 创建苹果支付订单
|
||||
func (a *ApplePayService) CreateOrder(ctx context.Context, req *AppleCreateOrderRequest) (*AppleCreateOrderResponse, error) {
|
||||
if err := a.validateCreateOrderRequest(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 生成 JWT token
|
||||
token, err := a.generateJWT()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("生成 JWT token 失败: %w", err)
|
||||
}
|
||||
|
||||
return &AppleCreateOrderResponse{
|
||||
OrderNo: req.OutTradeNo,
|
||||
PayParams: token,
|
||||
ExpireTime: time.Now().Add(30 * time.Minute), // 苹果支付订单默认30分钟过期
|
||||
CreateTime: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VerifyReceipt 验证支付收据
|
||||
func (a *ApplePayService) VerifyReceipt(ctx context.Context, receipt string) (*QueryOrderResponse, error) {
|
||||
if receipt == "" {
|
||||
return nil, errors.New("收据不能为空")
|
||||
}
|
||||
|
||||
// 构建验证请求
|
||||
verifyURL := "https://sandbox.itunes.apple.com/verifyReceipt"
|
||||
if a.config.IsProduction {
|
||||
verifyURL = "https://buy.itunes.apple.com/verifyReceipt"
|
||||
}
|
||||
|
||||
reqBody := map[string]interface{}{
|
||||
"receipt-data": receipt,
|
||||
"password": a.config.SharedSecret,
|
||||
}
|
||||
|
||||
// 发送验证请求
|
||||
resp, err := a.sendVerifyRequest(ctx, verifyURL, reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("验证收据失败: %w", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
if resp.Status != 0 {
|
||||
return nil, fmt.Errorf("验证收据失败: %d", resp.Status)
|
||||
}
|
||||
|
||||
// 获取最新的交易信息
|
||||
if len(resp.Receipt.InApp) == 0 {
|
||||
return nil, errors.New("未找到交易信息")
|
||||
}
|
||||
|
||||
latestReceipt := resp.Receipt.InApp[len(resp.Receipt.InApp)-1]
|
||||
purchaseDate, _ := time.Parse("2006-01-02 15:04:05 -0700", latestReceipt.PurchaseDate)
|
||||
|
||||
// 转换价格字符串为浮点数
|
||||
price, err := strconv.ParseFloat(latestReceipt.Price, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析价格失败: %w", err)
|
||||
}
|
||||
|
||||
return &QueryOrderResponse{
|
||||
OrderNo: latestReceipt.TransactionID,
|
||||
TransactionID: latestReceipt.TransactionID,
|
||||
Status: a.convertTransactionState(latestReceipt.TransactionState),
|
||||
Amount: price / 100, // 苹果支付金额单位为分
|
||||
PayTime: purchaseDate,
|
||||
PayMethod: MethodApple,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Refund 申请退款
|
||||
func (a *ApplePayService) Refund(ctx context.Context, req *RefundRequest) (*RefundResponse, error) {
|
||||
if err := a.validateRefundRequest(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 苹果支付不支持主动退款,需要通过 App Store Connect 处理
|
||||
return nil, errors.New("苹果支付不支持主动退款")
|
||||
}
|
||||
|
||||
// QueryOrder 查询订单
|
||||
func (a *ApplePayService) QueryOrder(ctx context.Context, req *QueryOrderRequest) (*QueryOrderResponse, error) {
|
||||
if err := a.validateQueryRequest(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 苹果支付不支持主动查询订单状态,需要通过验证收据获取
|
||||
return nil, errors.New("苹果支付不支持主动查询订单")
|
||||
}
|
||||
|
||||
// HandlePaymentNotification 处理支付结果通知
|
||||
func (a *ApplePayService) HandlePaymentNotification(req *http.Request) (*QueryOrderResponse, error) {
|
||||
// 苹果支付的通知是通过 App Store Server Notifications 发送的
|
||||
// 需要实现相应的通知处理逻辑
|
||||
return nil, errors.New("苹果支付通知处理暂未实现")
|
||||
}
|
||||
|
||||
// validateCreateOrderRequest 验证创建订单请求参数
|
||||
func (a *ApplePayService) validateCreateOrderRequest(req *AppleCreateOrderRequest) error {
|
||||
if req.Amount <= 0 {
|
||||
return errors.New("支付金额必须大于0")
|
||||
}
|
||||
if req.Subject == "" {
|
||||
return errors.New("商品描述不能为空")
|
||||
}
|
||||
if req.OutTradeNo == "" {
|
||||
return errors.New("商户订单号不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateRefundRequest 验证退款请求参数
|
||||
func (a *ApplePayService) validateRefundRequest(req *RefundRequest) error {
|
||||
if req.OrderNo == "" {
|
||||
return errors.New("商户订单号不能为空")
|
||||
}
|
||||
if req.RefundNo == "" {
|
||||
return errors.New("商户退款单号不能为空")
|
||||
}
|
||||
if req.RefundAmount <= 0 {
|
||||
return errors.New("退款金额必须大于0")
|
||||
}
|
||||
if req.RefundAmount > req.TotalAmount {
|
||||
return errors.New("退款金额不能大于订单总金额")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateQueryRequest 验证查询请求参数
|
||||
func (a *ApplePayService) validateQueryRequest(req *QueryOrderRequest) error {
|
||||
if req.OrderNo == "" && req.TransactionID == "" {
|
||||
return errors.New("商户订单号和交易号不能同时为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateJWT 生成 JWT token
|
||||
func (a *ApplePayService) generateJWT() (string, error) {
|
||||
now := time.Now()
|
||||
claims := jwt.MapClaims{
|
||||
"iss": a.config.TeamID,
|
||||
"iat": now.Unix(),
|
||||
"exp": now.Add(24 * time.Hour).Unix(),
|
||||
"aud": "appstoreconnect-v1",
|
||||
"bid": a.config.BundleID,
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
|
||||
token.Header["kid"] = a.config.KeyID
|
||||
|
||||
privateKey, err := jwt.ParseECPrivateKeyFromPEM([]byte(a.config.PrivateKey))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解析私钥失败: %w", err)
|
||||
}
|
||||
|
||||
return token.SignedString(privateKey)
|
||||
}
|
||||
|
||||
// convertTransactionState 转换交易状态
|
||||
func (a *ApplePayService) convertTransactionState(state string) PaymentStatus {
|
||||
switch state {
|
||||
case "0": // Purchased
|
||||
return StatusSuccess
|
||||
case "1": // Canceled
|
||||
return StatusClosed
|
||||
case "2": // Failed
|
||||
return StatusFailed
|
||||
default:
|
||||
return StatusFailed
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateOutTradeNo 生成商户订单号
|
||||
func (a *ApplePayService) GenerateOutTradeNo() string {
|
||||
return GenerateOrderNo(ApplePrefix)
|
||||
}
|
||||
|
||||
// AppleVerifyResponse 苹果支付验证响应
|
||||
type AppleVerifyResponse struct {
|
||||
Status int `json:"status"`
|
||||
Receipt Receipt `json:"receipt"`
|
||||
Environment string `json:"environment"`
|
||||
}
|
||||
|
||||
// Receipt 收据信息
|
||||
type Receipt struct {
|
||||
BundleID string `json:"bundle_id"`
|
||||
ApplicationVersion string `json:"application_version"`
|
||||
InApp []InApp `json:"in_app"`
|
||||
}
|
||||
|
||||
// InApp 应用内购买信息
|
||||
type InApp struct {
|
||||
Quantity string `json:"quantity"`
|
||||
ProductID string `json:"product_id"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
OriginalTransactionID string `json:"original_transaction_id"`
|
||||
PurchaseDate string `json:"purchase_date"`
|
||||
PurchaseDateMS string `json:"purchase_date_ms"`
|
||||
PurchaseDatePST string `json:"purchase_date_pst"`
|
||||
OriginalPurchaseDate string `json:"original_purchase_date"`
|
||||
OriginalPurchaseDateMS string `json:"original_purchase_date_ms"`
|
||||
OriginalPurchaseDatePST string `json:"original_purchase_date_pst"`
|
||||
ExpiresDate string `json:"expires_date"`
|
||||
ExpiresDateMS string `json:"expires_date_ms"`
|
||||
ExpiresDatePST string `json:"expires_date_pst"`
|
||||
WebOrderLineItemID string `json:"web_order_line_item_id"`
|
||||
IsTrialPeriod string `json:"is_trial_period"`
|
||||
IsInIntroOfferPeriod string `json:"is_in_intro_offer_period"`
|
||||
TransactionState string `json:"transaction_state"`
|
||||
Price string `json:"price"`
|
||||
}
|
||||
|
||||
func loadPrivateKey(path string) (*ecdsa.PrivateKey, error) {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取私钥文件失败: %w", err)
|
||||
}
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil || block.Type != "PRIVATE KEY" {
|
||||
return nil, errors.New("无效的私钥数据")
|
||||
}
|
||||
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析私钥失败: %w", err)
|
||||
}
|
||||
ecdsaKey, ok := key.(*ecdsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("私钥类型错误")
|
||||
}
|
||||
return ecdsaKey, nil
|
||||
}
|
||||
|
||||
func generateJWT(privateKey *ecdsa.PrivateKey, keyID, issuerID string) (string, error) {
|
||||
now := time.Now()
|
||||
claims := jwt.RegisteredClaims{
|
||||
Issuer: issuerID,
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(1 * time.Hour)),
|
||||
Audience: jwt.ClaimStrings{"appstoreconnect-v1"},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
|
||||
token.Header["kid"] = keyID
|
||||
tokenString, err := token.SignedString(privateKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// sendVerifyRequest 发送验证请求
|
||||
func (a *ApplePayService) sendVerifyRequest(ctx context.Context, url string, body map[string]interface{}) (*AppleVerifyResponse, error) {
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化请求体失败: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonBody))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("请求失败,状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var verifyResp AppleVerifyResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&verifyResp); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %w", err)
|
||||
}
|
||||
|
||||
return &verifyResp, nil
|
||||
}
|
||||
198
pkg/core/payment/types.go
Normal file
198
pkg/core/payment/types.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Order 订单信息
|
||||
type Order struct {
|
||||
OutTradeNo string // 商户订单号
|
||||
Subject string // 商品描述
|
||||
Amount float64 // 支付金额
|
||||
Platform PaymentPlatform // 支付平台
|
||||
NotifyURL string // 通知地址
|
||||
ReturnURL string // 支付完成跳转地址
|
||||
OpenID string // 用户OpenID(小程序支付必填)
|
||||
Extra map[string]string // 额外参数
|
||||
}
|
||||
|
||||
// OrderResult 订单创建结果
|
||||
type OrderResult struct {
|
||||
OrderNo string // 商户订单号
|
||||
PayParams interface{} // 支付参数
|
||||
ExpireTime time.Time // 过期时间
|
||||
CreateTime time.Time // 创建时间
|
||||
}
|
||||
|
||||
// Refund 退款信息
|
||||
type Refund struct {
|
||||
OrderNo string // 商户订单号
|
||||
RefundNo string // 商户退款单号
|
||||
RefundAmount float64 // 退款金额
|
||||
TotalAmount float64 // 订单总金额
|
||||
Reason string // 退款原因
|
||||
NotifyURL string // 退款结果通知地址
|
||||
}
|
||||
|
||||
// RefundResult 退款结果
|
||||
type RefundResult struct {
|
||||
OrderNo string // 商户订单号
|
||||
RefundNo string // 商户退款单号
|
||||
RefundID string // 退款交易号
|
||||
Status string // 退款状态
|
||||
RefundTime time.Time // 退款时间
|
||||
RefundAmount float64 // 退款金额
|
||||
}
|
||||
|
||||
// OrderQuery 订单查询参数
|
||||
type OrderQuery struct {
|
||||
OrderNo string // 商户订单号
|
||||
TransactionID string // 交易号
|
||||
}
|
||||
|
||||
// OrderQueryResult 订单查询结果
|
||||
type OrderQueryResult struct {
|
||||
OrderNo string // 商户订单号
|
||||
TransactionID string // 交易号
|
||||
Status PaymentStatus // 支付状态
|
||||
Amount float64 // 支付金额
|
||||
PayTime time.Time // 支付时间
|
||||
PayMethod PaymentMethod // 支付方式
|
||||
Platform PaymentPlatform // 支付平台
|
||||
}
|
||||
|
||||
// PaymentMethod 支付方式
|
||||
type PaymentMethod string
|
||||
|
||||
const (
|
||||
MethodAlipay PaymentMethod = "ALIPAY" // 支付宝支付
|
||||
MethodWechat PaymentMethod = "WECHAT" // 微信支付
|
||||
MethodApple PaymentMethod = "APPLE" // 苹果支付
|
||||
)
|
||||
|
||||
// PaymentPlatform 支付平台
|
||||
type PaymentPlatform string
|
||||
|
||||
const (
|
||||
PlatformApp PaymentPlatform = "APP" // APP支付
|
||||
PlatformH5 PaymentPlatform = "H5" // H5支付
|
||||
PlatformMiniProg PaymentPlatform = "MINI_PROG" // 小程序支付
|
||||
)
|
||||
|
||||
// PaymentStatus 支付状态
|
||||
type PaymentStatus string
|
||||
|
||||
const (
|
||||
StatusPending PaymentStatus = "PENDING" // 待支付
|
||||
StatusSuccess PaymentStatus = "SUCCESS" // 支付成功
|
||||
StatusFailed PaymentStatus = "FAILED" // 支付失败
|
||||
StatusClosed PaymentStatus = "CLOSED" // 已关闭
|
||||
StatusRefunded PaymentStatus = "REFUNDED" // 已退款
|
||||
StatusRefunding PaymentStatus = "REFUNDING" // 退款中
|
||||
)
|
||||
|
||||
// alipay
|
||||
// AlipayCreateOrderRequest 支付宝创建订单请求
|
||||
type AlipayCreateOrderRequest struct {
|
||||
Amount float64 `json:"amount"` // 支付金额(元)
|
||||
Subject string `json:"subject"` // 商品描述
|
||||
OutTradeNo string `json:"outTradeNo"` // 商户订单号
|
||||
Platform PaymentPlatform `json:"platform"` // 支付平台
|
||||
NotifyURL string `json:"notifyUrl"` // 支付结果通知地址
|
||||
ReturnURL string `json:"returnUrl"` // 支付完成跳转地址
|
||||
}
|
||||
|
||||
// AlipayCreateOrderResponse 支付宝创建订单响应
|
||||
type AlipayCreateOrderResponse struct {
|
||||
OrderNo string `json:"orderNo"` // 商户订单号
|
||||
PayParams string `json:"payParams"` // 支付参数(支付宝返回的支付字符串)
|
||||
}
|
||||
|
||||
// AlipayRefundRequest 支付宝退款请求
|
||||
type AlipayRefundRequest struct {
|
||||
OrderNo string `json:"orderNo"` // 商户订单号
|
||||
RefundNo string `json:"refundNo"` // 商户退款单号
|
||||
RefundAmount float64 `json:"refundAmount"` // 退款金额(元)
|
||||
}
|
||||
|
||||
// AlipayRefundResponse 支付宝退款响应
|
||||
type AlipayRefundResponse struct {
|
||||
OrderNo string `json:"orderNo"` // 商户订单号
|
||||
RefundNo string `json:"refundNo"` // 商户退款单号
|
||||
RefundID string `json:"refundId"` // 支付平台退款单号
|
||||
Status PaymentStatus `json:"status"` // 退款状态
|
||||
RefundTime time.Time `json:"refundTime"` // 退款时间
|
||||
RefundAmount float64 `json:"refundAmount"` // 退款金额
|
||||
}
|
||||
|
||||
// RefundRequest 退款请求
|
||||
type RefundRequest struct {
|
||||
OrderNo string `json:"orderNo"` // 商户订单号
|
||||
RefundNo string `json:"refundNo"` // 商户退款单号
|
||||
RefundAmount float64 `json:"refundAmount"` // 退款金额(元)
|
||||
TotalAmount float64 `json:"totalAmount"` // 订单总金额(元)
|
||||
Reason string `json:"reason"` // 退款原因
|
||||
NotifyURL string `json:"notifyUrl"` // 退款结果通知地址
|
||||
}
|
||||
|
||||
// RefundResponse 退款响应
|
||||
type RefundResponse struct {
|
||||
OrderNo string `json:"orderNo"` // 商户订单号
|
||||
RefundNo string `json:"refundNo"` // 商户退款单号
|
||||
RefundID string `json:"refundId"` // 支付平台退款单号
|
||||
Status string `json:"status"` // 退款状态
|
||||
RefundTime time.Time `json:"refundTime"` // 退款时间
|
||||
RefundAmount float64 `json:"refundAmount"` // 退款金额
|
||||
}
|
||||
|
||||
// WechatCreateOrderRequest 微信创建订单请求
|
||||
type WechatCreateOrderRequest struct {
|
||||
Amount float64 `json:"amount"` // 支付金额(元)
|
||||
Subject string `json:"subject"` // 商品描述
|
||||
OutTradeNo string `json:"outTradeNo"` // 商户订单号
|
||||
Platform PaymentPlatform `json:"platform"` // 支付平台
|
||||
NotifyURL string `json:"notifyUrl"` // 支付结果通知地址
|
||||
OpenID string `json:"openId"` // 用户的 OpenID(小程序支付必填)
|
||||
}
|
||||
|
||||
// AppleCreateOrderRequest 苹果创建订单请求
|
||||
type AppleCreateOrderRequest struct {
|
||||
Amount float64 `json:"amount"` // 支付金额(元)
|
||||
Subject string `json:"subject"` // 商品描述
|
||||
OutTradeNo string `json:"outTradeNo"` // 商户订单号
|
||||
Platform PaymentPlatform `json:"platform"` // 支付平台
|
||||
NotifyURL string `json:"notifyUrl"` // 支付结果通知地址
|
||||
}
|
||||
|
||||
// WechatCreateOrderResponse 微信创建订单响应
|
||||
type WechatCreateOrderResponse struct {
|
||||
OrderNo string `json:"orderNo"` // 商户订单号
|
||||
PrepayID string `json:"prepayId"` // 预支付ID
|
||||
PayParams interface{} `json:"payParams"` // 支付参数(微信返回的支付参数)
|
||||
ExpireTime time.Time `json:"expireTime"` // 订单过期时间
|
||||
CreateTime time.Time `json:"createTime"` // 订单创建时间
|
||||
}
|
||||
|
||||
// AppleCreateOrderResponse 苹果创建订单响应
|
||||
type AppleCreateOrderResponse struct {
|
||||
OrderNo string `json:"orderNo"` // 商户订单号
|
||||
PayParams string `json:"payParams"` // 支付参数(苹果返回的支付参数)
|
||||
ExpireTime time.Time `json:"expireTime"` // 订单过期时间
|
||||
CreateTime time.Time `json:"createTime"` // 订单创建时间
|
||||
}
|
||||
|
||||
// QueryOrderRequest 查询订单请求
|
||||
type QueryOrderRequest struct {
|
||||
OrderNo string `json:"orderNo"` // 商户订单号
|
||||
TransactionID string `json:"transactionId"` // 支付平台交易号
|
||||
}
|
||||
|
||||
// QueryOrderResponse 查询订单响应
|
||||
type QueryOrderResponse struct {
|
||||
OrderNo string `json:"orderNo"` // 商户订单号
|
||||
TransactionID string `json:"transactionId"` // 支付平台交易号
|
||||
Status PaymentStatus `json:"status"` // 支付状态
|
||||
Amount float64 `json:"amount"` // 支付金额
|
||||
PayTime time.Time `json:"payTime"` // 支付时间
|
||||
PayMethod PaymentMethod `json:"payMethod"` // 支付方式
|
||||
}
|
||||
51
pkg/core/payment/utils.go
Normal file
51
pkg/core/payment/utils.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// 订单号前缀
|
||||
AlipayPrefix = "ALI" // 支付宝订单前缀
|
||||
WechatPrefix = "WX" // 微信支付订单前缀
|
||||
ApplePrefix = "APP" // 苹果支付订单前缀
|
||||
)
|
||||
|
||||
const (
|
||||
RefundPrefix = "REF" // 退款订单前缀
|
||||
)
|
||||
|
||||
// 全局原子计数器
|
||||
var orderCounter uint32 = 0
|
||||
|
||||
// GenerateOrderNo 生成统一格式的订单号
|
||||
// prefix: 订单前缀(ALI/WX/APP)
|
||||
// 返回格式:前缀 + 时间戳(10位) + 计数器(6位) + 随机数(6位)
|
||||
// 例如:ALI202403151234567890123456
|
||||
func GenerateOrderNo(prefix string) string {
|
||||
// 获取当前时间戳(秒级)
|
||||
timestamp := time.Now().Unix()
|
||||
timeStr := strconv.FormatInt(timestamp, 10)
|
||||
|
||||
// 原子递增计数器
|
||||
counter := atomic.AddUint32(&orderCounter, 1)
|
||||
|
||||
// 生成4字节真随机数
|
||||
randomBytes := make([]byte, 4)
|
||||
_, err := rand.Read(randomBytes)
|
||||
if err != nil {
|
||||
// 如果随机数生成失败,回退到使用时间纳秒数据
|
||||
randomBytes = []byte(strconv.FormatInt(time.Now().UnixNano()%1000000, 16))
|
||||
}
|
||||
randomHex := hex.EncodeToString(randomBytes)
|
||||
|
||||
// 组合所有部分: 前缀 + 时间戳 + 计数器 + 随机数
|
||||
orderNo := fmt.Sprintf("%s_%s%06x%s", prefix, timeStr, counter%0xFFFFFF, randomHex[:6])
|
||||
|
||||
return orderNo
|
||||
}
|
||||
534
pkg/core/payment/wechatpayService.go
Normal file
534
pkg/core/payment/wechatpayService.go
Normal file
@@ -0,0 +1,534 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/app"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/h5"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
TradeStateSuccess = "SUCCESS" // 支付成功
|
||||
TradeStateRefund = "REFUND" // 转入退款
|
||||
TradeStateNotPay = "NOTPAY" // 未支付
|
||||
TradeStateClosed = "CLOSED" // 已关闭
|
||||
TradeStateRevoked = "REVOKED" // 已撤销(付款码支付)
|
||||
TradeStateUserPaying = "USERPAYING" // 用户支付中(付款码支付)
|
||||
TradeStatePayError = "PAYERROR" // 支付失败(其他原因,如银行返回失败)
|
||||
)
|
||||
|
||||
// WechatPayConfig 微信支付配置
|
||||
type WechatPayConfig struct {
|
||||
AppID string `json:"app_id"` // 应用ID
|
||||
MchID string `json:"mch_id"` // 商户号
|
||||
MchAPIv3Key string `json:"mch_api_v3_key"` // 商户APIv3密钥
|
||||
SerialNo string `json:"serial_no"` // 商户证书序列号
|
||||
PrivateKey string `json:"private_key"` // 商户私钥
|
||||
NotifyURL string `json:"notify_url"` // 支付通知地址
|
||||
RefundURL string `json:"refund_url"` // 退款通知地址
|
||||
}
|
||||
|
||||
// WechatPayService 微信支付服务
|
||||
type WechatPayService struct {
|
||||
config WechatPayConfig
|
||||
client *core.Client
|
||||
jsapiService *jsapi.JsapiApiService
|
||||
}
|
||||
|
||||
// NewWechatPayService 创建微信支付服务
|
||||
func NewWechatPayService(config *WechatPayConfig) (*WechatPayService, error) {
|
||||
if err := validateWechatPayConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 加载商户私钥
|
||||
privateKey, err := utils.LoadPrivateKey(config.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载商户私钥失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建微信支付客户端
|
||||
opts := []core.ClientOption{
|
||||
option.WithWechatPayAutoAuthCipher(config.MchID, config.SerialNo, privateKey, config.MchAPIv3Key),
|
||||
}
|
||||
client, err := core.NewClient(context.Background(), opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建微信支付客户端失败: %w", err)
|
||||
}
|
||||
|
||||
// 下载并加载微信支付平台证书
|
||||
certDownloader, err := downloader.NewCertificateDownloader(context.Background(), config.MchID, privateKey, config.MchAPIv3Key, config.SerialNo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建证书下载器失败: %w", err)
|
||||
}
|
||||
if err := certDownloader.DownloadCertificates(context.Background()); err != nil {
|
||||
return nil, fmt.Errorf("下载微信支付平台证书失败: %w", err)
|
||||
}
|
||||
|
||||
return &WechatPayService{
|
||||
config: *config,
|
||||
client: client,
|
||||
jsapiService: &jsapi.JsapiApiService{Client: client},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateOrder 创建支付订单
|
||||
func (w *WechatPayService) CreateOrder(order *Order) (*OrderResult, error) {
|
||||
if err := validateCreateOrder(order); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch order.Platform {
|
||||
case PlatformApp:
|
||||
return w.createAppOrder(order)
|
||||
case PlatformH5:
|
||||
return w.createH5Order(order)
|
||||
case PlatformMiniProg:
|
||||
return w.createJsapiOrder(order)
|
||||
default:
|
||||
return nil, errors.New("不支持的支付平台")
|
||||
}
|
||||
}
|
||||
|
||||
// createAppOrder 创建APP支付订单
|
||||
func (w *WechatPayService) createAppOrder(order *Order) (*OrderResult, error) {
|
||||
svc := app.AppApiService{Client: w.client}
|
||||
amount := &app.Amount{
|
||||
Total: core.Int64(int64(order.Amount * 100)), // 转换为分
|
||||
Currency: core.String("CNY"),
|
||||
}
|
||||
req := app.PrepayRequest{
|
||||
Appid: core.String(w.config.AppID),
|
||||
Mchid: core.String(w.config.MchID),
|
||||
Description: core.String(order.Subject),
|
||||
OutTradeNo: core.String(order.OutTradeNo),
|
||||
NotifyUrl: core.String(w.config.NotifyURL),
|
||||
Amount: amount,
|
||||
}
|
||||
|
||||
resp, _, err := svc.Prepay(context.Background(), req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建APP支付订单失败: %w", err)
|
||||
}
|
||||
|
||||
// 生成支付参数
|
||||
params, err := w.generateAppPayParams(resp.PrepayId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &OrderResult{
|
||||
OrderNo: order.OutTradeNo,
|
||||
PayParams: params,
|
||||
ExpireTime: time.Now().Add(30 * time.Minute),
|
||||
CreateTime: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// createH5Order 创建H5支付订单
|
||||
func (w *WechatPayService) createH5Order(order *Order) (*OrderResult, error) {
|
||||
svc := h5.H5ApiService{Client: w.client}
|
||||
amount := &h5.Amount{
|
||||
Total: core.Int64(int64(order.Amount * 100)), // 转换为分
|
||||
Currency: core.String("CNY"),
|
||||
}
|
||||
req := h5.PrepayRequest{
|
||||
Appid: core.String(w.config.AppID),
|
||||
Mchid: core.String(w.config.MchID),
|
||||
Description: core.String(order.Subject),
|
||||
OutTradeNo: core.String(order.OutTradeNo),
|
||||
NotifyUrl: core.String(w.config.NotifyURL),
|
||||
Amount: amount,
|
||||
SceneInfo: &h5.SceneInfo{
|
||||
PayerClientIp: core.String("127.0.0.1"), // TODO: 获取真实IP
|
||||
H5Info: &h5.H5Info{
|
||||
Type: core.String("Wap"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, _, err := svc.Prepay(context.Background(), req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建H5支付订单失败: %w", err)
|
||||
}
|
||||
|
||||
return &OrderResult{
|
||||
OrderNo: order.OutTradeNo,
|
||||
PayParams: resp.H5Url,
|
||||
ExpireTime: time.Now().Add(30 * time.Minute),
|
||||
CreateTime: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// createJsapiOrder 创建小程序支付订单
|
||||
func (w *WechatPayService) createJsapiOrder(order *Order) (*OrderResult, error) {
|
||||
if order.OpenID == "" {
|
||||
return nil, errors.New("小程序支付必须提供OpenID")
|
||||
}
|
||||
|
||||
svc := jsapi.JsapiApiService{Client: w.client}
|
||||
amount := &jsapi.Amount{
|
||||
Total: core.Int64(int64(order.Amount * 100)), // 转换为分
|
||||
Currency: core.String("CNY"),
|
||||
}
|
||||
req := jsapi.PrepayRequest{
|
||||
Appid: core.String(w.config.AppID),
|
||||
Mchid: core.String(w.config.MchID),
|
||||
Description: core.String(order.Subject),
|
||||
OutTradeNo: core.String(order.OutTradeNo),
|
||||
NotifyUrl: core.String(w.config.NotifyURL),
|
||||
Amount: amount,
|
||||
Payer: &jsapi.Payer{
|
||||
Openid: core.String(order.OpenID),
|
||||
},
|
||||
}
|
||||
|
||||
resp, _, err := svc.Prepay(context.Background(), req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建小程序支付订单失败: %w", err)
|
||||
}
|
||||
|
||||
// 生成支付参数
|
||||
params, err := w.generateJsapiPayParams(resp.PrepayId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &OrderResult{
|
||||
OrderNo: order.OutTradeNo,
|
||||
PayParams: params,
|
||||
ExpireTime: time.Now().Add(30 * time.Minute),
|
||||
CreateTime: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Refund 申请退款
|
||||
func (w *WechatPayService) Refund(refund *Refund) (*RefundResult, error) {
|
||||
if err := validateRefund(refund); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svc := refunddomestic.RefundsApiService{Client: w.client}
|
||||
amount := &refunddomestic.AmountReq{
|
||||
Refund: core.Int64(int64(refund.RefundAmount * 100)), // 转换为分
|
||||
Total: core.Int64(int64(refund.TotalAmount * 100)), // 转换为分
|
||||
Currency: core.String("CNY"),
|
||||
}
|
||||
req := refunddomestic.CreateRequest{
|
||||
OutTradeNo: core.String(refund.OrderNo),
|
||||
OutRefundNo: core.String(refund.RefundNo),
|
||||
Reason: core.String(refund.Reason),
|
||||
NotifyUrl: core.String(refund.NotifyURL),
|
||||
Amount: amount,
|
||||
}
|
||||
|
||||
resp, _, err := svc.Create(context.Background(), req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("申请退款失败: %w", err)
|
||||
}
|
||||
|
||||
return &RefundResult{
|
||||
OrderNo: refund.OrderNo,
|
||||
RefundNo: refund.RefundNo,
|
||||
RefundID: *resp.RefundId,
|
||||
Status: string(*resp.Status),
|
||||
RefundTime: time.Now(),
|
||||
RefundAmount: refund.RefundAmount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// QueryOrder 查询订单
|
||||
func (w *WechatPayService) QueryOrder(query *OrderQuery) (*OrderQueryResult, error) {
|
||||
if err := validateOrderQuery(query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp *payments.Transaction
|
||||
var err error
|
||||
|
||||
if query.TransactionID != "" {
|
||||
resp, _, err = w.jsapiService.QueryOrderById(context.Background(),
|
||||
jsapi.QueryOrderByIdRequest{
|
||||
TransactionId: core.String(query.TransactionID),
|
||||
})
|
||||
} else {
|
||||
resp, _, err = w.jsapiService.QueryOrderByOutTradeNo(context.Background(),
|
||||
jsapi.QueryOrderByOutTradeNoRequest{
|
||||
OutTradeNo: core.String(query.OrderNo),
|
||||
Mchid: core.String(w.config.MchID),
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询订单失败: %w", err)
|
||||
}
|
||||
|
||||
// 转换支付状态
|
||||
status := convertWechatPayStatus(*resp.TradeState)
|
||||
|
||||
// 解析支付时间
|
||||
payTime, _ := time.Parse(time.RFC3339, *resp.SuccessTime)
|
||||
|
||||
return &OrderQueryResult{
|
||||
OrderNo: *resp.OutTradeNo,
|
||||
TransactionID: *resp.TransactionId,
|
||||
Status: status,
|
||||
Amount: float64(*resp.Amount.Total) / 100, // 转换为元
|
||||
PayTime: payTime,
|
||||
PayMethod: MethodWechat,
|
||||
Platform: getWechatPayPlatform(*resp.TradeType),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HandlePaymentNotification 处理支付结果通知
|
||||
func (w *WechatPayService) HandlePaymentNotification(r *http.Request) (*OrderQueryResult, error) {
|
||||
// 初始化通知处理器
|
||||
handler := notify.NewNotifyHandler(w.config.MchAPIv3Key, verifiers.NewSHA256WithRSAVerifier(downloader.MgrInstance().GetCertificateVisitor(w.config.MchID)))
|
||||
|
||||
// 解析通知数据
|
||||
transaction := new(payments.Transaction)
|
||||
notifyReq, err := handler.ParseNotifyRequest(context.Background(), r, transaction)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析支付通知失败: %w", err)
|
||||
}
|
||||
|
||||
// 验证通知数据
|
||||
if notifyReq.EventType != "TRANSACTION.SUCCESS" {
|
||||
return nil, errors.New("非支付成功通知")
|
||||
}
|
||||
|
||||
// 转换支付状态
|
||||
status := convertWechatPayStatus(*transaction.TradeState)
|
||||
|
||||
// 解析支付时间
|
||||
payTime, _ := time.Parse(time.RFC3339, *transaction.SuccessTime)
|
||||
|
||||
return &OrderQueryResult{
|
||||
OrderNo: *transaction.OutTradeNo,
|
||||
TransactionID: *transaction.TransactionId,
|
||||
Status: status,
|
||||
Amount: float64(*transaction.Amount.Total) / 100, // 转换为元
|
||||
PayTime: payTime,
|
||||
PayMethod: MethodWechat,
|
||||
Platform: getWechatPayPlatform(*transaction.TradeType),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 生成APP支付参数
|
||||
func (w *WechatPayService) generateAppPayParams(prepayID *string) (map[string]string, error) {
|
||||
if prepayID == nil {
|
||||
return nil, errors.New("预支付ID不能为空")
|
||||
}
|
||||
|
||||
params := make(map[string]string)
|
||||
params["appid"] = w.config.AppID
|
||||
params["partnerid"] = w.config.MchID
|
||||
params["prepayid"] = *prepayID
|
||||
params["package"] = "Sign=WXPay"
|
||||
nonce, err := utils.GenerateNonce()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("生成随机字符串失败: %w", err)
|
||||
}
|
||||
params["noncestr"] = nonce
|
||||
params["timestamp"] = fmt.Sprintf("%d", time.Now().Unix())
|
||||
|
||||
// 生成签名
|
||||
sign, err := w.signParams(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params["sign"] = sign
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// 生成小程序支付参数
|
||||
func (w *WechatPayService) generateJsapiPayParams(prepayID *string) (map[string]string, error) {
|
||||
if prepayID == nil {
|
||||
return nil, errors.New("预支付ID不能为空")
|
||||
}
|
||||
|
||||
params := make(map[string]string)
|
||||
params["appId"] = w.config.AppID
|
||||
params["timeStamp"] = fmt.Sprintf("%d", time.Now().Unix())
|
||||
nonce, err := utils.GenerateNonce()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("生成随机字符串失败: %w", err)
|
||||
}
|
||||
params["nonceStr"] = nonce
|
||||
params["package"] = "prepay_id=" + *prepayID
|
||||
params["signType"] = "RSA"
|
||||
|
||||
// 生成签名
|
||||
sign, err := w.signParams(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params["paySign"] = sign
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// 签名参数
|
||||
func (w *WechatPayService) signParams(params map[string]string) (string, error) {
|
||||
// 按字典序排序参数
|
||||
var keys []string
|
||||
for k := range params {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var pairs []string
|
||||
for _, k := range keys {
|
||||
pairs = append(pairs, fmt.Sprintf("%s=%s", k, params[k]))
|
||||
}
|
||||
message := strings.Join(pairs, "&")
|
||||
|
||||
// 使用商户私钥签名
|
||||
privateKey, err := utils.LoadPrivateKey(w.config.PrivateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("加载商户私钥失败: %w", err)
|
||||
}
|
||||
|
||||
signature, err := utils.SignSHA256WithRSA(message, privateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("签名失败: %w", err)
|
||||
}
|
||||
|
||||
return signature, nil
|
||||
}
|
||||
|
||||
// 转换微信支付状态
|
||||
func convertWechatPayStatus(tradeState string) PaymentStatus {
|
||||
switch tradeState {
|
||||
case "SUCCESS":
|
||||
return StatusSuccess
|
||||
case "REFUND":
|
||||
return StatusRefunded
|
||||
case "NOTPAY":
|
||||
return StatusPending
|
||||
case "CLOSED":
|
||||
return StatusClosed
|
||||
case "PAYERROR":
|
||||
return StatusFailed
|
||||
default:
|
||||
return StatusPending
|
||||
}
|
||||
}
|
||||
|
||||
// 获取微信支付平台
|
||||
func getWechatPayPlatform(tradeType string) PaymentPlatform {
|
||||
switch tradeType {
|
||||
case "APP":
|
||||
return PlatformApp
|
||||
case "JSAPI":
|
||||
return PlatformMiniProg
|
||||
case "NATIVE", "MWEB":
|
||||
return PlatformH5
|
||||
default:
|
||||
return PlatformApp
|
||||
}
|
||||
}
|
||||
|
||||
// 验证微信支付配置
|
||||
func validateWechatPayConfig(config *WechatPayConfig) error {
|
||||
if config == nil {
|
||||
return errors.New("配置不能为空")
|
||||
}
|
||||
if config.AppID == "" {
|
||||
return errors.New("应用ID不能为空")
|
||||
}
|
||||
if config.MchID == "" {
|
||||
return errors.New("商户号不能为空")
|
||||
}
|
||||
if config.MchAPIv3Key == "" {
|
||||
return errors.New("商户APIv3密钥不能为空")
|
||||
}
|
||||
if config.SerialNo == "" {
|
||||
return errors.New("商户证书序列号不能为空")
|
||||
}
|
||||
if config.PrivateKey == "" {
|
||||
return errors.New("商户私钥不能为空")
|
||||
}
|
||||
if config.NotifyURL == "" {
|
||||
return errors.New("支付通知地址不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 验证创建订单参数
|
||||
func validateCreateOrder(order *Order) error {
|
||||
if order == nil {
|
||||
return errors.New("订单信息不能为空")
|
||||
}
|
||||
if order.OutTradeNo == "" {
|
||||
return errors.New("商户订单号不能为空")
|
||||
}
|
||||
if order.Subject == "" {
|
||||
return errors.New("商品描述不能为空")
|
||||
}
|
||||
if order.Amount <= 0 {
|
||||
return errors.New("支付金额必须大于0")
|
||||
}
|
||||
if order.NotifyURL == "" {
|
||||
return errors.New("通知地址不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 验证退款参数
|
||||
func validateRefund(refund *Refund) error {
|
||||
if refund == nil {
|
||||
return errors.New("退款信息不能为空")
|
||||
}
|
||||
if refund.OrderNo == "" {
|
||||
return errors.New("商户订单号不能为空")
|
||||
}
|
||||
if refund.RefundNo == "" {
|
||||
return errors.New("商户退款单号不能为空")
|
||||
}
|
||||
if refund.RefundAmount <= 0 {
|
||||
return errors.New("退款金额必须大于0")
|
||||
}
|
||||
if refund.TotalAmount <= 0 {
|
||||
return errors.New("订单总金额必须大于0")
|
||||
}
|
||||
if refund.RefundAmount > refund.TotalAmount {
|
||||
return errors.New("退款金额不能大于订单总金额")
|
||||
}
|
||||
if refund.NotifyURL == "" {
|
||||
return errors.New("退款通知地址不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 验证订单查询参数
|
||||
func validateOrderQuery(query *OrderQuery) error {
|
||||
if query == nil {
|
||||
return errors.New("查询参数不能为空")
|
||||
}
|
||||
if query.OrderNo == "" && query.TransactionID == "" {
|
||||
return errors.New("商户订单号和交易号不能同时为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateOutTradeNo 生成商户订单号
|
||||
func (w *WechatPayService) GenerateOutTradeNo() string {
|
||||
return GenerateOrderNo(WechatPrefix)
|
||||
}
|
||||
Reference in New Issue
Block a user