diff --git a/docs/06-troubleshooting.md b/docs/06-troubleshooting.md index 49db000..946db2e 100644 --- a/docs/06-troubleshooting.md +++ b/docs/06-troubleshooting.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Troubleshooting wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Troubleshooting -wiki_revision: a169b2323323d9de6304e9b430ddbe9888ea1d25 -synchronized_at: 2026-08-15T07:16:25Z +wiki_revision: 1a452e9aafdfe01580f37f9179584b89516cf992 +synchronized_at: 2026-08-15T07:59:38Z # 故障排查 @@ -134,3 +134,21 @@ synchronized_at: 2026-08-15T07:16:25Z 正式数据库未备份时不得执行该结构迁移。需要回退版本时停止服务并从迁移前备份恢复,不把 JSONB 反向猜测为旧文本。 + + +## Sense 旧媒体路由迁移排错 + +旧库迁移出现 `约束 "uni_sense_media_routes_path" 不存在 (SQLSTATE 42704)`,表示旧 `sense_media_routes.path` 由 PostgreSQL 自动命名的唯一约束保护,而新 GORM 模型准备改用唯一索引;GORM 按推导名称删除旧约束时找不到实际名称。修正该名称后若继续出现 `source_ready ... contains null values (SQLSTATE 23502)`,表示非空旧表还缺少当前模型要求的运行态列。 + +工单 #95 的兼容迁移只在 PostgreSQL 旧表存在时执行:取得 ACCESS EXCLUSIVE 表锁,确认只有一个单列 `UNIQUE(path)` 约束,将实际约束名规范为 GORM 可识别名称;同时为旧路由初始化保守运行态 `source_ready=false`、`failure_count=0`、`last_error_code=''`,再继续 AutoMigrate。迁移不会把旧路由伪装成已就绪,服务启动后仍由对账恢复真实状态。复合约束、多重 path 约束或其他无法确认的唯一性结构会拒绝迁移并整体回滚。 + +处理步骤: + +1. 停止连接该数据库的全部 Sense 实例,并确认 Sense 与 MediaMTX 相关端口已释放。 +2. 使用 `backup-sense.bat` 生成 PostgreSQL custom-format 备份;非标准 PostgreSQL 安装目录需通过 `SENSE_POSTGRES_BIN` 指向包含 `pg_dump.exe`、`pg_restore.exe` 的目录。 +3. 使用 `pg_restore --list <备份文件>` 确认备份可读取,再部署包含 #95 的 Windows 包。 +4. 先运行 `migrate-sense.bat`;成功后确认旧路由数量不变、运行态列无空值、`path` 仍有唯一索引。 +5. 再启动 Sense,检查首页、`/healthz`、MediaMTX Control API 和视频服务对账;验证完成后使用 `stop-sense.bat` 停止。 + +如果迁移报告不支持的唯一性结构,不要手工删除约束或路由;在备份副本中核对实际约束和业务数据。正式迁移失败时保留错误并从迁移前备份恢复,不通过关闭唯一性绕过迁移。 + diff --git a/docs/task/95-Sense旧媒体路由唯一约束兼容迁移.md b/docs/task/95-Sense旧媒体路由唯一约束兼容迁移.md new file mode 100644 index 0000000..7ce7189 --- /dev/null +++ b/docs/task/95-Sense旧媒体路由唯一约束兼容迁移.md @@ -0,0 +1,80 @@ + +generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) +wiki_page: Task-95-Sense旧媒体路由唯一约束兼容迁移 +wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Task-95-Sense%E6%97%A7%E5%AA%92%E4%BD%93%E8%B7%AF%E7%94%B1%E5%94%AF%E4%B8%80%E7%BA%A6%E6%9D%9F%E5%85%BC%E5%AE%B9%E8%BF%81%E7%A7%BB.- +wiki_revision: 5f3bdf3786f2a72dac5d29236ff466743e2912b5 +synchronized_at: 2026-08-16T11:31:47Z + + +# 95 Sense旧媒体路由唯一约束兼容迁移 + +- 类型:缺陷 +- 所属 Epic:#7 +- 所属 MVP / 版本:#8 +- 状态:已完成 +- 日期:2026-08-15 +- Gitea 工单:https://git.ilapage.cn/ila/yovision/issues/95 +- Wiki 页面:Task-95-Sense旧媒体路由唯一约束兼容迁移 +- Wiki revision:见本地镜像头 + +## 背景与目标 + +#92 修复进入真实旧库后,设备能力字段已成功转换为 JSONB;下一条媒体迁移因 GORM 尝试删除不存在的推导约束名 `uni_sense_media_routes_path` 而报 SQLSTATE 42704。只读核对确认旧 `sense_media_routes.path` 实际由 PostgreSQL 自动命名约束 `sense_media_routes_path_key` 保证唯一,且表中已有 2 条路由。 + +隔离回归越过约束错误后进一步确认,旧非空表缺少当前模型要求的运行态列,直接新增 `source_ready NOT NULL` 会报 SQLSTATE 23502。目标是在不删除路由、不削弱 path 唯一性、不伪造媒体已就绪的前提下完成旧表迁移。 + +## 最终方案 + +在现有 `2026081419000` 媒体迁移事务开头执行 PostgreSQL 专用兼容步骤。仅当旧表存在时取得 ACCESS EXCLUSIVE 锁,从 pg_catalog 读取包含 path 的唯一约束;只接受唯一的单列 `UNIQUE(path)`,复合、多重或冲突结构拒绝迁移并整体回滚。 + +确认结构后,把数据库实际约束名规范为 GORM 能识别和移除的名称,让 AutoMigrate 转换为模型的 `idx_sense_media_routes_path` 唯一索引。锁在整个迁移事务提交前持续有效,因此约束切换期间没有并发写入窗口。 + +兼容步骤同时为旧路由添加并回填当前模型要求的运行态列:`source_ready=false`、`failure_count=0`、`last_error_code=''`,随后设为 NOT NULL。保守初值表示服务启动后必须重新对账,不把旧路由冒充为已经就绪;`next_retry_at` 保持可空并由 AutoMigrate 建立。 + +## 修改文件 + +- `Sense/server/cmd/migrate/migration/version/2026081419000_media.go`:旧约束识别、规范化、锁表和运行态列兼容。 +- `Sense/server/cmd/migrate/migration/version/2026081419000_media_test.go`:隔离 PostgreSQL 旧表、非空路由、空库、无表、重复执行、唯一性和不安全结构回滚测试。 +- Wiki `Troubleshooting`、`docs/06-troubleshooting.md`:错误含义、备份、迁移、验证和回退步骤。 +- `wiki-docs.json`、`docs/task/95-Sense旧媒体路由唯一约束兼容迁移.md`:任务归档登记和镜像。 + +## 验收结果 + +| 验收标准 | 结果 | +|---|---| +| 旧约束名不再触发 SQLSTATE 42704 | 通过;隔离和真实 PostgreSQL 均完成媒体迁移 | +| 既有媒体路由完整保留 | 通过;真实库迁移前后均为 2 条 | +| path 始终具有唯一性保护 | 通过;迁移后 `idx_sense_media_routes_path` 唯一索引有效,重复 path 写入被拒绝 | +| 无表、新库、已迁移库和重复兼容 | 通过 | +| 不安全结构拒绝并回滚 | 通过;复合 path 约束夹具未发生部分变更 | +| Go 全量与隔离 PostgreSQL 回归 | 通过 | +| #70 Windows 包真实迁移与启动 smoke | 通过;Web/SPA/health/MediaMTX 200,停止后端口清空 | +| Wiki 镜像与任务归档一致 | 通过 | + +## 测试 + +- 隔离 PostgreSQL 17 `TestMediaMigrationOnPostgres`:旧 2 路由、旧约束、运行态回填、空库、无表、重复兼容、唯一性冲突和复合约束回滚全部通过;独立测试数据库每次运行后删除。 +- `go test ./...`、`go vet ./...`、`go build ./...`:通过。 +- `go test -race ./cmd/migrate/migration/version -run TestMediaMigrationOnPostgres -count=1`:使用隔离 PostgreSQL 通过。 +- #70 固定 Go 1.26.5、Node 22.22.1、pnpm 9.15.1 production build 与包审计通过。 +- 迁移前 PostgreSQL custom-format 备份通过 `pg_restore --list`;真实迁移完成,2 条旧路由保留、运行态列无空值、媒体迁移版本登记、唯一索引有效。 +- Web 首页、SPA、`/healthz` 与 MediaMTX Control API 均返回 200;`stop-sense.bat` 后相关端口无监听。 +- `git diff --check`:通过。 +- **未验证部分**:尚未在客户全新 Windows 主机、客户生产 PostgreSQL 账号和真实获准摄像机上验收;当前回归使用本机 PostgreSQL、脱敏业务计数和已配置测试摄像机环境。Harness strict 仍只受既存 #66/#67 归档格式影响。 + +## 遗留问题 + +- PowerShell `Invoke-WebRequest` 在本机受代理环境影响,访问 loopback 时失败;明确绕过代理的本机 HTTP 请求验证服务正常,不属于 Sense 服务端故障。 +- #95 需先经用户验收并合入 `dev`,随后 #70 才能按依赖顺序完成合并。 + +## 相关提交 + +- `3dd4890` 兼容旧媒体路由约束和运行态列迁移。 +- `6257859` 记录旧媒体路由迁移排错。 +- `c088caf` #70 集成 #95 后用于 Windows 包真实回归。 + + +## 人工验收 + +- 2026-08-16:用户明确验收通过 #95。 +- 按依赖顺序先将 PR #96 合入 `dev`;`main` 保持不变。 diff --git a/wiki-docs.json b/wiki-docs.json index 392276b..3fb80b4 100644 --- a/wiki-docs.json +++ b/wiki-docs.json @@ -136,6 +136,10 @@ "page": "Task-70-Sense-Windows配置启动与打包交付", "path": "docs/task/70-Sense-Windows配置启动与打包交付.md" }, + { + "page": "Task-95-Sense旧媒体路由唯一约束兼容迁移", + "path": "docs/task/95-Sense旧媒体路由唯一约束兼容迁移.md" + }, { "page": "Task-97-Sense免验证码登录与管理员密码重置", "path": "docs/task/97-Sense免验证码登录与管理员密码重置.md"