# 需求文档 — 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`