添加支付宝和微信充值功能,重构交易处理逻辑,优化资源创建与支付链接生成

This commit is contained in:
2025-04-18 16:22:38 +08:00
parent f6a97545c5
commit a7e59fb1d7
14 changed files with 939 additions and 224 deletions

View File

@@ -18,56 +18,37 @@ var Resource = &resourceService{}
type resourceService struct{}
func (s *resourceService) PrepareResource(ctx context.Context, data *CreateResourceData, uid int32, tradeNo string, method int32) error {
func (s *resourceService) PrepareResource(ctx context.Context, data *CreateResourceData, uid int32, method TransactionMethod) (*TransactionPrepareResult, error) {
amount := data.GetPrice()
// 保存到数据库
var result *TransactionPrepareResult
err := q.Q.Transaction(func(q *q.Query) error {
// 创建交易订单
var trade = m.Trade{
UserID: uid,
InnerNo: tradeNo,
Subject: "购买套餐 - " + data.GetName(),
Method: method,
Type: 1,
Status: 0,
Amount: amount,
}
err := q.Trade.Create(&trade)
if err != nil {
return err
}
var err error
// 保存用户帐
bill := m.Bill{
UserID: uid,
TradeID: trade.ID,
BillNo: ID.GenReadable("bil"),
Info: "购买套餐 - " + data.GetName(),
Type: 1,
Amount: -amount,
}
err = q.Bill.
Omit(q.Bill.ResourceID, q.Bill.RefundID).
Create(&bill)
// 生成交易订
result, err = Transaction.PrepareTransaction(ctx, q, uid, &TransactionPrepareData{
Subject: "购买套餐 - " + data.GetName(),
Amount: amount,
ExpireAt: time.Now().Add(30 * time.Minute),
Type: TransactionTypePurchase,
Method: method,
})
if err != nil {
return err
}
// 保存请求缓存
cache := &CreateResourceCache{
reqStr, err := json.Marshal(&CreateResourceCache{
CreateResourceData: *data,
Uid: uid,
TradeId: trade.ID,
BillId: bill.ID,
}
reqStr, err := json.Marshal(cache)
TradeId: result.Trade.ID,
BillId: result.Bill.ID,
})
if err != nil {
return err
}
err = rds.Client.Set(ctx, tradeNo, reqStr, 30*time.Minute).Err()
err = rds.Client.Set(ctx, result.TradeNo, reqStr, 30*time.Minute).Err()
if err != nil {
return err
}
@@ -75,20 +56,19 @@ func (s *resourceService) PrepareResource(ctx context.Context, data *CreateResou
return nil
})
if err != nil {
return err
return nil, err
}
return nil
return result, nil
}
func (s *resourceService) CreateResourcePrepared(ctx context.Context, tradeNo string, outerTradeNo string, payment float64, at time.Time) error {
func (s *resourceService) CompleteResource(ctx context.Context, tradeNo string, rs *TransactionVerifyResult) error {
// 获取请求缓存
reqStr, err := rds.Client.Get(ctx, tradeNo).Result()
if err != nil {
return err
}
cache := new(CreateResourceCache)
if err := json.Unmarshal([]byte(reqStr), cache); err != nil {
return err
@@ -103,21 +83,7 @@ func (s *resourceService) CreateResourcePrepared(ctx context.Context, tradeNo st
return err
}
// 更新订单状态
_, err = q.Trade.Debug().
Select(q.Trade.OuterNo, q.Trade.Payment, q.Trade.Status, q.Trade.PaidAt).
Updates(&m.Trade{
ID: cache.TradeId,
OuterNo: outerTradeNo,
Payment: payment,
Status: 1,
PaidAt: common.LocalDateTime(at),
})
if err != nil {
return err
}
// 更新账单状态
// 更新账单
_, err = q.Bill.Debug().
Select(q.Bill.ResourceID).
Updates(&m.Bill{
@@ -128,6 +94,15 @@ func (s *resourceService) CreateResourcePrepared(ctx context.Context, tradeNo st
return err
}
// 完成交易
_, err = Transaction.CompleteTransaction(ctx, q, &TransactionCompleteData{
TradeNo: tradeNo,
TransactionVerifyResult: *rs,
})
if err != nil {
return err
}
// 删除缓存
err = rds.Client.Del(ctx, tradeNo).Err()
if err != nil {
@@ -143,10 +118,24 @@ func (s *resourceService) CreateResourcePrepared(ctx context.Context, tradeNo st
return nil
}
func (s *resourceService) CreateResourceImmediately(data *CreateResourceData, uid int32) error {
func (s *resourceService) CreateResource(data *CreateResourceData, uid int32) error {
// 保存交易信息
err := q.Q.Transaction(func(q *q.Query) error {
amount := data.GetPrice()
// 检查用户
user, err := q.User.
Where(q.User.ID.Eq(uid)).
Take()
if err != nil {
return err
}
// 检查余额
if user.Balance < amount {
return fiber.NewError(fiber.StatusBadRequest, "余额不足")
}
// 保存套餐
resource, err := createResource(data, uid)
@@ -170,6 +159,14 @@ func (s *resourceService) CreateResourceImmediately(data *CreateResourceData, ui
return err
}
// 更新用户余额
_, err = q.User.
Where(q.User.ID.Eq(uid)).
Update(q.User.Balance, user.Balance-amount)
if err != nil {
return err
}
return nil
})
if err != nil {
@@ -221,27 +218,10 @@ type CreateResourceCache struct {
}
func createResource(data *CreateResourceData, uid int32) (*m.Resource, error) {
amount := data.GetPrice()
// 检查用户
user, err := q.User.
Where(
q.User.ID.Eq(uid),
q.User.Status.Eq(1),
).
Take()
if err != nil {
return nil, err
}
// 检查余额
if user.Balance < amount {
return nil, fiber.NewError(fiber.StatusBadRequest, "余额不足")
}
// 创建套餐
resource := &m.Resource{
UserID: user.ID,
resource := m.Resource{
UserID: uid,
ResourceNo: ID.GenReadable("res"),
Active: true,
Type: 1,
@@ -253,38 +233,29 @@ func createResource(data *CreateResourceData, uid int32) (*m.Resource, error) {
DailyLimit: data.DailyLimit,
},
}
err = q.Resource.Create(resource)
err := q.Resource.Create(&resource)
if err != nil {
return nil, err
}
// 更新用户余额
user.Balance -= amount
_, err = q.User.
Where(q.User.ID.Eq(uid)).
Update(q.User.Balance, user.Balance)
if err != nil {
return nil, err
}
return resource, nil
return &resource, nil
}
func (s *resourceService) CancelResource(ctx context.Context, tradeNo string, at time.Time) error {
// 获取请求缓存
func (s *resourceService) CancelResource(ctx context.Context, tradeNo string, at time.Time, method TransactionMethod) error {
// 删除请求缓存
_, err := rds.Client.Del(ctx, tradeNo).Result()
if err != nil {
return err
}
// 取消交易
err = Transaction.RevokeTransaction(ctx, tradeNo, method)
if err != nil {
return err
}
// 更新订单状态
_, err = q.Trade.
Where(q.Trade.InnerNo.Eq(tradeNo)).
Select(q.Trade.Status, q.Trade.CancelAt).
Updates(m.Trade{
Status: 2,
CancelAt: common.LocalDateTime(at),
})
err = Transaction.FinishTransaction(ctx, q.Q, tradeNo, at)
if err != nil {
return err
}

324
web/services/transaction.go Normal file
View File

@@ -0,0 +1,324 @@
package services
import (
"context"
"errors"
"io"
"log/slog"
"net/http"
"platform/pkg/env"
"platform/pkg/u"
"platform/web/common"
g "platform/web/globals"
m "platform/web/models"
q "platform/web/queries"
"strconv"
"time"
"github.com/smartwalle/alipay/v3"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
)
var Transaction = &transactionService{}
type transactionService struct {
}
func (s *transactionService) PrepareTransaction(ctx context.Context, q *q.Query, uid int32, data *TransactionPrepareData) (*TransactionPrepareResult, error) {
var subject = data.Subject
var amount = data.Amount
var expire = data.ExpireAt
var tType = data.Type
var method = data.Method
// 生成订单号
tradeNo, err := ID.GenSerial(ctx)
if err != nil {
return nil, err
}
// 创建支付订单
var payUrl string
switch method {
// 调用支付宝支付接口
case TransactionMethodAlipay:
resp, err := g.Alipay.TradePagePay(alipay.TradePagePay{
QRPayMode: "4",
Trade: alipay.Trade{
ProductCode: "FAST_INSTANT_TRADE_PAY",
OutTradeNo: tradeNo,
Subject: subject,
TotalAmount: strconv.FormatFloat(amount, 'f', 2, 64),
TimeExpire: expire.Format("2006-01-02 15:04:05"),
},
})
if err != nil {
return nil, err
}
payUrl = resp.String()
// 调用微信支付接口
case TransactionMethodWeChat:
resp, _, err := g.WechatPay.Native.Prepay(ctx, native.PrepayRequest{
Appid: &env.WechatPayAppId,
Mchid: &env.WechatPayMchId,
OutTradeNo: &tradeNo,
Description: &subject,
TimeExpire: &expire,
NotifyUrl: &env.WechatPayCallbackUrl,
Amount: &native.Amount{
Total: u.P(int64(amount * 100)),
},
})
if err != nil {
return nil, err
}
payUrl = *resp.CodeUrl
// 不支持的支付方式
default:
return nil, errors.New("不支持的支付方式")
}
// 保存交易订单
var trade = m.Trade{
UserID: uid,
InnerNo: tradeNo,
Subject: subject,
Method: int32(method),
Type: int32(tType),
Amount: amount,
Status: 0, // 0-待支付
PayURL: payUrl,
}
err = q.Trade.Create(&trade)
if err != nil {
return nil, err
}
// 保存用户帐单
var bill = m.Bill{
BillNo: ID.GenReadable("bil"),
UserID: uid,
TradeID: trade.ID,
Info: subject,
Type: int32(tType),
Amount: amount,
}
err = q.Bill.
Omit(q.Bill.ResourceID, q.Bill.RefundID).
Create(&bill)
if err != nil {
return nil, err
}
return &TransactionPrepareResult{
TradeNo: tradeNo,
PayURL: payUrl,
Bill: &bill,
Trade: &trade,
}, nil
}
func (s *transactionService) VerifyTransaction(ctx context.Context, data *TransactionVerifyData) (*TransactionVerifyResult, error) {
var tradeNo = data.TradeNo
var method = data.Method
// 检查交易号是否存在
var transId string
var paidAt time.Time
var payment float64
switch method {
// 检查支付宝交易
case TransactionMethodAlipay:
resp, err := g.Alipay.TradeQuery(ctx, alipay.TradeQuery{
OutTradeNo: tradeNo,
})
if err != nil {
return nil, err
}
if resp.Code != alipay.CodeSuccess {
slog.Warn("支付宝交易取消失败", "code", resp.Code, "sub_code", resp.SubCode, "msg", resp.Msg)
return nil, errors.New("交易查询失败")
}
if resp.TradeStatus != alipay.TradeStatusSuccess {
return nil, errors.New("交易未完成")
}
transId = resp.TradeNo
payment, err = strconv.ParseFloat(resp.TotalAmount, 64)
if err != nil {
return nil, err
}
paidAt, err = time.Parse("2006-01-02 15:04:05", resp.SendPayDate)
if err != nil {
return nil, err
}
// 检查微信交易
case TransactionMethodWeChat:
resp, _, err := g.WechatPay.Native.QueryOrderByOutTradeNo(ctx, native.QueryOrderByOutTradeNoRequest{
OutTradeNo: &tradeNo,
Mchid: &env.WechatPayMchId,
})
if err != nil {
return nil, err
}
if *resp.TradeState != "SUCCESS" {
return nil, errors.New("交易未完成")
}
transId = *resp.TransactionId
payment = float64(*resp.Amount.PayerTotal) / 100
paidAt, err = time.Parse(time.RFC3339, *resp.SuccessTime)
if err != nil {
return nil, err
}
// 不支持的支付方式
default:
return nil, errors.New("不支持的支付方式")
}
return &TransactionVerifyResult{
TransId: transId,
Payment: payment,
Time: paidAt,
}, nil
}
func (s *transactionService) CompleteTransaction(ctx context.Context, q *q.Query, data *TransactionCompleteData) (*TransactionCompleteResult, error) {
var transId = data.TransId
var tradeNo = data.TradeNo
var payment = data.Payment
var paidAt = data.Time
// 获取交易信息
trade, err := q.Trade.WithContext(ctx).
Where(q.Trade.InnerNo.Eq(tradeNo)).
First()
if err != nil {
return nil, err
}
// 检查交易状态
if trade.Status != 0 {
return nil, nil
}
// 更新交易状态
trade.Status = 1
trade.OuterNo = transId
trade.Payment = payment
trade.PaidAt = common.LocalDateTime(paidAt)
trade.PayURL = ""
_, err = q.Trade.WithContext(ctx).Updates(trade)
if err != nil {
return nil, err
}
return &TransactionCompleteResult{
Trade: trade,
}, nil
}
func (s *transactionService) RevokeTransaction(ctx context.Context, tradeNo string, method TransactionMethod) error {
switch method {
case TransactionMethodAlipay:
resp, err := g.Alipay.TradeCancel(ctx, alipay.TradeCancel{
OutTradeNo: tradeNo,
})
if err != nil {
return err
}
if resp.Code != alipay.CodeSuccess {
slog.Warn("支付宝交易取消失败", "code", resp.Code, "sub_code", resp.SubCode, "msg", resp.Msg)
return errors.New("交易取消失败")
}
case TransactionMethodWeChat:
resp, err := g.WechatPay.Native.CloseOrder(ctx, native.CloseOrderRequest{
Mchid: &env.WechatPayMchId,
OutTradeNo: &tradeNo,
})
if err != nil {
return err
}
if resp.Response.StatusCode != http.StatusNoContent {
body, _ := io.ReadAll(resp.Response.Body)
slog.Warn("微信交易取消失败", "code", resp.Response.StatusCode, "body", string(body))
return errors.New("交易取消失败")
}
}
return nil
}
func (s *transactionService) FinishTransaction(ctx context.Context, q *q.Query, tradeNo string, time time.Time) error {
_, err := q.Trade.
Where(q.Trade.InnerNo.Eq(tradeNo)).
Select(q.Trade.Status, q.Trade.CancelAt, q.Trade.PayURL).
Updates(m.Trade{
Status: 2,
CancelAt: common.LocalDateTime(time),
PayURL: "",
})
if err != nil {
return err
}
return nil
}
type TransactionType int32
const (
TransactionTypeRecharge TransactionType = iota
TransactionTypePurchase
)
type TransactionMethod int32
const (
TransactionMethodAlipay TransactionMethod = iota
TransactionMethodWeChat
)
type TransactionPrepareData struct {
Subject string
Amount float64
ExpireAt time.Time
Type TransactionType
Method TransactionMethod
}
type TransactionPrepareResult struct {
TradeNo string
PayURL string
Bill *m.Bill
Trade *m.Trade
}
type TransactionVerifyData struct {
TradeNo string
Method TransactionMethod
}
type TransactionVerifyResult struct {
TransId string
Payment float64
Time time.Time
}
type TransactionCompleteData struct {
TradeNo string
TransactionVerifyResult
}
type TransactionCompleteResult struct {
Trade *m.Trade
}

46
web/services/user.go Normal file
View File

@@ -0,0 +1,46 @@
package services
import (
"context"
q "platform/web/queries"
)
var User = &userService{}
type userService struct{}
func (s *userService) RechargeConfirm(ctx context.Context, tradeNo string, verified *TransactionVerifyResult) error {
err := q.Q.Transaction(func(tx *q.Query) error {
// 更新交易状态
result, err := Transaction.CompleteTransaction(ctx, tx, &TransactionCompleteData{
TradeNo: tradeNo,
TransactionVerifyResult: *verified,
})
if err != nil {
return err
}
// 更新用户余额
user, err := tx.User.
Where(tx.User.ID.Eq(result.Trade.UserID)).Take()
if err != nil {
return err
}
_, err = tx.User.
Where(tx.User.ID.Eq(user.ID)).
Update(tx.User.Balance, user.Balance+result.Trade.Amount)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return nil
}