Files
platform/web/services/transaction.go

394 lines
8.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 services
import (
"context"
"errors"
"io"
"log/slog"
"net/http"
"platform/pkg/env"
"platform/pkg/u"
bill2 "platform/web/domains/bill"
coupon2 "platform/web/domains/coupon"
trade2 "platform/web/domains/trade"
g "platform/web/globals"
"platform/web/globals/orm"
m "platform/web/models"
q "platform/web/queries"
"strconv"
"time"
"github.com/smartwalle/alipay/v3"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
"gorm.io/gorm"
)
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 expire = data.ExpireAt
var tType = data.Type
var method = data.Method
var amount = data.Amount
// 实际支付金额,只在创建真实订单时使用
var amountReal = data.Amount
if env.RunMode == "debug" {
amountReal = 0.01
}
// 附加优惠券
if data.CouponCode != "" {
coupon, err := q.Coupon.WithContext(ctx).
Where(
q.Coupon.Code.Eq(data.CouponCode),
q.Coupon.Status.Eq(int32(coupon2.StatusUnused)),
).
Take()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("优惠券不存在或已失效")
}
return nil, err
}
var expireAt = time.Time(coupon.ExpireAt)
if !expireAt.IsZero() && expireAt.Before(time.Now()) {
_, err = q.Coupon.
Where(q.Coupon.ID.Eq(coupon.ID)).
Update(q.Coupon.Status, coupon2.StatusExpired)
if err != nil {
return nil, err
}
return nil, errors.New("优惠券已过期")
}
if data.Amount < coupon.MinAmount {
return nil, errors.New("订单金额未达到使用优惠券的条件")
}
switch {
// 该优惠券不属于当前用户
default:
return nil, errors.New("优惠券不属于当前用户")
// 公开优惠券
case coupon.UserID == 0:
amount = amount - coupon.Amount
// 指定用户的优惠券
case coupon.UserID == uid:
amount = amount - coupon.Amount
if time.Time(coupon.ExpireAt).IsZero() {
_, err = q.Coupon.
Where(q.Coupon.ID.Eq(coupon.ID)).
Update(q.Coupon.Status, int32(coupon2.StatusUsed))
if err != nil {
return nil, err
}
}
}
}
// 生成订单号
tradeNo, err := ID.GenSerial(ctx)
if err != nil {
return nil, err
}
// 创建支付订单
var payUrl string
switch method {
// 调用支付宝支付接口
case trade2.MethodAlipay:
resp, err := g.Alipay.TradePagePay(alipay.TradePagePay{
QRPayMode: "4",
QRCodeWidth: "196", // 二维码宽度需要-4支付宝页面布局有问题
Trade: alipay.Trade{
ProductCode: "FAST_INSTANT_TRADE_PAY",
OutTradeNo: tradeNo,
Subject: subject,
TotalAmount: strconv.FormatFloat(amountReal, 'f', 2, 64),
TimeExpire: expire.Format("2006-01-02 15:04:05"),
},
})
if err != nil {
return nil, err
}
payUrl = resp.String()
// 调用微信支付接口
case trade2.MethodWeChat:
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(amountReal * 100)),
},
})
if err != nil {
return nil, err
}
payUrl = *resp.CodeUrl
// 不支持的支付方式
default:
return nil, ErrTransactionNotSupported
}
// 保存交易订单
var billType bill2.Type
switch tType {
case trade2.TypeRecharge:
billType = bill2.TypeRecharge
case trade2.TypePurchase:
billType = bill2.TypeConsume
}
var trade = m.Trade{
UserID: uid,
InnerNo: tradeNo,
Subject: subject,
Method: int32(method),
Type: int32(tType),
Amount: amount,
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(billType),
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 trade2.MethodAlipay:
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, ErrTransactionNotPaid
}
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 trade2.MethodWeChat:
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, ErrTransactionNotPaid
}
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, ErrTransactionNotSupported
}
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 != int32(trade2.StatusPending) {
return nil, nil
}
// 更新交易状态
trade.Status = int32(trade2.StatusSuccess)
trade.OuterNo = transId
trade.Payment = payment
trade.PaidAt = orm.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 trade2.Method) error {
switch method {
case trade2.MethodAlipay:
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 trade2.MethodWeChat:
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: int32(trade2.StatusCanceled),
CancelAt: orm.LocalDateTime(time),
PayURL: "",
})
if err != nil {
return err
}
return nil
}
type TransactionPrepareData struct {
Subject string
Amount float64
ExpireAt time.Time
Type trade2.Type
Method trade2.Method
CouponCode string
}
type TransactionPrepareResult struct {
TradeNo string
PayURL string
Bill *m.Bill
Trade *m.Trade
}
type TransactionVerifyData struct {
TradeNo string
Method trade2.Method
}
type TransactionVerifyResult struct {
TransId string
Payment float64
Time time.Time
}
type TransactionCompleteData struct {
TradeNo string
TransactionVerifyResult
}
type TransactionCompleteResult struct {
Trade *m.Trade
}
type TransactionErr string
func (e TransactionErr) Error() string {
return string(e)
}
var (
ErrTransactionNotPaid = TransactionErr("交易未支付")
ErrTransactionNotSupported = TransactionErr("不支持的支付方式")
)