286 lines
8.5 KiB
Go
286 lines
8.5 KiB
Go
package dao
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"strings"
|
||
"time"
|
||
|
||
tokenmodel "github.com/LoveLosita/smartflow/backend/services/tokenstore/model"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/clause"
|
||
)
|
||
|
||
// TokenStoreDAO 承载 token-store 私有表的持久化访问。
|
||
//
|
||
// 职责边界:
|
||
// 1. 只访问 token_products、token_orders、token_grants、token_reward_rules。
|
||
// 2. 只提供查询、事务和原子状态更新,不组装 RPC/HTTP 视图。
|
||
// 3. 业务状态机、幂等回退和提示文案由 sv 层负责。
|
||
type TokenStoreDAO struct {
|
||
db *gorm.DB
|
||
}
|
||
|
||
func NewTokenStoreDAO(db *gorm.DB) *TokenStoreDAO {
|
||
return &TokenStoreDAO{db: db}
|
||
}
|
||
|
||
func (dao *TokenStoreDAO) WithTx(tx *gorm.DB) *TokenStoreDAO {
|
||
return &TokenStoreDAO{db: tx}
|
||
}
|
||
|
||
// Transaction 在一个数据库事务内执行 token-store 写操作。
|
||
func (dao *TokenStoreDAO) Transaction(ctx context.Context, fn func(txDAO *TokenStoreDAO) error) error {
|
||
return dao.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
return fn(dao.WithTx(tx))
|
||
})
|
||
}
|
||
|
||
type ListTokenOrdersQuery struct {
|
||
UserID uint64
|
||
Page int
|
||
PageSize int
|
||
Status string
|
||
}
|
||
|
||
type ListTokenGrantsQuery struct {
|
||
UserID uint64
|
||
Page int
|
||
PageSize int
|
||
Source string
|
||
}
|
||
|
||
type TokenGrantSummary struct {
|
||
RecordedTokenTotal int64
|
||
AppliedTokenTotal int64
|
||
}
|
||
|
||
func (dao *TokenStoreDAO) ListActiveProducts(ctx context.Context) ([]tokenmodel.TokenProduct, error) {
|
||
var products []tokenmodel.TokenProduct
|
||
err := dao.db.WithContext(ctx).
|
||
Where("status = ?", tokenmodel.TokenProductStatusActive).
|
||
Order("sort_order ASC, id ASC").
|
||
Find(&products).Error
|
||
return products, err
|
||
}
|
||
|
||
// FindRewardRuleBySource 按来源读取社区奖励规则。
|
||
//
|
||
// 职责边界:
|
||
// 1. 只读取 token_reward_rules,不计算最终发放金额,也不判断停用语义;
|
||
// 2. 未找到规则时返回 nil,由服务层决定配置或默认值兜底;
|
||
// 3. source 在 DAO 层做一次规范化,避免大小写和空格造成规则漏命中。
|
||
func (dao *TokenStoreDAO) FindRewardRuleBySource(ctx context.Context, source string) (*tokenmodel.TokenRewardRule, error) {
|
||
source = strings.ToLower(strings.TrimSpace(source))
|
||
if source == "" {
|
||
return nil, nil
|
||
}
|
||
|
||
var rule tokenmodel.TokenRewardRule
|
||
err := dao.db.WithContext(ctx).
|
||
Where("source = ?", source).
|
||
First(&rule).Error
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, nil
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &rule, nil
|
||
}
|
||
|
||
func (dao *TokenStoreDAO) FindActiveProductByID(ctx context.Context, productID uint64) (*tokenmodel.TokenProduct, error) {
|
||
var product tokenmodel.TokenProduct
|
||
err := dao.db.WithContext(ctx).
|
||
Where("id = ? AND status = ?", productID, tokenmodel.TokenProductStatusActive).
|
||
First(&product).Error
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, nil
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &product, nil
|
||
}
|
||
|
||
func (dao *TokenStoreDAO) FindOrderByUserIdempotencyKey(ctx context.Context, userID uint64, key string) (*tokenmodel.TokenOrder, error) {
|
||
var order tokenmodel.TokenOrder
|
||
err := dao.db.WithContext(ctx).
|
||
Where("user_id = ? AND idempotency_key = ?", userID, key).
|
||
First(&order).Error
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, nil
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &order, nil
|
||
}
|
||
|
||
func (dao *TokenStoreDAO) CreateOrder(ctx context.Context, order *tokenmodel.TokenOrder) error {
|
||
return dao.db.WithContext(ctx).Create(order).Error
|
||
}
|
||
|
||
func (dao *TokenStoreDAO) CountOrders(ctx context.Context, query ListTokenOrdersQuery) (int64, error) {
|
||
db := dao.db.WithContext(ctx).
|
||
Model(&tokenmodel.TokenOrder{}).
|
||
Where("user_id = ?", query.UserID)
|
||
if status := strings.TrimSpace(query.Status); status != "" {
|
||
db = db.Where("status = ?", status)
|
||
}
|
||
|
||
var total int64
|
||
err := db.Count(&total).Error
|
||
return total, err
|
||
}
|
||
|
||
func (dao *TokenStoreDAO) ListOrders(ctx context.Context, query ListTokenOrdersQuery) ([]tokenmodel.TokenOrder, error) {
|
||
db := dao.db.WithContext(ctx).
|
||
Where("user_id = ?", query.UserID)
|
||
if status := strings.TrimSpace(query.Status); status != "" {
|
||
db = db.Where("status = ?", status)
|
||
}
|
||
|
||
var orders []tokenmodel.TokenOrder
|
||
err := db.Order("created_at DESC, id DESC").
|
||
Offset((query.Page - 1) * query.PageSize).
|
||
Limit(query.PageSize).
|
||
Find(&orders).Error
|
||
return orders, err
|
||
}
|
||
|
||
func (dao *TokenStoreDAO) FindOrderByID(ctx context.Context, orderID uint64) (*tokenmodel.TokenOrder, error) {
|
||
var order tokenmodel.TokenOrder
|
||
err := dao.db.WithContext(ctx).Where("id = ?", orderID).First(&order).Error
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, nil
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &order, nil
|
||
}
|
||
|
||
func (dao *TokenStoreDAO) LockOrderByID(ctx context.Context, orderID uint64) (*tokenmodel.TokenOrder, error) {
|
||
var order tokenmodel.TokenOrder
|
||
err := dao.db.WithContext(ctx).
|
||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||
Where("id = ?", orderID).
|
||
First(&order).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &order, nil
|
||
}
|
||
|
||
// UpdateOrderState 只负责把订单持久化到最新状态。
|
||
//
|
||
// 职责边界:
|
||
// 1. 调用方必须先完成状态机判断,并决定最终 status/paid_at/granted_at。
|
||
// 2. 这里不做“是否允许从 A -> B”校验,避免 DAO 层承载业务规则。
|
||
// 3. payment_mode 允许调用方显式回填,保证 mock paid 后订单快照完整。
|
||
func (dao *TokenStoreDAO) UpdateOrderState(ctx context.Context, orderID uint64, status string, paidAt *time.Time, grantedAt *time.Time, paymentMode string) error {
|
||
updates := map[string]any{
|
||
"status": status,
|
||
"paid_at": paidAt,
|
||
"granted_at": grantedAt,
|
||
"payment_mode": paymentMode,
|
||
"updated_at": time.Now(),
|
||
}
|
||
return dao.db.WithContext(ctx).
|
||
Model(&tokenmodel.TokenOrder{}).
|
||
Where("id = ?", orderID).
|
||
Updates(updates).Error
|
||
}
|
||
|
||
func (dao *TokenStoreDAO) FindGrantByEventID(ctx context.Context, eventID string) (*tokenmodel.TokenGrant, error) {
|
||
var grant tokenmodel.TokenGrant
|
||
err := dao.db.WithContext(ctx).
|
||
Where("event_id = ?", eventID).
|
||
First(&grant).Error
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, nil
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &grant, nil
|
||
}
|
||
|
||
func (dao *TokenStoreDAO) FindGrantByOrderID(ctx context.Context, orderID uint64) (*tokenmodel.TokenGrant, error) {
|
||
var grant tokenmodel.TokenGrant
|
||
err := dao.db.WithContext(ctx).
|
||
Where("order_id = ?", orderID).
|
||
Order("created_at DESC, id DESC").
|
||
First(&grant).Error
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, nil
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &grant, nil
|
||
}
|
||
|
||
func (dao *TokenStoreDAO) ListGrantsByOrderIDs(ctx context.Context, orderIDs []uint64) ([]tokenmodel.TokenGrant, error) {
|
||
if len(orderIDs) == 0 {
|
||
return []tokenmodel.TokenGrant{}, nil
|
||
}
|
||
|
||
var grants []tokenmodel.TokenGrant
|
||
err := dao.db.WithContext(ctx).
|
||
Where("order_id IN ?", orderIDs).
|
||
Order("created_at DESC, id DESC").
|
||
Find(&grants).Error
|
||
return grants, err
|
||
}
|
||
|
||
func (dao *TokenStoreDAO) CreateGrant(ctx context.Context, grant *tokenmodel.TokenGrant) error {
|
||
return dao.db.WithContext(ctx).Create(grant).Error
|
||
}
|
||
|
||
func (dao *TokenStoreDAO) CountGrants(ctx context.Context, query ListTokenGrantsQuery) (int64, error) {
|
||
db := dao.db.WithContext(ctx).
|
||
Model(&tokenmodel.TokenGrant{}).
|
||
Where("user_id = ?", query.UserID)
|
||
if source := strings.TrimSpace(query.Source); source != "" {
|
||
db = db.Where("source = ?", source)
|
||
}
|
||
|
||
var total int64
|
||
err := db.Count(&total).Error
|
||
return total, err
|
||
}
|
||
|
||
func (dao *TokenStoreDAO) ListGrants(ctx context.Context, query ListTokenGrantsQuery) ([]tokenmodel.TokenGrant, error) {
|
||
db := dao.db.WithContext(ctx).
|
||
Where("user_id = ?", query.UserID)
|
||
if source := strings.TrimSpace(query.Source); source != "" {
|
||
db = db.Where("source = ?", source)
|
||
}
|
||
|
||
var grants []tokenmodel.TokenGrant
|
||
err := db.Order("created_at DESC, id DESC").
|
||
Offset((query.Page - 1) * query.PageSize).
|
||
Limit(query.PageSize).
|
||
Find(&grants).Error
|
||
return grants, err
|
||
}
|
||
|
||
func (dao *TokenStoreDAO) SummarizePositiveGrants(ctx context.Context, userID uint64) (TokenGrantSummary, error) {
|
||
var summary TokenGrantSummary
|
||
err := dao.db.WithContext(ctx).
|
||
Model(&tokenmodel.TokenGrant{}).
|
||
Select(
|
||
`COALESCE(SUM(CASE WHEN amount > 0 AND status IN (?, ?) THEN amount ELSE 0 END), 0) AS recorded_token_total,
|
||
COALESCE(SUM(CASE WHEN amount > 0 AND (quota_applied = ? OR status = ?) THEN amount ELSE 0 END), 0) AS applied_token_total`,
|
||
tokenmodel.TokenGrantStatusRecorded,
|
||
tokenmodel.TokenGrantStatusApplied,
|
||
true,
|
||
tokenmodel.TokenGrantStatusApplied,
|
||
).
|
||
Where("user_id = ?", userID).
|
||
Scan(&summary).Error
|
||
return summary, err
|
||
}
|