## 数据完整性修复 (fix-critical-data-integrity) - 添加 error_translate.go 统一错误转换层 - 修复 nil 输入和 NotFound 错误处理 - 增强仓储层错误一致性 ## 仓储一致性修复 (fix-high-repository-consistency) - Group schema 添加 default_validity_days 字段 - Account schema 添加 proxy edge 关联 - 新增 UsageLog ent schema 定义 - 修复 UpdateBalance/UpdateConcurrency 受影响行数校验 ## 数据卫生修复 (fix-medium-data-hygiene) - UserSubscription 添加软删除支持 (SoftDeleteMixin) - RedeemCode/Setting 添加硬删除策略文档 - account_groups/user_allowed_groups 的 created_at 声明 timestamptz - 停止写入 legacy users.allowed_groups 列 - 新增迁移: 011-014 (索引优化、软删除、孤立数据审计、列清理) ## 测试补充 - 添加 UserSubscription 软删除测试 - 添加迁移回归测试 - 添加 NotFound 错误测试 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
240 lines
5.8 KiB
Go
240 lines
5.8 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
dbent "github.com/Wei-Shaw/sub2api/ent"
|
|
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
)
|
|
|
|
type redeemCodeRepository struct {
|
|
client *dbent.Client
|
|
}
|
|
|
|
func NewRedeemCodeRepository(client *dbent.Client) service.RedeemCodeRepository {
|
|
return &redeemCodeRepository{client: client}
|
|
}
|
|
|
|
func (r *redeemCodeRepository) Create(ctx context.Context, code *service.RedeemCode) error {
|
|
created, err := r.client.RedeemCode.Create().
|
|
SetCode(code.Code).
|
|
SetType(code.Type).
|
|
SetValue(code.Value).
|
|
SetStatus(code.Status).
|
|
SetNotes(code.Notes).
|
|
SetValidityDays(code.ValidityDays).
|
|
SetNillableUsedBy(code.UsedBy).
|
|
SetNillableUsedAt(code.UsedAt).
|
|
SetNillableGroupID(code.GroupID).
|
|
Save(ctx)
|
|
if err == nil {
|
|
code.ID = created.ID
|
|
code.CreatedAt = created.CreatedAt
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (r *redeemCodeRepository) CreateBatch(ctx context.Context, codes []service.RedeemCode) error {
|
|
if len(codes) == 0 {
|
|
return nil
|
|
}
|
|
|
|
builders := make([]*dbent.RedeemCodeCreate, 0, len(codes))
|
|
for i := range codes {
|
|
c := &codes[i]
|
|
b := r.client.RedeemCode.Create().
|
|
SetCode(c.Code).
|
|
SetType(c.Type).
|
|
SetValue(c.Value).
|
|
SetStatus(c.Status).
|
|
SetNotes(c.Notes).
|
|
SetValidityDays(c.ValidityDays).
|
|
SetNillableUsedBy(c.UsedBy).
|
|
SetNillableUsedAt(c.UsedAt).
|
|
SetNillableGroupID(c.GroupID)
|
|
builders = append(builders, b)
|
|
}
|
|
|
|
return r.client.RedeemCode.CreateBulk(builders...).Exec(ctx)
|
|
}
|
|
|
|
func (r *redeemCodeRepository) GetByID(ctx context.Context, id int64) (*service.RedeemCode, error) {
|
|
m, err := r.client.RedeemCode.Query().
|
|
Where(redeemcode.IDEQ(id)).
|
|
Only(ctx)
|
|
if err != nil {
|
|
if dbent.IsNotFound(err) {
|
|
return nil, service.ErrRedeemCodeNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return redeemCodeEntityToService(m), nil
|
|
}
|
|
|
|
func (r *redeemCodeRepository) GetByCode(ctx context.Context, code string) (*service.RedeemCode, error) {
|
|
m, err := r.client.RedeemCode.Query().
|
|
Where(redeemcode.CodeEQ(code)).
|
|
Only(ctx)
|
|
if err != nil {
|
|
if dbent.IsNotFound(err) {
|
|
return nil, service.ErrRedeemCodeNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return redeemCodeEntityToService(m), nil
|
|
}
|
|
|
|
func (r *redeemCodeRepository) Delete(ctx context.Context, id int64) error {
|
|
_, err := r.client.RedeemCode.Delete().Where(redeemcode.IDEQ(id)).Exec(ctx)
|
|
return err
|
|
}
|
|
|
|
func (r *redeemCodeRepository) List(ctx context.Context, params pagination.PaginationParams) ([]service.RedeemCode, *pagination.PaginationResult, error) {
|
|
return r.ListWithFilters(ctx, params, "", "", "")
|
|
}
|
|
|
|
func (r *redeemCodeRepository) ListWithFilters(ctx context.Context, params pagination.PaginationParams, codeType, status, search string) ([]service.RedeemCode, *pagination.PaginationResult, error) {
|
|
q := r.client.RedeemCode.Query()
|
|
|
|
if codeType != "" {
|
|
q = q.Where(redeemcode.TypeEQ(codeType))
|
|
}
|
|
if status != "" {
|
|
q = q.Where(redeemcode.StatusEQ(status))
|
|
}
|
|
if search != "" {
|
|
q = q.Where(redeemcode.CodeContainsFold(search))
|
|
}
|
|
|
|
total, err := q.Count(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
codes, err := q.
|
|
WithUser().
|
|
WithGroup().
|
|
Offset(params.Offset()).
|
|
Limit(params.Limit()).
|
|
Order(dbent.Desc(redeemcode.FieldID)).
|
|
All(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
outCodes := redeemCodeEntitiesToService(codes)
|
|
|
|
return outCodes, paginationResultFromTotal(int64(total), params), nil
|
|
}
|
|
|
|
func (r *redeemCodeRepository) Update(ctx context.Context, code *service.RedeemCode) error {
|
|
up := r.client.RedeemCode.UpdateOneID(code.ID).
|
|
SetCode(code.Code).
|
|
SetType(code.Type).
|
|
SetValue(code.Value).
|
|
SetStatus(code.Status).
|
|
SetNotes(code.Notes).
|
|
SetValidityDays(code.ValidityDays)
|
|
|
|
if code.UsedBy != nil {
|
|
up.SetUsedBy(*code.UsedBy)
|
|
} else {
|
|
up.ClearUsedBy()
|
|
}
|
|
if code.UsedAt != nil {
|
|
up.SetUsedAt(*code.UsedAt)
|
|
} else {
|
|
up.ClearUsedAt()
|
|
}
|
|
if code.GroupID != nil {
|
|
up.SetGroupID(*code.GroupID)
|
|
} else {
|
|
up.ClearGroupID()
|
|
}
|
|
|
|
updated, err := up.Save(ctx)
|
|
if err != nil {
|
|
if dbent.IsNotFound(err) {
|
|
return service.ErrRedeemCodeNotFound
|
|
}
|
|
return err
|
|
}
|
|
code.CreatedAt = updated.CreatedAt
|
|
return nil
|
|
}
|
|
|
|
func (r *redeemCodeRepository) Use(ctx context.Context, id, userID int64) error {
|
|
now := time.Now()
|
|
client := clientFromContext(ctx, r.client)
|
|
affected, err := client.RedeemCode.Update().
|
|
Where(redeemcode.IDEQ(id), redeemcode.StatusEQ(service.StatusUnused)).
|
|
SetStatus(service.StatusUsed).
|
|
SetUsedBy(userID).
|
|
SetUsedAt(now).
|
|
Save(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if affected == 0 {
|
|
return service.ErrRedeemCodeUsed
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *redeemCodeRepository) ListByUser(ctx context.Context, userID int64, limit int) ([]service.RedeemCode, error) {
|
|
if limit <= 0 {
|
|
limit = 10
|
|
}
|
|
|
|
codes, err := r.client.RedeemCode.Query().
|
|
Where(redeemcode.UsedByEQ(userID)).
|
|
WithGroup().
|
|
Order(dbent.Desc(redeemcode.FieldUsedAt)).
|
|
Limit(limit).
|
|
All(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return redeemCodeEntitiesToService(codes), nil
|
|
}
|
|
|
|
func redeemCodeEntityToService(m *dbent.RedeemCode) *service.RedeemCode {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
out := &service.RedeemCode{
|
|
ID: m.ID,
|
|
Code: m.Code,
|
|
Type: m.Type,
|
|
Value: m.Value,
|
|
Status: m.Status,
|
|
UsedBy: m.UsedBy,
|
|
UsedAt: m.UsedAt,
|
|
Notes: derefString(m.Notes),
|
|
CreatedAt: m.CreatedAt,
|
|
GroupID: m.GroupID,
|
|
ValidityDays: m.ValidityDays,
|
|
}
|
|
if m.Edges.User != nil {
|
|
out.User = userEntityToService(m.Edges.User)
|
|
}
|
|
if m.Edges.Group != nil {
|
|
out.Group = groupEntityToService(m.Edges.Group)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func redeemCodeEntitiesToService(models []*dbent.RedeemCode) []service.RedeemCode {
|
|
out := make([]service.RedeemCode, 0, len(models))
|
|
for i := range models {
|
|
if s := redeemCodeEntityToService(models[i]); s != nil {
|
|
out = append(out, *s)
|
|
}
|
|
}
|
|
return out
|
|
}
|