Skip to content

Commit 0b8e84f

Browse files
dexcoder6claude
andcommitted
feat: 增强用户管理功能,添加用户名、微信号和备注字段
- 新增User模型字段:username(用户名)、wechat(微信号)、notes(备注) - 扩展用户搜索功能,支持通过用户名和微信号搜索 - 添加用户个人资料更新功能,用户可自行编辑用户名和微信号 - 管理员用户列表新增用户名、微信号、备注显示列 - 备注字段仅对管理员可见,增强数据安全性 - 完善中英文国际化翻译 - 修复国际化文件中重复属性的TypeScript错误 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent f0fabf8 commit 0b8e84f

13 files changed

Lines changed: 401 additions & 21 deletions

File tree

backend/internal/handler/admin/user_handler.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ func NewUserHandler(adminService service.AdminService) *UserHandler {
2525
type CreateUserRequest struct {
2626
Email string `json:"email" binding:"required,email"`
2727
Password string `json:"password" binding:"required,min=6"`
28+
Username string `json:"username"`
29+
Wechat string `json:"wechat"`
30+
Notes string `json:"notes"`
2831
Balance float64 `json:"balance"`
2932
Concurrency int `json:"concurrency"`
3033
AllowedGroups []int64 `json:"allowed_groups"`
@@ -35,6 +38,9 @@ type CreateUserRequest struct {
3538
type UpdateUserRequest struct {
3639
Email string `json:"email" binding:"omitempty,email"`
3740
Password string `json:"password" binding:"omitempty,min=6"`
41+
Username *string `json:"username"`
42+
Wechat *string `json:"wechat"`
43+
Notes *string `json:"notes"`
3844
Balance *float64 `json:"balance"`
3945
Concurrency *int `json:"concurrency"`
4046
Status string `json:"status" binding:"omitempty,oneof=active disabled"`
@@ -94,6 +100,9 @@ func (h *UserHandler) Create(c *gin.Context) {
94100
user, err := h.adminService.CreateUser(c.Request.Context(), &service.CreateUserInput{
95101
Email: req.Email,
96102
Password: req.Password,
103+
Username: req.Username,
104+
Wechat: req.Wechat,
105+
Notes: req.Notes,
97106
Balance: req.Balance,
98107
Concurrency: req.Concurrency,
99108
AllowedGroups: req.AllowedGroups,
@@ -125,6 +134,9 @@ func (h *UserHandler) Update(c *gin.Context) {
125134
user, err := h.adminService.UpdateUser(c.Request.Context(), userID, &service.UpdateUserInput{
126135
Email: req.Email,
127136
Password: req.Password,
137+
Username: req.Username,
138+
Wechat: req.Wechat,
139+
Notes: req.Notes,
128140
Balance: req.Balance,
129141
Concurrency: req.Concurrency,
130142
Status: req.Status,

backend/internal/handler/user_handler.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ type ChangePasswordRequest struct {
2626
NewPassword string `json:"new_password" binding:"required,min=6"`
2727
}
2828

29+
// UpdateProfileRequest represents the update profile request payload
30+
type UpdateProfileRequest struct {
31+
Username *string `json:"username"`
32+
Wechat *string `json:"wechat"`
33+
}
34+
2935
// GetProfile handles getting user profile
3036
// GET /api/v1/users/me
3137
func (h *UserHandler) GetProfile(c *gin.Context) {
@@ -47,6 +53,9 @@ func (h *UserHandler) GetProfile(c *gin.Context) {
4753
return
4854
}
4955

56+
// 清空notes字段,普通用户不应看到备注
57+
userData.Notes = ""
58+
5059
response.Success(c, userData)
5160
}
5261

@@ -83,3 +92,40 @@ func (h *UserHandler) ChangePassword(c *gin.Context) {
8392

8493
response.Success(c, gin.H{"message": "Password changed successfully"})
8594
}
95+
96+
// UpdateProfile handles updating user profile
97+
// PUT /api/v1/users/me
98+
func (h *UserHandler) UpdateProfile(c *gin.Context) {
99+
userValue, exists := c.Get("user")
100+
if !exists {
101+
response.Unauthorized(c, "User not authenticated")
102+
return
103+
}
104+
105+
user, ok := userValue.(*model.User)
106+
if !ok {
107+
response.InternalError(c, "Invalid user context")
108+
return
109+
}
110+
111+
var req UpdateProfileRequest
112+
if err := c.ShouldBindJSON(&req); err != nil {
113+
response.BadRequest(c, "Invalid request: "+err.Error())
114+
return
115+
}
116+
117+
svcReq := service.UpdateProfileRequest{
118+
Username: req.Username,
119+
Wechat: req.Wechat,
120+
}
121+
updatedUser, err := h.userService.UpdateProfile(c.Request.Context(), user.ID, svcReq)
122+
if err != nil {
123+
response.BadRequest(c, "Failed to update profile: "+err.Error())
124+
return
125+
}
126+
127+
// 清空notes字段,普通用户不应看到备注
128+
updatedUser.Notes = ""
129+
130+
response.Success(c, updatedUser)
131+
}

backend/internal/model/user.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import (
1111
type User struct {
1212
ID int64 `gorm:"primaryKey" json:"id"`
1313
Email string `gorm:"uniqueIndex;size:255;not null" json:"email"`
14+
Username string `gorm:"size:100;default:''" json:"username"`
15+
Wechat string `gorm:"size:100;default:''" json:"wechat"`
16+
Notes string `gorm:"type:text;default:''" json:"notes"`
1417
PasswordHash string `gorm:"size:255;not null" json:"-"`
1518
Role string `gorm:"size:20;default:user;not null" json:"role"` // admin/user
1619
Balance float64 `gorm:"type:decimal(20,8);default:0;not null" json:"balance"`

backend/internal/repository/user_repo.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ func (r *UserRepository) ListWithFilters(ctx context.Context, params pagination.
6666
}
6767
if search != "" {
6868
searchPattern := "%" + search + "%"
69-
db = db.Where("email ILIKE ?", searchPattern)
69+
db = db.Where(
70+
"email ILIKE ? OR username ILIKE ? OR wechat ILIKE ?",
71+
searchPattern, searchPattern, searchPattern,
72+
)
7073
}
7174

7275
if err := db.Count(&total).Error; err != nil {

backend/internal/server/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ func registerRoutes(r *gin.Engine, h *handler.Handlers, s *service.Services, rep
8282
{
8383
user.GET("/profile", h.User.GetProfile)
8484
user.PUT("/password", h.User.ChangePassword)
85+
user.PUT("", h.User.UpdateProfile)
8586
}
8687

8788
// API Key管理

backend/internal/service/admin_service.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ type AdminService interface {
7171
type CreateUserInput struct {
7272
Email string
7373
Password string
74+
Username string
75+
Wechat string
76+
Notes string
7477
Balance float64
7578
Concurrency int
7679
AllowedGroups []int64
@@ -79,6 +82,9 @@ type CreateUserInput struct {
7982
type UpdateUserInput struct {
8083
Email string
8184
Password string
85+
Username *string
86+
Wechat *string
87+
Notes *string
8288
Balance *float64 // 使用指针区分"未提供"和"设置为0"
8389
Concurrency *int // 使用指针区分"未提供"和"设置为0"
8490
Status string
@@ -237,6 +243,9 @@ func (s *adminServiceImpl) GetUser(ctx context.Context, id int64) (*model.User,
237243
func (s *adminServiceImpl) CreateUser(ctx context.Context, input *CreateUserInput) (*model.User, error) {
238244
user := &model.User{
239245
Email: input.Email,
246+
Username: input.Username,
247+
Wechat: input.Wechat,
248+
Notes: input.Notes,
240249
Role: "user", // Always create as regular user, never admin
241250
Balance: input.Balance,
242251
Concurrency: input.Concurrency,
@@ -274,6 +283,18 @@ func (s *adminServiceImpl) UpdateUser(ctx context.Context, id int64, input *Upda
274283
return nil, err
275284
}
276285
}
286+
287+
// 更新用户字段
288+
if input.Username != nil {
289+
user.Username = *input.Username
290+
}
291+
if input.Wechat != nil {
292+
user.Wechat = *input.Wechat
293+
}
294+
if input.Notes != nil {
295+
user.Notes = *input.Notes
296+
}
297+
277298
// Role is not allowed to be changed via API to prevent privilege escalation
278299
if input.Status != "" {
279300
user.Status = input.Status

backend/internal/service/user_service.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ var (
2121
// UpdateProfileRequest 更新用户资料请求
2222
type UpdateProfileRequest struct {
2323
Email *string `json:"email"`
24+
Username *string `json:"username"`
25+
Wechat *string `json:"wechat"`
2426
Concurrency *int `json:"concurrency"`
2527
}
2628

@@ -77,6 +79,14 @@ func (s *UserService) UpdateProfile(ctx context.Context, userID int64, req Updat
7779
user.Email = *req.Email
7880
}
7981

82+
if req.Username != nil {
83+
user.Username = *req.Username
84+
}
85+
86+
if req.Wechat != nil {
87+
user.Wechat = *req.Wechat
88+
}
89+
8090
if req.Concurrency != nil {
8191
user.Concurrency = *req.Concurrency
8292
}

frontend/src/api/user.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,20 @@ import type { User, ChangePasswordRequest } from '@/types';
1111
* @returns User profile data
1212
*/
1313
export async function getProfile(): Promise<User> {
14-
const { data } = await apiClient.get<User>('/users/me');
14+
const { data } = await apiClient.get<User>('/user/profile');
15+
return data;
16+
}
17+
18+
/**
19+
* Update current user profile
20+
* @param profile - Profile data to update
21+
* @returns Updated user profile data
22+
*/
23+
export async function updateProfile(profile: {
24+
username?: string;
25+
wechat?: string;
26+
}): Promise<User> {
27+
const { data } = await apiClient.put<User>('/user', profile);
1528
return data;
1629
}
1730

@@ -29,12 +42,13 @@ export async function changePassword(
2942
new_password: newPassword,
3043
};
3144

32-
const { data } = await apiClient.post<{ message: string }>('/users/me/password', payload);
45+
const { data } = await apiClient.put<{ message: string }>('/user/password', payload);
3346
return data;
3447
}
3548

3649
export const userAPI = {
3750
getProfile,
51+
updateProfile,
3852
changePassword,
3953
};
4054

frontend/src/i18n/locales/en.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,15 @@ export default {
335335
memberSince: 'Member Since',
336336
administrator: 'Administrator',
337337
user: 'User',
338+
username: 'Username',
339+
wechat: 'WeChat ID',
340+
enterUsername: 'Enter username',
341+
enterWechat: 'Enter WeChat ID',
342+
editProfile: 'Edit Profile',
343+
updateProfile: 'Update Profile',
344+
updating: 'Updating...',
345+
updateSuccess: 'Profile updated successfully',
346+
updateFailed: 'Failed to update profile',
338347
changePassword: 'Change Password',
339348
currentPassword: 'Current Password',
340349
newPassword: 'New Password',
@@ -446,8 +455,28 @@ export default {
446455
admin: 'Admin',
447456
user: 'User',
448457
disabled: 'Disabled',
458+
email: 'Email',
459+
password: 'Password',
460+
username: 'Username',
461+
wechat: 'WeChat ID',
462+
notes: 'Notes',
463+
enterEmail: 'Enter email',
464+
enterPassword: 'Enter password',
465+
enterUsername: 'Enter username (optional)',
466+
enterWechat: 'Enter WeChat ID (optional)',
467+
enterNotes: 'Enter notes (admin only)',
468+
notesHint: 'This note is only visible to administrators',
469+
enterNewPassword: 'Enter new password (optional)',
470+
leaveEmptyToKeep: 'Leave empty to keep current password',
471+
generatePassword: 'Generate random password',
472+
copyPassword: 'Copy password',
473+
creating: 'Creating...',
474+
updating: 'Updating...',
449475
columns: {
450476
user: 'User',
477+
username: 'Username',
478+
wechat: 'WeChat ID',
479+
notes: 'Notes',
451480
role: 'Role',
452481
subscriptions: 'Subscriptions',
453482
balance: 'Balance',
@@ -471,16 +500,6 @@ export default {
471500
none: 'None',
472501
noUsersYet: 'No users yet',
473502
createFirstUser: 'Create your first user to get started.',
474-
email: 'Email',
475-
password: 'Password',
476-
enterEmail: 'Enter email',
477-
enterPassword: 'Enter password',
478-
enterNewPassword: 'Enter new password (optional)',
479-
leaveEmptyToKeep: 'Leave empty to keep current password',
480-
generatePassword: 'Generate random password',
481-
copyPassword: 'Copy password',
482-
creating: 'Creating...',
483-
updating: 'Updating...',
484503
userCreated: 'User created successfully',
485504
userUpdated: 'User updated successfully',
486505
userDeleted: 'User deleted successfully',

0 commit comments

Comments
 (0)