GoWind 开源生态GoWind 开源生态
首页
框架
GoWind Admin
GoWind CMS
GoWind IM
GoWind UBA
GoWind IoT
GoWind Toolkit
GoWind Quant
GitHub
首页
框架
GoWind Admin
GoWind CMS
GoWind IM
GoWind UBA
GoWind IoT
GoWind Toolkit
GoWind Quant
GitHub
  • 介绍

    • GoWind Admin 产品介绍
    • GoWind Admin 安装指南
  • 后端文档

    • 后端架构总览
    • 后端核心模块详解
    • 后端 API 与 Protobuf 定义
    • 后端配置与部署
    • 后端扩展开发
  • 前端文档

    • 前端架构总览
    • 前端核心功能详解
  • 二开教程

    • 后端新增业务模块实战教程
    • 前端新增业务页面实战教程
    • 前后端联调完整实战教程
  • 高级教程

    • Lua 脚本扩展实战教程
    • 权限系统深度解析教程
    • 多租户架构实战教程
    • 任务调度与异步处理教程
    • 文件上传与对象存储教程
    • 事件总线与解耦架构教程
    • 前端主题定制与国际化教程
    • 性能优化与监控教程
    • SSE 实时推送
    • 登录策略与安全加固
    • 加密工具实战

多租户架构实战教程

GoWind Admin 内置多租户支持,适用于 SaaS 平台开发。本教程深入讲解多租户架构设计、数据隔离策略和实战场景。

一、多租户模型

1.1 两种租户关系模型

GoWind Admin 支持两种用户-租户关系:

模型说明适用场景常量值
未启用不启用多租户单租户模式、独立部署UserTenantRelationNone
一对一一个用户只属于一个租户企业内部系统、独立 SaaS 客户UserTenantRelationOneToOne
一对多一个用户可以属于多个租户跨组织协作、顾问角色、多公司管理UserTenantRelationOneToMany

在 pkg/constants/tenant.go 中配置:

// UserTenantRelationType 用户-租户关系类型
type UserTenantRelationType int

const (
    UserTenantRelationNone     UserTenantRelationType = iota // 未启用多租户
    UserTenantRelationOneToOne                                 // 一对一
    UserTenantRelationOneToMany                                // 一对多
)

const (
    // 默认一对一模式
    DefaultUserTenantRelationType = UserTenantRelationOneToOne
    // 是否启用租户模式(None 时为 false)
    IsTenantModeEnabled = DefaultUserTenantRelationType == UserTenantRelationOneToOne ||
                          DefaultUserTenantRelationType == UserTenantRelationOneToMany
)

1.2 数据库表结构

注意:以下为 Ent Schema 定义的核心字段,实际表名有 sys_ 前缀(如 sys_tenants、sys_users、sys_memberships),由 Ent 的 entsql.Annotation 指定。

-- 租户表 (sys_tenants)
-- 核心字段:
--   id, name(唯一), code(唯一), logo_url, domain, industry,
--   admin_user_id, status(ON/OFF/EXPIRED/FREEZE),
--   type(TRIAL/PAID/INTERNAL/PARTNER/CUSTOM),
--   audit_status(PENDING/APPROVED/REJECTED),
--   subscription_at, unsubscribe_at, subscription_plan, expired_at

-- 用户表 (sys_users)
-- 核心字段:
--   id, username, nickname, realname, email, mobile,
--   avatar, gender, status(NORMAL/DISABLED/PENDING/LOCKED/EXPIRED/CLOSED)
-- 注意:tenant_id 通过 mixin.TenantID 自动注入,在租户范围内 username 唯一

-- 成员关联表 (sys_memberships) —— 一对多模型的核心
-- 核心字段:
--   id, user_id, tenant_id(通过 mixin 注入),
--   org_unit_id, position_id, role_id (冗余单一场景字段,
--   与独立关联表 membership_role/membership_org_unit/membership_position 互斥),
--   is_primary(是否主身份),
--   start_at, end_at, assigned_at, assigned_by, joined_at,
--   status(ACTIVE/DISABLED/PENDING/INVITED/EXPIRED/REJECTED)
-- 注意:tenant_id + user_id 联合唯一

关于角色关联:一对一模型通过 user_role 表关联,一对多模型通过 membership_role 表关联。角色代码有前缀约定:平台角色 platform:xxx,租户角色 tenant:xxx,模板角色 template:xxx。

二、一对一租户模型

2.1 配置

在 pkg/constants/tenant.go 中设置:

const DefaultUserTenantRelationType = UserTenantRelationOneToOne

2.2 登录流程

一对一模型的登录授权逻辑在 authorizeAndEnrichUserTokenPayloadUserTenantRelationOneToOne 方法中:

func (s *AuthenticationService) authorizeAndEnrichUserTokenPayloadUserTenantRelationOneToOne(
    ctx context.Context, userID, tenantID uint32,
    tokenPayload *authenticationV1.UserTokenPayload,
) error {
    // 1. 检查租户状态(仅当 tenantID > 0 时)
    if tenantID > 0 {
        tenant, _ := s.tenantRepo.Get(ctx, &identityV1.GetTenantRequest{
            QueryBy: &identityV1.GetTenantRequest_Id{Id: tenantID},
        })
        if tenant == nil || tenant.GetStatus() != identityV1.Tenant_ON {
            return authenticationV1.ErrorForbidden("insufficient authority")
        }
    }

    // 2. 通过 user_role 表获取角色 ID 列表
    roleIDs, err := s.userRepo.ListRoleIDsByUserID(ctx, userID)
    if err != nil || len(roleIDs) == 0 {
        return authenticationV1.ErrorForbidden("insufficient authority")
    }

    // 3. 通过 role_permission 表获取权限 ID 列表
    permissionIDs, err := s.roleRepo.ListPermissionIDsByRoleIDs(ctx, roleIDs)
    // ...获取权限代码列表
    permissionCodes, _ := s.permissionRepo.GetPermissionCodesByIDs(ctx, permissionIDs)

    // 4. 检查是否有后台访问权限 (sys:access_backend)
    if !containsPermission(permissionCodes, constants.SystemAccessBackendPermissionCode) {
        return authenticationV1.ErrorForbidden("insufficient authority")
    }

    // 5. 获取角色代码列表并写入 Token
    roleCodes, _ := s.roleRepo.ListRoleCodesByRoleIds(ctx, roleIDs)
    tokenPayload.Roles = roleCodes

    return nil
}

实际登录入口 doGrantTypePassword 的完整流程:

func (s *AuthenticationService) doGrantTypePassword(
    ctx context.Context, req *authenticationV1.LoginRequest,
) (*authenticationV1.LoginResponse, error) {
    // 1. 重置 Context(绕过隐私保护和 Viewer)
    ctx = s.resetContextForLogin(ctx)

    // 2. 验证用户凭证(用户名+密码)
    _, err := s.userCredentialRepo.VerifyCredential(ctx, &authenticationV1.VerifyCredentialRequest{
        IdentityType: authenticationV1.UserCredential_USERNAME,
        Identifier:   req.GetUsername(),
        Credential:   req.GetPassword(),
        NeedDecrypt:  true,
    })

    // 3. 获取用户信息
    user, _ := s.userRepo.Get(ctx, &identityV1.GetUserRequest{
        QueryBy: &identityV1.GetUserRequest_Username{Username: req.GetUsername()},
    })

    // 4. 构建令牌载荷
    tokenPayload := &authenticationV1.UserTokenPayload{
        UserId:   user.GetId(),
        TenantId: user.TenantId,
        Username: user.Username,
        ClientId: req.ClientId,
        DeviceId: req.DeviceId,
    }

    // 5. 解析用户权限信息(根据租户关系类型分发)
    s.resolveUserAuthority(ctx, user, tokenPayload)

    // 6. 生成访问令牌和刷新令牌
    accessToken, refreshToken, _ := s.authenticator.CreateUserToken(ctx, req.GetClientType(), tokenPayload)

    return &authenticationV1.LoginResponse{
        TokenType:    authenticationV1.TokenType_bearer,
        AccessToken:  accessToken,
        RefreshToken: &refreshToken,
    }, nil
}

2.3 数据隔离

数据隔离通过 Ent Viewer 机制 实现,而非手动添加 Where 条件:

中间件层:Auth 中间件在每次请求时自动将 UserViewer 注入 Context:

// pkg/middleware/auth/auth.go
// 在认证成功后,创建 UserViewer 并注入 Context
userViewer := appViewer.NewUserViewer(
    uint64(tokenPayload.GetUserId()),
    uint64(tokenPayload.GetTenantId()),  // 租户 ID 来自 JWT Token
    uint64(tokenPayload.GetOrgUnitId()),
    traceID,
    tokenPayload.GetDataScope(),         // 数据权限范围
)
ctx = viewer.WithContext(ctx, userViewer)

Viewer 层:UserViewer 提供租户 ID 和数据权限范围:

// pkg/entgo/viewer/user_viewer.go
type UserViewer struct {
    uid        uint64              // 用户 ID
    tid        uint64              // 租户 ID
    ouid       uint64              // 组织单元 ID
    dataScopes []viewer.DataScope  // 数据权限范围
}

// IsPlatformContext 平台视图(tenant_id == 0)
func (v UserViewer) IsPlatformContext() bool { return v.tid == 0 }

// IsTenantContext 租户视图(tenant_id > 0)
func (v UserViewer) IsTenantContext() bool { return v.tid > 0 }

数据权限范围 (DataScope):

值说明
SELF仅本人数据
UNIT_ONLY本部门
UNIT_AND_CHILD本部门及子部门
SELECTED_UNITS指定部门
ALL全部数据

go-crud 的 Ent 集成层会自动从 Viewer 中提取 tenant_id,在所有查询和写入中添加租户过滤。

三、一对多租户模型

3.1 配置

const DefaultUserTenantRelationType = UserTenantRelationOneToMany

3.2 Membership 表设计

sys_memberships 表是核心,记录用户在每个租户中的身份:

// Ent Schema 定义(仅核心字段)
func (Membership) Fields() []ent.Field {
    return []ent.Field{
        field.Uint32("user_id"),           // 用户 ID
        // tenant_id 通过 mixin.TenantID[uint32]{} 自动注入

        // 冗余单一场景字段(与独立关联表互斥,二选一)
        field.Uint32("org_unit_id").Optional().Nillable(),    // 组织架构 ID
        field.Uint32("position_id").Optional().Nillable(),   // 职位 ID
        field.Uint32("role_id").Optional().Nillable(),        // 角色 ID

        field.Bool("is_primary").Default(false).Nillable(),  // 是否主身份
        field.Enum("status").Default("ACTIVE").               // 状态
            NamedValues("Active", "ACTIVE", "Disabled", "DISABLED",
                       "Pending", "PENDING", "Invited", "INVITED",
                       "Expired", "EXPIRED", "Rejected", "REJECTED"),
    }
}

// 联合唯一索引
index.Fields("tenant_id", "user_id").Unique()
// 每个租户内用户只有一个主身份
index.Fields("tenant_id", "user_id", "is_primary").Unique()

注意:org_unit_id/position_id/role_id 是冗余的单一场景字段。对于多角色/多部门的复杂场景,使用独立的关联表 membership_role、membership_org_unit、membership_position。

3.3 登录流程

一对多模型的授权逻辑在 authorizeAndEnrichUserTokenPayloadUserTenantRelationOneToMany 方法中:

func (s *AuthenticationService) authorizeAndEnrichUserTokenPayloadUserTenantRelationOneToMany(
    ctx context.Context, userID, tenantID uint32,
    tokenPayload *authenticationV1.UserTokenPayload,
) error {
    // 1. 获取成员身份
    var memberships []*identityV1.Membership
    if tenantID > 0 {
        // 指定租户登录
        membership, err := s.membershipRepo.GetMembershipByUserTenant(ctx, userID)
        memberships = []*identityV1.Membership{membership}
    } else {
        // 获取所有活跃的成员身份
        memberships, err = s.membershipRepo.GetUserActiveMemberships(ctx, userID)
    }

    // 2. 遍历成员身份,收集有效角色和权限
    hasBackendAccess := false
    var validRoleIDs []uint32
    for _, m := range memberships {
        // 检查租户状态
        tenant, _ := s.tenantRepo.Get(ctx, ...)
        if tenant == nil || tenant.GetStatus() != identityV1.Tenant_ON {
            continue
        }

        // 通过 membership_role 获取角色
        roleIDs, _ := s.membershipRepo.GetRoleIDsByMembership(ctx, m.GetId())
        // 通过 role_permission 获取权限
        permissionIDs, _ := s.roleRepo.ListPermissionIDsByRoleIDs(ctx, roleIDs)
        permissionCodes, _ := s.permissionRepo.GetPermissionCodesByIDs(ctx, permissionIDs)

        // 检查是否有后台访问权限
        if containsPermission(permissionCodes, constants.SystemAccessBackendPermissionCode) {
            hasBackendAccess = true
            validRoleIDs = append(validRoleIDs, roleIDs...)
        }
    }

    // 3. 授权决策
    if !hasBackendAccess {
        return authenticationV1.ErrorForbidden("insufficient authority")
    }

    // 4. 写入角色代码到 Token
    roleCodes, _ := s.roleRepo.ListRoleCodesByRoleIds(ctx, validRoleIDs)
    tokenPayload.Roles = roleCodes
    return nil
}

3.4 切换租户

一对多模型下,用户需要能够在多个租户之间切换。切换流程:

  1. 前端调用后端 API 传入目标 tenant_id
  2. 后端验证用户是否拥有该租户的 Membership
  3. 重新生成 JWT Token(包含新的 tenant_id 和对应角色权限)
  4. 前端使用新 Token 刷新数据

四、数据隔离策略

4.1 Viewer 机制(核心隔离方案)

GoWind Admin 通过 go-crud 的 Ent Viewer + Privacy 机制实现应用级数据隔离,而非手动添加 Where 条件。

工作原理:

中间件自动注入(pkg/middleware/auth/auth.go):

// Auth 中间件在认证成功后自动创建 UserViewer
userViewer := appViewer.NewUserViewer(
    uint64(tokenPayload.GetUserId()),
    uint64(tokenPayload.GetTenantId()),  // 租户 ID 从 JWT Token 获取
    uint64(tokenPayload.GetOrgUnitId()),
    traceID,
    tokenPayload.GetDataScope(),
)
ctx = viewer.WithContext(ctx, userViewer)

Ent Privacy 策略:通过 Ent 的 Privacy 框架定义租户访问规则,确保数据在 ORM 层面自动隔离。

平台视图 vs 租户视图:

// UserViewer 提供上下文判断
func (v UserViewer) IsPlatformContext() bool { return v.tid == 0 }  // 平台管理员
func (v UserViewer) IsTenantContext() bool   { return v.tid > 0 }  // 租户用户

4.2 Schema 层面

所有需要多租户隔离的 Ent Schema 都通过 mixin.TenantID[uint32]{} 混入 tenant_id 字段:

// 例如 User Schema
func (User) Mixin() []ent.Mixin {
    return []ent.Mixin{
        mixin.AutoIncrementId{},
        mixin.TimeAt{},
        mixin.OperatorID{},
        mixin.TenantID[uint32]{},  // 自动添加 tenant_id 字段
    }
}

// 索引也在租户范围内:
index.Fields("tenant_id", "username").Unique()  // 租户内用户名唯一

同理,Membership、Role、Permission 等表也都混入了 TenantID。

4.3 数据库级隔离

如果需要更强的物理隔离,可使用 PostgreSQL Schema 进行数据库级隔离:

-- 为每个租户创建独立的 Schema
CREATE SCHEMA tenant_1;
CREATE SCHEMA tenant_2;

-- 切换 Schema
SET search_path TO tenant_1;

优点:完全隔离,安全性高 缺点:维护成本高,跨租户查询困难,Schema 迁移复杂

GoWind Admin 当前版本使用应用级隔离(Viewer 机制),数据库级隔离需要自行扩展 Ent 的数据库连接层。

五、租户初始化

5.1 创建租户的完整流程

通过 TenantService.CreateTenantWithAdminUser 方法一次性创建租户和管理员,整个过程在事务中执行:

func (s *TenantService) CreateTenantWithAdminUser(
    ctx context.Context,
    req *identityV1.CreateTenantWithAdminUserRequest,
) (*emptypb.Empty, error) {
    // 1. 检查租户编码/名称是否已存在
    s.tenantRepo.TenantExists(ctx, &identityV1.TenantExistsRequest{
        Code: req.GetTenant().GetCode(),
        Name: req.GetTenant().GetName(),
    })

    // 2. 检查管理员用户名是否已存在
    s.userRepo.UserExists(ctx, &identityV1.UserExistsRequest{
        QueryBy: &identityV1.UserExistsRequest_Username{
            Username: req.GetUser().GetUsername(),
        },
    })

    // 3. 开启事务
    tx, cleanup, _ := s.tenantRepo.BeginTx(ctx)
    defer func() {
        cleanup()
        if err == nil {
            s.authorizer.ResetPolicies(ctx)  // 重置权限策略缓存
        }
    }()

    // 4. 创建租户记录
    tenant, _ := s.tenantRepo.CreateWithTx(ctx, tx, req.Tenant)
    req.User.TenantId = tenant.Id

    // 5. 从模板角色创建租户管理员角色
    role, _ := s.roleRepo.CreateTenantRoleFromTemplate(
        ctx, tx, tenant.GetId(), operator.GetUserId(),
    )

    // 6. 创建管理员用户(带角色 ID)
    req.User.RoleId = role.Id
    adminUser, _ := s.userRepo.CreateWithTx(ctx, tx, req.User)

    // 7. 创建用户凭证(密码)
    s.userCredentialsRepo.CreateWithTx(ctx, tx, &authenticationV1.UserCredential{
        UserId:         adminUser.Id,
        TenantId:       tenant.Id,
        IdentityType:   authenticationV1.UserCredential_USERNAME.Enum(),
        Identifier:     adminUser.Username,
        CredentialType: authenticationV1.UserCredential_PASSWORD_HASH.Enum(),
        Credential:     trans.Ptr(req.GetPassword()),
        IsPrimary:      trans.Ptr(true),
        Status:         authenticationV1.UserCredential_ENABLED.Enum(),
    })

    // 8. 将管理员用户 ID 绑定到租户
    s.tenantRepo.AssignTenantAdmin(ctx, tx, *tenant.Id, *adminUser.Id)

    return &emptypb.Empty{}, nil
}

关键设计要点:

  • 事务保证原子性:租户、角色、用户、凭证要么全部成功,要么全部回滚
  • 从模板角色复制:CreateTenantRoleFromTemplate 从 template:tenant:manager 模板角色复制权限到新租户
  • 权限策略刷新:事务成功后自动重置 Casbin 权限策略缓存

5.2 权限体系

租户相关的系统权限代码定义在 pkg/constants/permission.go:

const (
    // 系统权限前缀
    SystemPermissionCodePrefix = "sys:"

    // 访问后台权限
    SystemAccessBackendPermissionCode = "sys:access_backend"
    // 管理租户权限
    SystemManageTenantsPermissionCode = "sys:manage_tenants"
    // 审计日志权限
    SystemAuditLogsPermissionCode     = "sys:audit_logs"
    // 平台管理员权限
    SystemPlatformAdminPermissionCode = "sys:platform_admin"
    // 租户管理员权限
    SystemTenantManagerPermissionCode = "sys:tenant_manager"
)

角色代码前缀约定:

const (
    PlatformRoleCodePrefix = "platform:"    // 平台角色
    TenantRoleCodePrefix   = "tenant:"      // 租户角色
    TemplateRoleCodePrefix = "template:"    // 模板角色(用于初始化)

    PlatformAdminRoleCode = "platform:admin"          // 平台管理员
    TenantAdminRoleCode   = "tenant:manager"           // 租户管理员
)

六、跨租户数据共享

6.1 场景

某些场景需要跨租户共享数据,如:

  • 平台管理员查看所有租户数据(sys:manage_tenants 权限)
  • 公共字典数据

6.2 实现方案

方案 1:平台视图(推荐)

当 tenant_id == 0 时为平台视图,UserViewer.IsPlatformContext() 返回 true,此时 Viewer 不添加租户过滤条件:

// 平台管理员登录后 tenant_id = 0
// Viewer 不添加租户过滤,可以查看所有数据
if userViewer.IsPlatformContext() {
    // 不添加 WHERE tenant_id = ? 条件
}

方案 2:共享标记

在需要共享的表上添加 is_shared 字段:

ALTER TABLE dict_entries ADD COLUMN is_shared BOOLEAN DEFAULT FALSE;

-- 查询时
SELECT * FROM dict_entries
WHERE (tenant_id = :current_tenant OR is_shared = TRUE);

方案 3:关联表

CREATE TABLE shared_resources (
    resource_type VARCHAR(50),
    resource_id INTEGER,
    from_tenant_id INTEGER,
    to_tenant_id INTEGER,
    permission_level VARCHAR(20),  -- READ/WRITE
    ...
);

七、前端多租户适配

7.1 租户选择器

登录时显示租户选择(一对多模型):

<template>
  <Select v-model:value="selectedTenant" placeholder="选择租户">
    <Select.Option
      v-for="tenant in availableTenants"
      :key="tenant.id"
      :value="tenant.id"
    >
      {{ tenant.name }}
    </Select.Option>
  </Select>
</template>

7.2 租户信息展示

在顶部导航栏显示当前租户:

<template>
  <div class="tenant-info">
    <span>当前租户:{{ currentTenant.name }}</span>
    <Dropdown @click="handleSwitchTenant">
      <Icon icon="mdi:swap-horizontal" />
    </Dropdown>
  </div>
</template>

7.3 租户信息传递机制

重要:GoWind Admin 的租户信息 不是通过 HTTP Header 传递,而是 内嵌在 JWT Token 中。

实际流程:

  1. 登录时,后端将 tenant_id 写入 UserTokenPayload
  2. authenticator.CreateUserToken() 将其编码进 JWT Token
  3. 每次请求携带 Authorization: Bearer <token>
  4. Auth 中间件解析 Token,提取 tenant_id
  5. 创建 UserViewer 并注入 Context
// JWT Token 中的 UserTokenPayload 包含以下字段:
type UserTokenPayload struct {
    UserId    uint32
    TenantId  uint32    // 租户 ID
    Username  string
    Roles     []string  // 角色代码列表
    OrgUnitId uint32    // 组织单元 ID
    DataScope DataScope // 数据权限范围
    ClientId  string
    DeviceId  string
}

八、性能优化

8.1 缓存租户配置

利用 Redis 缓存租户信息和权限策略:

// Casbin 权限策略在租户创建/更新后自动重置
s.authorizer.ResetPolicies(ctx)

8.2 索引优化

Ent Schema 已内置多租户相关索引:

// User Schema 索引
index.Fields("tenant_id", "username").Unique()          // 租户内用户名唯一
index.Fields("tenant_id", "email").Unique()             // 租户内邮箱唯一
index.Fields("tenant_id", "mobile")                     // 租户+手机号查询
index.Fields("tenant_id", "created_at")                 // 租户范围分页

// Membership Schema 索引
index.Fields("tenant_id", "user_id").Unique()           // 租户+用户唯一
index.Fields("tenant_id", "user_id", "is_primary").Unique() // 租户内单主身份

// Tenant Schema 索引
index.Fields("name").Unique()                           // 租户名唯一
index.Fields("code").Unique()                           // 租户编码唯一
index.Fields("status", "audit_status")                  // 状态联合过滤

8.3 批量操作

避免循环查询,使用批量操作:

// ❌ 错误做法
for _, tenantID := range tenantIDs {
    users := queryUsersByTenant(tenantID)
}

// ✅ 正确做法
users := queryUsersByTenants(tenantIDs)  // WHERE tenant_id IN (...)

8.4 DataScope 优化

登录时合并多个角色的 DataScope,取最大权限,避免运行时重复计算:

// mergeDataScopes 合并角色数据权限,取最高优先级
func (s *AuthenticationService) mergeDataScopes(
    dataScopes []identityV1.DataScope,
) identityV1.DataScope {
    // DataScope_ALL > UNIT_AND_CHILD > UNIT_ONLY > SELF
    // 优先级从高到低,遇到 ALL 直接返回
}

九、常见问题

Q1: 如何选择一对一还是一对多模型?

  • 一对一(默认):适合企业内部系统、独立 SaaS 客户,实现简单,用户通过 user_role 关联角色
  • 一对多:适合跨组织协作、顾问角色、多公司管理,用户通过 membership_role 关联角色
  • 未启用(None):单租户模式,不进行租户隔离

Q2: 如何处理租户删除?

通过更新租户状态实现软删除:

// 将租户状态设置为 OFF
s.tenantRepo.Update(ctx, &identityV1.UpdateTenantRequest{
    Data: &identityV1.Tenant{Status: identityV1.Tenant_OFF.Enum()},
})

租户被禁用后,该租户下的所有用户将无法登录(登录时会检查租户状态)。

Q3: 如何添加自定义权限?

权限代码格式为 <模块>:<操作>,系统权限使用 sys: 前缀,业务权限使用 biz: 前缀:

const (
    SystemPermissionModule = "sys"          // 系统模块
    DefaultBizPermissionModule = "biz"      // 业务模块
)

Q4: 多租户下的国际化如何处理?

GoWind Admin 内置语言管理(sys_languages 表),支持多语言字典。每个租户可以使用独立的字典数据。

十、相关文档

  • 后端架构设计
  • 权限系统深度解析
  • 后端核心模块详解
Edit this page
Last Updated:: 6/5/26, 11:12 AM
Contributors: Bobo
Prev
权限系统深度解析教程
Next
任务调度与异步处理教程