package storage import ( "errors" "fmt" "time" "github.com/google/uuid" "gorm.io/gorm" "gorm.io/gorm/clause" "mengyastore-backend/internal/database" "mengyastore-backend/internal/models" ) const amountMatchEpsilon = 0.009 type OrderStore struct { db *gorm.DB } func NewOrderStore(db *gorm.DB) (*OrderStore, error) { return &OrderStore{db: db}, nil } func orderRowToModel(row database.OrderRow) models.Order { return models.Order{ ID: row.ID, ProductID: row.ProductID, ProductName: row.ProductName, UserAccount: row.UserAccount, UserName: row.UserName, Quantity: row.Quantity, DeliveredCodes: row.DeliveredCodes, Status: row.Status, DeliveryMode: row.DeliveryMode, Note: row.Note, ContactPhone: row.ContactPhone, ContactEmail: row.ContactEmail, NotifyEmail: row.NotifyEmail, PaymentMethod: row.PaymentMethod, PaymentExpectedTotal: row.PaymentExpectedTotal, PaymentExpiresAt: row.PaymentExpiresAt, CreatedAt: row.CreatedAt, } } func (s *OrderStore) Create(order models.Order) (models.Order, error) { if order.ID == "" { order.ID = uuid.NewString() } if len(order.DeliveredCodes) == 0 { order.DeliveredCodes = []string{} } row := database.OrderRow{ ID: order.ID, ProductID: order.ProductID, ProductName: order.ProductName, UserAccount: order.UserAccount, UserName: order.UserName, Quantity: order.Quantity, DeliveredCodes: database.StringSlice(order.DeliveredCodes), Status: order.Status, DeliveryMode: order.DeliveryMode, Note: order.Note, ContactPhone: order.ContactPhone, ContactEmail: order.ContactEmail, NotifyEmail: order.NotifyEmail, PaymentMethod: order.PaymentMethod, PaymentExpectedTotal: order.PaymentExpectedTotal, PaymentExpiresAt: order.PaymentExpiresAt, } if err := s.db.Create(&row).Error; err != nil { return models.Order{}, err } order.CreatedAt = row.CreatedAt order.PaymentExpiresAt = row.PaymentExpiresAt return order, nil } // CreateTx 在已有事务内创建订单。 func (s *OrderStore) CreateTx(tx *gorm.DB, order models.Order) (models.Order, error) { if order.ID == "" { order.ID = uuid.NewString() } if len(order.DeliveredCodes) == 0 { order.DeliveredCodes = []string{} } row := database.OrderRow{ ID: order.ID, ProductID: order.ProductID, ProductName: order.ProductName, UserAccount: order.UserAccount, UserName: order.UserName, Quantity: order.Quantity, DeliveredCodes: database.StringSlice(order.DeliveredCodes), Status: order.Status, DeliveryMode: order.DeliveryMode, Note: order.Note, ContactPhone: order.ContactPhone, ContactEmail: order.ContactEmail, NotifyEmail: order.NotifyEmail, PaymentMethod: order.PaymentMethod, PaymentExpectedTotal: order.PaymentExpectedTotal, PaymentExpiresAt: order.PaymentExpiresAt, } if err := tx.Create(&row).Error; err != nil { return models.Order{}, err } order.CreatedAt = row.CreatedAt order.PaymentExpiresAt = row.PaymentExpiresAt return order, nil } func (s *OrderStore) GetByID(id string) (models.Order, error) { var row database.OrderRow if err := s.db.First(&row, "id = ?", id).Error; err != nil { return models.Order{}, fmt.Errorf("order not found") } return orderRowToModel(row), nil } func (s *OrderStore) Confirm(id string) (models.Order, error) { var row database.OrderRow if err := s.db.First(&row, "id = ?", id).Error; err != nil { return models.Order{}, fmt.Errorf("order not found") } if err := s.db.Model(&row).Update("status", "completed").Error; err != nil { return models.Order{}, err } row.Status = "completed" return orderRowToModel(row), nil } func (s *OrderStore) ListByAccount(account string) ([]models.Order, error) { var rows []database.OrderRow if err := s.db.Where("user_account = ?", account).Order("created_at DESC").Find(&rows).Error; err != nil { return nil, err } orders := make([]models.Order, len(rows)) for i, r := range rows { orders[i] = orderRowToModel(r) } return orders, nil } func (s *OrderStore) ListAll() ([]models.Order, error) { var rows []database.OrderRow if err := s.db.Order("created_at DESC").Find(&rows).Error; err != nil { return nil, err } orders := make([]models.Order, len(rows)) for i, r := range rows { orders[i] = orderRowToModel(r) } return orders, nil } func (s *OrderStore) CountPurchasedByAccount(account, productID string) (int, error) { var total int64 // pending:手动待发货;pending_payment:萌芽支付待到账(会计入限购)。 err := s.db.Model(&database.OrderRow{}). Where("user_account = ? AND product_id = ? AND status IN ?", account, productID, []string{"pending", "pending_payment", "completed"}). Select("COALESCE(SUM(quantity), 0)").Scan(&total).Error return int(total), err } func (s *OrderStore) Count() (int, error) { var count int64 if err := s.db.Model(&database.OrderRow{}).Count(&count).Error; err != nil { return 0, err } return int(count), nil } func (s *OrderStore) Delete(id string) error { return s.db.Delete(&database.OrderRow{}, "id = ?", id).Error } func (s *OrderStore) UpdateCodes(id string, codes []string) error { return s.db.Model(&database.OrderRow{}).Where("id = ?", id). Update("delivered_codes", database.StringSlice(codes)).Error } // TryFulfillMengyaPayment 在事务内按 FIFO 匹配一笔待支付订单并置为 completed、增加销量。无匹配时 ok=false。 func (s *OrderStore) TryFulfillMengyaPayment(amount float64, now time.Time) (order models.Order, ok bool, err error) { err = s.db.Transaction(func(tx *gorm.DB) error { var row database.OrderRow res := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("status = ? AND payment_expires_at IS NOT NULL AND payment_expires_at > ?", "pending_payment", now). Where("ABS(payment_expected_total - ?) < ?", amount, amountMatchEpsilon). Order("created_at ASC"). First(&row) if res.Error != nil { if errors.Is(res.Error, gorm.ErrRecordNotFound) { return nil } return res.Error } if err := tx.Model(&database.OrderRow{}).Where("id = ?", row.ID).Update("status", "completed").Error; err != nil { return err } if err := tx.Model(&database.ProductRow{}).Where("id = ?", row.ProductID). UpdateColumn("total_sold", gorm.Expr("total_sold + ?", row.Quantity)).Error; err != nil { return err } row.Status = "completed" order = orderRowToModel(row) ok = true return nil }) if err != nil { return models.Order{}, false, err } return order, ok, nil } // ListExpiredPendingPayments 返回已过期仍未支付的订单(用于释放预留库存)。 func (s *OrderStore) ListExpiredPendingPayments(now time.Time) ([]models.Order, error) { var rows []database.OrderRow if err := s.db.Where("status = ? AND payment_expires_at IS NOT NULL AND payment_expires_at <= ?", "pending_payment", now). Find(&rows).Error; err != nil { return nil, err } out := make([]models.Order, len(rows)) for i, r := range rows { out[i] = orderRowToModel(r) } return out, nil } // CancelPendingStaleIfEligible 仅在仍为 pending_payment 时将订单作废并清空预留内容;返回值 ok 表示成功取消(可安全退回库存)。 func (s *OrderStore) CancelPendingStaleIfEligible(id string) (ok bool, err error) { res := s.db.Model(&database.OrderRow{}). Where("id = ? AND status = ?", id, "pending_payment"). Updates(map[string]interface{}{ "status": "cancelled", "delivered_codes": database.StringSlice([]string{}), }) return res.RowsAffected > 0, res.Error }