release(main): 同步 dev 最新候选改动
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled

This commit is contained in:
Cheng Zhou
2026-07-16 17:15:50 +08:00
parent 32167fba02
commit d5279b124f
393 changed files with 51630 additions and 9711 deletions
+2 -1
View File
@@ -4,8 +4,9 @@ CTMS 文档入口只展示当前仍会影响开发、发布和运维决策的内
## 当前约束
- [`desktop-project-plan.md`](desktop-project-plan.md): 桌面端 Tauri 项目边界、阶段计划与必读约束
- [`../AGENTS.md`](../AGENTS.md): Agent 执行约束、桌面端当前边界、分支治理和常用质量门禁
- [`desktop-phase-1-design.md`](desktop-phase-1-design.md): 桌面端第一阶段 macOS 在线客户端详细方案
- [`desktop-local-cache-plan.md`](desktop-local-cache-plan.md): 桌面端在线辅助缓存、请求去重、失效和后续持久化缓存边界
- [`branch-governance.md`](branch-governance.md): 长期分支治理规则
- [`guides/release-checklist.md`](guides/release-checklist.md): 发布前检查项与回归门禁
- [`guides/client-release.md`](guides/client-release.md): Web/桌面端统一版本、构建与发布流程
@@ -2,9 +2,9 @@
状态: `active`
适用范围: Web 与 macOS Desktop 统一客户端发布
最后更新: `2026-07-01`
最后更新: `2026-07-14`
本清单用于第一、二阶段桌面端能力完成后的准发布稳定化。它不引入离线登录、本地业务数据存储、内嵌后端服务或离线同步。
本清单用于第一、二阶段桌面端能力完成后的准发布稳定化。当前允许按 `docs/desktop-local-cache-plan.md` 引入在线辅助本地缓存,但不引入离线登录、离线写入、本地业务权威数据、内嵌后端服务或离线同步。
## 1. 发布链路门禁
@@ -28,10 +28,12 @@ npm run desktop:build:app
- [ ] `frontend/package.json``package-lock.json`、Tauri 配置、Cargo manifest/lock 版本一致。
- [ ] `VITE_BUILD_CHANNEL=release``VITE_BUILD_COMMIT=<release tag commit>` 由 CI 注入,且 `npm run release:env:check` 通过。
- [ ] 在正式 release tag 和签名环境中执行 `npm run desktop:release-readiness:check`,确认 tag、构建元数据、签名/公证变量、updater 私钥和生产 artifact HTTPS 基址齐备。
- [ ] macOS app 已签名和公证。
- [ ] updater `.sig` 使用组织 CI secret 或密钥库中的私钥生成,私钥未进入仓库。
- [ ] 设置 `TAURI_SIGNING_PRIVATE_KEY``TAURI_SIGNING_PRIVATE_KEY_PASSWORD` 和 Apple 签名/公证变量后,以 `REQUIRE_DESKTOP_SIGNING=true` 再次执行 `npm run release:env:check`,随后执行 `npm run desktop:build -- --bundles app`
- [ ] 正式 updater feed 执行 `npm run desktop:update-feed:check -- --feed <latest.json> --artifacts-dir <artifact-dir>`
- [ ] 设置 `TAURI_SIGNING_PRIVATE_KEY``TAURI_SIGNING_PRIVATE_KEY_PASSWORD` 和 Apple 签名/公证变量后,以 `REQUIRE_DESKTOP_SIGNING=true` 再次执行 `npm run release:env:check`,随后执行 `npm run desktop:build:macos-release -- --ci`
- [ ] 正式 updater feed 执行 `npm run desktop:update-feed:create -- --artifact <CTMS.app.tar.gz> --base-url <versioned-https-artifact-prefix> --output-dir <release-dir>` 生成 `latest.json``SHA256SUMS.txt`
- [ ] 正式 updater feed 执行 `npm run desktop:update-feed:check -- --feed <release-dir>/latest.json --artifacts-dir <release-dir>`,并确认 checksum manifest、updater artifact、`.sig``latest.json` 均通过校验。
- [ ] 不可变制品先上传,`latest.json` 最后原子替换;若 feed 校验未通过,不替换线上 `latest.json`
- [ ] Web 与 Desktop 制品记录同一产品版本、Git 标签和完整提交 SHA。
@@ -42,20 +44,39 @@ npm run desktop:build:app
- [ ] Tauri bundle 启用 `app``dmg` 和 updater artifacts。
- [ ] updater public key 已配置。
- [ ] CSP 禁止 wildcard source、`unsafe-eval`、宽泛 HTTP API 访问和 `object-src`
- [ ] `script-src` 保持仅允许自身;ONLYOFFICE 仅在 `frame-src` 增加 HTTPS 和 localhost/127.0.0.1 开发来源。
- [ ] ONLYOFFICE frame 地址只由 `frontend/src/runtime/onlyoffice.ts` 使用已校验服务器 origin 与固定 `/onlyoffice-host.html` 生成。
- [ ] ONLYOFFICE 宿主消息同时校验 `origin``source` 和一次性内存 nonce,配置不进入本地缓存或请求去重。
- [ ] Tauri capability 不包含 shell 权限、持久文件系统 scope 或宽泛目录读写。
- [ ] 文件系统与 opener scope 只允许 `$TEMP/ctms-desktop/**`
- [ ] Tauri capability 不包含 `remote` 授权,远程 ONLYOFFICE frame 无文件、通知、对话框或 opener 权限
- [ ] 文件系统仅开放临时文件所需的 read/write/mkdir/remove 命令,且文件系统与 opener scope 只允许 `$TEMP/ctms-desktop/**`
- [ ] 单实例插件先于其他桌面插件注册。
- [ ] Tauri command 白名单仅包含凭据和更新命令
- [ ] macOS 首个顶层 submenu 为应用菜单,包含关于、设置、服务、隐藏和退出;文件菜单保持独立
- [ ] macOS 红色按钮保持真正关闭窗口的系统语义,Dock/Finder reopen 事件在需要时重建、显示并聚焦主窗口。
- [ ] 桌面快捷键只通过受控 command 同步到原生菜单,不向 WebView 开放原始窗口或菜单控制权限。
- [ ] 桌面主题支持跟随系统、明亮和暗黑,并通过受控 command 同步窗口外观。
- [ ] Tauri command 白名单仅包含凭据、更新和已审计的桌面 UI 窄命令。
- [ ] 前端源码不通过 query string 传递 token。
- [ ] `ctms_token` 只允许由 `secureSessionStorage` 处理。
- [ ] 登录表单密码不写入 `localStorage``sessionStorage`;Web 端只使用浏览器凭据管理能力,Desktop 端只使用系统凭据库。
- [ ] 本地缓存能力只通过 `frontend/src/runtime/desktopDataCache.ts` 或后续同名 runtime 入口暴露,业务模块不直接调用 Tauri 存储 API、SQLite、IndexedDB、Cache Storage 或文件系统。
- [ ] 本地缓存命名空间包含 server origin、user id 和 cache schema version。
- [ ] 本地缓存不保存 token、密码、Authorization/Bearer 文本、下载凭据、临时授权 URL 或系统通知正文。
- [ ] 系统通知只能通过 `frontend/src/runtime/notifications.ts` 发送,标题和正文保持通用。
- [ ] 通知 capability 只暴露权限查询、权限请求和发送通知,不使用 `notification:default`
- [ ] opener capability 只允许打开 `$TEMP/ctms-desktop/**` 下的临时文件,不开放 URL 或 reveal 权限。
- [ ] updater capability 不直接暴露给 WebView,自动更新只走受控 Tauri command。
- [ ] 更新弹窗 release notes 过滤 URL、token 查询参数和 Authorization/Bearer 形态文本。
- [ ] CI release 候选 workflow 包含 version/runtime/desktop/ui/type/unit/build/desktop app smoke 门禁。
- [ ] signed macOS release candidate workflow 只允许从 `vX.Y.Z` tag 运行,并包含签名环境检查、Universal macOS 构建、update feed 生成、checksum 校验和 verified release directory 上传。
人工复审还必须确认:
- [ ] token 不出现在 URL、日志、系统通知正文、下载链接或持久化业务缓存中。
- [ ] 密码不出现在 URL、日志、系统通知正文、诊断信息或明文浏览器存储中。
- [ ] 桌面端通知正文只显示通用内容,不包含项目、文件或版本详情。
- [ ] 服务端权限、审计和业务数据持久化仍由 FastAPI 后端裁决。
- [ ] 本地缓存只作为服务端响应副本,mutation、审批、权限判断和审计判断仍以后端结果为准。
- [ ] Web 运行时不直接导入 Tauri API。
## 3. 端到端回归矩阵
@@ -63,16 +84,27 @@ npm run desktop:build:app
| 场景 | Web | macOS Desktop | 预期 |
| --- | --- | --- | --- |
| 登录与项目恢复 | 必测 | 必测 | 登录成功后恢复可访问项目;401 后重新登录 |
| 记住密码 | 必测 | 必测 | Web 使用浏览器凭据管理/自动填充;Desktop 使用系统凭据库;未勾选时不继续写入保存密码 |
| 30 天免登录 | 不适用 | 必测 | 关闭并重启 App 后复用系统凭据库中的后端在线会话;超过 30 天或 `/me` 校验失败后重新登录 |
| 服务器地址未配置 | 不适用 | 必测 | 自动进入服务器设置,不进入业务页 |
| 服务器地址切换 | 不适用 | 必测 | 清除当前会话和项目上下文,要求重新登录 |
| 服务端不可达 | 必测 | 必测 | 显示可恢复错误,不进入离线模式 |
| 本地缓存命中 | 必测 | 必测 | `/me` 校验通过后可先展示缓存再后台刷新 |
| 本地缓存清理 | 必测 | 必测 | 登出、切换服务器、切换用户、401/403 后旧命名空间不可读取 |
| mutation 缓存失效 | 必测 | 必测 | 新增、编辑、删除或审批成功后相关列表、详情、统计和图表缓存失效 |
| 附件上传 | 必测 | 必测 | Web 使用浏览器文件选择,Desktop 使用原生选择 |
| 附件下载/保存/打开 | 必测 | 必测 | 使用 Authorization header;无 `?token=` |
| Office 附件/版本只读预览 | 必测 | 必测 | 独立全幅页面;不下载到本地;无编辑、复制、打印、下载入口;标签失活和服务器切换立即销毁 |
| ONLYOFFICE 服务不可用 | 必测 | 必测 | 显示可重试错误,不回退 iframe 或自动下载;原保存/打开功能不受影响 |
| 临时文件清理 | 不适用 | 必测 | 启动时清理 `$TEMP/ctms-desktop/**` |
| 系统通知开启 | 不适用 | 必测 | 用户主动开启后请求 OS 权限并创建订阅 |
| 系统通知拒绝 | 不适用 | 必测 | 开关回退,提示系统权限未开启 |
| 通知领取与 ack | 不适用 | 必测 | 显示成功后 ack;失败等待租约重试 |
| 单实例重复启动 | 不适用 | 必测 | 恢复、显示并聚焦主窗口 |
| 关闭窗口后 Dock 重开 | 不适用 | 必测 | 红色关闭按钮真正关闭主窗口;点击 Dock 后重建、显示并聚焦 |
| 原生应用菜单 | 不适用 | 必测 | CTMS、文件、编辑、显示、导航、窗口和帮助菜单顺序及职责符合 macOS 习惯 |
| 自定义快捷键同步 | 不适用 | 必测 | 设置修改后原生菜单立即显示并响应新的后退、前进和刷新快捷键 |
| 跟随系统主题 | 不适用 | 必测 | 系统明暗模式变化时 WebView、标题栏和系统控件同步更新 |
| 自动更新检查 | 不适用 | 必测 | release 通道按当前 CTMS origin 派生清单 |
| 更新稍后提醒 | 不适用 | 必测 | 同版本 24 小时内不重复提示 |
| 更新安装失败 | 不适用 | 必测 | 不打断业务录入,显示可排障错误 |
@@ -80,20 +112,26 @@ npm run desktop:build:app
## 4. 桌面体验验收
- [ ] 登录页显示当前桌面服务器地址,长 URL 不撑破登录面板。
- [ ] 30 天免登录仍只保存系统凭据库会话记录,不把 token 写入 URL、日志、通知正文或业务缓存。
- [ ] 记住密码与 30 天免登录使用独立凭据记录;服务器切换后不复用旧服务器保存的密码。
- [ ] 服务器设置页显示当前服务器、连接检查状态、HTTP 错误、超时和网络失败原因。
- [ ] 个人中心显示客户端类型、版本、平台、构建通道、提交、服务器和能力状态。
- [ ] 个人中心可复制诊断信息,内容不包含 token 或业务敏感数据。
- [ ] 系统偏好或个人中心提供本地缓存记录数、容量和最近清理时间,且支持手动清理。
- [ ] 通知开关显示 OS 权限状态。
- [ ] 手动检查更新能反馈“已是最新版本”、未启用更新或检查失败。
- [ ] 关键弹窗、表单、按钮在最小窗口尺寸 `1180x760` 下不重叠、不溢出。
- [ ] “设置…”位于 macOS CTMS 应用菜单并使用 `⌘,`;退出位于应用菜单而不是文件菜单。
- [ ] “显示”和“导航”菜单中的快捷键与桌面偏好保存值一致。
- [ ] 更新弹窗只显示版本、发布日期和通用 release notes,不展示 token、下载链接或业务详情。
## 5. 不允许项
- [ ] 不实现离线登录、离线浏览、离线队列或离线同步。
- [ ] 不在桌面端保存 CTMS 业务数据副本
- [ ]内嵌 FastAPI、PostgreSQL、SQLite 或本地业务 API 镜像
- [ ]绕过后端做本地权限裁决或本地审计回放
- [ ] 不实现离线登录、离线写入、离线队列或离线同步。
- [ ] 不在 `/me` 校验通过前展示业务缓存
- [ ]把本地缓存作为 CTMS 业务权威数据或本地优先数据源
- [ ]内嵌 FastAPI、PostgreSQL 或本地业务 API 镜像
- [ ] 不绕过后端做本地权限裁决、本地审计判定或审计回放。
## 6. 2026-07-01 收尾验证记录
@@ -125,3 +163,101 @@ npm run desktop:build:app
- 签名后的 updater artifacts、`.sig`、checksum manifest 和 `latest.json` 在真实发布目录内通过 `npm run desktop:update-feed:check`
- 不可变制品上传完成后,再原子替换线上 `latest.json`
- Desktop 端到端人工回归矩阵、最小窗口体验验收和系统通知/自动更新真实环境验证。
## 7. 2026-07-02 发布稳定化推进记录
本次推进补齐了发布链路自动化,不改变桌面端产品边界:
- 新增 `npm run desktop:build:macos-release`,封装 Universal macOS `app`/`dmg` release candidate 构建命令。
- 新增 `npm run desktop:update-feed:create`,从签名 updater artifact 和 `.sig` 生成 `latest.json`、复制发布目录文件并生成 `SHA256SUMS.txt`
- `npm run desktop:update-feed:check` 在传入 `--artifacts-dir` 时要求并校验 `SHA256SUMS.txt`
- 新增 `npm run desktop:release-readiness:check`,在正式签名候选构建前检查 release tag、构建元数据、签名/公证变量、updater 私钥和生产 artifact HTTPS 基址。
- 新增 `.github/workflows/desktop-release-candidate.yml`,在 release tag 上执行签名候选构建、feed 生成、feed 校验并上传 verified release directory。
- `npm run desktop:release:check` 已检查上述脚本和 workflow,避免发布链路回退。
仍未自动完成、正式发布前必须人工确认:
- Apple Developer 凭据、证书、签名身份、公证结果和组织 updater 私钥。
- 生产下载源的不可变制品上传和线上 `latest.json` 原子替换。
- 真实环境下的自动更新安装、系统通知、单实例和完整人工回归。
## 8. 2026-07-02 端到端回归优化记录
本轮端到端优化仍保持在线桌面客户端边界,不引入离线、本地业务存储或本地权限裁决。
已完成的自动化收口:
- 服务器地址切换时,桌面设置页调用 `auth.logout({ rememberCurrentStudy: false })`,避免退出时把旧服务器项目记入当前用户的最近项目;随后继续清除当前项目上下文。
- 系统偏好连接页与独立服务器设置页保持同一切换服务器语义,切换时不记忆旧服务器项目并清除当前项目上下文。
- 系统通知轮询在部分通知显示失败或系统通知未实际发起时,先 ack 已成功发起系统通知的通知,再让失败项通过租约重试,贴合“显示成功后 ack;失败等待重试”的回归预期。
- 自动更新管理器新增稍后提醒 24 小时抑制、安装失败可重试、检查失败不打断业务和未启用更新状态的单元覆盖。
- 附件 API 新增 blob 下载、multipart 上传和删除端点单元覆盖,确保下载凭据继续由 axios Authorization header 承载而不是进入 URL。
- 文件任务反馈 helper 新增选择、保存、取消保存和打开的单元覆盖,约束桌面保存/打开继续走 `frontend/src/runtime/` 适配层。
- Keychain/凭据库会话新增旧浏览器 token 迁移、未配置服务器不读取凭据、本地 30 天上限、服务器切换删除旧服务器凭据,以及恢复 token 必须先经 `/me` 校验的单元覆盖。
- 桌面发布门禁新增单实例重复启动处理校验,要求重复启动时恢复、显示并聚焦 `main` 窗口。
- 会话刷新后的跨窗口 token 更新不再写入 `localStorage` fallback,只通过内存态 BroadcastChannel 通知,避免 token 进入明文广播缓存。
- `desktop:release:check` 新增 session broadcast 静态门禁,防止 `TOKEN_UPDATED` payload 回退写入 `localStorage`
- 新增相关单元测试覆盖服务器切换不记忆旧项目、通知权限未授权不领取、部分通知失败时只 ack 成功项、系统通知未发起时不 ack、自动更新失败恢复路径、附件文件流契约、30 天在线会话恢复边界、单实例恢复行为和 token 广播存储边界。
仍需人工或真实环境验证:
- Keychain/凭据库 30 天在线会话恢复。
- 原生附件上传、下载、保存和打开。
- 系统通知拒绝路径的 OS 级交互。
- 单实例重复启动聚焦主窗口。
- 签名 release 构建下的自动更新 feed、验签、安装和重启实物流。
## 9. 2026-07-02 安全边界复审记录
本轮安全复审在端到端自动化收口之后推进,不改变桌面端在线客户端边界。
已完成的安全边界收口:
- Tauri notification capability 从 `notification:default` 收敛为 `notification:allow-is-permission-granted``notification:allow-request-permission``notification:allow-notify`
- `desktop:release:check` 新增 capability 最小化约束,拒绝 `notification:default`、opener URL/reveal 权限和 WebView 直连 updater 权限。
- `desktop:release:check` 将 token URL 检查扩展到 `token``access_token` 查询参数,并扩大日志敏感词检查到 `token``access_token``authorization``bearer`
- 更新弹窗 release notes 增加清理逻辑,过滤 URL、token 查询参数、`access_token``Authorization``Bearer` 形态文本,避免 feed 内容把下载链接或凭据样式文本带入 UI。
- 凭据库 Rust 单测新增带凭据 server origin 拒绝,以及 Keychain/Credential Manager account 不暴露原始服务器 origin 的覆盖。
仍需人工复审确认:
- 真实生产 release notes 内容保持通用,不写入项目、文件、下载链接或敏感业务详情。
- 正式签名、公证和 updater feed 环境继续使用组织 secret,不在日志、artifact 或配置中泄露私钥材料。
## 10. 2026-07-02 桌面体验收口记录
本轮体验收口聚焦登录、服务器设置、个人中心诊断、系统偏好和最小窗口布局稳定性,不改变业务能力边界。
已完成的体验收口:
- 个人中心新增只读客户端诊断信息,展示客户端类型、版本、平台、构建通道、提交、服务器和能力状态,并支持复制诊断信息。
- 登录页在桌面最小窗口附近收紧左右分栏 padding 和卡片宽度,长服务器地址继续在登录面板内换行,不撑破布局。
- 服务器设置页增加面板内滚动和健康检查 URL 换行约束,避免 `1180x760` 下长 URL 或错误信息溢出。
- 系统偏好通知/更新控制区允许换行,长状态说明使用 `overflow-wrap`,避免按钮和权限状态标签在窄视口重叠。
- 个人中心对话框内容区改为内部滚动,诊断值使用强制换行,避免新增诊断信息后超过最小窗口高度。
- 通知权限运行时改为优先读取 WebView `Notification.permission``granted`/`denied` 状态;请求权限返回 `default` 时保持“待授权”,不再误标为“已拒绝”。
- 系统偏好通知页已移除授权成功态的固定标签文案,开启状态只由开关表达;待授权、已拒绝和不可用状态继续显示提示标签。
- 系统偏好通知页新增“测试通知”命令,完整走 `frontend/src/runtime/notifications.ts` -> `@tauri-apps/plugin-notification` 的系统通知发送链路;测试通知文案为固定通用内容,不包含项目、文件、token 或其他敏感业务信息。
- `showSystemNotification` 和测试通知调用会在发送前确认系统通知权限,只有真正发起系统通知时才返回成功;桌面通知轮询据此只 ack 已实际发起系统通知的后端通知。
- 真实 macOS `.app` 已验证系统偏好通知开关可调用系统通知权限并生效。验证截图见 `/Users/zcc/Library/Application Support/CleanShot/media/media_boKznpjOwt/CleanShot 2026-07-02 at 11.07.52@2x.png`
- `Layout.desktop.test.ts` 新增静态契约覆盖个人中心诊断、登录页最小窗口断点、服务器设置滚动和偏好页控制区换行约束。
- `notifications.test.ts` 新增授权、拒绝、待授权、取消授权请求和测试通知发送链路的单元覆盖。
仍需人工体验验收:
- 在真实 macOS `.app` 中以 `1180x760` 检查登录页、服务器设置、个人中心、系统偏好和更新弹窗无重叠、无横向溢出。
- 在真实系统通知权限拒绝流程中确认权限状态提示、开关回退和错误提示与 OS 状态一致。
## 11. 2026-07-13 macOS 原生体验适配记录
本轮继续保留 Tauri 和共享 Vue 前端,不引入 Swift 业务 UI,也不改变 Windows 内测边界:
- macOS 新增标准 CTMS 应用菜单,将关于、设置、服务、隐藏和退出等项目收敛到首个应用 submenu;文件、编辑、显示、导航、窗口和帮助菜单保持独立。
- macOS 红色关闭按钮保持真正关闭主窗口的系统语义;Tauri `RunEvent::Reopen` 在 Dock/Finder 再激活时从受控配置重建、显示并聚焦主窗口,重复启动继续复用同一恢复 helper。
- 新增受控 `desktop_menu_set_shortcuts` command,只接受后退、前进和刷新三项经过双层校验的快捷键,并同步原生菜单 accelerator。
- 桌面主题新增“跟随系统”,WebView 监听 `prefers-color-scheme`,受控 `desktop_window_set_theme` command 同步 Tauri 窗口外观;未向 WebView 增加原始窗口权限。
- `desktop:release:check` 已增加应用菜单、Dock reopen、关闭后重建、快捷键同步、系统主题和新增 command 白名单约束。
真实 `.app` 仍需人工验证:红色按钮真正关闭主窗口后 Dock 重建、菜单顺序、个人中心自身关闭按钮、编辑菜单文本输入行为、自定义快捷键即时同步,以及系统明暗模式切换时的标题栏一致性。
本轮未引入以下条件性 Swift 能力:Quick Look 需先确认附件审阅频率和 Windows 降级语义;Touch ID 会改变现有 30 天会话恢复体验,需先确定凭据访问控制策略;通知点击回跳需先定义固定、无敏感信息的动作和目标页面。三项均不是当前发布前置条件。
+180
View File
@@ -0,0 +1,180 @@
# CTMS 桌面端本地缓存执行方案
## 目标
桌面端新增本地缓存的目标是降低重复网络请求、改善页面回访和 App 重启后的等待时间。当前项目不涉及受试者隐私问题,后端返回的 CTMS 业务 GET 数据均可进入本地缓存候选范围,但缓存仍只是服务端响应副本,不是业务权威数据。
本方案不改变以下边界:
- 登录、会话恢复和用户身份确认仍必须依赖后端 token 校验和 `/me`
- 新增、编辑、删除、审批、导入、导出、通知 ack、权限判断和审计判断仍必须以后端结果为准。
- 不实现离线登录、离线写入队列、冲突解决、本地优先工作流、本地 API 镜像或内嵌后端服务。
- token、登录密码、附件下载凭据、临时授权 URL、Authorization/Bearer 文本不得写入本地缓存、URL、日志或系统通知正文。
## 缓存范围
默认可缓存:
- 字典、枚举、菜单、权限摘要、用户可见组织/中心、应用配置、系统元数据。
- 项目、任务、机构、用户、角色、报表、监控页面等业务 GET 响应。
- 页面查询结果、分页列表、详情页只读数据和图表数据。
默认不缓存:
- POST、PUT、PATCH、DELETE 等 mutation 请求响应,除非只用于立即失效相关 GET 缓存。
- 文件下载二进制正文、一次性下载链接、带签名的临时 URL。
- token、密码、验证码、MFA 信息、Authorization header、Cookie 原文。
- 系统通知正文和可能包含临时凭据的 release notes 或错误日志。
## 架构约束
- 统一入口命名为 `desktopDataCache`,位于 `frontend/src/runtime/`,并通过 `clientRuntime` 暴露。
- 业务模块不得直接调用 Tauri 存储 API、文件系统、SQLite、IndexedDB 或 Cache Storage 来实现桌面缓存。
- Web 与 Desktop 共用业务代码,平台差异只在 runtime 适配层之后收敛。
- Tauri command 只做窄职责存储读写、清理、容量统计或目录定位,不包含 CTMS 业务规则。
- 如果引入新的 Tauri command、capability、CSP 或存储插件,必须同步评估 `frontend/scripts/verify-desktop-release.mjs``npm run runtime:check` 和桌面发布检查清单。
## 缓存命名和数据模型
缓存命名空间必须包含:
- `serverOrigin`:规范化后的后端服务地址,不包含用户名、密码、token 或 query 凭据。
- `userId`:后端 `/me` 确认后的用户标识。
- `schemaVersion`:缓存结构版本,用于升级时整体失效。
- `requestSignature`:请求方法、路径、排序后的 query、稳定序列化后的 body 摘要和必要的业务上下文。
每条缓存记录至少包含:
- `payload`:后端响应副本。
- `status`:HTTP 状态码,仅缓存成功的可展示响应。
- `headers`:只保存白名单响应头,例如 `ETag``Last-Modified``Cache-Control`、业务版本头。
- `createdAt``updatedAt``expiresAt``lastAccessedAt`
- `sourceEndpoint``cacheTags`,用于诊断和精准失效。
## 缓存策略
第一优先级是请求减量:
- 同一会话内相同 GET 请求合并为一个 in-flight promise。
- 页面返回、tab 切换和重复组件挂载时优先命中短时内存缓存。
- 页面可使用 stale-while-revalidate:先显示本地副本,再后台刷新,刷新失败时保留旧数据并显示可恢复错误。
第二优先级是持久化缓存:
- 仅在后端会话恢复和 `/me` 成功后启用业务缓存读取。
- 默认缓存 GET 响应;mutation 成功后通过 tags 或 endpoint 规则失效相关 GET 缓存。
- TTL 先按数据类型配置,缺省值建议 5 分钟;字典和元数据可延长到 24 小时;高频业务列表建议 1 到 5 分钟。
- 设置总容量上限和 LRU 清理策略,避免缓存无限增长。
第三优先级是 HTTP 条件请求:
- 后端为稳定数据提供 `ETag``Last-Modified`
- 客户端对有验证器的缓存请求带 `If-None-Match``If-Modified-Since`
- 收到 304 时刷新缓存元数据,不重复下载 payload。
## 失效和清理
必须整体清理:
- 用户登出。
- 切换服务器。
- 切换用户。
- 后端返回 401 或 403。
- `/me` 返回的用户标识、角色、权限版本或租户上下文变化。
- `schemaVersion` 升级。
必须精准失效:
- mutation 成功后,清理相关详情、列表、统计和图表缓存。
- 权限、角色、组织、项目状态变化后,清理依赖这些上下文的页面缓存。
- 后端返回明确业务版本头或失效事件时,按 tags 清理。
应提供用户入口:
- 系统偏好或个人中心诊断信息展示缓存大小、记录数、最近清理时间。
- 提供“清理本地缓存”按钮。
- 清理提示只描述缓存状态,不包含具体业务内容。
## 分阶段实施
### 当前落地状态
2026-07-08 已完成阶段 1 的首批实现:
- 新增 `frontend/src/runtime/desktopDataCache.ts`,提供内存级缓存 scope、TTL、tag 失效、清理和统计能力,并通过 `clientRuntime.dataCache` 暴露。
- `frontend/src/api/axios.ts` 已支持 GET 并发去重、显式 GET 内存缓存、mutation 成功后默认清理缓存、401/403 清理缓存、服务器切换清理缓存。
- 缓存清理、scope 切换和 tag 失效会递增 generationGET 响应返回时若 generation 已变化,不再写入缓存,避免旧用户、旧服务器或 mutation 前响应回填到新缓存空间。
- `/api/v1/auth/login-key` 明确禁用 GET 去重,避免复用登录 challenge。
- GET 去重和缓存 key 已包含 schema version、server baseURL、请求参数、非敏感请求头、responseType、paramsSerializer 和 withCredentials;包含敏感 header 或 axios auth 的请求不会进入去重或缓存。
- 登录态已在 `/me` 成功后绑定缓存 scope,登出时清空 scope 和缓存。
- 首批缓存接入范围包括项目列表/详情、项目配置、权限摘要、系统权限、权限模板、机构、成员、项目概览和仪表盘摘要。
- `npm run runtime:check``npm run desktop:release:check` 已增加底层缓存 API 边界检查,业务模块不得直接调用 `indexedDB``caches.open``CacheStorage` 或 SQLite 入口。
仍未完成:
- 持久化缓存尚未落地,当前缓存只存在于本次前端运行内存中。
- 后端 `ETag` / `Last-Modified` 和客户端 304 处理尚未落地。
- 系统偏好或个人中心缓存诊断与手动清理入口尚未落地。
- mutation 失效当前以默认全量清理为主,后续再按 tags 收敛为精准失效。
### 阶段 0:请求观测和缓存清单
- 梳理 `frontend/src/api/`、Pinia store 和高频页面的 GET 请求。
- 标记每个接口的缓存 tags、TTL、mutation 失效关系和是否需要后端 ETag。
- 记录启动、登录后首页、常用列表和详情页的请求数量作为基线。
### 阶段 1:请求去重和内存缓存
- 在 API 客户端层增加 GET 请求 in-flight 去重。
- 增加短 TTL 内存缓存,先覆盖字典、菜单、权限摘要、组织/中心和常用列表。
- 增加单元测试覆盖请求合并、TTL 过期、mutation 后失效。
### 阶段 2runtime 持久化缓存
-`frontend/src/runtime/desktopDataCache.ts` 定义平台无关接口。
- Web 端提供内存或 IndexedDB 实现;Desktop 端通过 runtime 封装 IndexedDB、Cache Storage 或 Tauri app data 存储。
- 增加命名空间、容量限制、LRU、schema version、手动清理和诊断 API。
- 更新 `runtime:check` 和桌面发布检查,禁止业务页面直接使用底层缓存存储。
### 阶段 3:页面接入
- 先接入基础数据和全局布局依赖接口。
- 再接入项目列表、任务列表、报表图表、详情页只读数据。
- 为 mutation 建立集中失效规则,避免页面各自手写清理逻辑。
### 阶段 4:后端条件请求
- 为稳定 GET 接口补 `ETag``Last-Modified`
- 前端 API 客户端接入 304 处理。
- 增加后端和前端测试,确认 304 不改变业务数据语义。
### 阶段 5:诊断、验收和发布门禁
- 在个人中心或系统偏好加入缓存诊断和清理入口。
- 验证登出、切换服务器、切换用户、401/403、权限变化和版本升级清理行为。
- 执行相关质量门禁,并把新增缓存边界纳入发布检查清单。
## 验收标准
- App 重启并完成 `/me` 校验后,已访问过的常用页面可以先展示缓存再后台刷新。
- 同一路由重复进入、组件重复挂载、多个组件请求同一资源时,不产生重复并发请求。
- 登出、切换服务器、切换用户、401/403 后不能读取旧命名空间缓存。
- mutation 成功后相关列表、详情、统计和图表缓存被失效或刷新。
- token、密码、Authorization/Bearer、下载凭据不会出现在缓存文件、IndexedDB、日志、URL 或通知正文中。
- 业务模块不直接调用 Tauri API、IndexedDB、SQLite、Cache Storage 或文件系统实现缓存。
## 质量门禁
本地缓存相关实现至少执行:
```bash
cd frontend
npm run runtime:check
npm run desktop:release:check
npm run ui:contract
npm run type-check
npm run test:unit
npm run build
```
如果新增或修改后端条件请求、缓存头、权限版本或失效事件,还需执行对应后端测试。若新增 Tauri command、capability、CSP 或存储插件,还需执行 `npm run desktop:build:app` 并在真实 macOS `.app` 中验证缓存清理和诊断入口。
+6 -4
View File
@@ -34,13 +34,15 @@
- macOSKeychain。
- WindowsCredential Manager。
桌面端登录使用后端签发的在线会话 token,可在系统凭据库中保存最长 30 天,以支持重启 App 后免输入密码。启动恢复后仍必须使用后端 token 校验和 `/auth/me` 用户状态校验;服务端不可达或会话被后端拒绝时不得进入离线模式。
Rust 仅暴露固定 service 下的读取、写入、删除命令。凭据 account 使用规范化服务端 origin 的 SHA-256,避免明文服务端地址散落在系统凭据项名称中。
应用挂载前异步初始化 token
1. Web 端继续读取 `localStorage.ctms_token`
2. 桌面端先删除 legacy `localStorage.ctms_token`
3. 若 legacy token 仍有效,则迁移系统凭据库。
3. 若 legacy token 仍有效,则迁移为带 30 天本机到期时间的系统凭据库会话记录
4. 若迁移或读取凭据失败,则内存 token 置空并要求重新登录,不回退明文存储。
登出、服务器切换、认证失效时必须同步清除内存 token 和当前服务端 origin 对应的系统凭据。
@@ -75,12 +77,12 @@ API
- `POST /api/v1/desktop-notifications/ack`
- `POST /api/v1/desktop-notifications/{distribution_id}/read`
claim 跨有效项目查询匹配当前用户或角色的活动文件分发,使用五分钟租约、唯一约束和事务防重。客户端显示系统通知后 ack;显示失败则等待租约到期后重试。
claim 跨有效项目查询当前用户未读且未解决的通用业务提醒,使用五分钟租约、唯一约束和事务防重。客户端显示系统通知后 ack;显示失败则等待租约到期后重试。系统通知只是通用提醒 Feed 的可选投递通道,不单独维护文件分发提醒来源。
系统通知正文只显示通用内容:
- 标题:`CTMS 文件更新`
- 正文:`有新的文件版本待查看`
- 标题:`CTMS 待办提醒`
- 正文:`有新的业务提醒待查看`
项目、文件、版本等详细信息仅在应用内列表显示,避免锁屏泄露。
-144
View File
@@ -1,144 +0,0 @@
# CTMS 桌面端项目计划书
## 目的
本文档是 CTMS 桌面端工作的长期方向约束。每次开始任何桌面端相关任务前,都必须先阅读本文档,包括 Tauri 初始化、macOS 打包、Windows 适配、桌面端存储、文件集成、系统通知、安全边界和发布流程。
桌面端必须服务于现有 CTMS Web 应用和后端架构。目标是为当前 CTMS 服务提供原生桌面入口,而不是创建一个独立的离线产品。
## 不可突破的边界
- 技术路线固定为 Tauri。
- 第一开发目标是 macOS 桌面端。
- Windows 仍只作为第二阶段兼容性验证目标,未获明确批准前不发布正式安装包。
- 当前桌面端工作只允许在第一、二阶段边界内做修复、稳定化、体验收口和发布准备,不新增第三阶段能力。
- 不做离线功能。
- 不在桌面 App 内嵌本地后端服务。
- 不在桌面 App 内嵌或分发本地数据库来保存 CTMS 业务数据。
- 不实现离线同步、冲突解决、本地业务数据队列、本地优先工作流。
- 不把 CTMS 业务 UI 拆成一套独立的桌面端产品;除非桌面能力确实需要小范围适配层。
- FastAPI 后端仍是业务权威来源。权限裁决、审计判断、认证、业务数据持久化都保持在服务端。
## 当前技术基线
桌面端工作基于当前 CTMS Web 技术栈:
- 前端:Vue 3、Vite、TypeScript、Element Plus、Pinia、Vue Router、Axios、ECharts。
- 后端:FastAPI、Uvicorn、SQLAlchemy、Alembic。
- 数据库:PostgreSQL。
- 当前部署:Docker Compose、nginx 托管前端静态资源、nginx 反代 `/api`
桌面端应复用现有前端代码和 API 契约。任何共享适配层都应保持小而明确,并可测试。
## 已完成阶段边界
第一阶段 macOS 在线桌面壳和第二阶段原生能力主体改造已经形成。详细历史方案见 [`desktop-phase-1-design.md`](desktop-phase-1-design.md) 和 [`desktop-phase-2-design.md`](desktop-phase-2-design.md)。
后续不再按第一阶段空白项目初始化 Tauri,也不再扩展第二阶段以外的新桌面产品能力。当前允许推进的工作仅包括:
- 修复既有 Tauri、运行时适配层、文件、通知、凭据、更新、菜单/快捷键和打包问题。
- 稳定 Web 与 Desktop 共用业务代码,确保平台差异继续收敛在 `frontend/src/runtime/` 后面。
- 完成 macOS 正式发布前的签名、公证、updater 签名、制品发布、CI 门禁和人工回归。
- 做 Windows 第二阶段兼容性验证,但不发布正式 Windows 安装包。
仍然不允许:
- 离线登录、离线浏览、离线队列、离线同步或本地优先工作流。
- 本地 PostgreSQL、SQLite、IndexedDB 业务数据缓存或本地 API 镜像。
- 内嵌 Python/FastAPI 后端服务。
- 绕过后端做本地权限裁决、本地审计缓存或审计回放。
- 为桌面端复制或重写一套独立业务 UI。
## 架构方向
使用运行时适配层,不在业务页面里散落平台判断。
当前已采用并必须继续保持的适配层边界:
- `platform`:识别 Web、macOS 桌面端、Windows 桌面端。
- `apiBaseUrl`:分别解析 Web 和桌面端的服务端 API 地址。
- `desktopServerConfig`:管理桌面服务端地址配置和切换事件。
- `secureSessionStorage`:隔离浏览器 token 存储与桌面系统凭据库。
- `files`:隔离浏览器上传下载与原生文件能力。
- `notifications`:隔离 Web 通知与桌面系统通知。
- `updates`:隔离桌面自动更新检查与安装入口。
- `appMetadata`:在可用时提供桌面 App 版本、平台、构建通道。
- `desktopMenu``desktopUiPreferences`:承接桌面菜单命令、最近访问和收藏等桌面体验状态。
- `clientRuntime`:作为业务侧获取平台能力的聚合入口。
业务模块应调用这些适配层,而不是直接调用 Tauri API。Tauri command 应保持窄职责,不包含 CTMS 业务规则。
## 安全与合规方向
- 将桌面端视为受监管业务系统的在线客户端。
- 非本地服务连接优先使用 HTTPS。
- 认证与授权决策保留在后端。
- 审计敏感决策保留在后端。
- 敏感凭据必须继续使用明确批准的安全存储方案。
- 不向前端暴露宽泛文件系统访问权限。
- Tauri 权限保持最小化,并按功能精确授权。
- 每个新增 Tauri command 都需要被视为桌面端安全边界的一部分进行审查。
## 分支与工作区
桌面端工作在以下位置开发:
- Worktree`/Users/zcc/MyCTMS/ctms-dev/worktrees/ctms-desktop`
- 短期分支:Agent 默认使用 `codex/<任务名称>`,也可按任务类型使用 `feature/*``fix/*``docs/*``release-prep/*`;所有普通桌面端工作都必须从最新 `dev` 创建
当前代码状态:Tauri 基线和第二阶段原生能力已经形成,后续桌面端工作默认是第二阶段范围内的修复、稳定化、体验收口和发布准备。`codex/ctms-desktop` 是历史临时集成分支,不再作为当前工作线;不得继续向该分支提交、变基或推送新的桌面端工作,除非用户明确要求做收尾或删除分支。
## 2026-07-01 审查结论与后续方向
本轮审查结论:桌面端已经完成从 Tauri 接入基线到第二阶段原生能力的主体改造,不再按空白桌面项目推进。当前重点不是扩展离线或本地业务能力,而是围绕既有在线桌面客户端做准发布稳定化。
已形成的能力边界:
- Tauri 工程、macOS App/DMG/updater artifacts 配置已经存在。
- `frontend/src/runtime/` 已作为平台能力统一入口,业务代码不得绕过该适配层直接使用 Tauri API。
- 桌面服务器地址、API baseURL、客户端元数据请求头、安全 session 存储、原生文件能力、系统通知、单实例、菜单/快捷键和自动更新入口已经形成。
- 后端已经包含桌面通知订阅/投递状态、相关 API 和客户端诊断请求头支持。
- 已有 `npm run version:check``npm run runtime:check``npm run desktop:release:check` 等门禁用于约束版本、运行时边界和桌面发布安全边界。
后续优化优先级:
1. 发布稳定化:补齐 macOS 签名、公证、组织 updater 私钥签名、release tag 构建变量注入、不可变制品上传和 `latest.json` 原子替换。
2. 端到端回归:按 `docs/audits/desktop-release-stabilization-checklist.md` 覆盖服务器配置、服务器切换清会话、Keychain/凭据库、附件上传下载、系统通知、单实例和自动更新失败恢复。
3. 安全复审:持续确认 token 不进入 URL、日志、系统通知正文、下载链接或明文持久化;Tauri command、capability、CSP 和 updater 改动必须同步评估发布门禁。
4. 桌面体验收口:重点检查登录、服务器设置、个人中心诊断信息、通知开关、更新弹窗和最小窗口 `1180x760` 下的布局稳定性。
5. CI 与发布流程:Web 与 Desktop 必须从同一提交、同一语义化版本号和同一正式标签构建;发布候选应执行本文档列出的相关质量门禁。
6. Windows 兼容验证:仅作为第二阶段兼容性目标,验证 Credential Manager、路径处理、通知/updater 编译、WebView2 和安装器假设;未获明确批准前不发布正式 Windows 安装包。
如果后续任务试图新增离线登录、本地业务数据存储、内嵌后端、本地业务队列、离线同步或绕过后端权限审计,应先修改并评审本计划书,不能直接实现。
## 当前质量门禁
前端或桌面端代码变更应按影响范围执行相关检查。发布、桌面端适配层、Tauri 配置或安全边界相关变更至少考虑:
```bash
cd frontend
npm run version:check
npm run runtime:check
npm run desktop:release:check
npm run ui:contract
npm run type-check
npm run test:unit
npm run build
npm run desktop:build:app
```
正式桌面发布构建仍必须使用组织批准的 updater 签名私钥和 Apple 签名/公证流程;未签名或 ad-hoc 构建只能作为内部验证构建描述。文档-only 变更可以不执行完整代码门禁,但必须在结果说明中明确未运行。
## 每次开发前必须执行的检查
开始任何桌面端相关任务前:
- 先阅读本文档。
- 确认任务属于第一阶段或第二阶段。
- 确认任务不会引入离线能力。
- 确认实现不会破坏 Web 运行时。
- 确认 Tauri API 使用被隔离在适配层之后,除非有明确记录的理由。
- 确认不会重复初始化 Tauri 或绕过既有 `frontend/src/runtime/` 运行时边界。
- 涉及 Tauri 权限、CSP、updater、凭据、文件或通知能力时,确认桌面发布检查脚本和发布清单是否需要同步更新。
如果用户请求与本文档冲突,先停止实现并确认范围,不要直接推进。
+23 -4
View File
@@ -147,12 +147,12 @@ bash scripts/install.sh release --base-url https://ctms.example.com
```text
1. 检查 docker、docker compose、openssl、curl。
2. 检查 .env已存在则保留不改,不存在则新建。
3. 新建 main/release 的 .env 时自动生成 JWT 和 RSA 私钥。
2. 检查 .env保留已有身份与登录密钥,补齐并默认启用标准 ONLYOFFICE 配置,不存在则新建。
3. 所有环境自动生成独立的 ONLYOFFICE JWT 与稳定实例标识;新建 main/release 的 .env 时同时生成登录 JWT 和 RSA 私钥。
4. main/release 先构建并执行 backend-init,确保数据库迁移使用当前代码中的 Alembic revision。
5. 启动容器;默认执行 docker compose up -d --build--skip-build 时执行 docker compose up -d
5. 默认构建并启动数据库、后端、前端/Nginx 和 ONLYOFFICEONLYOFFICE 不再使用可选 Compose Profile
6. 执行 docker compose run --rm backend python -m alembic upgrade head。
7. 检查容器状态、后端环境、/health、/api/v1/auth/login-key。
7. 检查全部容器状态、后端环境、/health、/api/v1/auth/login-key 和 /onlyoffice/healthcheck
```
更新进入“执行部署更新”后会持续显示 Docker 镜像拉取与构建进度:交互终端使用滚动实时输出窗口,CI、远程面板或管道环境直接流式输出构建日志,并在两种模式下显示累计耗时。
@@ -164,6 +164,7 @@ git checkout
git pull
docker compose down -v
删除 pg_data
删除 backend/app/uploads
删除数据库 volume
自动安装系统依赖
```
@@ -194,6 +195,12 @@ ENV=development
JWT_SECRET_KEY=dev-secret
LOGIN_RSA_KEY_ID=default
LOGIN_RSA_PRIVATE_KEY=
ONLYOFFICE_ENABLED=true
ONLYOFFICE_JWT_SECRET=<替换为独立的随机密钥,至少 32 个字符>
ONLYOFFICE_INTERNAL_URL=http://onlyoffice
ONLYOFFICE_STORAGE_BASE_URL=http://backend:8000
ONLYOFFICE_INSTANCE_ID=ctms-dev-<替换为随机实例标识>
ONLYOFFICE_CONFIG_TTL_SECONDS=300
EOF
```
@@ -315,6 +322,12 @@ ENV=production
JWT_SECRET_KEY=<替换为 JWT 密钥>
LOGIN_RSA_KEY_ID=main-YYYYMMDD
LOGIN_RSA_PRIVATE_KEY=<替换为 RSA 私钥单行文本>
ONLYOFFICE_ENABLED=true
ONLYOFFICE_JWT_SECRET=<替换为不同于 JWT_SECRET_KEY 的独立随机密钥>
ONLYOFFICE_INTERNAL_URL=http://onlyoffice
ONLYOFFICE_STORAGE_BASE_URL=http://backend:8000
ONLYOFFICE_INSTANCE_ID=ctms-main-<替换为随机实例标识>
ONLYOFFICE_CONFIG_TTL_SECONDS=300
EOF
```
@@ -494,6 +507,12 @@ ENV=production
JWT_SECRET_KEY=<替换为生产 JWT 密钥>
LOGIN_RSA_KEY_ID=release-YYYYMMDD
LOGIN_RSA_PRIVATE_KEY=<替换为生产 RSA 私钥单行文本>
ONLYOFFICE_ENABLED=true
ONLYOFFICE_JWT_SECRET=<替换为不同于 JWT_SECRET_KEY 的独立生产随机密钥>
ONLYOFFICE_INTERNAL_URL=http://onlyoffice
ONLYOFFICE_STORAGE_BASE_URL=http://backend:8000
ONLYOFFICE_INSTANCE_ID=ctms-release-<替换为随机实例标识>
ONLYOFFICE_CONFIG_TTL_SECONDS=300
EOF
```
+2 -2
View File
@@ -106,8 +106,8 @@ npm run desktop:build:app
npm run desktop:build:app
```
正式桌面发布构建仍必须设置 updater 签名私钥执行
`npm run desktop:build -- --bundles app`
正式桌面发布构建仍必须设置 updater 签名私钥和 Apple 签名/公证变量后,先执行
`npm run desktop:release-readiness:check`,再执行 `npm run desktop:build:macos-release -- --ci`
后端改动应补充执行受影响模块的后端测试、迁移检查和接口回归。
+34 -4
View File
@@ -30,12 +30,19 @@ The runtime contract currently provides:
- file picker/save/open adapters
- desktop system notification adapters
- desktop updater adapters
- native desktop application menu and shortcut synchronization
- desktop window lifecycle and system appearance synchronization
Business modules must use this public runtime entry point. They must not inspect
Tauri globals or import Tauri packages directly. Native files, notifications,
secure session storage, and automatic updates must remain behind
`frontend/src/runtime/` and explicit capability flags.
Desktop menu accelerators and window appearance are synchronized through narrow
Tauri commands owned by `frontend/src/runtime/`. macOS close/reopen behavior is
handled by the Tauri event loop so a genuinely closed main window can be rebuilt from the Dock;
these concerns must not move into Vue business modules.
## Version Change
Update every client manifest with one command:
@@ -58,6 +65,10 @@ Manual edits that leave these files inconsistent fail CI.
## Stabilization Gates
Client builds require Node.js 22.13.0 or newer. The development frontend container,
Web CI, macOS release candidates, Windows internal validation, and the production
Web image use the same Node 22.13 baseline.
Client release candidates must pass the shared Web checks and the Desktop
release/security gate before promotion:
@@ -122,8 +133,8 @@ The release pipeline must:
2. build macOS Universal desktop artifacts;
3. sign and notarize the macOS app;
4. produce updater artifacts and `.sig` files with the updater private key;
5. generate `latest.json` and a checksum manifest;
6. verify the feed with `npm run desktop:update-feed:check -- --feed <latest.json> --artifacts-dir <artifact-dir>`;
5. generate `latest.json` and a checksum manifest with `npm run desktop:update-feed:create`;
6. verify the feed with `npm run desktop:update-feed:check -- --feed <release-dir>/latest.json --artifacts-dir <release-dir>`;
7. upload immutable artifacts first;
8. atomically replace `latest.json` last.
@@ -131,6 +142,13 @@ For Universal macOS artifacts, `latest.json` must provide both
`darwin-aarch64` and `darwin-x86_64` entries pointing at the same Universal
update package.
The signed release candidate workflow lives at
`.github/workflows/desktop-release-candidate.yml`. It must be run from a
matching `vX.Y.Z` tag and produces a verified release directory as a GitHub
artifact. That artifact is still only a release candidate; the release owner
must upload immutable files to the production download origin and replace
`latest.json` atomically after validation.
## Windows Build Readiness
Second-phase Windows work is limited to CI compatibility validation. A
@@ -147,6 +165,16 @@ Windows validation must cover:
- notification and updater compilation;
- future code-signing requirements.
The manual internal Windows validation workflow lives at
`.github/workflows/desktop-windows-internal.yml`. It is `workflow_dispatch`
only, runs on `windows-latest`, injects `VITE_BUILD_CHANNEL` and
`VITE_BUILD_COMMIT` from the selected branch context, executes the shared client
and Desktop safety gates, builds an unsigned NSIS installer with updater
artifacts disabled, and uploads the `.exe` plus `SHA256SUMS.txt` as a GitHub
Actions artifact. This workflow is for internal compatibility verification
only; it must not generate `latest.json`, update feeds, signed release
directories, or formal Windows release artifacts.
## Required Checks
```bash
@@ -167,8 +195,10 @@ export TAURI_SIGNING_PRIVATE_KEY="$UPDATER_PRIVATE_KEY"
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$UPDATER_PRIVATE_KEY_PASSWORD"
export REQUIRE_DESKTOP_SIGNING=true
npm run release:env:check
npm run desktop:build -- --bundles app
npm run desktop:update-feed:check -- --feed src-tauri/target/release/bundle/latest.json --artifacts-dir src-tauri/target/release/bundle
npm run desktop:release-readiness:check
npm run desktop:build:macos-release -- --ci
npm run desktop:update-feed:create -- --artifact <CTMS.app.tar.gz> --base-url <versioned-https-artifact-prefix> --output-dir <release-dir>
npm run desktop:update-feed:check -- --feed <release-dir>/latest.json --artifacts-dir <release-dir>
```
The Desktop build must run on macOS for the current first-phase target. A signed
+47
View File
@@ -0,0 +1,47 @@
# ONLYOFFICE 标准组件运行说明
ONLYOFFICE Document Server 是 CTMS 的标准部署组件,用于附件和文档版本预览,以及共享库在线协作编辑。PDF、图片、另存为和桌面端“打开”仍使用各自原有链路。
## 默认安装
执行标准安装即可同时安装 CTMS 与 ONLYOFFICE
```bash
bash scripts/install.sh dev
```
安装脚本会自动完成以下工作:
- 首次安装时生成独立的 256-bit 随机 `ONLYOFFICE_JWT_SECRET`,写入根目录 `.env`,且不在终端回显。
- 自动生成稳定、按部署环境隔离的实例标识并写入 `.env`
-`.env` 权限设置为 `0600`;该文件已被 Git 忽略。
- 默认构建并启动 backend、Document Server 与 Nginx,不再使用可选 Compose Profile。
- 将 Document Server 纳入容器状态、后端配置和 HTTP 健康检查。
- 后续启动默认复用密钥,防止后端和 Document Server 因签名密钥漂移而无法通信。
开发环境需要单独重建、验证或主动轮换密钥时,可使用兼容维护脚本:
```bash
bash scripts/onlyoffice-dev-up.sh
bash scripts/onlyoffice-dev-up.sh --rotate-secret
```
轮换会强制重建相关容器,已签发但尚未使用的短时预览配置会立即失效。生产环境不使用该自动生成流程,仍必须由部署密钥管理系统显式提供密钥。
可用 `ONLYOFFICE_IMAGE` 覆盖默认的 `onlyoffice/documentserver:9.4.0.1`。默认派生镜像只增加 Noto Sans CJK/Noto Serif CJK,不包含微软字体。
## 配置边界
- Compose 安装中的 `ONLYOFFICE_ENABLED` 默认 `true`;安装或更新旧环境时也会自动启用。
- `ONLYOFFICE_INTERNAL_URL` 默认 `http://onlyoffice`,只供后端健康探测。
- `ONLYOFFICE_STORAGE_BASE_URL` 默认 `http://backend:8000`,只允许后端生成固定的内部文件地址。
- `ONLYOFFICE_CONFIG_TTL_SECONDS` 默认 300 秒,允许范围为 60–900 秒。
- 内部内容接口不经过 Nginx 公网入口,仅接受 `AuthorizationJwt`,且 JWT 必须绑定到请求的精确 URL。
- 配置响应禁止缓存;JWT、内部文件 URL 和磁盘路径不得进入日志、审计详情或桌面缓存。
- `onlyoffice_data``onlyoffice_lib``onlyoffice_logs` 不是 CTMS 业务权威数据,不纳入业务备份或恢复来源。
## 生产部署要求
标准生产安装同样包含 ONLYOFFICE,并自动生成独立随机密钥和稳定、唯一的 `ONLYOFFICE_INSTANCE_ID`。正式上线前仍必须确认商业许可或 Community 版本授权边界、批准镜像、容量与备份监控策略,并完成 Web、macOS 和 Windows 内测端的真实 DOCX/XLSX/PPTX/WPS/ET/DPS 预览验证。
故障回退可临时关闭 `ONLYOFFICE_ENABLED`;下一次标准安装或更新会按标准组件策略重新启用。回退不得影响附件下载、另存为、打开、PDF 或图片预览。Community 与商业版本的授权边界以 [ONLYOFFICE 官方许可说明](https://helpcenter.onlyoffice.com/docs/faq/docs-community.aspx) 为准。
+1
View File
@@ -11,6 +11,7 @@
## 1.1 容器构建校验
- [ ] `docker compose config` 渲染成功
- [ ] 后端 `/code/app/uploads` 已持久化挂载到宿主机 `backend/app/uploads`
- [ ] `docker compose build backend frontend` 成功
- [ ] `docker compose run --rm --build backend-init` 成功
@@ -0,0 +1,94 @@
# 系统监测简易运维指南
## 定位与访问边界
系统监测用于单实例或小规模 CTMS 部署的日常巡检、访问追踪和初步故障定位,不替代集中式指标、日志和告警平台。监测页面及 `/api/v1/permission-monitoring/*` 仅允许系统管理员访问,项目 PM 不具备系统级监测权限。
## 健康检查
- `GET /health`:进程存活探针,不访问数据库,适合判断 HTTP 服务是否仍在响应。
- `GET /readyz`:服务就绪探针,会执行数据库查询;数据库不可用时返回 `503`。Nginx、安装脚本和后端容器健康检查均使用或暴露此探针。
- `GET /api/v1/permission-monitoring/health`:管理员健康详情,包含权限检查质量、缓存、进程内告警、数据库延迟、权限/安全日志写入队列、留存任务和监测表数据量。
部署后的最小检查:
```bash
curl -fsS http://127.0.0.1:8888/health
curl -fsS http://127.0.0.1:8888/readyz
docker compose ps
```
## 数据留存
后台任务在应用启动后立即清理一次,之后按固定间隔执行。默认策略:
- 权限访问日志和安全访问日志:90 天。
- 权限小时指标:400 天。
- 账号登录活动:180 天。
- 清理周期:86400 秒(每天)。
可通过后端环境变量调整:
```text
MONITORING_ACCESS_LOG_RETENTION_DAYS=90
MONITORING_METRIC_RETENTION_DAYS=400
MONITORING_RETENTION_INTERVAL_SECONDS=86400
MONITORING_IP_GEO_FALLBACK_ENABLED=true
MONITORING_IP_GEO_FALLBACK_TIMEOUT_SECONDS=2.5
MONITORING_IP_GEO_FALLBACK_CACHE_SECONDS=604800
MONITORING_IP_GEO_FALLBACK_MAX_LOOKUPS=10
USER_LOGIN_ACTIVITY_RETENTION_DAYS=180
USER_SESSION_ONLINE_SECONDS=300
```
访问日志留存范围为 7–3650 天,指标留存范围为 30–3650 天,清理周期范围为 60–604800 秒。修改后需重启后端。正式环境部署新版本前必须先执行 Alembic migration。
账号管理页的“在线”状态由服务端会话心跳计算:最近 `USER_SESSION_ONLINE_SECONDS` 秒内成功心跳且未退出的会话视为在线。登录记录保存客户端类型、版本、服务端观测到的登录 IP、登录/最近活动/退出时间;IP 属地由本地离线数据库按需解析,不发送到外部服务。登录记录接口仅限系统管理员访问并禁止 HTTP 缓存,不保存 Token 或密码,并与登录活动一起按 `USER_LOGIN_ACTIVITY_RETENTION_DAYS` 清理。
登录 IP 只接受可信反向代理提供的转发地址。Docker Compose 默认信任容器常用的 `172.16.0.0/12``192.168.0.0/16` 内部网段;其他部署必须通过 `TRUSTED_PROXY_CIDRS` 明确配置实际代理网段,不得直接信任任意来源的 `X-Forwarded-For`
## 来源地图服务器位置
来源地图中的服务器标记不使用固定城市或前端坐标。后端启动时按以下顺序确定部署服务器的公网地址,并缓存定位结果:
1. `MONITORING_SERVER_PUBLIC_IP` 明确指定的公网 IP,适用于禁止主动访问公网探测服务的生产网络。
2. `FRONTEND_PUBLIC_URL` 主机名解析出的公网 IP。
3. `MONITORING_PUBLIC_IP_DISCOVERY_URLS` 配置的公网出口 IP 查询服务。
默认公网查询服务为 `https://api64.ipify.org,https://icanhazip.com`,单次超时 2.5 秒。解析成功后通过本地 ip2region 数据库确定属地并转换为地图坐标;本地库没有坐标时复用下述 IP2Location.io 受控兜底。两条链路均失败时 API 返回 `server_location: null`,地图不会使用任意默认城市代替。默认打开全球视图,以便服务器与访问来源分属不同国家时仍能完整显示飞线。
## 访问来源坐标兜底
访问来源始终先使用本地 ip2region 和内置行政区质心解析。只有公网 IP 已识别、但本地链路无法得到地图坐标时,后端才调用 IPAddress.my 使用的 IP2Location.io 官方 JSON API `https://api.ip2location.io/` 补充经纬度;私网、回环、链路本地和无效地址不会发送给第三方。
兜底默认启用,单次请求最多查询 10 个尚未缓存的公网 IP,最多并发 5 个请求,超时 2.5 秒。成功结果缓存 7 天,失败结果缓存 1 小时;第三方不可用时继续返回本地解析结果,不影响访问来源接口。可设置 `MONITORING_IP_GEO_FALLBACK_ENABLED=false` 完全禁用外部查询。无密钥模式受服务方每日额度限制;如需配置 API Key,使用 `MONITORING_IP_GEO_FALLBACK_API_KEY`,后端通过 `Authorization: Bearer` 发送,禁止把密钥写入 URL 或日志。
受限网络建议显式配置:
```text
MONITORING_SERVER_PUBLIC_IP=<部署服务器公网IP>
MONITORING_PUBLIC_IP_DISCOVERY_URLS=
```
公网 IP 仅用于服务端定位,不写入来源分析 API 响应或浏览器界面。
## 日志完整性与隐私
- 每个请求由服务端生成 UUID 请求标识,并通过响应头 `X-Request-ID` 返回;权限日志和安全日志以该标识消除同一请求的重复统计。
- 写入队列保持非阻塞;队列满或数据库批次写入最终失败时不阻塞业务请求,但会累计丢弃量、失败批次、队列占用和最近错误时间,并在管理员健康页显示降级。
- 请求正文不采集。请求头仅保留运维白名单字段,认证、Cookie、密钥类字段统一脱敏;查询参数中的令牌、账号、受试者/患者、姓名、邮箱、电话、证件和地址类值会替换为 `[redacted]`
- IP、User-Agent 和请求路径仍属于运维审计数据,应按管理员最小授权和上述留存策略管理,不应复制到公开工单或通知正文。
## 常见异常处理
| 现象 | 首要检查 | 建议处理 |
| --- | --- | --- |
| `/health` 正常、`/readyz` 返回 503 | 数据库容器、连接串、迁移状态 | 检查 `docker compose ps`、数据库日志和 `alembic current` |
| 日志写入器降级 | 队列占用、丢弃量、失败批次、最近错误 | 检查数据库连接和容量;恢复后确认队列回落并重启以清零进程累计计数 |
| 留存任务异常 | 最近成功/错误时间 | 检查数据库权限、表结构和后端日志,确认 migration 已升级 |
| 页面显示“可能是上次成功结果” | 对应 API 或网络请求 | 使用刷新按钮重试,并结合 `/readyz` 与浏览器网络面板定位 |
| 安全事件突然增多 | 分类、来源 IP、路径和状态码 | 优先核对敏感路径探测、5xx 和无效令牌,不要仅按 4xx 总量或地理位置判断 |
## 当前边界与后续升级
权限缓存指标、告警列表和写入器累计计数仍为单进程内存状态,重启后会重置;当前也不负责短信、邮件、Slack 等外部通知。需要多实例部署、跨重启趋势、值班通知或长期容量分析时,应接入 Prometheus/OpenTelemetry、集中日志和 Alertmanager 类告警链路,并保留本模块作为管理员快速诊断入口。
+76
View File
@@ -0,0 +1,76 @@
# ONLYOFFICE 在线协作模块
## 范围
在线协作是共享库下的独立业务模块,不复用附件、文档管理、文件版本管理或 eTMF 的数据表和文件标识。当前支持新建或导入 `DOCX``XLSX``PPTX`,通过 ONLYOFFICE 进行多人共同编辑,并把保存结果固化为不可变修订。
原有附件与文档版本仍保持只读预览流程,不会自动进入协作空间,也不会因协作回调被覆盖。
## 数据与权限边界
- `collaboration_files` 保存协作文件元数据、当前修订指针和统一的内容操作权限设置。
- `collaboration_revisions` 保存不可变文件修订;每次有效保存或历史恢复都会创建新修订。
- `collaboration_members` 保存文件级编辑者和管理者。
- `collaboration_edit_requests` 保存系统内项目成员发起的编辑权限申请及审批结果;同一成员、同一文件同时只允许存在一条待处理申请。
- `notifications` 是收件人级通用通知表,统一保存通知类别、优先级、内部操作路径、业务来源、去重键、已读和业务关闭状态。编辑权限申请只通知当前有效的文件所有者和文件管理者;申请处理后,同一业务来源的所有收件人通知一并关闭。
- `collaboration_sessions` 使用稳定的文件代次生成 ONLYOFFICE `document.key`,同一代次的用户进入同一共同编辑会话。
- `collaboration_callback_receipts` 对回调做幂等确认。
- `collaboration_share_links` 保存单文件公开链接的启停状态、查看/编辑权限、有效期策略和密码哈希;不保存明文密码或共享令牌。
- 项目接口权限只控制在线协作模块入口、项目级新建/导入/文件夹操作,以及账号能否被授予文件角色;文件创建后的编辑、管理、导出和所有权转让统一由文件身份判定,项目角色不再隐式覆盖文件角色。
- 文件所有者始终拥有编辑、管理、导出和转让能力;文件管理者始终拥有编辑、管理和导出能力,但不能转让所有权;文件编辑者可以编辑,导出能力由文件“权限设置”开关控制。系统管理员继续保留平台级完整访问能力。
- 授予编辑者、管理者或转让所有权时,目标账号必须是当前项目的有效成员,并具备对应项目角色资格;不合格账号在联系人列表中不可选择,后端同时拒绝绕过界面的授权请求。
- 文件管理者可开启“允许申请编辑权限”。只读项目成员可从 CTMS 顶栏或 ONLYOFFICE 的“请求编辑”入口提交申请;批准后写入文件级编辑者授权,拒绝或重复处理均有明确状态和审计记录。匿名链接访问者不能申请系统账号权限。
- 顶栏铃铛通过通用通知 Feed 展示当前收件人的未关闭通知并记录已读状态;编辑权限申请通知可直接进入对应文件的“权限设置”,审批结果作为一次性信息通知申请人。AE、监查问题、文件回执和里程碑时效提醒也由同一 Feed 返回,具体规则见 `docs/project-notifications.md`
- Excel 文件可通过“允许所有协作者添加、删除工作表”控制工作簿结构。关闭时 CTMS 在新的不可变修订中启用工作簿结构保护并推进文件代次;重新开启时恢复文件原有的工作簿保护状态。该开关作用于所有协作者且不影响单元格内容编辑权限。
- 仅文档所有者和系统管理员可转让所有权。目标联系人必须具备文件管理者资格;转让后目标联系人升级为文件管理者,原所有者保留管理者身份,所有权变更写入审计。
- 文件编辑器“下载为”由文件导出规则判定:所有者和管理者始终允许,编辑者受文件开关控制,并由 CTMS 网页/桌面文件运行时保存到本机且记录下载审计。“另存为…”还会在项目工作区创建独立协作文件,因此额外要求项目级 `collaboration:create` 权限。
## 公开分享链接
- 文件管理者可开启链接,并选择只读或协作编辑、1 天/7 天/30 天/永久有效及可选访问密码;设置修改后自动保存,无需额外提交整张表单。
- 文件操作菜单以一个“访问与权限”弹窗统一管理账号授权、匿名访问和“权限设置”;下载、打印、另存和复制不在两种访问方式下重复配置。
- “权限设置”中的内容操作开关只作用于已授权编辑者和匿名访问者。所有者、文件管理者和系统管理员始终保留导出能力;外部链接不具备向项目工作区另存副本的权限。
- 共享令牌只放在网页 URL 的 fragment`#...`)中,不进入服务器请求路径、查询参数或本地缓存;前端调用公开 API 时通过会被审计层脱敏的请求头传递。
- 每个文件首次创建分享记录后使用固定共享地址;修改权限、密码、有效期或关闭后重新开启都不会改变 URL。关闭期间同一地址暂停访问,重新开启后恢复访问。
- 链接密码仅保存 bcrypt 哈希;验证成功后签发短时、链接记录绑定的内存访问凭证。连续错误达到阈值后链接密码验证会临时锁定。
- 外部共享页不依赖 CTMS 登录状态,不提供工作区“另存为”;开启允许导出后可下载到访问者本机,编辑链接仍通过现有 ONLYOFFICE 会话写入不可变修订。
- `JWT_SECRET_KEY` 轮换属于平台级全局失效操作,会使现有公开分享链接和短时访问凭证失效;生产环境必须按安全变更流程评估影响并提前通知链接使用者。
## 保存流程
1. 浏览器请求 `/api/v1/studies/{study_id}/collaboration/files/{file_id}/editor-config`
2. 后端生成带 JWT 的编辑配置、内部内容地址和回调地址。
3. Document Server 从内部内容接口读取当前修订。
4. 共同编辑期间使用 fast 模式;强制保存产生新修订并更新会话恢复基线。
5. 最后一位编辑者退出后,状态 2 回调产生最终修订并推进文件代次;下一次编辑使用新的 `document.key`
6. 重复回调通过指纹幂等处理;旧代次回调不会覆盖当前文件。
内部内容和回调接口不经过 Nginx 公网入口,只接受 `AuthorizationJwt`。回调结果文件仅允许从配置的 Document Server 内部源获取,禁止重定向、凭据 URL 和任意主机。
## 本地开发
标准开发安装会直接启动 ONLYOFFICE
```bash
bash scripts/install.sh dev
```
安装脚本会生成或复用独立的开发 JWT 密钥和稳定实例标识,默认启动 Document Server,并验证后端配置和 HTTP 健康状态。`bash scripts/onlyoffice-dev-up.sh` 仅用于开发环境单独重建、验证或轮换密钥。协作修订保存在现有后端上传卷下的独立 `collaboration/` 目录。
关键配置:
- `ONLYOFFICE_ENABLED`
- `ONLYOFFICE_JWT_SECRET`
- `ONLYOFFICE_INTERNAL_URL`
- `ONLYOFFICE_STORAGE_BASE_URL`
- `ONLYOFFICE_INSTANCE_ID`
- `ONLYOFFICE_CONFIG_TTL_SECONDS`
- `COLLABORATION_MAX_FILE_BYTES`
生产标准安装同样包含 ONLYOFFICE;正式上线前必须按既有发布要求确认镜像许可、独立 JWT 密钥、内部网络、容量、备份与监控。
参考:
- [ONLYOFFICE 共同编辑模式](https://api.onlyoffice.com/docs/docs-api/get-started/how-it-works/co-editing/)
- [ONLYOFFICE 回调处理器](https://api.onlyoffice.com/docs/docs-api/usage-api/callback-handler/)
- [ONLYOFFICE 文档权限](https://api.onlyoffice.com/docs/docs-api/usage-api/config/document/permissions/)
+72
View File
@@ -0,0 +1,72 @@
# 项目提醒中心
## 产品边界
项目提醒是既有业务状态面向明确收件人的可操作投影,不是独立任务、日历或聊天系统。源业务记录和业务审计始终是权威数据;提醒的阅读、桌面投递均不能替代 AE 上报、问题关闭、文件回执、审批或其他业务动作。
当前范围只包括已认证在线客户端:
- 网页端和桌面端共用当前项目的通用提醒 Feed。
- 顶栏铃铛展示摘要,`/project/notifications` 提供当前提醒、未读和待处理筛选。已经解决或因权限变化失效的提醒不再向客户端返回。
- 桌面系统通知是用户主动开启的可选投递渠道,只显示不含项目、文件、受试者或风险详情的通用文案。
- 不提供离线提醒、本地业务权威数据、个人自由定时任务、短信、即时通讯或浏览器 Push。
- 管理员权限监控告警、登录会话提醒和客户端更新“稍后提醒”不进入业务提醒中心。
## 状态模型
`notifications` 是收件人级通用提醒表:
- `read_at`:用户已经看过提醒。
- `resolved_at`:源业务已经完成、关闭、失效,或一次性信息通知已经被阅读。
- `requires_action`:区分业务待办和一次性信息通知。
- `due_at`:用于展示业务截止时间;业务规则仍以源记录为准。
- `source_type``source_id``source_version``dedupe_key`:用于来源追踪、幂等同步、阶段升级和自动关闭。
阅读待处理提醒不会写入 `resolved_at`。业务动作完成后由对应服务即时关闭提醒,定时同步还会对遗漏或权限变化进行对账。一次性信息通知在首次阅读时归档。
桌面投递状态继续保存在 `desktop_notification_deliveries`,但通过 `notification_id` 关联通用提醒。投递成功只记录 `delivered_at`,不自动标记业务提醒已读或已处理;提醒升级或重新打开时可以重新进入桌面投递队列。
桌面客户端每 60 秒执行一次在线投递检查:先确认本机系统权限,再读取当前账号的服务端订阅,随后领取待投递提醒、调用系统通知并确认实际成功投递的提醒。任一系统通知调用失败时不会确认该条提醒;网络或投递失败采用指数退避,最长 15 分钟。用户可在“设置 → 通知”查看本次客户端运行期间的最近检查、最近投递和当前链路状态,并主动执行一次真实业务提醒检查。
## 当前规则
| 来源 | 收件人 | 触发和升级 | 自动关闭 |
|---|---|---|---|
| 协作编辑申请 | 文件所有者、文件管理者 | 申请创建 | 申请批准或拒绝 |
| 协作申请结果 | 申请人 | 申请批准或拒绝 | 申请人阅读后归档 |
| 文件分发与回执 | 指定用户、指定角色、中心联系人 | 新分发;3 天内到期;逾期 | 用户完成接收回执、分发关闭或权限失效 |
| AE 上报时效 | 具备 AE 读取权限且在中心范围内的项目成员 | 3 天内到期;逾期;按数量增加重新提醒 | 数量归零或权限失效 |
| 监查问题整改 | 具备监查问题读取权限且在中心范围内的项目成员 | 3 天内到期;逾期;按数量增加重新提醒 | 数量归零或权限失效 |
| 项目里程碑 | 里程碑负责人 | 7 天内到期;逾期;日期或状态变化重新提醒 | 完成、负责人变化或日期移出提醒窗口 |
| 受试者访视窗口 | 具备访视更新权限的有效项目成员;CRA 仅限负责中心 | 窗口开始前 3 天、窗口进行中;错过窗口后升级 | 录入实际访视、取消访视、受试者结束、中心停用或权限/中心范围失效 |
所有规则都要求当前有效项目成员和对应业务权限。系统管理员不会仅因平台权限而批量接收所有项目提醒;需要作为有效项目成员或明确业务收件人进入提醒范围。
日期型 AE、里程碑和访视窗口规则按 `Asia/Shanghai` 业务日计算,带时区的截止时间统一按 UTC 存储和比较。若未来引入项目级时区,应由项目配置替代固定业务时区,不能使用浏览器本地时区驱动服务端规则。
访视提醒按访视记录生成并可跳转到对应受试者详情,但提醒标题和正文不包含受试者编号、姓名、中心名称或其他可识别信息。收件人资格以 `visits:update` 及其前置权限为准,避免仅具备只读权限的 PV、QA、CTA 等角色被动接收受试者访视提醒。
## 同步与诊断
服务端启动提醒同步任务,默认每 300 秒对所有有效项目成员执行幂等同步;间隔通过 `NOTIFICATION_SYNC_INTERVAL_SECONDS` 配置,允许 603600 秒。PostgreSQL advisory lock 防止多个后端进程重复执行全量任务。
用户读取 Feed 时还会执行一次当前用户轻量对账,用于弥补短时任务失败。单个成员同步失败不会中断其他成员;失败写入 `ctms.notifications` 日志。提醒创建、更新和自动关闭本身不替代源业务审计。
## 桌面隐私边界
系统通知固定为:
- 标题:`CTMS 待办提醒`
- 正文:`有新的业务提醒待查看`
系统通知 API 不接受动态标题或正文。项目名、文件名、版本、受试者、AE、监查问题等详情只允许在完成 token 校验和项目权限确认后的应用内 Feed 中展示;访视提醒即使在应用内 Feed 也不直接显示受试者标识,只通过受控详情路径访问。
## 系统通知授权边界
系统权限和服务端订阅是两个独立条件:
- 首次开启时,客户端先请求操作系统授权;只有操作系统返回已授权,才开启当前账号的服务端订阅。
- 用户在操作系统中撤销权限后,服务端订阅可以仍然保持开启,但客户端不会领取提醒,设置页会明确显示“系统权限已拒绝”。
- macOS 或 Windows 已拒绝权限时,系统通常不会再次展示授权框。设置页分别引导用户前往“系统设置 → 通知 → CTMS”或“设置 → 系统 → 通知 → CTMS”,返回后通过“重新检测权限”恢复链路。
- “发送测试通知”只验证本机通知能力,不访问业务提醒队列;“立即检查业务提醒”会走服务端订阅、领取、系统投递和确认的真实路径。
- CTMS 不申请打开任意 URL、执行系统命令或读取操作系统通知内容的额外 Tauri 权限。授权引导仅展示设置路径,避免为便利入口扩大桌面原生能力边界。