242 lines
8.0 KiB
Go
242 lines
8.0 KiB
Go
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
|
||
}
|