--- id: T-009 title: 建立 PostgreSQL 双 Schema 与 Bell 配额投影基础 phase: 2 deps: [T-008] status: DONE created: 2026-08-07 issue: 31 context_ref: 8a4514076e9921b12bc26380b2aceee9334e947d claim_branch: claims/T-009 work_branch: agent/codex/T-009 write_paths: - docs/tasks/T-009.md - deploy/postgres/ - scripts/test_postgres.ps1 - Sense/internal/store/ - Sense/internal/config/ - Sense/cmd/sense-api/main.go - Sense/go.mod - Sense/go.sum - Sense/README.md - docs/03-tech-stack.md - docs/04-architecture.md - docs/06-tasks.md - docs/api.md - docs/current-state.md - tests/test_postgres_contract.py --- ## 问题 / 背景 T-008 已冻结 Sense Control API v1 和 `bell.site_quota_v1` 只读投影,但当前 Sense 仍只使用 M1 SQLite,Bell 也没有 PostgreSQL 源表、schema、角色或 migration。若直接实现 HTTP handler,tenant、配额读取、并发准入和事务语义仍会绑定 SQLite,后续迁移必然返工。 项目负责人指定使用本机现有 `D:\pgsql17`,已核实二进制与服务版本为 PostgreSQL `17.10`、默认端口 `5432`。现有实例要求 SCRAM 且当前执行环境没有管理员密码,因此自动验收必须使用同一套本机二进制启动隔离临时集群;不得修改现有实例的认证、角色、数据库或数据。生产/共享实例安装只有在操作者显式提供管理员连接信息时才执行。 ## 关联需求与交互(如适用) - 用户故事:US-001、US-002、US-008、US-009、US-010。 - 交互清单:IX-001~IX-004、IX-013~IX-016、IX-019、IX-020;本任务是数据基础,不修改已确认原型或实现 HTTP 页面交互。 - 前置契约:`docs/contracts/sense-control-v1.openapi.json`、`docs/contracts/site-quota-v1.sql` 和 `docs/contracts/README.md`。 ## 方案 1. 冻结 PostgreSQL `17.10` 与 Go PostgreSQL driver;增加可审计、顺序执行的初始化 SQL,创建 NOLOGIN 权限角色 `bell_app`/`sense_app`、`bell`/`sense` schema、Bell Site 配额源表和 Sense 设备/能力/调和表。 2. migration 创建与 T-008 完全一致的 `bell.site_quota_v1(tenant_id, site_id, max_video_channels, source_version, source_updated_at)`;Bell 拥有源表和视图,Sense 只有 Bell schema `USAGE` 与视图 `SELECT`,没有 Bell 源表或写权限。 3. Bell Site 在数据库层执行默认 16、范围 1~128、逻辑删除和单调版本;配额更新通过 trigger 增加版本,不能由 Sense 修改。 4. 新增 Sense PostgreSQL repository,覆盖现有 SQLite 被对账/探活使用的全部 port;设备新增/启用在同一事务内按 tenant/site 读取配额投影、串行化同站点准入、统计 `enabled + video_capture`,并拒绝投影缺失、越界或版本回退。 5. Sense 进程增加显式 `SENSE_DB_DRIVER=sqlite|postgres`;默认继续使用 SQLite 保持单摄像头开发路径,生产选择 PostgreSQL 时必须显式提供 DSN。不得把 DSN、密码或完整连接串写入日志、错误、任务证据或仓库。 6. 使用 `D:\pgsql17\bin` 的 `initdb/pg_ctl/psql` 启动仅绑定回环地址的隔离临时集群,执行 migration、权限断言和 PostgreSQL repository 集成测试,结束后验证目标路径再清理临时数据目录;不停止或重启现有 Windows PostgreSQL 服务。 7. 增加无 PostgreSQL 也能运行的静态契约测试,校验 migration 顺序、角色权限、配额视图签名、范围、版本 trigger 和禁止的宽权限;同步技术栈、架构、API、路线图、Sense 启动说明与当前状态。 ## 不可变约束 - 阈值 / 数值边界:`max_video_channels` 默认 16、有效范围 1~128;16/128 不是单机承载保证。只统计 `desired_state=enabled` 且具有 `video_capture` capability 的设备;同站点并发创建/启用不能突破配额。 - 判定式 / 状态转换:配额行缺失、读取失败、值越界或 `source_version` 回退时,PostgreSQL repository 只拒绝相关视频新增/启用,不停已有设备、不修改期望态;降低配额不会自动停用已有流。相同期望态重复提交不增加 generation。 - 数据所有权:Bell 是 Tenant/Site/配额真相源;Sense 不建立可写 Site 真相副本,不写 Bell schema、不读取 `bell.sites` 源表。Sense 设备和调和状态只写 `sense` schema;两个 schema 只以 tenant/site 逻辑 ID 关联。 - 安全边界:仓库、测试输出、Issue/PR 和日志不得出现 PostgreSQL 密码、DSN 凭据或私有数据。自动测试只使用临时 trust 集群且只绑定 `127.0.0.1`;现有 `D:\pgsql17\data`、Windows 服务和其他数据库严格只读。 - 兼容与恢复:M1 SQLite 默认路径和现有五路集成工具继续可用;T-009 不搬迁或删除现有 SQLite 数据。生产切换失败时可回到 SQLite 配置;初始 PostgreSQL schema 不提供自动破坏性 down migration,清理由管理员在备份/确认后限定到 YoVision 专用数据库执行。 - 既有契约:不修改 T-008 OpenAPI 路径、字段、错误语义、配额视图列签名或 Brain 事件 v0.1;PostgreSQL repository 的出现不表示 Sense HTTP handler、认证或 Bell 管理服务已经实现。 ## 验收要点 - 任务相关验证:在本机执行 `./scripts/test_postgres.ps1 -PgRoot D:\pgsql17`,必须完成隔离集群启动、migration 重放、权限断言、tenant/配额/并发/版本回退 repository 集成测试和自动清理;运行 `python -m unittest discover -s tests -p "test_postgres_contract.py"`,静态负例能够拦截 Bell 写权限与契约漂移。 - 完整门禁:运行 `./init.ps1`、`python scripts/validate_agent_context.py`、`python -m unittest discover -s tests -p "test_*.py"`、`python scripts/validate_harness_governance.py`、`go -C Sense test ./...`、`go -C Sense vet ./...`、`go -C Sense build ./...` 和 `git diff --check`,全部通过。 - 人工 / 设备验收:不需要摄像头、UI 或客户现场;任务所有者必须核对隔离集群确实没有使用 `D:\pgsql17\data`,现有服务 PID/端口在测试前后保持运行,且共享实例未新增 YoVision 对象。 - 构建产物:PostgreSQL migration、权限/迁移断言、本机隔离测试入口、Sense PostgreSQL repository 和配置/运维文档;不提交临时数据目录、数据库 dump 或秘密。 ## 边界(不改什么) 不实现 Sense Control API HTTP handler、Bearer 认证、RBAC、幂等收据、ETag/cursor/batch operation、Area/capture policy 投影、Bell Go 服务、SQLite→PostgreSQL 数据搬迁、WireGuard、孤儿删除、16 路容量压测或 T-007 现场验收;不修改 Brain、MediaMTX API、摄像头适配器、事件契约或已确认 UI 原型。 ## 协作约束 - 责任 Agent:由 dispatcher 分配。 - 唯一写入者:同责任 Agent。 - 委派:默认不启用。 - Gitea:任务文件先进入默认分支,再创建唯一 Issue 并回填编号;领取时记录 `context_ref`、claim 与工作分支。 任何新增写路径先检查与其他活跃任务是否重叠;同一时刻只有一个 Agent 修改本任务的 `write_paths`。 ## 执行记录 ### 2026-08-07 完成 PostgreSQL 数据基础 - 冻结 PostgreSQL `17.10` 与 `pgx/v5 v5.10.0`;新增四段可重放初始化 SQL,创建 NOLOGIN `bell_app`/`sense_app`、`bell`/`sense` schema、Bell Site 单调版本 trigger、T-008 五列配额视图、Sense 设备/能力/调和/投影观察表及最小权限。 - 新增 PostgreSQL repository 和 `SENSE_DB_DRIVER=sqlite|postgres` 显式选择;默认 SQLite 与 T-006 工具保持不变。PostgreSQL 启动检查 migration 和跨 schema 权限,Sense 对 Bell 源表或配额视图有写权限时拒绝启动。 - 视频新增/启用在一个事务内使用 tenant/site transaction-scoped advisory lock,读取并记录 `source_version` 后再计数写入;覆盖默认 16、128/129、非视频不占路、并发不超配额、缺失/回退失败关闭、降配不关流、tenant 隔离、相同期望态不增 generation 和全部调和 repository port。 - `./scripts/test_postgres.ps1 -PgRoot D:\pgsql17` 使用随机回环端口启动隔离 PostgreSQL 17.10,migration 连续执行两遍、SQL 权限断言及 9 个 `TestPostgres*` 测试通过,临时集群停止并清理;脚本核对现有 5432 listener 前后相同且从不引用 `D:\pgsql17\data`。 - `./init.ps1` 通过;`python -m unittest discover -s tests -p "test_*.py"` 共 33 项通过,其中 PostgreSQL 静态契约 7 项;`go -C Sense test ./...`、`go -C Sense vet ./...`、`go -C Sense build ./...`、上下文/治理校验和 `git diff --check` 均通过。不需要摄像头、UI 或客户现场验收。 ### 2026-08-07 领取任务 - dispatcher `ila` 将任务分配给 `codex`;`context_ref` 为 `8a4514076e9921b12bc26380b2aceee9334e947d`,claim 为 `claims/T-009`,工作分支为 `agent/codex/T-009`。 - 接受任务文件声明的全部写路径;当前其他任务无活跃写路径冲突,T-007 仍因外部真实设备保持 waiting。 ### 2026-08-07 任务定义 - 项目负责人批准创建并实施 T-009,并指定复用本机 `D:\pgsql17`;核实版本为 PostgreSQL `17.10`。 - 当前实例监听 `5432` 且 SCRAM 认证有效,执行环境未持有管理员密码;任务据此选择“本机同版二进制 + 隔离临时集群”作为自动验收,不触碰现有数据目录和服务。