# 需求文档 — NS4.1 + P16：Admin-Web 管理后台增强

## 简介

本 Spec 合并两个独立需求：NS4.1（租户管理员页面重构 + 项目级注册体系）和 P16（调度任务最小运行间隔机制）。两者均为 admin-web 管理后台的功能增强，改动文件无重叠，合并执行以减少上下文切换。

### 合并理由

- 两者都是 admin-web 后台功能迭代，共享同一前端项目（`apps/admin-web/`）和后端项目（`apps/backend/`）
- NS4.1 改动集中在租户管理员页面（`TenantAdmins/`）+ 新建注册体系路由，P16 改动集中在调度任务页面（`ScheduleTab.tsx`）+ 调度器逻辑
- 改动文件完全不重叠，合并不增加冲突风险
- 共享收尾流程（DDL 合并、BD 手册、文档同步）

### 依赖

- NS4（租户管理后台基础设施）— `tenant-admin-web` spec 已完成
- P3（用户认证体系）— `03-miniapp-auth-system` spec 已完成

### 来源文档（权威参考）

实施过程中如遇细节不明确，应优先查阅以下 PRD 原文：

- `docs/prd/Neo_Specs/NS4.1-tenant-admin-redesign.md` — NS4.1 PRD 主文档（数据模型 DDL、页面布局、接口设计、迁移步骤、边界条件）
- `docs/prd/specs/P16-task-min-run-interval.md` — P16 PRD 主文档（调度器逻辑、API 扩展、前端变更、边界条件）

### 不在本 spec 范围

- P15（AI 监控后台 + 测试重建 + 回填）— 单独 Spec
- 多连接器完整实现（仅预留 `biz.connectors` 表结构）
- `dwd.dim_site` 物理迁移（保留在 `dwd` schema）
- 租户管理员自助注册
- 简写ID 自动生成
- ETL 任务注册机制修改（`TaskMeta`/`TaskRegistry`）
- 批量 seed SQL 设定初始间隔值

## 术语表

- **admin-web**：系统管理后台（`apps/admin-web/`），面向系统管理员
- **tenant-admin**：租户管理后台（`apps/tenant-admin/`），面向租户管理员
- **biz.connectors**：连接器注册表，记录接入的上游 SaaS 系统
- **biz.tenants**：租户注册表，连接器下的租户
- **biz.sites**：店铺注册表，合并原 `auth.site_code_mapping`
- **biz.site_code_history**：简写ID 变更历史表
- **site_code**：店铺简写ID，6 位字符（3+3 格式，如 `LLQ001`）
- **min_run_interval**：调度任务最小运行间隔，任务开始执行后的最小等待时间
- **scheduled_tasks**：调度任务表（`public.scheduled_tasks`），存储 ETL 调度配置
- **scheduler.py**：调度器核心逻辑，每 30 秒轮询到期任务

## 需求

### 模块 A：NS4.1 — 租户管理员页面重构 + 项目级注册体系

#### 需求 A1：项目级注册体系 — 连接器/租户/店铺三级表

**用户故事：** 作为系统管理员，我希望建立「连接器 → 租户 → 店铺」三级注册体系，以便统一管理上游 SaaS 系统、租户和店铺的关系，并为简写ID 提供归属。

##### 验收标准

1. THE Backend SHALL 在 `biz` schema 新建 `connectors` 表（id, connector_key UNIQUE, display_name, is_active, created_at），初始数据插入 `('feiqiu', '飞球')`
2. THE Backend SHALL 在 `biz` schema 新建 `tenants` 表（id, connector_id FK, tenant_id BIGINT, tenant_name, is_active, created_at, updated_at），UNIQUE(connector_id, tenant_id)
3. THE Backend SHALL 在 `biz` schema 新建 `sites` 表（id, tenant_id FK, site_id BIGINT UNIQUE, site_name, site_code VARCHAR(6) UNIQUE, site_label, is_active, created_at, updated_at）
4. THE Backend SHALL 在 `biz` schema 新建 `site_code_history` 表（id, site_id BIGINT, site_code VARCHAR(6) UNIQUE, is_current BOOLEAN, created_at, retired_at）
5. THE Backend SHALL 将 `auth.site_code_mapping` 中真实数据（`tenant_id IS NOT NULL`）迁移到 `biz.sites`，并为已有 `site_code` 创建 `site_code_history` 记录（`is_current=true`）
6. THE Backend SHALL 在迁移完成并验证后，将 `auth.site_code_mapping` 重命名为 `auth._archived_site_code_mapping`

#### 需求 A1b：数据迁移后初始同步

**用户故事：** 作为系统管理员，我希望迁移完成后立即运行一次 ETL 同步，补充 `biz.sites` 中缺失的店铺（`dwd.dim_site` 中有但 `auth.site_code_mapping` 中没有的），确保注册体系数据完整。

##### 验收标准

1. THE Backend SHALL 在数据迁移（A1）完成后、代码切换（A6）之前，执行一次店铺同步（复用 A5 的同步逻辑），将 `dwd.dim_site`（`scd2_is_current=1`）中存在但 `biz.sites` 中不存在的店铺补充插入
2. THE Backend SHALL 在初始同步完成后输出同步结果（新增数/更新数），供验证使用

#### 需求 A2：租户/店铺管理 API

**用户故事：** 作为系统管理员，我希望通过 API 查询租户列表和店铺列表，以便在创建管理员时选择所属租户和管辖门店。

##### 验收标准

1. THE Backend SHALL 实现 `GET /api/admin/tenants` 端点，返回所有活跃租户（含连接器名称）
2. THE Backend SHALL 实现 `GET /api/admin/tenants/{tenant_id}/sites` 端点，返回指定租户下所有活跃店铺（含当前 site_code）
3. THE Backend SHALL 实现 `DELETE /api/admin/tenant-admins/{id}` 端点，软删除管理员（`is_active=false`）
4. THE Backend SHALL 实现 `PUT /api/admin/sites/{site_id}/site-code` 端点，设置/修改店铺简写ID
5. THE Backend SHALL 实现 `GET /api/admin/sites/{site_id}/site-code-history` 端点，查看简写ID 变更历史
6. THE Backend SHALL 修改 `POST /api/admin/tenant-admins` 端点，创建时 `tenant_id` 从 `biz.tenants` 选择，`managed_site_ids` 从 `biz.sites` 选择
7. THE Backend SHALL 修改 `GET /api/admin/tenant-admins` 端点，默认只返回 `is_active=true`，增加 `include_inactive` 参数
8. THE Backend SHALL 修改 `PATCH /api/admin/tenant-admins/{id}` 端点，支持修改 `username`（需校验全局唯一性，冲突返回 409）

#### 需求 A3：简写ID 管理逻辑

**用户故事：** 作为系统管理员，我希望在管理后台设置和修改店铺简写ID，并保留变更历史，以便保护已提交但未审核的用户申请。

##### 验收标准

1. WHEN 修改简写ID 时，THE Backend SHALL 在事务内执行：旧 code 标记 `is_current=false` + `retired_at=NOW()`，新 code 插入 `site_code_history`（`is_current=true`），更新 `biz.sites.site_code`
2. THE Backend SHALL 校验新 code 格式（6 位，3+3 模式，统一大写存储）和全局唯一性（含 `biz.sites.site_code` + `biz.site_code_history.site_code`）
3. WHEN 旧 code 有未审核申请引用（`auth.user_applications WHERE site_code = :old_code AND status = 'pending'`）时，THE Backend SHALL 保留历史记录不删除
4. WHEN 旧 code 无任何申请引用时，THE Backend SHALL 从 `biz.site_code_history` 中删除该条记录

#### 需求 A4：租户管理员页面重构（admin-web）

**用户故事：** 作为系统管理员，我希望在 admin-web 中通过改进的界面管理租户管理员，支持 2 步创建流程、软删除和简写ID 管理。

##### 验收标准

1. THE admin-web SHALL 重构租户管理员列表页，新增「删除」操作按钮（二次确认 → 软删除），默认只显示活跃记录，可选「显示已禁用」开关
2. THE admin-web SHALL 实现 2 步创建流程：第 1 步选择租户（下拉 `biz.tenants`）+ 输入账号信息 + 选择管辖门店（`biz.sites`）；第 2 步可选设置简写ID
3. THE admin-web SHALL 在编辑弹窗中增加「管理简写ID」区域，展示该租户下所有店铺及其当前 code，支持修改；编辑时所属租户（`tenant_id`）为只读不可修改；用户名（`username`）可修改（需校验唯一性）
4. THE admin-web SHALL 新增简写ID 管理弹窗，展示变更历史，支持修改操作
5. THE admin-web SHALL 新增 `src/api/registry.ts` 封装租户/店铺列表 API 调用

#### 需求 A5：ETL 店铺信息增量同步

**用户故事：** 作为系统管理员，我希望 ETL 完成后能自动同步店铺信息到业务库，以便 `biz.sites` 中的店铺名称和标签保持最新。

##### 验收标准

1. THE Backend SHALL 实现店铺同步逻辑：通过 FDW 读取 ETL 库 `dwd.dim_site`（`scd2_is_current=1`），对比 `biz.sites`，新增店铺 INSERT（`site_code` 留空），名称/标签变更 UPDATE
2. THE Backend SHALL 不删除已有店铺记录（即使上游标记为关闭）
3. THE Backend SHALL 支持手动触发同步（管理后台按钮或 API 端点）
4. THE Backend SHALL 支持定时触发同步（随 ETL 日常调度，DWD 层完成后通过内部 API 触发）

#### 需求 A6：后端代码切换 — site_code 查询源

**用户故事：** 作为后端开发者，我希望所有读取 `auth.site_code_mapping` 的代码切换到 `biz.sites` + `biz.site_code_history`，以便完成数据迁移。

##### 验收标准

1. THE Backend SHALL 将所有读取 `auth.site_code_mapping` 的查询切换到 `biz.sites`
2. THE 小程序端 SHALL 将用户申请时的 `site_code` 查询从 `auth.site_code_mapping` 切换到 `biz.sites` + `biz.site_code_history`
3. THE Backend SHALL 确保切换后所有现有功能（用户申请、关联建议匹配等）正常工作

---

### 模块 B：P16 — 调度任务最小运行间隔机制

#### 需求 B1：scheduled_tasks 表扩展

**用户故事：** 作为管理员，我希望为每个调度任务设置最小运行间隔，使任务即使调度到期也不会在间隔内重复执行。

##### 验收标准

1. THE Backend SHALL 在 `scheduled_tasks` 表新增 `min_run_interval_value`（INTEGER DEFAULT 0）、`min_run_interval_unit`（VARCHAR(20) DEFAULT 'minutes'）、`last_success_at`（TIMESTAMPTZ NULL）3 个字段
2. THE Backend SHALL 确保 `min_run_interval_value = 0` 表示无限制，与现有行为完全一致（向后兼容）

#### 需求 B2：调度器核心逻辑 — 并发检查 + 间隔检查

**用户故事：** 作为管理员，我希望调度器在轮询时自动检查最小间隔和并发状态，避免任务重复执行或并发执行。

##### 验收标准

1. THE scheduler SHALL 在 `check_and_enqueue()` 中新增并发检查：若 `last_status = 'running'`，跳过本次入队，日志记录 `skipped_concurrent`
2. THE scheduler SHALL 在 `check_and_enqueue()` 中新增间隔检查：若 `min_run_interval_value > 0` 且 `now() - last_run_at < min_interval_seconds`，跳过本次执行并推进 `next_run_at`，日志记录 `skipped_interval`
3. WHEN `last_run_at IS NULL`（从未执行）时，THE scheduler SHALL 跳过间隔检查，正常执行
4. THE scheduler SHALL 在任务成功完成时同时更新 `last_success_at = NOW()`，失败时不更新 `last_success_at`
5. THE scheduler SHALL 实现 `_convert_interval_to_seconds(value, unit)` 辅助函数，支持 `minutes`/`hours`/`days` 单位

#### 需求 B3：API 扩展 — 创建/更新/手动执行

**用户故事：** 作为管理员，我希望通过 API 配置最小运行间隔，并在必要时强制执行任务。

##### 验收标准

1. THE Backend SHALL 在 `POST /api/schedules` 和 `PUT /api/schedules/{id}` 端点的请求体中新增 `min_run_interval_value`（int, default=0）和 `min_run_interval_unit`（str, default='minutes'）
2. THE Backend SHALL 在 `GET /api/schedules` 响应中新增 `min_run_interval_value`、`min_run_interval_unit`、`last_success_at` 字段
3. THE Backend SHALL 在 `POST /api/schedules/{id}/run` 端点新增 `force: bool = False` 查询参数
4. WHEN `force=true` 时，THE Backend SHALL 绕过最小间隔和并发检查，直接入队执行
5. WHEN `force=false` 且间隔未到时，THE Backend SHALL 返回 409 Conflict，提示"最小运行间隔未到，距下次可执行还有 X 分钟"
6. WHEN `force=false` 且任务正在运行时，THE Backend SHALL 返回 409 Conflict，提示"任务正在执行中"

#### 需求 B4：Admin Web 前端 — ScheduleTab 扩展

**用户故事：** 作为管理员，我希望在调度任务管理界面中看到和配置最小运行间隔。

##### 验收标准

1. THE admin-web SHALL 在创建/编辑调度任务表单中新增「最小运行间隔」行：`InputNumber`（数值）+ `Select`（单位：分钟/小时/天），数值为 0 时显示提示"无限制"
2. THE admin-web SHALL 在任务列表表格中新增「最小间隔」列（显示如"10 天"、"无限制"）和「上次成功」列（相对时间）
3. THE admin-web SHALL 在手动执行确认框中新增「强制执行（忽略最小间隔）」勾选项，默认不勾选
4. WHEN 勾选强制执行时，THE admin-web SHALL 调用 `POST /api/schedules/{id}/run?force=true`
