feat(backend): add user custom attributes system

Add a flexible user attribute system that allows admins to define
custom fields for users (text, textarea, number, email, url, date,
select, multi_select types).

- Add Ent schemas for UserAttributeDefinition and UserAttributeValue
- Add service layer with validation logic
- Add repository layer with batch operations support
- Add admin API endpoints for CRUD and reorder operations
- Add batch API for loading attribute values for multiple users
- Add database migration (018_user_attributes.sql)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Edric Li
2026-01-01 18:58:34 +08:00
parent 7331220e06
commit 3c3fed886f
36 changed files with 10649 additions and 122 deletions

View File

@@ -477,7 +477,6 @@ var (
{Name: "concurrency", Type: field.TypeInt, Default: 5},
{Name: "status", Type: field.TypeString, Size: 20, Default: "active"},
{Name: "username", Type: field.TypeString, Size: 100, Default: ""},
{Name: "wechat", Type: field.TypeString, Size: 100, Default: ""},
{Name: "notes", Type: field.TypeString, Default: "", SchemaType: map[string]string{"postgres": "text"}},
}
// UsersTable holds the schema information for the "users" table.
@@ -531,6 +530,92 @@ var (
},
},
}
// UserAttributeDefinitionsColumns holds the columns for the "user_attribute_definitions" table.
UserAttributeDefinitionsColumns = []*schema.Column{
{Name: "id", Type: field.TypeInt64, Increment: true},
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "deleted_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "key", Type: field.TypeString, Size: 100},
{Name: "name", Type: field.TypeString, Size: 255},
{Name: "description", Type: field.TypeString, Default: "", SchemaType: map[string]string{"postgres": "text"}},
{Name: "type", Type: field.TypeString, Size: 20},
{Name: "options", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
{Name: "required", Type: field.TypeBool, Default: false},
{Name: "validation", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
{Name: "placeholder", Type: field.TypeString, Size: 255, Default: ""},
{Name: "display_order", Type: field.TypeInt, Default: 0},
{Name: "enabled", Type: field.TypeBool, Default: true},
}
// UserAttributeDefinitionsTable holds the schema information for the "user_attribute_definitions" table.
UserAttributeDefinitionsTable = &schema.Table{
Name: "user_attribute_definitions",
Columns: UserAttributeDefinitionsColumns,
PrimaryKey: []*schema.Column{UserAttributeDefinitionsColumns[0]},
Indexes: []*schema.Index{
{
Name: "userattributedefinition_key",
Unique: false,
Columns: []*schema.Column{UserAttributeDefinitionsColumns[4]},
},
{
Name: "userattributedefinition_enabled",
Unique: false,
Columns: []*schema.Column{UserAttributeDefinitionsColumns[13]},
},
{
Name: "userattributedefinition_display_order",
Unique: false,
Columns: []*schema.Column{UserAttributeDefinitionsColumns[12]},
},
{
Name: "userattributedefinition_deleted_at",
Unique: false,
Columns: []*schema.Column{UserAttributeDefinitionsColumns[3]},
},
},
}
// UserAttributeValuesColumns holds the columns for the "user_attribute_values" table.
UserAttributeValuesColumns = []*schema.Column{
{Name: "id", Type: field.TypeInt64, Increment: true},
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "value", Type: field.TypeString, Size: 2147483647, Default: ""},
{Name: "user_id", Type: field.TypeInt64},
{Name: "attribute_id", Type: field.TypeInt64},
}
// UserAttributeValuesTable holds the schema information for the "user_attribute_values" table.
UserAttributeValuesTable = &schema.Table{
Name: "user_attribute_values",
Columns: UserAttributeValuesColumns,
PrimaryKey: []*schema.Column{UserAttributeValuesColumns[0]},
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "user_attribute_values_users_attribute_values",
Columns: []*schema.Column{UserAttributeValuesColumns[4]},
RefColumns: []*schema.Column{UsersColumns[0]},
OnDelete: schema.NoAction,
},
{
Symbol: "user_attribute_values_user_attribute_definitions_values",
Columns: []*schema.Column{UserAttributeValuesColumns[5]},
RefColumns: []*schema.Column{UserAttributeDefinitionsColumns[0]},
OnDelete: schema.NoAction,
},
},
Indexes: []*schema.Index{
{
Name: "userattributevalue_user_id_attribute_id",
Unique: true,
Columns: []*schema.Column{UserAttributeValuesColumns[4], UserAttributeValuesColumns[5]},
},
{
Name: "userattributevalue_attribute_id",
Unique: false,
Columns: []*schema.Column{UserAttributeValuesColumns[5]},
},
},
}
// UserSubscriptionsColumns holds the columns for the "user_subscriptions" table.
UserSubscriptionsColumns = []*schema.Column{
{Name: "id", Type: field.TypeInt64, Increment: true},
@@ -627,6 +712,8 @@ var (
UsageLogsTable,
UsersTable,
UserAllowedGroupsTable,
UserAttributeDefinitionsTable,
UserAttributeValuesTable,
UserSubscriptionsTable,
}
)
@@ -676,6 +763,14 @@ func init() {
UserAllowedGroupsTable.Annotation = &entsql.Annotation{
Table: "user_allowed_groups",
}
UserAttributeDefinitionsTable.Annotation = &entsql.Annotation{
Table: "user_attribute_definitions",
}
UserAttributeValuesTable.ForeignKeys[0].RefTable = UsersTable
UserAttributeValuesTable.ForeignKeys[1].RefTable = UserAttributeDefinitionsTable
UserAttributeValuesTable.Annotation = &entsql.Annotation{
Table: "user_attribute_values",
}
UserSubscriptionsTable.ForeignKeys[0].RefTable = GroupsTable
UserSubscriptionsTable.ForeignKeys[1].RefTable = UsersTable
UserSubscriptionsTable.ForeignKeys[2].RefTable = UsersTable