|
| 1 | +# 出口 IP 唯一约束修复实施计划 |
| 2 | + |
| 3 | +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
| 4 | +
|
| 5 | +**Goal:** 让多条代理线路可以处于未检测状态或共享同一真实出口 IP,并通过 |
| 6 | +`v4.2.19` 自动升级现有 SQLite 数据库。 |
| 7 | + |
| 8 | +**Architecture:** 使用 SQLite 表重建迁移移除 `egress_ips.ip_address` 的唯一 |
| 9 | +约束,同时临时备份并恢复引用它的绑定表。API 和前端统一用空字符串表达尚未 |
| 10 | +检测的 IP,真实地址继续由探测流程写入。 |
| 11 | + |
| 12 | +**Tech Stack:** Go 1.26、`database/sql`、`modernc.org/sqlite`、React 19、 |
| 13 | +TypeScript、Vitest、GitHub Actions。 |
| 14 | + |
| 15 | +## Global Constraints |
| 16 | + |
| 17 | +- v1 仍为单宿主机部署,不改变容器、隧道或出口验证架构。 |
| 18 | +- 每个用户容器仍必须绑定至少一个出口资源。 |
| 19 | +- 不新增第三方运行时依赖。 |
| 20 | +- 所有仓库路径使用项目根目录相对路径。 |
| 21 | +- 发布版本固定为 `v4.2.19`。 |
| 22 | + |
| 23 | +--- |
| 24 | + |
| 25 | +### Task 1: SQLite 升级迁移 |
| 26 | + |
| 27 | +**Files:** |
| 28 | +- Create: `internal/store/repository/migration_0003_test.go` |
| 29 | +- Create: `internal/store/migrations/0003_drop_egress_ip_address_unique.sql` |
| 30 | + |
| 31 | +**Interfaces:** |
| 32 | +- Consumes: `migrator.RunMigrations(context.Context, *sql.DB, embed.FS)` |
| 33 | +- Produces: 最终结构中 `egress_ips.ip_address TEXT NOT NULL` 且不唯一 |
| 34 | + |
| 35 | +- [x] **Step 1: 写失败的升级测试** |
| 36 | + |
| 37 | +测试先建立仅应用 `0001`、`0002` 的旧数据库,写入用户、主机、出口和绑定, |
| 38 | +再通过 migrator 应用 `0003`。断言重复 IP 可插入、绑定保留、外键仍生效。 |
| 39 | + |
| 40 | +- [x] **Step 2: 运行测试确认红灯** |
| 41 | + |
| 42 | +Run: `go test ./internal/store/repository -run TestMigration0003 -count=1` |
| 43 | + |
| 44 | +Expected: 因缺少 `0003_drop_egress_ip_address_unique.sql` 或重复 IP 唯一冲突失败。 |
| 45 | + |
| 46 | +- [x] **Step 3: 实现最小迁移** |
| 47 | + |
| 48 | +迁移按以下顺序执行:备份 `host_egress_bindings`、删除子表、重建 |
| 49 | +`egress_ips`、复制数据、恢复子表和索引、删除备份表。 |
| 50 | + |
| 51 | +- [x] **Step 4: 运行迁移测试确认绿灯** |
| 52 | + |
| 53 | +Run: `go test ./internal/store/repository -run TestMigration0003 -count=1` |
| 54 | + |
| 55 | +Expected: PASS。 |
| 56 | + |
| 57 | +### Task 2: API 空 IP 契约 |
| 58 | + |
| 59 | +**Files:** |
| 60 | +- Modify: `internal/controlplane/http/admin_egress_ips_test.go` |
| 61 | +- Modify: `internal/controlplane/http/admin_egress_ips.go` |
| 62 | + |
| 63 | +**Interfaces:** |
| 64 | +- Consumes: `createEgressIPRequest.IPAddress`、`updateEgressIPRequest.IPAddress` |
| 65 | +- Produces: 空字符串合法;非空值必须通过 `net.ParseIP` |
| 66 | + |
| 67 | +- [x] **Step 1: 写创建空 IP 和更新非法 IP 的失败测试** |
| 68 | + |
| 69 | +创建请求传 `ip_address: ""` 应返回 201;更新请求传 `not-an-ip` 应返回 400。 |
| 70 | + |
| 71 | +- [x] **Step 2: 运行测试确认红灯** |
| 72 | + |
| 73 | +Run: `go test ./internal/controlplane/http -run TestAdminEgressIPsHandler -count=1` |
| 74 | + |
| 75 | +Expected: 创建空 IP 用例得到 400,或更新非法 IP 用例得到 200。 |
| 76 | + |
| 77 | +- [x] **Step 3: 实现统一校验** |
| 78 | + |
| 79 | +对创建和更新都执行 `strings.TrimSpace`;仅在非空时调用 `net.ParseIP`。 |
| 80 | + |
| 81 | +- [x] **Step 4: 运行 API 测试确认绿灯** |
| 82 | + |
| 83 | +Run: `go test ./internal/controlplane/http -run TestAdminEgressIPsHandler -count=1` |
| 84 | + |
| 85 | +Expected: PASS。 |
| 86 | + |
| 87 | +### Task 3: 前端不再生成占位 IP |
| 88 | + |
| 89 | +**Files:** |
| 90 | +- Create: `web/admin/src/lib/egress-ip-address.ts` |
| 91 | +- Create: `web/admin/src/lib/__tests__/egress-ip-address.test.ts` |
| 92 | +- Modify: `web/admin/src/components/egress-ips/egress-ip-drawer.tsx` |
| 93 | + |
| 94 | +**Interfaces:** |
| 95 | +- Produces: `normalizeEgressIPAddress(value: string): string` |
| 96 | +- Consumes: 表单 `values.ip_address` |
| 97 | + |
| 98 | +- [x] **Step 1: 写失败的纯函数测试** |
| 99 | + |
| 100 | +断言空白输入返回空字符串,显式 IP 去除首尾空格后保持原值。 |
| 101 | + |
| 102 | +- [x] **Step 2: 运行测试确认红灯** |
| 103 | + |
| 104 | +Run: `pnpm test:unit -- src/lib/__tests__/egress-ip-address.test.ts` |
| 105 | + |
| 106 | +Expected: 因模块或导出不存在失败。 |
| 107 | + |
| 108 | +- [x] **Step 3: 实现函数并接入提交载荷** |
| 109 | + |
| 110 | +`normalizeEgressIPAddress` 只执行 `trim()`;抽屉提交时直接把结果放入 |
| 111 | +`ip_address`,删除 `0.0.0.0` 回退逻辑和未使用的正则。 |
| 112 | + |
| 113 | +- [x] **Step 4: 运行前端测试确认绿灯** |
| 114 | + |
| 115 | +Run: `pnpm test:unit -- src/lib/__tests__/egress-ip-address.test.ts` |
| 116 | + |
| 117 | +Expected: PASS。 |
| 118 | + |
| 119 | +### Task 4: 验证与发布 |
| 120 | + |
| 121 | +**Files:** |
| 122 | +- Modify: `.planning/debug/egress-ip-unique-collision.md` |
| 123 | + |
| 124 | +- [x] **Step 1: 运行格式化与定向验证** |
| 125 | + |
| 126 | +Run: `gofmt -w internal/controlplane/http/admin_egress_ips.go internal/controlplane/http/admin_egress_ips_test.go internal/store/repository/migration_0003_test.go` |
| 127 | + |
| 128 | +Run: `go test ./internal/store/... ./internal/controlplane/http/...` |
| 129 | + |
| 130 | +Run: `pnpm test:unit && pnpm build` |
| 131 | + |
| 132 | +Run: `pnpm typecheck`,并与 `origin/main` 基线比较;本次不新增类型错误。 |
| 133 | + |
| 134 | +- [x] **Step 2: 运行全量 Go 验证** |
| 135 | + |
| 136 | +Run: `go test ./...` |
| 137 | + |
| 138 | +Expected: PASS。 |
| 139 | + |
| 140 | +- [x] **Step 3: 检查隐私、差异和迁移结构** |
| 141 | + |
| 142 | +确认新增文件没有绝对路径、凭据或个人信息;确认迁移后 |
| 143 | +`PRAGMA foreign_key_check` 无结果。 |
| 144 | + |
| 145 | +- [ ] **Step 4: 提交并发布** |
| 146 | + |
| 147 | +提交聚焦改动,推送修复分支,合入 `main`,在合入提交上创建并推送 |
| 148 | +`v4.2.19` 标签。 |
| 149 | + |
| 150 | +- [ ] **Step 5: 监控发布完成** |
| 151 | + |
| 152 | +确认 Release workflow、GitHub Release、控制面镜像和 managed-user 镜像均成功。 |
0 commit comments