package services import ( "context" "errors" "github.com/shopspring/decimal" "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" "platform/web/globals/orm" m "platform/web/models" q "platform/web/queries" "platform/web/tasks" "time" "github.com/smartwalle/alipay/v3" "github.com/wechatpay-apiv3/wechatpay-go/services/payments/native" "gorm.io/gorm" ) var Trade = &tradeService{} type tradeService struct { } func (s *tradeService) CreateTrade(q *q.Query, uid int32, now time.Time, data *TradeCreateData) (*TradeCreateResult, error) { var subject = data.Subject var expire = data.ExpireAt var tType = data.Type var method = data.Method var platform = data.Platform var amount = data.Amount // 实际支付金额,只在创建真实订单时使用 var amountReal = data.Amount if env.RunMode == "debug" { amountReal = decimal.NewFromFloat(0.01) } // 附加优惠券 if data.CouponCode != "" { coupon, err := q.Coupon. 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(u.Z(coupon.ExpireAt)) if !expireAt.IsZero() && expireAt.Before(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 amount.Cmp(coupon.MinAmount) < 0 { return nil, errors.New("订单金额未达到使用优惠券的条件") } if coupon.UserID != nil { switch *coupon.UserID { // 指定用户的优惠券 case uid: amount = amount.Sub(coupon.Amount) if 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 } } // 该优惠券不属于当前用户 default: return nil, errors.New("优惠券不属于当前用户") } } else { // 公开优惠券 amount = amount.Sub(coupon.Amount) } } // 生成订单号 tradeNo, err := ID.GenSerial(context.Background()) if err != nil { return nil, err } // 创建支付订单 var payUrl string switch { // 支付宝 + 电脑网站 case method == trade2.MethodAlipay && platform == trade2.PlatformDesktop: 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: amountReal.StringFixed(2), TimeExpire: expire.Format("2006-01-02 15:04:05"), }, }) if err != nil { return nil, err } payUrl = resp.String() // 微信 + 电脑网站 case method == trade2.MethodWeChat && platform == trade2.PlatformDesktop: resp, _, err := g.WechatPay.Native.Prepay(context.Background(), native.PrepayRequest{ Appid: &env.WechatPayAppId, Mchid: &env.WechatPayMchId, OutTradeNo: &tradeNo, Description: &subject, TimeExpire: &expire, NotifyUrl: &env.WechatPayCallbackUrl, Amount: &native.Amount{ Total: u.P(amountReal.Mul(decimal.NewFromInt(100)).Round(0).IntPart()), }, }) if err != nil { return nil, err } payUrl = *resp.CodeUrl // 商福通 + 电脑网站 case method == trade2.MethodSft && platform == trade2.PlatformDesktop: resp, err := g.SFTPay.PaymentScanPay(&g.PaymentScanPayReq{ MchOrderNo: tradeNo, Subject: subject, Body: subject, Amount: amountReal.Mul(decimal.NewFromInt(100)).Round(0).IntPart(), Currency: "cny", ClientIp: "", OrderTimeout: u.P(expire.Format("2006-01-02 15:04:05")), }) if err != nil { return nil, err } payUrl = u.Z(u.Z(resp.PayInfo).QrCodeUrl) // 商福通 + 手机网站 case (method == trade2.MethodSftAlipay || method == trade2.MethodSftWeChat) && platform == trade2.PlatformMobile: var payType g.SftPayType if method == trade2.MethodSftAlipay { payType = g.SftAlipay } else { payType = g.SftWeChat } resp, err := g.SFTPay.PaymentH5Pay(&g.PaymentH5PayReq{ MchOrderNo: tradeNo, Subject: subject, Body: subject, Amount: amountReal.Mul(decimal.NewFromInt(100)).Round(0).IntPart(), PayType: payType, Currency: "cny", ClientIp: "", OrderTimeout: u.P(expire.Format("2006-01-02 15:04:05")), }) if err != nil { return nil, err } payUrl = u.Z(u.Z(resp.PayInfo).PayUrl) // 不支持的支付方式 default: slog.Warn(ErrTransactionNotSupported.Error(), "method", method, "platform", platform) return nil, ErrTransactionNotSupported } // 保存交易订单 var trade = m.Trade{ UserID: uid, InnerNo: tradeNo, Subject: subject, Type: int32(tType), Method: int32(method), Platform: int32(platform), Amount: amount, PayURL: &payUrl, } err = q.Trade.Create(&trade) if err != nil { return nil, err } // 保存用户帐单 var billType bill2.Type switch tType { case trade2.TypeRecharge: billType = bill2.TypeRecharge case trade2.TypePurchase: billType = bill2.TypeConsume } 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 } // 提交异步任务更新订单状态 _, err = g.Asynq.Enqueue(tasks.NewUpdateTrade(tradeNo, method)) if err != nil { return nil, err } return &TradeCreateResult{ TradeNo: tradeNo, PayURL: payUrl, Bill: &bill, Trade: &trade, }, nil } func (s *tradeService) OnTradeCreated(q *q.Query, data *OnTradeCreateData) (*m.Trade, error) { var transId = data.TransId var tradeNo = data.TradeNo var payment = data.Payment var paidAt = data.Time var acquirer = data.Acquirer // 获取交易信息 trade, err := q.Trade. Where(q.Trade.InnerNo.Eq(tradeNo)). First() if err != nil { return nil, err } // 检查交易状态 switch trade2.Status(trade.Status) { // 如果已退款或取消,则返回错误 case trade2.StatusCanceled, trade2.StatusRefunded: return nil, errors.New("交易已取消或已退款") // 如果是未支付,则更新支付状态 case trade2.StatusPending: trade.Status = int32(trade2.StatusSuccess) trade.OuterNo = &transId trade.Payment = payment trade.Acquirer = int32(acquirer) trade.PaidAt = u.P(orm.LocalDateTime(paidAt)) trade.PayURL = u.P("") _, err = q.Trade.Updates(trade) if err != nil { return nil, err } case trade2.StatusSuccess: } return trade, nil } func (s *tradeService) CancelTrade(tradeNo string, method trade2.Method) error { switch method { case trade2.MethodAlipay: resp, err := g.Alipay.TradeCancel(context.Background(), 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(context.Background(), 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("交易取消失败") } case trade2.MethodSft: resp, err := g.SFTPay.OrderClose(&g.OrderCloseReq{ MchOrderNo: &tradeNo, }) if err != nil { return err } if resp.State != "TRADE_CLOSE" { slog.Warn("商福通交易取消失败", "state", resp.State) return errors.New("交易取消失败") } default: return ErrTransactionNotSupported } return nil } func (s *tradeService) OnTradeCanceled(q *q.Query, tradeNo string, now 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: u.P(orm.LocalDateTime(now)), PayURL: u.P(""), }) if err != nil { return err } return nil } func (s *tradeService) SendRefundTrade(tradeNo string, method trade2.Method) error { panic("todo") } func (s *tradeService) OnTradeRefunded(q *q.Query, tradeNo string, now time.Time) error { panic("todo") } func (s *tradeService) VerifyTrade(data *TradeVerifyData) (*TradeSuccessResult, error) { var tradeNo = data.TradeNo var method = data.Method // 检查交易号是否存在 var transId string var paidAt time.Time var payment decimal.Decimal switch method { // 检查支付宝交易 case trade2.MethodAlipay: resp, err := g.Alipay.TradeQuery(context.Background(), 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 = decimal.NewFromString(resp.TotalAmount) 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(context.Background(), native.QueryOrderByOutTradeNoRequest{ OutTradeNo: &tradeNo, Mchid: &env.WechatPayMchId, }) if err != nil { return nil, err } if *resp.TradeState != "SUCCESS" { return nil, ErrTransactionNotPaid } transId = *resp.TransactionId payment = decimal.NewFromInt(*resp.Amount.PayerTotal).Div(decimal.NewFromInt(100)) paidAt, err = time.Parse(time.RFC3339, *resp.SuccessTime) if err != nil { return nil, err } // 检查商福通交易 case trade2.MethodSft, trade2.MethodSftAlipay, trade2.MethodSftWeChat: resp, err := g.SFTPay.QueryTrade(&g.QueryTradeReq{ MchOrderNo: &tradeNo, }) if err != nil { return nil, err } if resp.State != g.SftTradeSuccess { return nil, ErrTransactionNotPaid } if resp.PayOrderId == nil { return nil, errors.New("商福通交易号不存在") } if resp.PayTime == nil { return nil, errors.New("商福通交易时间不存在") } transId = *resp.PayOrderId payment = decimal.NewFromInt(resp.Amount).Div(decimal.NewFromInt(100)) paidAt, err = time.Parse("2006-01-02 15:04:05", *resp.PayTime) if err != nil { return nil, err } // 不支持的支付方式 default: return nil, ErrTransactionNotSupported } return &TradeSuccessResult{ TransId: transId, Payment: payment, Time: paidAt, }, nil } type TradeCreateData struct { Subject string Amount decimal.Decimal ExpireAt time.Time Type trade2.Type Method trade2.Method Platform trade2.Platform CouponCode string } type TradeCreateResult struct { TradeNo string PayURL string Bill *m.Bill Trade *m.Trade } type TradeVerifyData struct { TradeNo string Method trade2.Method } type TradeSuccessResult struct { TransId string Payment decimal.Decimal Time time.Time Acquirer trade2.Acquirer } type OnTradeCreateData struct { TradeNo string TradeSuccessResult } type TradeErr string func (e TradeErr) Error() string { return string(e) } var ( ErrTransactionNotPaid = core.NewBizErr("交易未支付") ErrTransactionNotSupported = core.NewBizErr("不支持的支付方式") )