开发者指南
从第一个可编辑表单到高级服务器端和客户端扩展的实用路径。
FormPlatform 初级、中级和高级开发者指南
开发或修改表单控件前,同时阅读客户端组件契约。
English: DEVELOPER_GUIDE.md
这份指南按能力分级,而不是按职位分级。初级开发者可以只用 Designer 和既有 Data Model 完成功能;中级开发者扩展服务器和控件;高级开发者维护平台边界、性能、安全、迁移和发布兼容性。
1. 所有人都要先建立的概念
开始前阅读 系统架构,并记住:表单结构、表单 metadata、Management Database 和业务记录不是同一种数据;客户端可见性不是安全;固定 DDL 不属于 initializer;第三方模块不引用 Host DLL。
推荐环境:.NET 10 SDK、Node.js 22、PostgreSQL 17(也支持 MySQL/MSSQL)、Git、浏览器开发者工具。PostgreSQL 集成测试另需 Docker。
典型本地配置使用 User Secrets:
dotnet user-secrets --project src/FormPlatform.Host/FormPlatform.csproj set "ManagementDatabase:ConnectionString" "Host=localhost;Database=formplatform;Username=postgres;Password=..."
dotnet user-secrets --project src/FormPlatform.Host/FormPlatform.csproj set "FormStorage:ConnectionString" "Host=localhost;Database=formplatform;Username=postgres;Password=..."
不要把真实密码写回 `appsettings.json`。
2. 初级开发者:用表单完成业务
2.1 目标能力
完成此阶段后,你应该能:
- 启动前后端并判断改动是否需要重新 build;
- 创建 Data Model 和表单映射;
- 使用 Designer、Viewer、DataGrid、AsyncSelect、Tree、Pagination;
- 配置必填、可见、只读、事件和 substitution;
- 从浏览器 Network/Console 和服务器日志定位常见错误;
- 不修改 Host 代码地完成一个 CRUD 页面。
2.2 项目运行方式
开发时可以分开运行:
# terminal 1
dotnet run --project src/FormPlatform.Host/FormPlatform.csproj --urls http://localhost:5080
# terminal 2
cd src/FormPlatform.Host/ClientApp
npm run dev
访问 Vite 的 `http://localhost:5173`。如果只改数据库中的表单,无需 `npm run build` 或重新编译;改 Vue/JS/CSS 后由 Vite 热更新;改 C# 后需要重新构建/启动。发布模式由 .NET 提供 `wwwroot`,客户端改动必须先 `npm run build`。
2.3 从数据库表到表单
最省力流程:
1. 在 Manage Data Models 选择数据库表并预览导入。
2. 核对 Name、DB Object、Schema、Primary Key、Calculated/Nullable 和类型。
3. 数据库生成的 `id` 标为 Calculated/Generated。
4. 对外键建立有语义的 Reference,例如 `createdBy`、`updatedBy`。
5. 从 Data Model 生成表单。
6. 在 Form and Data Mapping 检查每个输入控件的 `propertyName` 到 Attribute 的映射。
7. 用 Viewer 新建、跳转到返回的 record id,再重新加载验证更新。
`propertyName` 是表单数据键;Data Model Attribute 才映射数据库列。不要在控件中写 `app_users.user_name` 作为保存键。
`createdBy.user_name` 等 Reference 展示路径保持只读。服务器可通过 `@userId` 自动填充本地 `createdBy` 审计属性;关系加载会按 `DataValueKind` 规范化两端键值,因此刚写入的 `System.Guid` 能与目标表读回的规范 UUID 字符串匹配。表单不应为了显示引用用户名而提交该审计外键。
2.4 控件和数据规则
- Textbox/Textarea 输入采用 debounce/blur 等策略,不要每个字符触发全部依赖链。
- Boolean Checkbox 必须保持 `true`/`false`;Radio Group 或自定义值 Checkbox 未选择时为 `null`。需要区分“未作答”和“明确选择否”时使用 Boolean Radio 选项。
- Designer 新建的 Input 和 TextArea 默认启用 Full width。Div 的 Children view 为 Row 时,直属、启用 Full width、非文件型的 Input 平均分配该行可用宽度;直属 TextArea 默认独占一整行。取消 Full width 可退出自动布局,也可用 Div 的 Children container style 显式覆盖;已有表单控件不会被迁移。
- 已持久化的 Data Model 记录会把所有可用逻辑主键同时放入 `keys` 和 `data`。FormReader 将没有对应控件的服务器数据保存在独立只读条件上下文,因此普通单主键模型无需增加隐藏 ID 控件,就能在可见或只读条件中直接使用 `data.id`(也可写成 `Boolean(data.id)` 或 `data.id != null`)。这两类条件采用标准 JavaScript truthy/falsy 语义;自定义校验仍须明确返回布尔值,可使用 `Boolean(data.id)` 或 `data.id != null`。该上下文不会进入 `getData()` 或提交 payload;新建记录的 `initialData` 也不会伪造主键。更新、删除仍以服务器接收的 `keys`/`recordKeys` 为权威。若映射后的表单数据已经占用同名属性,服务器保留原值而不覆盖。
- Hidden 控件不提交。
- 必填和自定义验证是 validation;Tooltip 不是验证。
- Error CSS Class 作用于控件本身;错误消息位置独立配置。
- Pagination 的 Next 可按页面规则验证/保存草稿,Submit 验证全部页。
- DataGrid/ItemRenderer/AsyncSelect/Tree 运行时可访问 API;Designer 使用模拟数据。
- DataGrid 新增记录默认显示由 Columns/API 字段生成的内联编辑弹窗。将“新增显示”设为“在新窗口打开编辑表单”后,它会复用已设置的 Edit Form,以普通 Viewer 新建记录;未设置 Edit Form 或表达式尚未解析时自动回退到内联编辑器。浏览器可把新窗口呈现为新标签页,返回列表窗口时网格会刷新。目标表单仍执行自己的 ACL、Mapping、Trigger 和部门范围。
- DataGrid 的 Edit Form 下拉框在 Server/ORM、Management API 和 Static 模式下都只列出与已选 Data Model 具有相同 `metadata.mapping.entityId` 的表单;未选 Data Model 时不列候选项。已有不匹配值只作为“当前值”保留,不会被属性面板静默删除。
- 在 Edit Form 下拉框选择“自定义表达式…”会先写入 `{editFormId}` 并立即显示下方输入框。可在该输入框中改写宿主表单 substitution;它不是 JavaScript,也不会按 DataGrid 的每一行求值。
API 模式集合的 URL 可以携带固定的服务器范围查询,例如 `?location=dashboard`。分页、搜索和排序参数必须通过 `mergeQueryParameters` 合并,不能再追加第二个 `?`;非空运行时同名值覆盖固定值,空运行时值不删除固定值。URL 合并不替代服务器端参数校验和授权。
应用壳层以 `768px` 为导航断点:桌面端继续显示可折叠、可调整宽度的 `SystemLeftPane`;手机端不为左侧面板保留布局宽度,而由页眉右上角按钮打开同一个 `SystemLeftPane` 的离屏抽屉。不要为手机端复制另一份菜单表单或权限逻辑。手机抽屉支持遮罩点击、关闭按钮、`Escape`、焦点循环和选择菜单项后关闭,并在打开期间锁定页面滚动;独立页面和 Full viewport 表单不显示应用壳层导航。
2.5 Substitution 和 i18n
常见表达式:`{name}`、`{row.name}`、`{amount:0,000.00}`、日期格式。Action 参数也可以 substitution。系统表单固定文字使用 `@message.key`,在 i18n catalog 中提供中英文;业务录入内容通常直接保存,不当作系统 key。表单专属的 `displayName`、`description` 译文保存在定义自己的 `translations[locale]` 中,译文是字面量、不加 `@`,缺项按字段回退到基础文字,并随表单删除。只有多个表单/模块共享的固定名称才应继续使用顶层显式 `@key`;未加前缀的基础值不查询目录,也不要保存未加 `@` 的 `form.*` key。技术表单名 `name` 永不翻译。
Designer 顶栏只保留技术名称 `name`。`displayName`、`description`、它们的各语言覆盖、`isActive`、`hidden` 以及由 Administrator 控制的部门归属统一在 **表单设置 / Action Code** 中编辑。关系存储启动时会在实际 FormStorage 数据库中幂等补齐 `form_definitions.translations_json`,不修改已发布 Core migration。`hidden` 只在分页前抑制非 Administrator 的 Form Center 发现,不替代 ACL、部门范围、`isActive` 或直达路由授权。
2.6 初级调试清单
1. Network 看 URL、method、status、request payload 和统一错误 body。
2. 确认当前是 Designer、Preview 还是 Viewer。
3. 确认表单 id、record id、deployment id 是否进入 URL。
4. 检查 metadata 的 form type、anonymous、mapping/entity。
5. SQL 日志确认查询列、filter、sort 和参数,不只看 UI。
6. `SurveySystemFormInitializer` 只在表单缺失时采用完整代码默认 Schema,普通重启绝不能整体替换已有 Designer Schema。已有表单仍可接受只补缺失合同的窄范围修补,例如多租户 `departmentId` 控件和 Data Model mapping;修补必须保留已有控件、布局及显式自定义映射。`AppUsersFormInitializer` 更严格,只创建缺失表单,已有 Schema 和 metadata 完全保留;其源码 Schema/Action Code 可由 `tools/sync-system-forms.js --form AppUsersForm` 明确更新。破坏性升级应先备份并在 Designer 中手工合并。
任何使用 Survey 表单的 deployment 一旦已有答卷记录,原表单即不可再修改。Designer 和 metadata 保存会返回稳定的 `409 survey.formLocked` 合同。这是有意的数据安全边界:直接修改原 Schema 会使已采集答案的含义不明,或与 deployment 专属响应表不一致。错误响应不返回 deployment 标识,因为可设计表单并不代表可读取使用共享表单的所有 deployment。正确流程是复制表单建立新版本,修改副本后再建新 deployment;不应通过更新或删除已采集答卷来绕过锁定。
受访者部署 Dashboard 只有在清单分配、时间窗和部门范围过滤完成后,才投影 `formName`、原始 `completionStatus` 和可本地化的 `completionStatusText`。没有绑定答卷时状态为 `not_started`,最新答卷为草稿时是 `in_progress`,最新答卷完成时是 `completed`。每个部署拥有独立动态响应表,所以只查询当前部署分页的进度,并批量读取表单摘要。表单名与进度应保持为 ItemRenderer 卡片中的独立子组件,使 Designer 可以分别隐藏或删除显示,而不改变 API 合同。
受访者进入问卷后,会话栏提供“打印 / PDF”。打印使用浏览器当前已渲染的状态,因此 `not_started`、`in_progress`、`completed` 都可打印,尚未保存的字段值也可以出现在结果中;打印不会创建或更新答卷。打印 CSS 隐藏会话操作、表单按钮和分页导航,并展开全部可见的 Pagination 与 Tab 页面。`?print=1` 继续作为自动打印入口,且不再要求 `responseId`;读取仍经过正常的 deployment/respondent 授权。
2.7 初级练习
建立 `InventoryCategory` 表和 Data Model,生成 CRUD 表单;再建立一个商品表单,使用 AsyncSelect 选择 Category,并用 DataGrid 显示、搜索、排序、导出商品。完成后验证匿名/登录权限和中英文标签。
3. 中级开发者:扩展服务和运行时
3.1 目标能力
- 使用 ORM 编写安全的 Query/Mutation 和事务;
- 增加领域服务、Minimal API、Server Action 和 Trigger;
- 编写可在 Designer/Viewer 正确工作的 Vue 控件;
- 使用统一错误、toast、i18n 和 ACL;
- 为 schema 变化增加 migration;
- 编写集成和 E2E 测试。
3.2 增加服务器功能
官方 Host 端点放入对应的 `Hosting/Endpoints/*Endpoints.cs`;客户功能优先独立模块。端点只做绑定、鉴权和状态码,业务逻辑放服务中。
group.MapPost("/", async Task<IResult> (
SaveRequest request,
ClaimsPrincipal principal,
ItemService service,
CancellationToken ct) =>
{
var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier)
?? throw new UnauthorizedAccessException("Login is required.");
var saved = await service.SaveAsync(userId, request, ct);
return Results.Created($"/api/items/{saved.Id}", saved);
});
领域冲突抛出:
throw new PlatformApiException(
StatusCodes.Status409Conflict,
"inventory.versionConflict",
"inventory.versionConflict",
"The record was modified by another user.");
不要新建 `{ message }`、`{ error }` 私有格式。
3.3 ORM 使用原则
从 `IUnitOfWorkFactory` 开始事务,在同一 work 中完成读取、验证和写入;成功显式 commit,异常 rollback/dispose。Query Specification 使用已注册的 entity/attribute/reference 名称,ORM 负责标识符引用和参数。
高效查询应:
- 只 select 需要的字段;
- filter/sort 在数据库完成;
- 大数据用 cursor/keyset,而不是不断增大的 offset;
- 关联相同目标表时使用不同 Reference 名称;
- 所有租户/用户边界都进入服务器 filter。
保留 SQL 的条件见架构文档。保留时也必须参数化值、白名单标识符并写 SQL 日志。
库存分配等单行“比较并更新”使用 SDK 1.8 的 `TryUpdateWhereAsync`:Filter 包含完整主键
等值条件和库存等附加条件,更新使用 `FieldUpdate.Increment`/`Set`。比较与修改会在同一
条数据库语句中完成,条件已变化时返回 false,并继续参与调用者的事务和 mutation guard。
不要改成先读取再 `PatchAsync`,否则并发请求可能产生丢失更新或超卖。
原生标识符参数必须保持数据库类型。新增记录执行唯一性检查时没有“当前 ID”可排除,应直接省略 `id <> @id` 谓词及参数,不能用空字符串充当 UUID/`uniqueidentifier`/二进制标识符哨兵;更新时才用真实的强类型 ID 加入该谓词。这样 PostgreSQL、SQL Server、MySQL 语义一致,也不会产生 PostgreSQL `uuid <> text` 比较错误。
3.4 Action、Trigger 和提交链
Server Action 实现 `IServerActionsProvider`;Trigger 返回稳定结果,before trigger 可修改待保存字段,after trigger 不应假装回滚已经提交的事务。Action 参数先做 substitution,再解析类型。
`SetFields` 之类通用 Action 应区分 token 与普通字符串,例如 `@userId`、`@Datetime`、`@id`、密码哈希 token。Calculated/不可写字段需要明确的受信 Action 写入策略,不能简单开放所有客户端写入。
Trigger 的性能取决于依赖追踪:只在依赖字段变化或相应生命周期发生时求值,不因 hover 无关控件而重跑全表单验证。
3.5 新控件
控件至少要定义:runtime component、Designer preview、属性 schema、事件清单、默认值、数据/非数据标志和异步加载方式。使用 Vue 响应式状态,不用 `querySelector` 修改业务状态。确实需要 DOM 的焦点、尺寸、打印或第三方库集成要封装在 ref/lifecycle 中。
控件必须验证:
- Designer 不请求真实 API且外观不为空;
- Preview 与 Viewer 的 value/null/boolean 行为一致;
- `disabled`、`readOnly`、`required`、validation class 正确;
- 事件只出现在适用控件;
- 外来 attributes/listeners 通过 props/emits/`inheritAttrs` 正确处理;
- i18n、打印和布局容器下正常。
3.6 权限
先定义能力再写按钮。读取、设计、删除、管理提交/系统用户代填分别调用相应 ACL,并叠加服务器解析的部门范围;Respondent 路径还需 deployment/list/time-window 验证。客户端 `visibleCondition` 可以使用 context 权限改善体验,但服务器端检查不可省略。
3.7 Migration
每次 schema 变化增加新的 ID:
new ModuleDatabaseMigration(
"002_add_version",
["ALTER TABLE inventory_item ADD COLUMN IF NOT EXISTS version integer NOT NULL DEFAULT 1"],
["ALTER TABLE inventory_item ADD COLUMN IF NOT EXISTS version int NOT NULL DEFAULT 1"],
["IF COL_LENGTH(N'inventory_item',N'version') IS NULL ALTER TABLE inventory_item ADD version int NOT NULL CONSTRAINT df_inventory_version DEFAULT 1"])
不要改已经进入 `app_schema_migrations` 的内容,否则 checksum 会阻止启动。数据回填要考虑锁、批量和回滚策略。
3.8 中级练习
为初级练习增加库存调整 Server Action:事务内写库存和审计表,冲突返回 409;再做一个图表控件和 `/api/inventory/summary`,Designer 显示三条模拟数据;补 PostgreSQL 集成测试和 Playwright 保存流程。
4. 高级开发者:维护平台和扩展生态
4.1 目标能力
- 设计 SDK/Host 边界和兼容策略;
- 评审认证、ACL、多身份和数据泄露风险;
- 设计高数据量查询、缓存和异步工作;
- 维护跨 provider migrations;
- 建立可重复 CI、升级和回滚流程;
- 判断功能应进入表单、控件、官方服务还是客户模块。
4.2 边界决策
选择顺序:
1. 仅布局/字段/事件变化:编辑表单。
2. 多个表单需要相同交互:新控件或 Runtime API。
3. 需要数据库表、API、Action/Trigger:独立模块。
4. 所有安装都必须具备且涉及平台安全/生命周期:才考虑进入 Core。
公共 SDK 只暴露稳定契约,Host implementation 不进入模块引用。新增 SDK API 要考虑版本范围、XML 文档、二进制兼容和所有模块 CI。
`ISurveyOfflineGateway` 是“功能可选,但必须复用 Core 安全事务”的参考边界。SDK 1.7 保持原受访者包/提交接口稳定,继续使用独立的 `ISurveyOfflineFileGateway` 与 `ISurveyOfflineOptionGateway`,并新增 `ISurveyOfflineAssistanceGateway`,而不是弱化受访者认证。Host 负责 deployment/form ACL、目标受访者成员关系与部门授权、SystemValue/富文本处理、表单/选项指纹、响应乐观并发、选项键强制校验、文件内容验证、配额、审计身份和事务级幂等;`FormPlatform.Offline` 自己负责 PWA、绑定模式/所有者的包授权、加密、有效期和 Outbox。deployment/form/department/响应版本、操作员/目标身份、选项授权及配额都来自服务器授权记录或模块策略,不能把同步 payload 当作可信权限。模块不得复制 `SurveyApplicationService` 或引用 Host 程序集。
浏览器模块可以复用客户端扩展 API 暴露的稳定 `FormReader`。可信宿主可传入只存在于运行时的 `fileHandler`(`stage`、`preview`、`remove`)和 `optionHandler.query`;后者让 AsyncSelect/Tree 使用本地不可变的搜索/分页数据源,而不改变普通在线行为。这些回调绝不能序列化进表单 metadata。离线模块必须在联网时调用 `preloadFormComponents(schema)`;选项快照必须完整、有限额、有版本和过期时间、静态加密、同步时重新授权,无法安全重现动态 Filter 时必须失败关闭。
浏览器模块可以注册受访者 Dashboard 工具,并复用客户端扩展 API 暴露的稳定 `FormReader`。宣布包可离线以前,应在联网状态调用 `preloadFormComponents(schema)` 解析懒加载控件 chunk,不能依赖 Vite 文件名。离线应用外壳路由可以公开,以便断网启动;任何包/数据 API 仍必须自行认证和授权。生产离线功能遇到不支持的联网控件必须失败关闭,不能渲染残缺行为。
4.3 性能设计
- 用数据库端 ACL 候选过滤和 cursor pagination。
- Query projection 避免关联表 `SELECT *`。
- 批量接口避免 N+1 读取表单和 metadata。
- 全局表单 CSS 使用内容 hash/ETag,未变化不重建。
- 复杂通知、邮件、导出和大文件可迁到后台队列;请求链保持可取消。
- 日志结构化且限制敏感参数。
Keyset cursor 通常包含稳定排序键和 id;参照记录删除不会破坏语义,查询使用 `(sortKey,id) > cursor` 继续,但客户端必须接受并发插入/删除导致的“弱快照”。要求严格快照时使用事务快照或服务端导出任务。
4.4 安全设计
模块是可信同进程代码;不可信第三方需要独立进程/API,而不是当前 loader。上传文件验证大小、类型、访问部署和所有者,下载也重新鉴权。OIDC 使用 provider subject 建立内部 identity,不直接把 email 当稳定主键。密码只保存强哈希。
错误响应对 500 隐藏内部异常,详细堆栈只写受保护日志。配置采用 User Secrets/systemd credentials/环境变量;定期轮换数据库、SMTP、OIDC 和支付密钥。
系统用户与受访者 Cookie 有意共用 `App_Data/keys` Data Protection 密钥环。不同 Authentication Scheme 会形成不同的 Data Protection purpose,因此两类 ticket 不能互换。Windows 上新持久化的密钥由当前运行身份的 DPAPI 加密;已有明文密钥文件不会被重写,目录 ACL 仍是必须保留的安全边界。
4.5 跨 provider 与时间
逻辑时间点使用 `DateTimeOffset`/UTC 和 `timestamptz`;纯本地日期使用 Date。浏览器 `datetime-local` 没有 offset,服务器必须按明确时区转换。Provider migration 要同时考虑 PostgreSQL transactional DDL、MySQL implicit commit 和 SQL Server 条件 DDL。
4.6 发布与兼容
模块 manifest 版本必须匹配程序集 Major/Minor/Build。兼容范围 minimum inclusive、maximum exclusive。发布顺序通常是:备份、迁移验证、发布 Host/SDK、重编译所有模块、部署客户端资产、冒烟测试。不要只复制新 `module.json` 配旧 DLL。
4.7 高级评审清单
- 是否突破 SDK 边界或复制 Core implementation?
- 是否有无界查询、大 OFFSET、N+1 或全表序列化?
- 是否所有入口都鉴权并限制 owner/tenant?
- 是否统一错误/i18n/traceId?
- 是否有新的不可逆 migration 和恢复计划?
- Designer/Preview/Viewer/打印是否一致?
- PostgreSQL 集成和关键 Playwright 流程是否覆盖?
- 升级时旧表单、旧 module manifest 和缓存如何处理?
5. 团队工作流
每个改动先写清“表单、客户端、服务、数据库、权限、测试”的影响面。保持小提交,不混入无关格式化;不覆盖他人 dirty worktree。完成后更新中英文核心文档(若公共行为变化)和 `docs/OPENCODE_CHANGES.md`。
建议 Code Review 顺序:契约和安全 → 数据/migration → 领域逻辑 → API 错误 → Vue 响应式与 Designer → i18n/UX → 测试与部署。