Files
junhong_cmp_fiber/internal/service/permission/service.go
huang 409a68d60b
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m45s
feat: OpenAPI 契约对齐与框架优化
主要变更:
1. OpenAPI 文档契约对齐
   - 统一错误响应字段名为 msg(非 message)
   - 规范 envelope 响应结构(code, msg, data, timestamp)
   - 个人客户路由纳入文档体系(使用 Register 机制)
   - 新增 BuildDocHandlers() 统一管理 handler 构造
   - 确保文档生成的幂等性

2. Service 层错误处理统一
   - 全面替换 fmt.Errorf 为 errors.New/Wrap
   - 统一错误码使用规范
   - Handler 层参数校验不泄露底层细节
   - 新增错误码验证集成测试

3. 代码质量提升
   - 删除未使用的 Task handler 和路由
   - 新增代码规范检查脚本(check-service-errors.sh)
   - 新增注释路径一致性检查(check-comment-paths.sh)
   - 更新 API 文档生成指南

4. OpenSpec 归档
   - 归档 openapi-contract-alignment 变更(63 tasks)
   - 归档 service-error-unify-core 变更
   - 归档 service-error-unify-support 变更
   - 归档 code-cleanup-docs-update 变更
   - 归档 handler-validation-security 变更
   - 同步 delta specs 到主规范文件

影响范围:
- pkg/openapi: 新增 handlers.go,优化 generator.go
- internal/service/*: 48 个 service 文件错误处理统一
- internal/handler/admin: 优化参数校验错误提示
- internal/routes: 个人客户路由改造,删除 task 路由
- scripts: 新增 3 个代码检查脚本
- docs: 更新 OpenAPI 文档(15750+ 行)
- openspec/specs: 同步 3 个主规范文件

破坏性变更:无
向后兼容:是
2026-01-30 11:40:36 +08:00

346 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package permission 提供权限管理的业务逻辑服务
// 包含权限创建、查询、更新、删除、权限检查等功能
package permission
import (
"context"
"encoding/json"
"regexp"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/internal/store"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
// permCodeRegex 权限编码格式验证正则module:action
var permCodeRegex = regexp.MustCompile(`^[a-z][a-z0-9_]*:[a-z][a-z0-9_]*$`)
// Service 权限业务服务
type Service struct {
permissionStore *postgres.PermissionStore
accountRoleStore *postgres.AccountRoleStore
rolePermStore *postgres.RolePermissionStore
redisClient *redis.Client
}
// New 创建权限服务
func New(
permissionStore *postgres.PermissionStore,
accountRoleStore *postgres.AccountRoleStore,
rolePermStore *postgres.RolePermissionStore,
redisClient *redis.Client,
) *Service {
return &Service{
permissionStore: permissionStore,
accountRoleStore: accountRoleStore,
rolePermStore: rolePermStore,
redisClient: redisClient,
}
}
// Create 创建权限
func (s *Service) Create(ctx context.Context, req *dto.CreatePermissionRequest) (*model.Permission, error) {
// 获取当前用户 ID
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
// 验证权限编码格式
if !permCodeRegex.MatchString(req.PermCode) {
return nil, errors.New(errors.CodeInvalidPermCode, "权限编码格式不正确(应为 module:action 格式)")
}
// 检查权限编码唯一性
existing, err := s.permissionStore.GetByCode(ctx, req.PermCode)
if err == nil && existing != nil {
return nil, errors.New(errors.CodePermCodeExists, "权限编码已存在")
}
// 验证 parent_id 存在(如果提供)
if req.ParentID != nil {
parent, err := s.permissionStore.GetByID(ctx, *req.ParentID)
if err != nil || parent == nil {
return nil, errors.New(errors.CodeNotFound, "上级权限不存在")
}
}
// 创建权限
permission := &model.Permission{
PermName: req.PermName,
PermCode: req.PermCode,
PermType: req.PermType,
Platform: req.Platform,
URL: req.URL,
ParentID: req.ParentID,
Sort: req.Sort,
Status: constants.StatusEnabled,
}
// 如果未指定 platform默认为 all
if permission.Platform == "" {
permission.Platform = constants.PlatformAll
}
if err := s.permissionStore.Create(ctx, permission); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "创建权限失败")
}
return permission, nil
}
// Get 获取权限
func (s *Service) Get(ctx context.Context, id uint) (*model.Permission, error) {
permission, err := s.permissionStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodePermissionNotFound, "权限不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "获取权限失败")
}
return permission, nil
}
// Update 更新权限
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePermissionRequest) (*model.Permission, error) {
// 获取当前用户 ID
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
// 获取现有权限
permission, err := s.permissionStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodePermissionNotFound, "权限不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "获取权限失败")
}
// 更新字段
if req.PermName != nil {
permission.PermName = *req.PermName
}
if req.PermCode != nil {
// 验证权限编码格式
if !permCodeRegex.MatchString(*req.PermCode) {
return nil, errors.New(errors.CodeInvalidPermCode, "权限编码格式不正确(应为 module:action 格式)")
}
// 检查新权限编码唯一性
existing, err := s.permissionStore.GetByCode(ctx, *req.PermCode)
if err == nil && existing != nil && existing.ID != id {
return nil, errors.New(errors.CodePermCodeExists, "权限编码已存在")
}
permission.PermCode = *req.PermCode
}
if req.Platform != nil {
permission.Platform = *req.Platform
}
if req.URL != nil {
permission.URL = *req.URL
}
if req.ParentID != nil {
// 验证 parent_id 存在
parent, err := s.permissionStore.GetByID(ctx, *req.ParentID)
if err != nil || parent == nil {
return nil, errors.New(errors.CodeNotFound, "上级权限不存在")
}
permission.ParentID = req.ParentID
}
if req.Sort != nil {
permission.Sort = *req.Sort
}
if req.Status != nil {
permission.Status = *req.Status
}
permission.Updater = currentUserID
if err := s.permissionStore.Update(ctx, permission); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "更新权限失败")
}
return permission, nil
}
// Delete 软删除权限
func (s *Service) Delete(ctx context.Context, id uint) error {
// 检查权限存在
_, err := s.permissionStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodePermissionNotFound, "权限不存在")
}
return errors.Wrap(errors.CodeInternalError, err, "获取权限失败")
}
if err := s.permissionStore.Delete(ctx, id); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "删除权限失败")
}
return nil
}
// List 查询权限列表
func (s *Service) List(ctx context.Context, req *dto.PermissionListRequest) ([]*model.Permission, int64, error) {
opts := &store.QueryOptions{
Page: req.Page,
PageSize: req.PageSize,
OrderBy: "sort ASC, id ASC",
}
if opts.Page == 0 {
opts.Page = 1
}
if opts.PageSize == 0 {
opts.PageSize = constants.DefaultPageSize
}
filters := make(map[string]interface{})
if req.PermName != "" {
filters["perm_name"] = req.PermName
}
if req.PermCode != "" {
filters["perm_code"] = req.PermCode
}
if req.PermType != nil {
filters["perm_type"] = *req.PermType
}
if req.Platform != "" {
filters["platform"] = req.Platform
}
if req.AvailableForRoleType != nil {
filters["available_for_role_type"] = *req.AvailableForRoleType
}
if req.ParentID != nil {
filters["parent_id"] = *req.ParentID
}
if req.Status != nil {
filters["status"] = *req.Status
}
return s.permissionStore.List(ctx, opts, filters)
}
// GetTree 获取权限树
func (s *Service) GetTree(ctx context.Context, availableForRoleType *int) ([]*dto.PermissionTreeNode, error) {
permissions, err := s.permissionStore.GetAll(ctx, availableForRoleType)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "获取权限列表失败")
}
return buildPermissionTree(permissions), nil
}
// buildPermissionTree 构建权限树
func buildPermissionTree(permissions []*model.Permission) []*dto.PermissionTreeNode {
nodeMap := make(map[uint]*dto.PermissionTreeNode)
for _, p := range permissions {
nodeMap[p.ID] = &dto.PermissionTreeNode{
ID: p.ID,
PermName: p.PermName,
PermCode: p.PermCode,
PermType: p.PermType,
Platform: p.Platform,
AvailableForRoleTypes: p.AvailableForRoleTypes,
URL: p.URL,
Sort: p.Sort,
Children: make([]*dto.PermissionTreeNode, 0),
}
}
var roots []*dto.PermissionTreeNode
for _, p := range permissions {
node := nodeMap[p.ID]
if p.ParentID == nil || *p.ParentID == 0 {
roots = append(roots, node)
} else if parent, ok := nodeMap[*p.ParentID]; ok {
parent.Children = append(parent.Children, node)
} else {
roots = append(roots, node)
}
}
return roots
}
// permissionCacheItem 权限缓存项
type permissionCacheItem struct {
PermCode string `json:"perm_code"`
Platform string `json:"platform"`
}
// CheckPermission 检查用户是否拥有指定权限(实现 PermissionChecker 接口)
// userID: 用户ID
// permCode: 权限编码
// platform: 端口类型 (all/web/h5)
func (s *Service) CheckPermission(ctx context.Context, userID uint, permCode string, platform string) (bool, error) {
userType := middleware.GetUserTypeFromContext(ctx)
if userType == constants.UserTypeSuperAdmin {
return true, nil
}
cacheKey := constants.RedisUserPermissionsKey(userID)
cachedData, err := s.redisClient.Get(ctx, cacheKey).Result()
if err == nil && cachedData != "" {
var permissions []permissionCacheItem
if err := json.Unmarshal([]byte(cachedData), &permissions); err == nil {
return s.matchPermission(permissions, permCode, platform), nil
}
}
roleIDs, err := s.accountRoleStore.GetRoleIDsByAccountID(ctx, userID)
if err != nil {
return false, errors.Wrap(errors.CodeInternalError, err, "查询用户角色失败")
}
if len(roleIDs) == 0 {
return false, nil
}
permIDs, err := s.rolePermStore.GetPermIDsByRoleIDs(ctx, roleIDs)
if err != nil {
return false, errors.Wrap(errors.CodeInternalError, err, "查询角色权限失败")
}
if len(permIDs) == 0 {
return false, nil
}
permissions, err := s.permissionStore.GetByIDs(ctx, permIDs)
if err != nil {
return false, errors.Wrap(errors.CodeInternalError, err, "查询权限详情失败")
}
cacheItems := make([]permissionCacheItem, 0, len(permissions))
for _, perm := range permissions {
cacheItems = append(cacheItems, permissionCacheItem{
PermCode: perm.PermCode,
Platform: perm.Platform,
})
}
if cacheData, err := json.Marshal(cacheItems); err == nil {
s.redisClient.Set(ctx, cacheKey, cacheData, 30*time.Minute)
}
return s.matchPermission(cacheItems, permCode, platform), nil
}
func (s *Service) matchPermission(permissions []permissionCacheItem, permCode string, platform string) bool {
for _, perm := range permissions {
if perm.PermCode == permCode {
if perm.Platform == constants.PlatformAll || perm.Platform == platform {
return true
}
}
}
return false
}