Files
image2api/backend/internal/repo/cdk_repo.go
T
chiyiandClaude Opus 4.8 606caaf047 Initial open-source release (MIT): image2api AI gateway
Full Go backend + Vue 3 frontend, OpenAI-compatible API, multi-provider
account pools, billing/admin, Docker one-command deploy with auto HTTPS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 22:59:04 +08:00

135 lines
3.7 KiB
Go

package repo
import (
"context"
"errors"
"time"
"backend/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// ErrCDKBatchLimit is returned when a user tries to redeem a second code from
// the same marketing batch (one per user per batch).
var ErrCDKBatchLimit = errors.New("cdk marketing batch already redeemed by this user")
type CDKRepository struct {
db *gorm.DB
}
func NewCDKRepository(db *gorm.DB) *CDKRepository {
return &CDKRepository{db: db}
}
func (r *CDKRepository) List(ctx context.Context) ([]model.CDKCode, error) {
var items []model.CDKCode
if err := r.db.WithContext(ctx).Order("created_at desc").Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (r *CDKRepository) Stats(ctx context.Context) (map[string]any, error) {
var total, active, redeemed int64
if err := r.db.WithContext(ctx).Model(&model.CDKCode{}).Count(&total).Error; err != nil {
return nil, err
}
if err := r.db.WithContext(ctx).Model(&model.CDKCode{}).Where("status = ?", "active").Count(&active).Error; err != nil {
return nil, err
}
if err := r.db.WithContext(ctx).Model(&model.CDKCode{}).Where("status = ?", "redeemed").Count(&redeemed).Error; err != nil {
return nil, err
}
type sumRow struct {
Total *float64 `gorm:"column:total"`
}
var activeAmount, redeemedAmount sumRow
if err := r.db.WithContext(ctx).
Model(&model.CDKCode{}).
Select("SUM(amount) AS total").
Where("status = ?", "active").
Scan(&activeAmount).Error; err != nil {
return nil, err
}
if err := r.db.WithContext(ctx).
Model(&model.CDKCode{}).
Select("SUM(amount) AS total").
Where("status = ?", "redeemed").
Scan(&redeemedAmount).Error; err != nil {
return nil, err
}
activeAmt := 0.0
if activeAmount.Total != nil {
activeAmt = *activeAmount.Total
}
redeemedAmt := 0.0
if redeemedAmount.Total != nil {
redeemedAmt = *redeemedAmount.Total
}
return map[string]any{
"total": total,
"active": active,
"redeemed": redeemed,
"active_amount": activeAmt,
"redeemed_amount": redeemedAmt,
}, nil
}
func (r *CDKRepository) CreateBatch(ctx context.Context, items []model.CDKCode) error {
return r.db.WithContext(ctx).Create(&items).Error
}
func (r *CDKRepository) Delete(ctx context.Context, code string) (int64, error) {
res := r.db.WithContext(ctx).Delete(&model.CDKCode{}, "code = ?", code)
return res.RowsAffected, res.Error
}
func (r *CDKRepository) DeleteByCodes(ctx context.Context, codes []string) (int64, error) {
if len(codes) == 0 {
return 0, nil
}
res := r.db.WithContext(ctx).Delete(&model.CDKCode{}, "code IN ?", codes)
return res.RowsAffected, res.Error
}
func (r *CDKRepository) Redeem(ctx context.Context, code, userID string) (*model.CDKCode, error) {
var out *model.CDKCode
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var item model.CDKCode
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&item, "code = ?", code).Error; err != nil {
return err
}
if item.Status == "redeemed" {
return gorm.ErrDuplicatedKey
}
// Marketing codes: a user may redeem only ONE code per batch. The partial
// unique index (batch_id, redeemed_by) is the hard backstop against
// concurrent double-redeems; this check gives a friendly error first.
if item.Type == "marketing" && item.BatchID != "" {
var cnt int64
if err := tx.Model(&model.CDKCode{}).
Where("batch_id = ? AND type = 'marketing' AND redeemed_by = ?", item.BatchID, userID).
Count(&cnt).Error; err != nil {
return err
}
if cnt > 0 {
return ErrCDKBatchLimit
}
}
now := time.Now()
item.Status = "redeemed"
item.RedeemedBy = &userID
item.RedeemedAt = &now
if err := tx.Save(&item).Error; err != nil {
return err
}
out = &item
return nil
})
return out, err
}