Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
e53b34f3
"frontend/src/api/vscode:/vscode.git/clone" did not exist on "ebac0dc628ef285623414c0ae579b8975f56e191"
Commit
e53b34f3
authored
Dec 23, 2025
by
shaw
Browse files
Merge PR #15: feat: 增强用户管理功能,添加用户名、微信号和备注字段
parents
12ddae01
0b8e84f9
Changes
13
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/user_handler.go
View file @
e53b34f3
...
@@ -25,6 +25,9 @@ func NewUserHandler(adminService service.AdminService) *UserHandler {
...
@@ -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 {
...
@@ -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) {
...
@@ -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) {
...
@@ -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
,
...
...
backend/internal/handler/user_handler.go
View file @
e53b34f3
...
@@ -26,6 +26,12 @@ type ChangePasswordRequest struct {
...
@@ -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) {
...
@@ -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) {
...
@@ -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
)
}
backend/internal/model/user.go
View file @
e53b34f3
...
@@ -11,6 +11,9 @@ import (
...
@@ -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"`
...
...
backend/internal/repository/user_repo.go
View file @
e53b34f3
...
@@ -66,7 +66,10 @@ func (r *UserRepository) ListWithFilters(ctx context.Context, params pagination.
...
@@ -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
{
...
...
backend/internal/server/router.go
View file @
e53b34f3
...
@@ -82,6 +82,7 @@ func registerRoutes(r *gin.Engine, h *handler.Handlers, s *service.Services, rep
...
@@ -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管理
...
...
backend/internal/service/admin_service.go
View file @
e53b34f3
...
@@ -71,6 +71,9 @@ type AdminService interface {
...
@@ -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 {
...
@@ -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,
...
@@ -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
...
@@ -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
...
...
backend/internal/service/user_service.go
View file @
e53b34f3
...
@@ -21,6 +21,8 @@ var (
...
@@ -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
...
@@ -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
}
}
...
...
frontend/src/api/user.ts
View file @
e53b34f3
...
@@ -11,7 +11,20 @@ import type { User, ChangePasswordRequest } from '@/types';
...
@@ -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(
...
@@ -29,12 +42,13 @@ export async function changePassword(
new_password
:
newPassword
,
new_password
:
newPassword
,
};
};
const
{
data
}
=
await
apiClient
.
p
os
t
<
{
message
:
string
}
>
(
'
/user
s/me
/password
'
,
payload
);
const
{
data
}
=
await
apiClient
.
p
u
t
<
{
message
:
string
}
>
(
'
/user/password
'
,
payload
);
return
data
;
return
data
;
}
}
export
const
userAPI
=
{
export
const
userAPI
=
{
getProfile
,
getProfile
,
updateProfile
,
changePassword
,
changePassword
,
};
};
...
...
frontend/src/i18n/locales/en.ts
View file @
e53b34f3
...
@@ -335,6 +335,15 @@ export default {
...
@@ -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 {
...
@@ -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 {
...
@@ -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
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
e53b34f3
...
@@ -335,6 +335,15 @@ export default {
...
@@ -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 {
...
@@ -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 {
...
@@ -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 {
...
@@ -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 {
...
@@ -515,9 +577,7 @@ export default {
userDeletedSuccess
:
'
用户删除成功
'
,
userDeletedSuccess
:
'
用户删除成功
'
,
balanceAdjustedSuccess
:
'
余额调整成功
'
,
balanceAdjustedSuccess
:
'
余额调整成功
'
,
concurrencyAdjustedSuccess
:
'
并发数调整成功
'
,
concurrencyAdjustedSuccess
:
'
并发数调整成功
'
,
failedToLoad
:
'
加载用户列表失败
'
,
failedToSave
:
'
保存用户失败
'
,
failedToSave
:
'
保存用户失败
'
,
failedToDelete
:
'
删除用户失败
'
,
failedToAdjust
:
'
调整失败
'
,
failedToAdjust
:
'
调整失败
'
,
setAllowedGroups
:
'
设置允许分组
'
,
setAllowedGroups
:
'
设置允许分组
'
,
allowedGroupsHint
:
'
选择此用户可以使用的标准分组。订阅类型分组请在订阅管理中配置。
'
,
allowedGroupsHint
:
'
选择此用户可以使用的标准分组。订阅类型分组请在订阅管理中配置。
'
,
...
...
frontend/src/types/index.ts
View file @
e53b34f3
...
@@ -7,6 +7,8 @@
...
@@ -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 {
...
@@ -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
;
...
...
frontend/src/views/admin/UsersView.vue
View file @
e53b34f3
...
@@ -73,6 +73,27 @@
...
@@ -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 @@
...
@@ -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 @@
...
@@ -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()
...
@@ -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)
...
@@ -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({
...
@@ -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 = () => {
...
@@ -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) => {
...
@@ -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 () => {
...
@@ -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
}
}
...
...
frontend/src/views/user/ProfileView.vue
View file @
e53b34f3
...
@@ -55,11 +55,25 @@
...
@@ -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 @@
...
@@ -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({
...
@@ -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 () => {
...
@@ -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
>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment