Files
sub2api/backend/internal/repository/redeem_code_repo.go
kyx236 04a1a7c2b5 feat(admin): Add email search and rate limit filtering for accounts and redeem codes
- Add used_by_email column to redeem code export CSV for better user identification
- Implement rate_limited status filter in account listing with RateLimitResetAt check
- Extend redeem code search to include user email in addition to code matching
- Add API key search capability to user listing filters
- Display user email in redeem code table used_by column for improved visibility
- Update search placeholders in UI to reflect expanded search capabilities (email, username, notes, API key)
- Improve Chinese and English localization strings for search hints
2026-02-11 16:39:42 +08:00

297 lines
7.5 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/ent/user"
"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.Or(
redeemcode.CodeContainsFold(search),
redeemcode.HasUserWith(user.EmailContainsFold(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
}
// ListByUserPaginated returns paginated balance/concurrency history for a user.
// Supports optional type filter (e.g. "balance", "admin_balance", "concurrency", "admin_concurrency", "subscription").
func (r *redeemCodeRepository) ListByUserPaginated(ctx context.Context, userID int64, params pagination.PaginationParams, codeType string) ([]service.RedeemCode, *pagination.PaginationResult, error) {
q := r.client.RedeemCode.Query().
Where(redeemcode.UsedByEQ(userID))
// Optional type filter
if codeType != "" {
q = q.Where(redeemcode.TypeEQ(codeType))
}
total, err := q.Count(ctx)
if err != nil {
return nil, nil, err
}
codes, err := q.
WithGroup().
Offset(params.Offset()).
Limit(params.Limit()).
Order(dbent.Desc(redeemcode.FieldUsedAt)).
All(ctx)
if err != nil {
return nil, nil, err
}
return redeemCodeEntitiesToService(codes), paginationResultFromTotal(int64(total), params), nil
}
// SumPositiveBalanceByUser returns total recharged amount (sum of value > 0 where type is balance/admin_balance).
func (r *redeemCodeRepository) SumPositiveBalanceByUser(ctx context.Context, userID int64) (float64, error) {
var result []struct {
Sum float64 `json:"sum"`
}
err := r.client.RedeemCode.Query().
Where(
redeemcode.UsedByEQ(userID),
redeemcode.ValueGT(0),
redeemcode.TypeIn("balance", "admin_balance"),
).
Aggregate(dbent.As(dbent.Sum(redeemcode.FieldValue), "sum")).
Scan(ctx, &result)
if err != nil {
return 0, err
}
if len(result) == 0 {
return 0, nil
}
return result[0].Sum, 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
}