package services import ( "context" "errors" "io" "log/slog" "net/http" "platform/pkg/env" "platform/pkg/u" "platform/web/core" bill2 "platform/web/domains/bill" coupon2 "platform/web/domains/coupon" trade2 "platform/web/domains/trade" 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" "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", 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 = core.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: core.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 } var ( ErrTransactionNotPaid = core.NewErr("transaction", "交易未完成") ErrTransactionNotSupported = core.NewErr("transaction", "不支持的支付方式") )