feat(account): 实现平台账号管理功能
- 新增平台账号列表查询接口(自动筛选超级管理员和平台用户) - 新增密码修改和状态切换专用接口 - 增强角色分配功能,支持空数组清空所有角色 - 新增超级管理员保护机制,禁止分配角色 - 新增完整的集成测试和OpenSpec规范文档
This commit is contained in:
@@ -164,3 +164,59 @@ func (h *AccountHandler) RemoveRole(c *fiber.Ctx) error {
|
||||
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
|
||||
// UpdatePassword 修改账号密码
|
||||
// PUT /api/admin/platform-accounts/:id/password
|
||||
func (h *AccountHandler) UpdatePassword(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的账号 ID")
|
||||
}
|
||||
|
||||
var req model.UpdatePasswordRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
if err := h.service.UpdatePassword(c.UserContext(), uint(id), req.NewPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
|
||||
// UpdateStatus 修改账号状态
|
||||
// PUT /api/admin/platform-accounts/:id/status
|
||||
func (h *AccountHandler) UpdateStatus(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的账号 ID")
|
||||
}
|
||||
|
||||
var req model.UpdateStatusRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
if err := h.service.UpdateStatus(c.UserContext(), uint(id), req.Status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
|
||||
// ListPlatformAccounts 查询平台账号列表
|
||||
// GET /api/admin/platform-accounts
|
||||
func (h *AccountHandler) ListPlatformAccounts(c *fiber.Ctx) error {
|
||||
var req model.PlatformAccountListRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
accounts, total, err := h.service.ListPlatformAccounts(c.UserContext(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.SuccessWithPagination(c, accounts, total, req.Page, req.PageSize)
|
||||
}
|
||||
|
||||
@@ -44,8 +44,9 @@ type AccountResponse struct {
|
||||
}
|
||||
|
||||
// AssignRolesRequest 分配角色请求
|
||||
// 支持传递空数组以清空账号的所有角色
|
||||
type AssignRolesRequest struct {
|
||||
RoleIDs []uint `json:"role_ids" validate:"required,min=1" required:"true" minItems:"1" description:"角色ID列表"`
|
||||
RoleIDs []uint `json:"role_ids" validate:"omitempty" description:"角色ID列表,传空数组可清空所有角色"`
|
||||
}
|
||||
|
||||
// AccountPageResult 账号分页响应
|
||||
@@ -55,3 +56,39 @@ type AccountPageResult struct {
|
||||
Page int `json:"page" description:"当前页码"`
|
||||
Size int `json:"size" description:"每页数量"`
|
||||
}
|
||||
|
||||
// ========== 平台账号管理专用 DTO ==========
|
||||
|
||||
// UpdatePasswordRequest 修改密码请求
|
||||
// 用于管理员重置密码场景,无需验证旧密码
|
||||
type UpdatePasswordRequest struct {
|
||||
NewPassword string `json:"new_password" validate:"required,min=8,max=32" required:"true" minLength:"8" maxLength:"32" description:"新密码(8-32位)"`
|
||||
}
|
||||
|
||||
// UpdateStatusRequest 状态切换请求
|
||||
// 用于启用/禁用账号
|
||||
type UpdateStatusRequest struct {
|
||||
Status int `json:"status" validate:"required,min=0,max=1" required:"true" minimum:"0" maximum:"1" description:"状态(0:禁用,1:启用)"`
|
||||
}
|
||||
|
||||
// PlatformAccountListRequest 平台账号列表查询请求
|
||||
// 自动筛选 user_type IN (1, 2) 的账号
|
||||
type PlatformAccountListRequest struct {
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
|
||||
Username string `json:"username" query:"username" validate:"omitempty,max=50" maxLength:"50" description:"用户名模糊查询"`
|
||||
Phone string `json:"phone" query:"phone" validate:"omitempty,max=20" maxLength:"20" description:"手机号模糊查询"`
|
||||
Status *int `json:"status" query:"status" validate:"omitempty,min=0,max=1" minimum:"0" maximum:"1" description:"状态"`
|
||||
}
|
||||
|
||||
// UpdatePasswordParams 修改密码参数(用于 OpenAPI 生成)
|
||||
type UpdatePasswordParams struct {
|
||||
ID uint `params:"id" description:"账号ID"`
|
||||
RequestBody UpdatePasswordRequest `json:"body" description:"请求体"`
|
||||
}
|
||||
|
||||
// UpdateStatusParams 状态切换参数(用于 OpenAPI 生成)
|
||||
type UpdateStatusParams struct {
|
||||
ID uint `params:"id" description:"账号ID"`
|
||||
RequestBody UpdateStatusRequest `json:"body" description:"请求体"`
|
||||
}
|
||||
|
||||
@@ -70,4 +70,81 @@ func registerAccountRoutes(api fiber.Router, h *admin.AccountHandler, doc *opena
|
||||
Input: new(model.RemoveRoleParams),
|
||||
Output: nil,
|
||||
})
|
||||
|
||||
registerPlatformAccountRoutes(api, h, doc, basePath)
|
||||
}
|
||||
|
||||
func registerPlatformAccountRoutes(api fiber.Router, h *admin.AccountHandler, doc *openapi.Generator, basePath string) {
|
||||
platformAccounts := api.Group("/platform-accounts")
|
||||
groupPath := basePath + "/platform-accounts"
|
||||
|
||||
Register(platformAccounts, doc, groupPath, "GET", "", h.ListPlatformAccounts, RouteSpec{
|
||||
Summary: "平台账号列表",
|
||||
Tags: []string{"PlatformAccount"},
|
||||
Input: new(model.PlatformAccountListRequest),
|
||||
Output: new(model.AccountPageResult),
|
||||
})
|
||||
|
||||
Register(platformAccounts, doc, groupPath, "POST", "", h.Create, RouteSpec{
|
||||
Summary: "新增平台账号",
|
||||
Tags: []string{"PlatformAccount"},
|
||||
Input: new(model.CreateAccountRequest),
|
||||
Output: new(model.AccountResponse),
|
||||
})
|
||||
|
||||
Register(platformAccounts, doc, groupPath, "GET", "/:id", h.Get, RouteSpec{
|
||||
Summary: "获取平台账号详情",
|
||||
Tags: []string{"PlatformAccount"},
|
||||
Input: new(model.IDReq),
|
||||
Output: new(model.AccountResponse),
|
||||
})
|
||||
|
||||
Register(platformAccounts, doc, groupPath, "PUT", "/:id", h.Update, RouteSpec{
|
||||
Summary: "编辑平台账号",
|
||||
Tags: []string{"PlatformAccount"},
|
||||
Input: new(model.UpdateAccountParams),
|
||||
Output: new(model.AccountResponse),
|
||||
})
|
||||
|
||||
Register(platformAccounts, doc, groupPath, "DELETE", "/:id", h.Delete, RouteSpec{
|
||||
Summary: "删除平台账号",
|
||||
Tags: []string{"PlatformAccount"},
|
||||
Input: new(model.IDReq),
|
||||
Output: nil,
|
||||
})
|
||||
|
||||
Register(platformAccounts, doc, groupPath, "PUT", "/:id/password", h.UpdatePassword, RouteSpec{
|
||||
Summary: "修改密码",
|
||||
Tags: []string{"PlatformAccount"},
|
||||
Input: new(model.UpdatePasswordParams),
|
||||
Output: nil,
|
||||
})
|
||||
|
||||
Register(platformAccounts, doc, groupPath, "PUT", "/:id/status", h.UpdateStatus, RouteSpec{
|
||||
Summary: "启用/禁用账号",
|
||||
Tags: []string{"PlatformAccount"},
|
||||
Input: new(model.UpdateStatusParams),
|
||||
Output: nil,
|
||||
})
|
||||
|
||||
Register(platformAccounts, doc, groupPath, "POST", "/:id/roles", h.AssignRoles, RouteSpec{
|
||||
Summary: "分配角色",
|
||||
Tags: []string{"PlatformAccount"},
|
||||
Input: new(model.AssignRolesParams),
|
||||
Output: nil,
|
||||
})
|
||||
|
||||
Register(platformAccounts, doc, groupPath, "GET", "/:id/roles", h.GetRoles, RouteSpec{
|
||||
Summary: "获取账号角色",
|
||||
Tags: []string{"PlatformAccount"},
|
||||
Input: new(model.IDReq),
|
||||
Output: new([]model.Role),
|
||||
})
|
||||
|
||||
Register(platformAccounts, doc, groupPath, "DELETE", "/:id/roles/:role_id", h.RemoveRole, RouteSpec{
|
||||
Summary: "移除角色",
|
||||
Tags: []string{"PlatformAccount"},
|
||||
Input: new(model.RemoveRoleParams),
|
||||
Output: nil,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -210,15 +210,13 @@ func (s *Service) List(ctx context.Context, req *model.AccountListRequest) ([]*m
|
||||
return s.accountStore.List(ctx, opts, filters)
|
||||
}
|
||||
|
||||
// AssignRoles 为账号分配角色
|
||||
// AssignRoles 为账号分配角色(支持空数组清空所有角色,超级管理员禁止分配)
|
||||
func (s *Service) AssignRoles(ctx context.Context, accountID uint, roleIDs []uint) ([]*model.AccountRole, error) {
|
||||
// 获取当前用户 ID
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
// 检查账号存在
|
||||
account, err := s.accountStore.GetByID(ctx, accountID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
@@ -227,19 +225,29 @@ func (s *Service) AssignRoles(ctx context.Context, accountID uint, roleIDs []uin
|
||||
return nil, fmt.Errorf("获取账号失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查用户类型是否允许分配角色
|
||||
// 超级管理员禁止分配角色
|
||||
if account.UserType == constants.UserTypeSuperAdmin {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "超级管理员不允许分配角色")
|
||||
}
|
||||
|
||||
// 空数组:清空所有角色
|
||||
if len(roleIDs) == 0 {
|
||||
if err := s.accountRoleStore.DeleteByAccountID(ctx, accountID); err != nil {
|
||||
return nil, fmt.Errorf("清空账号角色失败: %w", err)
|
||||
}
|
||||
return []*model.AccountRole{}, nil
|
||||
}
|
||||
|
||||
maxRoles := constants.GetMaxRolesForUserType(account.UserType)
|
||||
if maxRoles == 0 {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "该用户类型不需要分配角色")
|
||||
}
|
||||
|
||||
// 检查角色数量限制
|
||||
existingCount, err := s.accountRoleStore.CountByAccountID(ctx, accountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("统计现有角色数量失败: %w", err)
|
||||
}
|
||||
|
||||
// 计算将要分配的新角色数量(排除已存在的)
|
||||
newRoleCount := 0
|
||||
for _, roleID := range roleIDs {
|
||||
exists, _ := s.accountRoleStore.Exists(ctx, accountID, roleID)
|
||||
@@ -248,12 +256,10 @@ func (s *Service) AssignRoles(ctx context.Context, accountID uint, roleIDs []uin
|
||||
}
|
||||
}
|
||||
|
||||
// 检查角色数量限制(-1 表示无限制)
|
||||
if maxRoles != -1 && int(existingCount)+newRoleCount > maxRoles {
|
||||
return nil, errors.New(errors.CodeInvalidParam, fmt.Sprintf("该用户类型最多只能分配 %d 个角色", maxRoles))
|
||||
}
|
||||
|
||||
// 验证所有角色存在并检查角色类型是否匹配
|
||||
for _, roleID := range roleIDs {
|
||||
role, err := s.roleStore.GetByID(ctx, roleID)
|
||||
if err != nil {
|
||||
@@ -263,19 +269,16 @@ func (s *Service) AssignRoles(ctx context.Context, accountID uint, roleIDs []uin
|
||||
return nil, fmt.Errorf("获取角色失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查角色类型与用户类型是否匹配
|
||||
if !constants.IsRoleTypeMatchUserType(role.RoleType, account.UserType) {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "角色类型与账号类型不匹配")
|
||||
}
|
||||
}
|
||||
|
||||
// 创建关联
|
||||
var ars []*model.AccountRole
|
||||
for _, roleID := range roleIDs {
|
||||
// 检查是否已分配
|
||||
exists, _ := s.accountRoleStore.Exists(ctx, accountID, roleID)
|
||||
if exists {
|
||||
continue // 跳过已存在的关联
|
||||
continue
|
||||
}
|
||||
|
||||
ar := &model.AccountRole{
|
||||
@@ -344,6 +347,83 @@ func (s *Service) ValidatePassword(plainPassword, hashedPassword string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// UpdatePassword 修改账号密码(管理员重置场景,无需旧密码)
|
||||
func (s *Service) UpdatePassword(ctx context.Context, accountID uint, newPassword string) error {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
_, err := s.accountStore.GetByID(ctx, accountID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeAccountNotFound, "账号不存在")
|
||||
}
|
||||
return fmt.Errorf("获取账号失败: %w", err)
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("密码哈希失败: %w", err)
|
||||
}
|
||||
|
||||
if err := s.accountStore.UpdatePassword(ctx, accountID, string(hashedPassword), currentUserID); err != nil {
|
||||
return fmt.Errorf("更新密码失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateStatus 修改账号状态(启用/禁用)
|
||||
func (s *Service) UpdateStatus(ctx context.Context, accountID uint, status int) error {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
_, err := s.accountStore.GetByID(ctx, accountID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeAccountNotFound, "账号不存在")
|
||||
}
|
||||
return fmt.Errorf("获取账号失败: %w", err)
|
||||
}
|
||||
|
||||
if err := s.accountStore.UpdateStatus(ctx, accountID, status, currentUserID); err != nil {
|
||||
return fmt.Errorf("更新状态失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListPlatformAccounts 查询平台账号列表(自动筛选 user_type IN (1, 2))
|
||||
func (s *Service) ListPlatformAccounts(ctx context.Context, req *model.PlatformAccountListRequest) ([]*model.Account, int64, error) {
|
||||
opts := &store.QueryOptions{
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
OrderBy: "id DESC",
|
||||
}
|
||||
if opts.Page == 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
if opts.PageSize == 0 {
|
||||
opts.PageSize = constants.DefaultPageSize
|
||||
}
|
||||
|
||||
filters := make(map[string]interface{})
|
||||
if req.Username != "" {
|
||||
filters["username"] = req.Username
|
||||
}
|
||||
if req.Phone != "" {
|
||||
filters["phone"] = req.Phone
|
||||
}
|
||||
if req.Status != nil {
|
||||
filters["status"] = *req.Status
|
||||
}
|
||||
|
||||
return s.accountStore.ListPlatformAccounts(ctx, opts, filters)
|
||||
}
|
||||
|
||||
// CreateSystemAccount 系统内部创建账号方法,用于系统初始化场景(绕过当前用户检查)
|
||||
func (s *Service) CreateSystemAccount(ctx context.Context, account *model.Account) error {
|
||||
if account.Username == "" {
|
||||
|
||||
@@ -129,3 +129,71 @@ func (s *AccountStore) List(ctx context.Context, opts *store.QueryOptions, filte
|
||||
|
||||
return accounts, total, nil
|
||||
}
|
||||
|
||||
// ListPlatformAccounts 查询平台账号列表(自动筛选 user_type IN (1, 2))
|
||||
func (s *AccountStore) ListPlatformAccounts(ctx context.Context, opts *store.QueryOptions, filters map[string]interface{}) ([]*model.Account, int64, error) {
|
||||
var accounts []*model.Account
|
||||
var total int64
|
||||
|
||||
query := s.db.WithContext(ctx).Model(&model.Account{})
|
||||
|
||||
// 固定筛选平台账号:超级管理员(1) 和 平台用户(2)
|
||||
query = query.Where("user_type IN ?", []int{1, 2})
|
||||
|
||||
// 应用过滤条件
|
||||
if username, ok := filters["username"].(string); ok && username != "" {
|
||||
query = query.Where("username LIKE ?", "%"+username+"%")
|
||||
}
|
||||
if phone, ok := filters["phone"].(string); ok && phone != "" {
|
||||
query = query.Where("phone LIKE ?", "%"+phone+"%")
|
||||
}
|
||||
if status, ok := filters["status"].(int); ok {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
// 计算总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页
|
||||
if opts == nil {
|
||||
opts = store.DefaultQueryOptions()
|
||||
}
|
||||
offset := (opts.Page - 1) * opts.PageSize
|
||||
query = query.Offset(offset).Limit(opts.PageSize)
|
||||
|
||||
// 排序
|
||||
if opts.OrderBy != "" {
|
||||
query = query.Order(opts.OrderBy)
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
if err := query.Find(&accounts).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return accounts, total, nil
|
||||
}
|
||||
|
||||
// UpdatePassword 更新账号密码
|
||||
func (s *AccountStore) UpdatePassword(ctx context.Context, id uint, hashedPassword string, updater uint) error {
|
||||
return s.db.WithContext(ctx).
|
||||
Model(&model.Account{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"password": hashedPassword,
|
||||
"updater": updater,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// UpdateStatus 更新账号状态
|
||||
func (s *AccountStore) UpdateStatus(ctx context.Context, id uint, status int, updater uint) error {
|
||||
return s.db.WithContext(ctx).
|
||||
Model(&model.Account{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"updater": updater,
|
||||
}).Error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user