diff --git a/backend/internal/handler/admin/user_handler.go b/backend/internal/handler/admin/user_handler.go index 4d4d5d7b..f3b8dbd7 100644 --- a/backend/internal/handler/admin/user_handler.go +++ b/backend/internal/handler/admin/user_handler.go @@ -25,6 +25,9 @@ func NewUserHandler(adminService service.AdminService) *UserHandler { type CreateUserRequest struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` + Username string `json:"username"` + Wechat string `json:"wechat"` + Notes string `json:"notes"` Balance float64 `json:"balance"` Concurrency int `json:"concurrency"` AllowedGroups []int64 `json:"allowed_groups"` @@ -35,6 +38,9 @@ type CreateUserRequest struct { type UpdateUserRequest struct { Email string `json:"email" binding:"omitempty,email"` Password string `json:"password" binding:"omitempty,min=6"` + Username *string `json:"username"` + Wechat *string `json:"wechat"` + Notes *string `json:"notes"` Balance *float64 `json:"balance"` Concurrency *int `json:"concurrency"` Status string `json:"status" binding:"omitempty,oneof=active disabled"` @@ -94,6 +100,9 @@ func (h *UserHandler) Create(c *gin.Context) { user, err := h.adminService.CreateUser(c.Request.Context(), &service.CreateUserInput{ Email: req.Email, Password: req.Password, + Username: req.Username, + Wechat: req.Wechat, + Notes: req.Notes, Balance: req.Balance, Concurrency: req.Concurrency, AllowedGroups: req.AllowedGroups, @@ -125,6 +134,9 @@ func (h *UserHandler) Update(c *gin.Context) { user, err := h.adminService.UpdateUser(c.Request.Context(), userID, &service.UpdateUserInput{ Email: req.Email, Password: req.Password, + Username: req.Username, + Wechat: req.Wechat, + Notes: req.Notes, Balance: req.Balance, Concurrency: req.Concurrency, Status: req.Status, diff --git a/backend/internal/handler/user_handler.go b/backend/internal/handler/user_handler.go index 7a49a0bd..372a145c 100644 --- a/backend/internal/handler/user_handler.go +++ b/backend/internal/handler/user_handler.go @@ -26,6 +26,12 @@ type ChangePasswordRequest struct { NewPassword string `json:"new_password" binding:"required,min=6"` } +// UpdateProfileRequest represents the update profile request payload +type UpdateProfileRequest struct { + Username *string `json:"username"` + Wechat *string `json:"wechat"` +} + // GetProfile handles getting user profile // GET /api/v1/users/me func (h *UserHandler) GetProfile(c *gin.Context) { @@ -47,6 +53,9 @@ func (h *UserHandler) GetProfile(c *gin.Context) { return } + // 清空notes字段,普通用户不应看到备注 + userData.Notes = "" + response.Success(c, userData) } @@ -83,3 +92,40 @@ func (h *UserHandler) ChangePassword(c *gin.Context) { response.Success(c, gin.H{"message": "Password changed successfully"}) } + +// UpdateProfile handles updating user profile +// PUT /api/v1/users/me +func (h *UserHandler) UpdateProfile(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + var req UpdateProfileRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + svcReq := service.UpdateProfileRequest{ + Username: req.Username, + Wechat: req.Wechat, + } + updatedUser, err := h.userService.UpdateProfile(c.Request.Context(), user.ID, svcReq) + if err != nil { + response.BadRequest(c, "Failed to update profile: "+err.Error()) + return + } + + // 清空notes字段,普通用户不应看到备注 + updatedUser.Notes = "" + + response.Success(c, updatedUser) +} diff --git a/backend/internal/model/user.go b/backend/internal/model/user.go index cf3877fc..2f90d8a1 100644 --- a/backend/internal/model/user.go +++ b/backend/internal/model/user.go @@ -11,6 +11,9 @@ import ( type User struct { ID int64 `gorm:"primaryKey" json:"id"` Email string `gorm:"uniqueIndex;size:255;not null" json:"email"` + Username string `gorm:"size:100;default:''" json:"username"` + Wechat string `gorm:"size:100;default:''" json:"wechat"` + Notes string `gorm:"type:text;default:''" json:"notes"` PasswordHash string `gorm:"size:255;not null" json:"-"` Role string `gorm:"size:20;default:user;not null" json:"role"` // admin/user Balance float64 `gorm:"type:decimal(20,8);default:0;not null" json:"balance"` diff --git a/backend/internal/repository/user_repo.go b/backend/internal/repository/user_repo.go index 0b49a52d..a17843d0 100644 --- a/backend/internal/repository/user_repo.go +++ b/backend/internal/repository/user_repo.go @@ -66,7 +66,10 @@ func (r *UserRepository) ListWithFilters(ctx context.Context, params pagination. } if search != "" { searchPattern := "%" + search + "%" - db = db.Where("email ILIKE ?", searchPattern) + db = db.Where( + "email ILIKE ? OR username ILIKE ? OR wechat ILIKE ?", + searchPattern, searchPattern, searchPattern, + ) } if err := db.Count(&total).Error; err != nil { diff --git a/backend/internal/server/router.go b/backend/internal/server/router.go index d44a021b..a1cf5750 100644 --- a/backend/internal/server/router.go +++ b/backend/internal/server/router.go @@ -82,6 +82,7 @@ func registerRoutes(r *gin.Engine, h *handler.Handlers, s *service.Services, rep { user.GET("/profile", h.User.GetProfile) user.PUT("/password", h.User.ChangePassword) + user.PUT("", h.User.UpdateProfile) } // API Key管理 diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index fced40b5..71ee3f1b 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -71,6 +71,9 @@ type AdminService interface { type CreateUserInput struct { Email string Password string + Username string + Wechat string + Notes string Balance float64 Concurrency int AllowedGroups []int64 @@ -79,6 +82,9 @@ type CreateUserInput struct { type UpdateUserInput struct { Email string Password string + Username *string + Wechat *string + Notes *string Balance *float64 // 使用指针区分"未提供"和"设置为0" Concurrency *int // 使用指针区分"未提供"和"设置为0" Status string @@ -237,6 +243,9 @@ func (s *adminServiceImpl) GetUser(ctx context.Context, id int64) (*model.User, func (s *adminServiceImpl) CreateUser(ctx context.Context, input *CreateUserInput) (*model.User, error) { user := &model.User{ Email: input.Email, + Username: input.Username, + Wechat: input.Wechat, + Notes: input.Notes, Role: "user", // Always create as regular user, never admin Balance: input.Balance, Concurrency: input.Concurrency, @@ -274,6 +283,18 @@ func (s *adminServiceImpl) UpdateUser(ctx context.Context, id int64, input *Upda return nil, err } } + + // 更新用户字段 + if input.Username != nil { + user.Username = *input.Username + } + if input.Wechat != nil { + user.Wechat = *input.Wechat + } + if input.Notes != nil { + user.Notes = *input.Notes + } + // Role is not allowed to be changed via API to prevent privilege escalation if input.Status != "" { user.Status = input.Status diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go index e939b12f..0cb8e477 100644 --- a/backend/internal/service/user_service.go +++ b/backend/internal/service/user_service.go @@ -21,6 +21,8 @@ var ( // UpdateProfileRequest 更新用户资料请求 type UpdateProfileRequest struct { Email *string `json:"email"` + Username *string `json:"username"` + Wechat *string `json:"wechat"` Concurrency *int `json:"concurrency"` } @@ -77,6 +79,14 @@ func (s *UserService) UpdateProfile(ctx context.Context, userID int64, req Updat user.Email = *req.Email } + if req.Username != nil { + user.Username = *req.Username + } + + if req.Wechat != nil { + user.Wechat = *req.Wechat + } + if req.Concurrency != nil { user.Concurrency = *req.Concurrency } diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts index 890c0fb8..db994e73 100644 --- a/frontend/src/api/user.ts +++ b/frontend/src/api/user.ts @@ -11,7 +11,20 @@ import type { User, ChangePasswordRequest } from '@/types'; * @returns User profile data */ export async function getProfile(): Promise { - const { data } = await apiClient.get('/users/me'); + const { data } = await apiClient.get('/user/profile'); + return data; +} + +/** + * Update current user profile + * @param profile - Profile data to update + * @returns Updated user profile data + */ +export async function updateProfile(profile: { + username?: string; + wechat?: string; +}): Promise { + const { data } = await apiClient.put('/user', profile); return data; } @@ -29,12 +42,13 @@ export async function changePassword( new_password: newPassword, }; - const { data } = await apiClient.post<{ message: string }>('/users/me/password', payload); + const { data } = await apiClient.put<{ message: string }>('/user/password', payload); return data; } export const userAPI = { getProfile, + updateProfile, changePassword, }; diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index b2833025..a50d7067 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -335,6 +335,15 @@ export default { memberSince: 'Member Since', administrator: 'Administrator', user: 'User', + username: 'Username', + wechat: 'WeChat ID', + enterUsername: 'Enter username', + enterWechat: 'Enter WeChat ID', + editProfile: 'Edit Profile', + updateProfile: 'Update Profile', + updating: 'Updating...', + updateSuccess: 'Profile updated successfully', + updateFailed: 'Failed to update profile', changePassword: 'Change Password', currentPassword: 'Current Password', newPassword: 'New Password', @@ -446,8 +455,28 @@ export default { admin: 'Admin', user: 'User', disabled: 'Disabled', + email: 'Email', + password: 'Password', + username: 'Username', + wechat: 'WeChat ID', + notes: 'Notes', + enterEmail: 'Enter email', + enterPassword: 'Enter password', + enterUsername: 'Enter username (optional)', + enterWechat: 'Enter WeChat ID (optional)', + enterNotes: 'Enter notes (admin only)', + notesHint: 'This note is only visible to administrators', + enterNewPassword: 'Enter new password (optional)', + leaveEmptyToKeep: 'Leave empty to keep current password', + generatePassword: 'Generate random password', + copyPassword: 'Copy password', + creating: 'Creating...', + updating: 'Updating...', columns: { user: 'User', + username: 'Username', + wechat: 'WeChat ID', + notes: 'Notes', role: 'Role', subscriptions: 'Subscriptions', balance: 'Balance', @@ -471,16 +500,6 @@ export default { none: 'None', noUsersYet: 'No users yet', createFirstUser: 'Create your first user to get started.', - email: 'Email', - password: 'Password', - enterEmail: 'Enter email', - enterPassword: 'Enter password', - enterNewPassword: 'Enter new password (optional)', - leaveEmptyToKeep: 'Leave empty to keep current password', - generatePassword: 'Generate random password', - copyPassword: 'Copy password', - creating: 'Creating...', - updating: 'Updating...', userCreated: 'User created successfully', userUpdated: 'User updated successfully', userDeleted: 'User deleted successfully', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 1d8e8792..ed3b0b0c 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -335,6 +335,15 @@ export default { memberSince: '注册时间', administrator: '管理员', user: '用户', + username: '用户名', + wechat: '微信号', + enterUsername: '输入用户名', + enterWechat: '输入微信号', + editProfile: '编辑个人资料', + updateProfile: '更新资料', + updating: '更新中...', + updateSuccess: '资料更新成功', + updateFailed: '资料更新失败', changePassword: '修改密码', currentPassword: '当前密码', newPassword: '新密码', @@ -460,12 +469,38 @@ export default { deleteUser: '删除用户', deleteConfirmMessage: "确定要删除用户 '{email}' 吗?此操作无法撤销。", searchPlaceholder: '搜索用户...', + searchUsers: '搜索用户...', roleFilter: '角色筛选', allRoles: '全部角色', + allStatus: '全部状态', statusFilter: '状态筛选', allStatuses: '全部状态', + admin: '管理员', + user: '用户', + disabled: '禁用', + email: '邮箱', + password: '密码', + username: '用户名', + wechat: '微信号', + notes: '备注', + enterEmail: '请输入邮箱', + enterPassword: '请输入密码', + enterUsername: '请输入用户名(选填)', + enterWechat: '请输入微信号(选填)', + enterNotes: '请输入备注(仅管理员可见)', + notesHint: '此备注仅对管理员可见', + enterNewPassword: '请输入新密码(选填)', + leaveEmptyToKeep: '留空则保持原密码不变', + generatePassword: '生成随机密码', + copyPassword: '复制密码', + creating: '创建中...', + updating: '更新中...', columns: { + user: '用户', email: '邮箱', + username: '用户名', + wechat: '微信号', + notes: '备注', role: '角色', subscriptions: '订阅分组', balance: '余额', @@ -474,13 +509,33 @@ export default { status: '状态', created: '创建时间', actions: '操作', - user: '用户', }, today: '今日', total: '累计', noSubscription: '暂无订阅', daysRemaining: '{days}天', expired: '已过期', + disableUser: '禁用用户', + enableUser: '启用用户', + viewApiKeys: '查看 API 密钥', + userApiKeys: '用户 API 密钥', + noApiKeys: '此用户暂无 API 密钥', + group: '分组', + none: '无', + noUsersYet: '暂无用户', + createFirstUser: '创建您的第一个用户以开始使用系统', + userCreated: '用户创建成功', + userUpdated: '用户更新成功', + userDeleted: '用户删除成功', + userEnabled: '用户已启用', + userDisabled: '用户已禁用', + failedToLoad: '加载用户列表失败', + failedToCreate: '创建用户失败', + failedToUpdate: '更新用户失败', + failedToDelete: '删除用户失败', + failedToToggle: '更新用户状态失败', + failedToLoadApiKeys: '加载用户 API 密钥失败', + deleteConfirm: "确定要删除用户 '{email}' 吗?此操作无法撤销。", roles: { admin: '管理员', user: '用户', @@ -492,6 +547,13 @@ export default { form: { emailLabel: '邮箱', emailPlaceholder: '请输入邮箱', + usernameLabel: '用户名', + usernamePlaceholder: '请输入用户名(选填)', + wechatLabel: '微信号', + wechatPlaceholder: '请输入微信号(选填)', + notesLabel: '备注', + notesPlaceholder: '请输入备注(仅管理员可见)', + notesHint: '此备注仅对管理员可见', passwordLabel: '密码', passwordPlaceholder: '请输入密码(留空则不修改)', roleLabel: '角色', @@ -515,9 +577,7 @@ export default { userDeletedSuccess: '用户删除成功', balanceAdjustedSuccess: '余额调整成功', concurrencyAdjustedSuccess: '并发数调整成功', - failedToLoad: '加载用户列表失败', failedToSave: '保存用户失败', - failedToDelete: '删除用户失败', failedToAdjust: '调整失败', setAllowedGroups: '设置允许分组', allowedGroupsHint: '选择此用户可以使用的标准分组。订阅类型分组请在订阅管理中配置。', diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index e78d0981..330ecc2b 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -7,6 +7,8 @@ export interface User { id: number; username: string; + wechat: string; + notes: string; email: string; role: 'admin' | 'user'; // User role for authorization balance: number; // User balance for API usage @@ -563,6 +565,9 @@ export interface ApiKeyUsageTrendPoint { export interface UpdateUserRequest { email?: string; password?: string; + username?: string; + wechat?: string; + notes?: string; role?: 'admin' | 'user'; balance?: number; concurrency?: number; diff --git a/frontend/src/views/admin/UsersView.vue b/frontend/src/views/admin/UsersView.vue index 6a541d12..b838c4aa 100644 --- a/frontend/src/views/admin/UsersView.vue +++ b/frontend/src/views/admin/UsersView.vue @@ -73,6 +73,27 @@ + + + + + +