# 如何设置权限

xly 的权限模型有两张互补表。它们回答*不同*问题，并位于不同层；正确阅读这里很重要，因为名字看起来很相似。

## 两张表

| 表 | 粒度 | 存储内容 | 加载时机 |
|---|---|---|---|
| [`gdsjurisdiction`](../../auto-catalog/tables/gdsjurisdiction.md) | 每**模块** | 每个模块存在的动作 / 按钮**目录**（`BtnAdd`、`BtnUpd`、`BtnDel` 等） | 每次 `getModelBysId`（ADMIN 跳过，见[切片 1](../../slices/01-hello-world.md)） |
| [`sysjurisdiction`](../../auto-catalog/tables/sysjurisdiction.md) | 每**角色**（也可每用户） | **授权**：哪个角色（或用户）可在某模块上执行哪个动作 | 按用户角色在 `getModelBysId` 中解析；`BusinessGdsconfigformsServiceImpl.getJurisdictionData()` |

心智模型：`gdsjurisdiction` 是框架知道的每模块权限项*菜单*；`sysjurisdiction` 是*座位表*，记录谁能用哪个项。

## `gdsjurisdiction` 包含什么

按模块组织的权限项树：

| 列 | 含义 |
|---|---|
| `sParentId` | 该权限项所属的 `gdsmodule.sId` |
| `sName` | 动作 key，例如 `BtnAdd`、`BtnUpd`、`BtnDel`、`BtnExport` |
| `sChinese` / `sEnglish` / `sBig5` | 显示标签（如 `新增`） |
| `iOrder` | 权限 UI 中的排序 |
| `sBrandsId` / `sSubsidiaryId` | 租户作用域 |

当前实时 DB 中有 **4,335 行 `gdsjurisdiction`**，覆盖 892 个不同模块和 186 个不同动作名。`gdsjurisdiction` **不**包含角色列或用户列，它只是目录。

## `sysjurisdiction` 包含什么

真实授权：

| 列 | 含义 |
|---|---|
| `sParentId` | 授权所属模块 `sId` |
| `sParent2Id` | 父模块（组 / 分类），供权限 UI 渲染树使用 |
| `sJurisdictionClassifyId` | 角色 / 权限分类 id（到 `sisjurisdictionclassify` 的 FK 语义） |
| `sUserId` | 用户 id，用于**每用户覆盖**；为空表示“角色级授权” |
| `sAction` | 被授予的动作，例如 `BtnBsOperation.BtnDesignFunction` |
| `sKey` | 复合路径 key，编码拥有该动作的模块层级 |

当前实时 DB 中有 **28,045 行 `sysjurisdiction`**，**全部都是角色级**（每个 `sUserId` 都为空）：22 个角色 × 各角色可使用的模块和动作。

`BusinessGdsconfigformsServiceImpl.getJurisdictionData`（第 212 行）在用户级和角色级授权之间选择：

```java
Boolean hasCheckUser = checkUserJurisdiction(map);
if (hasCheckUser) {
    return this.getGroupJurisdictionUserNew(map);  // 每用户覆盖
} else {
    return this.getGroupJurisdictionNew(map);      // 仅角色 fallback
}
```

如果调用用户在 `sysjurisdiction` 中有任何带其 `sUserId` 的行，则用户路径胜出；否则使用角色路径。当前 dev DB 中用户路径不可达。

## 请求中如何加载权限

`BusinessBaseServiceImpl.getModelBysId` 中相关伪代码：

```java
List<Map<String, Object>> jList = new ArrayList<>();
if (!UserType.ADMIN.getTypeName().equals(map.get("sUserType"))) {
    jList = businessGdsconfigformsService.getJurisdictionData(qMap);
}
returnMap.put("gdsjurisdiction", jList);
```

**ADMIN 完全绕过权限**并得到空授权列表，SPA 将其解释为“显示所有按钮”。非管理员用户得到其角色 + 模块 + 租户可解析出的动作过滤列表。SPA 根据返回结果启用 / 禁用按钮和列。

## 如何授予权限

新模块的典型路径：

1. 定义模块 + 表单（[配方](define-form.md)）。
2. 为模块暴露的每个按钮 / 动作插入 `gdsjurisdiction` 行（`BtnAdd`、`BtnUpd`、`BtnExport` 等），每动作一行，`sParentId = 你的模块 sId`。
3. 决定哪些角色获得哪些动作，插入 `sysjurisdiction` 行：`sParentId = module sId`、`sJurisdictionClassifyId = role sId`、`sAction = action key`。角色级授权保持 `sUserId` 为空。
4. 如需用户级覆盖，插入带 `sUserId` 的行；每用户覆盖会遮蔽该用户的角色级授权。
5. 通过**后台**保存，让缓存失效传播（运行时按 `(sUserId, sModelsId, sBrandsId, sSubsidiaryId)` 缓存 `getJurisdictionData`，见第 209 行的 `@Cacheable`）。

## ADMIN 特例

硬编码的 ADMIN 绕过是真实逃生口。按约定每个部署至少有一个 ADMIN 账号。从安全审计角度，ADMIN 访问应在外部严格控制和审计；框架本身没有应用内限制。

## 跨租户边界不是权限

需要明确：`gdsjurisdiction` + `sysjurisdiction` 强制的是**租户内**权限。跨租户边界（一家公司不能读取另一家公司数据）由[多租户作用域](../../concepts/multi-tenancy.md)强制，即自动注入 `sBrandsId` / `sSubsidiaryId`，不是由 jurisdiction 规则强制。

## 关于 `plat_base_authority_*`

schema 中有三张 `plat_base_authority` 下的表（`plat_base_authority`、`plat_base_authority_button_type`、`plat_base_authority_data_type`），看起来像按钮类型和数据权限类型 lookup。**三张表在实时 DB 中都为空**；它们属于不在本 Wiki 范围内的 `xlyPlat*` B2B 平台层，不属于框架自身权限流。记录框架权限时不要引用它们；上面的权限接口面是自包含的。
