feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更: - backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔 - admin-web: ETL 状态页、任务管理、调度配置、登录优化 - miniprogram: 看板页面、聊天集成、UI 组件、导航更新 - etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强 - tenant-admin: 项目初始化 - db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8) - packages/shared: 枚举和工具函数更新 - tools: 数据库工具、报表生成、健康检查 - docs: PRD/架构/部署/合约文档更新 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
119
docs/prd/Neo_Specs/review-audit/P10-NS4-02.md
Normal file
119
docs/prd/Neo_Specs/review-audit/P10-NS4-02.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# P10-NS4-02:门店切换功能的交互规范
|
||||
|
||||
## 简要结论
|
||||
- 状态:⚠️ 部分解决
|
||||
- 后端数据隔离和 JWT 门店权限体系完整;前端 SiteSelector 组件已实现但仅在维客线索页集成,全局布局未放置门店选择器,用户审核和 Excel 上传页面缺少门店筛选,门店名称显示为 ID 而非中文名。
|
||||
|
||||
## 详细审查
|
||||
|
||||
### 数据库端
|
||||
|
||||
**auth.tenant_admins 表**(DDL: `db/zqyy_app/migrations/2026-03-20__ns4_tenant_admin_tables.sql`)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| managed_site_ids | BIGINT[] | 管辖门店 ID 数组,用于数据隔离 |
|
||||
| tenant_id | BIGINT | 所属租户 |
|
||||
| is_active | BOOLEAN | 账号状态 |
|
||||
|
||||
✅ `managed_site_ids` 为 PostgreSQL 数组类型,支持多门店管辖场景。
|
||||
|
||||
**public.site_code_mapping 表**(被后端多处引用)
|
||||
|
||||
| 用途 | 说明 |
|
||||
|------|------|
|
||||
| site_code → site_id 映射 | 用户申请时通过球房编号关联门店 |
|
||||
| site_id → site_name 映射 | 用户列表、线索搜索时补充门店名称 |
|
||||
|
||||
✅ 门店名称映射表存在,后端已在 `tenant_users.py` 和 `tenant_clues.py` 中使用。
|
||||
|
||||
**结论**:数据库层面完整支持多门店管辖和门店名称映射。
|
||||
|
||||
### 后端代码
|
||||
|
||||
#### 1. 登录与 JWT(`tenant_auth.py`)
|
||||
✅ 登录成功后返回的 JWT payload 包含 `managed_site_ids` 数组:
|
||||
```python
|
||||
payload = {
|
||||
"sub": str(admin_id),
|
||||
"tenant_id": tenant_id,
|
||||
"managed_site_ids": managed_site_ids, # ← 门店列表
|
||||
"aud": "tenant-admin",
|
||||
}
|
||||
```
|
||||
✅ refresh_token 也携带 `managed_site_ids`,刷新后不丢失。
|
||||
|
||||
#### 2. 认证中间件(`auth/tenant_admins.py`)
|
||||
✅ `CurrentTenantAdmin` dataclass 包含 `managed_site_ids: list[int]`。
|
||||
✅ `site_filter_clause()` 工具函数生成 `site_id IN (...)` SQL 片段,用于数据隔离。
|
||||
✅ `verify_site_access()` 校验单个 site_id 是否在管辖范围内,越权返回 403。
|
||||
✅ 空 managed_site_ids 时生成 `1 = 0` 条件,不匹配任何行(安全兜底)。
|
||||
|
||||
#### 3. 查询接口(`tenant_users.py`)
|
||||
✅ `list_applications` — 通过 `site_filter_clause` 附加 `site_id IN (管辖列表)` 条件。
|
||||
✅ `list_users` — 同上,JOIN `user_site_roles` 表做门店隔离。
|
||||
✅ `approve_application` / `reject_application` — 调用 `verify_site_access` 校验单门店权限。
|
||||
✅ `edit_user` — 新 site_id 必须在管辖范围内。
|
||||
|
||||
#### 4. 维客线索(`tenant_clues.py`,grep 结果确认)
|
||||
✅ 搜索接口支持 `site_id` 参数筛选。
|
||||
✅ 返回结果补充 `site_name`(通过 `site_code_mapping` 查询)。
|
||||
|
||||
**后端结论**:数据隔离体系完整,所有查询均附加 `site_id IN (管辖列表)` 条件,符合 P10 spec 要求。
|
||||
|
||||
### 前端代码
|
||||
|
||||
#### 1. SiteSelector 组件(`components/SiteSelector/index.tsx`)
|
||||
✅ 组件已实现,基于 Ant Design Select,支持多选/全选。
|
||||
✅ 配套 `useSiteFilter` hook,管理选中状态,提供 `effectiveSiteIds`(空选时返回全部)。
|
||||
✅ 数据源来自 `useAuth().user.managedSiteIds`。
|
||||
|
||||
#### 2. useAuth hook(`hooks/useAuth.tsx`)
|
||||
✅ 从 JWT payload 解析 `managedSiteIds` 数组。
|
||||
✅ 登录后通过 `extractUserInfo` 提取并存入 React Context。
|
||||
✅ 页面刷新时从 localStorage 恢复。
|
||||
|
||||
#### 3. API 服务(`services/api.ts`)
|
||||
✅ JWT 自动附加到请求头。
|
||||
✅ 401 时自动刷新 token(含并发保护)。
|
||||
⚠️ 无全局 site_id 拦截器——各页面需自行传递 site_id 参数。
|
||||
|
||||
#### 4. App.tsx 全局布局
|
||||
❌ **全局布局未集成 SiteSelector**。侧边栏仅包含导航菜单和退出按钮,无门店选择器。
|
||||
❌ 无全局 "当前选中门店" 状态管理(无 SiteContext/SiteProvider)。
|
||||
|
||||
#### 5. 各页面集成情况
|
||||
|
||||
| 页面 | SiteSelector 集成 | site_id 传递 | 门店名称显示 |
|
||||
|------|-------------------|-------------|-------------|
|
||||
| 维客线索 (RetentionClues) | ✅ 已集成 | ✅ 单门店时传 site_id | ⚠️ 显示 `门店 {id}` |
|
||||
| 用户管理 (UserManagement) | ❌ 未集成 | ❌ 依赖后端 JWT 隔离 | ⚠️ 显示 `门店 {id}` |
|
||||
| 用户审核 (UserApproval) | ❌ 未集成 | ❌ 依赖后端 JWT 隔离 | ❌ 仅显示球房编号 |
|
||||
| Excel 上传 (ExcelUpload) | ❌ 未集成 | ❌ 无门店筛选 | ❌ 无门店信息 |
|
||||
|
||||
#### 6. 门店名称问题
|
||||
⚠️ SiteSelector 选项显示为 `门店 {id}`(硬编码数字),未查询 `site_code_mapping` 获取真实门店名称。用户无法通过名称识别门店。
|
||||
|
||||
### 差距分析
|
||||
|
||||
| P10 要求 | 当前状态 | 差距 |
|
||||
|----------|---------|------|
|
||||
| 租户级管理员管辖所有店铺 | ✅ managed_site_ids 支持 | 无 |
|
||||
| 店铺级管理员只管指定 site_id | ✅ JWT + site_filter_clause | 无 |
|
||||
| 所有查询附加 site_id IN 条件 | ✅ 后端全部实现 | 无 |
|
||||
| 门店选择器 UI | ⚠️ 组件存在但未全局集成 | 仅维客线索页使用 |
|
||||
| 切换门店后数据自动刷新 | ⚠️ 维客线索页有效 | 其他页面无此机制 |
|
||||
| 门店选择器位置 | ❌ 未定义全局位置 | App.tsx 布局无选择器 |
|
||||
| 门店名称可读性 | ❌ 显示 ID 而非名称 | 需查询 site_name |
|
||||
|
||||
### 建议
|
||||
|
||||
1. **全局门店选择器**:在 `App.tsx` 的 `Content` 区域顶部(或 Header)放置 SiteSelector,创建 `SiteContext` 全局管理当前选中门店,各页面通过 Context 消费。
|
||||
|
||||
2. **门店名称映射**:登录后或首次加载时调用后端接口获取 `managed_site_ids → site_name` 映射,SiteSelector 选项显示真实门店名称(如"朗朗桌球·XX店")。
|
||||
|
||||
3. **页面集成**:UserApproval、UserManagement、ExcelUpload 页面接入全局门店筛选,API 请求携带 `site_ids` 参数。
|
||||
|
||||
4. **切换刷新机制**:门店切换时触发数据重新加载(可通过 `useEffect` 监听 `selectedSiteIds` 变化,或使用 React Query 的 `queryKey` 包含 site_ids 实现自动 refetch)。
|
||||
|
||||
5. **后端补充**:新增 `GET /api/tenant/sites` 接口,返回当前管理员管辖门店的 `{site_id, site_name}` 列表,供前端门店选择器使用。
|
||||
Reference in New Issue
Block a user