Merge PR #122: feat: 用户自定义属性系统 + Wechat 字段迁移
This commit is contained in:
@@ -294,7 +294,6 @@ func userEntityToService(u *dbent.User) *service.User {
|
||||
ID: u.ID,
|
||||
Email: u.Email,
|
||||
Username: u.Username,
|
||||
Wechat: u.Wechat,
|
||||
Notes: u.Notes,
|
||||
PasswordHash: u.PasswordHash,
|
||||
Role: u.Role,
|
||||
|
||||
@@ -40,7 +40,6 @@ func mustCreateUser(t *testing.T, client *dbent.Client, u *service.User) *servic
|
||||
SetBalance(u.Balance).
|
||||
SetConcurrency(u.Concurrency).
|
||||
SetUsername(u.Username).
|
||||
SetWechat(u.Wechat).
|
||||
SetNotes(u.Notes)
|
||||
if !u.CreatedAt.IsZero() {
|
||||
create.SetCreatedAt(u.CreatedAt)
|
||||
|
||||
@@ -23,7 +23,6 @@ func TestMigrationsRunner_IsIdempotent_AndSchemaIsUpToDate(t *testing.T) {
|
||||
|
||||
// users: columns required by repository queries
|
||||
requireColumn(t, tx, "users", "username", "character varying", 100, false)
|
||||
requireColumn(t, tx, "users", "wechat", "character varying", 100, false)
|
||||
requireColumn(t, tx, "users", "notes", "text", 0, false)
|
||||
|
||||
// accounts: schedulable and rate-limit fields
|
||||
|
||||
385
backend/internal/repository/user_attribute_repo.go
Normal file
385
backend/internal/repository/user_attribute_repo.go
Normal file
@@ -0,0 +1,385 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
"github.com/Wei-Shaw/sub2api/ent/userattributedefinition"
|
||||
"github.com/Wei-Shaw/sub2api/ent/userattributevalue"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
)
|
||||
|
||||
// UserAttributeDefinitionRepository implementation
|
||||
type userAttributeDefinitionRepository struct {
|
||||
client *dbent.Client
|
||||
}
|
||||
|
||||
// NewUserAttributeDefinitionRepository creates a new repository instance
|
||||
func NewUserAttributeDefinitionRepository(client *dbent.Client) service.UserAttributeDefinitionRepository {
|
||||
return &userAttributeDefinitionRepository{client: client}
|
||||
}
|
||||
|
||||
func (r *userAttributeDefinitionRepository) Create(ctx context.Context, def *service.UserAttributeDefinition) error {
|
||||
client := clientFromContext(ctx, r.client)
|
||||
|
||||
created, err := client.UserAttributeDefinition.Create().
|
||||
SetKey(def.Key).
|
||||
SetName(def.Name).
|
||||
SetDescription(def.Description).
|
||||
SetType(string(def.Type)).
|
||||
SetOptions(toEntOptions(def.Options)).
|
||||
SetRequired(def.Required).
|
||||
SetValidation(toEntValidation(def.Validation)).
|
||||
SetPlaceholder(def.Placeholder).
|
||||
SetEnabled(def.Enabled).
|
||||
Save(ctx)
|
||||
|
||||
if err != nil {
|
||||
return translatePersistenceError(err, nil, service.ErrAttributeKeyExists)
|
||||
}
|
||||
|
||||
def.ID = created.ID
|
||||
def.DisplayOrder = created.DisplayOrder
|
||||
def.CreatedAt = created.CreatedAt
|
||||
def.UpdatedAt = created.UpdatedAt
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *userAttributeDefinitionRepository) GetByID(ctx context.Context, id int64) (*service.UserAttributeDefinition, error) {
|
||||
client := clientFromContext(ctx, r.client)
|
||||
|
||||
e, err := client.UserAttributeDefinition.Query().
|
||||
Where(userattributedefinition.IDEQ(id)).
|
||||
Only(ctx)
|
||||
if err != nil {
|
||||
return nil, translatePersistenceError(err, service.ErrAttributeDefinitionNotFound, nil)
|
||||
}
|
||||
return defEntityToService(e), nil
|
||||
}
|
||||
|
||||
func (r *userAttributeDefinitionRepository) GetByKey(ctx context.Context, key string) (*service.UserAttributeDefinition, error) {
|
||||
client := clientFromContext(ctx, r.client)
|
||||
|
||||
e, err := client.UserAttributeDefinition.Query().
|
||||
Where(userattributedefinition.KeyEQ(key)).
|
||||
Only(ctx)
|
||||
if err != nil {
|
||||
return nil, translatePersistenceError(err, service.ErrAttributeDefinitionNotFound, nil)
|
||||
}
|
||||
return defEntityToService(e), nil
|
||||
}
|
||||
|
||||
func (r *userAttributeDefinitionRepository) Update(ctx context.Context, def *service.UserAttributeDefinition) error {
|
||||
client := clientFromContext(ctx, r.client)
|
||||
|
||||
updated, err := client.UserAttributeDefinition.UpdateOneID(def.ID).
|
||||
SetName(def.Name).
|
||||
SetDescription(def.Description).
|
||||
SetType(string(def.Type)).
|
||||
SetOptions(toEntOptions(def.Options)).
|
||||
SetRequired(def.Required).
|
||||
SetValidation(toEntValidation(def.Validation)).
|
||||
SetPlaceholder(def.Placeholder).
|
||||
SetDisplayOrder(def.DisplayOrder).
|
||||
SetEnabled(def.Enabled).
|
||||
Save(ctx)
|
||||
|
||||
if err != nil {
|
||||
return translatePersistenceError(err, service.ErrAttributeDefinitionNotFound, service.ErrAttributeKeyExists)
|
||||
}
|
||||
|
||||
def.UpdatedAt = updated.UpdatedAt
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *userAttributeDefinitionRepository) Delete(ctx context.Context, id int64) error {
|
||||
client := clientFromContext(ctx, r.client)
|
||||
|
||||
_, err := client.UserAttributeDefinition.Delete().
|
||||
Where(userattributedefinition.IDEQ(id)).
|
||||
Exec(ctx)
|
||||
return translatePersistenceError(err, service.ErrAttributeDefinitionNotFound, nil)
|
||||
}
|
||||
|
||||
func (r *userAttributeDefinitionRepository) List(ctx context.Context, enabledOnly bool) ([]service.UserAttributeDefinition, error) {
|
||||
client := clientFromContext(ctx, r.client)
|
||||
|
||||
q := client.UserAttributeDefinition.Query()
|
||||
if enabledOnly {
|
||||
q = q.Where(userattributedefinition.EnabledEQ(true))
|
||||
}
|
||||
|
||||
entities, err := q.Order(dbent.Asc(userattributedefinition.FieldDisplayOrder)).All(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]service.UserAttributeDefinition, 0, len(entities))
|
||||
for _, e := range entities {
|
||||
result = append(result, *defEntityToService(e))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *userAttributeDefinitionRepository) UpdateDisplayOrders(ctx context.Context, orders map[int64]int) error {
|
||||
tx, err := r.client.Tx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
for id, order := range orders {
|
||||
if _, err := tx.UserAttributeDefinition.UpdateOneID(id).
|
||||
SetDisplayOrder(order).
|
||||
Save(ctx); err != nil {
|
||||
return translatePersistenceError(err, service.ErrAttributeDefinitionNotFound, nil)
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *userAttributeDefinitionRepository) ExistsByKey(ctx context.Context, key string) (bool, error) {
|
||||
client := clientFromContext(ctx, r.client)
|
||||
return client.UserAttributeDefinition.Query().
|
||||
Where(userattributedefinition.KeyEQ(key)).
|
||||
Exist(ctx)
|
||||
}
|
||||
|
||||
// UserAttributeValueRepository implementation
|
||||
type userAttributeValueRepository struct {
|
||||
client *dbent.Client
|
||||
}
|
||||
|
||||
// NewUserAttributeValueRepository creates a new repository instance
|
||||
func NewUserAttributeValueRepository(client *dbent.Client) service.UserAttributeValueRepository {
|
||||
return &userAttributeValueRepository{client: client}
|
||||
}
|
||||
|
||||
func (r *userAttributeValueRepository) GetByUserID(ctx context.Context, userID int64) ([]service.UserAttributeValue, error) {
|
||||
client := clientFromContext(ctx, r.client)
|
||||
|
||||
entities, err := client.UserAttributeValue.Query().
|
||||
Where(userattributevalue.UserIDEQ(userID)).
|
||||
All(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]service.UserAttributeValue, 0, len(entities))
|
||||
for _, e := range entities {
|
||||
result = append(result, service.UserAttributeValue{
|
||||
ID: e.ID,
|
||||
UserID: e.UserID,
|
||||
AttributeID: e.AttributeID,
|
||||
Value: e.Value,
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *userAttributeValueRepository) GetByUserIDs(ctx context.Context, userIDs []int64) ([]service.UserAttributeValue, error) {
|
||||
if len(userIDs) == 0 {
|
||||
return []service.UserAttributeValue{}, nil
|
||||
}
|
||||
|
||||
client := clientFromContext(ctx, r.client)
|
||||
|
||||
entities, err := client.UserAttributeValue.Query().
|
||||
Where(userattributevalue.UserIDIn(userIDs...)).
|
||||
All(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]service.UserAttributeValue, 0, len(entities))
|
||||
for _, e := range entities {
|
||||
result = append(result, service.UserAttributeValue{
|
||||
ID: e.ID,
|
||||
UserID: e.UserID,
|
||||
AttributeID: e.AttributeID,
|
||||
Value: e.Value,
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *userAttributeValueRepository) UpsertBatch(ctx context.Context, userID int64, inputs []service.UpdateUserAttributeInput) error {
|
||||
if len(inputs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tx, err := r.client.Tx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
for _, input := range inputs {
|
||||
// Use upsert (ON CONFLICT DO UPDATE)
|
||||
err := tx.UserAttributeValue.Create().
|
||||
SetUserID(userID).
|
||||
SetAttributeID(input.AttributeID).
|
||||
SetValue(input.Value).
|
||||
OnConflictColumns(userattributevalue.FieldUserID, userattributevalue.FieldAttributeID).
|
||||
UpdateValue().
|
||||
UpdateUpdatedAt().
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *userAttributeValueRepository) DeleteByAttributeID(ctx context.Context, attributeID int64) error {
|
||||
client := clientFromContext(ctx, r.client)
|
||||
|
||||
_, err := client.UserAttributeValue.Delete().
|
||||
Where(userattributevalue.AttributeIDEQ(attributeID)).
|
||||
Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *userAttributeValueRepository) DeleteByUserID(ctx context.Context, userID int64) error {
|
||||
client := clientFromContext(ctx, r.client)
|
||||
|
||||
_, err := client.UserAttributeValue.Delete().
|
||||
Where(userattributevalue.UserIDEQ(userID)).
|
||||
Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// Helper functions for entity to service conversion
|
||||
func defEntityToService(e *dbent.UserAttributeDefinition) *service.UserAttributeDefinition {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return &service.UserAttributeDefinition{
|
||||
ID: e.ID,
|
||||
Key: e.Key,
|
||||
Name: e.Name,
|
||||
Description: e.Description,
|
||||
Type: service.UserAttributeType(e.Type),
|
||||
Options: toServiceOptions(e.Options),
|
||||
Required: e.Required,
|
||||
Validation: toServiceValidation(e.Validation),
|
||||
Placeholder: e.Placeholder,
|
||||
DisplayOrder: e.DisplayOrder,
|
||||
Enabled: e.Enabled,
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// Type conversion helpers (map types <-> service types)
|
||||
func toEntOptions(opts []service.UserAttributeOption) []map[string]any {
|
||||
if opts == nil {
|
||||
return []map[string]any{}
|
||||
}
|
||||
result := make([]map[string]any, len(opts))
|
||||
for i, o := range opts {
|
||||
result[i] = map[string]any{"value": o.Value, "label": o.Label}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func toServiceOptions(opts []map[string]any) []service.UserAttributeOption {
|
||||
if opts == nil {
|
||||
return []service.UserAttributeOption{}
|
||||
}
|
||||
result := make([]service.UserAttributeOption, len(opts))
|
||||
for i, o := range opts {
|
||||
result[i] = service.UserAttributeOption{
|
||||
Value: getString(o, "value"),
|
||||
Label: getString(o, "label"),
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func toEntValidation(v service.UserAttributeValidation) map[string]any {
|
||||
result := map[string]any{}
|
||||
if v.MinLength != nil {
|
||||
result["min_length"] = *v.MinLength
|
||||
}
|
||||
if v.MaxLength != nil {
|
||||
result["max_length"] = *v.MaxLength
|
||||
}
|
||||
if v.Min != nil {
|
||||
result["min"] = *v.Min
|
||||
}
|
||||
if v.Max != nil {
|
||||
result["max"] = *v.Max
|
||||
}
|
||||
if v.Pattern != nil {
|
||||
result["pattern"] = *v.Pattern
|
||||
}
|
||||
if v.Message != nil {
|
||||
result["message"] = *v.Message
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func toServiceValidation(v map[string]any) service.UserAttributeValidation {
|
||||
result := service.UserAttributeValidation{}
|
||||
if val := getInt(v, "min_length"); val != nil {
|
||||
result.MinLength = val
|
||||
}
|
||||
if val := getInt(v, "max_length"); val != nil {
|
||||
result.MaxLength = val
|
||||
}
|
||||
if val := getInt(v, "min"); val != nil {
|
||||
result.Min = val
|
||||
}
|
||||
if val := getInt(v, "max"); val != nil {
|
||||
result.Max = val
|
||||
}
|
||||
if val := getStringPtr(v, "pattern"); val != nil {
|
||||
result.Pattern = val
|
||||
}
|
||||
if val := getStringPtr(v, "message"); val != nil {
|
||||
result.Message = val
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Helper functions for type conversion
|
||||
func getString(m map[string]any, key string) string {
|
||||
if v, ok := m[key]; ok {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getStringPtr(m map[string]any, key string) *string {
|
||||
if v, ok := m[key]; ok {
|
||||
if s, ok := v.(string); ok {
|
||||
return &s
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getInt(m map[string]any, key string) *int {
|
||||
if v, ok := m[key]; ok {
|
||||
switch n := v.(type) {
|
||||
case int:
|
||||
return &n
|
||||
case int64:
|
||||
i := int(n)
|
||||
return &i
|
||||
case float64:
|
||||
i := int(n)
|
||||
return &i
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
dbuser "github.com/Wei-Shaw/sub2api/ent/user"
|
||||
"github.com/Wei-Shaw/sub2api/ent/userallowedgroup"
|
||||
"github.com/Wei-Shaw/sub2api/ent/userattributevalue"
|
||||
"github.com/Wei-Shaw/sub2api/ent/usersubscription"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
@@ -50,7 +51,6 @@ func (r *userRepository) Create(ctx context.Context, userIn *service.User) error
|
||||
created, err := txClient.User.Create().
|
||||
SetEmail(userIn.Email).
|
||||
SetUsername(userIn.Username).
|
||||
SetWechat(userIn.Wechat).
|
||||
SetNotes(userIn.Notes).
|
||||
SetPasswordHash(userIn.PasswordHash).
|
||||
SetRole(userIn.Role).
|
||||
@@ -133,7 +133,6 @@ func (r *userRepository) Update(ctx context.Context, userIn *service.User) error
|
||||
updated, err := txClient.User.UpdateOneID(userIn.ID).
|
||||
SetEmail(userIn.Email).
|
||||
SetUsername(userIn.Username).
|
||||
SetWechat(userIn.Wechat).
|
||||
SetNotes(userIn.Notes).
|
||||
SetPasswordHash(userIn.PasswordHash).
|
||||
SetRole(userIn.Role).
|
||||
@@ -171,28 +170,38 @@ func (r *userRepository) Delete(ctx context.Context, id int64) error {
|
||||
}
|
||||
|
||||
func (r *userRepository) List(ctx context.Context, params pagination.PaginationParams) ([]service.User, *pagination.PaginationResult, error) {
|
||||
return r.ListWithFilters(ctx, params, "", "", "")
|
||||
return r.ListWithFilters(ctx, params, service.UserListFilters{})
|
||||
}
|
||||
|
||||
func (r *userRepository) ListWithFilters(ctx context.Context, params pagination.PaginationParams, status, role, search string) ([]service.User, *pagination.PaginationResult, error) {
|
||||
func (r *userRepository) ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters service.UserListFilters) ([]service.User, *pagination.PaginationResult, error) {
|
||||
q := r.client.User.Query()
|
||||
|
||||
if status != "" {
|
||||
q = q.Where(dbuser.StatusEQ(status))
|
||||
if filters.Status != "" {
|
||||
q = q.Where(dbuser.StatusEQ(filters.Status))
|
||||
}
|
||||
if role != "" {
|
||||
q = q.Where(dbuser.RoleEQ(role))
|
||||
if filters.Role != "" {
|
||||
q = q.Where(dbuser.RoleEQ(filters.Role))
|
||||
}
|
||||
if search != "" {
|
||||
if filters.Search != "" {
|
||||
q = q.Where(
|
||||
dbuser.Or(
|
||||
dbuser.EmailContainsFold(search),
|
||||
dbuser.UsernameContainsFold(search),
|
||||
dbuser.WechatContainsFold(search),
|
||||
dbuser.EmailContainsFold(filters.Search),
|
||||
dbuser.UsernameContainsFold(filters.Search),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// If attribute filters are specified, we need to filter by user IDs first
|
||||
var allowedUserIDs []int64
|
||||
if len(filters.Attributes) > 0 {
|
||||
allowedUserIDs = r.filterUsersByAttributes(ctx, filters.Attributes)
|
||||
if len(allowedUserIDs) == 0 {
|
||||
// No users match the attribute filters
|
||||
return []service.User{}, paginationResultFromTotal(0, params), nil
|
||||
}
|
||||
q = q.Where(dbuser.IDIn(allowedUserIDs...))
|
||||
}
|
||||
|
||||
total, err := q.Clone().Count(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -252,6 +261,59 @@ func (r *userRepository) ListWithFilters(ctx context.Context, params pagination.
|
||||
return outUsers, paginationResultFromTotal(int64(total), params), nil
|
||||
}
|
||||
|
||||
// filterUsersByAttributes returns user IDs that match ALL the given attribute filters
|
||||
func (r *userRepository) filterUsersByAttributes(ctx context.Context, attrs map[int64]string) []int64 {
|
||||
if len(attrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// For each attribute filter, get the set of matching user IDs
|
||||
// Then intersect all sets to get users matching ALL filters
|
||||
var resultSet map[int64]struct{}
|
||||
first := true
|
||||
|
||||
for attrID, value := range attrs {
|
||||
// Query user_attribute_values for this attribute
|
||||
values, err := r.client.UserAttributeValue.Query().
|
||||
Where(
|
||||
userattributevalue.AttributeIDEQ(attrID),
|
||||
userattributevalue.ValueContainsFold(value),
|
||||
).
|
||||
All(ctx)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
currentSet := make(map[int64]struct{}, len(values))
|
||||
for _, v := range values {
|
||||
currentSet[v.UserID] = struct{}{}
|
||||
}
|
||||
|
||||
if first {
|
||||
resultSet = currentSet
|
||||
first = false
|
||||
} else {
|
||||
// Intersect with previous results
|
||||
for userID := range resultSet {
|
||||
if _, ok := currentSet[userID]; !ok {
|
||||
delete(resultSet, userID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Early exit if no users match
|
||||
if len(resultSet) == 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]int64, 0, len(resultSet))
|
||||
for userID := range resultSet {
|
||||
result = append(result, userID)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *userRepository) UpdateBalance(ctx context.Context, id int64, amount float64) error {
|
||||
client := clientFromContext(ctx, r.client)
|
||||
n, err := client.User.Update().Where(dbuser.IDEQ(id)).AddBalance(amount).Save(ctx)
|
||||
|
||||
@@ -166,7 +166,7 @@ func (s *UserRepoSuite) TestListWithFilters_Status() {
|
||||
s.mustCreateUser(&service.User{Email: "active@test.com", Status: service.StatusActive})
|
||||
s.mustCreateUser(&service.User{Email: "disabled@test.com", Status: service.StatusDisabled})
|
||||
|
||||
users, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, service.StatusActive, "", "")
|
||||
users, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, service.UserListFilters{Status: service.StatusActive})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(users, 1)
|
||||
s.Require().Equal(service.StatusActive, users[0].Status)
|
||||
@@ -176,7 +176,7 @@ func (s *UserRepoSuite) TestListWithFilters_Role() {
|
||||
s.mustCreateUser(&service.User{Email: "user@test.com", Role: service.RoleUser})
|
||||
s.mustCreateUser(&service.User{Email: "admin@test.com", Role: service.RoleAdmin})
|
||||
|
||||
users, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, "", service.RoleAdmin, "")
|
||||
users, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, service.UserListFilters{Role: service.RoleAdmin})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(users, 1)
|
||||
s.Require().Equal(service.RoleAdmin, users[0].Role)
|
||||
@@ -186,7 +186,7 @@ func (s *UserRepoSuite) TestListWithFilters_Search() {
|
||||
s.mustCreateUser(&service.User{Email: "alice@test.com", Username: "Alice"})
|
||||
s.mustCreateUser(&service.User{Email: "bob@test.com", Username: "Bob"})
|
||||
|
||||
users, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, "", "", "alice")
|
||||
users, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, service.UserListFilters{Search: "alice"})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(users, 1)
|
||||
s.Require().Contains(users[0].Email, "alice")
|
||||
@@ -196,22 +196,12 @@ func (s *UserRepoSuite) TestListWithFilters_SearchByUsername() {
|
||||
s.mustCreateUser(&service.User{Email: "u1@test.com", Username: "JohnDoe"})
|
||||
s.mustCreateUser(&service.User{Email: "u2@test.com", Username: "JaneSmith"})
|
||||
|
||||
users, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, "", "", "john")
|
||||
users, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, service.UserListFilters{Search: "john"})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(users, 1)
|
||||
s.Require().Equal("JohnDoe", users[0].Username)
|
||||
}
|
||||
|
||||
func (s *UserRepoSuite) TestListWithFilters_SearchByWechat() {
|
||||
s.mustCreateUser(&service.User{Email: "w1@test.com", Wechat: "wx_hello"})
|
||||
s.mustCreateUser(&service.User{Email: "w2@test.com", Wechat: "wx_world"})
|
||||
|
||||
users, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, "", "", "wx_hello")
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(users, 1)
|
||||
s.Require().Equal("wx_hello", users[0].Wechat)
|
||||
}
|
||||
|
||||
func (s *UserRepoSuite) TestListWithFilters_LoadsActiveSubscriptions() {
|
||||
user := s.mustCreateUser(&service.User{Email: "sub@test.com", Status: service.StatusActive})
|
||||
groupActive := s.mustCreateGroup("g-sub-active")
|
||||
@@ -226,7 +216,7 @@ func (s *UserRepoSuite) TestListWithFilters_LoadsActiveSubscriptions() {
|
||||
c.SetExpiresAt(time.Now().Add(-1 * time.Hour))
|
||||
})
|
||||
|
||||
users, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, "", "", "sub@")
|
||||
users, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, service.UserListFilters{Search: "sub@"})
|
||||
s.Require().NoError(err, "ListWithFilters")
|
||||
s.Require().Len(users, 1, "expected 1 user")
|
||||
s.Require().Len(users[0].Subscriptions, 1, "expected 1 active subscription")
|
||||
@@ -238,7 +228,6 @@ func (s *UserRepoSuite) TestListWithFilters_CombinedFilters() {
|
||||
s.mustCreateUser(&service.User{
|
||||
Email: "a@example.com",
|
||||
Username: "Alice",
|
||||
Wechat: "wx_a",
|
||||
Role: service.RoleUser,
|
||||
Status: service.StatusActive,
|
||||
Balance: 10,
|
||||
@@ -246,7 +235,6 @@ func (s *UserRepoSuite) TestListWithFilters_CombinedFilters() {
|
||||
target := s.mustCreateUser(&service.User{
|
||||
Email: "b@example.com",
|
||||
Username: "Bob",
|
||||
Wechat: "wx_b",
|
||||
Role: service.RoleAdmin,
|
||||
Status: service.StatusActive,
|
||||
Balance: 1,
|
||||
@@ -257,7 +245,7 @@ func (s *UserRepoSuite) TestListWithFilters_CombinedFilters() {
|
||||
Status: service.StatusDisabled,
|
||||
})
|
||||
|
||||
users, page, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, service.StatusActive, service.RoleAdmin, "b@")
|
||||
users, page, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, service.UserListFilters{Status: service.StatusActive, Role: service.RoleAdmin, Search: "b@"})
|
||||
s.Require().NoError(err, "ListWithFilters")
|
||||
s.Require().Equal(int64(1), page.Total, "ListWithFilters total mismatch")
|
||||
s.Require().Len(users, 1, "ListWithFilters len mismatch")
|
||||
@@ -448,7 +436,6 @@ func (s *UserRepoSuite) TestCRUD_And_Filters_And_AtomicUpdates() {
|
||||
user1 := s.mustCreateUser(&service.User{
|
||||
Email: "a@example.com",
|
||||
Username: "Alice",
|
||||
Wechat: "wx_a",
|
||||
Role: service.RoleUser,
|
||||
Status: service.StatusActive,
|
||||
Balance: 10,
|
||||
@@ -456,7 +443,6 @@ func (s *UserRepoSuite) TestCRUD_And_Filters_And_AtomicUpdates() {
|
||||
user2 := s.mustCreateUser(&service.User{
|
||||
Email: "b@example.com",
|
||||
Username: "Bob",
|
||||
Wechat: "wx_b",
|
||||
Role: service.RoleAdmin,
|
||||
Status: service.StatusActive,
|
||||
Balance: 1,
|
||||
@@ -501,7 +487,7 @@ func (s *UserRepoSuite) TestCRUD_And_Filters_And_AtomicUpdates() {
|
||||
s.Require().Equal(user1.Concurrency+3, got5.Concurrency)
|
||||
|
||||
params := pagination.PaginationParams{Page: 1, PageSize: 10}
|
||||
users, page, err := s.repo.ListWithFilters(s.ctx, params, service.StatusActive, service.RoleAdmin, "b@")
|
||||
users, page, err := s.repo.ListWithFilters(s.ctx, params, service.UserListFilters{Status: service.StatusActive, Role: service.RoleAdmin, Search: "b@"})
|
||||
s.Require().NoError(err, "ListWithFilters")
|
||||
s.Require().Equal(int64(1), page.Total, "ListWithFilters total mismatch")
|
||||
s.Require().Len(users, 1, "ListWithFilters len mismatch")
|
||||
|
||||
@@ -36,6 +36,8 @@ var ProviderSet = wire.NewSet(
|
||||
NewUsageLogRepository,
|
||||
NewSettingRepository,
|
||||
NewUserSubscriptionRepository,
|
||||
NewUserAttributeDefinitionRepository,
|
||||
NewUserAttributeValueRepository,
|
||||
|
||||
// Cache implementations
|
||||
NewGatewayCache,
|
||||
|
||||
Reference in New Issue
Block a user