FForm Platform
enzh-CN

离线问卷

为可靠的离线采集打包受支持的问卷控件、加密附件和代填流程。

离线问卷模块

`FormPlatform.Offline` 是面向已认证受访者及经 ACL 授权系统用户代填的可选可信模块。它下载绑定明确版本的问卷包,在 IndexedDB 中加密保存草稿和待同步响应,恢复网络后再通过 Survey 原有验证与持久化链路同步。卸载模块即可移除页面和 API;Host 只保留稳定网关及事务级幂等基础能力。

架构

实现分为三个边界:

1. `ISurveyOfflineGateway` 及保持原接口二进制兼容的独立能力 `ISurveyOfflineFileGateway`、`ISurveyOfflineOptionGateway` 继续作为受访者契约。SDK 1.7 新增独立的 `ISurveyOfflineAssistanceGateway`,不会弱化或复用受访者认证。Host 实现负责取得已授权部署、应用可信 SystemValue、清理富文本、通过规范查询服务捕获 Data Model 选项、校验准确表单/选项指纹,并调用原 Survey 响应/文件写入链路。

2. Core migration `FormPlatform.Core/015_survey_submission_idempotency` 与 `016_survey_offline_file_idempotency` 分别保存响应和文件幂等回执。响应回执与动态响应表变更在一个事务中提交;文件回执、Survey 文件记录及包配额增量也在一个事务中提交。相同请求重试返回原结果;任一客户端 UUID 被不同内容复用时返回 409。

3. `FormPlatform.Offline` 自己拥有包授权、有效期策略、API、PWA 资产、离线口令加密、本地草稿和 Outbox。模块 migration 为 `FormPlatform.Offline/001_offline_survey_package_grants`、`002_offline_option_snapshot_manifest` 和 `003_offline_assisted_entry`。003 加入明确授权模式和操作员绑定;权威目标仍是既有 `respondent_id`。显示名称只随浏览器加密包返回,不在授权表中重复保存。

模块不引用 `FormPlatform.Host.dll`,只依赖 `FormPlatform.Sdk` 与 `FormPlatform.Extension.Abstractions`。安装模块就是站点级的明确启用动作:它为受访者加入离线工具,并向系统菜单声明**离线代填**入口;每个包仍只允许当前身份可用的 deployment,并受下述失败关闭兼容面限制。不向 `survey_deployment` 或表单增加离线属性。

安全模型

  • 受访者包下载与同步必须通过隔离的 `Respondent` 认证策略。代填路由使用普通系统用户 Cookie 和独立服务器授权模式;两种模式不能相互读取、撤销、上传文件或同步对方的授权包。
  • 代填下载要求 Survey Responses 工作区 Read、目标 Survey Read、目标 Survey 代填权限(`submissions` 或 `SurveyAssistant` 默认值)及当前部门范围。同步和每次文件上传都要求同一操作员重新在线认证并重复这些检查。
  • 下载时执行原部署检查:当前时间窗口、受访者 Active、List Active、成员关系、deployment/list/respondent 部门完全一致,以及匿名/外部身份规则。
  • 同步时重新检查包所有者、授权模式、身份类型、撤销状态、包有效窗口、时钟偏差和上传宽限期;Host 再次检查成员关系、适用的 Active 状态、部门相等/范围、表单 ACL/类型、字段校验和跨记录规则。代填授权同时绑定操作员 ID 和目标受访者 ID,两个值都不来自浏览器同步正文。包还绑定 `survey_deployment.updated_at`;list、form、重复策略、时间窗口、名称或部门任何一项变化,都会让尚未提交成功的旧包失效。
  • `CompletedAt` 必须位于包签发窗口内且不能明显晚于服务器当前时间。它只用于判断部署时间窗口,不能提供权限或部门值。
  • 客户端不能决定部署、表单、部门、指纹、响应 ID、响应版本、配额或完成状态;这些值来自认证包授权记录或模块策略。答案同步只携带包 ID、幂等 ID、答案数据和客户端完成时间;原始文件端点接收不透明客户端文件 ID 与受限文件元数据,模块再从包授权构造全部可信网关上下文。
  • 表单与完整 metadata 一起计算 SHA-256。Schema、Mapping、Trigger 或行为 metadata 改变时,尚未提交成功的上传会返回 409,必须重新下载;已经事务提交的相同幂等 ID 在通过当前成员授权后,仍返回保存的第一次结果。
  • 每份 Data Model AsyncSelect/Tree 快照只包含规范选项服务在应用静态 Filter、引用过滤及适用部门约定后返回的记录。受访者模式沿用受访者端点约定;代填模式使用操作员当前允许部门 ID,与在线代填选项端点一致。SHA-256 版本同时绑定控件 ID/类型、答案属性、是否多选、表单指纹和完整有序选项集。授权清单保存在服务器包授权行中;同步时在当前范围下重新查询并核对浏览器版本表,拒绝版本变化或不属于授权集合的提交键。客户端过滤只改善体验,不构成安全边界。
  • 更新携带下载时看到的响应 `updated_at`;当前值不同就返回 409,不覆盖其他设备的修改。非重复问卷下载时尚无响应、但另一设备已新增响应,也返回 409。
  • 浏览器数据使用 AES-256-GCM 加密。包正文、草稿、响应数据、所有者信息、有效期、答案 Outbox metadata、文件 metadata 和每个文件 Blob 都在密文内;密文外只保留不透明 IndexedDB 记录 ID、设备 UUID、salt、IV 与 ciphertext。文件 metadata 与二进制使用不同 IV 和认证上下文。每份密文还把 store 名和记录 ID 作为附加认证数据,因此合法的包/Outbox 密文不能被移动到另一条本地记录。密钥由 8–64 个字符的离线口令经 PBKDF2-SHA-256、600,000 次迭代生成;口令和派生密钥都不持久化。敏感问卷应使用较长的口令短语,不要只用短数字 PIN。口令丢失后本地数据无法恢复;锁定页面仍提供明确的本机数据重置入口。
  • Attachment、Camera 和 Signature 文件在加密前计算 SHA-256。浏览器在落盘前检查包的文件数量、单文件字节数、总字节数、全局 MIME 及控件 `accept`;上传时模块重复检查大小与全局 MIME,Host 重算 SHA-256、按文件魔数或 Open XML 容器验证声明 MIME、核对准确控件及存储模式,并以数据库原子更新执行服务器包配额。客户端检查只改善体验,服务器检查才是安全边界。
  • 离线文件回执会让尚未关联响应的 Survey 文件保留到包同步宽限期结束,普通过期上传清理不会提前删除它们;窗口结束后,回执、未引用文件、媒体资产和无用配额行才进入正常清理范围。

浏览器加密只能保护磁盘中的静态数据,不能阻止同源恶意脚本。生产环境仍必须使用 HTTPS、严格 CSP、经过审核的可信模块和及时更新的浏览器。编译后的应用文件、模块文件、表单生成 CSS 和同源表单设计资产属于普通 Cache Storage,并不是加密答案;静态设计资产中不应放入受访者专属秘密。`CompletedAt` 必然只是客户端时钟声明:服务器会把它限制在签发包窗口、当前时间和允许偏差内,但纯离线软件无法证明受访者实际填写答案的物理时间,因此不能把它当作高可信法律时间戳。

下载与同步流程

1. 受访者从 Dashboard 打开 **离线**,建立或输入本机离线口令。

2. 联网时下载可用部署。服务器建立包授权,并在配置上限内捕获完整的已授权 AsyncSelect/Tree 选项;浏览器加密保存表单、选项快照与初始响应快照。

3. 客户端预加载 Schema 用到的全部受支持 FormReader 控件,并让模块 Service Worker 缓存当前 SPA 外壳、模块文件、生成的表单 CSS 和已加载控件 chunk。

4. 草稿与已选择文件都在本机自动加密保存。AsyncSelect、Tree 的搜索与分页只读取加密快照,绝不回退到网络 API。文件值刻意使用服务器无效的 `offline:*` 占位符,避免实现缺陷把它误当成正式文件 ID 提交。Pagination 正常的 `surveyProgress` 下一页事件也只保存本地草稿;只有完成响应及其准确选项版本表才会冻结为该包唯一的一个 Outbox 项。

5. 用户点击同步或页面在前台收到 `online` 事件后,先逐件解密并幂等上传引用文件,核对每个服务器 ID/SHA-256 回执,把冻结答案副本中的全部本地占位符替换为正式文件 ID,然后才提交答案。Survey API 响应绝不进入缓存。服务器按完成时间校验选项有效期,核对包绑定版本表、当前授权选项版本及每一个已选键。只有收到服务器规范结果才删除本地项。确定未提交的字段/文件/选项值校验失败会解冻队列并保留草稿供修正;网络结果不确定和冲突错误仍保持冻结。

6. 如果数据库已经提交但 HTTP 响应丢失,未变化的 Outbox 会用同一个客户端 UUID 重试,并获得第一次已经提交的结果。

7. 浏览器收到成功结果后,该下载包标记为已同步并转为只读。再次填写前需移除并重新下载,以取得最新响应版本和表单指纹。

已排队的答案数据刻意保持不可变。同一个幂等 UUID 下不能改写不确定请求;需要放弃时应移除并重新下载该包。

支持范围

可离线使用的本地控件包括:Input(文件类型除外)、Textarea、Radio、Dropdown、Checkbox、RichText、Table、Spreadsheet、Pagination、Tabs、GridLayout、Header/静态内容、二维码、标签/消息/统计、静态或表单数据图表、地理定位、SystemValue、Attachment(`FileUpload`)、Camera、Signature,以及使用 Data Model 与静态 Filter 的 AsyncSelect/Tree。FileUpload/Camera/Signature 必须使用 `auto` 或 `survey` 存储模式;custom、form media 和 manual 模式会让下载失败关闭。Image 控件只允许空来源或内嵌的 `data:image/*` 来源。

遇到以下功能,下载会失败关闭:

  • DataGrid、ItemRenderer;
  • 使用 `apiUrl` 的 AsyncSelect,或没有 Data Model 的 AsyncSelect/Tree;
  • 没有 `parentIdField` 的 Tree;
  • Filter 包含运行时 `{property}` 替换的 AsyncSelect/Tree,因为一份不可变下载无法安全重现依赖答案变化的查询;
  • 完整授权结果超过所配置单控件、整包记录数或字节数的选项控件/包;系统绝不签发截断的选项快照;
  • 文件类型 Input(请改用 FileUpload/Attachment);
  • 引用网络资源的 Image 控件;
  • 使用 form-name 或 JSON 来源、而不是普通包内 children 的 CustomBlock 控件;
  • Menu、Breadcrumb、SystemSlot、未知扩展控件;
  • 非空的表单 Action Code;
  • 除本地 `validateForm()` 和 Button 点击的 `submitForm()` 外的已配置控件动作、Pagination action-chain 完成模式,以及非 `surveyProgress` 的 submit-on-next 模式。
  • 配置为 API 数据模式的图表。

这些能力分别需要不可变选项/数据快照或可执行代码版本协议。模块不会显示一个看似可用但实际残缺的离线表单。受支持控件上的自定义验证、可见性和只读表达式必须是纯本地逻辑,只能依赖包内表单数据/context;会调用网络服务的表达式不兼容离线运行。SystemValue 的界面值来自下载快照,同步时服务器一定重新计算;因此 Server Time 表示同步时间。若业务需要实际完成时间,应另设由受访者填写或专门定义的完成时间字段。

受支持控件仍须按离线场景设计:选项和图表数据必须内嵌或来自表单本身;地理位置取决于浏览器/设备能力;远程图片和字体不会进入缓存。只有同源编译资产、模块资产、表单生成资产及 Service Worker allow-list 明确允许的资源才属于离线外壳。Chart.js 与延迟加载的图表控件 chunk 一起打包,因此模块预加载器可在断网前缓存它。

配置

模块自己的 `appsettings.offline.json`:

{
  "OfflineSurveys": {
    "PackageLifetimeHours": 168,
    "SyncGracePeriodHours": 720,
    "AllowedClockSkewMinutes": 5,
    "MaximumPackagesPerRespondent": 50,
    "MaximumPackagesPerOperator": 500,
    "OptionSnapshotLifetimeHours": 72,
    "MaximumOptionSnapshotRecordsPerControl": 1000,
    "MaximumOptionSnapshotRecordsPerPackage": 5000,
    "MaximumOptionSnapshotBytesPerPackage": 2097152,
    "MaximumAttachmentsPerPackage": 25,
    "MaximumAttachmentBytes": 10485760,
    "MaximumTotalAttachmentBytes": 52428800,
    "AllowedAttachmentMimeTypes": [
      "image/jpeg",
      "image/png",
      "image/webp",
      "image/gif",
      "image/heic",
      "image/heif",
      "application/pdf",
      "text/plain",
      "text/csv",
      "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
      "application/vnd.openxmlformats-officedocument.presentationml.presentation"
    ]
  }
}

`PackageLifetimeHours` 最高限制为 90 天,并会缩短到部署结束时间。`MaximumPackagesPerRespondent` 限制为 1–500;`MaximumPackagesPerOperator` 限制为 1–5,000,因为一台现场设备可能有意携带许多明确选择的代填包。`SyncGracePeriodHours` 允许在包/部署填写窗口结束后上传,但响应声明的完成时间仍必须位于原窗口内;撤销及当前成员/授权检查始终优先。

选项快照在包到期时间与 `OptionSnapshotLifetimeHours` 两者中较早者到期;该配置限制在 1–2,160 小时。单控件记录数限制为 1–5,000,整包为 1–25,000,序列化字节数为 64 KiB–16 MiB;默认分别是 1,000、5,000、2 MiB 和 72 小时。过期判断使用 `CompletedAt`,因此到期前已经完成的响应仍可在正常同步宽限期内上传;但同步时仍会重查选项,版本变化会返回 409。提高上限前必须评估离线数据暴露、下载大小、IndexedDB 容量及查询成本。

附件限制随服务器签发的包响应下发,并在上传时重复执行,浏览器不能自行提高。允许的 MIME 必须属于 Host 已实现内容验证的类型。`MaximumAttachmentBytes` 应不大于 Host 的 `SurveyFiles:MaxFileSizeBytes`。若文件已上传、随后答案校验失败,这些未关联文件在保留窗口结束前仍计入该包服务器配额;这是为了防止反复替换文件绕过总容量上限。

HTTP API

受访者路由要求隔离的 Respondent 策略。代填路由要求普通系统用户认证,并继续执行在线代填使用的工作区/表单 ACL。

方法路由用途
`GET``/api/offline/packages`列出当前受访者仍有效的服务器包授权
`POST``/api/offline/packages`为一个 deployment/form/device 签发版本化包
`DELETE``/api/offline/packages`撤销当前受访者身份的全部包授权
`DELETE``/api/offline/packages/{packageId}`撤销当前受访者拥有的包
`POST``/api/offline/packages/{packageId}/files/{clientFileId}`幂等上传一个绑定 SHA-256 的加密 Outbox 原始文件
`POST``/api/offline/sync`同步一个版本绑定、幂等的响应
`POST``/api/offline/assisted/packages`为一个明确选择、已分配的受访者和设备签发代填包
`DELETE``/api/offline/assisted/packages`撤销当前系统用户拥有的全部代填授权
`DELETE``/api/offline/assisted/packages/{packageId}`撤销当前系统用户拥有的一个代填授权
`POST``/api/offline/assisted/packages/{packageId}/files/{clientFileId}`在当前操作员/ACL/范围检查后上传一个代填包文件
`POST``/api/offline/assisted/sync`以授权行绑定的目标受访者身份同步一个代填响应

`/offline`、`/offline/assisted`、`/offline-sw.js`、`/offline.webmanifest` 和 `/offline-assisted.webmanifest` 是公共应用外壳资源,因此两种加密工作区断网时都能启动。公共外壳不公开包数据,也不会让任何 API 匿名。本地记录在密文内包含授权模式/所有者绑定,每个页面只显示和同步自己的模式。解锁状态清除页面时会撤销并删除该模式的授权/数据;锁定状态紧急重置因为无法在不知道口令时安全分类密文,会清除全部本地加密数据。无法连接的服务器授权按正常有效期失效。Service Worker 使用当前 PathBase 作为 scope,以 network-first 更新白名单静态资产,只对两个离线应用路由回退缓存外壳,并且不缓存 Survey API 响应;加密业务数据只保留在 IndexedDB。

服务器撤销只能阻止后续同步,无法远程擦除浏览器中已经下载且仍可用口令解密的包。若运维政策要求清除本地数据,应在卸载模块前让受访者执行**清除此设备数据**。删除模块文件后,在线应用下一次刷新会移除实时路由/API,但浏览器仍可能保留加密 IndexedDB 记录和以前缓存的静态文件,直至清除站点数据;无法连接的授权不能再写入服务器,并会按有效期失效。

构建与部署

构建升级后的 Host/SDK 和模块,然后只部署模块程序集与包文件,不能把模块输出目录中的私有 `FormPlatform.Sdk.dll` 一起复制:

Modules/FormPlatform.Offline/
  FormPlatform.Offline.dll
  FormPlatform.Offline.deps.json
  module.json
  appsettings.offline.json
  client/*

重启 FormPlatform。启用 migration 时,会先应用 Core `015`、Core `016` 和模块 `001`、`002`、`003`。Host 会自动发现 `client/index.js`,并把模块拥有的代填入口投影到 `SystemLeftPane`;无需修改 `Program.cs`、SPA 固定路由或 `ClientModules` 配置。

应在 HTTPS(或浏览器认可的安全 localhost)验证受访者模式:以 Active 受访者登录,下载受支持部署,至少联网访问一次 `/offline`,断网后刷新,确认 AsyncSelect/Tree 搜索、分页和选择没有网络请求,再拍摄 Camera 图片、绘制 Signature、选择允许与拒绝的 Attachment 文件,填写并排队,恢复网络同步。还要覆盖上限、选项到期/变化、伪造键、先文件后响应、幂等重试、版本变化、包过期/撤销、Respondent/List 停用和跨公司根拒绝。

另行验证代填模式:给一个非管理员配置 Survey Responses Read、目标 Survey Read 和 `submissions`(或使用 `SurveyAssistant`),打开 `/offline/assisted`,选择范围内 deployment/respondent 并下载。断网后刷新代填路由,不使用受访者凭据即可解锁、填写和排队。恢复网络后先以另一个系统用户登录,确认同步被拒;再以原操作员登录,确认成功,并检查 `survey_response_audit.actor_type = system_user`、`actor_id` 是原操作员、`respondent_id` 是目标受访者。分别在撤销表单 ACL、删除 deployment 成员关系、缩小操作员部门范围、改变选项集/表单/deployment/响应以及选择另一公司根受访者后重试,均须失败关闭。还要验证代填 Attachment/Camera/Signature 上传,以及受访者端点不能使用代填包 ID(反向也一样)。

离线代填模式

离线代填已作为独立授权模式在 `/offline/assisted` 启用。联网时,系统用户选择一个当前有效、范围内的 deployment,再明确选择其中已分配的 respondent;每条授权都由服务器而不是浏览器解析并保存操作员 ID、目标受访者 ID、部门、deployment/form/响应版本、选项授权、设备、有效期和文件策略。目标显示名称只进入浏览器加密包。模块菜单只对能读取 Survey Responses 的用户显示,而 API 还要求目标 Survey Read 和代填权限。一次搜索最多显示 500 名受访者;更大的名单可用搜索框缩小,然后单个下载,或选择本页进行逐个独立鉴权的顺序批量签发。遇到失败时批次停止,已经在本机加密保存的包不会回滚。

包在本机加密后,现场收集既不需要受访者凭据,也不要求操作员保持在线会话。设备口令只是本地资料库秘密,不是 FormPlatform 登录凭据,也不是可转交的服务器凭据。恢复网络后,必须通过普通系统登录认证授权行绑定的同一操作员。每个文件和响应请求都会重新检查工作区/表单 ACL、当前部门范围、deployment 成员关系、目标绑定、包时间窗口、版本、选项快照及所有规范 Survey 校验。审计行用 `actor_id` 保存系统操作员,用 `respondent_id` 保存答案所代表的人。

受访者授权与代填授权使用不同的服务器路由、所有者条件和授权模式值。同步正文不能选择目标受访者,受访者包也绝不会被当作 bearer package。系统不会缓存密码或受访者凭据。如果另一名操作员要接手,应由原操作员撤销/移除旧包,再由新操作员在自己的授权和审计身份下重新签发。