# 实施计划：NS4.1 + P16 — Admin-Web 管理后台增强

> 权威参考：实施过程中如遇细节不明确，应优先查阅 PRD 原文：
> - `docs/prd/Neo_Specs/NS4.1-tenant-admin-redesign.md` — NS4.1 完整设计（数据模型 DDL、页面布局、接口设计、迁移步骤、边界条件）
> - `docs/prd/specs/P16-task-min-run-interval.md` — P16 完整设计（调度器逻辑、API 扩展、前端变更、边界条件）

## 概述

按依赖关系分两条并行线实施：模块 A（NS4.1 注册体系 + 租户管理员重构）和模块 B（P16 调度任务间隔）。两者改动文件无重叠，可交替执行。整体顺序：DDL 迁移 → 后端 API → 前端页面 → 数据迁移/切换 → 收尾。

后端使用 Python（FastAPI + Pydantic），前端使用 TypeScript（React + Vite + Ant Design）。

## 任务

### 阶段一：DDL 迁移（模块 A + B）

- [x] 1. DDL 迁移 — 模块 A：注册体系四张新表
  - [x] 1.1 创建迁移脚本 `db/zqyy_app/migrations/2026-03-22__ns41_registry_tables.sql`
    - CREATE TABLE `biz.connectors`（id SERIAL PK, connector_key VARCHAR(50) UNIQUE, display_name, is_active, created_at）
    - CREATE TABLE `biz.tenants`（id SERIAL PK, connector_id FK, tenant_id BIGINT, tenant_name, is_active, created_at, updated_at, UNIQUE(connector_id, tenant_id)）
    - CREATE TABLE `biz.sites`（id SERIAL PK, tenant_id FK, site_id BIGINT UNIQUE, site_name, site_code VARCHAR(6) UNIQUE, site_label, is_active, created_at, updated_at）
    - CREATE TABLE `biz.site_code_history`（id SERIAL PK, site_id BIGINT, site_code VARCHAR(6) UNIQUE, is_current BOOLEAN, created_at, retired_at）
    - INSERT 种子数据：connectors('feiqiu')、tenants(朗朗桌球)
    - INSERT 迁移数据：从 `auth.site_code_mapping` 迁移真实数据到 `biz.sites`，创建 `site_code_history` 记录
    - 编写回滚脚本（逆序 DROP TABLE）
    - _需求: A1.1, A1.2, A1.3, A1.4, A1.5_

- [x] 2. DDL 迁移 — 模块 B：scheduled_tasks 新增字段
  - [x] 2.1 创建迁移脚本 `db/zqyy_app/migrations/2026-03-22__p16_min_run_interval.sql`
    - ALTER TABLE `scheduled_tasks` ADD COLUMN `min_run_interval_value` INTEGER NOT NULL DEFAULT 0
    - ALTER TABLE `scheduled_tasks` ADD COLUMN `min_run_interval_unit` VARCHAR(20) NOT NULL DEFAULT 'minutes'
    - ALTER TABLE `scheduled_tasks` ADD COLUMN `last_success_at` TIMESTAMPTZ
    - 添加 COMMENT ON COLUMN 注释
    - 编写回滚脚本（ALTER TABLE DROP COLUMN）
    - _需求: B1.1, B1.2_

### 阶段二：后端 API — 模块 A（注册体系 + 管理员重构）

- [x] 3. 后端 Schema — 注册体系
  - [x] 3.1 创建 `apps/backend/app/schemas/admin_registry.py`
    - 定义 `TenantItem`（id, tenant_id, tenant_name, connector_name, is_active）
    - 定义 `SiteItem`（id, site_id, site_name, site_code, site_label, is_active）
    - 定义 `UpdateSiteCodeRequest`（new_code: str）
    - 定义 `SiteCodeResult`（site_id, old_code, new_code, history_cleaned）
    - 定义 `SiteCodeHistoryItem`（id, site_code, is_current, created_at, retired_at）
    - _需求: A2.1, A2.2, A2.4, A2.5_

  - [x] 3.2 修改 `apps/backend/app/schemas/admin_tenant_admins.py`
    - `TenantAdminListItem` 新增 `tenant_name` 字段
    - `TenantAdminCreateRequest` 添加字段说明注释（tenant_id 从 biz.tenants 选择）
    - _需求: A2.6_

- [x] 4. 后端路由 — 注册体系 API
  - [x] 4.1 创建 `apps/backend/app/routers/admin_registry.py`
    - `GET /api/admin/tenants` — 所有活跃租户列表（JOIN biz.connectors 获取 connector_name）
    - `GET /api/admin/tenants/{tenant_id}/sites` — 指定租户下所有活跃店铺
    - `PUT /api/admin/sites/{site_id}/site-code` — 设置/修改简写ID（事务内执行）
      - 校验格式（6 位，3+3，统一大写）
      - 校验全局唯一（biz.sites + biz.site_code_history）
      - 事务：旧 code 标记 retired → 新 code 插入 history → 更新 sites.site_code
      - 检查旧 code 是否有未审核申请引用，决定是否清理历史记录
    - `GET /api/admin/sites/{site_id}/site-code-history` — 简写ID 变更历史
    - _需求: A2.1, A2.2, A2.4, A2.5, A3.1, A3.2, A3.3, A3.4_

  - [x] 4.2 在 `apps/backend/app/main.py` 中注册 admin_registry router
    - _需求: A2.1_

- [x] 5. 后端路由 — 管理员 CRUD 扩展
  - [x] 5.1 修改 `apps/backend/app/routers/admin_tenant_admins.py`
    - 新增 `DELETE /api/admin/tenant-admins/{id}` — 软删除（is_active=false），已禁用返回 409
    - 修改 `POST /api/admin/tenant-admins` — 创建时校验 tenant_id 在 biz.tenants 中存在
    - 修改 `GET /api/admin/tenant-admins` — 默认 is_active=true，新增 include_inactive 查询参数
    - 修改 `PATCH /api/admin/tenant-admins/{id}` — 支持修改 `username`（校验全局唯一性，冲突返回 409）
    - 列表查询 JOIN biz.tenants 获取 tenant_name
    - _需求: A2.3, A2.6, A2.7, A2.8, A4.1_

- [x] 6. 编写属性测试 — 模块 A
  - [x] 6.1 创建 `tests/test_site_code_props.py`
    - **Property 1: 简写ID 全局唯一性** — 使用 Hypothesis 生成随机 code，验证已存在的 code 被拒绝
    - **Property 2: 简写ID 变更事务完整性** — 验证事务后 sites.site_code、history.is_current、history.retired_at 状态一致
    - **Property 3: 简写ID 格式校验** — 生成随机字符串，验证仅 6 位 3+3 格式通过
    - **验证: 需求 A3.1, A3.2**

  - [x] 6.2 创建 `tests/test_tenant_admin_props.py`（扩展已有文件或新建）
    - **Property 4: 租户管理员软删除一致性** — 删除后默认列表不返回，include_inactive 返回
    - **验证: 需求 A2.3, A2.7**

- [x] 7. 检查点 — 模块 A 后端验证
  - 确保注册体系 API 和管理员 CRUD 扩展所有测试通过，ask the user if questions arise.

### 阶段三：后端 API — 模块 B（调度器间隔）

- [x] 8. 后端 Schema + 路由 — 调度器间隔
  - [x] 8.1 修改 `apps/backend/app/schemas/schedules.py`
    - `CreateScheduleRequest` 新增 `min_run_interval_value`（int, default=0）、`min_run_interval_unit`（str, default='minutes'）
    - `UpdateScheduleRequest` 新增同上两个可选字段
    - `ScheduleResponse` 新增 `min_run_interval_value`、`min_run_interval_unit`、`last_success_at`
    - _需求: B3.1, B3.2_

  - [x] 8.2 修改 `apps/backend/app/services/scheduler.py`
    - 新增 `_convert_interval_to_seconds(value, unit)` 辅助函数
    - 扩展 `check_and_enqueue()` SQL 查询：新增读取 min_run_interval_value, min_run_interval_unit, last_run_at, last_status
    - 新增并发检查：last_status == 'running' → 跳过，日志记录 skipped_concurrent
    - 新增间隔检查：min_run_interval_value > 0 且 now - last_run_at < min_interval → 跳过，推进 next_run_at
    - _需求: B2.1, B2.2, B2.4_

  - [x] 8.3 修改任务完成回调（`scheduler.py` 或 `task_queue.py`）
    - 成功时：`last_status='completed'`, `last_success_at=NOW()`
    - 失败时：`last_status='failed'`（last_success_at 不变）
    - _需求: B2.3_

  - [x] 8.4 修改 `apps/backend/app/routers/schedules.py`
    - 创建/更新端点支持新字段写入
    - 列表端点响应包含新字段
    - `POST /api/schedules/{id}/run` 新增 `force: bool = False` 查询参数
    - force=false 时检查并发和间隔，不满足返回 409
    - force=true 时绕过所有检查
    - _需求: B3.1, B3.2, B3.3, B3.4, B3.5_

- [x] 9. 编写属性测试 — 模块 B
  - [x] 9.1 创建 `tests/test_scheduler_interval_props.py`
    - **Property 7: 间隔转换正确性** — 生成随机 (value, unit)，验证秒数计算正确
    - **Property 8: 调度器间隔跳过正确性** — 生成随机任务状态，验证跳过/执行决策
    - **Property 9: 调度器并发跳过正确性** — last_status='running' 时跳过
    - **Property 10: 强制执行绕过所有检查** — force=true 时无论状态都执行
    - **Property 11: last_success_at 仅成功时更新** — 成功更新，失败不变
    - **验证: 需求 B2.1, B2.2, B2.3, B2.4, B3.4**

- [x] 10. 检查点 — 模块 B 后端验证
  - 确保调度器间隔逻辑和 API 扩展所有测试通过，ask the user if questions arise.

### 阶段四：ETL 店铺同步（模块 A）

- [x] 11. 后端 — 店铺信息增量同步
  - [x] 11.1 实现同步逻辑（新建 service 或在 admin_registry 路由中实现）
    - 通过 FDW 读取 ETL 库 `dwd.dim_site`（`scd2_is_current=1`）
    - 对比 `biz.sites`：新增店铺 INSERT（site_code 留空，tenant_id 通过 dim_site.tenant_id 关联 biz.tenants），名称/标签变更 UPDATE
    - 不删除已有店铺记录
    - _需求: A5.1, A5.2_

  - [x] 11.2 实现手动触发端点
    - `POST /api/admin/sites/sync` — 手动触发同步，返回同步结果（新增数/更新数）
    - _需求: A5.3_

  - [x] 11.3 执行一次初始同步（数据迁移补数据）
    - 在 DDL 迁移（任务 1）完成后、代码切换（任务 15）之前，调用同步逻辑补充 `auth.site_code_mapping` 中没有但 `dwd.dim_site` 中有的店铺
    - 输出同步结果（新增数/更新数）供验证使用
    - _需求: A1b.1, A1b.2_

  - [x] 11.4 预留定时触发入口（随 ETL DWD 完成后通过内部 API 触发）
    - _需求: A5.4_

### 阶段五：前端页面

- [x] 12. 前端 — 模块 A：租户管理员页面重构
  - [x] 12.1 创建 `apps/admin-web/src/api/registry.ts`
    - 封装 `GET /api/admin/tenants` 和 `GET /api/admin/tenants/{id}/sites` API 调用
    - 封装 `PUT /api/admin/sites/{site_id}/site-code` 和 `GET /api/admin/sites/{site_id}/site-code-history`
    - 封装 `POST /api/admin/sites/sync`（手动同步）
    - _需求: A4.5_

  - [x] 12.2 修改 `apps/admin-web/src/api/tenantAdmins.ts`
    - 新增 `deleteTenantAdmin(id)` API 调用
    - 修改 `listTenantAdmins` 支持 `include_inactive` 参数
    - _需求: A4.1_

  - [x] 12.3 重构 `apps/admin-web/src/pages/TenantAdmins/index.tsx`
    - 列表页新增「删除」操作按钮（Popconfirm 二次确认 → 调用 DELETE API）
    - 列表页新增「显示已禁用」Switch 开关
    - 列表页新增「简写ID」操作按钮（打开简写ID 管理弹窗）
    - 列表新增「租户」列（显示 tenant_name）
    - 编辑弹窗中 `username` 改为可编辑（需校验唯一性，409 时提示"用户名已存在"）；`tenant_id` 只读
    - _需求: A4.1, A4.3, A2.8_

  - [x] 12.4 实现 2 步创建流程
    - 使用 Ant Design Steps 组件
    - 第 1 步：选择租户（Select，数据源 GET /api/admin/tenants）→ 输入用户名/密码/显示名称 → 选择管辖门店（Select multiple，数据源 GET /api/admin/tenants/{id}/sites）
    - 第 2 步：展示所选租户下所有店铺，可为每个店铺设置简写ID（可跳过）
    - _需求: A4.2_

  - [x] 12.5 实现简写ID 管理弹窗
    - Modal 内嵌 Table：店铺名称、当前 ID、操作（修改）
    - 修改行：Input + 保存/取消按钮，格式校验（6 位 3+3）
    - 变更历史区域：展示 site_code_history 列表
    - _需求: A4.3, A4.4_

- [x] 13. 前端 — 模块 B：ScheduleTab 扩展
  - [x] 13.1 修改 `apps/admin-web/src/components/ScheduleTab.tsx`
    - 创建/编辑表单新增「最小运行间隔」行：InputNumber（数值）+ Select（单位：分钟/小时/天）
    - 数值为 0 时显示 placeholder "无限制"
    - 位置：在调度类型配置区域下方
    - _需求: B4.1_

  - [x] 13.2 修改列表表格
    - 新增「最小间隔」列：显示格式如"10 天"、"1 小时"、"无限制"（value=0 时）
    - 新增「上次成功」列：显示 last_success_at 的相对时间（dayjs fromNow）
    - _需求: B4.2_

  - [x] 13.3 修改手动执行确认框
    - 新增 Checkbox「强制执行（忽略最小间隔）」，默认不勾选
    - 勾选后调用 `POST /api/schedules/{id}/run?force=true`
    - 不勾选时调用 `POST /api/schedules/{id}/run`，409 时展示错误提示
    - _需求: B4.3, B4.4_

- [x] 14. 检查点 — 前端页面验证
  - 确保所有前端组件渲染正常，API 调用层工作正确，ask the user if questions arise.

### 阶段六：数据迁移与代码切换

- [x] 15. site_code 查询源切换
  - [x] 15.1 修改 `apps/backend/app/routers/tenant_users.py`
    - `match-suggestions` 中的 site_code 查询从 `auth.site_code_mapping` 切换到 `biz.sites` + `biz.site_code_history`
    - _需求: A6.1_

  - [x] 15.2 搜索并修改所有其他引用 `auth.site_code_mapping` 的代码
    - 小程序端用户申请时的 site_code 验证
    - 其他后端路由中的 site_code 查询
    - _需求: A6.1, A6.2_

  - [x] 15.3 验证切换后功能正常
    - 用户申请流程中 site_code 查询正确
    - 关联建议匹配正确
    - _需求: A6.3_

- [x] 16. 废弃原表
  - [x] 16.1 验证 `biz.sites` 数据与 `auth.site_code_mapping` 一致
    - 编写验证 SQL 对比两表数据
    - _需求: A1.5_

  - [x] 16.2 重命名原表为 `auth._archived_site_code_mapping`
    - _需求: A1.6_

### 阶段七：收尾

- [x] 17. 数据库变更审计与 DDL 合并
  - [x] 17.1 审计本次实现中对数据库的所有改动
    - 检查新建表（biz.connectors/tenants/sites/site_code_history）、新增字段（scheduled_tasks 三字段）、废弃表（auth.site_code_mapping）
  - [x] 17.2 执行两个迁移脚本到测试库（`test_zqyy_app`）
    - 验证新表和新字段已正确创建（使用 BD 手册中的验证 SQL）
  - [x] 17.3 合并到主 DDL 基线文件
    - 模块 A 新表 → `docs/database/ddl/zqyy_app__biz.sql`
    - 模块 B 新字段 → `docs/database/ddl/zqyy_app__public.sql`
  - [x] 17.4 验证回滚脚本可执行（任务 1、2 中已编写）

- [x] 18. BD 手册更新
  - [x] 18.1 创建 `docs/database/BD_Manual_biz_registry_tables.md`
    - 覆盖 biz.connectors、biz.tenants、biz.sites、biz.site_code_history 四张表
    - 包含：字段明细、约束与索引、验证 SQL（≥3 条）、回滚策略
    - _规范: db-docs.md_
  - [x] 18.2 更新 `docs/database/BD_Manual_tenant_admin_tables.md`
    - 补充软删除逻辑说明、tenant_id 从 biz.tenants 选择的变更
  - [x] 18.3 创建/更新 `docs/database/BD_Manual_scheduled_tasks.md`
    - 新增 min_run_interval_value、min_run_interval_unit、last_success_at 字段说明
    - 包含：字段明细、约束、验证 SQL、回滚策略

- [x] 19. 前后端联调与集成验证
  - [x] 19.1 启动后端服务，使用测试库验证各端点完整请求-响应链路
    - 验证注册体系 API（tenants/sites/site-code）JSON 响应结构与 Schema 定义一致
    - 验证调度器 API（schedules）新增字段和 force 参数正常工作
    - 验证权限校验在真实请求中生效
  - [x] 19.2 前端联调验证
    - 确认租户管理员页面能正确调用新增 API 并渲染数据（2 步创建、删除、简写ID 管理）
    - 确认 ScheduleTab 扩展字段正确展示和提交
    - 验证空数据/降级场景下前端不崩溃

- [x] 20. 文档同步更新
  - [x] 20.1 更新后端 API 参考文档
    - 在 `apps/backend/docs/API-REFERENCE.md` 新增 admin_registry 路由模块文档
    - 更新 schedules 路由模块文档（新增字段和 force 参数）
    - 更新 `apps/backend/README.md` 路由模块摘要
  - [x] 20.2 更新 admin-web README
    - 在 `apps/admin-web/README.md` 更新页面说明（租户管理员重构、ScheduleTab 扩展）
  - [x] 20.3 更新文档地图
    - 在 `docs/DOCUMENTATION-MAP.md` 新增本次模块条目（BD 手册、Spec）
    - _规范: doc-map.md_

- [x] 21. 最终检查点 — 全量验证
  - 运行 Monorepo 属性测试：`cd C:\Project\NeoZQYY && pytest tests/ -v`
  - 运行后端单元测试：`cd apps/backend && pytest tests/ -v`
  - 确保所有属性测试（Property 1-11）和单元测试全部通过
  - 确保 DDL 迁移已合并到主基线
  - 确保 BD 手册已同步更新
  - 确保 API 文档、后端 README、admin-web README、文档地图均已更新
  - 确保前端页面连接真实后端运行正常（租户管理员页面 + ScheduleTab）
  - 确保 `auth.site_code_mapping` 已废弃重命名
  - ask the user if questions arise.

- [x] 22. 服务清理
  - [x] 22.1 关闭浏览器、停止后端和前端服务、清理资源
    - 停止 uvicorn 后端进程（controlPwshProcess stop）
    - 停止前端开发服务器（controlPwshProcess stop）

## 备注

- 标记 `*` 的子任务为可选（属性测试），可跳过以加速 MVP
- 每个任务引用了具体的需求编号以确保可追溯性（A1-A6 对应 NS4.1，B1-B4 对应 P16）
- 属性测试验证 11 个正确性属性（Property 1-11），单元测试验证具体边界条件
- 检查点任务确保增量验证，避免问题累积（任务 7、10、14、21）
- 模块 A 和模块 B 改动文件无重叠，可交替执行
- 后端使用 Python（FastAPI + Pydantic + Hypothesis），前端使用 TypeScript（React + Vite + Ant Design）
- 数据迁移采用渐进策略：新建表 → 迁移数据 → 切换代码 → 验证 → 废弃原表
- 收尾阶段遵循 `spec-closing-checklist.md`（全栈类 Spec，步骤 1-6 全覆盖）：
  - 步骤 1（最终测试）→ 任务 21
  - 步骤 2（前后端联调）→ 任务 19
  - 步骤 3（DDL 合并）→ 任务 17
  - 步骤 4（BD 手册）→ 任务 18
  - 步骤 5（文档同步）→ 任务 20
  - 步骤 6（服务清理）→ 任务 22
