Merge PR #15: feat: 增强用户管理功能,添加用户名、微信号和备注字段
This commit is contained in:
@@ -25,6 +25,9 @@ func NewUserHandler(adminService service.AdminService) *UserHandler {
|
|||||||
type CreateUserRequest struct {
|
type CreateUserRequest struct {
|
||||||
Email string `json:"email" binding:"required,email"`
|
Email string `json:"email" binding:"required,email"`
|
||||||
Password string `json:"password" binding:"required,min=6"`
|
Password string `json:"password" binding:"required,min=6"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Wechat string `json:"wechat"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
Balance float64 `json:"balance"`
|
Balance float64 `json:"balance"`
|
||||||
Concurrency int `json:"concurrency"`
|
Concurrency int `json:"concurrency"`
|
||||||
AllowedGroups []int64 `json:"allowed_groups"`
|
AllowedGroups []int64 `json:"allowed_groups"`
|
||||||
@@ -35,6 +38,9 @@ type CreateUserRequest struct {
|
|||||||
type UpdateUserRequest struct {
|
type UpdateUserRequest struct {
|
||||||
Email string `json:"email" binding:"omitempty,email"`
|
Email string `json:"email" binding:"omitempty,email"`
|
||||||
Password string `json:"password" binding:"omitempty,min=6"`
|
Password string `json:"password" binding:"omitempty,min=6"`
|
||||||
|
Username *string `json:"username"`
|
||||||
|
Wechat *string `json:"wechat"`
|
||||||
|
Notes *string `json:"notes"`
|
||||||
Balance *float64 `json:"balance"`
|
Balance *float64 `json:"balance"`
|
||||||
Concurrency *int `json:"concurrency"`
|
Concurrency *int `json:"concurrency"`
|
||||||
Status string `json:"status" binding:"omitempty,oneof=active disabled"`
|
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{
|
user, err := h.adminService.CreateUser(c.Request.Context(), &service.CreateUserInput{
|
||||||
Email: req.Email,
|
Email: req.Email,
|
||||||
Password: req.Password,
|
Password: req.Password,
|
||||||
|
Username: req.Username,
|
||||||
|
Wechat: req.Wechat,
|
||||||
|
Notes: req.Notes,
|
||||||
Balance: req.Balance,
|
Balance: req.Balance,
|
||||||
Concurrency: req.Concurrency,
|
Concurrency: req.Concurrency,
|
||||||
AllowedGroups: req.AllowedGroups,
|
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{
|
user, err := h.adminService.UpdateUser(c.Request.Context(), userID, &service.UpdateUserInput{
|
||||||
Email: req.Email,
|
Email: req.Email,
|
||||||
Password: req.Password,
|
Password: req.Password,
|
||||||
|
Username: req.Username,
|
||||||
|
Wechat: req.Wechat,
|
||||||
|
Notes: req.Notes,
|
||||||
Balance: req.Balance,
|
Balance: req.Balance,
|
||||||
Concurrency: req.Concurrency,
|
Concurrency: req.Concurrency,
|
||||||
Status: req.Status,
|
Status: req.Status,
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ type ChangePasswordRequest struct {
|
|||||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
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
|
// GetProfile handles getting user profile
|
||||||
// GET /api/v1/users/me
|
// GET /api/v1/users/me
|
||||||
func (h *UserHandler) GetProfile(c *gin.Context) {
|
func (h *UserHandler) GetProfile(c *gin.Context) {
|
||||||
@@ -47,6 +53,9 @@ func (h *UserHandler) GetProfile(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清空notes字段,普通用户不应看到备注
|
||||||
|
userData.Notes = ""
|
||||||
|
|
||||||
response.Success(c, userData)
|
response.Success(c, userData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,3 +92,40 @@ func (h *UserHandler) ChangePassword(c *gin.Context) {
|
|||||||
|
|
||||||
response.Success(c, gin.H{"message": "Password changed successfully"})
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import (
|
|||||||
type User struct {
|
type User struct {
|
||||||
ID int64 `gorm:"primaryKey" json:"id"`
|
ID int64 `gorm:"primaryKey" json:"id"`
|
||||||
Email string `gorm:"uniqueIndex;size:255;not null" json:"email"`
|
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:"-"`
|
PasswordHash string `gorm:"size:255;not null" json:"-"`
|
||||||
Role string `gorm:"size:20;default:user;not null" json:"role"` // admin/user
|
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"`
|
Balance float64 `gorm:"type:decimal(20,8);default:0;not null" json:"balance"`
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ func (r *UserRepository) ListWithFilters(ctx context.Context, params pagination.
|
|||||||
}
|
}
|
||||||
if search != "" {
|
if search != "" {
|
||||||
searchPattern := "%" + 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 {
|
if err := db.Count(&total).Error; err != nil {
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ func registerRoutes(r *gin.Engine, h *handler.Handlers, s *service.Services, rep
|
|||||||
{
|
{
|
||||||
user.GET("/profile", h.User.GetProfile)
|
user.GET("/profile", h.User.GetProfile)
|
||||||
user.PUT("/password", h.User.ChangePassword)
|
user.PUT("/password", h.User.ChangePassword)
|
||||||
|
user.PUT("", h.User.UpdateProfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// API Key管理
|
// API Key管理
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ type AdminService interface {
|
|||||||
type CreateUserInput struct {
|
type CreateUserInput struct {
|
||||||
Email string
|
Email string
|
||||||
Password string
|
Password string
|
||||||
|
Username string
|
||||||
|
Wechat string
|
||||||
|
Notes string
|
||||||
Balance float64
|
Balance float64
|
||||||
Concurrency int
|
Concurrency int
|
||||||
AllowedGroups []int64
|
AllowedGroups []int64
|
||||||
@@ -79,6 +82,9 @@ type CreateUserInput struct {
|
|||||||
type UpdateUserInput struct {
|
type UpdateUserInput struct {
|
||||||
Email string
|
Email string
|
||||||
Password string
|
Password string
|
||||||
|
Username *string
|
||||||
|
Wechat *string
|
||||||
|
Notes *string
|
||||||
Balance *float64 // 使用指针区分"未提供"和"设置为0"
|
Balance *float64 // 使用指针区分"未提供"和"设置为0"
|
||||||
Concurrency *int // 使用指针区分"未提供"和"设置为0"
|
Concurrency *int // 使用指针区分"未提供"和"设置为0"
|
||||||
Status string
|
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) {
|
func (s *adminServiceImpl) CreateUser(ctx context.Context, input *CreateUserInput) (*model.User, error) {
|
||||||
user := &model.User{
|
user := &model.User{
|
||||||
Email: input.Email,
|
Email: input.Email,
|
||||||
|
Username: input.Username,
|
||||||
|
Wechat: input.Wechat,
|
||||||
|
Notes: input.Notes,
|
||||||
Role: "user", // Always create as regular user, never admin
|
Role: "user", // Always create as regular user, never admin
|
||||||
Balance: input.Balance,
|
Balance: input.Balance,
|
||||||
Concurrency: input.Concurrency,
|
Concurrency: input.Concurrency,
|
||||||
@@ -274,6 +283,18 @@ func (s *adminServiceImpl) UpdateUser(ctx context.Context, id int64, input *Upda
|
|||||||
return nil, err
|
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
|
// Role is not allowed to be changed via API to prevent privilege escalation
|
||||||
if input.Status != "" {
|
if input.Status != "" {
|
||||||
user.Status = input.Status
|
user.Status = input.Status
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ var (
|
|||||||
// UpdateProfileRequest 更新用户资料请求
|
// UpdateProfileRequest 更新用户资料请求
|
||||||
type UpdateProfileRequest struct {
|
type UpdateProfileRequest struct {
|
||||||
Email *string `json:"email"`
|
Email *string `json:"email"`
|
||||||
|
Username *string `json:"username"`
|
||||||
|
Wechat *string `json:"wechat"`
|
||||||
Concurrency *int `json:"concurrency"`
|
Concurrency *int `json:"concurrency"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +79,14 @@ func (s *UserService) UpdateProfile(ctx context.Context, userID int64, req Updat
|
|||||||
user.Email = *req.Email
|
user.Email = *req.Email
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Username != nil {
|
||||||
|
user.Username = *req.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Wechat != nil {
|
||||||
|
user.Wechat = *req.Wechat
|
||||||
|
}
|
||||||
|
|
||||||
if req.Concurrency != nil {
|
if req.Concurrency != nil {
|
||||||
user.Concurrency = *req.Concurrency
|
user.Concurrency = *req.Concurrency
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,20 @@ import type { User, ChangePasswordRequest } from '@/types';
|
|||||||
* @returns User profile data
|
* @returns User profile data
|
||||||
*/
|
*/
|
||||||
export async function getProfile(): Promise<User> {
|
export async function getProfile(): Promise<User> {
|
||||||
const { data } = await apiClient.get<User>('/users/me');
|
const { data } = await apiClient.get<User>('/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<User> {
|
||||||
|
const { data } = await apiClient.put<User>('/user', profile);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,12 +42,13 @@ export async function changePassword(
|
|||||||
new_password: newPassword,
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const userAPI = {
|
export const userAPI = {
|
||||||
getProfile,
|
getProfile,
|
||||||
|
updateProfile,
|
||||||
changePassword,
|
changePassword,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -335,6 +335,15 @@ export default {
|
|||||||
memberSince: 'Member Since',
|
memberSince: 'Member Since',
|
||||||
administrator: 'Administrator',
|
administrator: 'Administrator',
|
||||||
user: 'User',
|
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',
|
changePassword: 'Change Password',
|
||||||
currentPassword: 'Current Password',
|
currentPassword: 'Current Password',
|
||||||
newPassword: 'New Password',
|
newPassword: 'New Password',
|
||||||
@@ -446,8 +455,28 @@ export default {
|
|||||||
admin: 'Admin',
|
admin: 'Admin',
|
||||||
user: 'User',
|
user: 'User',
|
||||||
disabled: 'Disabled',
|
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: {
|
columns: {
|
||||||
user: 'User',
|
user: 'User',
|
||||||
|
username: 'Username',
|
||||||
|
wechat: 'WeChat ID',
|
||||||
|
notes: 'Notes',
|
||||||
role: 'Role',
|
role: 'Role',
|
||||||
subscriptions: 'Subscriptions',
|
subscriptions: 'Subscriptions',
|
||||||
balance: 'Balance',
|
balance: 'Balance',
|
||||||
@@ -471,16 +500,6 @@ export default {
|
|||||||
none: 'None',
|
none: 'None',
|
||||||
noUsersYet: 'No users yet',
|
noUsersYet: 'No users yet',
|
||||||
createFirstUser: 'Create your first user to get started.',
|
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',
|
userCreated: 'User created successfully',
|
||||||
userUpdated: 'User updated successfully',
|
userUpdated: 'User updated successfully',
|
||||||
userDeleted: 'User deleted successfully',
|
userDeleted: 'User deleted successfully',
|
||||||
|
|||||||
@@ -335,6 +335,15 @@ export default {
|
|||||||
memberSince: '注册时间',
|
memberSince: '注册时间',
|
||||||
administrator: '管理员',
|
administrator: '管理员',
|
||||||
user: '用户',
|
user: '用户',
|
||||||
|
username: '用户名',
|
||||||
|
wechat: '微信号',
|
||||||
|
enterUsername: '输入用户名',
|
||||||
|
enterWechat: '输入微信号',
|
||||||
|
editProfile: '编辑个人资料',
|
||||||
|
updateProfile: '更新资料',
|
||||||
|
updating: '更新中...',
|
||||||
|
updateSuccess: '资料更新成功',
|
||||||
|
updateFailed: '资料更新失败',
|
||||||
changePassword: '修改密码',
|
changePassword: '修改密码',
|
||||||
currentPassword: '当前密码',
|
currentPassword: '当前密码',
|
||||||
newPassword: '新密码',
|
newPassword: '新密码',
|
||||||
@@ -460,12 +469,38 @@ export default {
|
|||||||
deleteUser: '删除用户',
|
deleteUser: '删除用户',
|
||||||
deleteConfirmMessage: "确定要删除用户 '{email}' 吗?此操作无法撤销。",
|
deleteConfirmMessage: "确定要删除用户 '{email}' 吗?此操作无法撤销。",
|
||||||
searchPlaceholder: '搜索用户...',
|
searchPlaceholder: '搜索用户...',
|
||||||
|
searchUsers: '搜索用户...',
|
||||||
roleFilter: '角色筛选',
|
roleFilter: '角色筛选',
|
||||||
allRoles: '全部角色',
|
allRoles: '全部角色',
|
||||||
|
allStatus: '全部状态',
|
||||||
statusFilter: '状态筛选',
|
statusFilter: '状态筛选',
|
||||||
allStatuses: '全部状态',
|
allStatuses: '全部状态',
|
||||||
|
admin: '管理员',
|
||||||
|
user: '用户',
|
||||||
|
disabled: '禁用',
|
||||||
|
email: '邮箱',
|
||||||
|
password: '密码',
|
||||||
|
username: '用户名',
|
||||||
|
wechat: '微信号',
|
||||||
|
notes: '备注',
|
||||||
|
enterEmail: '请输入邮箱',
|
||||||
|
enterPassword: '请输入密码',
|
||||||
|
enterUsername: '请输入用户名(选填)',
|
||||||
|
enterWechat: '请输入微信号(选填)',
|
||||||
|
enterNotes: '请输入备注(仅管理员可见)',
|
||||||
|
notesHint: '此备注仅对管理员可见',
|
||||||
|
enterNewPassword: '请输入新密码(选填)',
|
||||||
|
leaveEmptyToKeep: '留空则保持原密码不变',
|
||||||
|
generatePassword: '生成随机密码',
|
||||||
|
copyPassword: '复制密码',
|
||||||
|
creating: '创建中...',
|
||||||
|
updating: '更新中...',
|
||||||
columns: {
|
columns: {
|
||||||
|
user: '用户',
|
||||||
email: '邮箱',
|
email: '邮箱',
|
||||||
|
username: '用户名',
|
||||||
|
wechat: '微信号',
|
||||||
|
notes: '备注',
|
||||||
role: '角色',
|
role: '角色',
|
||||||
subscriptions: '订阅分组',
|
subscriptions: '订阅分组',
|
||||||
balance: '余额',
|
balance: '余额',
|
||||||
@@ -474,13 +509,33 @@ export default {
|
|||||||
status: '状态',
|
status: '状态',
|
||||||
created: '创建时间',
|
created: '创建时间',
|
||||||
actions: '操作',
|
actions: '操作',
|
||||||
user: '用户',
|
|
||||||
},
|
},
|
||||||
today: '今日',
|
today: '今日',
|
||||||
total: '累计',
|
total: '累计',
|
||||||
noSubscription: '暂无订阅',
|
noSubscription: '暂无订阅',
|
||||||
daysRemaining: '{days}天',
|
daysRemaining: '{days}天',
|
||||||
expired: '已过期',
|
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: {
|
roles: {
|
||||||
admin: '管理员',
|
admin: '管理员',
|
||||||
user: '用户',
|
user: '用户',
|
||||||
@@ -492,6 +547,13 @@ export default {
|
|||||||
form: {
|
form: {
|
||||||
emailLabel: '邮箱',
|
emailLabel: '邮箱',
|
||||||
emailPlaceholder: '请输入邮箱',
|
emailPlaceholder: '请输入邮箱',
|
||||||
|
usernameLabel: '用户名',
|
||||||
|
usernamePlaceholder: '请输入用户名(选填)',
|
||||||
|
wechatLabel: '微信号',
|
||||||
|
wechatPlaceholder: '请输入微信号(选填)',
|
||||||
|
notesLabel: '备注',
|
||||||
|
notesPlaceholder: '请输入备注(仅管理员可见)',
|
||||||
|
notesHint: '此备注仅对管理员可见',
|
||||||
passwordLabel: '密码',
|
passwordLabel: '密码',
|
||||||
passwordPlaceholder: '请输入密码(留空则不修改)',
|
passwordPlaceholder: '请输入密码(留空则不修改)',
|
||||||
roleLabel: '角色',
|
roleLabel: '角色',
|
||||||
@@ -515,9 +577,7 @@ export default {
|
|||||||
userDeletedSuccess: '用户删除成功',
|
userDeletedSuccess: '用户删除成功',
|
||||||
balanceAdjustedSuccess: '余额调整成功',
|
balanceAdjustedSuccess: '余额调整成功',
|
||||||
concurrencyAdjustedSuccess: '并发数调整成功',
|
concurrencyAdjustedSuccess: '并发数调整成功',
|
||||||
failedToLoad: '加载用户列表失败',
|
|
||||||
failedToSave: '保存用户失败',
|
failedToSave: '保存用户失败',
|
||||||
failedToDelete: '删除用户失败',
|
|
||||||
failedToAdjust: '调整失败',
|
failedToAdjust: '调整失败',
|
||||||
setAllowedGroups: '设置允许分组',
|
setAllowedGroups: '设置允许分组',
|
||||||
allowedGroupsHint: '选择此用户可以使用的标准分组。订阅类型分组请在订阅管理中配置。',
|
allowedGroupsHint: '选择此用户可以使用的标准分组。订阅类型分组请在订阅管理中配置。',
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
export interface User {
|
export interface User {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
|
wechat: string;
|
||||||
|
notes: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: 'admin' | 'user'; // User role for authorization
|
role: 'admin' | 'user'; // User role for authorization
|
||||||
balance: number; // User balance for API usage
|
balance: number; // User balance for API usage
|
||||||
@@ -563,6 +565,9 @@ export interface ApiKeyUsageTrendPoint {
|
|||||||
export interface UpdateUserRequest {
|
export interface UpdateUserRequest {
|
||||||
email?: string;
|
email?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
username?: string;
|
||||||
|
wechat?: string;
|
||||||
|
notes?: string;
|
||||||
role?: 'admin' | 'user';
|
role?: 'admin' | 'user';
|
||||||
balance?: number;
|
balance?: number;
|
||||||
concurrency?: number;
|
concurrency?: number;
|
||||||
|
|||||||
@@ -73,6 +73,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #cell-username="{ value }">
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300">{{ value || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell-wechat="{ value }">
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300">{{ value || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell-notes="{ value }">
|
||||||
|
<div class="max-w-xs">
|
||||||
|
<span
|
||||||
|
v-if="value"
|
||||||
|
:title="value.length > 30 ? value : undefined"
|
||||||
|
class="text-sm text-gray-600 dark:text-gray-400 block truncate"
|
||||||
|
>
|
||||||
|
{{ value.length > 30 ? value.substring(0, 25) + '...' : value }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-sm text-gray-400">-</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #cell-role="{ value }">
|
<template #cell-role="{ value }">
|
||||||
<span
|
<span
|
||||||
:class="[
|
:class="[
|
||||||
@@ -293,6 +314,34 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.users.username') }}</label>
|
||||||
|
<input
|
||||||
|
v-model="createForm.username"
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
:placeholder="t('admin.users.enterUsername')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.users.wechat') }}</label>
|
||||||
|
<input
|
||||||
|
v-model="createForm.wechat"
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
:placeholder="t('admin.users.enterWechat')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.users.notes') }}</label>
|
||||||
|
<textarea
|
||||||
|
v-model="createForm.notes"
|
||||||
|
rows="3"
|
||||||
|
class="input"
|
||||||
|
:placeholder="t('admin.users.enterNotes')"
|
||||||
|
></textarea>
|
||||||
|
<p class="input-hint">{{ t('admin.users.notesHint') }}</p>
|
||||||
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label">{{ t('admin.users.columns.balance') }}</label>
|
<label class="input-label">{{ t('admin.users.columns.balance') }}</label>
|
||||||
@@ -399,6 +448,34 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.users.username') }}</label>
|
||||||
|
<input
|
||||||
|
v-model="editForm.username"
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
:placeholder="t('admin.users.enterUsername')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.users.wechat') }}</label>
|
||||||
|
<input
|
||||||
|
v-model="editForm.wechat"
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
:placeholder="t('admin.users.enterWechat')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.users.notes') }}</label>
|
||||||
|
<textarea
|
||||||
|
v-model="editForm.notes"
|
||||||
|
rows="3"
|
||||||
|
class="input"
|
||||||
|
:placeholder="t('admin.users.enterNotes')"
|
||||||
|
></textarea>
|
||||||
|
<p class="input-hint">{{ t('admin.users.notesHint') }}</p>
|
||||||
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label">{{ t('admin.users.columns.balance') }}</label>
|
<label class="input-label">{{ t('admin.users.columns.balance') }}</label>
|
||||||
@@ -689,6 +766,9 @@ const appStore = useAppStore()
|
|||||||
|
|
||||||
const columns = computed<Column[]>(() => [
|
const columns = computed<Column[]>(() => [
|
||||||
{ key: 'email', label: t('admin.users.columns.user'), sortable: true },
|
{ key: 'email', label: t('admin.users.columns.user'), sortable: true },
|
||||||
|
{ key: 'username', label: t('admin.users.columns.username'), sortable: true },
|
||||||
|
{ key: 'wechat', label: t('admin.users.columns.wechat'), sortable: false },
|
||||||
|
{ key: 'notes', label: t('admin.users.columns.notes'), sortable: false },
|
||||||
{ key: 'role', label: t('admin.users.columns.role'), sortable: true },
|
{ key: 'role', label: t('admin.users.columns.role'), sortable: true },
|
||||||
{ key: 'subscriptions', label: t('admin.users.columns.subscriptions'), sortable: false },
|
{ key: 'subscriptions', label: t('admin.users.columns.subscriptions'), sortable: false },
|
||||||
{ key: 'balance', label: t('admin.users.columns.balance'), sortable: true },
|
{ key: 'balance', label: t('admin.users.columns.balance'), sortable: true },
|
||||||
@@ -751,6 +831,9 @@ const savingAllowedGroups = ref(false)
|
|||||||
const createForm = reactive({
|
const createForm = reactive({
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
|
username: '',
|
||||||
|
wechat: '',
|
||||||
|
notes: '',
|
||||||
balance: 0,
|
balance: 0,
|
||||||
concurrency: 1
|
concurrency: 1
|
||||||
})
|
})
|
||||||
@@ -758,6 +841,9 @@ const createForm = reactive({
|
|||||||
const editForm = reactive({
|
const editForm = reactive({
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
|
username: '',
|
||||||
|
wechat: '',
|
||||||
|
notes: '',
|
||||||
balance: 0,
|
balance: 0,
|
||||||
concurrency: 1
|
concurrency: 1
|
||||||
})
|
})
|
||||||
@@ -881,6 +967,9 @@ const closeCreateModal = () => {
|
|||||||
showCreateModal.value = false
|
showCreateModal.value = false
|
||||||
createForm.email = ''
|
createForm.email = ''
|
||||||
createForm.password = ''
|
createForm.password = ''
|
||||||
|
createForm.username = ''
|
||||||
|
createForm.wechat = ''
|
||||||
|
createForm.notes = ''
|
||||||
createForm.balance = 0
|
createForm.balance = 0
|
||||||
createForm.concurrency = 1
|
createForm.concurrency = 1
|
||||||
passwordCopied.value = false
|
passwordCopied.value = false
|
||||||
@@ -905,6 +994,9 @@ const handleEdit = (user: User) => {
|
|||||||
editingUser.value = user
|
editingUser.value = user
|
||||||
editForm.email = user.email
|
editForm.email = user.email
|
||||||
editForm.password = ''
|
editForm.password = ''
|
||||||
|
editForm.username = user.username || ''
|
||||||
|
editForm.wechat = user.wechat || ''
|
||||||
|
editForm.notes = user.notes || ''
|
||||||
editForm.balance = user.balance
|
editForm.balance = user.balance
|
||||||
editForm.concurrency = user.concurrency
|
editForm.concurrency = user.concurrency
|
||||||
editPasswordCopied.value = false
|
editPasswordCopied.value = false
|
||||||
@@ -926,6 +1018,9 @@ const handleUpdateUser = async () => {
|
|||||||
// Build update data - only include password if not empty
|
// Build update data - only include password if not empty
|
||||||
const updateData: Record<string, any> = {
|
const updateData: Record<string, any> = {
|
||||||
email: editForm.email,
|
email: editForm.email,
|
||||||
|
username: editForm.username,
|
||||||
|
wechat: editForm.wechat,
|
||||||
|
notes: editForm.notes,
|
||||||
balance: editForm.balance,
|
balance: editForm.balance,
|
||||||
concurrency: editForm.concurrency
|
concurrency: editForm.concurrency
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,11 +55,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-6 py-4">
|
<div class="px-6 py-4">
|
||||||
<div class="flex items-center gap-3 text-sm text-gray-600 dark:text-gray-400">
|
<div class="space-y-3">
|
||||||
<svg class="w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
<div class="flex items-center gap-3 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
|
<svg class="w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||||
</svg>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
|
||||||
<span class="truncate">{{ user?.email }}</span>
|
</svg>
|
||||||
|
<span class="truncate">{{ user?.email }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="user?.username" class="flex items-center gap-3 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<svg class="w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
|
||||||
|
</svg>
|
||||||
|
<span class="truncate">{{ user.username }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="user?.wechat" class="flex items-center gap-3 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<svg class="w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z" />
|
||||||
|
</svg>
|
||||||
|
<span class="truncate">{{ user.wechat }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,6 +95,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Profile Section -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-100 dark:border-dark-700">
|
||||||
|
<h2 class="text-lg font-medium text-gray-900 dark:text-white">{{ t('profile.editProfile') }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-6">
|
||||||
|
<form @submit.prevent="handleUpdateProfile" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="username" class="input-label">
|
||||||
|
{{ t('profile.username') }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
v-model="profileForm.username"
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
:placeholder="t('profile.enterUsername')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="wechat" class="input-label">
|
||||||
|
{{ t('profile.wechat') }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="wechat"
|
||||||
|
v-model="profileForm.wechat"
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
:placeholder="t('profile.enterWechat')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end pt-4">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="updatingProfile"
|
||||||
|
class="btn btn-primary"
|
||||||
|
>
|
||||||
|
{{ updatingProfile ? t('profile.updating') : t('profile.updateProfile') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Change Password Section -->
|
<!-- Change Password Section -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="px-6 py-4 border-b border-gray-100 dark:border-dark-700">
|
<div class="px-6 py-4 border-b border-gray-100 dark:border-dark-700">
|
||||||
@@ -191,13 +251,25 @@ const passwordForm = ref({
|
|||||||
confirm_password: ''
|
confirm_password: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const profileForm = ref({
|
||||||
|
username: '',
|
||||||
|
wechat: ''
|
||||||
|
})
|
||||||
|
|
||||||
const changingPassword = ref(false)
|
const changingPassword = ref(false)
|
||||||
|
const updatingProfile = ref(false)
|
||||||
const contactInfo = ref('')
|
const contactInfo = ref('')
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const settings = await authAPI.getPublicSettings()
|
const settings = await authAPI.getPublicSettings()
|
||||||
contactInfo.value = settings.contact_info || ''
|
contactInfo.value = settings.contact_info || ''
|
||||||
|
|
||||||
|
// Initialize profile form with current user data
|
||||||
|
if (user.value) {
|
||||||
|
profileForm.value.username = user.value.username || ''
|
||||||
|
profileForm.value.wechat = user.value.wechat || ''
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load contact info:', error)
|
console.error('Failed to load contact info:', error)
|
||||||
}
|
}
|
||||||
@@ -250,4 +322,23 @@ const handleChangePassword = async () => {
|
|||||||
changingPassword.value = false
|
changingPassword.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleUpdateProfile = async () => {
|
||||||
|
updatingProfile.value = true
|
||||||
|
try {
|
||||||
|
const updatedUser = await userAPI.updateProfile({
|
||||||
|
username: profileForm.value.username,
|
||||||
|
wechat: profileForm.value.wechat
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update auth store with new user data
|
||||||
|
authStore.user = updatedUser
|
||||||
|
|
||||||
|
appStore.showSuccess(t('profile.updateSuccess'))
|
||||||
|
} catch (error: any) {
|
||||||
|
appStore.showError(error.response?.data?.detail || t('profile.updateFailed'))
|
||||||
|
} finally {
|
||||||
|
updatingProfile.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user