diff --git a/docs/admin-openapi.yaml b/docs/admin-openapi.yaml index 1c0a7d3..9391f44 100644 --- a/docs/admin-openapi.yaml +++ b/docs/admin-openapi.yaml @@ -1356,6 +1356,19 @@ components: description: 跳过原因 type: string type: object + DtoBatchRemovePermissionsRequest: + properties: + perm_ids: + description: 权限ID列表 + items: + minimum: 0 + type: integer + minItems: 1 + nullable: true + type: array + required: + - perm_ids + type: object DtoBatchSetCardSeriesBindngRequest: properties: iccids: @@ -6641,6 +6654,9 @@ components: data_usage_mb: description: 累计流量使用(MB) type: integer + device_virtual_no: + description: 绑定设备的虚拟号(未绑定时为空字符串) + type: string enable_polling: description: 是否参与轮询 type: boolean @@ -14541,13 +14557,13 @@ paths: description: 卡接入号(模糊查询) maxLength: 20 type: string - - description: 虚拟号(模糊查询) + - description: 是否为独立卡(true:仅返回未绑定设备的卡, false:仅返回已绑定设备的卡, 不传:返回全部) in: query - name: virtual_no + name: is_standalone schema: - description: 虚拟号(模糊查询) - maxLength: 50 - type: string + description: 是否为独立卡(true:仅返回未绑定设备的卡, false:仅返回已绑定设备的卡, 不传:返回全部) + nullable: true + type: boolean - description: 批次号 in: query name: batch_no @@ -19500,6 +19516,52 @@ paths: summary: 更新角色状态 tags: - 角色 + /api/admin/roles/{role_id}/permissions: + delete: + parameters: + - description: 角色ID + in: path + name: role_id + required: true + schema: + description: 角色ID + minimum: 0 + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DtoBatchRemovePermissionsRequest' + responses: + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 请求参数错误 + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 未认证或认证已过期 + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 无权访问 + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 服务器内部错误 + security: + - BearerAuth: [] + summary: 批量移除权限 + tags: + - 角色 /api/admin/roles/{role_id}/permissions/{perm_id}: delete: parameters: diff --git a/internal/handler/admin/role.go b/internal/handler/admin/role.go index c9930b1..3be51b4 100644 --- a/internal/handler/admin/role.go +++ b/internal/handler/admin/role.go @@ -197,6 +197,35 @@ func (h *RoleHandler) RemovePermission(c *fiber.Ctx) error { return response.Success(c, nil) } +// BatchRemovePermissions 批量移除角色的权限 +// DELETE /api/admin/roles/:role_id/permissions +func (h *RoleHandler) BatchRemovePermissions(c *fiber.Ctx) error { + roleID, err := strconv.ParseUint(c.Params("role_id"), 10, 64) + if err != nil { + return errors.New(errors.CodeInvalidParam, "无效的角色 ID") + } + + var req dto.BatchRemovePermissionsRequest + if err := c.BodyParser(&req); err != nil { + return errors.New(errors.CodeInvalidParam, "请求参数解析失败") + } + + if err := h.validator.Struct(&req); err != nil { + logger.GetAppLogger().Warn("参数验证失败", + zap.String("path", c.Path()), + zap.String("method", c.Method()), + zap.Error(err), + ) + return errors.New(errors.CodeInvalidParam) + } + + if err := h.service.BatchRemovePermissions(c.UserContext(), uint(roleID), req.PermIDs); err != nil { + return err + } + + return response.Success(c, nil) +} + // UpdateStatus 更新角色状态 // PUT /api/admin/roles/:id/status func (h *RoleHandler) UpdateStatus(c *fiber.Ctx) error { diff --git a/internal/model/dto/role_dto.go b/internal/model/dto/role_dto.go index f536177..be3430e 100644 --- a/internal/model/dto/role_dto.go +++ b/internal/model/dto/role_dto.go @@ -67,6 +67,17 @@ type RemovePermissionParams struct { PermID uint `path:"perm_id" required:"true" description:"权限ID"` } +// BatchRemovePermissionsRequest 批量移除权限请求 +type BatchRemovePermissionsRequest struct { + PermIDs []uint `json:"perm_ids" validate:"required,min=1" required:"true" minItems:"1" description:"权限ID列表"` +} + +// BatchRemovePermissionsParams 批量移除权限参数聚合 +type BatchRemovePermissionsParams struct { + RoleID uint `path:"role_id" required:"true" description:"角色ID"` + BatchRemovePermissionsRequest +} + // UpdateRoleStatusRequest 更新角色状态请求 type UpdateRoleStatusRequest struct { Status *int `json:"status" validate:"required,min=0,max=1" description:"状态 (0:禁用, 1:启用)"` diff --git a/internal/routes/registry.go b/internal/routes/registry.go index 929ecbe..eeec90e 100644 --- a/internal/routes/registry.go +++ b/internal/routes/registry.go @@ -20,6 +20,7 @@ type RouteSpec struct { Summary string // 简短摘要(中文,一行) Description string // 详细说明,支持 Markdown 语法(可选) Input interface{} // 请求参数结构体 (Query/Path/Body) + Body interface{} // 显式 JSON 请求体(用于 DELETE 等方法,库不自动生成 requestBody 的场景) Output interface{} // 响应参数结构体 Tags []string // 分类标签 Auth bool // 是否需要认证图标 (预留) @@ -54,7 +55,7 @@ func Register(router fiber.Router, doc *openapi.Generator, basePath, method, pat } doc.AddMultipartOperation(method, openapiPath, spec.Summary, spec.Description, spec.Input, spec.Output, spec.Auth, fileFields, spec.Tags...) } else { - doc.AddOperation(method, openapiPath, spec.Summary, spec.Description, spec.Input, spec.Output, spec.Auth, spec.Tags...) + doc.AddOperation(method, openapiPath, spec.Summary, spec.Description, spec.Input, spec.Body, spec.Output, spec.Auth, spec.Tags...) } } } diff --git a/internal/routes/role.go b/internal/routes/role.go index 68ad06c..c7f339f 100644 --- a/internal/routes/role.go +++ b/internal/routes/role.go @@ -87,4 +87,13 @@ func registerRoleRoutes(api fiber.Router, h *admin.RoleHandler, doc *openapi.Gen Output: nil, Auth: true, }) + + Register(roles, doc, groupPath, "DELETE", "/:role_id/permissions", h.BatchRemovePermissions, RouteSpec{ + Summary: "批量移除权限", + Tags: []string{"角色"}, + Input: new(dto.BatchRemovePermissionsParams), + Body: new(dto.BatchRemovePermissionsRequest), + Output: nil, + Auth: true, + }) } diff --git a/internal/service/role/service.go b/internal/service/role/service.go index 6573fd4..1481ae8 100644 --- a/internal/service/role/service.go +++ b/internal/service/role/service.go @@ -286,6 +286,23 @@ func (s *Service) RemovePermission(ctx context.Context, roleID, permID uint) err return nil } +// BatchRemovePermissions 批量移除角色的权限 +func (s *Service) BatchRemovePermissions(ctx context.Context, roleID uint, permIDs []uint) error { + _, err := s.roleStore.GetByID(ctx, roleID) + if err != nil { + if err == gorm.ErrRecordNotFound { + return errors.New(errors.CodeRoleNotFound, "角色不存在") + } + return errors.Wrap(errors.CodeInternalError, err, "获取角色失败") + } + + if err := s.rolePermissionStore.BatchDelete(ctx, roleID, permIDs); err != nil { + return errors.Wrap(errors.CodeInternalError, err, "批量删除角色-权限关联失败") + } + + return nil +} + // UpdateStatus 更新角色状态 func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error { currentUserID := middleware.GetUserIDFromContext(ctx) diff --git a/internal/store/postgres/role_permission_store.go b/internal/store/postgres/role_permission_store.go index e067b56..20e6eaf 100644 --- a/internal/store/postgres/role_permission_store.go +++ b/internal/store/postgres/role_permission_store.go @@ -58,6 +58,17 @@ func (s *RolePermissionStore) Delete(ctx context.Context, roleID, permID uint) e return nil } +// BatchDelete 批量软删除角色-权限关联 +func (s *RolePermissionStore) BatchDelete(ctx context.Context, roleID uint, permIDs []uint) error { + if err := s.db.WithContext(ctx). + Where("role_id = ? AND perm_id IN ?", roleID, permIDs). + Delete(&model.RolePermission{}).Error; err != nil { + return err + } + s.clearRoleUsersCaches(ctx, roleID) + return nil +} + // DeleteByRoleID 删除角色的所有权限关联 func (s *RolePermissionStore) DeleteByRoleID(ctx context.Context, roleID uint) error { if err := s.db.WithContext(ctx). diff --git a/pkg/openapi/generator.go b/pkg/openapi/generator.go index e04de5f..b787e26 100644 --- a/pkg/openapi/generator.go +++ b/pkg/openapi/generator.go @@ -120,7 +120,7 @@ type FileUploadField struct { // - output: 响应结构体(可为 nil) // - tags: 标签列表 // - requiresAuth: 是否需要认证 -func (g *Generator) AddOperation(method, path, summary, description string, input interface{}, output interface{}, requiresAuth bool, tags ...string) { +func (g *Generator) AddOperation(method, path, summary, description string, input interface{}, body interface{}, output interface{}, requiresAuth bool, tags ...string) { op := openapi3.Operation{ Summary: &summary, Tags: tags, @@ -138,6 +138,18 @@ func (g *Generator) AddOperation(method, path, summary, description string, inpu } } + + // u663eu5f0fu4f20u5165u7684 JSON bodyuff0cu7528u4e8e DELETE u7b49u65b9u6cd5u4e0du81eau52a8u751fu6210 requestBody u7684u573au666f + if body != nil { + tempOp := openapi3.Operation{} + if err := g.Reflector.SetRequest(&tempOp, body, "POST"); err != nil { + panic(err) + } + if tempOp.RequestBody != nil { + op.RequestBody = tempOp.RequestBody + } + } + // 反射输出 (响应 Body) if output != nil { // 将输出包裹在 envelope 中