diff --git a/docs/operational-tasks.md b/docs/operational-tasks.md index a36e474..7adf218 100644 --- a/docs/operational-tasks.md +++ b/docs/operational-tasks.md @@ -52,11 +52,14 @@ test "$(psql "$PROJECT_BIZ_DSN" -XAtc 'select current_database()')" = "tjwater_v2" ``` -完成备份和回滚演练后,再应用: +完成备份和回滚演练后,再按依赖顺序应用。`017` 是工作台范围的注释迁移,要求告警、工单、任务和 Run 表均已存在: ```bash +psql "$PROJECT_BIZ_DSN" -v ON_ERROR_STOP=1 -f resources/sql/008_workbench_alert_events.sql +psql "$PROJECT_BIZ_DSN" -v ON_ERROR_STOP=1 -f resources/sql/011_workbench_work_orders.sql psql "$PROJECT_BIZ_DSN" -v ON_ERROR_STOP=1 -f resources/sql/014_operational_tasks.sql psql "$PROJECT_BIZ_DSN" -v ON_ERROR_STOP=1 -f resources/sql/015_workbench_events_and_task_runs.sql +psql "$PROJECT_BIZ_DSN" -v ON_ERROR_STOP=1 -f resources/sql/017_workbench_schema_comments.sql ``` -迁移不会创建或修改外围系统的业务表,也不授予 `PUBLIC` 权限。生产运行前需按部署身份显式授予新表权限。SQL 文件包含自己的事务边界,不能把外层 `ROLLBACK` 当作演练;具体要求见数据库变更门禁。 +`017_workbench_schema_comments.sql` 只增加数据库可见的表、列和 schema 注释,不改变数据、约束或索引。迁移不会创建或修改外围系统的业务表,也不授予 `PUBLIC` 权限。生产运行前需按部署身份显式授予新表权限。SQL 文件包含自己的事务边界,不能把外层 `ROLLBACK` 当作演练;具体要求见数据库变更门禁。 diff --git a/docs/workbench-database-rollout.md b/docs/workbench-database-rollout.md index 387b2b4..d6682a7 100644 --- a/docs/workbench-database-rollout.md +++ b/docs/workbench-database-rollout.md @@ -6,6 +6,8 @@ 截至 2026-09-24,`014_operational_tasks.sql` 和 `015_workbench_events_and_task_runs.sql` 已应用到 `tjwater_v2`、`szh_v2`、`md_v2`、`fengyang_v2` 和 `zjb`。这是已有基线,不代表后续迁移可以继续批量应用。 +`017_workbench_schema_comments.sql` 只为 `workbench` schema 及其 17 张表和关键列增加数据库可见注释。它先在 `tjwater_v2` 应用;其他项目数据库保持原状,直到 `tjwater_v2` 验证通过并获得明确扩展确认。 + 后续数据库变更默认只写入 `tjwater_v2`。其他项目保持现状,必须等 `tjwater_v2` 验证通过并由维护者明确确认后,再逐库扩展。禁止把“所有活跃项目”作为写操作的默认范围。 ## 变更顺序 diff --git a/docs/workbench-operations-storage.md b/docs/workbench-operations-storage.md index 4cf33d8..e56c8f1 100644 --- a/docs/workbench-operations-storage.md +++ b/docs/workbench-operations-storage.md @@ -10,6 +10,8 @@ DDL 位于 `resources/sql/008_workbench_alert_events.sql`。任务通过 `POST / `workbench.work_orders` 的 DDL 位于 `resources/sql/011_workbench_work_orders.sql`。每行保存一个只读工单投影,文档必须满足 `WorkbenchRuntimeWorkOrder`: +`resources/sql/017_workbench_schema_comments.sql` 在 `008`、`011`、`014` 和 `015` 之后执行,为工作台 schema、17 张表和关键列增加数据库可见的用途说明。 + - 一张主工单包含一个或多个 `subtasks`。 - 每个子任务保存自己的位置、班组、时间窗和复令要求。 - 每个子任务包含一个或多个有序 `stages`,`sequence` 从 1 连续递增,阶段编号在子任务内唯一,最多只有一个 `current` 阶段。 diff --git a/resources/sql/017_workbench_schema_comments.sql b/resources/sql/017_workbench_schema_comments.sql new file mode 100644 index 0000000..ebaef43 --- /dev/null +++ b/resources/sql/017_workbench_schema_comments.sql @@ -0,0 +1,171 @@ +-- Database-visible descriptions for the workbench schema. + +BEGIN; + +COMMENT ON SCHEMA workbench IS + '项目级工作台业务投影、工况任务、执行记录、通知及事件日志。'; + +COMMENT ON TABLE workbench.alert_events IS + '告警来源提交的追加式状态事件,用于幂等接收 raised、acknowledged 和 resolved 事实。'; +COMMENT ON COLUMN workbench.alert_events.producer_id IS + '告警事件生产者标识;与 event_id 共同确定来源侧幂等性。'; +COMMENT ON COLUMN workbench.alert_events.event_id IS + '生产者范围内的事件唯一标识。'; +COMMENT ON COLUMN workbench.alert_events.event_type IS + '告警状态变化类型:raised、acknowledged 或 resolved。'; +COMMENT ON COLUMN workbench.alert_events.document IS + '经过接口模型校验的完整告警事件文档。'; + +COMMENT ON TABLE workbench.alerts IS + '每项告警的最新状态投影,供工作台列表和状态查询读取。'; +COMMENT ON COLUMN workbench.alerts.revision IS + '告警投影的单调修订号,用于拒绝过期更新。'; +COMMENT ON COLUMN workbench.alerts.status IS + '告警当前状态:active、acknowledged 或 resolved。'; +COMMENT ON COLUMN workbench.alerts.document IS + '经过接口模型校验的完整告警投影文档。'; + +COMMENT ON TABLE workbench.domain_event_log IS + '项目级持久事件日志,为 SSE 断线补读和工作进程唤醒提供单调游标;不保存业务正文。'; +COMMENT ON COLUMN workbench.domain_event_log.event_seq IS + '跨项目单调递增的事件游标,也是 SSE 事件 ID。'; +COMMENT ON COLUMN workbench.domain_event_log.topic IS + '固定白名单中的工作台事件主题。'; +COMMENT ON COLUMN workbench.domain_event_log.aggregate_revision IS + '事件产生时聚合对象的业务修订号。'; +COMMENT ON COLUMN workbench.domain_event_log.metadata IS + '用于定向失效的最小元数据;客户端必须回查权威业务接口。'; + +COMMENT ON TABLE workbench.operational_tasks IS + '工况任务的权威业务投影,保存任务生命周期和当前计划、报告引用。'; +COMMENT ON COLUMN workbench.operational_tasks.status IS + '兼容旧接口的任务与执行混合状态;新业务以 lifecycle_status 和活动 Run 为准。'; +COMMENT ON COLUMN workbench.operational_tasks.lifecycle_status IS + '任务业务生命周期:open、waiting_human、completed、failed 或 cancelled。'; +COMMENT ON COLUMN workbench.operational_tasks.revision IS + '任务业务投影的单调修订号;Agent 心跳不得增加该值。'; +COMMENT ON COLUMN workbench.operational_tasks.execution_generation IS + '兼容字段,记录当前执行代次;执行权威记录位于 operational_task_runs。'; +COMMENT ON COLUMN workbench.operational_tasks.execution_request_id IS + '兼容字段,记录当前执行请求号;执行权威记录位于 operational_task_runs。'; +COMMENT ON COLUMN workbench.operational_tasks.agent_session_id IS + '兼容字段,记录当前 Agent 会话;活动会话以 operational_task_runs 为准。'; +COMMENT ON COLUMN workbench.operational_tasks.agent_lease_expires_at IS + '兼容字段,记录 Agent 租约到期时间;活动租约以 operational_task_runs 为准。'; +COMMENT ON COLUMN workbench.operational_tasks.document IS + '经过接口模型校验的完整任务文档;标量列承担约束和索引。'; + +COMMENT ON TABLE workbench.operational_task_runs IS + '工况任务的每代 Agent 执行记录,保存队列状态、租约、心跳、重试和终态。'; +COMMENT ON COLUMN workbench.operational_task_runs.generation IS + '同一任务内单调递增的执行代次,用于阻止旧执行写回。'; +COMMENT ON COLUMN workbench.operational_task_runs.execution_request_id IS + '投递给 Agent 的幂等执行请求号。'; +COMMENT ON COLUMN workbench.operational_task_runs.status IS + '执行状态:queued、claimed、running、succeeded、failed、timed_out 或 cancelled。'; +COMMENT ON COLUMN workbench.operational_task_runs.revision IS + 'Run 的单调修订号;心跳只更新该修订号。'; +COMMENT ON COLUMN workbench.operational_task_runs.lease_expires_at IS + '已领取或运行中执行的租约到期时间。'; +COMMENT ON COLUMN workbench.operational_task_runs.next_dispatch_at IS + '失败退避后允许再次投递的最早时间。'; + +COMMENT ON TABLE workbench.operational_task_requests IS + '工况命令幂等记录,按项目、命令范围和请求号保存请求摘要及既有响应。'; +COMMENT ON COLUMN workbench.operational_task_requests.scope IS + '幂等请求所属的命令范围。'; +COMMENT ON COLUMN workbench.operational_task_requests.request_digest IS + '规范化请求内容摘要,用于识别请求号复用但内容不同的冲突。'; +COMMENT ON COLUMN workbench.operational_task_requests.response IS + '首次成功处理后保存的响应,用于幂等重放。'; + +COMMENT ON TABLE workbench.operational_task_evidence IS + '工况任务采集或引用的证据记录,以 evidence_id 在任务内唯一标识。'; +COMMENT ON COLUMN workbench.operational_task_evidence.evidence_id IS + '任务范围内稳定的证据标识。'; +COMMENT ON COLUMN workbench.operational_task_evidence.document IS + '经过接口模型校验的完整证据文档。'; + +COMMENT ON TABLE workbench.operational_task_plans IS + '工况任务按修订号保存的计划版本,用于审批和执行代次核验。'; +COMMENT ON COLUMN workbench.operational_task_plans.revision IS + '任务范围内单调递增的计划修订号。'; +COMMENT ON COLUMN workbench.operational_task_plans.document IS + '经过接口模型校验的完整计划文档。'; + +COMMENT ON TABLE workbench.operational_task_reports IS + '工况任务按修订号保存的阶段或最终报告版本。'; +COMMENT ON COLUMN workbench.operational_task_reports.revision IS + '任务范围内单调递增的报告修订号。'; +COMMENT ON COLUMN workbench.operational_task_reports.document IS + '经过接口模型校验的完整报告文档。'; + +COMMENT ON TABLE workbench.operational_task_actions IS + '任务建议动作的当前投影,保存审批状态、执行状态和最新版本。'; +COMMENT ON COLUMN workbench.operational_task_actions.version IS + '动作当前版本号,用于乐观并发和审批前置校验。'; +COMMENT ON COLUMN workbench.operational_task_actions.approval_status IS + '人工审批状态:pending、approved 或 rejected。'; +COMMENT ON COLUMN workbench.operational_task_actions.execution_status IS + '外部动作执行状态;accepted 不等同于 succeeded。'; +COMMENT ON COLUMN workbench.operational_task_actions.document IS + '经过接口模型校验的完整动作当前态文档。'; + +COMMENT ON TABLE workbench.operational_task_action_revisions IS + '任务动作的不可变版本历史,用于审计审批和执行状态变化。'; +COMMENT ON COLUMN workbench.operational_task_action_revisions.version IS + '动作范围内单调递增的历史版本号。'; +COMMENT ON COLUMN workbench.operational_task_action_revisions.document IS + '该版本对应的完整动作快照。'; + +COMMENT ON TABLE workbench.operational_task_external_feedback IS + '外围工单或其他外部系统反馈的幂等任务投影,用于触发后续分析。'; +COMMENT ON COLUMN workbench.operational_task_external_feedback.source_event_id IS + '外部来源事件的稳定标识,用于拒绝重复反馈。'; +COMMENT ON COLUMN workbench.operational_task_external_feedback.source_revision IS + '外部来源对象的修订号。'; +COMMENT ON COLUMN workbench.operational_task_external_feedback.document IS + '经过接口模型校验的完整外部反馈文档。'; + +COMMENT ON TABLE workbench.operational_task_notifications IS + '工况任务产生的用户通知内容;它是业务收件箱,不承担事件总线职责。'; +COMMENT ON COLUMN workbench.operational_task_notifications.document IS + '经过接口模型校验的完整通知文档。'; + +COMMENT ON TABLE workbench.operational_task_notification_reads IS + '用户维度的通知已读状态,使同一通知可由多个用户独立确认。'; +COMMENT ON COLUMN workbench.operational_task_notification_reads.user_id IS + '读取通知的产品用户标识。'; +COMMENT ON COLUMN workbench.operational_task_notification_reads.version IS + '用户读取后返回的通知视图版本。'; +COMMENT ON COLUMN workbench.operational_task_notification_reads.read_at IS + '该用户首次确认已读的时间。'; + +COMMENT ON TABLE workbench.operational_task_schedules IS + '周期性工况任务的调度定义,保存下一触发时间、间隔、启用状态和版本。'; +COMMENT ON COLUMN workbench.operational_task_schedules.schedule_key IS + '项目范围内稳定的调度业务键。'; +COMMENT ON COLUMN workbench.operational_task_schedules.next_run_at IS + '下一次允许生成任务的计划时间。'; +COMMENT ON COLUMN workbench.operational_task_schedules.interval_seconds IS + '两次计划触发之间的秒数,最小值为 60。'; +COMMENT ON COLUMN workbench.operational_task_schedules.version IS + '调度定义的单调版本号,用于乐观并发。'; +COMMENT ON COLUMN workbench.operational_task_schedules.document IS + '经过接口模型校验的完整调度定义文档。'; + +COMMENT ON TABLE workbench.operational_schedule_occurrences IS + '周期调度的幂等触发记录,将一个 schedule 的到期时刻关联到唯一任务。'; +COMMENT ON COLUMN workbench.operational_schedule_occurrences.due_at IS + '本次调度理论到期时间;与 schedule_id 共同防止重复触发。'; +COMMENT ON COLUMN workbench.operational_schedule_occurrences.task_id IS + '本次调度触发所创建的工况任务。'; + +COMMENT ON TABLE workbench.work_orders IS + '从外围工单系统同步或受控种子写入的只读预约工单投影。'; +COMMENT ON COLUMN workbench.work_orders.work_order_id IS + '来源系统中的稳定工单标识。'; +COMMENT ON COLUMN workbench.work_orders.document IS + '经过接口模型校验的完整工作台工单文档。'; + +COMMIT; diff --git a/tests/integration/test_operational_tasks_storage.py b/tests/integration/test_operational_tasks_storage.py index d334a95..f5b34e7 100644 --- a/tests/integration/test_operational_tasks_storage.py +++ b/tests/integration/test_operational_tasks_storage.py @@ -30,7 +30,6 @@ pytestmark = pytest.mark.skipif( os.environ.get("OPERATIONAL_TASKS_STORAGE_TEST") != "1", reason="explicit PostgreSQL test opt-in required", ) - @pytest.fixture def anyio_backend(): return "asyncio" @@ -40,7 +39,13 @@ def anyio_backend(): async def test_postgres_task_artifacts_approval_and_project_isolation_roll_back(): migration = "\n".join( (Path(__file__).parents[2] / f"resources/sql/{name}").read_text() - for name in ("014_operational_tasks.sql", "015_workbench_events_and_task_runs.sql") + for name in ( + "008_workbench_alert_events.sql", + "011_workbench_work_orders.sql", + "014_operational_tasks.sql", + "015_workbench_events_and_task_runs.sql", + "017_workbench_schema_comments.sql", + ) ) # The migration is normally atomic itself. This test owns an outer rollback # transaction, so it executes its statements without transaction delimiters. @@ -51,6 +56,16 @@ async def test_postgres_task_artifacts_approval_and_project_isolation_roll_back( try: async with conn.transaction(force_rollback=True): await conn.execute(migration) + comment_rows = await ( + await conn.execute( + """SELECT c.relname, obj_description(c.oid, 'pg_class') AS description + FROM pg_class c + JOIN pg_namespace n ON n.oid=c.relnamespace + WHERE n.nspname='workbench' AND c.relkind='r'""", + ) + ).fetchall() + assert len(comment_rows) >= 17 + assert all(row["description"] for row in comment_rows) repository = OperationalTaskRepository(conn, context.project_id) service = OperationalTaskService(repository, context, permissions_for_context(context)) now = datetime.now(timezone.utc)