Files
mengyastore/mengyastore-backend-go/internal/storage/orderstore.go
2026-05-13 12:11:46 +08:00

242 lines
8.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}