Identity, roles and form access
Manage system identity, respondent identity and explicit form access rules without treating authorization as a client-side feature.
表单访问权限:设计与实现开发指南
本文面向刚接触 FormPlatform 的开发者,说明表单权限从数据库、管理表单、权限计算、HTTP 接口,到 Form Center 卡片和游标分页的完整链路。
本文讨论的是“系统用户访问表单”的 ACL(Access Control List)。受访者通过 Deployment 访问问卷调查的资格仍由 Survey 模块管理,不能用普通表单 ACL 替代。
1. 设计目标
这套权限系统需要同时解决五个问题:
1. 决定谁可以通过 Form Viewer 查看或运行表单。
2. 决定谁可以通过 Form Designer 修改表单。
3. 决定谁可以删除表单。
4. 决定管理员或 SurveyAssistant 是否可以代填问卷调查。
5. 决定哪些角色或系统用户可以建立新表单。
还必须满足以下安全和性能约束:
- deny 的优先级高于普通 grant 和角色默认权限。
- Administrator 必须始终可以恢复系统,不能被 ACL 锁在系统之外。
- 客户端隐藏按钮只是界面行为,服务器端每一个敏感接口仍必须重新鉴权。
- Form Center 不能先读取全部表单再分页;数据库应先产生有权限的候选表单,再使用游标读取当前页。
- FormStorage 可以是 Database、File 或 Memory。只有表单库与管理库位于同一关系数据库时,Form Center 才能执行数据库快速查询;其他模式必须保持逻辑正确的兼容路径。
2. 核心术语
2.1 Subject
权限可以授予或拒绝两类主体:
- `user`:一个系统用户,`subject_id` 是 `app_users.id`。
- `role`:一个系统角色,`subject_id` 是 `app_roles.id`。
运行时用户 ID 来自 `ClaimTypes.NameIdentifier`,角色名来自 `ClaimTypes.Role`。
2.2 Permission
`app_form_access_rules.permission` 当前支持:
| 值 | 含义 |
|---|---|
| `deny` | 明确拒绝。对非 Administrator 优先于 read/edit/delete 和角色默认值。 |
| `read` | 允许通过 Viewer 读取和运行表单。 |
| `edit` | 允许读取设计定义、metadata,并通过 Designer 修改表单。 |
| `delete` | 允许请求删除表单;领域删除保护仍会继续执行。 |
| `create` | 仅在全局“建立表单”作用域中使用,允许建立空白表单、从 Data Model 生成表单,或调用创建 API。 |
`assist` 没有存成 ACL permission。问卷代填由 Administrator 和 SurveyAssistant 的内置规则决定,代码入口是 `CanAssistSurveyAsync`。
`create` 不是某个具体表单的规则。它复用 `app_form_access_rules` 的索引和审计字段,使用固定全局 scope `form_id = 00000000-0000-0000-0000-000000000000`;该值不会作为真实表单 ID 使用。首次升级会写入一次 `create-configured` 系统标记和 FormDesigner 的 `create` grant,以保持旧版默认行为。管理员保存空角色/用户列表后,标记仍保留,因此只有 Administrator 能建立表单。
2.3 显式规则和默认规则
- 显式规则:保存在 `app_form_access_rules` 中。
- 默认规则:由平台角色和表单类型在代码中计算,不需要为每张表单插入重复记录。
这样即使有几万个表单,也不需要为 FormDesigner 自动生成几万条 edit 记录。
3. 有效权限规则
3.1 默认权限矩阵
| 主体/情况 | Read | Edit | Delete | Assist survey |
|---|---|---|---|---|
| Administrator | 是 | 是 | 是 | 是 |
| FormDesigner + 非匿名 System/Functional | 否 | 是 | 否 | 否 |
| FormDesigner + Survey | 是 | 是 | 否 | 否 |
| FormDesigner + 匿名表单 | 是 | 是 | 否 | 否 |
| SurveyAssistant + Survey | 是 | 否 | 否 | 是 |
| 普通已登录用户 | 由显式规则决定 | 由显式规则决定 | 由显式规则决定 | 否 |
| 未登录用户 + 匿名表单 | 是 | 否 | 否 | 否 |
| Respondent | 由 Deployment/List/时间范围决定 | 否 | 否 | 否 |
“默认否”不表示永远不能授权。例如可以给一个普通角色显式授予 edit;但 SurveyAssistant 的默认角色本身不会自动获得 edit。
3.2 优先级
普通系统用户的计算顺序是:
Administrator?
└─ 是:直接允许
Read 且表单匿名?
└─ 是:直接允许
没有登录?
└─ 是:拒绝
存在匹配 user/role deny?
└─ 是:拒绝
存在对应显式 grant?
└─ 是:允许
命中内置角色默认规则?
└─ 是:允许
否则拒绝
重要细节:
- Administrator 判断发生在 deny 之前,因此 Administrator 用户上的 deny 也不能锁死管理员。
- 保存 ACL 时明确禁止把 Administrator 角色放入 deny。
- 匿名 read 是表单的公共属性,不允许为匿名表单配置 deny/read。
- 匿名表单仍可以配置 edit/delete,因为这两个操作要求已登录身份。
3.3 五个判定方法
代码位于 `Data/FormAccessControl.cs`:
CanReadAsync(formId, metadata, principal, ct)
CanEditAsync(formId, metadata, principal, ct)
CanDeleteAsync(formId, principal, ct)
CanAssistSurveyAsync(formId, metadata, principal, ct)
CanCreateAsync(principal, ct)
不要在 Endpoint、Vue 或 Action Code 中重新复制角色判断。新的服务器入口必须调用这些方法之一。
`EvaluateAsync` 是批量版本,返回:
FormEffectiveAccess(
bool CanRead,
bool CanEdit,
bool CanDelete)
它用于 File/Memory/分库模式下的 Form Center 兼容查询。关系数据库同库模式使用等价的数据库端计算。
4. 数据库结构
4.1 ACL 表
表名:`app_form_access_rules`
| 列 | 用途 |
|---|---|
| `id` | 规则 ID。 |
| `form_id` | 表单 ID。 |
| `subject_type` | `user` 或 `role`。 |
| `subject_id` | 用户或角色 ID。 |
| `permission` | `deny/read/edit/delete`。 |
| `created_at` | 创建时间。 |
| `created_by` | 执行配置的管理员 ID。 |
唯一约束:
(form_id, subject_type, subject_id, permission)
主要索引:
ix_app_form_access_rules_form(form_id)
ix_app_form_access_rules_subject(subject_type, subject_id, permission, form_id)
第二个索引用于快速找到当前用户和角色相关的规则。表结构和索引由 `Data/ManagementDatabaseInitializer.cs` 管理。
ACL 表没有强制引用 `form_definitions` 的外键。这是有意设计:FormStorage 可能是 File、Memory,或位于另一数据库。删除表单时由应用服务显式清理 ACL。
4.2 可索引的 metadata 投影
`FormMetadata` 的完整内容仍保存在 `form_metadata.metadata_json`,但 Form Center 高频过滤需要两个普通数据库列:
form_definitions.form_type
form_definitions.is_anonymous
实现位于 `Data/RelationalFormStore.cs`。
保存 metadata 时,同一个数据库事务执行:
UPDATE form_definitions
SET form_type = ..., is_anonymous = ...
UPSERT form_metadata.metadata_json
COMMIT
如果表单不存在,事务回滚并返回 `KeyNotFoundException`。因此 JSON 和索引列不会出现一边成功、一边失败的状态。
旧数据通过一次性迁移 `normalize-form-metadata-v1` 回填。迁移记录保存在:
form_store_migrations
完成后,后续启动不会再次扫描所有 metadata。
Form Center 使用以下索引:
ix_form_definitions_updated_id(updated_at DESC, id DESC)
ix_form_definitions_access_cursor(is_anonymous, updated_at DESC, id DESC)
ix_form_definitions_type_cursor(form_type, updated_at DESC, id DESC)
5. 管理员配置权限的链路
5.1 管理表单
系统表单:
Name: System_Form_Access
ID: f0000000-0000-0000-0000-000000000064
定义位于 `Data/FormAccessSystemForms.cs`。它本身是普通 FormPlatform 表单,使用:
- AsyncSelect 选择表单;
- AsyncSelect 多选角色;
- AsyncSelect 多选用户;
- 独立的“建立表单”角色/用户 AsyncSelect 与保存按钮;
- Action Code 加载和保存 ACL。
它只向 Administrator 显示,并且后端 `/api/admin/form-access` 分组要求 `Administration` policy。前端隐藏菜单不能替代这项服务器授权。
5.2 管理 API
| 方法 | 路由 | 用途 |
|---|---|---|
| GET | `/api/admin/form-access/forms/options` | 搜索选择表单。 |
| GET | `/api/admin/form-access/create` | 读取全局建立表单角色/用户授权。 |
| PUT | `/api/admin/form-access/create` | 替换全局建立表单角色/用户授权。 |
| GET | `/api/admin/form-access/{formId}` | 读取该表单的显式规则。 |
| PUT | `/api/admin/form-access/{formId}` | 替换该表单的显式规则。 |
| GET | `/api/admin/system-access/roles/options` | AsyncSelect 读取角色。 |
| GET | `/api/admin/system-access/users/options` | AsyncSelect 读取用户。 |
表单选项接口是管理员低频配置接口,目前仍使用内存过滤和 OFFSET。不要把它复用为大型公共表单目录;公开 Form Center 使用专门的数据库游标查询。
普通已登录用户可调用 `GET /api/forms/create-access` 获得自己的 `canCreate` 标志,仅用于显示 Form Center 的“新建”和“从 Data Model 生成”按钮;服务器创建接口仍会再次调用 `CanCreateAsync`。
5.3 保存事务
`FormAccessControlService.SaveAsync` 的流程:
flowchart TD
A["接收 role/user ID 数组"] --> B["清理空值并去重"]
B --> C["验证表单存在"]
C --> D["读取 FormMetadata"]
D --> E{"是否匿名表单"}
E -->|是| F["拒绝 deny/read 配置"]
E -->|否| G["继续"]
G --> H["验证所有 role/user ID 存在"]
H --> I["禁止 deny Administrator role"]
I --> J["开启 Unit of Work"]
J --> K["删除该 form 的旧规则"]
K --> L["插入新的完整规则集合"]
L --> M["Commit"]
M --> N["重新读取并返回保存结果"]
这里采用“整组替换”而不是逐项 PATCH,原因是管理表单提交的是一个完整权限快照。删除旧规则和插入新规则在同一事务中完成。
5.4 匿名状态改变
`PUT /api/forms/{id}/metadata` 保存后,如果 `IsAnonymous = true`,会调用:
ClearAnonymousReadRulesAsync(formId, ct)
它删除旧的 deny/read 规则,避免表单从非匿名改成匿名后遗留互相矛盾的数据。edit/delete 不会被清理。
6. 单个表单请求的服务器鉴权
6.1 Viewer 路径
典型入口:
GET /api/forms/{id}/view
GET /api/forms/{id}/action-module.js
GET /api/forms/{id}/grids/{gridId}/data
GET /api/forms/{id}/item-renderers/{rendererId}/data
POST /api/forms/{id}/async-select/{controlId}/data
POST /api/forms/{id}/submissions
这些接口按用途调用 `CanReadAsync`。允许匿名的入口仍会在代码中区分:
- 未登录且不允许:通常返回 `401 Unauthorized`;
- 已登录但无权限:返回 `403 Forbidden`;
- Survey 表单通过普通 `/forms/{id}` 访问:按 Survey 路由规则返回 `404`。
数据接口和 Action Module 也必须鉴权。只保护最初的 `/view` 而放开后续数据接口,会形成直接 API 绕过。
6.2 Designer 路径
典型入口:
GET /api/forms/{id}
PUT /api/forms/{id}
GET /api/forms/{id}/metadata
PUT /api/forms/{id}/metadata
DELETE /api/forms/{id}/metadata/mapping
GET /api/forms/{id}/data
它们调用 `CanEditAsync`。这解释了一个重要区别:
- `/api/forms/{id}/view` 是运行时读取接口;
- `/api/forms/{id}` 是设计定义接口。
只有 read 的用户不能为了方便而调用设计接口,否则会收到 403。
6.3 Delete 路径
DELETE /api/forms/{id}
删除过程不是只有 ACL:
flowchart LR
A["请求删除"] --> B["CanDeleteAsync"]
B -->|拒绝| C["403"]
B -->|允许| D["检查系统保护表单"]
D --> E["检查 Survey 部署/锁定等领域约束"]
E --> F["删除表单存储"]
F --> G["DeleteAllAsync 清理 ACL"]
因此 delete grant 只表示“有资格请求删除”,不能绕过:
- System shell/management protected forms;
- 已部署或被锁定的调查表单;
- 其他领域完整性规则。
旧的 system-grid 删除入口也会执行同样的 ACL 和清理,避免从历史 API 绕过。
6.4 SurveyAssistant 代填
代填入口通过 `CanAssistSurveyAsync`:
- Administrator 始终允许;
- 必须是 Survey 表单;
- SurveyAssistant 必须登录;
- 匹配 deny 时拒绝。
普通 read grant 不自动等于“可以代填”。这是有意分离的业务能力。
6.5 Respondent 边界
Respondent 使用独立认证 scheme、Deployment、Survey List、开始/结束时间和 response 规则。Respondent 不是普通系统角色,不应通过给它写 `app_form_access_rules` 来开放问卷。
管理员代填和受访者作答最终可以进入同一份调查数据,但授权入口不同。
7. Form Center:从权限到卡片
Form Center 是权限系统中最需要关注性能的列表。
完整请求链:
sequenceDiagram
participant UI as FormItemRenderer
participant API as /api/forms/.../item-renderers/.../data
participant ACL as FormAccessControlService
participant FC as FormCenterQueryService
participant DB as RelationalFormStore / Database
UI->>API: limit, search, cursor
API->>ACL: CanRead(Form_Center)
ACL-->>API: allow / deny
API->>FC: principal + query
FC->>FC: validate cursor and detect same database
FC->>DB: candidate IDs + effective ACL + seek + limit+1
DB-->>FC: at most pageSize+1 rows
FC->>FC: hasMore and nextCursor
FC-->>UI: items + canRead/canEdit/canDelete
UI->>UI: show Open/Edit/Delete buttons
注意第一项 ACL 检查:用户必须先有 Form Center 系统表单本身的 read 权限。之后数据库查询才决定这个用户能看到哪些业务表单卡片。
7.1 同库快速路径判定
`FormCenterQueryService` 比较 FormStorage 和 ManagementDatabase 的:
- provider;
- server/host;
- port;
- database;
- schema。
账号和密码可以不同,只要最终指向同一数据库和 schema。
同库并且底层是关系存储时,日志出现:
Form Center uses database-side ACL candidate filtering and cursor pagination.
日志在该 Singleton 第一次被请求创建时写入,不一定出现在应用启动阶段。
如果不是同库,则使用兼容路径:
Form Center uses the compatible in-memory ACL pagination path ...
兼容路径保证权限正确,但会读取全部摘要和 metadata,不适合几万表单的高并发目录。
7.2 候选表单 ID
非 Administrator 的数据库查询先构造候选 ID:
匿名表单
UNION
当前 user/role 的显式 read/edit/delete grant
UNION
内置角色默认范围
内置角色范围:
- FormDesigner:所有表单都是 edit 候选,因此直接从完整 catalog 开始,避免冗余 UNION。
- SurveyAssistant:`form_type = 'Survey'`。
然后聚合当前主体匹配的规则:
denied
can_read_grant
can_edit_grant
can_delete_grant
最后计算有效权限并执行:
WHERE can_read = 1
OR can_edit = 1
OR can_delete = 1
没有任何权限的表单不会发送给浏览器。这样既避免信息泄露,也保证每次加载得到完整的授权页面。
Administrator 使用独立快速分支,直接读取有序 `form_definitions`,不连接 ACL 表。
7.3 卡片按钮
数据库为每条卡片返回:
{
"canRead": true,
"canEdit": false,
"canDelete": false
}
`Data/FormCenterSystemForms.cs` 中三个按钮的条件是:
fc-open: row.canRead === true
fc-edit: row.canEdit === true
fc-delete: row.canDelete === true
不要再写:
context.user.roles.includes('FormDesigner')
因为它无法表达显式 user grant、role grant、deny、匿名规则和 Administrator 恢复规则。
再次强调:按钮条件不是安全检查。即使用户用开发者工具强制显示按钮,目标 API 仍会调用 `CanReadAsync`、`CanEditAsync` 或 `CanDeleteAsync`。
8. 游标分页
排序固定为:
ORDER BY updated_at DESC, id DESC
游标保存上一页最后显示记录的:
{
"UpdatedAt": "2026-08-08T10:30:00Z",
"Id": "..."
}
对外将 JSON 编码成 Base64Url 字符串。客户端不应解析或构造它,只需要原样回传。
下一页条件:
WHERE updated_at < @cursorUpdatedAt
OR (
updated_at = @cursorUpdatedAt
AND id < @cursorId
)
`id` 是相同时间下的唯一稳定排序键。只使用 `updated_at` 会漏掉更新时间相同的记录。
每次查询 `pageSize + 1`:
- 多出的第 1 条不显示,只说明 `hasMore = true`;
- 当前显示页最后一条用于生成 `nextCursor`;
- 不执行完整 `COUNT(*)`;
- `total = -1` 表示此游标结果不提供精确总数。
响应示例:
{
"items": [],
"total": -1,
"limit": 24,
"nextCursor": "eyJVcGRhdGVkQXQiOi4uLn0",
"hasMore": true
}
参照记录被删除不会破坏下一页,因为查询使用游标中的排序值,不会先按 ID 重新查找参照记录。
客户端实现位于 `ClientApp/src/components/FormItemRenderer.vue`:
- 首次查询不带 cursor;
- Load More 发送上次 `nextCursor`;
- `hasMore` 优先使用服务器布尔值;
- 搜索、pageSize 或数据源改变时重新从第一页开始。
9. 受保护的平台角色
平台角色:
Administrator
FormDesigner
SurveyAssistant
由 `ManagementIdentityStore` 幂等初始化。`SystemAccessManagementService` 阻止:
- 删除这些角色;
- 重命名这些角色。
原因是默认权限逻辑使用稳定的角色名。如果允许删除或改名,代码默认规则会失效。
新增平台角色时必须同时检查:
1. `FormAccessNames`;
2. 角色初始化;
3. 角色保护;
4. 单表单 ACL 计算;
5. Form Center 数据库查询;
6. 管理表单说明和 i18n;
7. 本文默认矩阵。
10. 新增服务器功能时怎样接入权限
10.1 先确定能力
不要根据路由名称猜权限。先回答:
- 这是运行表单所需的数据吗?使用 read。
- 这是 Designer、schema、metadata 或批量数据管理吗?使用 edit。
- 这是删除表单吗?使用 delete,并继续领域检查。
- 这是管理员代填调查吗?使用 assist。
- 这是 Respondent 作答吗?使用 Survey Deployment 授权链。
10.2 Endpoint 模板
Viewer 数据接口示例:
var form = await store.GetAsync(id, ct);
if (form is null) return Results.NotFound();
var metadata = await store.GetMetadataAsync(id, ct)
?? FormMetadata.Default(id);
if (!await access.CanReadAsync(id, metadata, principal, ct))
return principal.Identity?.IsAuthenticated == true
? Results.Forbid()
: Results.Unauthorized();
// 只有通过鉴权后才能读取业务数据。
Designer 接口将 `CanReadAsync` 换成 `CanEditAsync`。
10.3 不要采用的写法
// 错误:绕过显式 grant/deny 和匿名规则
if (!principal.IsInRole("FormDesigner")) return Results.Forbid();
// 错误:客户端角色判断不能保护服务器资源
const allowed = context.user.roles.includes('Administrator');
// 错误:只保护 view,却忘了 action-module、grid、async-select 等子资源
return Results.Ok(await repository.QueryAsync(...));
11. 修改权限模型时的检查清单
如果增加新的 permission,例如 `publish`,至少需要修改:
1. `FormAccessSelection` 和 `SaveFormAccessRequest`。
2. `FormAccessControlService.GetAsync/SaveAsync/Rules`。
3. 单项有效权限方法。
4. `EvaluateAsync` 批量语义。
5. `FormCenterStoreQuery/FormCenterStoreRow`(如果影响卡片)。
6. `RelationalFormStore.QueryFormCenterAsync` 中的规则聚合、候选集和 CASE。
7. `System_Form_Access` 的组件与 Action Code 字段数组。
8. 管理 API DTO。
9. i18n 文本。
10. 所有需要该能力的服务器 Endpoint。
11. 索引是否仍覆盖新的查询模式。
12. 单元/集成测试矩阵和本文档。
最重要的原则是:单表单判定和数据库批量判定必须保持同一语义。只修改其中一个,会导致“卡片显示但接口 403”或“卡片消失但直接 URL 可以访问”。
12. 建议验证矩阵
至少建立以下测试用户:
- Administrator;
- FormDesigner;
- SurveyAssistant;
- 普通用户 A;
- 普通用户 B;
- Respondent。
对 System、Functional、Survey,以及匿名/非匿名组合验证:
| 场景 | Form Center | Open | Edit | Delete | 直接 API |
|---|---|---|---|---|---|
| 无任何权限 | 不显示 | 拒绝 | 拒绝 | 拒绝 | 401/403 |
| 只有 read | 显示 | 显示 | 隐藏 | 隐藏 | Viewer 允许,Designer 拒绝 |
| 只有 edit | 显示 | 隐藏 | 显示 | 隐藏 | Designer 允许,Viewer 按规则拒绝 |
| 只有 delete | 显示 | 隐藏 | 隐藏 | 显示 | 删除仍受领域保护 |
| grant + deny | 不显示 | 拒绝 | 拒绝 | 拒绝 | 403 |
| Administrator + deny | 显示 | 显示 | 显示 | 显示 | 允许 |
| 匿名、未登录 | Form Center 通常不可进入 | Viewer 允许 | 拒绝 | 拒绝 | 公共子资源按 read 开放 |
还应验证:
- role grant 与 user grant 分别生效;
- 用户同时属于多个角色时,只要任一匹配 deny 就拒绝;
- metadata 从非匿名改为匿名后 deny/read 被清理;
- 删除表单后 ACL 被清理;
- Load More 不重复、不漏记录;
- 游标参照记录删除后仍能继续;
- 搜索改变后 cursor 被重置;
- 同库日志显示数据库快速路径;
- File/Memory/分库日志显示兼容路径。
13. 常见排错
13.1 Form Center 返回 403
先检查用户是否对 Form Center 系统表单本身有 read:
f0000000-0000-0000-0000-000000000060
不要调用 `GET /api/forms/{id}` 来加载 Form Center Viewer;该接口需要 edit。运行时页面应调用:
GET /api/forms/{id}/view
13.2 卡片按钮与直接 API 不一致
检查:
1. `FormAccessControlService` 的单项规则;
2. `EvaluateAsync`;
3. `RelationalFormStore.QueryFormCenterAsync` 的 CASE;
4. 卡片是否使用 `row.canRead/canEdit/canDelete`;
5. metadata 投影列是否已同步。
13.3 没看到快速路径日志
日志只在 `FormCenterQueryService` 第一次实例化时写一次,通常发生在首次请求 ItemRenderer 时。查看 `Logs/formplatform-YYYYMMDD.log`,搜索:
Form Center uses database-side ACL candidate filtering and cursor pagination.
13.4 出现兼容回退日志
核对 `appsettings.json`:
- FormStorage 和 ManagementDatabase provider 是否相同;
- host、port、database、schema 是否相同;
- FormStorage 是否为 File/Memory。
不同账号不是问题,只要连接到同一个数据库/schema。
13.5 SQL 里仍看到单表单 ACL 查询
进入 Form Center 前,服务器必须先验证当前用户能否读取 Form Center 本身。日志中的 `SystemAccessRole` 和只含 Form Center ID 的 `SystemFormAccessRule` 查询属于入口鉴权,不代表卡片查询退回了全量模式。
14. 主要代码导航
| 文件 | 职责 |
|---|---|
| `Data/FormAccessControl.cs` | ACL DTO、单表单/批量有效权限、保存、清理。 |
| `Data/FormAccessSystemForms.cs` | Form Access 管理表单和管理员菜单入口。 |
| `Data/PlatformMigrationModule.cs`、`Data/ManagementSchema.cs` | ACL 表和索引的版本化 migration。 |
| `Data/SystemAccessManagementService.cs` | ACL EntityModel、用户/角色管理、平台角色保护。 |
| `Data/ManagementIdentityStore.cs` | 受保护角色的幂等初始化。 |
| `Data/RelationalFormStore.cs` | metadata 投影列同步、候选 ID、有效权限 SQL、游标查询。 |
| `Data/IFormStore.cs` | `IFormCenterPageStore` 可选快速路径契约。 |
| `Data/PublishingFormStore.cs` | 将快速查询转发到内部关系存储。 |
| `Data/FormCenterQueryService.cs` | 同库检测、游标编解码、`pageSize + 1`、兼容回退。 |
| `Data/FormCenterSystemForms.cs` | 卡片结构和三个权限按钮条件。 |
| `ClientApp/src/components/FormItemRenderer.vue` | 保存/回传 cursor,处理 `hasMore`。 |
| `Program.cs` | 管理 API、Viewer/Designer/Delete/Survey 等服务器鉴权入口。 |
15. 设计原则总结
新开发者只需要牢牢记住以下原则:
1. 权限在服务器判定,Vue 只负责呈现。
2. deny 优先;Administrator 是不可锁死的恢复主体。
3. read、edit、delete、assist 是不同能力,不要互相推断。
4. Respondent Deployment 授权与系统用户 ACL 是两条不同链路。
5. 单表单判定和列表批量判定必须语义一致。
6. 大列表必须先在数据库过滤权限,再搜索、排序和分页。
7. Load More 使用 `(updated_at,id)` 游标和 `pageSize + 1`,不需要精确 COUNT。
8. delete grant 后仍必须执行系统表单、Deployment 和领域完整性保护。
9. metadata JSON 是完整配置;`form_type/is_anonymous` 是为查询建立的事务同步投影。
10. 新增任何权限能力时,要沿“数据库 → 管理表单 → ACL 服务 → 批量查询 → Endpoint → 客户端呈现 → 测试”整条链路修改。