package service import ( "context" "errors" "fmt" "log" "net/http" "time" "gorm.io/gorm" "github.com/wangjia/pay/internal/channel" "github.com/wangjia/pay/internal/model" "github.com/wangjia/pay/internal/util" ) var ( ErrProductNotFound = errors.New("套餐不存在或已下架") ErrAmountMismatch = errors.New("回调金额与订单金额不符") ) type OrderService struct { db *gorm.DB reg *channel.Registry baseURL string } func NewOrderService(db *gorm.DB, reg *channel.Registry, baseURL string) *OrderService { return &OrderService{db: db, reg: reg, baseURL: baseURL} } // prepare 校验套餐、取渠道、落库一张待支付订单(金额一律取服务端套餐价,不信任前端)。 func (s *OrderService) prepare(productID uint64, clientIP string) (channel.Channel, *model.Merchant, *model.Order, error) { var p model.Product if err := s.db.First(&p, "id = ? AND active = ?", productID, true).Error; err != nil { return nil, nil, nil, ErrProductNotFound } ch, m, err := s.reg.ByMerchantID(p.MerchantID) if err != nil { return nil, nil, nil, err } order := &model.Order{ OutTradeNo: util.NewOutTradeNo(m.Code), MerchantID: m.ID, Channel: m.Channel, ProductID: p.ID, Subject: p.Name, Amount: p.Price, // 权威金额 Status: model.OrderPending, ClientIP: clientIP, } if err := s.db.Create(order).Error; err != nil { return nil, nil, nil, fmt.Errorf("创建订单失败: %w", err) } return ch, m, order, nil } func (s *OrderService) notifyURL(channel string) string { return s.baseURL + "/api/v1/notify/" + channel } // Create 网页支付下单,返回收银台跳转 URL。 func (s *OrderService) Create(ctx context.Context, productID uint64, clientIP string) (string, *model.Order, error) { ch, m, order, err := s.prepare(productID, clientIP) if err != nil { return "", nil, err } payURL, err := ch.PagePay(ctx, channel.CreateReq{ OutTradeNo: order.OutTradeNo, Subject: order.Subject, Amount: order.Amount, NotifyURL: s.notifyURL(m.Channel), ReturnURL: s.baseURL + "/result?out_trade_no=" + order.OutTradeNo, }) if err != nil { return "", nil, err } return payURL, order, nil } // CreateQR 扫码(当面付)下单,返回二维码码串供前端渲染。 func (s *OrderService) CreateQR(ctx context.Context, productID uint64, clientIP string) (string, *model.Order, error) { ch, m, order, err := s.prepare(productID, clientIP) if err != nil { return "", nil, err } qr, err := ch.PreCreate(ctx, channel.CreateReq{ OutTradeNo: order.OutTradeNo, Subject: order.Subject, Amount: order.Amount, NotifyURL: s.notifyURL(m.Channel), }) if err != nil { return "", nil, err } return qr, order, nil } // HandleAlipayNotify 处理支付宝异步回调:反查商户 → 验签 → 核对金额 → 幂等更新。 // 返回 nil 表示已正确处理(调用方应给支付宝回 "success")。 func (s *OrderService) HandleAlipayNotify(ctx context.Context, r *http.Request) error { if err := r.ParseForm(); err != nil { return fmt.Errorf("解析回调失败: %w", err) } appID := r.PostFormValue("app_id") outTradeNo := r.PostFormValue("out_trade_no") ch, m, err := s.reg.AlipayByAppID(appID) if err != nil { s.logNotify("alipay", outTradeNo, false, "not_found", r.Form.Encode()) return err } res, err := ch.VerifyNotify(ctx, r) if err != nil { s.logNotify("alipay", outTradeNo, false, "verify_failed", r.Form.Encode()) return err } result, err := s.applyPaid(m, res) s.logNotify("alipay", res.OutTradeNo, true, result, res.Raw) return err } // applyPaid 在一个事务里完成「金额核对 + 幂等置为已支付」。返回处理结果标记。 func (s *OrderService) applyPaid(m *model.Merchant, res *channel.NotifyResult) (string, error) { if !res.Paid { return "ignored", nil // 非成功状态(如 WAIT_BUYER_PAY),确认收到即可 } var resultTag string err := s.db.Transaction(func(tx *gorm.DB) error { var o model.Order if err := tx.First(&o, "out_trade_no = ? AND merchant_id = ?", res.OutTradeNo, m.ID).Error; err != nil { resultTag = "not_found" return fmt.Errorf("订单不存在: %s", res.OutTradeNo) } if o.Status == model.OrderPaid { resultTag = "duplicate" // 幂等:已处理过,直接成功返回 return nil } if !util.AmountEqual(o.Amount, res.Amount) { resultTag = "amount_mismatch" return ErrAmountMismatch } now := time.Now() upd := tx.Model(&model.Order{}). Where("out_trade_no = ? AND status = ?", o.OutTradeNo, model.OrderPending). Updates(map[string]any{ "status": model.OrderPaid, "trade_no": res.TradeNo, "buyer_logon_id": res.BuyerLogonID, "paid_at": &now, }) if upd.Error != nil { return upd.Error } if upd.RowsAffected == 0 { resultTag = "duplicate" // 并发下被另一路(如查单)先置位 return nil } resultTag = "processed" log.Printf("[notify] 订单 %s 已支付 trade_no=%s amount=%s", o.OutTradeNo, res.TradeNo, res.Amount) return nil }) return resultTag, err } func (s *OrderService) logNotify(ch, outTradeNo string, verified bool, result, raw string) { _ = s.db.Create(&model.NotifyLog{ Channel: ch, OutTradeNo: outTradeNo, Verified: verified, Result: result, Raw: raw, }).Error } // GetByOutTradeNo 供前端结果页轮询。 func (s *OrderService) GetByOutTradeNo(outTradeNo string) (*model.Order, error) { var o model.Order if err := s.db.First(&o, "out_trade_no = ?", outTradeNo).Error; err != nil { return nil, err } return &o, nil } // SyncPending 兜底:把近期待支付订单拿去主动查单,命中已支付则补记(防回调丢失)。 func (s *OrderService) SyncPending(ctx context.Context, maxAge time.Duration) { var orders []model.Order cutoff := time.Now().Add(-maxAge) if err := s.db.Where("status = ? AND created_at > ?", model.OrderPending, cutoff). Limit(100).Find(&orders).Error; err != nil { log.Printf("[query_sync] 查询待支付订单失败: %v", err) return } for i := range orders { o := &orders[i] ch, m, err := s.reg.ByMerchantID(o.MerchantID) if err != nil { continue } qr, err := ch.Query(ctx, o.OutTradeNo) if err != nil || qr == nil || !qr.Found || !qr.Paid { continue } result, _ := s.applyPaid(m, &channel.NotifyResult{ OutTradeNo: qr.OutTradeNo, TradeNo: qr.TradeNo, Amount: qr.Amount, Paid: true, }) if result == "processed" { log.Printf("[query_sync] 订单 %s 经主动查单补记为已支付", o.OutTradeNo) } } } // StartQuerySync 启动后台查单兜底循环。 func (s *OrderService) StartQuerySync(interval, maxAge time.Duration) { go func() { ticker := time.NewTicker(interval) defer ticker.Stop() for range ticker.C { s.SyncPending(context.Background(), maxAge) } }() }