Files
Neo 6f8f12314f 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>
2026-04-06 00:03:48 +08:00

120 lines
5.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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}` 列表,供前端门店选择器使用。