refactor(数据库): 迁移持久层到 Ent 并清理 GORM
将仓储层/基础设施改为 Ent + 原生 SQL 执行路径,并移除 AutoMigrate 与 GORM 依赖。 重构内容包括: - 仓储层改用 Ent/SQL(含 usage_log/account 等复杂查询),统一错误映射 - 基础设施与 setup 初始化切换为 Ent + SQL migrations - 集成测试与 fixtures 迁移到 Ent 事务模型 - 清理遗留 GORM 模型/依赖,补充迁移与文档说明 - 增加根目录 Makefile 便于前后端编译 测试: - go test -tags unit ./... - go test -tags integration ./...
This commit is contained in:
@@ -4,25 +4,35 @@ 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"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type redeemCodeRepository struct {
|
||||
db *gorm.DB
|
||||
client *dbent.Client
|
||||
}
|
||||
|
||||
func NewRedeemCodeRepository(db *gorm.DB) service.RedeemCodeRepository {
|
||||
return &redeemCodeRepository{db: db}
|
||||
func NewRedeemCodeRepository(client *dbent.Client) service.RedeemCodeRepository {
|
||||
return &redeemCodeRepository{client: client}
|
||||
}
|
||||
|
||||
func (r *redeemCodeRepository) Create(ctx context.Context, code *service.RedeemCode) error {
|
||||
m := redeemCodeModelFromService(code)
|
||||
err := r.db.WithContext(ctx).Create(m).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 {
|
||||
applyRedeemCodeModelToService(code, m)
|
||||
code.ID = created.ID
|
||||
code.CreatedAt = created.CreatedAt
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -31,36 +41,55 @@ func (r *redeemCodeRepository) CreateBatch(ctx context.Context, codes []service.
|
||||
if len(codes) == 0 {
|
||||
return nil
|
||||
}
|
||||
models := make([]redeemCodeModel, 0, len(codes))
|
||||
|
||||
builders := make([]*dbent.RedeemCodeCreate, 0, len(codes))
|
||||
for i := range codes {
|
||||
m := redeemCodeModelFromService(&codes[i])
|
||||
if m != nil {
|
||||
models = append(models, *m)
|
||||
}
|
||||
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.db.WithContext(ctx).Create(&models).Error
|
||||
|
||||
return r.client.RedeemCode.CreateBulk(builders...).Exec(ctx)
|
||||
}
|
||||
|
||||
func (r *redeemCodeRepository) GetByID(ctx context.Context, id int64) (*service.RedeemCode, error) {
|
||||
var m redeemCodeModel
|
||||
err := r.db.WithContext(ctx).First(&m, id).Error
|
||||
m, err := r.client.RedeemCode.Query().
|
||||
Where(redeemcode.IDEQ(id)).
|
||||
Only(ctx)
|
||||
if err != nil {
|
||||
return nil, translatePersistenceError(err, service.ErrRedeemCodeNotFound, nil)
|
||||
if dbent.IsNotFound(err) {
|
||||
return nil, service.ErrRedeemCodeNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return redeemCodeModelToService(&m), nil
|
||||
return redeemCodeEntityToService(m), nil
|
||||
}
|
||||
|
||||
func (r *redeemCodeRepository) GetByCode(ctx context.Context, code string) (*service.RedeemCode, error) {
|
||||
var m redeemCodeModel
|
||||
err := r.db.WithContext(ctx).Where("code = ?", code).First(&m).Error
|
||||
m, err := r.client.RedeemCode.Query().
|
||||
Where(redeemcode.CodeEQ(code)).
|
||||
Only(ctx)
|
||||
if err != nil {
|
||||
return nil, translatePersistenceError(err, service.ErrRedeemCodeNotFound, nil)
|
||||
if dbent.IsNotFound(err) {
|
||||
return nil, service.ErrRedeemCodeNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return redeemCodeModelToService(&m), nil
|
||||
return redeemCodeEntityToService(m), nil
|
||||
}
|
||||
|
||||
func (r *redeemCodeRepository) Delete(ctx context.Context, id int64) error {
|
||||
return r.db.WithContext(ctx).Delete(&redeemCodeModel{}, id).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) {
|
||||
@@ -68,61 +97,88 @@ func (r *redeemCodeRepository) List(ctx context.Context, params pagination.Pagin
|
||||
}
|
||||
|
||||
func (r *redeemCodeRepository) ListWithFilters(ctx context.Context, params pagination.PaginationParams, codeType, status, search string) ([]service.RedeemCode, *pagination.PaginationResult, error) {
|
||||
var codes []redeemCodeModel
|
||||
var total int64
|
||||
|
||||
db := r.db.WithContext(ctx).Model(&redeemCodeModel{})
|
||||
q := r.client.RedeemCode.Query()
|
||||
|
||||
if codeType != "" {
|
||||
db = db.Where("type = ?", codeType)
|
||||
q = q.Where(redeemcode.TypeEQ(codeType))
|
||||
}
|
||||
if status != "" {
|
||||
db = db.Where("status = ?", status)
|
||||
q = q.Where(redeemcode.StatusEQ(status))
|
||||
}
|
||||
if search != "" {
|
||||
searchPattern := "%" + search + "%"
|
||||
db = db.Where("code ILIKE ?", searchPattern)
|
||||
q = q.Where(redeemcode.CodeContainsFold(search))
|
||||
}
|
||||
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
total, err := q.Count(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := db.Preload("User").Preload("Group").Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&codes).Error; err != nil {
|
||||
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 := make([]service.RedeemCode, 0, len(codes))
|
||||
for i := range codes {
|
||||
outCodes = append(outCodes, *redeemCodeModelToService(&codes[i]))
|
||||
}
|
||||
outCodes := redeemCodeEntitiesToService(codes)
|
||||
|
||||
return outCodes, paginationResultFromTotal(total, params), nil
|
||||
return outCodes, paginationResultFromTotal(int64(total), params), nil
|
||||
}
|
||||
|
||||
func (r *redeemCodeRepository) Update(ctx context.Context, code *service.RedeemCode) error {
|
||||
m := redeemCodeModelFromService(code)
|
||||
err := r.db.WithContext(ctx).Save(m).Error
|
||||
if err == nil {
|
||||
applyRedeemCodeModelToService(code, m)
|
||||
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()
|
||||
}
|
||||
return err
|
||||
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()
|
||||
result := r.db.WithContext(ctx).Model(&redeemCodeModel{}).
|
||||
Where("id = ? AND status = ?", id, service.StatusUnused).
|
||||
Updates(map[string]any{
|
||||
"status": service.StatusUsed,
|
||||
"used_by": userID,
|
||||
"used_at": now,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
affected, err := r.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 result.RowsAffected == 0 {
|
||||
return service.ErrRedeemCodeUsed.WithCause(gorm.ErrRecordNotFound)
|
||||
if affected == 0 {
|
||||
return service.ErrRedeemCodeUsed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -132,49 +188,24 @@ func (r *redeemCodeRepository) ListByUser(ctx context.Context, userID int64, lim
|
||||
limit = 10
|
||||
}
|
||||
|
||||
var codes []redeemCodeModel
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Group").
|
||||
Where("used_by = ?", userID).
|
||||
Order("used_at DESC").
|
||||
codes, err := r.client.RedeemCode.Query().
|
||||
Where(redeemcode.UsedByEQ(userID)).
|
||||
WithGroup().
|
||||
Order(dbent.Desc(redeemcode.FieldUsedAt)).
|
||||
Limit(limit).
|
||||
Find(&codes).Error
|
||||
All(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
outCodes := make([]service.RedeemCode, 0, len(codes))
|
||||
for i := range codes {
|
||||
outCodes = append(outCodes, *redeemCodeModelToService(&codes[i]))
|
||||
}
|
||||
return outCodes, nil
|
||||
return redeemCodeEntitiesToService(codes), nil
|
||||
}
|
||||
|
||||
type redeemCodeModel struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
Code string `gorm:"uniqueIndex;size:32;not null"`
|
||||
Type string `gorm:"size:20;default:balance;not null"`
|
||||
Value float64 `gorm:"type:decimal(20,8);not null"`
|
||||
Status string `gorm:"size:20;default:unused;not null"`
|
||||
UsedBy *int64 `gorm:"index"`
|
||||
UsedAt *time.Time
|
||||
Notes string `gorm:"type:text"`
|
||||
CreatedAt time.Time `gorm:"not null"`
|
||||
|
||||
GroupID *int64 `gorm:"index"`
|
||||
ValidityDays int `gorm:"default:30"`
|
||||
|
||||
User *userModel `gorm:"foreignKey:UsedBy"`
|
||||
Group *groupModel `gorm:"foreignKey:GroupID"`
|
||||
}
|
||||
|
||||
func (redeemCodeModel) TableName() string { return "redeem_codes" }
|
||||
|
||||
func redeemCodeModelToService(m *redeemCodeModel) *service.RedeemCode {
|
||||
func redeemCodeEntityToService(m *dbent.RedeemCode) *service.RedeemCode {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return &service.RedeemCode{
|
||||
out := &service.RedeemCode{
|
||||
ID: m.ID,
|
||||
Code: m.Code,
|
||||
Type: m.Type,
|
||||
@@ -182,38 +213,26 @@ func redeemCodeModelToService(m *redeemCodeModel) *service.RedeemCode {
|
||||
Status: m.Status,
|
||||
UsedBy: m.UsedBy,
|
||||
UsedAt: m.UsedAt,
|
||||
Notes: m.Notes,
|
||||
Notes: derefString(m.Notes),
|
||||
CreatedAt: m.CreatedAt,
|
||||
GroupID: m.GroupID,
|
||||
ValidityDays: m.ValidityDays,
|
||||
User: userModelToService(m.User),
|
||||
Group: groupModelToService(m.Group),
|
||||
}
|
||||
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 redeemCodeModelFromService(r *service.RedeemCode) *redeemCodeModel {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return &redeemCodeModel{
|
||||
ID: r.ID,
|
||||
Code: r.Code,
|
||||
Type: r.Type,
|
||||
Value: r.Value,
|
||||
Status: r.Status,
|
||||
UsedBy: r.UsedBy,
|
||||
UsedAt: r.UsedAt,
|
||||
Notes: r.Notes,
|
||||
CreatedAt: r.CreatedAt,
|
||||
GroupID: r.GroupID,
|
||||
ValidityDays: r.ValidityDays,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyRedeemCodeModelToService(code *service.RedeemCode, m *redeemCodeModel) {
|
||||
if code == nil || m == nil {
|
||||
return
|
||||
}
|
||||
code.ID = m.ID
|
||||
code.CreatedAt = m.CreatedAt
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user