feat: chat integration, tenant admin spec, backend chat service, miniprogram updates, DEMO moved to tmp, XCX-TEST removed, migrations & docs
This commit is contained in:
@@ -125,7 +125,6 @@ WHERE site_id = current_setting('app.current_site_id')::bigint;
|
||||
| `app.v_dws_member_visit_detail` | `dws.dws_member_visit_detail` |
|
||||
| `app.v_dws_member_winback_index` | `dws.dws_member_winback_index` |
|
||||
| `app.v_dws_member_newconv_index` | `dws.dws_member_newconv_index` |
|
||||
| `app.v_dws_member_recall_index` | `dws.dws_member_recall_index` |
|
||||
| `app.v_dws_member_assistant_relation_index` | `dws.dws_member_assistant_relation_index` |
|
||||
| `app.v_dws_member_assistant_intimacy` | `dws.dws_member_assistant_intimacy` |
|
||||
| `app.v_dws_assistant_daily_detail` | `dws.dws_assistant_daily_detail` |
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
- [x] 5. Checkpoint — ETL 层验证
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
- [ ] 6. 数据库视图层同步
|
||||
- [x] 6. 数据库视图层同步
|
||||
- [x] 6.1 RLS 视图重建 `app.v_dws_finance_recharge_summary`
|
||||
- 创建迁移脚本 `db/zqyy_app/migrations/` 下
|
||||
- `CREATE OR REPLACE VIEW` 包含全部 6 个新字段
|
||||
|
||||
@@ -141,7 +141,7 @@ async def get_finance_board(time, area, compare, site_id) -> dict:
|
||||
| `get_all_assistants` | `v_dim_assistant` | BOARD-1 助教列表 |
|
||||
| `get_salary_calc_batch` | `v_dws_assistant_salary_calc` | BOARD-1 批量绩效(基于已有 `get_salary_calc` SQL 模式扩展为批量查询) |
|
||||
| `get_top_customers_for_coaches` | `v_dws_member_assistant_relation_index` + `v_dim_member` | BOARD-1 Top 客户(基于已有 `get_relation_index` 扩展为按助教批量查询) |
|
||||
| `get_coach_sv_data` | `v_dws_assistant_monthly_summary` | BOARD-1 sv 维度(助教月度储值汇总,已按助教预聚合) |
|
||||
| `get_coach_sv_data` | `v_dws_assistant_recharge_commission` | BOARD-1 sv 维度(助教储值提成明细,取 recharge_amount + commission_amount) |
|
||||
| `get_customer_board_recall` | `v_dws_member_winback_index` + `v_dim_member` | BOARD-2 recall 维度(ETL 已计算 WBI 指数) |
|
||||
| `get_customer_board_potential` | `v_dws_member_spending_power_index` | BOARD-2 potential 维度(ETL 已计算 SPI 指数) |
|
||||
| `get_customer_board_balance` | `v_dim_member_card_account` + `v_dim_member` | BOARD-2 balance 维度 |
|
||||
@@ -576,7 +576,7 @@ def get_finance_overview(conn, site_id, start_date, end_date):
|
||||
⚠️ 已有函数复用说明:
|
||||
- `get_salary_calc_batch` 基于已有 `get_salary_calc()` 的 SQL 模式,扩展为 `WHERE assistant_id = ANY(%s)` 批量查询
|
||||
- `get_top_customers_for_coaches` 基于已有 `get_relation_index()` 的 SQL 模式,扩展为按助教维度批量查询 + JOIN v_dim_member
|
||||
- `get_coach_sv_data` 使用 `v_dws_assistant_monthly_summary`(已按助教预聚合),无需从 `v_dws_member_consumption_summary` 手动聚合
|
||||
- `get_coach_sv_data` 使用 `v_dws_assistant_recharge_commission`(助教储值提成明细表,取 recharge_amount + commission_amount),原 `v_dws_assistant_monthly_summary` 口径错误(M15 修复)
|
||||
- BOARD-2 recall/potential/recent/recharge 维度使用 ETL 已计算好的专用指数视图,避免在后端重复计算
|
||||
|
||||
关键 SQL 模式:
|
||||
|
||||
1
.kiro/specs/rns1-chat-integration/.config.kiro
Normal file
1
.kiro/specs/rns1-chat-integration/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"specId": "9eccc890-b6c3-41a3-8ba1-bb2f0e09f653", "workflowType": "requirements-first", "specType": "feature"}
|
||||
704
.kiro/specs/rns1-chat-integration/design.md
Normal file
704
.kiro/specs/rns1-chat-integration/design.md
Normal file
@@ -0,0 +1,704 @@
|
||||
# 技术设计文档 — RNS1.4:CHAT 对齐与联调收尾
|
||||
|
||||
## 概述
|
||||
|
||||
RNS1.4 是 RNS1 系列的收尾 spec,覆盖三大块工作:
|
||||
|
||||
1. **CHAT 模块路径迁移与功能补全**(T4-1 ~ T4-3):将现有 `/api/ai/*` 路由迁移到 `/api/xcx/chat/*`,实现 CHAT-1/2/3/4 四个端点,支持 referenceCard 和多入口参数路由
|
||||
2. **FDW 端到端验证**(T4-4):验证 `test_zqyy_app` → `test_etl_feiqiu` 链路上所有 FDW 视图可访问、性能达标、索引完备
|
||||
3. **全量前后端联调**(T4-5):13 个页面移除 mock 数据,连接真实后端,修复 notes 触底加载和 customer-service-records 按月请求
|
||||
|
||||
### 设计原则
|
||||
|
||||
- **复用优先**:现有 `biz.ai_conversations` / `biz.ai_messages` 表结构通过 DDL 迁移扩展,新增 CHAT 所需字段(`customer_id`、`title`、`last_message`、`reference_card`),不新建表
|
||||
- **契约驱动**:所有端点严格遵循 `API-contract.md` 中 CHAT-1/2/3/4 的定义
|
||||
- **权限一致**:所有 CHAT 端点使用 `require_approved()` 依赖,与 RNS1.1-1.3 保持一致
|
||||
- **DWD-DOC 强制规则**:referenceCard 中涉及金额使用 `items_sum` 口径,会员信息通过 `member_id` JOIN `dim_member`
|
||||
|
||||
### 依赖关系
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
RNS10[RNS1.0<br/>基础设施] --> RNS14[RNS1.4<br/>CHAT + 联调]
|
||||
RNS11[RNS1.1<br/>任务/绩效] --> RNS14
|
||||
RNS12[RNS1.2<br/>客户/助教] --> RNS14
|
||||
RNS13[RNS1.3<br/>三看板] --> RNS14
|
||||
style RNS14 fill:#f9f,stroke:#333
|
||||
```
|
||||
|
||||
## 架构
|
||||
|
||||
### 整体架构
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "微信小程序 (apps/miniprogram/)"
|
||||
FE_CHAT[pages/chat/chat.ts]
|
||||
FE_HIST[pages/chat-history/chat-history.ts]
|
||||
FE_API[services/api.ts]
|
||||
FE_REQ[utils/request.ts]
|
||||
FE_CHAT --> FE_API
|
||||
FE_HIST --> FE_API
|
||||
FE_API --> FE_REQ
|
||||
end
|
||||
|
||||
subgraph "FastAPI 后端 (apps/backend/app/)"
|
||||
MW[ResponseWrapperMiddleware<br/>SSE 自动跳过]
|
||||
ROUTER[routers/xcx_chat.py<br/>CHAT-1/2/3/4]
|
||||
SCHEMA[schemas/xcx_chat.py<br/>Pydantic CamelModel]
|
||||
SVC[services/chat_service.py<br/>对话业务逻辑]
|
||||
AI_SVC[ai/conversation_service.py<br/>AI 调用 + 持久化]
|
||||
BAILIAN[ai/bailian_client.py<br/>百炼 API]
|
||||
FDW_Q[services/fdw_queries.py<br/>FDW 查询]
|
||||
DB[database.py]
|
||||
|
||||
MW --> ROUTER
|
||||
ROUTER --> SCHEMA
|
||||
ROUTER --> SVC
|
||||
SVC --> AI_SVC
|
||||
SVC --> FDW_Q
|
||||
AI_SVC --> BAILIAN
|
||||
SVC --> DB
|
||||
end
|
||||
|
||||
subgraph "数据库"
|
||||
APP_DB[(zqyy_app<br/>biz.ai_conversations<br/>biz.ai_messages)]
|
||||
ETL_DB[(etl_feiqiu via FDW<br/>fdw_etl.v_dim_member<br/>fdw_etl.v_dws_member_*)]
|
||||
end
|
||||
|
||||
FE_REQ -->|HTTP JSON / SSE| MW
|
||||
DB --> APP_DB
|
||||
FDW_Q --> ETL_DB
|
||||
|
||||
style ROUTER fill:#f9f,stroke:#333
|
||||
style SVC fill:#f9f,stroke:#333
|
||||
```
|
||||
|
||||
### 请求-响应流程(SSE 流式)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant MP as 小程序 chat.ts
|
||||
participant MW as ResponseWrapper
|
||||
participant R as xcx_chat.py
|
||||
participant S as chat_service.py
|
||||
participant AI as BailianClient
|
||||
participant DB as zqyy_app
|
||||
|
||||
MP->>MW: POST /api/xcx/chat/stream<br/>{chatId, content}
|
||||
MW->>R: 透传(SSE 跳过包装)
|
||||
R->>R: require_approved() + 验证 chatId 归属
|
||||
R->>S: stream_chat(chatId, content, user)
|
||||
S->>DB: INSERT user message → ai_messages
|
||||
S->>AI: 流式调用百炼 API
|
||||
|
||||
loop 逐 token
|
||||
AI-->>S: token 片段
|
||||
S-->>R: SSEEvent(type=message, token=...)
|
||||
R-->>MW: data: {"token": "..."}
|
||||
MW-->>MP: 透传 SSE 事件
|
||||
end
|
||||
|
||||
S->>DB: INSERT AI reply → ai_messages
|
||||
S->>DB: UPDATE ai_conversations.last_message
|
||||
S-->>R: SSEEvent(type=done, messageId, createdAt)
|
||||
R-->>MW: data: {"messageId": "...", "createdAt": "..."}
|
||||
MW-->>MP: 透传 done 事件
|
||||
```
|
||||
|
||||
|
||||
## 组件与接口
|
||||
|
||||
### 组件 1:xcx_chat 路由模块(路径迁移 + CHAT-1/2/3/4)
|
||||
|
||||
**位置**:`apps/backend/app/routers/xcx_chat.py`(新文件,替代 `xcx_ai_chat.py`)
|
||||
|
||||
**职责**:将现有 `/api/ai/*` 路由迁移到 `/api/xcx/chat/*`,并实现 CHAT-1/2/3/4 四个端点。
|
||||
|
||||
**迁移策略**:
|
||||
- 新建 `xcx_chat.py`,prefix 为 `/api/xcx/chat`
|
||||
- 从 `xcx_ai_chat.py` 迁移 SSE 流式对话、历史列表、消息查询三个端点
|
||||
- 在 `main.py` 中替换路由注册:移除 `xcx_ai_chat.router`,注册 `xcx_chat.router`
|
||||
- 删除 `xcx_ai_chat.py`(不保留旧路径兼容)
|
||||
|
||||
**端点定义**:
|
||||
|
||||
```python
|
||||
router = APIRouter(prefix="/api/xcx/chat", tags=["小程序 CHAT"])
|
||||
|
||||
# CHAT-1: 对话历史列表
|
||||
@router.get("/history")
|
||||
async def list_chat_history(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
user: CurrentUser = Depends(require_approved()),
|
||||
) -> ChatHistoryResponse: ...
|
||||
|
||||
# CHAT-2a: 通过 chatId 查询消息
|
||||
@router.get("/{chat_id}/messages")
|
||||
async def get_chat_messages(
|
||||
chat_id: int,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=100),
|
||||
user: CurrentUser = Depends(require_approved()),
|
||||
) -> ChatMessagesResponse: ...
|
||||
|
||||
# CHAT-2b: 通过上下文查询消息(自动查找/创建对话)
|
||||
@router.get("/messages")
|
||||
async def get_chat_messages_by_context(
|
||||
context_type: str = Query(..., alias="contextType"), # task / customer / coach
|
||||
context_id: str = Query(..., alias="contextId"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=100),
|
||||
user: CurrentUser = Depends(require_approved()),
|
||||
) -> ChatMessagesResponse: ...
|
||||
|
||||
# CHAT-3: 发送消息(同步回复)
|
||||
@router.post("/{chat_id}/messages")
|
||||
async def send_message(
|
||||
chat_id: int,
|
||||
body: SendMessageRequest,
|
||||
user: CurrentUser = Depends(require_approved()),
|
||||
) -> SendMessageResponse: ...
|
||||
|
||||
# CHAT-4: SSE 流式端点
|
||||
@router.post("/stream")
|
||||
async def chat_stream(
|
||||
body: ChatStreamRequest,
|
||||
user: CurrentUser = Depends(require_approved()),
|
||||
) -> StreamingResponse: ...
|
||||
```
|
||||
|
||||
### 组件 2:chat_service 业务逻辑层
|
||||
|
||||
**位置**:`apps/backend/app/services/chat_service.py`(新文件)
|
||||
|
||||
**职责**:封装 CHAT 模块的核心业务逻辑,包括对话管理、消息持久化、referenceCard 组装。
|
||||
|
||||
**关键方法**:
|
||||
|
||||
```python
|
||||
class ChatService:
|
||||
"""CHAT 模块业务逻辑。"""
|
||||
|
||||
def get_chat_history(
|
||||
self, user_id: int, site_id: int, page: int, page_size: int
|
||||
) -> tuple[list[dict], int]:
|
||||
"""CHAT-1: 查询对话历史列表,返回 (items, total)。"""
|
||||
|
||||
def get_or_create_session(
|
||||
self, user_id: int, site_id: int,
|
||||
context_type: str, context_id: str | None
|
||||
) -> int:
|
||||
"""按入口上下文查找或创建对话,返回 chat_id。
|
||||
复用规则:
|
||||
- context_type='task': 同一 taskId 始终复用(无时限)
|
||||
- context_type='customer'/'coach': 最后消息 ≤ 3 天复用,> 3 天新建
|
||||
- context_type='general': 始终新建
|
||||
"""
|
||||
|
||||
def get_messages(
|
||||
self, chat_id: int, user_id: int, site_id: int,
|
||||
page: int, page_size: int
|
||||
) -> tuple[list[dict], int, int]:
|
||||
"""CHAT-2: 查询消息列表,返回 (messages, total, chat_id)。
|
||||
验证 chat_id 归属当前用户。"""
|
||||
|
||||
def send_message_sync(
|
||||
self, chat_id: int, content: str, user_id: int, site_id: int
|
||||
) -> dict:
|
||||
"""CHAT-3: 发送消息并获取同步 AI 回复。
|
||||
1. 验证 chatId 归属
|
||||
2. 存入用户消息
|
||||
3. 调用 AI 获取回复
|
||||
4. 存入 AI 回复
|
||||
5. 更新 session 的 last_message / last_message_at
|
||||
6. AI 失败时返回错误提示消息(HTTP 200)"""
|
||||
|
||||
def build_reference_card(
|
||||
self, customer_id: int, site_id: int
|
||||
) -> dict | None:
|
||||
"""组装 referenceCard:从 FDW 查询客户关键指标。
|
||||
遵循 DWD-DOC 规则:金额用 items_sum,会员信息通过 member_id JOIN dim_member。"""
|
||||
|
||||
def generate_title(self, session: dict) -> str:
|
||||
"""生成对话标题:自定义标题 > 上下文名称 > 首条消息前20字。"""
|
||||
```
|
||||
|
||||
**referenceCard 组装逻辑**:
|
||||
|
||||
```python
|
||||
def build_reference_card(self, customer_id: int, site_id: int) -> dict | None:
|
||||
"""从 FDW 查询客户指标,组装为 referenceCard 结构。"""
|
||||
# 1. 通过 member_id JOIN fdw_etl.v_dim_member 获取客户姓名
|
||||
# 2. 通过 fdw_etl.v_dws_member_consumption_summary 获取:
|
||||
# - 余额(balance)
|
||||
# - 近30天消费(items_sum 口径,非 consume_money)
|
||||
# - 到店次数
|
||||
# 3. 组装为 referenceCard 结构
|
||||
return {
|
||||
"type": "customer",
|
||||
"title": f"{member_name} — 消费概览",
|
||||
"summary": f"余额 ¥{balance},近30天消费 ¥{consume_30d}",
|
||||
"data": {
|
||||
"余额": f"¥{balance}",
|
||||
"近30天消费": f"¥{consume_30d}",
|
||||
"到店次数": f"{visit_count}次",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 组件 3:Pydantic Schema(CamelModel)
|
||||
|
||||
**位置**:`apps/backend/app/schemas/xcx_chat.py`(新文件)
|
||||
|
||||
**职责**:定义 CHAT 模块所有请求/响应的 Pydantic schema,继承 `CamelModel` 统一 camelCase 输出。
|
||||
|
||||
```python
|
||||
from app.schemas.base import CamelModel
|
||||
|
||||
class ChatHistoryItem(CamelModel):
|
||||
id: int
|
||||
title: str
|
||||
customer_name: str | None = None
|
||||
last_message: str | None = None
|
||||
timestamp: str # ISO 8601,最后消息时间
|
||||
unread_count: int = 0
|
||||
|
||||
class ChatHistoryResponse(CamelModel):
|
||||
items: list[ChatHistoryItem]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
class ReferenceCard(CamelModel):
|
||||
type: str # 'customer' | 'record'
|
||||
title: str
|
||||
summary: str
|
||||
data: dict[str, str] # 键值对详情
|
||||
|
||||
class ChatMessageItem(CamelModel):
|
||||
id: int
|
||||
role: str # 'user' | 'assistant'
|
||||
content: str
|
||||
created_at: str # ISO 8601(统一字段名,替代 timestamp / created_at)
|
||||
reference_card: ReferenceCard | None = None
|
||||
|
||||
class ChatMessagesResponse(CamelModel):
|
||||
chat_id: int
|
||||
items: list[ChatMessageItem]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
class SendMessageRequest(CamelModel):
|
||||
content: str
|
||||
|
||||
class SendMessageResponse(CamelModel):
|
||||
user_message: MessageBrief
|
||||
ai_reply: MessageBrief
|
||||
|
||||
class MessageBrief(CamelModel):
|
||||
id: int
|
||||
content: str
|
||||
created_at: str
|
||||
|
||||
class ChatStreamRequest(CamelModel):
|
||||
chat_id: int
|
||||
content: str
|
||||
```
|
||||
|
||||
### 组件 4:前端 chat 页面改造
|
||||
|
||||
**位置**:`apps/miniprogram/miniprogram/pages/chat/chat.ts`
|
||||
|
||||
**改造要点**:
|
||||
|
||||
1. **多入口参数路由**(GAP-49/50):
|
||||
```typescript
|
||||
onLoad(options) {
|
||||
if (options.historyId) {
|
||||
// 从 chat-history 跳转:直接用 historyId 作为 chatId
|
||||
this.chatId = options.historyId
|
||||
this.loadMessages(this.chatId)
|
||||
} else if (options.taskId) {
|
||||
// 从 task-detail 跳转:同一 taskId 始终复用同一对话
|
||||
this.loadMessagesByContext('task', options.taskId)
|
||||
} else if (options.customerId) {
|
||||
// 从 customer-detail 跳转:3 天内复用,超过 3 天新建
|
||||
this.loadMessagesByContext('customer', options.customerId)
|
||||
} else if (options.coachId) {
|
||||
// 从 coach-detail 跳转:3 天内复用,超过 3 天新建
|
||||
this.loadMessagesByContext('coach', options.coachId)
|
||||
} else {
|
||||
// 无参数:始终新建通用对话
|
||||
this.loadMessagesByContext('general', '')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **SSE 替换 mock 流式输出**:
|
||||
- 移除 `simulateStreamOutput()` 调用和 `mockAIReplies`
|
||||
- 使用 `wx.request` + `enableChunked: true`(微信基础库 2.20.2+)接收 SSE
|
||||
- 备选方案:轮询 CHAT-3 同步端点(不支持 chunked 的低版本基础库)
|
||||
|
||||
3. **referenceCard 渲染**:
|
||||
- 消息列表中检测 `referenceCard` 字段,已有 `toDataList()` 和 WXML 模板,无需大改
|
||||
- 确保从真实 API 返回的 `referenceCard` 结构与 mock 一致
|
||||
|
||||
### 组件 5:前端 chat-history 页面改造
|
||||
|
||||
**位置**:`apps/miniprogram/miniprogram/pages/chat-history/chat-history.ts`
|
||||
|
||||
**改造要点**:
|
||||
- 移除 `mockChatHistory` 导入
|
||||
- 调用 `fetchChatHistory()` 获取真实数据
|
||||
- 响应字段映射:后端返回 `timestamp`(ISO 8601)→ 前端 `formatRelativeTime()` 处理
|
||||
|
||||
### 组件 6:前端 services/api.ts CHAT 模块对接
|
||||
|
||||
**位置**:`apps/miniprogram/miniprogram/services/api.ts`
|
||||
|
||||
**改造要点**:
|
||||
- `fetchChatHistory()`:调用 `GET /api/xcx/chat/history`
|
||||
- `fetchChatMessages()`:调用 `GET /api/xcx/chat/{chatId}/messages`
|
||||
- `fetchChatMessagesByContext(contextType, contextId)`:新增,调用 `GET /api/xcx/chat/messages?contextType={type}&contextId={id}`
|
||||
- `sendChatMessage()`:调用 `POST /api/xcx/chat/{chatId}/messages`
|
||||
- 移除所有 CHAT 相关 mock 数据导入
|
||||
- `USE_REAL_API` 开关对 CHAT 模块设为 `true`
|
||||
|
||||
### 组件 7:FDW 验证脚本
|
||||
|
||||
**位置**:`scripts/ops/verify_fdw_e2e.py`(新文件)
|
||||
|
||||
**职责**:一次性验证脚本,检查 `test_zqyy_app` → `test_etl_feiqiu` FDW 链路。
|
||||
|
||||
**验证项**:
|
||||
1. 所有 `fdw_etl.*` 视图可访问(SELECT 1 FROM ... LIMIT 1)
|
||||
2. 带典型过滤条件的查询响应时间 < 3s
|
||||
3. 关键索引存在检查(`chat_sessions` 的 `(assistant_id, customer_id)` 等)
|
||||
4. 结果输出为结构化报告(JSON),失败项标注需 DBA 介入
|
||||
|
||||
### 组件 8:联调修复 — notes 触底加载 & customer-service-records 按月请求
|
||||
|
||||
**notes 页面**(`pages/notes/notes.ts`):
|
||||
- 实现 `onReachBottom()` 生命周期函数
|
||||
- 维护 `page` 状态,触底时 `page++` 调用 `fetchNotes({ page, pageSize })`
|
||||
- 追加数据到已有列表,`hasMore === false` 时停止加载
|
||||
|
||||
**customer-service-records 页面**(`pages/customer-service-records/customer-service-records.ts`):
|
||||
- 月份切换时调用 `fetchCustomerRecords({ customerId, year, month })`
|
||||
- 清空已有列表 → 显示 loading → 渲染新数据
|
||||
- 首次加载默认当前月份
|
||||
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 表结构变更:扩展 `biz.ai_conversations`
|
||||
|
||||
现有 `biz.ai_conversations` 表需新增字段以支持 CHAT API 契约:
|
||||
|
||||
```sql
|
||||
-- 迁移脚本:扩展 ai_conversations 支持 CHAT 模块
|
||||
ALTER TABLE biz.ai_conversations
|
||||
ADD COLUMN IF NOT EXISTS context_type varchar(20), -- 关联上下文类型:task / customer / coach / general
|
||||
ADD COLUMN IF NOT EXISTS context_id varchar(50), -- 关联上下文 ID(taskId / customerId / coachId)
|
||||
ADD COLUMN IF NOT EXISTS title varchar(200), -- 对话标题
|
||||
ADD COLUMN IF NOT EXISTS last_message text, -- 最后一条消息摘要
|
||||
ADD COLUMN IF NOT EXISTS last_message_at timestamptz; -- 最后消息时间
|
||||
|
||||
COMMENT ON COLUMN biz.ai_conversations.context_type IS '对话关联上下文类型:task(任务)/ customer(客户)/ coach(助教)/ general(通用)';
|
||||
COMMENT ON COLUMN biz.ai_conversations.context_id IS '关联上下文 ID:task 入口为 taskId,customer 入口为 customerId,coach 入口为 coachId,general 为 NULL';
|
||||
COMMENT ON COLUMN biz.ai_conversations.title IS '对话标题:自定义 > 上下文名称 > 首条消息前20字';
|
||||
COMMENT ON COLUMN biz.ai_conversations.last_message IS '最后一条消息内容摘要(截断至100字)';
|
||||
COMMENT ON COLUMN biz.ai_conversations.last_message_at IS '最后消息时间,用于历史列表排序和对话复用时限判断';
|
||||
```
|
||||
|
||||
**新增索引**:
|
||||
|
||||
```sql
|
||||
-- 上下文对话查找(按 context_type + context_id 查找可复用对话)
|
||||
CREATE INDEX idx_ai_conv_context
|
||||
ON biz.ai_conversations (user_id, site_id, context_type, context_id, last_message_at DESC NULLS LAST)
|
||||
WHERE context_type IS NOT NULL;
|
||||
|
||||
-- 历史列表排序优化(CHAT-1: 按 last_message_at 倒序)
|
||||
CREATE INDEX idx_ai_conv_last_msg
|
||||
ON biz.ai_conversations (user_id, site_id, last_message_at DESC NULLS LAST);
|
||||
```
|
||||
|
||||
### 对话复用规则
|
||||
|
||||
不同入口的对话创建/复用策略:
|
||||
|
||||
| 入口 | context_type | context_id | 复用规则 |
|
||||
|------|-------------|-----------|---------|
|
||||
| task-detail | `task` | taskId | **始终复用**:同一 taskId 归为同一个对话,无时限 |
|
||||
| customer-detail | `customer` | customerId | **3 天时限**:最后一条消息 ≤ 3 天则复用,> 3 天则新建 |
|
||||
| coach-detail | `coach` | coachId | **3 天时限**:最后一条消息 ≤ 3 天则复用,> 3 天则新建 |
|
||||
| chat-history | — | — | **直接打开**:用 historyId 作为 chatId 加载已有对话 |
|
||||
| 无参数(AI 按钮等) | `general` | NULL | **始终新建**:每次创建新对话 |
|
||||
|
||||
复用查找 SQL 模式:
|
||||
```sql
|
||||
-- task 入口:始终复用(无时限)
|
||||
SELECT id FROM biz.ai_conversations
|
||||
WHERE user_id = :user_id AND site_id = :site_id
|
||||
AND context_type = 'task' AND context_id = :task_id
|
||||
ORDER BY created_at DESC LIMIT 1;
|
||||
|
||||
-- customer / coach 入口:3 天时限复用
|
||||
SELECT id FROM biz.ai_conversations
|
||||
WHERE user_id = :user_id AND site_id = :site_id
|
||||
AND context_type = :type AND context_id = :context_id
|
||||
AND last_message_at > NOW() - INTERVAL '3 days'
|
||||
ORDER BY last_message_at DESC LIMIT 1;
|
||||
```
|
||||
|
||||
### 表结构变更:扩展 `biz.ai_messages`
|
||||
|
||||
```sql
|
||||
-- 迁移脚本:扩展 ai_messages 支持 referenceCard
|
||||
ALTER TABLE biz.ai_messages
|
||||
ADD COLUMN IF NOT EXISTS reference_card jsonb; -- 引用卡片 JSON
|
||||
|
||||
COMMENT ON COLUMN biz.ai_messages.reference_card IS 'referenceCard JSON:{type, title, summary, data}';
|
||||
```
|
||||
|
||||
### 变更后完整表结构
|
||||
|
||||
#### biz.ai_conversations(扩展后)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | bigint PK | 对话 ID(即 chatId) |
|
||||
| `user_id` | varchar(50) | 用户 ID(助教) |
|
||||
| `nickname` | varchar(100) | 用户昵称 |
|
||||
| `app_id` | varchar(30) | AI 应用 ID(CHAT 模块固定为 `app1_chat`) |
|
||||
| `site_id` | bigint | 门店 ID |
|
||||
| `source_page` | varchar(100) | 来源页面 |
|
||||
| `source_context` | jsonb | 来源上下文 |
|
||||
| `created_at` | timestamptz | 创建时间 |
|
||||
| `customer_id` | — | **已移除** — 改用 `context_type` + `context_id` 通用方案 |
|
||||
| `context_type` | varchar(20) | **新增** — 对话关联上下文类型:task/customer/coach/general |
|
||||
| `context_id` | varchar(50) | **新增** — 关联上下文 ID(taskId/customerId/coachId) |
|
||||
| `title` | varchar(200) | **新增** — 对话标题 |
|
||||
| `last_message` | text | **新增** — 最后消息摘要 |
|
||||
| `last_message_at` | timestamptz | **新增** — 最后消息时间 |
|
||||
|
||||
#### biz.ai_messages(扩展后)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | bigint PK | 消息 ID |
|
||||
| `conversation_id` | bigint FK | 对话 ID |
|
||||
| `role` | varchar(10) | 角色:`user` / `assistant` |
|
||||
| `content` | text | 消息内容 |
|
||||
| `tokens_used` | integer | token 消耗量 |
|
||||
| `created_at` | timestamptz | 创建时间 |
|
||||
| `reference_card` | jsonb | **新增** — 引用卡片 |
|
||||
|
||||
### 字段映射:数据库 → API 响应
|
||||
|
||||
| 数据库字段 | API 响应字段(camelCase) | 说明 |
|
||||
|-----------|------------------------|------|
|
||||
| `ai_conversations.id` | `id` / `chatId` | 对话 ID |
|
||||
| `ai_conversations.title` | `title` | 对话标题 |
|
||||
| `ai_conversations.last_message` | `lastMessage` | 最后消息摘要 |
|
||||
| `ai_conversations.last_message_at` | `timestamp` | CHAT-1 历史列表时间 |
|
||||
| `ai_conversations.context_id` → 当 `context_type=customer` 时 JOIN `v_dim_member` | `customerName` | 客户姓名(仅 context_type=customer 时有值) |
|
||||
| `ai_messages.id` | `id` | 消息 ID |
|
||||
| `ai_messages.role` | `role` | 消息角色 |
|
||||
| `ai_messages.content` | `content` | 消息内容 |
|
||||
| `ai_messages.created_at` | `createdAt` | 统一时间字段名 |
|
||||
| `ai_messages.reference_card` | `referenceCard` | 引用卡片 JSON |
|
||||
|
||||
### referenceCard JSON 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "customer",
|
||||
"title": "张伟 — 消费概览",
|
||||
"summary": "余额 ¥5,200,近30天消费 ¥2,380",
|
||||
"data": {
|
||||
"余额": "¥5,200",
|
||||
"近30天消费": "¥2,380",
|
||||
"到店次数": "8次",
|
||||
"最近到店": "3天前"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### DWD-DOC 强制规则在数据模型中的体现
|
||||
|
||||
| 规则 | 影响范围 | 实施方式 |
|
||||
|------|---------|---------|
|
||||
| `items_sum` 口径 | referenceCard 中"近30天消费" | SQL 使用 `items_sum` 字段,禁用 `consume_money` |
|
||||
| 助教费用拆分 | referenceCard 中如涉及助教费用 | 使用 `assistant_pd_money` + `assistant_cx_money` |
|
||||
| 会员信息 JOIN | referenceCard 中客户姓名 | 通过 `member_id` JOIN `fdw_etl.v_dim_member`(`scd2_is_current=1`),禁用结算单冗余字段 |
|
||||
|
||||
|
||||
## 正确性属性
|
||||
|
||||
*属性(Property)是一个在系统所有合法执行路径上都应成立的特征或行为——本质上是对"系统应该做什么"的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
|
||||
|
||||
### Property 1: 路由迁移完整性
|
||||
|
||||
*For any* CHAT 端点路径(`/history`、`/{chatId}/messages`、`/messages`、`/{chatId}/messages` POST、`/stream`),以 `/api/xcx/chat` 为前缀的请求应返回非 404 响应(需认证),而以 `/api/ai` 为前缀的对应旧路径应返回 404。
|
||||
|
||||
**Validates: Requirements 1.1, 1.2**
|
||||
|
||||
### Property 2: CHAT API 响应结构完整性
|
||||
|
||||
*For any* CHAT-1 历史列表项,响应必须包含 `id`、`title`、`lastMessage`、`timestamp` 字段;*For any* CHAT-2 消息项,响应必须包含 `id`、`role`、`content`、`createdAt` 字段;*For any* CHAT-3 发送消息响应,必须包含 `userMessage` 和 `aiReply`,各含 `id`、`content`、`createdAt` 字段。
|
||||
|
||||
**Validates: Requirements 2.2, 3.3, 5.3**
|
||||
|
||||
### Property 3: 列表排序不变量
|
||||
|
||||
*For any* CHAT-1 返回的对话历史列表,相邻两项的 `timestamp` 应满足前项 ≥ 后项(按时间倒序);*For any* CHAT-2 返回的消息列表,相邻两项的 `createdAt` 应满足前项 ≤ 后项(按时间正序)。
|
||||
|
||||
**Validates: Requirements 2.3, 3.5**
|
||||
|
||||
### Property 4: 对话标题生成优先级
|
||||
|
||||
*For any* 对话记录,标题生成应遵循优先级链:若 `title` 字段非空则使用 `title`;否则若 `customer_id` 关联的客户姓名非空则使用客户姓名;否则使用首条消息内容的前 20 个字符。生成结果应始终为非空字符串。
|
||||
|
||||
**Validates: Requirements 2.4**
|
||||
|
||||
### Property 5: 权限控制与数据隔离
|
||||
|
||||
*For any* CHAT 端点(CHAT-1/2/3/4),未通过审核的用户(status ≠ approved)应收到 HTTP 403 响应;*For any* 已认证用户请求的对话数据,返回的所有对话记录的 `user_id` 应等于当前用户 ID;*For any* 不属于当前用户的 `chatId`,CHAT-3 和 CHAT-4 应返回 HTTP 403。
|
||||
|
||||
**Validates: Requirements 2.6, 5.6, 11.1, 11.2, 11.3**
|
||||
|
||||
### Property 6: 对话复用规则正确性
|
||||
|
||||
*For any* `context_type='task'` 的入口,同一 `(user_id, site_id, context_id)` 多次调用 `get_or_create_session` 应始终返回同一个 `chatId`;*For any* `context_type='customer'` 或 `context_type='coach'` 的入口,若最后消息时间 ≤ 3 天则返回已有 `chatId`,若 > 3 天则返回新的 `chatId`;*For any* `context_type='general'` 的入口,每次调用应返回不同的 `chatId`。
|
||||
|
||||
**Validates: Requirements 3.8, 3.9, 3.10**
|
||||
|
||||
### Property 7: referenceCard 持久化 Round Trip
|
||||
|
||||
*For any* 合法的 referenceCard JSON 对象(包含 `type`、`title`、`summary`、`data` 字段),存入 `ai_messages.reference_card` 后再读取,应得到与原始对象结构等价的 JSON。
|
||||
|
||||
**Validates: Requirements 4.1, 4.3**
|
||||
|
||||
### Property 8: 消息持久化与会话元数据更新
|
||||
|
||||
*For any* 通过 CHAT-3 或 CHAT-4 发送的消息,用户消息和 AI 回复均应被持久化到 `ai_messages` 表;发送后对应 `ai_conversations` 记录的 `last_message` 应更新为最新消息内容,`last_message_at` 应更新为最新消息时间。
|
||||
|
||||
**Validates: Requirements 5.2, 5.4, 6.3, 6.4**
|
||||
|
||||
### Property 9: SSE 事件类型有效性
|
||||
|
||||
*For any* CHAT-4 SSE 流中的事件,其 `event` 字段应为 `message`、`done`、`error` 三者之一;`message` 事件的 `data` 应包含 `token` 字段;`done` 事件的 `data` 应包含 `messageId` 和 `createdAt` 字段;`error` 事件的 `data` 应包含 `message` 字段。
|
||||
|
||||
**Validates: Requirements 6.2**
|
||||
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 后端错误处理
|
||||
|
||||
所有 CHAT 端点的错误响应遵循 RNS1.0 全局异常处理器格式:`{ code: <HTTP状态码>, message: <错误详情> }`。
|
||||
|
||||
| 场景 | HTTP 状态码 | 响应 | 处理方式 |
|
||||
|------|-----------|------|---------|
|
||||
| 未认证(无 token / token 过期) | 401 | `{ code: 401, message: "无效的令牌" }` | `get_current_user` 依赖抛出 |
|
||||
| 未通过审核(status ≠ approved) | 403 | `{ code: 403, message: "用户未通过审核,无法访问此资源" }` | `require_approved()` 依赖抛出 |
|
||||
| chatId 不属于当前用户 | 403 | `{ code: 403, message: "无权访问此对话" }` | `chat_service` 验证后抛出 |
|
||||
| chatId 不存在 | 404 | `{ code: 404, message: "对话不存在" }` | `chat_service` 查询后抛出 |
|
||||
| 消息内容为空 | 422 | `{ code: 422, message: "消息内容不能为空" }` | 路由层 Pydantic 验证 |
|
||||
| AI 服务调用失败(CHAT-3) | 200 | `aiReply.content = "抱歉,AI 助手暂时无法回复,请稍后重试"` | 用户消息仍保存,AI 回复为错误提示 |
|
||||
| AI 服务调用失败(CHAT-4 SSE) | SSE error 事件 | `event: error\ndata: {"message": "AI 服务暂时不可用"}` | 流中发送 error 事件后关闭 |
|
||||
| 数据库连接失败 | 500 | `{ code: 500, message: "Internal Server Error" }` | 全局 `unhandled_exception_handler` |
|
||||
| FDW 查询失败(referenceCard) | 静默降级 | referenceCard 返回 `null` | 不影响消息本身,仅 referenceCard 缺失 |
|
||||
|
||||
### CHAT-3 AI 失败降级策略
|
||||
|
||||
```python
|
||||
async def send_message_sync(self, chat_id, content, user_id, site_id):
|
||||
# 1. 存入用户消息(无论 AI 是否成功)
|
||||
user_msg_id = self._save_message(chat_id, "user", content)
|
||||
|
||||
# 2. 调用 AI
|
||||
try:
|
||||
ai_reply = await self._call_ai(content, chat_id)
|
||||
except Exception as e:
|
||||
logger.error("AI 服务调用失败: %s", e)
|
||||
ai_reply = "抱歉,AI 助手暂时无法回复,请稍后重试"
|
||||
|
||||
# 3. 存入 AI 回复(包括错误提示)
|
||||
ai_msg_id = self._save_message(chat_id, "assistant", ai_reply)
|
||||
|
||||
# 4. 更新 session 元数据
|
||||
self._update_session_metadata(chat_id, ai_reply)
|
||||
|
||||
# 5. HTTP 200 返回(不抛异常)
|
||||
return { "userMessage": {...}, "aiReply": {...} }
|
||||
```
|
||||
|
||||
### 前端错误处理
|
||||
|
||||
| 场景 | 处理方式 |
|
||||
|------|---------|
|
||||
| CHAT API 返回 401 | 跳转登录页(`request()` 全局拦截) |
|
||||
| CHAT API 返回 403 | Toast 提示"权限不足" |
|
||||
| CHAT API 返回 404 | Toast 提示"对话不存在" |
|
||||
| CHAT API 返回 500 | Toast 提示"服务暂时不可用" |
|
||||
| SSE 连接中断 | 停止流式显示,显示"连接中断"提示,允许重试 |
|
||||
| 网络超时 | `wx.request` fail 回调,显示网络错误提示 |
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 双轨测试方法
|
||||
|
||||
RNS1.4 采用属性测试(Property-Based Testing)+ 单元测试(Unit Testing)双轨并行:
|
||||
|
||||
- **属性测试**:验证对话管理、消息持久化、权限控制、referenceCard round trip 等通用规则
|
||||
- **单元测试**:验证具体端点行为、边界条件、AI 失败降级等
|
||||
|
||||
### 属性测试配置
|
||||
|
||||
- **测试库**:[Hypothesis](https://hypothesis.readthedocs.io/)(Python,项目已使用)
|
||||
- **测试位置**:`tests/` 目录(Monorepo 级属性测试)
|
||||
- **最小迭代次数**:每个属性测试 100 次(`@settings(max_examples=100)`)
|
||||
- **标签格式**:每个测试函数的 docstring 中标注 `Feature: rns1-chat-integration, Property {N}: {property_text}`
|
||||
|
||||
### 属性测试清单
|
||||
|
||||
| Property | 测试函数 | 生成器 | 验证逻辑 |
|
||||
|----------|---------|--------|---------|
|
||||
| P3: 列表排序不变量 | `test_chat_list_ordering` | `st.lists(st.datetimes())` 生成随机时间戳列表 | 对话列表按时间倒序,消息列表按时间正序 |
|
||||
| P4: 标题生成优先级 | `test_title_generation_priority` | `st.fixed_dictionaries({"title": st.one_of(st.none(), st.text(min_size=1)), "customer_name": st.one_of(st.none(), st.text(min_size=1)), "first_message": st.text(min_size=1)})` | 标题遵循优先级链,结果非空 |
|
||||
| P6: customerId 幂等性 | `test_customer_id_get_or_create_idempotent` | `st.integers(min_value=1)` 生成随机 user_id 和 customer_id | 多次调用返回同一 chatId |
|
||||
| P7: referenceCard Round Trip | `test_reference_card_roundtrip` | `st.fixed_dictionaries({"type": st.sampled_from(["customer", "record"]), "title": st.text(min_size=1), "summary": st.text(), "data": st.dictionaries(st.text(min_size=1), st.text())})` | JSON 序列化→存储→读取→反序列化等于原始对象 |
|
||||
| P8: 消息持久化 | `test_message_persistence_after_send` | `st.text(min_size=1, max_size=500)` 生成随机消息内容 | 发送后 ai_messages 包含用户消息和 AI 回复,session 元数据已更新 |
|
||||
| P9: SSE 事件类型 | `test_sse_event_type_validity` | `st.sampled_from(["message", "done", "error"])` + 对应 data 结构 | 事件类型为三者之一,data 结构符合定义 |
|
||||
|
||||
### 单元测试清单
|
||||
|
||||
| 测试目标 | 测试文件 | 关键用例 |
|
||||
|---------|---------|---------|
|
||||
| 路由迁移(P1) | `apps/backend/tests/unit/test_xcx_chat_routes.py` | `/api/xcx/chat/history` 返回 200;`/api/ai/conversations` 返回 404 |
|
||||
| 响应结构(P2) | `apps/backend/tests/unit/test_xcx_chat_schema.py` | ChatHistoryItem / ChatMessageItem / SendMessageResponse 序列化验证 |
|
||||
| 权限控制(P5) | `apps/backend/tests/unit/test_xcx_chat_auth.py` | 未审核用户 403;chatId 不属于当前用户 403 |
|
||||
| AI 失败降级(edge case) | `apps/backend/tests/unit/test_xcx_chat_ai_fallback.py` | AI 超时时返回错误提示消息,HTTP 200 |
|
||||
| SSE 跳过包装 | 已由 RNS1.0 测试覆盖 | `text/event-stream` 不经过 ResponseWrapper |
|
||||
| FDW 验证 | `scripts/ops/verify_fdw_e2e.py` | 一次性运行,输出验证报告 |
|
||||
| 联调验证 | 手动测试 | 13 页面逐一验证真实数据渲染 |
|
||||
|
||||
### 测试执行命令
|
||||
|
||||
```bash
|
||||
# 属性测试(Hypothesis)
|
||||
cd C:\NeoZQYY && pytest tests/ -v -k "rns1_chat"
|
||||
|
||||
# 单元测试
|
||||
cd apps/backend && pytest tests/unit/ -v -k "xcx_chat"
|
||||
|
||||
# FDW 验证脚本
|
||||
cd C:\NeoZQYY && uv run python scripts/ops/verify_fdw_e2e.py
|
||||
```
|
||||
|
||||
201
.kiro/specs/rns1-chat-integration/requirements.md
Normal file
201
.kiro/specs/rns1-chat-integration/requirements.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# 需求文档 — RNS1.4:CHAT 对齐与联调收尾
|
||||
|
||||
## 简介
|
||||
|
||||
RNS1.4 是 NS1 小程序后端 API 补全项目的最后一个子 spec,负责 CHAT 模块路径迁移和功能补全、FDW 端到端验证、以及全量前后端联调。本 spec 依赖 RNS1.0-1.3 全部完成,是整个 RNS1 系列的收尾阶段,确保 13 个页面全部连接真实后端运行,无 mock 数据残留。
|
||||
|
||||
### 依赖
|
||||
|
||||
- RNS1.0(基础设施与契约重写)— 全局响应包装中间件、camelCase 转换、重写后的 API 契约
|
||||
- RNS1.1(任务与绩效接口)— TASK-1/2、PERF-1/2、PIN 接口已实现
|
||||
- RNS1.2(客户与助教接口)— CUST-1/2、COACH-1 接口已实现
|
||||
- RNS1.3(三看板接口)— BOARD-1/2/3、CONFIG-1 接口已实现
|
||||
- 后端已有 `xcx_ai.py`(现有 `/api/ai/*` 路由,需迁移)
|
||||
- 前端已有 chat.ts 和 chat-history.ts 页面(P5.2 交付),当前使用 mock 数据和模拟流式输出
|
||||
|
||||
### 来源文档
|
||||
|
||||
- `docs/prd/Neo_Specs/RNS1-split-plan.md` — 拆分计划主文档
|
||||
- `docs/miniprogram-dev/API-contract.md` — API 契约(RNS1.0 T0-5 重写后版本)
|
||||
- `docs/prd/Neo_Specs/storyboard-walkthrough-assistant-view.md` — 助教视角走查报告(GAP-45~51)
|
||||
|
||||
## 术语表
|
||||
|
||||
- **Backend**:FastAPI 后端应用,位于 `apps/backend/`
|
||||
- **Miniprogram**:微信小程序前端应用,位于 `apps/miniprogram/`
|
||||
- **CHAT_1_API**:对话历史列表接口 `GET /api/xcx/chat/history`,返回分页的对话列表
|
||||
- **CHAT_2_API**:对话消息接口 `GET /api/xcx/chat/{chatId}/messages` 或 `GET /api/xcx/chat/messages?customerId={customerId}`,返回指定对话的消息列表
|
||||
- **CHAT_3_API**:发送消息接口 `POST /api/xcx/chat/{chatId}/messages`,发送用户消息并获取 AI 同步回复
|
||||
- **CHAT_4_SSE**:SSE 流式端点 `POST /api/xcx/chat/stream`,通过 Server-Sent Events 逐 token 返回 AI 回复
|
||||
- **SSE**:Server-Sent Events,服务端推送事件协议,用于 AI 流式回复的逐 token 输出
|
||||
- **referenceCard**:引用卡片,消息中附带的结构化上下文数据(类型/标题/摘要/键值对),用于展示客户概览等信息
|
||||
- **FDW**:PostgreSQL Foreign Data Wrapper,用于从业务库 `zqyy_app` 访问 ETL 库 `etl_feiqiu` 的数据
|
||||
- **chat_sessions**:业务库 `zqyy_app` 中的对话会话表,存储对话元数据
|
||||
- **chat_messages**:业务库 `zqyy_app` 中的消息表,存储对话消息内容
|
||||
- **Response_Wrapper**:RNS1.0 实现的全局响应包装中间件,`text/event-stream` 响应自动跳过包装
|
||||
- **items_sum**:DWD-DOC 强制使用的消费金额口径
|
||||
- **联调**:前后端联合调试,验证所有页面使用真实后端数据正常运行
|
||||
|
||||
## 需求
|
||||
|
||||
### 需求 1:CHAT 路径迁移(T4-1)
|
||||
|
||||
**用户故事:** 作为前后端开发者,我希望 CHAT 模块的 API 路径从 `/api/ai/*` 统一迁移到 `/api/xcx/chat/*`,以便与其他小程序接口保持一致的路径命名规范。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Backend SHALL 将现有 `/api/ai/*` 路由全部迁移到 `/api/xcx/chat/*` 路径下,包括同步端点和 SSE 流式端点
|
||||
2. THE Backend SHALL 在迁移后移除原 `/api/ai/*` 路径,不保留旧路径的兼容映射
|
||||
3. THE Backend SHALL 将迁移后的路由注册到 `xcx_chat` router(或等效命名),与其他 `xcx_*` 路由模块保持一致的组织结构
|
||||
4. THE Miniprogram SHALL 更新 `services/api.ts` 中所有 CHAT 相关的 API 调用路径,从 `/api/ai/*` 改为 `/api/xcx/chat/*`
|
||||
5. WHEN CHAT_4_SSE 端点迁移到 `/api/xcx/chat/stream` 后,THE Response_Wrapper SHALL 继续对 `text/event-stream` 响应跳过包装(RNS1.0 已实现的行为不受路径变更影响)
|
||||
|
||||
### 需求 2:实现 CHAT-1 对话历史列表(T4-2 历史列表部分)
|
||||
|
||||
**用户故事:** 作为助教,我希望在对话历史页面看到所有对话记录(含对话标题和最后消息摘要),以便快速找到并继续之前的对话。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE CHAT_1_API SHALL 实现 `GET /api/xcx/chat/history` 端点,接受 `page`(默认 1)和 `pageSize`(默认 20)查询参数,返回分页的对话历史列表
|
||||
2. THE CHAT_1_API SHALL 为每条对话记录返回以下字段:`id`(对话 ID)、`title`(对话标题)、`customerName`(关联客户姓名,可选)、`lastMessage`(最后一条消息摘要)、`timestamp`(最后消息时间,ISO 8601 格式)、`unreadCount`(未读消息数)
|
||||
3. THE CHAT_1_API SHALL 从 `zqyy_app.chat_sessions` 查询对话列表,按最后消息时间倒序排列
|
||||
4. THE CHAT_1_API SHALL 为 `title` 字段生成对话标题:优先使用对话会话中存储的自定义标题,若无自定义标题则使用关联客户姓名,若均无则使用首条消息内容的前 20 个字符作为标题
|
||||
5. THE CHAT_1_API SHALL 返回标准分页字段:`total`(总记录数)、`page`(当前页码)、`pageSize`(每页条数)
|
||||
6. THE CHAT_1_API SHALL 通过当前登录用户的身份过滤对话列表,确保每位助教只能看到自己的对话记录
|
||||
|
||||
### 需求 3:实现 CHAT-2 对话消息查看(T4-2 消息查看部分)
|
||||
|
||||
**用户故事:** 作为助教,我希望查看指定对话的消息列表,以便回顾与 AI 助手的对话内容。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE CHAT_2_API SHALL 实现两个等效的消息查询端点:`GET /api/xcx/chat/{chatId}/messages`(通过对话 ID 查询)和 `GET /api/xcx/chat/messages?contextType={type}&contextId={id}`(通过上下文类型和 ID 查询)
|
||||
2. THE CHAT_2_API SHALL 接受 `page`(默认 1)和 `pageSize`(默认 50)查询参数,返回分页的消息列表
|
||||
3. THE CHAT_2_API SHALL 为每条消息返回以下字段:`id`(消息 ID)、`role`(`user` 或 `assistant`)、`content`(消息内容)、`createdAt`(创建时间,ISO 8601 格式)、`referenceCard`(引用卡片,可选)
|
||||
4. THE CHAT_2_API SHALL 统一使用 `createdAt` 作为消息时间字段名(替代前端使用的 `timestamp` 和旧契约的 `created_at`,遵循 camelCase 规范)
|
||||
5. THE CHAT_2_API SHALL 从 `zqyy_app.chat_messages` 查询消息列表,按 `createdAt` 正序排列(最早的消息在前)
|
||||
6. THE CHAT_2_API SHALL 在响应中返回 `chatId` 字段,供前端后续发送消息时使用(尤其是通过上下文入口时,前端需要获取对应的 `chatId`)
|
||||
7. THE CHAT_2_API SHALL 返回标准分页字段:`total`(总记录数)、`page`(当前页码)、`pageSize`(每页条数)
|
||||
|
||||
#### 3.2 上下文对话复用规则
|
||||
|
||||
8. WHEN 通过 `contextType` 和 `contextId` 查询参数调用消息端点时,THE CHAT_2_API SHALL 按以下规则查找或创建对话:
|
||||
- `contextType='task'`:查找同一用户、同一 `contextId`(taskId)的已有对话,找到则复用(无时限),找不到则新建
|
||||
- `contextType='customer'` 或 `contextType='coach'`:查找同一用户、同一 `contextId` 的已有对话,若最后消息时间 ≤ 3 天则复用,> 3 天或不存在则新建
|
||||
- `contextType='general'`:始终新建对话
|
||||
9. THE CHAT_2_API SHALL 在 `ai_conversations` 中记录 `context_type` 和 `context_id` 字段,用于后续对话查找
|
||||
10. THE CHAT_2_API SHALL 确保对话复用查找基于 `(user_id, site_id, context_type, context_id)` 组合,不同用户的对话互不影响
|
||||
|
||||
|
||||
### 需求 4:CHAT referenceCard 支持(T4-3)
|
||||
|
||||
**用户故事:** 作为助教,我希望在与 AI 助手对话时,消息中能附带客户概览卡片(含余额、消费、到店频次等键值对数据),以便在对话上下文中快速查看客户关键信息。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE CHAT_2_API SHALL 为消息返回可选的 `referenceCard` 字段,结构包含:`type`(引用类型,`customer` 或 `record` 枚举)、`title`(卡片标题,如 `"张伟 — 消费概览"`)、`summary`(摘要文字)、`data`(`Record<string, string>` 键值对详情,如 `{ "近30天消费": "¥2,380", "到店次数": "8次" }`)
|
||||
2. WHEN AI 助手回复消息涉及特定客户时,THE Backend SHALL 从 FDW 查询该客户的关键指标(余额、近期消费、到店频次等),组装为 `referenceCard` 附加到 AI 回复消息中
|
||||
3. THE Backend SHALL 将 `referenceCard` 数据持久化存储到 `chat_messages` 表中(作为 JSON 字段),以便历史消息查看时仍能展示引用卡片
|
||||
4. THE Miniprogram SHALL 在 chat 页面的消息列表中,检测消息的 `referenceCard` 字段,若存在则渲染为结构化卡片组件(标题 + 摘要 + 键值对列表)
|
||||
|
||||
#### 4.2 多入口参数路由(GAP-50)
|
||||
|
||||
5. THE Miniprogram SHALL 在 chat 页面的 `onLoad(options)` 中实现多入口参数路由逻辑,按以下优先级处理入口参数:
|
||||
- 若 `options.historyId` 存在(从 chat-history 跳转),使用 `historyId` 作为 `chatId` 直接加载历史消息
|
||||
- 若 `options.taskId` 存在(从 task-detail 跳转),使用 `contextType=task` + `contextId=taskId` 调用 CHAT_2_API,由后端查找同一任务的已有对话(始终复用,无时限)
|
||||
- 若 `options.customerId` 存在(从 customer-detail 跳转),使用 `contextType=customer` + `contextId=customerId` 调用 CHAT_2_API,由后端按 3 天时限判断复用或新建
|
||||
- 若 `options.coachId` 存在(从 coach-detail 跳转),使用 `contextType=coach` + `contextId=coachId` 调用 CHAT_2_API,由后端按 3 天时限判断复用或新建
|
||||
6. WHEN 通过上下文入口进入对话后,THE Miniprogram SHALL 将后端返回的 `chatId` 缓存到页面 data 中,后续发送消息和 SSE 流式请求均使用该 `chatId`
|
||||
7. IF chat 页面未收到任何入口参数(`historyId`/`taskId`/`customerId`/`coachId` 均为空),THEN THE Miniprogram SHALL 使用 `contextType=general` 调用 CHAT_2_API 创建一个通用对话
|
||||
|
||||
### 需求 5:CHAT-3 发送消息(T4-2 发送部分)
|
||||
|
||||
**用户故事:** 作为助教,我希望在对话页面发送消息后能立即收到 AI 的同步回复,以便在不支持 SSE 的场景下也能正常使用 AI 助手。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE CHAT_3_API SHALL 实现 `POST /api/xcx/chat/{chatId}/messages` 端点,接受请求体 `{ content: string }`
|
||||
2. THE CHAT_3_API SHALL 将用户消息存入 `chat_messages` 表,调用 AI 服务获取回复,将 AI 回复也存入 `chat_messages` 表
|
||||
3. THE CHAT_3_API SHALL 返回包含用户消息和 AI 回复的响应:`userMessage`(含 `id`/`content`/`createdAt`)和 `aiReply`(含 `id`/`content`/`createdAt`)
|
||||
4. THE CHAT_3_API SHALL 在发送消息后更新 `chat_sessions` 表的 `lastMessage` 和最后消息时间字段
|
||||
5. IF AI 服务调用失败或超时,THEN THE CHAT_3_API SHALL 仍保存用户消息,并返回 AI 回复为错误提示消息(如 `{ content: "抱歉,AI 助手暂时无法回复,请稍后重试" }`),HTTP 状态码保持 200
|
||||
6. THE CHAT_3_API SHALL 验证请求的 `chatId` 属于当前登录助教,不属于时返回 HTTP 403
|
||||
|
||||
### 需求 6:CHAT-4 SSE 流式端点(T4-1 SSE 部分)
|
||||
|
||||
**用户故事:** 作为助教,我希望 AI 助手的回复能以流式方式逐字显示,以便获得更自然的对话体验,减少等待感。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE CHAT_4_SSE SHALL 实现 `POST /api/xcx/chat/stream` 端点,接受请求体 `{ chatId: string, content: string }`,响应内容类型为 `text/event-stream`
|
||||
2. THE CHAT_4_SSE SHALL 发送以下三种 SSE 事件类型:
|
||||
- `event: message` — 逐 token 输出,`data` 为 `{"token": "<文本片段>"}`
|
||||
- `event: done` — 流结束,`data` 为 `{"messageId": "<完整消息ID>", "createdAt": "<ISO 8601>"}`
|
||||
- `event: error` — 错误,`data` 为 `{"message": "<错误描述>"}`
|
||||
3. THE CHAT_4_SSE SHALL 在流开始前将用户消息存入 `chat_messages` 表,在流结束后将完整的 AI 回复存入 `chat_messages` 表
|
||||
4. THE CHAT_4_SSE SHALL 在流结束后更新 `chat_sessions` 表的 `lastMessage` 和最后消息时间字段
|
||||
5. THE Response_Wrapper SHALL 对 `text/event-stream` 响应跳过全局包装,直接透传 SSE 事件流(RNS1.0 已实现)
|
||||
6. THE CHAT_4_SSE SHALL 验证请求的 `chatId` 属于当前登录助教,不属于时返回 HTTP 403(此时响应为普通 JSON 错误,非 SSE)
|
||||
7. THE Miniprogram SHALL 将 chat 页面现有的 `simulateStreamOutput()`(模拟逐字输出)替换为真实的 SSE 连接,通过 `wx.request` 或兼容方案接收 `text/event-stream` 响应
|
||||
|
||||
|
||||
### 需求 7:FDW 端到端验证(T4-4)
|
||||
|
||||
**用户故事:** 作为后端开发者,我希望验证所有 FDW 查询在测试环境链路(`test_zqyy_app` → `test_etl_feiqiu`)上正常工作,以便确保 RNS1.1-1.3 实现的所有接口在真实数据链路上不会因 FDW 连接、权限或性能问题而失败。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Backend SHALL 验证所有 `fdw_etl.*` 视图在 `test_zqyy_app` 数据库中可正常访问,包括但不限于:`v_dws_assistant_salary_calc`、`v_dwd_assistant_service_log`、`v_dim_member`、`v_dim_assistant`、`v_dws_member_consumption_summary`、`v_dws_member_assistant_relation_index`、`v_dws_finance_*` 系列视图
|
||||
2. THE Backend SHALL 验证每个 FDW 视图的查询响应时间在可接受范围内(单次查询不超过 3 秒),对超时的查询记录慢查询日志并评估是否需要添加索引
|
||||
3. THE Backend SHALL 验证 FDW 查询在带有典型过滤条件(如 `assistant_id`、`member_id`、日期范围)时能正确返回数据,且结果集与直接查询 `test_etl_feiqiu` 的结果一致
|
||||
4. IF 某个 FDW 视图不存在或权限不足,THEN THE Backend SHALL 记录具体的错误信息(视图名、错误类型),并在验证报告中标注需要 DBA 介入修复
|
||||
5. THE Backend SHALL 检查 FDW 链路上的关键索引是否存在:`chat_sessions` 表的 `(assistant_id, customer_id)` 索引、`chat_messages` 表的 `(session_id, created_at)` 索引
|
||||
|
||||
### 需求 8:前端联调修复 — notes 页触底加载(T4-5 F11)
|
||||
|
||||
**用户故事:** 作为助教,我希望在备注列表页面滚动到底部时自动加载更多备注,以便查看全部备注记录而不需要手动翻页。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Miniprogram SHALL 在 notes 页面实现 `onReachBottom()` 生命周期函数,当用户滚动到页面底部时自动请求下一页备注数据
|
||||
2. WHEN 触底加载触发时,THE Miniprogram SHALL 将 `page` 参数加 1,调用 `fetchNotes({ page, pageSize })` 接口,将返回的备注追加到已有列表末尾
|
||||
3. WHEN 后端返回的 `hasMore` 为 `false` 或返回的备注数量小于 `pageSize` 时,THE Miniprogram SHALL 停止触底加载,显示"没有更多了"提示
|
||||
4. THE Miniprogram SHALL 在触底加载过程中显示加载状态指示器,防止重复触发请求
|
||||
|
||||
### 需求 9:前端联调修复 — customer-service-records 按月请求(T4-5 F10)
|
||||
|
||||
**用户故事:** 作为助教,我希望客户服务记录页面在切换月份时向后端请求对应月份的数据,以便在数据量大时页面仍能快速响应,而不是全量加载后本地过滤。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Miniprogram SHALL 修改 customer-service-records 页面的月份切换逻辑,从当前的"全量加载后本地过滤"改为"按月请求 API"
|
||||
2. WHEN 用户切换月份时,THE Miniprogram SHALL 使用新的 `year`/`month` 参数调用 `fetchCustomerRecords({ customerId, year, month })` 接口,加载对应月份的服务记录
|
||||
3. THE Miniprogram SHALL 在月份切换时清空已有记录列表,显示加载状态,待新数据返回后渲染
|
||||
4. THE Miniprogram SHALL 在首次加载时默认请求当前月份的数据,而非全量数据
|
||||
5. THE Backend SHALL 确保 CUST-2 接口支持 `year` 和 `month` 查询参数,仅返回指定月份的服务记录(RNS1.2 T2-4 已实现按月查询能力)
|
||||
|
||||
### 需求 10:全量前后端联调(T4-5 联调部分)
|
||||
|
||||
**用户故事:** 作为开发团队,我们希望 13 个小程序页面全部连接真实后端运行,无 mock 数据残留,以便确认整个应用在真实数据环境下功能完整、交互正常。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Miniprogram SHALL 移除所有页面中的内联 mock 数据和 mock 数据导入(`import { mockXxx } from '../../utils/mock-data'`),全部替换为真实 API 调用
|
||||
2. THE Miniprogram SHALL 确保以下 13 个页面均能使用真实后端数据正常渲染:`task-list`、`task-detail`、`notes`、`performance`、`performance-records`、`customer-detail`、`customer-service-records`、`coach-detail`、`board-coach`、`board-customer`、`board-finance`、`chat-history`、`chat`
|
||||
3. WHEN 某个页面的 API 调用失败时,THE Miniprogram SHALL 显示友好的错误提示(如 Toast 或空状态占位),不出现白屏或未捕获异常
|
||||
4. THE Miniprogram SHALL 验证所有页面间的跳转参数传递正确(RNS1.0 T0-6 已修复的跨页面参数),确保目标页面能正确加载对应数据
|
||||
5. THE Miniprogram SHALL 验证 chat 页面从 4 个入口(task-detail、customer-detail、coach-detail、chat-history)进入时均能正确关联上下文并加载对应对话
|
||||
6. IF 联调过程中发现新的 Bug 或数据不一致问题,THEN THE Miniprogram 和 Backend SHALL 在本 spec 范围内修复,修复内容记录到联调问题清单中
|
||||
|
||||
### 需求 11:全局约束与权限控制
|
||||
|
||||
**用户故事:** 作为系统管理员,我希望所有 CHAT 接口遵循统一的权限控制和数据隔离规则,以确保每位助教只能访问自己的对话数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Backend SHALL 对所有 RNS1.4 CHAT 接口(CHAT-1、CHAT-2、CHAT-3、CHAT-4)执行 `require_approved()` 权限检查,确保用户状态为 `approved`
|
||||
2. THE Backend SHALL 通过当前登录用户的身份信息过滤对话数据,确保每位助教只能访问自己创建的或与自己关联的对话
|
||||
3. IF 当前用户未通过审核(状态非 `approved`),THEN THE Backend SHALL 返回 HTTP 403 `{ code: 403, message: "用户未通过审核,无法访问此资源" }`
|
||||
4. THE Backend SHALL 对所有 CHAT 接口的响应字段名统一使用 camelCase 格式(与 RNS1.0 的 CamelCase_Converter 一致)
|
||||
5. THE Backend SHALL 确保 CHAT 模块的错误响应格式与全局异常处理器一致:`{ code: <HTTP状态码>, message: <错误详情> }`
|
||||
6. WHEN CHAT 模块查询 FDW 数据(如为 referenceCard 获取客户指标)时,THE Backend SHALL 遵循 DWD-DOC 强制规则:金额使用 `items_sum` 口径,会员信息通过 `member_id` JOIN `dim_member` 获取
|
||||
311
.kiro/specs/rns1-chat-integration/tasks.md
Normal file
311
.kiro/specs/rns1-chat-integration/tasks.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# 实施计划:RNS1.4 CHAT 对齐与联调收尾
|
||||
|
||||
## 概述
|
||||
|
||||
按照设计文档的 8 个组件,将实施拆分为:DDL 迁移 → 后端 Schema/Service/Router → FDW 验证 → 前端改造 → 联调收尾。每个任务增量构建,确保无孤立代码。属性测试(Hypothesis)和单元测试作为可选子任务紧跟实现步骤。
|
||||
|
||||
## 任务
|
||||
|
||||
- [x] 1. DDL 迁移:扩展 ai_conversations 和 ai_messages 表
|
||||
- [x] 1.1 创建 DDL 迁移脚本 `db/zqyy_app/migrations/2026-03-20__rns14_chat_module_extend.sql`
|
||||
- ALTER TABLE `biz.ai_conversations` 新增 `context_type varchar(20)`、`context_id varchar(50)`、`title varchar(200)`、`last_message text`、`last_message_at timestamptz` 五个字段
|
||||
- ALTER TABLE `biz.ai_messages` 新增 `reference_card` jsonb 字段
|
||||
- 创建索引 `idx_ai_conv_context` ON `(user_id, site_id, context_type, context_id, last_message_at DESC NULLS LAST) WHERE context_type IS NOT NULL`
|
||||
- 创建排序索引 `idx_ai_conv_last_msg` ON `(user_id, site_id, last_message_at DESC NULLS LAST)`
|
||||
- 添加 COMMENT ON COLUMN 注释
|
||||
- _需求: R2.3, R3.8, R3.10, R4.3, R7.5_
|
||||
|
||||
- [x] 2. 后端 Pydantic Schema 定义(组件 3)
|
||||
- [x] 2.1 创建 `apps/backend/app/schemas/xcx_chat.py`
|
||||
- 继承 `CamelModel` 基类,定义 `ChatHistoryItem`、`ChatHistoryResponse`、`ReferenceCard`、`ChatMessageItem`、`ChatMessagesResponse`、`SendMessageRequest`、`SendMessageResponse`、`MessageBrief`、`ChatStreamRequest`
|
||||
- 字段类型和可选性严格遵循设计文档组件 3 定义
|
||||
- _需求: R2.2, R3.3, R4.1, R5.1, R5.3, R6.1, R11.4_
|
||||
- [x] 2.2 编写 Schema 序列化单元测试 `apps/backend/tests/unit/test_xcx_chat_schema.py`
|
||||
- 验证 ChatHistoryItem / ChatMessageItem / SendMessageResponse 的 camelCase 序列化输出
|
||||
- 验证 ReferenceCard 可选字段为 None 时不报错
|
||||
- _需求: R2.2, R3.3, R5.3, R11.4_
|
||||
|
||||
- [x] 3. 后端 chat_service 业务逻辑层(组件 2)
|
||||
- [x] 3.1 创建 `apps/backend/app/services/chat_service.py`
|
||||
- 实现 `ChatService` 类,包含 `get_chat_history`、`get_or_create_session`、`get_messages`、`send_message_sync`、`build_reference_card`、`generate_title` 方法
|
||||
- `get_chat_history`:查询 `biz.ai_conversations`,按 `last_message_at` 倒序,JOIN `v_dim_member` 获取 `customerName`,分页返回
|
||||
- `get_or_create_session`:按 `(user_id, site_id, context_type, context_id)` 查找或创建对话。复用规则:task 入口始终复用(无时限);customer/coach 入口 ≤ 3 天复用、> 3 天新建;general 入口始终新建
|
||||
- `get_messages`:查询 `biz.ai_messages`,按 `created_at` 正序,验证 chatId 归属当前用户
|
||||
- `send_message_sync`:存入用户消息 → 调用 AI → 存入 AI 回复 → 更新 session 元数据;AI 失败时返回错误提示消息(HTTP 200)
|
||||
- ⚠️ **P5 PRD 合规**:对话落库必须遵循 `docs/prd/specs/P5-miniapp-ai-integration.md` 数据写入规则:
|
||||
- `app_id` 固定为 `app1_chat`
|
||||
- 用户消息发送时即写入 `ai_messages`(role=user)
|
||||
- 流式完成后完整 assistant 回复写入 `ai_messages`(role=assistant),含 `tokens_used`
|
||||
- 首条消息为页面上下文 JSON(`current_time`/`source_page`/`page_context`/`screen_content`)
|
||||
- `get_or_create_session` 仅用于 task/customer/coach 入口的对话复用(task 无时限,customer/coach 3 天时限);general 入口始终新建(保持 P5 PRD 兼容)
|
||||
- `build_reference_card`:从 FDW 查询客户指标(`items_sum` 口径),组装 referenceCard JSON
|
||||
- `generate_title`:自定义标题 > 客户姓名 > 首条消息前 20 字
|
||||
- _需求: R2.1-R2.6, R3.1-R3.10, R4.1-R4.3, R5.1-R5.6, R11.2, R11.6_
|
||||
|
||||
- [x] 3.2 编写属性测试:标题生成优先级
|
||||
- **Property 4: 对话标题生成优先级**
|
||||
- 使用 Hypothesis `st.fixed_dictionaries` 生成随机 title/customer_name/first_message 组合
|
||||
- 验证:有 title 用 title,否则用 customer_name,否则用首条消息前 20 字,结果始终非空
|
||||
- **验证: 需求 R2.4**
|
||||
|
||||
- [x] 3.3 编写属性测试:对话复用规则正确性
|
||||
- **Property 6: 对话复用规则正确性**
|
||||
- 使用 Hypothesis 生成随机 context_type/context_id/last_message_at 组合
|
||||
- 验证:task 入口同一 context_id 始终返回同一 chatId;customer/coach 入口 ≤ 3 天复用、> 3 天新建;general 入口每次返回不同 chatId
|
||||
- **验证: 需求 R3.8, R3.9, R3.10**
|
||||
|
||||
- [x] 3.4 编写属性测试:referenceCard Round Trip
|
||||
- **Property 7: referenceCard 持久化 Round Trip**
|
||||
- 使用 Hypothesis 生成随机 referenceCard JSON(type/title/summary/data)
|
||||
- 验证:JSON 序列化→存储→读取→反序列化等于原始对象
|
||||
- **验证: 需求 R4.1, R4.3**
|
||||
|
||||
- [x] 3.5 编写属性测试:消息持久化与会话元数据更新
|
||||
- **Property 8: 消息持久化与会话元数据更新**
|
||||
- 使用 Hypothesis `st.text(min_size=1, max_size=500)` 生成随机消息内容
|
||||
- 验证:发送后 ai_messages 包含用户消息和 AI 回复,session 的 last_message 和 last_message_at 已更新
|
||||
- **验证: 需求 R5.2, R5.4, R6.3, R6.4**
|
||||
|
||||
- [x] 3.6 编写单元测试:AI 失败降级
|
||||
- 测试文件 `apps/backend/tests/unit/test_xcx_chat_ai_fallback.py`
|
||||
- 验证 AI 服务超时/异常时,用户消息仍保存,AI 回复为错误提示消息,HTTP 200
|
||||
- _需求: R5.5_
|
||||
|
||||
- [x] 4. 后端路由迁移与 CHAT-1/2/3/4 端点实现(组件 1)
|
||||
- [x] 4.1 创建 `apps/backend/app/routers/xcx_chat.py`,实现 CHAT-1/2/3/4 五个端点
|
||||
- `GET /history` — CHAT-1 对话历史列表,调用 `chat_service.get_chat_history`
|
||||
- `GET /{chat_id}/messages` — CHAT-2a 通过 chatId 查询消息
|
||||
- `GET /messages?contextType=&contextId=` — CHAT-2b 通过上下文查询消息(按复用规则自动查找/创建对话)
|
||||
- `POST /{chat_id}/messages` — CHAT-3 发送消息(同步回复)
|
||||
- `POST /stream` — CHAT-4 SSE 流式端点,返回 `StreamingResponse(media_type="text/event-stream")`
|
||||
- 所有端点使用 `Depends(require_approved())` 权限检查
|
||||
- chatId 归属验证:CHAT-3/4 不属于当前用户返回 HTTP 403
|
||||
- _需求: R1.1, R1.3, R2.1, R3.1, R5.1, R6.1, R11.1, R11.2_
|
||||
|
||||
- [x] 4.2 在 `apps/backend/app/main.py` 中注册 `xcx_chat.router`,移除 `xcx_ai_chat.router`
|
||||
- 删除 `xcx_ai_chat.py` 文件(不保留旧路径兼容)
|
||||
- _需求: R1.2, R1.3_
|
||||
|
||||
- [x] 4.3 编写属性测试:SSE 事件类型有效性
|
||||
- **Property 9: SSE 事件类型有效性**
|
||||
- 使用 Hypothesis `st.sampled_from(["message", "done", "error"])` + 对应 data 结构
|
||||
- 验证:事件类型为三者之一,data 结构符合定义(message→token, done→messageId+createdAt, error→message)
|
||||
- **验证: 需求 R6.2**
|
||||
|
||||
- [x] 4.4 编写属性测试:列表排序不变量
|
||||
- **Property 3: 列表排序不变量**
|
||||
- 使用 Hypothesis `st.lists(st.datetimes())` 生成随机时间戳列表
|
||||
- 验证:CHAT-1 对话列表按时间倒序,CHAT-2 消息列表按时间正序
|
||||
- **验证: 需求 R2.3, R3.5**
|
||||
|
||||
- [x] 4.5 编写单元测试:路由迁移与权限控制
|
||||
- 测试文件 `apps/backend/tests/unit/test_xcx_chat_routes.py`
|
||||
- 验证 `/api/xcx/chat/history` 返回 200(需认证);`/api/ai/conversations` 返回 404
|
||||
- 验证未审核用户收到 403;chatId 不属于当前用户收到 403
|
||||
- **Property 1: 路由迁移完整性** / **Property 5: 权限控制与数据隔离**
|
||||
- **验证: 需求 R1.1, R1.2, R5.6, R6.6, R11.1, R11.3**
|
||||
|
||||
- [x] 5. 检查点 — 后端实现验证
|
||||
- 确保所有后端测试通过,ask the user if questions arise.
|
||||
|
||||
- [x] 6. FDW 端到端验证脚本(组件 7)
|
||||
- [x] 6.1 创建 `scripts/ops/verify_fdw_e2e.py`
|
||||
- 验证所有 `fdw_etl.*` 视图在 `test_zqyy_app` 中可访问(SELECT 1 FROM ... LIMIT 1)
|
||||
- 验证带典型过滤条件(assistant_id、member_id、日期范围)的查询响应时间 < 3s
|
||||
- 检查关键索引存在:`chat_sessions(assistant_id, customer_id)`、`chat_messages(session_id, created_at)`
|
||||
- 输出结构化 JSON 报告,失败项标注需 DBA 介入
|
||||
- 使用 `load_dotenv` 加载根 `.env`,连接 `test_zqyy_app`(遵循 testing-env.md 规范)
|
||||
- _需求: R7.1, R7.2, R7.3, R7.4, R7.5_
|
||||
|
||||
- [x] 7. 前端 services/api.ts CHAT 模块对接(组件 6)
|
||||
- [x] 7.1 修改 `apps/miniprogram/miniprogram/services/api.ts`
|
||||
- `fetchChatHistory()`:调用 `GET /api/xcx/chat/history`
|
||||
- `fetchChatMessages(chatId)`:调用 `GET /api/xcx/chat/{chatId}/messages`
|
||||
- 新增 `fetchChatMessagesByContext(contextType, contextId)`:调用 `GET /api/xcx/chat/messages?contextType={type}&contextId={id}`
|
||||
- `sendChatMessage(chatId, content)`:调用 `POST /api/xcx/chat/{chatId}/messages`
|
||||
- 移除所有 CHAT 相关 mock 数据导入,`USE_REAL_API` 对 CHAT 模块设为 `true`
|
||||
- _需求: R1.4, R10.1_
|
||||
|
||||
- [x] 8. 前端 chat 页面改造(组件 4)
|
||||
- [x] 8.1 修改 `apps/miniprogram/miniprogram/pages/chat/chat.ts` 实现多入口参数路由
|
||||
- `onLoad(options)` 中按优先级处理:`historyId` → `taskId` → `customerId` → `coachId` → 无参数(通用对话)
|
||||
- `historyId` 入口:直接用作 chatId 加载历史消息
|
||||
- `taskId` 入口:调用 `fetchChatMessagesByContext('task', taskId)`,同一 taskId 始终复用同一对话(无时限)
|
||||
- `customerId` 入口:调用 `fetchChatMessagesByContext('customer', customerId)`,≤ 3 天复用、> 3 天新建
|
||||
- `coachId` 入口:调用 `fetchChatMessagesByContext('coach', coachId)`,≤ 3 天复用、> 3 天新建
|
||||
- 无参数入口:调用 `fetchChatMessagesByContext('general', '')`,始终新建
|
||||
- _需求: R4.5, R4.6, R4.7_
|
||||
|
||||
- [x] 8.2 修改 chat.ts 将 `simulateStreamOutput()` 替换为真实 SSE 连接
|
||||
- 使用 `wx.request` + `enableChunked: true` 接收 `POST /api/xcx/chat/stream` 的 SSE 响应
|
||||
- 解析 `event: message`(逐 token 追加)、`event: done`(流结束)、`event: error`(错误处理)
|
||||
- 移除 `simulateStreamOutput()` 和 `mockAIReplies` 相关代码
|
||||
- SSE 连接中断时显示"连接中断"提示,允许重试
|
||||
- _需求: R6.7, R10.1_
|
||||
|
||||
- [x] 8.3 确保 chat 页面 referenceCard 渲染与真实 API 数据兼容
|
||||
- 验证 `toDataList()` 和 WXML 模板能正确渲染后端返回的 referenceCard 结构
|
||||
- _需求: R4.4_
|
||||
|
||||
- [x] 9. 前端 chat-history 页面改造(组件 5)
|
||||
- [x] 9.1 修改 `apps/miniprogram/miniprogram/pages/chat-history/chat-history.ts`
|
||||
- 移除 `mockChatHistory` 导入,调用 `fetchChatHistory()` 获取真实数据
|
||||
- 响应字段映射:后端 `timestamp`(ISO 8601)→ `formatRelativeTime()` 处理
|
||||
- 点击对话项跳转 chat 页面时传递 `historyId` 参数
|
||||
- _需求: R2.1, R10.1, R10.2_
|
||||
|
||||
- [x] 10. 前端联调修复(组件 8)
|
||||
- [x] 10.1 修改 notes 页面实现触底加载
|
||||
- 在 `apps/miniprogram/miniprogram/pages/notes/notes.ts` 实现 `onReachBottom()` 生命周期函数
|
||||
- 维护 `page` 状态,触底时 `page++` 调用 `fetchNotes({ page, pageSize })`
|
||||
- 追加数据到已有列表,`hasMore === false` 时停止加载并显示"没有更多了"
|
||||
- 加载过程中显示加载状态指示器,防止重复触发
|
||||
- _需求: R8.1, R8.2, R8.3, R8.4_
|
||||
|
||||
- [x] 10.2 修改 customer-service-records 页面实现按月请求
|
||||
- 在 `apps/miniprogram/miniprogram/pages/customer-service-records/customer-service-records.ts` 修改月份切换逻辑
|
||||
- 月份切换时调用 `fetchCustomerRecords({ customerId, year, month })`,清空列表 → loading → 渲染
|
||||
- 首次加载默认当前月份数据
|
||||
- _需求: R9.1, R9.2, R9.3, R9.4_
|
||||
|
||||
- [x] 11. 检查点 — 前端改造验证
|
||||
- 确保所有前端改造完成,chat 页面 4 个入口(task-detail、customer-detail、coach-detail、chat-history)均能正确进入并加载对话。ask the user if questions arise.
|
||||
|
||||
- [x] 12. Mock 数据移除与全量联调
|
||||
- [x] 12.1 移除所有页面中的 mock 数据残留
|
||||
- 搜索并移除所有 `import { mockXxx } from '../../utils/mock-data'` 引用
|
||||
- 确保 13 个页面(task-list、task-detail、notes、performance、performance-records、customer-detail、customer-service-records、coach-detail、board-coach、board-customer、board-finance、chat-history、chat)均使用真实 API
|
||||
- _需求: R10.1, R10.2_
|
||||
|
||||
- [x] 12.2 验证全量联调
|
||||
- 确保所有页面 API 调用失败时显示友好错误提示(Toast 或空状态占位),不出现白屏
|
||||
- 验证页面间跳转参数传递正确(RNS1.0 T0-6 已修复的跨页面参数)
|
||||
- 验证 chat 页面从 4 个入口进入时均能正确关联上下文
|
||||
- _需求: R10.2, R10.3, R10.4, R10.5_
|
||||
|
||||
- [x] 13. 全链路端到端测试(真实 AI 接口)
|
||||
- [x] 13.1 后端全链路测试:使用真实百炼 API 验证 CHAT-3(同步)和 CHAT-4(SSE 流式)
|
||||
- 启动后端服务连接 `test_zqyy_app`,使用真实测试用户 token
|
||||
- 调用 `POST /api/xcx/chat/{chatId}/messages` 发送真实消息,验证:
|
||||
- AI 回复内容质量(非乱码、语义相关、中文正常)
|
||||
- 用户消息和 AI 回复均已持久化到 `biz.ai_messages`
|
||||
- `biz.ai_conversations` 的 `last_message` 和 `last_message_at` 已更新
|
||||
- `tokens_used` 字段已记录
|
||||
- 调用 `POST /api/xcx/chat/stream` 验证 SSE 流式返回:
|
||||
- 逐 token 事件格式正确(`event: message`、`event: done`)
|
||||
- 完整回复拼接后语义通顺
|
||||
- 流结束后消息已落库
|
||||
- 手动评估 AI 返回内容质量(至少 3 轮对话),记录评估结果
|
||||
- _需求: R5.2, R5.4, R6.2, R6.3, R6.4, AC1, AC9_
|
||||
|
||||
- [x] 13.2 前端→后端→AI→数据库全链路验证
|
||||
- 在微信开发者工具中启动小程序,连接本地后端
|
||||
- 从 4 个入口(task-detail、customer-detail、coach-detail、chat-history)进入 chat 页面
|
||||
- 每个入口发送至少 1 条消息,验证:
|
||||
- SSE 流式逐字显示正常
|
||||
- 消息发送后页面状态正确(loading → 显示回复)
|
||||
- 返回 chat-history 页面能看到刚才的对话记录
|
||||
- referenceCard 在有客户关联时正确渲染(如有数据)
|
||||
- 验证错误场景:网络断开时的提示、空消息拦截
|
||||
- _需求: R6.7, R10.5, R4.4_
|
||||
|
||||
- [x] 13.3 AI 对话落库合规性验证(对照 P5 PRD + 用户确认的复用规则)
|
||||
- 验证对话复用规则:task 入口同一 taskId 始终复用;customer/coach 入口 ≤ 3 天复用、> 3 天新建;general 入口始终新建
|
||||
- 验证首条消息格式:应用 1 的首条消息应为页面上下文 JSON(`source_page`/`page_context`/`screen_content`/`current_time`),对照 P5 PRD 应用 1 Prompt 数据结构
|
||||
- 验证 `app_id` 字段:CHAT 模块对话的 `app_id` 应为 `app1_chat`
|
||||
- 验证 `ai_messages.role` 值:仅 `user`/`assistant`/`system` 三种(CHECK 约束)
|
||||
- 验证 `tokens_used` 记录:AI 回复消息应记录 token 消耗量
|
||||
- 验证 `source_page` 和 `source_context`:从不同入口进入时应正确记录来源页面和上下文
|
||||
- 验证 `context_type` 和 `context_id`:不同入口写入正确的上下文类型和 ID
|
||||
- _需求: AC9, P5-PRD 数据写入规则_
|
||||
|
||||
- [x] 14. DDL 迁移合并到主 DDL 基线
|
||||
- [x] 14.1 执行迁移脚本到 `test_zqyy_app`
|
||||
- 运行 `db/zqyy_app/migrations/2026-03-20__rns14_chat_module_extend.sql`(任务 1 创建的脚本)
|
||||
- 验证新字段和索引已正确创建(使用 BD 手册中的验证 SQL)
|
||||
- _需求: R2.3, R3.8, R3.10, R4.3, R7.5_
|
||||
|
||||
- [x] 14.2 合并到主 DDL 基线 `docs/database/ddl/zqyy_app__biz.sql`
|
||||
- 在 `biz.ai_conversations` 表定义中追加 5 个新字段:`context_type varchar(20)`、`context_id varchar(50)`、`title varchar(200)`、`last_message text`、`last_message_at timestamptz`
|
||||
- 在 `biz.ai_messages` 表定义中追加 1 个新字段:`reference_card jsonb`
|
||||
- 在索引区追加 2 个新索引:`idx_ai_conv_context`(条件索引,WHERE context_type IS NOT NULL)、`idx_ai_conv_last_msg`
|
||||
- 更新文件头部的"生成日期"注释
|
||||
- _需求: DDL 基线同步_
|
||||
|
||||
- [x] 15. 文档更新落地
|
||||
- [x] 15.1 更新 BD 手册 `docs/database/BD_Manual_ai_tables.md`
|
||||
- 在 `biz.ai_conversations` 字段明细中追加 5 个新字段(context_type/context_id/title/last_message/last_message_at)
|
||||
- 在 `biz.ai_messages` 字段明细中追加 reference_card 字段
|
||||
- 在约束与索引表中追加 2 个新索引(idx_ai_conv_context、idx_ai_conv_last_msg)
|
||||
- 更新兼容性影响:标注 RNS1.4 CHAT 模块依赖新字段
|
||||
- 更新验证 SQL:追加新字段和索引的验证查询
|
||||
- 更新回滚策略:追加 DROP INDEX 和 ALTER TABLE DROP COLUMN
|
||||
- _规范: db-docs.md 强制要求_
|
||||
|
||||
- [x] 15.2 更新 API 契约 `docs/miniprogram-dev/API-contract.md`
|
||||
- CHAT 部分路径从 `/api/ai/*` 更新为 `/api/xcx/chat/*`
|
||||
- 补充 CHAT-1(历史列表)、CHAT-2a/2b(消息查询,含 customerId 参数)、CHAT-3(发送消息)端点定义
|
||||
- 补充 referenceCard 结构定义
|
||||
- 补充 SSE 事件类型定义(message/done/error)
|
||||
- _需求: R1.1, R1.4_
|
||||
|
||||
- [x] 15.3 更新后端 API 参考 `apps/backend/docs/API-REFERENCE.md`
|
||||
- 新增 `xcx_chat` 路由模块文档(5 个端点)
|
||||
- 移除 `xcx_ai_chat` 路由模块文档(已删除)
|
||||
- _需求: R1.2, R1.3_
|
||||
|
||||
- [x] 15.4 更新后端 README `apps/backend/README.md`
|
||||
- 路由模块摘要中:移除 `xcx_ai_chat`,新增 `xcx_chat`(CHAT-1/2/3/4)
|
||||
- 服务层中:新增 `chat_service.py` 说明
|
||||
- _需求: R1.3_
|
||||
|
||||
- [x] 15.5 更新文档地图 `docs/DOCUMENTATION-MAP.md`
|
||||
- 在 3.1 FastAPI 后端部分新增 RNS1.4 模块(xcx_chat.py、chat_service.py、xcx_chat schema)
|
||||
- 在 5.6 Spec 文件表中新增 `rns1-chat-integration` 条目
|
||||
- 更新"最后更新"日期
|
||||
- _规范: doc-map.md 归档规则_
|
||||
|
||||
- [x] 15.6 更新 RNS1 拆分计划 `docs/prd/Neo_Specs/RNS1-split-plan.md`
|
||||
- 标注 RNS1.4 状态为"实施中"或"已完成"(视进度)
|
||||
- _需求: 项目追踪_
|
||||
|
||||
- [x] 16. 最终检查点 — 全量验证
|
||||
- 确保所有测试通过,13 个页面均连接真实后端运行,无 mock 数据残留
|
||||
- 确保 DDL 迁移已合并到主基线,BD 手册已同步更新
|
||||
- 确保 API 契约、后端 README、文档地图均已更新
|
||||
- 确保 AI 对话落库符合 P5 PRD 规范
|
||||
- ask the user if questions arise.
|
||||
|
||||
## 备注
|
||||
|
||||
- 标记 `*` 的子任务为可选,可跳过以加速 MVP 交付
|
||||
- 每个任务引用了具体的需求编号(R1-R11)以确保可追溯性
|
||||
- 属性测试验证通用正确性属性(Property 1-9),单元测试验证具体边界条件
|
||||
- 检查点任务确保增量验证,避免问题累积
|
||||
- 后端使用 Python(FastAPI + Pydantic),前端使用 TypeScript(微信小程序)
|
||||
|
||||
### PRD 合规注意事项
|
||||
|
||||
- **P5 PRD 数据写入规则**(`docs/prd/specs/P5-miniapp-ai-integration.md`):
|
||||
- 流式返回完成后,完整 assistant 回复写入 `ai_messages`(role=assistant)
|
||||
- 用户消息在发送时即写入(role=user)
|
||||
- 所有 AI 调用记录写入 `ai_conversations` + `ai_messages`(含 tokens_used 统计)
|
||||
- 首条消息为页面上下文 JSON(`source_page`/`page_context`/`screen_content`/`current_time`)
|
||||
- `app_id` 固定为 `app1_chat`
|
||||
- **对话复用规则**(用户已确认,覆盖 P5 PRD 的"始终新建"规则):
|
||||
- `task` 入口:同一 taskId 始终复用同一对话(无时限)
|
||||
- `customer` / `coach` 入口:最后消息 ≤ 3 天复用,> 3 天新建
|
||||
- `general` 入口(无参数):始终新建
|
||||
- `chat-history` 入口:直接打开已有对话(传 historyId)
|
||||
|
||||
### 文档更新清单
|
||||
|
||||
| 文档 | 更新内容 | 任务 |
|
||||
|------|---------|------|
|
||||
| `docs/database/ddl/zqyy_app__biz.sql` | 合并 5+1 新字段、2 新索引 | 14.2 |
|
||||
| `docs/database/BD_Manual_ai_tables.md` | 新字段(context_type/context_id/title/last_message/last_message_at/reference_card)/索引/兼容性/回滚/验证 SQL | 15.1 |
|
||||
| `docs/miniprogram-dev/API-contract.md` | CHAT 路径迁移 + 新端点定义 | 15.2 |
|
||||
| `apps/backend/docs/API-REFERENCE.md` | xcx_chat 路由模块文档 | 15.3 |
|
||||
| `apps/backend/README.md` | 路由模块 + 服务层更新 | 15.4 |
|
||||
| `docs/DOCUMENTATION-MAP.md` | RNS1.4 模块 + spec 条目 | 15.5 |
|
||||
| `docs/prd/Neo_Specs/RNS1-split-plan.md` | RNS1.4 状态更新 | 15.6 |
|
||||
1
.kiro/specs/tenant-admin-web/.config.kiro
Normal file
1
.kiro/specs/tenant-admin-web/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"specId": "9eccc890-b6c3-41a3-8ba1-bb2f0e09f653", "workflowType": "requirements-first", "specType": "feature"}
|
||||
623
.kiro/specs/tenant-admin-web/design.md
Normal file
623
.kiro/specs/tenant-admin-web/design.md
Normal file
@@ -0,0 +1,623 @@
|
||||
# 技术设计文档:租户管理后台(tenant-admin-web)
|
||||
|
||||
## 概述
|
||||
|
||||
本设计为 NS4 租户管理后台提供完整的技术方案。系统面向租户管理员(Tenant_Admin),提供用户审核与管理、Excel 数据上传(4 种模板)、维客线索管理三大功能模块。
|
||||
|
||||
前端为独立 React 应用(`apps/tenant-admin/`),后端复用现有 FastAPI 服务(`apps/backend/`)新增 4 个路由模块,所有端点注册在 `/api/tenant/` 前缀下。认证体系与小程序完全隔离,使用独立的 `auth.tenant_admins` 表、用户名+密码登录、JWT `aud=tenant-admin`。
|
||||
|
||||
### 关键设计决策
|
||||
|
||||
| 决策 | 选择 | 理由 |
|
||||
|------|------|------|
|
||||
| 认证表 | 独立 `auth.tenant_admins` | 与小程序 `auth.users`(微信登录)完全隔离,避免 user_type 判断复杂度 |
|
||||
| JWT 隔离 | `aud` 字段区分 | 复用现有 `jose` + `bcrypt` 基础设施,仅扩展 payload |
|
||||
| Excel 写入策略 | staging 表 + ETL 同步(方案 B) | 数据经 ETL 标准流程,一致性有保障;助教奖罚直接写 biz |
|
||||
| FDW 多店铺查询 | 逐 site_id 查询合并 | RLS 视图要求 `SET LOCAL app.current_site_id`,单次只能设一个 |
|
||||
| 前端技术栈 | React + Vite + Ant Design | 与 `apps/admin-web/` 一致,降低维护成本 |
|
||||
| 响应格式 | `{ code: 0, data }` | 复用现有 `ResponseWrapperMiddleware`,前端统一解包 |
|
||||
|
||||
---
|
||||
|
||||
## 架构
|
||||
|
||||
### 高层架构图
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "前端 apps/tenant-admin/"
|
||||
FE[React + Vite + Ant Design]
|
||||
API_LAYER[services/api.ts<br/>JWT 自动附加/刷新]
|
||||
end
|
||||
|
||||
subgraph "后端 apps/backend/"
|
||||
subgraph "路由层 /api/tenant/*"
|
||||
R_AUTH[tenant_auth.py<br/>登录/刷新]
|
||||
R_USERS[tenant_users.py<br/>审核+管理]
|
||||
R_EXCEL[tenant_excel.py<br/>上传/校验/冲突]
|
||||
R_CLUES[tenant_clues.py<br/>线索管理]
|
||||
end
|
||||
|
||||
AUTH_DEP[require_tenant_admin<br/>认证依赖注入]
|
||||
MIDDLEWARE[ResponseWrapperMiddleware<br/>全局响应包装]
|
||||
end
|
||||
|
||||
subgraph "数据层"
|
||||
APP_DB[(zqyy_app<br/>auth + biz + public)]
|
||||
ETL_DB[(etl_feiqiu<br/>fdw_etl 视图)]
|
||||
end
|
||||
|
||||
FE --> API_LAYER
|
||||
API_LAYER -->|HTTP| MIDDLEWARE
|
||||
MIDDLEWARE --> R_AUTH
|
||||
MIDDLEWARE --> R_USERS
|
||||
MIDDLEWARE --> R_EXCEL
|
||||
MIDDLEWARE --> R_CLUES
|
||||
R_USERS --> AUTH_DEP
|
||||
R_EXCEL --> AUTH_DEP
|
||||
R_CLUES --> AUTH_DEP
|
||||
R_AUTH -->|登录无需认证| APP_DB
|
||||
AUTH_DEP --> APP_DB
|
||||
R_USERS -->|FDW 人员匹配| ETL_DB
|
||||
R_EXCEL -->|FDW 助教匹配| ETL_DB
|
||||
R_CLUES -->|FDW 客户搜索| ETL_DB
|
||||
```
|
||||
|
||||
### 认证流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant FE as Tenant Admin Web
|
||||
participant BE as Backend API
|
||||
participant DB as auth.tenant_admins
|
||||
|
||||
FE->>BE: POST /api/tenant/auth/login {username, password}
|
||||
BE->>DB: SELECT password_hash WHERE username=? AND is_active=true
|
||||
alt 凭据有效
|
||||
BE->>BE: bcrypt.checkpw() 验证
|
||||
BE->>BE: 签发 JWT (aud=tenant-admin, sub=admin_id, managed_site_ids)
|
||||
BE-->>FE: {access_token, refresh_token}
|
||||
else 凭据无效
|
||||
BE-->>FE: 401 用户名或密码错误
|
||||
else 账号禁用
|
||||
BE-->>FE: 403 账号已被禁用
|
||||
end
|
||||
|
||||
Note over FE,BE: 后续请求
|
||||
FE->>BE: GET /api/tenant/* (Authorization: Bearer <token>)
|
||||
BE->>BE: require_tenant_admin() 验证 aud=tenant-admin
|
||||
BE->>BE: 提取 managed_site_ids,附加数据隔离条件
|
||||
```
|
||||
|
||||
### Excel 上传流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[选择模板类型 + 上传 Excel] --> B[后端解析 + 格式校验]
|
||||
B --> C{校验结果}
|
||||
C -->|格式错误| D[返回错误行号/列名/描述<br/>前端标红展示]
|
||||
D --> A
|
||||
C -->|校验通过| E[人员匹配校验<br/>仅 salary_adj / recharge_commission]
|
||||
E --> F[冲突检测<br/>按主键规则匹配]
|
||||
F --> G{有冲突?}
|
||||
G -->|是| H[返回 diff 数据<br/>前端展示 diff 表格]
|
||||
H --> I[用户逐行选择<br/>替换/保留]
|
||||
G -->|否| J[标记为待写入]
|
||||
I --> K[确认写入]
|
||||
J --> K
|
||||
K --> L{写入目标}
|
||||
L -->|助教奖罚| M[biz.salary_adjustments]
|
||||
L -->|财务支出| N[biz.stg_finance_expense]
|
||||
L -->|团购收入| O[biz.stg_platform_income]
|
||||
L -->|充值归属| P[biz.stg_recharge_commission]
|
||||
K --> Q[更新 excel_upload_log<br/>status=confirmed]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 组件与接口
|
||||
|
||||
### 后端路由模块
|
||||
|
||||
#### 1. `tenant_auth.py` — 认证路由
|
||||
|
||||
| 端点 | 方法 | 路径 | 认证 | 说明 |
|
||||
|------|------|------|------|------|
|
||||
| 登录 | POST | `/api/tenant/auth/login` | 无 | 用户名+密码 → JWT 令牌对 |
|
||||
| 刷新 | POST | `/api/tenant/auth/refresh` | 无 | refresh_token → 新令牌对 |
|
||||
|
||||
|
||||
#### 2. `tenant_users.py` — 用户审核 + 管理路由
|
||||
|
||||
| 端点 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 申请列表 | GET | `/api/tenant/applications` | status 筛选 + 分页 |
|
||||
| 关联建议 | GET | `/api/tenant/applications/{id}/match-suggestions` | FDW 助教/员工匹配 |
|
||||
| 审核通过 | POST | `/api/tenant/applications/{id}/approve` | 分配角色 + 绑定 |
|
||||
| 审核拒绝 | POST | `/api/tenant/applications/{id}/reject` | 填写拒绝原因 |
|
||||
| 用户列表 | GET | `/api/tenant/users` | 角色筛选 + 搜索 + 分页 |
|
||||
| 编辑用户 | PATCH | `/api/tenant/users/{id}` | 修改角色/门店/状态 |
|
||||
| 修改绑定 | PUT | `/api/tenant/users/{id}/binding` | 更新助教/员工绑定 |
|
||||
|
||||
#### 3. `tenant_excel.py` — Excel 上传路由
|
||||
|
||||
| 端点 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 上传解析 | POST | `/api/tenant/excel/upload` | multipart/form-data,校验+冲突检测 |
|
||||
| 确认写入 | POST | `/api/tenant/excel/confirm` | upload_id + resolutions[] |
|
||||
| 上传记录 | GET | `/api/tenant/excel/logs` | 历史记录列表 + 分页 |
|
||||
| 模板下载 | GET | `/api/tenant/excel/template/{type}` | 下载空白 Excel 模板 |
|
||||
|
||||
#### 4. `tenant_clues.py` — 维客线索路由
|
||||
|
||||
| 端点 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 客户搜索 | GET | `/api/tenant/customers/search` | keyword + site_id 筛选 |
|
||||
| 线索列表 | GET | `/api/tenant/customers/{member_id}/clues` | 该客户全部线索 |
|
||||
| 修改线索 | PATCH | `/api/tenant/clues/{id}` | category + summary + detail |
|
||||
| 删除线索 | DELETE | `/api/tenant/clues/{id}` | 物理删除 |
|
||||
| 隐藏/显示 | PATCH | `/api/tenant/clues/{id}/visibility` | is_hidden 切换 |
|
||||
|
||||
### 认证依赖注入
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class CurrentTenantAdmin:
|
||||
"""从 JWT 解析出的租户管理员上下文。"""
|
||||
admin_id: int
|
||||
tenant_id: int
|
||||
managed_site_ids: list[int]
|
||||
display_name: str | None = None
|
||||
|
||||
async def require_tenant_admin(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(_bearer_scheme),
|
||||
) -> CurrentTenantAdmin:
|
||||
"""
|
||||
验证 JWT aud=tenant-admin,提取管理员信息。
|
||||
拒绝小程序 JWT(aud 不匹配)。
|
||||
"""
|
||||
payload = decode_access_token(token) # 复用现有解码
|
||||
if payload.get("aud") != "tenant-admin":
|
||||
raise HTTPException(401, "令牌类型不匹配")
|
||||
return CurrentTenantAdmin(
|
||||
admin_id=int(payload["sub"]),
|
||||
tenant_id=payload["tenant_id"],
|
||||
managed_site_ids=payload["managed_site_ids"],
|
||||
)
|
||||
```
|
||||
|
||||
数据隔离通过 `managed_site_ids` 实现:
|
||||
|
||||
```python
|
||||
def site_filter_clause(admin: CurrentTenantAdmin) -> tuple[str, tuple]:
|
||||
"""生成 site_id IN (...) SQL 片段。"""
|
||||
placeholders = ",".join(["%s"] * len(admin.managed_site_ids))
|
||||
return f"site_id IN ({placeholders})", tuple(admin.managed_site_ids)
|
||||
```
|
||||
|
||||
### 前端组件结构
|
||||
|
||||
```
|
||||
apps/tenant-admin/src/
|
||||
├── pages/
|
||||
│ ├── Login/ # 登录页
|
||||
│ ├── UserApproval/ # 用户审核(申请列表 + 关联建议 + 审核操作)
|
||||
│ ├── UserManagement/ # 用户管理(列表 + 编辑 + 绑定)
|
||||
│ ├── ExcelUpload/ # Excel 上传(模板选择 + 上传 + 校验 + diff + 确认)
|
||||
│ └── RetentionClues/ # 维客线索(客户搜索 + 线索列表 + 编辑/删除/隐藏)
|
||||
├── components/
|
||||
│ ├── SiteSelector/ # 门店筛选器(页面顶部)
|
||||
│ ├── DiffTable/ # 冲突 diff 交互表格
|
||||
│ └── ClueEditor/ # 线索编辑表单
|
||||
├── services/
|
||||
│ └── api.ts # API 调用封装(JWT 自动附加/刷新)
|
||||
├── hooks/
|
||||
│ └── useAuth.ts # 认证状态管理
|
||||
├── utils/
|
||||
│ └── format.ts # 格式化工具(手机号脱敏等)
|
||||
└── App.tsx # 路由配置 + 侧边栏布局
|
||||
```
|
||||
|
||||
### API 调用层设计
|
||||
|
||||
复用 `apps/admin-web/src/api/client.ts` 的模式:
|
||||
- axios 实例,`baseURL: "/api/tenant"`
|
||||
- 请求拦截器:从 localStorage 读取 access_token 附加 Authorization header
|
||||
- 响应拦截器:401 时用 refresh_token 刷新,刷新失败重定向 `/login`
|
||||
- 并发刷新保护:多个 401 只触发一次 refresh,其余排队
|
||||
- 响应解包:`{ code: 0, data }` → 提取 `data`
|
||||
|
||||
### 响应格式约定
|
||||
|
||||
成功响应(由 `ResponseWrapperMiddleware` 自动包装):
|
||||
```json
|
||||
{ "code": 0, "data": { ... } }
|
||||
```
|
||||
|
||||
错误响应(由异常处理器格式化):
|
||||
```json
|
||||
{ "code": 401, "message": "用户名或密码错误" }
|
||||
```
|
||||
|
||||
分页响应:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"items": [...],
|
||||
"total": 100,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Pydantic 模型使用 `alias_generator=to_camel` 实现 snake_case → camelCase 转换。
|
||||
|
||||
---
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 新建表
|
||||
|
||||
#### 1. `auth.tenant_admins` — 租户管理员表
|
||||
|
||||
| 列名 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | BIGSERIAL | PK | 自增主键 |
|
||||
| username | VARCHAR(50) | UNIQUE NOT NULL | 登录用户名 |
|
||||
| password_hash | VARCHAR(255) | NOT NULL | bcrypt 哈希 |
|
||||
| display_name | VARCHAR(100) | — | 显示名称 |
|
||||
| tenant_id | BIGINT | NOT NULL | 所属租户 |
|
||||
| managed_site_ids | BIGINT[] | NOT NULL | 管辖门店 ID 列表 |
|
||||
| is_active | BOOLEAN | DEFAULT true | 账号状态 |
|
||||
| created_by | BIGINT | — | 创建者(Operator ID) |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
|
||||
| last_login_at | TIMESTAMPTZ | — | 最后登录时间 |
|
||||
|
||||
索引:`idx_tenant_admin_tenant ON (tenant_id)`
|
||||
|
||||
#### 2. `biz.excel_upload_log` — Excel 上传记录表
|
||||
|
||||
| 列名 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | BIGSERIAL | PK | 自增主键 |
|
||||
| site_id | BIGINT | NOT NULL | 门店 ID |
|
||||
| upload_type | VARCHAR(30) | CHECK IN (expense, platform_income, salary_adj, recharge_commission) | 模板类型 |
|
||||
| file_name | VARCHAR(255) | NOT NULL | 原始文件名 |
|
||||
| uploaded_by | BIGINT | NOT NULL | 上传人(管理员 ID) |
|
||||
| row_count | INTEGER | DEFAULT 0 | 数据行数 |
|
||||
| conflict_count | INTEGER | DEFAULT 0 | 冲突行数 |
|
||||
| resolved_count | INTEGER | DEFAULT 0 | 已解决冲突数 |
|
||||
| status | VARCHAR(20) | CHECK IN (pending, confirmed, failed) | 批次状态 |
|
||||
| error_detail | JSONB | — | 错误详情 |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 上传时间 |
|
||||
| confirmed_at | TIMESTAMPTZ | — | 确认时间 |
|
||||
|
||||
索引:`idx_excel_log_site ON (site_id, created_at DESC)`
|
||||
|
||||
#### 3. `biz.salary_adjustments` — 助教奖罚明细表
|
||||
|
||||
| 列名 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | BIGSERIAL | PK | 自增主键 |
|
||||
| site_id | BIGINT | NOT NULL | 门店 ID |
|
||||
| assistant_id | BIGINT | — | 匹配到的助教 ID(可空) |
|
||||
| assistant_name | VARCHAR(100) | NOT NULL | 助教姓名 |
|
||||
| assistant_number | VARCHAR(50) | NOT NULL | 助教编号 |
|
||||
| salary_month | VARCHAR(7) | NOT NULL | 月份 YYYY-MM |
|
||||
| adjustment_type | VARCHAR(20) | CHECK IN (deduction, bonus) | 类型 |
|
||||
| amount | NUMERIC(12,2) | CHECK > 0 | 金额 |
|
||||
| reason | VARCHAR(200) | NOT NULL | 原因 |
|
||||
| upload_batch_id | BIGINT | FK → excel_upload_log | 上传批次 |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
|
||||
| created_by | BIGINT | — | 上传人 |
|
||||
|
||||
索引:`(site_id, salary_month)`, `(assistant_id, salary_month)`
|
||||
|
||||
#### 4. `biz.stg_finance_expense` — 财务支出暂存表
|
||||
|
||||
| 列名 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | BIGSERIAL | PK | 自增主键 |
|
||||
| site_id | BIGINT | NOT NULL | 门店 ID |
|
||||
| expense_month | VARCHAR(7) | NOT NULL | 月份 |
|
||||
| category | VARCHAR(50) | NOT NULL | 支出类别(8 值枚举) |
|
||||
| amount | NUMERIC(12,2) | NOT NULL | 金额 |
|
||||
| remark | TEXT | — | 备注 |
|
||||
| upload_batch_id | BIGINT | FK → excel_upload_log | 上传批次 |
|
||||
| synced_at | TIMESTAMPTZ | — | ETL 同步时间(NULL=未同步) |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
|
||||
|
||||
#### 5. `biz.stg_platform_income` — 团购收入暂存表
|
||||
|
||||
| 列名 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | BIGSERIAL | PK | 自增主键 |
|
||||
| site_id | BIGINT | NOT NULL | 门店 ID |
|
||||
| income_month | VARCHAR(7) | NOT NULL | 月份 |
|
||||
| platform_name | VARCHAR(100) | NOT NULL | 平台名称 |
|
||||
| amount | NUMERIC(12,2) | NOT NULL | 收入金额 |
|
||||
| remark | TEXT | — | 备注 |
|
||||
| upload_batch_id | BIGINT | FK → excel_upload_log | 上传批次 |
|
||||
| synced_at | TIMESTAMPTZ | — | ETL 同步时间 |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
|
||||
|
||||
#### 6. `biz.stg_recharge_commission` — 充值业绩归属暂存表
|
||||
|
||||
| 列名 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | BIGSERIAL | PK | 自增主键 |
|
||||
| site_id | BIGINT | NOT NULL | 门店 ID |
|
||||
| recharge_date | DATE | NOT NULL | 充值日期 |
|
||||
| member_name | VARCHAR(100) | NOT NULL | 会员名称 |
|
||||
| recharge_amount | NUMERIC(12,2) | NOT NULL | 充值金额 |
|
||||
| assigned_assistant | VARCHAR(100) | NOT NULL | 归属助教 |
|
||||
| reward_amount | NUMERIC(12,2) | NOT NULL | 奖励金额 |
|
||||
| upload_batch_id | BIGINT | FK → excel_upload_log | 上传批次 |
|
||||
| synced_at | TIMESTAMPTZ | — | ETL 同步时间 |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
|
||||
|
||||
### 表结构变更
|
||||
|
||||
#### `public.member_retention_clue` — 新增 `is_hidden` 列
|
||||
|
||||
```sql
|
||||
ALTER TABLE public.member_retention_clue
|
||||
ADD COLUMN is_hidden BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
COMMENT ON COLUMN public.member_retention_clue.is_hidden
|
||||
IS '是否隐藏(true=管理后台保留但小程序不展示)';
|
||||
```
|
||||
|
||||
已有数据通过 `DEFAULT false` 保证兼容。小程序端线索查询需增加 `WHERE is_hidden = false`。
|
||||
|
||||
### 复用的现有表(auth Schema)
|
||||
|
||||
| 表 | 用途 |
|
||||
|---|------|
|
||||
| `auth.users` | 小程序用户主表(审核时更新 status) |
|
||||
| `auth.user_applications` | 用户入驻申请(审核列表数据源) |
|
||||
| `auth.site_code_mapping` | 球房编号 → site_id 映射 |
|
||||
| `auth.roles` | 角色定义(coach/staff/site_admin/tenant_admin) |
|
||||
| `auth.user_site_roles` | 用户-门店-角色关联(审核通过时写入) |
|
||||
| `auth.user_assistant_binding` | 用户-助教/员工绑定(审核通过时写入) |
|
||||
|
||||
### FDW 数据源(ETL 库只读)
|
||||
|
||||
| 视图 | 用途 | 查询场景 |
|
||||
|------|------|---------|
|
||||
| `fdw_etl.v_dim_assistant` | 助教维度表 | 用户审核关联建议、Excel 人员匹配 |
|
||||
| `fdw_etl.v_dim_staff` + `v_dim_staff_ex` | 员工维度表 | 用户审核关联建议、Excel 人员匹配 |
|
||||
| `fdw_etl.v_dim_member` | 会员维度表 | 维客线索客户搜索(DQ-6 规则) |
|
||||
|
||||
多店铺查询策略:逐 `site_id` 调用 `get_etl_readonly_connection(site_id)` 查询后合并结果。
|
||||
|
||||
### Pydantic Schema 设计
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic.alias_generators import to_camel
|
||||
|
||||
class TenantBaseModel(BaseModel):
|
||||
"""租户管理后台基础模型,统一驼峰命名。"""
|
||||
model_config = ConfigDict(
|
||||
alias_generator=to_camel,
|
||||
populate_by_name=True,
|
||||
)
|
||||
|
||||
class PaginatedResponse(TenantBaseModel, Generic[T]):
|
||||
items: list[T]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 正确性属性(Correctness Properties)
|
||||
|
||||
*属性(Property)是在系统所有有效执行中都应成立的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规格说明与机器可验证正确性保证之间的桥梁。*
|
||||
|
||||
### Property 1: 认证正确性
|
||||
|
||||
*For any* 用户名和密码组合,如果该组合对应一个活跃的 `auth.tenant_admins` 记录且密码哈希匹配,则登录应返回包含 `aud=tenant-admin` 的有效 JWT 令牌对;如果用户名不存在或密码不匹配,则应返回 401 且错误消息不区分是用户名还是密码错误。
|
||||
|
||||
**Validates: Requirements 1.1, 1.2**
|
||||
|
||||
### Property 2: JWT 隔离与端点保护
|
||||
|
||||
*For any* JWT 令牌和任意 `/api/tenant/*` 受保护端点,当且仅当令牌有效且 `aud=tenant-admin` 时请求应被允许;`aud` 不匹配(如小程序 JWT `aud=xcx`)、令牌过期或令牌无效时应返回 401。
|
||||
|
||||
**Validates: Requirements 1.4, 1.6, 17.3**
|
||||
|
||||
### Property 3: 数据隔离
|
||||
|
||||
*For any* 租户管理员和任意数据查询端点,返回结果中每条记录的 `site_id` 必须属于该管理员的 `managed_site_ids` 集合;尝试访问不在 `managed_site_ids` 范围内的数据应返回 403。
|
||||
|
||||
**Validates: Requirements 2.1, 2.2, 2.3**
|
||||
|
||||
### Property 4: 审核通过多表一致性
|
||||
|
||||
*For any* 状态为 `pending` 的用户申请,执行审核通过操作后,以下条件应同时成立:`auth.users.status = 'approved'`、`auth.user_site_roles` 中存在对应角色记录、`auth.user_assistant_binding` 中存在绑定记录(如提供了 assistant_id/staff_id)、`auth.user_applications.status = 'approved'` 且审核人和审核时间已填写。
|
||||
|
||||
**Validates: Requirements 3.4**
|
||||
|
||||
### Property 5: 审核拒绝状态更新
|
||||
|
||||
*For any* 状态为 `pending` 的用户申请和任意非空拒绝原因字符串,执行审核拒绝操作后,`auth.user_applications.status` 应为 `rejected`,`review_note` 应等于提交的拒绝原因,`reviewed_at` 应非空。
|
||||
|
||||
**Validates: Requirements 3.5**
|
||||
|
||||
### Property 6: 申请关联匹配
|
||||
|
||||
*For any* 用户申请中的手机号和球房编号,关联匹配逻辑应:先通过 `site_code_mapping` 解析 `site_id`,然后在 `v_dim_assistant`(`scd2_is_current=1`)和 `v_dim_staff` + `v_dim_staff_ex` 中按 phone 匹配,返回的候选列表应仅包含 phone 匹配的记录。
|
||||
|
||||
**Validates: Requirements 3.3**
|
||||
|
||||
### Property 7: 用户管理编辑
|
||||
|
||||
*For any* 已通过审核的用户和任意有效的编辑参数(角色、site_id 在管辖范围内、状态),编辑操作后用户记录应反映新值;修改绑定关系后 `user_assistant_binding` 应更新为新的 `assistant_id`/`staff_id`。
|
||||
|
||||
**Validates: Requirements 4.2, 4.3, 4.4**
|
||||
|
||||
### Property 8: Excel 格式校验
|
||||
|
||||
*For any* 模板类型和任意数据行集合,校验器应:对符合模板列定义的行标记为通过,对不符合的行返回包含行号、列名和错误描述的错误信息;校验全部通过时应创建 `excel_upload_log` 记录(status=pending);通过行数 + 警告行数 + 错误行数 = 总行数。
|
||||
|
||||
**Validates: Requirements 5.1, 5.2, 5.3, 5.4, 6.5**
|
||||
|
||||
### Property 9: 人员匹配校验
|
||||
|
||||
*For any* 助教姓名+编号组合和给定的助教/员工数据集,匹配逻辑应:优先在 `v_dim_assistant`(nickname + assistant_number)中匹配,未匹配时再查 `v_dim_staff` + `v_dim_staff_ex`(name + staff_number);匹配成功时填充 `assistant_id`,匹配失败时标记为 warning 且不阻断流程。
|
||||
|
||||
**Validates: Requirements 6.1, 6.2, 6.3, 6.4**
|
||||
|
||||
### Property 10: 冲突检测
|
||||
|
||||
*For any* 模板类型、上传数据集和已有数据集,冲突检测应:按模板主键规则(如财务支出的"月份+支出类别")识别主键重复的行,对冲突行返回逐字段 diff(旧值 vs 新值),非冲突行标记为待写入。
|
||||
|
||||
**Validates: Requirements 7.1, 7.2**
|
||||
|
||||
### Property 11: 数据写入 round-trip
|
||||
|
||||
*For any* 确认写入的数据批次和冲突解决方案,写入后从目标表读取的数据应与提交的数据一致(选择"替换"的行使用新值,选择"保留"的行保持旧值);`excel_upload_log` 状态应更新为 `confirmed`,实际写入行数应正确记录。
|
||||
|
||||
**Validates: Requirements 7.4, 7.5, 8.1, 8.2**
|
||||
|
||||
### Property 12: 客户搜索
|
||||
|
||||
*For any* 搜索关键词和管辖门店范围,搜索结果应满足:每条结果的 `nickname` 包含关键词(模糊匹配)或 `mobile` 等于关键词(精确匹配),且 `site_id` 在管辖范围内;指定门店筛选时结果应仅包含该门店的客户。
|
||||
|
||||
**Validates: Requirements 9.1, 9.3**
|
||||
|
||||
### Property 13: 线索编辑校验
|
||||
|
||||
*For any* 有效的编辑参数(category 在 6 值枚举内、summary 非空且 ≤200 字符),编辑操作后线索记录应反映新值;对于无效的 category 或空/超长 summary,应拒绝操作。
|
||||
|
||||
**Validates: Requirements 11.1, 11.2**
|
||||
|
||||
### Property 14: 线索删除
|
||||
|
||||
*For any* 存在且在管辖门店范围内的线索,执行删除操作后,该线索应不可通过任何查询接口获取(物理删除)。
|
||||
|
||||
**Validates: Requirements 12.2**
|
||||
|
||||
### Property 15: 线索隐藏/显示 round-trip
|
||||
|
||||
*For any* 线索,将 `is_hidden` 设为 `true` 后,小程序端查询(`WHERE is_hidden = false`)不应返回该线索,但管理后台查询应返回;再将 `is_hidden` 设回 `false` 后,小程序端查询应恢复返回该线索。
|
||||
|
||||
**Validates: Requirements 13.1, 13.2, 13.3, 15.3**
|
||||
|
||||
### Property 16: 响应格式一致性
|
||||
|
||||
*For any* `/api/tenant/*` 端点的成功响应,响应体应符合 `{ code: 0, data: ... }` 格式;错误响应应符合 `{ code: <status_code>, message: <string> }` 格式;分页响应的 `data` 应包含 `items`、`total`、`page`、`pageSize` 字段。
|
||||
|
||||
**Validates: Requirements 17.4**
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 认证错误
|
||||
|
||||
| 场景 | HTTP 状态码 | 错误消息 | 处理方式 |
|
||||
|------|------------|---------|---------|
|
||||
| 用户名或密码错误 | 401 | "用户名或密码错误" | 统一消息,不区分 |
|
||||
| 账号已禁用 | 403 | "账号已被禁用" | 检查 `is_active` |
|
||||
| JWT 过期 | 401 | "令牌已过期" | 前端自动刷新 |
|
||||
| JWT 无效/aud 不匹配 | 401 | "无效的令牌" | 重定向登录页 |
|
||||
| 刷新令牌过期 | 401 | "刷新令牌已过期" | 清除令牌,重定向登录页 |
|
||||
|
||||
### 数据隔离错误
|
||||
|
||||
| 场景 | HTTP 状态码 | 错误消息 |
|
||||
|------|------------|---------|
|
||||
| 访问非管辖门店数据 | 403 | "无权访问该门店数据" |
|
||||
| 修改 site_id 超出管辖范围 | 403 | "目标门店不在管辖范围内" |
|
||||
|
||||
### 业务逻辑错误
|
||||
|
||||
| 场景 | HTTP 状态码 | 错误消息 |
|
||||
|------|------------|---------|
|
||||
| 审核非 pending 状态的申请 | 409 | "该申请已被处理" |
|
||||
| 线索 ID 不存在 | 404 | "线索不存在" |
|
||||
| 线索不在管辖范围 | 404 | "线索不存在" |
|
||||
| category 枚举值无效 | 422 | "无效的线索大类" |
|
||||
| summary 为空或超长 | 422 | "摘要不能为空且不超过200字符" |
|
||||
|
||||
### Excel 上传错误
|
||||
|
||||
| 场景 | HTTP 状态码 | 错误消息 |
|
||||
|------|------------|---------|
|
||||
| 文件格式非 .xlsx/.xls | 400 | "请上传有效的 Excel 文件" |
|
||||
| 格式校验失败 | 200 | 返回错误行详情(不阻断,前端标红) |
|
||||
| 人员匹配失败 | 200 | 标记为 warning(不阻断,前端黄色高亮) |
|
||||
| 写入数据库失败 | 500 | 回滚整批,log status=failed,记录 error_detail |
|
||||
|
||||
### 数据库事务策略
|
||||
|
||||
- Excel 写入:整批次在单个事务中执行,任何行写入失败则回滚全部
|
||||
- 审核通过:多表写入在单个事务中执行(users + user_site_roles + user_assistant_binding + user_applications)
|
||||
- 线索操作:单条记录操作,无需跨表事务
|
||||
|
||||
---
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 双轨测试方法
|
||||
|
||||
本项目采用单元测试 + 属性测试的双轨策略:
|
||||
|
||||
- **单元测试**:验证具体示例、边界条件和错误处理
|
||||
- **属性测试**:验证跨所有输入的通用属性
|
||||
|
||||
两者互补:单元测试捕获具体 bug,属性测试验证通用正确性。
|
||||
|
||||
### 属性测试配置
|
||||
|
||||
- **库**:Python 后端使用 `hypothesis`(项目已有 `.hypothesis/` 目录);前端使用 `fast-check`
|
||||
- **迭代次数**:每个属性测试最少 100 次迭代
|
||||
- **标签格式**:`Feature: tenant-admin-web, Property {number}: {property_text}`
|
||||
- **每个正确性属性对应一个属性测试**
|
||||
|
||||
### 单元测试覆盖
|
||||
|
||||
| 模块 | 测试重点 |
|
||||
|------|---------|
|
||||
| `tenant_auth.py` | 登录成功/失败、禁用账号、JWT 签发/验证、刷新令牌 |
|
||||
| `tenant_users.py` | 申请列表筛选、审核通过/拒绝、非 pending 状态拒绝(409)、用户编辑、绑定修改 |
|
||||
| `tenant_excel.py` | 4 种模板格式校验、人员匹配、冲突检测、写入回滚、模板下载 |
|
||||
| `tenant_clues.py` | 客户搜索、线索 CRUD、隐藏/显示、枚举校验、越权访问 |
|
||||
| 前端 `services/api.ts` | JWT 自动附加、401 刷新、并发刷新保护 |
|
||||
|
||||
### 属性测试覆盖
|
||||
|
||||
| Property | 测试描述 | 生成器 |
|
||||
|----------|---------|--------|
|
||||
| P1 | 认证正确性 | 随机用户名+密码 |
|
||||
| P2 | JWT 隔离 | 随机 JWT payload(不同 aud) |
|
||||
| P3 | 数据隔离 | 随机 managed_site_ids + 随机数据 |
|
||||
| P4 | 审核通过一致性 | 随机 pending 申请 + 角色/绑定参数 |
|
||||
| P5 | 审核拒绝 | 随机 pending 申请 + 随机拒绝原因 |
|
||||
| P6 | 关联匹配 | 随机手机号 + 随机助教/员工数据集 |
|
||||
| P7 | 用户编辑 | 随机用户 + 随机编辑参数 |
|
||||
| P8 | Excel 校验 | 随机模板类型 + 随机数据行(含有效/无效) |
|
||||
| P9 | 人员匹配 | 随机姓名+编号 + 随机助教/员工数据集 |
|
||||
| P10 | 冲突检测 | 随机上传数据 + 随机已有数据 |
|
||||
| P11 | 写入 round-trip | 随机数据 + 随机冲突解决方案 |
|
||||
| P12 | 客户搜索 | 随机关键词 + 随机客户数据集 |
|
||||
| P13 | 线索编辑 | 随机 category + 随机 summary |
|
||||
| P14 | 线索删除 | 随机线索 ID |
|
||||
| P15 | 隐藏/显示 round-trip | 随机线索 + 隐藏→显示→验证 |
|
||||
| P16 | 响应格式 | 随机端点调用 |
|
||||
|
||||
### 边界条件测试(单元测试覆盖)
|
||||
|
||||
- 禁用账号登录(1.3)→ 403
|
||||
- 非 pending 申请审核(3.6)→ 409
|
||||
- 越权修改 site_id(4.5)→ 403
|
||||
- 无效 Excel 文件格式(5.5)→ 400
|
||||
- 写入数据库失败回滚(8.3)→ log status=failed
|
||||
- 线索 ID 不存在/越权(11.3, 12.3, 13.4)→ 404
|
||||
241
.kiro/specs/tenant-admin-web/requirements.md
Normal file
241
.kiro/specs/tenant-admin-web/requirements.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# 需求文档:租户管理后台(tenant-admin-web)
|
||||
|
||||
## 简介
|
||||
|
||||
构建独立的租户管理 Web 应用(`apps/tenant-admin/`),面向租户管理员,提供用户审核与管理、Excel 数据上传(4 种模板)、维客线索管理三大功能模块。前端采用 React + Vite + Ant Design(与 `apps/admin-web/` 同技术栈),后端复用 `apps/backend/` FastAPI 新增 4 个路由模块。认证体系与小程序完全隔离,使用独立的 `auth.tenant_admins` 表、用户名+密码登录、不同 JWT audience。所有数据查询附加 `site_id IN (管辖列表)` 条件实现多门店数据隔离。
|
||||
|
||||
## 术语表
|
||||
|
||||
- **Tenant_Admin_Web**:租户管理后台前端应用,部署在 `apps/tenant-admin/`
|
||||
- **Backend_API**:FastAPI 后端服务,部署在 `apps/backend/`,为 Tenant_Admin_Web 提供 RESTful API
|
||||
- **Tenant_Admin**:租户管理员,由系统管理后台 Operator 创建的账号,使用用户名+密码登录租户管理后台
|
||||
- **Operator**:系统管理后台操作员,在 `apps/admin-web/` 中管理租户管理员账号
|
||||
- **Site**:门店,通过 `site_id` 标识,是多门店数据隔离的基本单位
|
||||
- **Site_Code**:球房编号,用户申请时输入的门店标识码,通过 `auth.site_code_mapping` 映射到 `site_id`
|
||||
- **User_Application**:用户入驻申请,小程序用户提交的申请记录,存储在 `auth.user_applications`
|
||||
- **Retention_Clue**:维客线索,助教为会员记录的销售/维护线索,存储在 `public.member_retention_clue`
|
||||
- **Staging_Table**:暂存表,Excel 上传数据先写入业务库暂存表,再由 ETL 同步到 DWS 层
|
||||
- **Upload_Batch**:上传批次,一次 Excel 上传操作的记录,存储在 `biz.excel_upload_log`
|
||||
- **Salary_Adjustment**:助教奖罚明细,通过 Excel 上传写入 `biz.salary_adjustments`
|
||||
- **Managed_Site_Ids**:管辖门店列表,Tenant_Admin 被授权管理的 `site_id` 数组
|
||||
|
||||
## 需求
|
||||
|
||||
### 需求 1:租户管理员认证
|
||||
|
||||
**用户故事:** 作为 Tenant_Admin,我希望通过用户名和密码登录租户管理后台,以便安全地执行用户审核、数据上传等管理操作。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Tenant_Admin 提交有效的用户名和密码, THE Backend_API SHALL 验证凭据(bcrypt 哈希比对),签发 JWT 访问令牌(audience 为 `tenant-admin`,与小程序 `xcx` 隔离),并返回访问令牌和刷新令牌
|
||||
2. WHEN Tenant_Admin 提交无效的用户名或密码, THE Backend_API SHALL 返回 401 状态码和错误描述,不泄露具体是用户名还是密码错误
|
||||
3. IF Tenant_Admin 账号状态为禁用(`is_active = false`), THEN THE Backend_API SHALL 返回 403 状态码并提示账号已被禁用
|
||||
4. WHILE Tenant_Admin 持有有效的 JWT 令牌, THE Backend_API SHALL 允许访问 `/api/tenant/*` 路径下的受保护端点
|
||||
5. WHEN JWT 访问令牌过期, THE Tenant_Admin_Web SHALL 使用刷新令牌自动获取新的访问令牌;WHEN 刷新令牌也过期, THE Tenant_Admin_Web SHALL 将 Tenant_Admin 重定向到登录页面
|
||||
6. THE Backend_API SHALL 通过 JWT 中的 `aud` 字段区分租户管理员和小程序用户,使用独立的认证依赖注入(`require_tenant_admin()`),拒绝小程序 JWT 访问租户管理端点
|
||||
|
||||
### 需求 2:数据隔离与权限控制
|
||||
|
||||
**用户故事:** 作为 Tenant_Admin,我希望只能查看和操作自己管辖门店的数据,以确保多门店之间的数据安全隔离。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Backend_API SHALL 在所有租户管理端点的数据查询中附加 `site_id IN (managed_site_ids)` 条件,其中 `managed_site_ids` 从当前 Tenant_Admin 的 JWT 令牌或 `auth.tenant_admins` 记录中获取
|
||||
2. WHEN Tenant_Admin 的 `managed_site_ids` 包含多个 site_id, THE Backend_API SHALL 支持跨门店聚合查询(合并多个 site_id 的结果)
|
||||
3. IF Tenant_Admin 尝试访问不在其 `managed_site_ids` 范围内的数据, THEN THE Backend_API SHALL 返回 403 状态码
|
||||
4. THE Tenant_Admin_Web SHALL 在页面顶部提供门店筛选器,允许 Tenant_Admin 在管辖范围内切换或选择门店
|
||||
|
||||
### 需求 3:用户申请审核
|
||||
|
||||
**用户故事:** 作为 Tenant_Admin,我希望查看和审核小程序用户的入驻申请,以便控制哪些用户可以使用系统。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Tenant_Admin 打开用户审核页面, THE Tenant_Admin_Web SHALL 从 Backend_API 获取管辖门店范围内的用户申请列表,支持按状态筛选(全部 / 待审核 pending / 已通过 approved / 已拒绝 rejected),支持分页
|
||||
2. THE Backend_API SHALL 返回每条申请的以下信息:申请人昵称、手机号、球房编号(site_code)、申请角色文本、员工编号、申请时间、当前状态
|
||||
3. WHEN Tenant_Admin 查看某条待审核申请的关联建议, THE Backend_API SHALL 根据申请中的球房编号通过 `auth.site_code_mapping` 查得 `site_id`,再并行匹配 `fdw_etl.v_dim_assistant`(phone 匹配,`scd2_is_current=1`)和 `fdw_etl.v_dim_staff` + `v_dim_staff_ex`(phone 匹配),返回匹配建议列表
|
||||
4. WHEN Tenant_Admin 审核通过一条申请, THE Backend_API SHALL 接受角色(助教/管理者/员工)、关联助教 ID(可选)、关联员工 ID(可选),执行以下操作:更新 `auth.users.status = 'approved'`、写入 `auth.user_site_roles`(分配角色)、写入 `auth.user_assistant_binding`(关联助教/员工,含 staff_id)、更新 `auth.user_applications.status = 'approved'` 及审核人和审核时间
|
||||
5. WHEN Tenant_Admin 审核拒绝一条申请, THE Backend_API SHALL 接受拒绝原因(必填),更新 `auth.user_applications.status = 'rejected'`、`review_note` 和审核时间
|
||||
6. IF 申请状态不是 pending, THEN THE Backend_API SHALL 拒绝审核操作并返回 409 状态码
|
||||
|
||||
### 需求 4:用户管理
|
||||
|
||||
**用户故事:** 作为 Tenant_Admin,我希望管理已通过审核的用户信息,包括修改角色、店铺归属和助教绑定关系,以便维护用户数据的准确性。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Tenant_Admin 打开用户管理页面, THE Tenant_Admin_Web SHALL 展示管辖门店范围内已通过审核的用户列表,包含姓名、角色、关联助教姓名、所属门店、账号状态,支持按角色筛选和关键词搜索,支持分页
|
||||
2. WHEN Tenant_Admin 编辑用户信息, THE Backend_API SHALL 允许修改以下字段:角色(助教/管理者/员工)、所属门店(`site_id`,限管辖范围内)、账号状态(启用/禁用)
|
||||
3. WHEN Tenant_Admin 修改用户的助教/员工绑定关系, THE Backend_API SHALL 更新 `auth.user_assistant_binding` 记录,接受新的 `assistant_id` 和/或 `staff_id`
|
||||
4. WHEN Tenant_Admin 禁用某用户账号, THE Backend_API SHALL 将 `auth.users.status` 设为 `disabled`,该用户后续登录小程序时 SHALL 被拒绝
|
||||
5. IF Tenant_Admin 尝试将用户的 site_id 修改为不在其管辖范围内的值, THEN THE Backend_API SHALL 返回 403 状态码
|
||||
|
||||
### 需求 5:Excel 上传 — 文件解析与格式校验
|
||||
|
||||
**用户故事:** 作为 Tenant_Admin,我希望上传 Excel 文件并获得即时的格式校验反馈,以便在数据写入前发现和修正错误。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Tenant_Admin 选择模板类型并上传 Excel 文件, THE Backend_API SHALL 解析文件内容,按对应模板的列定义进行格式校验,并返回校验结果
|
||||
2. THE Backend_API SHALL 支持 4 种模板类型的格式校验:
|
||||
- 财务支出(expense):月份(YYYY-MM,不超过当前月)、支出类别(枚举 8 值:房租/水电/物业/食品饮料进货/耗材/报销/固定人员工资/其他费用)、金额(> 0,精度 2 位小数)、备注(可选,最长 500 字符)
|
||||
- 团购收入(platform_income):月份(YYYY-MM)、平台名称(非空)、收入金额(> 0)、备注(可选,最长 500 字符)
|
||||
- 助教奖罚(salary_adj):月份(YYYY-MM)、助教姓名(非空)、助教编号(非空)、类型(枚举:扣款/奖金)、金额(> 0)、原因(非空,最长 200 字符)
|
||||
- 充值业绩归属(recharge_commission):充值日期(YYYY-MM-DD)、会员名称(非空)、充值金额(> 0)、归属助教(非空)、奖励金额(≥ 0)
|
||||
3. WHEN 校验发现格式错误行, THE Backend_API SHALL 返回错误行号、列名和具体错误描述,Tenant_Admin_Web SHALL 标红展示错误行
|
||||
4. WHEN 校验全部通过, THE Backend_API SHALL 创建 `biz.excel_upload_log` 记录(状态为 pending),返回 upload_id 和解析后的数据预览
|
||||
5. IF 上传文件不是有效的 Excel 格式(.xlsx/.xls), THEN THE Backend_API SHALL 返回 400 状态码并提示文件格式错误
|
||||
|
||||
### 需求 6:Excel 上传 — 人员匹配校验
|
||||
|
||||
**用户故事:** 作为 Tenant_Admin,我希望上传助教奖罚和充值业绩归属数据时,系统能自动校验助教信息的准确性,以减少数据录入错误。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 上传助教奖罚(salary_adj)或充值业绩归属(recharge_commission)模板时, THE Backend_API SHALL 对每行数据中的助教姓名+助教编号执行匹配校验
|
||||
2. THE Backend_API SHALL 按以下顺序匹配:先查 `fdw_etl.v_dim_assistant`(nickname + assistant_number,`scd2_is_current=1`),如不匹配再查 `fdw_etl.v_dim_staff` + `v_dim_staff_ex`(name + staff_number)
|
||||
3. WHEN 匹配成功, THE Backend_API SHALL 在返回数据中填充 `assistant_id`
|
||||
4. WHEN 匹配失败, THE Backend_API SHALL 标记该行为校验警告(warning),不阻断上传流程,但在前端以黄色高亮提示 Tenant_Admin 确认
|
||||
5. THE Tenant_Admin_Web SHALL 在校验结果页面汇总展示:通过行数、警告行数、错误行数
|
||||
|
||||
### 需求 7:Excel 上传 — 冲突检测与解决
|
||||
|
||||
**用户故事:** 作为 Tenant_Admin,我希望在数据写入前看到与已有数据的冲突情况,并能逐行选择处理方式,以避免误覆盖历史数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 格式校验通过后, THE Backend_API SHALL 按各模板的主键规则检测冲突:
|
||||
- 财务支出:月份 + 支出类别
|
||||
- 团购收入:月份 + 平台名称
|
||||
- 助教奖罚:月份 + 助教姓名 + 助教编号 + 类型 + 原因
|
||||
- 充值业绩归属:充值日期 + 会员名称 + 归属助教
|
||||
2. WHEN 检测到冲突行(主键已存在), THE Backend_API SHALL 返回 diff 数据(旧值 vs 新值,按字段逐一对比)
|
||||
3. THE Tenant_Admin_Web SHALL 展示 diff 交互表格,每行显示字段名、旧值、新值和操作选项(替换/保留),支持"全部替换"和"全部保留"快捷操作
|
||||
4. WHEN Tenant_Admin 确认冲突解决方案并提交, THE Backend_API SHALL 接受 upload_id 和每行的解决方案(resolutions 数组),按选择执行写入
|
||||
5. WHEN 无冲突行, THE Backend_API SHALL 直接标记为"待写入",Tenant_Admin 确认后写入
|
||||
|
||||
### 需求 8:Excel 上传 — 数据写入与记录
|
||||
|
||||
**用户故事:** 作为 Tenant_Admin,我希望确认后的数据能正确写入目标表,并保留上传历史记录以便追溯。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Tenant_Admin 确认写入, THE Backend_API SHALL 将数据写入对应目标表:
|
||||
- 助教奖罚 → `biz.salary_adjustments`(直接写入业务库)
|
||||
- 财务支出 → `biz.stg_finance_expense`(staging 表,由 ETL 同步到 DWS)
|
||||
- 团购收入 → `biz.stg_platform_income`(staging 表)
|
||||
- 充值业绩归属 → `biz.stg_recharge_commission`(staging 表)
|
||||
2. THE Backend_API SHALL 在写入完成后更新 `biz.excel_upload_log` 记录:状态设为 `confirmed`、记录实际写入行数、冲突解决数、确认时间
|
||||
3. IF 写入过程中发生数据库错误, THEN THE Backend_API SHALL 回滚整个批次的写入,将 `biz.excel_upload_log` 状态设为 `failed`,记录错误详情到 `error_detail` 字段
|
||||
4. WHEN Tenant_Admin 查看上传记录页面, THE Backend_API SHALL 返回管辖门店范围内的历史上传记录列表,包含模板类型、文件名、上传人、上传时间、行数、冲突数、状态,支持分页
|
||||
5. WHEN Tenant_Admin 下载空白模板, THE Backend_API SHALL 返回对应模板类型的 Excel 文件(含表头和格式说明)
|
||||
|
||||
### 需求 9:维客线索 — 客户搜索
|
||||
|
||||
**用户故事:** 作为 Tenant_Admin,我希望通过客户姓名或手机号搜索客户,以便查看和管理其维客线索。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Tenant_Admin 输入搜索关键词, THE Backend_API SHALL 在管辖门店范围内搜索客户,匹配 `fdw_etl.v_dim_member` 的 `nickname`(模糊匹配)或 `mobile`(精确匹配),返回客户列表(member_id、姓名、手机号脱敏、所属门店)
|
||||
2. THE Backend_API SHALL 通过 `member_id` JOIN `fdw_etl.v_dim_member`(`scd2_is_current=1`)获取客户姓名和手机号,不使用结算单上的冗余字段(DQ-6 规则)
|
||||
3. WHEN Tenant_Admin 选择门店筛选条件, THE Backend_API SHALL 在搜索结果中仅返回该门店的客户
|
||||
4. IF 搜索结果为空, THEN THE Tenant_Admin_Web SHALL 展示"未找到匹配客户"的提示
|
||||
|
||||
### 需求 10:维客线索 — 线索列表与展示
|
||||
|
||||
**用户故事:** 作为 Tenant_Admin,我希望查看某客户的全部维客线索,以便了解客户画像和历史记录。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Tenant_Admin 选择某客户, THE Backend_API SHALL 返回该客户在管辖门店范围内的全部维客线索列表,包含:线索 ID、大类标签(category)、摘要(summary)、详情(detail)、提供人(recorded_by_name)、来源(source:manual / ai_consumption / ai_note)、记录时间(recorded_at)、隐藏状态(is_hidden)
|
||||
2. THE Tenant_Admin_Web SHALL 按大类标签分组展示线索,支持按来源和隐藏状态筛选
|
||||
3. THE Tenant_Admin_Web SHALL 对已隐藏的线索以灰色或删除线样式区分展示
|
||||
|
||||
### 需求 11:维客线索 — 编辑
|
||||
|
||||
**用户故事:** 作为 Tenant_Admin,我希望修改维客线索的标签、摘要和详情,以便纠正错误或补充信息。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Tenant_Admin 编辑某条线索, THE Backend_API SHALL 接受修改后的 category(枚举 6 值:客户基础/消费习惯/玩法偏好/促销偏好/社交关系/重要反馈)、summary(非空,最长 200 字符)、detail(可选)
|
||||
2. THE Backend_API SHALL 验证 category 值在枚举范围内,summary 非空且不超过长度限制
|
||||
3. IF 线索 ID 不存在或不在管辖门店范围内, THEN THE Backend_API SHALL 返回 404 状态码
|
||||
|
||||
### 需求 12:维客线索 — 删除
|
||||
|
||||
**用户故事:** 作为 Tenant_Admin,我希望删除错误或无效的维客线索,以保持数据整洁。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Tenant_Admin 请求删除某条线索, THE Tenant_Admin_Web SHALL 弹出二次确认对话框,明确提示"删除后不可恢复"
|
||||
2. WHEN Tenant_Admin 确认删除, THE Backend_API SHALL 对该线索执行物理删除(`DELETE FROM public.member_retention_clue WHERE id = ?`)
|
||||
3. IF 线索 ID 不存在或不在管辖门店范围内, THEN THE Backend_API SHALL 返回 404 状态码
|
||||
|
||||
### 需求 13:维客线索 — 隐藏与显示
|
||||
|
||||
**用户故事:** 作为 Tenant_Admin,我希望隐藏某些线索使其不在小程序端展示,同时保留在管理后台可见,以便灵活控制线索的可见性。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Tenant_Admin 切换某条线索的隐藏状态, THE Backend_API SHALL 更新 `public.member_retention_clue.is_hidden` 字段(true=隐藏,false=显示)
|
||||
2. WHILE 线索的 `is_hidden = true`, THE Backend_API SHALL 确保小程序端查询线索时通过 `WHERE is_hidden = false` 条件过滤该线索
|
||||
3. THE Tenant_Admin_Web SHALL 允许 Tenant_Admin 将已隐藏的线索恢复为显示状态(`is_hidden = false`)
|
||||
4. IF 线索 ID 不存在或不在管辖门店范围内, THEN THE Backend_API SHALL 返回 404 状态码
|
||||
|
||||
### 需求 14:管理后台 — 租户管理员账号管理
|
||||
|
||||
**用户故事:** 作为 Operator,我希望在系统管理后台(admin-web)中创建、编辑和管理租户管理员账号,以便控制谁可以登录租户管理后台。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Operator 打开租户管理员管理页面, THE admin-web SHALL 展示所有租户管理员列表,包含用户名、显示名称、管辖门店、账号状态(启用/禁用)、创建时间、最后登录时间,支持分页和关键词搜索
|
||||
2. WHEN Operator 创建新的租户管理员, THE Backend_API SHALL 接受用户名(唯一)、初始密码、显示名称、tenant_id、managed_site_ids(门店列表),将密码 bcrypt 哈希后写入 `auth.tenant_admins`,记录 `created_by` 为当前 Operator 的 user_id
|
||||
3. IF 用户名已存在, THEN THE Backend_API SHALL 返回 409 状态码并提示"用户名已存在"
|
||||
4. WHEN Operator 编辑租户管理员信息, THE Backend_API SHALL 允许修改以下字段:显示名称、managed_site_ids、is_active(启用/禁用)
|
||||
5. WHEN Operator 重置租户管理员密码, THE Backend_API SHALL 接受新密码,bcrypt 哈希后更新 `auth.tenant_admins.password_hash`
|
||||
6. IF Operator 禁用某租户管理员(is_active=false), THEN 该管理员后续登录租户管理后台时 SHALL 被拒绝(返回 403)
|
||||
7. THE Backend_API SHALL 要求 Operator 具有 site_admin 或 tenant_admin 角色才能访问租户管理员管理端点
|
||||
|
||||
### 需求 15:数据库变更 — 新建表
|
||||
|
||||
**用户故事:** 作为开发者,我需要创建 NS4 所需的新数据库表,以支撑租户管理后台的全部功能。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 迁移脚本 SHALL 在 `auth` Schema 中创建 `tenant_admins` 表,包含字段:id(BIGSERIAL PK)、username(VARCHAR(50) UNIQUE NOT NULL)、password_hash(VARCHAR(255) NOT NULL)、display_name(VARCHAR(100))、tenant_id(BIGINT NOT NULL)、managed_site_ids(BIGINT[] NOT NULL)、is_active(BOOLEAN DEFAULT true)、created_by(BIGINT)、created_at(TIMESTAMPTZ DEFAULT NOW())、last_login_at(TIMESTAMPTZ),并创建 tenant_id 索引
|
||||
2. THE 迁移脚本 SHALL 在 `biz` Schema 中创建 `salary_adjustments` 表,包含字段:id(BIGSERIAL PK)、site_id(BIGINT NOT NULL)、assistant_id(BIGINT 可空)、assistant_name(VARCHAR(100) NOT NULL)、assistant_number(VARCHAR(50) NOT NULL)、salary_month(VARCHAR(7) NOT NULL)、adjustment_type(CHECK IN deduction/bonus)、amount(NUMERIC(12,2) > 0)、reason(VARCHAR(200) NOT NULL)、upload_batch_id(FK → excel_upload_log)、created_at、created_by,并创建 (site_id, salary_month) 和 (assistant_id, salary_month) 索引
|
||||
3. THE 迁移脚本 SHALL 在 `biz` Schema 中创建 `excel_upload_log` 表,包含字段:id(BIGSERIAL PK)、site_id(BIGINT NOT NULL)、upload_type(CHECK IN expense/platform_income/salary_adj/recharge_commission)、file_name(VARCHAR(255) NOT NULL)、uploaded_by(BIGINT NOT NULL)、row_count(INTEGER DEFAULT 0)、conflict_count(INTEGER DEFAULT 0)、resolved_count(INTEGER DEFAULT 0)、status(CHECK IN pending/confirmed/failed)、error_detail(JSONB)、created_at、confirmed_at,并创建 (site_id, created_at DESC) 索引
|
||||
4. THE 迁移脚本 SHALL 在 `biz` Schema 中创建 3 张 staging 表:`stg_finance_expense`、`stg_platform_income`、`stg_recharge_commission`,各表包含 site_id、业务字段、upload_batch_id(FK)、synced_at(TIMESTAMPTZ 可空,NULL 表示未同步)、created_at
|
||||
|
||||
### 需求 16:数据库变更 — 表结构修改
|
||||
|
||||
**用户故事:** 作为开发者,我需要为现有表添加 NS4 所需的字段,以支持维客线索隐藏功能。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 迁移脚本 SHALL 为 `public.member_retention_clue` 表添加 `is_hidden` 列(BOOLEAN NOT NULL DEFAULT false),并添加列注释说明"是否隐藏(true=管理后台保留但小程序不展示)"
|
||||
2. THE 迁移脚本 SHALL 确保已有数据的 `is_hidden` 值为 false(通过 DEFAULT 约束保证)
|
||||
3. THE 小程序端现有的线索查询 API SHALL 在查询条件中增加 `WHERE is_hidden = false`,确保隐藏线索不在小程序端展示
|
||||
|
||||
### 需求 17:前端应用骨架
|
||||
|
||||
**用户故事:** 作为开发者,我需要搭建租户管理后台的前端项目骨架,以便后续功能页面的开发。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 前端项目 SHALL 在 `apps/tenant-admin/` 目录下创建,使用 React + Vite + Ant Design 技术栈,包含 `package.json`、`vite.config.ts`、`tsconfig.json` 等配置文件
|
||||
2. THE Tenant_Admin_Web SHALL 提供侧边栏导航,包含四个功能模块入口:用户审核、用户管理、Excel 上传、维客线索管理
|
||||
3. THE Tenant_Admin_Web SHALL 实现登录页面,未认证时自动重定向到登录页
|
||||
4. THE Tenant_Admin_Web SHALL 封装统一的 API 调用层(`services/api.ts`),处理 JWT 令牌的自动附加、刷新和过期重定向
|
||||
5. THE Tenant_Admin_Web SHALL 使用与 Backend_API 一致的响应格式(`{ code: 0, data: ... }`),Pydantic 使用 `alias_generator=to_camel` 实现驼峰命名转换
|
||||
|
||||
### 需求 18:后端路由模块
|
||||
|
||||
**用户故事:** 作为开发者,我需要在 FastAPI 后端中创建租户管理专用的路由模块,以提供 NS4 所需的全部 API 端点。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Backend_API SHALL 在 `apps/backend/app/routers/` 下创建 4 个路由文件:`tenant_auth.py`(登录/JWT 签发/鉴权)、`tenant_users.py`(用户审核+用户管理)、`tenant_excel.py`(Excel 上传/校验/冲突处理)、`tenant_clues.py`(维客线索管理)
|
||||
2. THE Backend_API SHALL 将所有租户管理端点注册在 `/api/tenant/` 路径前缀下,与小程序端点(`/api/xcx/`)和管理后台端点(`/api/admin/`)隔离
|
||||
3. THE Backend_API SHALL 为所有 `/api/tenant/*` 端点(登录接口除外)添加 `require_tenant_admin()` 认证依赖,验证 JWT 的 `aud` 字段为 `tenant-admin`
|
||||
4. THE Backend_API SHALL 遵循现有的 API 响应格式约定:成功返回 `{ code: 0, data: ... }`,失败返回 `{ code: number, message: string }`,分页返回 `{ items: T[], total: number, page: number, pageSize: number }`
|
||||
409
.kiro/specs/tenant-admin-web/tasks.md
Normal file
409
.kiro/specs/tenant-admin-web/tasks.md
Normal file
@@ -0,0 +1,409 @@
|
||||
# 实施计划:租户管理后台(tenant-admin-web)
|
||||
|
||||
## 概述
|
||||
|
||||
按照设计文档,将实施拆分为:DDL 迁移 → 后端认证模块 → 后端路由模块 → 前端项目骨架 → 前端页面实现 → 联调收尾。每个任务增量构建,确保无孤立代码。属性测试(Hypothesis / fast-check)和单元测试作为可选子任务紧跟实现步骤。
|
||||
|
||||
后端使用 Python(FastAPI + Pydantic),前端使用 TypeScript(React + Vite + Ant Design)。
|
||||
|
||||
## 任务
|
||||
|
||||
- [ ] 1. DDL 迁移:新建表 + 表结构变更
|
||||
- [ ] 1.1 创建 DDL 迁移脚本 `db/zqyy_app/migrations/2026-03-xx__ns4_tenant_admin_tables.sql`
|
||||
- 在 `auth` Schema 创建 `tenant_admins` 表(id, username, password_hash, display_name, tenant_id, managed_site_ids, is_active, created_by, created_at, last_login_at)
|
||||
- 创建索引 `idx_tenant_admin_tenant ON auth.tenant_admins(tenant_id)`
|
||||
- 在 `biz` Schema 创建 `excel_upload_log` 表(id, site_id, upload_type, file_name, uploaded_by, row_count, conflict_count, resolved_count, status, error_detail, created_at, confirmed_at)
|
||||
- 创建索引 `idx_excel_log_site ON biz.excel_upload_log(site_id, created_at DESC)`
|
||||
- 在 `biz` Schema 创建 `salary_adjustments` 表(id, site_id, assistant_id, assistant_name, assistant_number, salary_month, adjustment_type, amount, reason, upload_batch_id FK, created_at, created_by)
|
||||
- 创建索引 `(site_id, salary_month)` 和 `(assistant_id, salary_month)`
|
||||
- 在 `biz` Schema 创建 3 张 staging 表:`stg_finance_expense`、`stg_platform_income`、`stg_recharge_commission`,各含 site_id、业务字段、upload_batch_id FK、synced_at、created_at
|
||||
- _需求: 14.1, 14.2, 14.3, 14.4_
|
||||
|
||||
- [ ] 1.2 创建 DDL 迁移脚本 `db/zqyy_app/migrations/2026-03-xx__ns4_member_clue_is_hidden.sql`
|
||||
- ALTER TABLE `public.member_retention_clue` ADD COLUMN `is_hidden` BOOLEAN NOT NULL DEFAULT false
|
||||
- 添加 COMMENT ON COLUMN 注释:"是否隐藏(true=管理后台保留但小程序不展示)"
|
||||
- _需求: 15.1, 15.2_
|
||||
|
||||
- [ ] 1.3 更新小程序端线索查询增加 `WHERE is_hidden = false` 条件
|
||||
- 搜索现有线索查询 SQL,在 WHERE 子句中追加 `AND is_hidden = false`
|
||||
- _需求: 15.3_
|
||||
|
||||
- [ ] 2. 后端认证模块(auth.tenant_admins + JWT + 依赖注入)
|
||||
- [ ] 2.1 创建 `apps/backend/app/auth/tenant_admins.py`
|
||||
- 定义 `CurrentTenantAdmin` dataclass(admin_id, tenant_id, managed_site_ids, display_name)
|
||||
- 实现 `require_tenant_admin()` 依赖注入:验证 JWT `aud=tenant-admin`,提取管理员信息
|
||||
- 实现 `site_filter_clause()` 工具函数:生成 `site_id IN (...)` SQL 片段
|
||||
- 实现 `verify_site_access()` 工具函数:校验 site_id 是否在 managed_site_ids 内,否则抛 403
|
||||
- _需求: 1.4, 1.6, 2.1, 2.3, 17.3_
|
||||
|
||||
- [ ]* 2.2 编写属性测试:JWT 隔离与端点保护
|
||||
- **Property 2: JWT 隔离与端点保护**
|
||||
- 使用 Hypothesis 生成随机 JWT payload(不同 aud 值:tenant-admin / xcx / 无效值)
|
||||
- 验证:仅 `aud=tenant-admin` 时 `require_tenant_admin()` 返回成功,其余返回 401
|
||||
- **验证: 需求 1.4, 1.6, 17.3**
|
||||
|
||||
- [ ]* 2.3 编写属性测试:数据隔离
|
||||
- **Property 3: 数据隔离**
|
||||
- 使用 Hypothesis 生成随机 managed_site_ids 集合和随机 site_id
|
||||
- 验证:`verify_site_access()` 仅当 site_id ∈ managed_site_ids 时通过,否则抛 403
|
||||
- **验证: 需求 2.1, 2.2, 2.3**
|
||||
|
||||
- [ ] 3. 后端路由:tenant_auth.py(登录/刷新)
|
||||
- [ ] 3.1 创建 `apps/backend/app/routers/tenant_auth.py`
|
||||
- 实现 `POST /api/tenant/auth/login`:接受 username + password,查询 `auth.tenant_admins`,bcrypt 验证,签发 JWT(aud=tenant-admin, sub=admin_id, tenant_id, managed_site_ids),返回 access_token + refresh_token
|
||||
- 实现 `POST /api/tenant/auth/refresh`:验证 refresh_token,签发新令牌对
|
||||
- 登录成功时更新 `last_login_at`
|
||||
- 账号禁用(is_active=false)返回 403
|
||||
- 凭据无效返回 401,错误消息不区分用户名/密码
|
||||
- _需求: 1.1, 1.2, 1.3, 1.5_
|
||||
|
||||
- [ ] 3.2 在 `apps/backend/app/main.py` 中注册 tenant_auth router,路径前缀 `/api/tenant/auth`
|
||||
- _需求: 17.2_
|
||||
|
||||
- [ ]* 3.3 编写属性测试:认证正确性
|
||||
- **Property 1: 认证正确性**
|
||||
- 使用 Hypothesis 生成随机用户名+密码组合
|
||||
- 验证:有效凭据返回含 `aud=tenant-admin` 的 JWT;无效凭据返回 401 且错误消息统一
|
||||
- **验证: 需求 1.1, 1.2**
|
||||
|
||||
- [ ]* 3.4 编写单元测试:登录边界条件
|
||||
- 测试文件 `apps/backend/tests/unit/test_tenant_auth.py`
|
||||
- 验证:禁用账号登录返回 403、用户名不存在返回 401、密码错误返回 401、JWT 过期处理、refresh_token 刷新
|
||||
- _需求: 1.1, 1.2, 1.3, 1.5_
|
||||
|
||||
- [ ] 4. 检查点 — 认证模块验证
|
||||
- 确保认证模块所有测试通过,ask the user if questions arise.
|
||||
|
||||
- [ ] 5. 后端路由:tenant_users.py(用户审核 + 用户管理)
|
||||
- [ ] 5.1 创建 Pydantic Schema `apps/backend/app/schemas/tenant_users.py`
|
||||
- 继承 `TenantBaseModel`(alias_generator=to_camel),定义:
|
||||
- `ApplicationListItem`(昵称、手机号、球房编号、申请角色、员工编号、申请时间、状态)
|
||||
- `MatchSuggestion`(assistant_id/staff_id、姓名、编号、来源表)
|
||||
- `ApproveRequest`(role、assistant_id 可选、staff_id 可选)
|
||||
- `RejectRequest`(reason 必填)
|
||||
- `UserListItem`(姓名、角色、关联助教姓名、所属门店、账号状态)
|
||||
- `UserEditRequest`(role 可选、site_id 可选、status 可选)
|
||||
- `UserBindingRequest`(assistant_id 可选、staff_id 可选)
|
||||
- _需求: 3.2, 4.1_
|
||||
|
||||
- [ ] 5.2 创建 `apps/backend/app/routers/tenant_users.py`
|
||||
- `GET /api/tenant/applications`:申请列表,status 筛选 + 分页,附加 site_id IN 条件
|
||||
- `GET /api/tenant/applications/{id}/match-suggestions`:通过 site_code_mapping 查 site_id,并行匹配 v_dim_assistant(phone, scd2_is_current=1)和 v_dim_staff + v_dim_staff_ex(phone)
|
||||
- `POST /api/tenant/applications/{id}/approve`:事务内执行:更新 users.status='approved' → 写入 user_site_roles → 写入 user_assistant_binding → 更新 user_applications.status='approved' + 审核人 + 审核时间
|
||||
- `POST /api/tenant/applications/{id}/reject`:更新 user_applications.status='rejected' + review_note + reviewed_at
|
||||
- 非 pending 状态审核返回 409
|
||||
- `GET /api/tenant/users`:已通过审核用户列表,角色筛选 + 关键词搜索 + 分页
|
||||
- `PATCH /api/tenant/users/{id}`:编辑角色/门店/状态,site_id 超出管辖范围返回 403
|
||||
- `PUT /api/tenant/users/{id}/binding`:更新 user_assistant_binding
|
||||
- 禁用用户时设 users.status='disabled'
|
||||
- _需求: 3.1-3.6, 4.1-4.5_
|
||||
|
||||
- [ ] 5.3 在 `apps/backend/app/main.py` 中注册 tenant_users router,路径前缀 `/api/tenant`
|
||||
- _需求: 17.2_
|
||||
|
||||
- [ ]* 5.4 编写属性测试:审核通过多表一致性
|
||||
- **Property 4: 审核通过多表一致性**
|
||||
- 使用 Hypothesis 生成随机 pending 申请 + 角色/绑定参数
|
||||
- 验证:审核通过后 users.status='approved'、user_site_roles 存在记录、user_assistant_binding 存在记录(如提供 assistant_id)、user_applications.status='approved' 且审核人和时间已填写
|
||||
- **验证: 需求 3.4**
|
||||
|
||||
- [ ]* 5.5 编写属性测试:审核拒绝状态更新
|
||||
- **Property 5: 审核拒绝状态更新**
|
||||
- 使用 Hypothesis 生成随机 pending 申请 + 随机拒绝原因字符串
|
||||
- 验证:拒绝后 user_applications.status='rejected'、review_note 等于提交的原因、reviewed_at 非空
|
||||
- **验证: 需求 3.5**
|
||||
|
||||
- [ ]* 5.6 编写属性测试:申请关联匹配
|
||||
- **Property 6: 申请关联匹配**
|
||||
- 使用 Hypothesis 生成随机手机号 + 随机助教/员工数据集
|
||||
- 验证:匹配结果仅包含 phone 匹配的记录,优先 v_dim_assistant 再 v_dim_staff
|
||||
- **验证: 需求 3.3**
|
||||
|
||||
- [ ]* 5.7 编写属性测试:用户管理编辑
|
||||
- **Property 7: 用户管理编辑**
|
||||
- 使用 Hypothesis 生成随机编辑参数(角色、site_id 在管辖范围内、状态)
|
||||
- 验证:编辑后用户记录反映新值;site_id 超出管辖范围时返回 403
|
||||
- **验证: 需求 4.2, 4.3, 4.4**
|
||||
|
||||
- [ ]* 5.8 编写单元测试:用户审核与管理边界条件
|
||||
- 测试文件 `apps/backend/tests/unit/test_tenant_users.py`
|
||||
- 验证:非 pending 状态审核返回 409、越权修改 site_id 返回 403、禁用用户后 status='disabled'
|
||||
- _需求: 3.6, 4.4, 4.5_
|
||||
|
||||
- [ ] 6. 后端路由:tenant_excel.py(Excel 上传/校验/冲突/写入)
|
||||
- [ ] 6.1 创建 Pydantic Schema `apps/backend/app/schemas/tenant_excel.py`
|
||||
- 定义 4 种模板的行数据模型:`ExpenseRow`、`PlatformIncomeRow`、`SalaryAdjRow`、`RechargeCommissionRow`
|
||||
- 定义校验结果模型:`ValidationResult`(errors[], warnings[], passed_rows[], upload_id)
|
||||
- 定义冲突模型:`ConflictDiff`(row_index, field_diffs[{field, old_value, new_value}])
|
||||
- 定义确认请求:`ConfirmRequest`(upload_id, resolutions[{row_index, action: replace/keep}])
|
||||
- 定义上传记录模型:`UploadLogItem`(模板类型、文件名、上传人、时间、行数、冲突数、状态)
|
||||
- _需求: 5.2, 7.2, 8.4_
|
||||
|
||||
- [ ] 6.2 创建 `apps/backend/app/routers/tenant_excel.py`
|
||||
- `POST /api/tenant/excel/upload`:multipart/form-data 接收文件 + upload_type 参数
|
||||
- 校验文件格式(.xlsx/.xls),非法返回 400
|
||||
- 按模板类型执行格式校验(月份格式、枚举值、金额精度、非空等)
|
||||
- salary_adj / recharge_commission 模板执行人员匹配校验(v_dim_assistant → v_dim_staff 降级匹配)
|
||||
- 格式校验通过后执行冲突检测(按模板主键规则匹配已有数据)
|
||||
- 创建 excel_upload_log 记录(status=pending),返回 upload_id + 校验结果 + 冲突 diff
|
||||
- `POST /api/tenant/excel/confirm`:接受 upload_id + resolutions[]
|
||||
- 单事务写入目标表(salary_adj → biz.salary_adjustments,其余 → staging 表)
|
||||
- 替换行执行 UPDATE,新增行执行 INSERT
|
||||
- 写入失败回滚整批,log status=failed,记录 error_detail
|
||||
- 写入成功更新 log status=confirmed + 实际行数 + 冲突解决数 + confirmed_at
|
||||
- `GET /api/tenant/excel/logs`:上传记录列表,分页,附加 site_id IN 条件
|
||||
- `GET /api/tenant/excel/template/{type}`:返回空白 Excel 模板文件(含表头和格式说明)
|
||||
- _需求: 5.1-5.5, 6.1-6.5, 7.1-7.5, 8.1-8.5_
|
||||
|
||||
- [ ] 6.3 在 `apps/backend/app/main.py` 中注册 tenant_excel router,路径前缀 `/api/tenant/excel`
|
||||
- _需求: 17.2_
|
||||
|
||||
- [ ]* 6.4 编写属性测试:Excel 格式校验
|
||||
- **Property 8: Excel 格式校验**
|
||||
- 使用 Hypothesis 生成随机模板类型 + 随机数据行(含有效/无效混合)
|
||||
- 验证:符合模板定义的行标记通过,不符合的行返回行号+列名+错误描述;通过行数 + 警告行数 + 错误行数 = 总行数
|
||||
- **验证: 需求 5.1, 5.2, 5.3, 5.4, 6.5**
|
||||
|
||||
- [ ]* 6.5 编写属性测试:人员匹配校验
|
||||
- **Property 9: 人员匹配校验**
|
||||
- 使用 Hypothesis 生成随机助教姓名+编号 + 随机助教/员工数据集
|
||||
- 验证:优先 v_dim_assistant 匹配,未匹配再查 v_dim_staff;匹配成功填充 assistant_id,失败标记 warning 不阻断
|
||||
- **验证: 需求 6.1, 6.2, 6.3, 6.4**
|
||||
|
||||
- [ ]* 6.6 编写属性测试:冲突检测
|
||||
- **Property 10: 冲突检测**
|
||||
- 使用 Hypothesis 生成随机上传数据 + 随机已有数据
|
||||
- 验证:按模板主键规则识别冲突行,冲突行返回逐字段 diff,非冲突行标记待写入
|
||||
- **验证: 需求 7.1, 7.2**
|
||||
|
||||
- [ ]* 6.7 编写属性测试:数据写入 round-trip
|
||||
- **Property 11: 数据写入 round-trip**
|
||||
- 使用 Hypothesis 生成随机数据 + 随机冲突解决方案(replace/keep)
|
||||
- 验证:写入后从目标表读取的数据与提交一致(replace 用新值,keep 保持旧值);log status=confirmed,行数正确
|
||||
- **验证: 需求 7.4, 7.5, 8.1, 8.2**
|
||||
|
||||
- [ ]* 6.8 编写单元测试:Excel 上传边界条件
|
||||
- 测试文件 `apps/backend/tests/unit/test_tenant_excel.py`
|
||||
- 验证:无效文件格式返回 400、写入失败回滚(log status=failed)、模板下载返回正确文件
|
||||
- _需求: 5.5, 8.3, 8.5_
|
||||
|
||||
- [ ] 7. 后端路由:tenant_clues.py(维客线索管理)
|
||||
- [ ] 7.1 创建 Pydantic Schema `apps/backend/app/schemas/tenant_clues.py`
|
||||
- 定义 `CustomerSearchItem`(member_id、姓名、手机号脱敏、所属门店)
|
||||
- 定义 `ClueListItem`(id、category、summary、detail、recorded_by_name、source、recorded_at、is_hidden)
|
||||
- 定义 `ClueEditRequest`(category 枚举 6 值、summary 非空 ≤200 字符、detail 可选)
|
||||
- 定义 `ClueVisibilityRequest`(is_hidden: bool)
|
||||
- _需求: 9.1, 10.1, 11.1_
|
||||
|
||||
- [ ] 7.2 创建 `apps/backend/app/routers/tenant_clues.py`
|
||||
- `GET /api/tenant/customers/search`:keyword + site_id 参数,在管辖门店范围内搜索 v_dim_member(nickname 模糊匹配 OR mobile 精确匹配,scd2_is_current=1),手机号脱敏返回
|
||||
- `GET /api/tenant/customers/{member_id}/clues`:返回该客户在管辖门店范围内的全部线索,支持 source 和 is_hidden 筛选
|
||||
- `PATCH /api/tenant/clues/{id}`:编辑线索 category/summary/detail,校验 category 枚举和 summary 长度
|
||||
- `DELETE /api/tenant/clues/{id}`:物理删除(DELETE FROM),线索不存在或不在管辖范围返回 404
|
||||
- `PATCH /api/tenant/clues/{id}/visibility`:切换 is_hidden 状态
|
||||
- 所有线索操作先校验线索 site_id 是否在管辖范围内,不在则返回 404
|
||||
- _需求: 9.1-9.4, 10.1, 11.1-11.3, 12.2-12.3, 13.1-13.4_
|
||||
|
||||
- [ ] 7.3 在 `apps/backend/app/main.py` 中注册 tenant_clues router,路径前缀 `/api/tenant`
|
||||
- _需求: 17.2_
|
||||
|
||||
- [ ]* 7.4 编写属性测试:客户搜索
|
||||
- **Property 12: 客户搜索**
|
||||
- 使用 Hypothesis 生成随机关键词 + 随机客户数据集
|
||||
- 验证:结果中 nickname 包含关键词(模糊)或 mobile 等于关键词(精确),且 site_id 在管辖范围内
|
||||
- **验证: 需求 9.1, 9.3**
|
||||
|
||||
- [ ]* 7.5 编写属性测试:线索编辑校验
|
||||
- **Property 13: 线索编辑校验**
|
||||
- 使用 Hypothesis 生成随机 category(含有效/无效值)+ 随机 summary(含空/超长)
|
||||
- 验证:有效参数编辑成功且记录反映新值;无效 category 或空/超长 summary 被拒绝
|
||||
- **验证: 需求 11.1, 11.2**
|
||||
|
||||
- [ ]* 7.6 编写属性测试:线索删除
|
||||
- **Property 14: 线索删除**
|
||||
- 使用 Hypothesis 生成随机线索 ID
|
||||
- 验证:删除后该线索不可通过任何查询获取(物理删除)
|
||||
- **验证: 需求 12.2**
|
||||
|
||||
- [ ]* 7.7 编写属性测试:线索隐藏/显示 round-trip
|
||||
- **Property 15: 线索隐藏/显示 round-trip**
|
||||
- 使用 Hypothesis 生成随机线索
|
||||
- 验证:设 is_hidden=true 后小程序端查询(WHERE is_hidden=false)不返回该线索,管理后台查询返回;设回 false 后小程序端恢复返回
|
||||
- **验证: 需求 13.1, 13.2, 13.3, 15.3**
|
||||
|
||||
- [ ]* 7.8 编写单元测试:维客线索边界条件
|
||||
- 测试文件 `apps/backend/tests/unit/test_tenant_clues.py`
|
||||
- 验证:线索不存在返回 404、越权访问返回 404、无效 category 返回 422、空 summary 返回 422
|
||||
- _需求: 11.2, 11.3, 12.3, 13.4_
|
||||
|
||||
- [ ] 8. 检查点 — 后端路由模块验证
|
||||
- 确保所有后端路由模块测试通过,4 个路由文件均已注册到 main.py,ask the user if questions arise.
|
||||
|
||||
- [ ] 9. 前端项目骨架(apps/tenant-admin/)
|
||||
- [ ] 9.1 创建 `apps/tenant-admin/` 项目结构
|
||||
- 初始化 `package.json`(React + Vite + Ant Design + axios 依赖)
|
||||
- 创建 `vite.config.ts`(proxy 配置 `/api/tenant` → 后端地址)
|
||||
- 创建 `tsconfig.json`
|
||||
- 创建 `index.html` 入口
|
||||
- _需求: 16.1_
|
||||
|
||||
- [ ] 9.2 创建 `apps/tenant-admin/src/services/api.ts` — API 调用封装
|
||||
- axios 实例,baseURL: `/api/tenant`
|
||||
- 请求拦截器:从 localStorage 读取 access_token 附加 Authorization header
|
||||
- 响应拦截器:401 时用 refresh_token 刷新,刷新失败重定向 `/login`
|
||||
- 并发刷新保护:多个 401 只触发一次 refresh,其余排队
|
||||
- 响应解包:`{ code: 0, data }` → 提取 `data`
|
||||
- _需求: 16.4, 16.5_
|
||||
|
||||
- [ ] 9.3 创建 `apps/tenant-admin/src/hooks/useAuth.ts` — 认证状态管理
|
||||
- 登录/登出方法、token 存储、用户信息(display_name, managed_site_ids)
|
||||
- _需求: 1.5, 16.3_
|
||||
|
||||
- [ ] 9.4 创建 `apps/tenant-admin/src/App.tsx` — 路由配置 + 侧边栏布局
|
||||
- react-router-dom 路由配置
|
||||
- Ant Design Layout + Sider 侧边栏导航(用户审核、用户管理、Excel 上传、维客线索管理)
|
||||
- 未认证时重定向到 `/login`
|
||||
- _需求: 16.2, 16.3_
|
||||
|
||||
- [ ] 9.5 创建 `apps/tenant-admin/src/components/SiteSelector/` — 门店筛选器组件
|
||||
- 页面顶部门店选择器,数据源为当前管理员的 managed_site_ids
|
||||
- 支持多选/全选
|
||||
- _需求: 2.4_
|
||||
|
||||
- [ ]* 9.6 编写属性测试:响应格式一致性(前端 fast-check)
|
||||
- **Property 16: 响应格式一致性**
|
||||
- 使用 fast-check 生成随机 API 响应数据
|
||||
- 验证:成功响应符合 `{ code: 0, data }` 格式;错误响应符合 `{ code: number, message: string }` 格式;分页响应 data 包含 items/total/page/pageSize
|
||||
- **验证: 需求 17.4**
|
||||
|
||||
- [ ] 10. 前端页面:登录页
|
||||
- [ ] 10.1 创建 `apps/tenant-admin/src/pages/Login/index.tsx`
|
||||
- 用户名 + 密码表单(Ant Design Form + Input + Button)
|
||||
- 调用 `POST /api/tenant/auth/login`,成功后存储 token 并跳转首页
|
||||
- 错误提示:401 显示"用户名或密码错误"、403 显示"账号已被禁用"
|
||||
- _需求: 1.1, 1.2, 1.3, 16.3_
|
||||
|
||||
- [ ] 11. 前端页面:用户审核
|
||||
- [ ] 11.1 创建 `apps/tenant-admin/src/pages/UserApproval/index.tsx`
|
||||
- 申请列表表格(Ant Design Table),支持状态筛选(全部/待审核/已通过/已拒绝)+ 分页
|
||||
- 展示字段:昵称、手机号、球房编号、申请角色、员工编号、申请时间、状态
|
||||
- 待审核行提供"审核"操作按钮
|
||||
- _需求: 3.1, 3.2_
|
||||
|
||||
- [ ] 11.2 实现审核操作交互
|
||||
- 点击"审核"打开 Modal,展示关联建议列表(调用 match-suggestions API)
|
||||
- 通过操作:选择角色 + 关联助教/员工 → 调用 approve API
|
||||
- 拒绝操作:填写拒绝原因(必填)→ 调用 reject API
|
||||
- 操作成功后刷新列表
|
||||
- _需求: 3.3, 3.4, 3.5_
|
||||
|
||||
- [ ] 12. 前端页面:用户管理
|
||||
- [ ] 12.1 创建 `apps/tenant-admin/src/pages/UserManagement/index.tsx`
|
||||
- 用户列表表格,支持角色筛选 + 关键词搜索 + 分页
|
||||
- 展示字段:姓名、角色、关联助教姓名、所属门店、账号状态
|
||||
- 每行提供"编辑"和"绑定"操作按钮
|
||||
- _需求: 4.1_
|
||||
|
||||
- [ ] 12.2 实现编辑与绑定交互
|
||||
- 编辑 Modal:修改角色(Select)、所属门店(Select,限管辖范围)、账号状态(Switch)
|
||||
- 绑定 Modal:修改关联助教 ID / 员工 ID
|
||||
- 禁用用户时二次确认
|
||||
- _需求: 4.2, 4.3, 4.4_
|
||||
|
||||
- [ ] 13. 前端页面:Excel 上传
|
||||
- [ ] 13.1 创建 `apps/tenant-admin/src/pages/ExcelUpload/index.tsx`
|
||||
- 模板类型选择(Radio/Select:财务支出/团购收入/助教奖罚/充值业绩归属)
|
||||
- 模板下载按钮(调用 template/{type} API)
|
||||
- 文件上传组件(Ant Design Upload,限 .xlsx/.xls)
|
||||
- 上传后展示校验结果:错误行标红、警告行黄色高亮
|
||||
- 汇总展示:通过行数、警告行数、错误行数
|
||||
- _需求: 5.1, 5.3, 6.4, 6.5, 8.5_
|
||||
|
||||
- [ ] 13.2 创建 `apps/tenant-admin/src/components/DiffTable/index.tsx` — 冲突 diff 交互表格
|
||||
- 每行显示:字段名、旧值、新值、操作选项(替换/保留 Radio)
|
||||
- 支持"全部替换"和"全部保留"快捷操作按钮
|
||||
- 确认按钮提交 resolutions 数组
|
||||
- _需求: 7.3, 7.4_
|
||||
|
||||
- [ ] 13.3 实现确认写入与上传记录
|
||||
- 无冲突时直接展示"待写入"确认按钮
|
||||
- 有冲突时展示 DiffTable,用户选择后确认
|
||||
- 调用 confirm API 写入,成功/失败提示
|
||||
- 上传记录 Tab:展示历史上传记录列表(模板类型、文件名、上传人、时间、行数、冲突数、状态),分页
|
||||
- _需求: 7.5, 8.1, 8.2, 8.4_
|
||||
|
||||
- [ ] 14. 前端页面:维客线索管理
|
||||
- [ ] 14.1 创建 `apps/tenant-admin/src/pages/RetentionClues/index.tsx`
|
||||
- 客户搜索栏(Input.Search),支持姓名模糊/手机号精确搜索
|
||||
- 门店筛选器(复用 SiteSelector 组件)
|
||||
- 搜索结果列表:member_id、姓名、手机号脱敏、所属门店
|
||||
- 搜索结果为空时展示"未找到匹配客户"提示
|
||||
- _需求: 9.1, 9.3, 9.4_
|
||||
|
||||
- [ ] 14.2 实现线索列表与操作
|
||||
- 选择客户后展示该客户全部线索,按大类标签分组
|
||||
- 支持按来源和隐藏状态筛选
|
||||
- 已隐藏线索以灰色/删除线样式区分
|
||||
- 每条线索提供:编辑、删除、隐藏/显示操作按钮
|
||||
- _需求: 10.1, 10.2, 10.3_
|
||||
|
||||
- [ ] 14.3 创建 `apps/tenant-admin/src/components/ClueEditor/index.tsx` — 线索编辑表单
|
||||
- category Select(6 值枚举:客户基础/消费习惯/玩法偏好/促销偏好/社交关系/重要反馈)
|
||||
- summary Input(必填,最长 200 字符)
|
||||
- detail TextArea(可选)
|
||||
- _需求: 11.1_
|
||||
|
||||
- [ ] 14.4 实现删除与隐藏/显示交互
|
||||
- 删除:二次确认对话框(Modal.confirm),明确提示"删除后不可恢复"
|
||||
- 隐藏/显示:Switch 切换,即时调用 visibility API
|
||||
- _需求: 12.1, 12.2, 13.1, 13.3_
|
||||
|
||||
- [ ] 15. 检查点 — 前端页面验证
|
||||
- 确保所有前端页面组件渲染正常,API 调用层工作正确,ask the user if questions arise.
|
||||
|
||||
- [ ] 16. 前后端联调与集成
|
||||
- [ ] 16.1 前端所有页面对接后端真实 API
|
||||
- 登录页 → tenant_auth 登录/刷新
|
||||
- 用户审核页 → tenant_users 申请列表/关联建议/审核
|
||||
- 用户管理页 → tenant_users 用户列表/编辑/绑定
|
||||
- Excel 上传页 → tenant_excel 上传/校验/冲突/确认/记录/模板下载
|
||||
- 维客线索页 → tenant_clues 客户搜索/线索列表/编辑/删除/隐藏
|
||||
- 验证所有页面 API 调用失败时显示友好错误提示,不出现白屏
|
||||
- _需求: 1.1-1.6, 3.1-3.6, 4.1-4.5, 5.1-5.5, 9.1-9.4, 10.1-10.3, 11.1-11.3, 12.1-12.3, 13.1-13.4_
|
||||
|
||||
- [ ] 16.2 DDL 迁移合并到主 DDL 基线
|
||||
- 执行迁移脚本到 `test_zqyy_app`
|
||||
- 合并新表定义到 `docs/database/ddl/` 对应基线文件
|
||||
- _需求: 14.1-14.4, 15.1-15.2_
|
||||
|
||||
- [ ] 17. 文档更新
|
||||
- [ ] 17.1 创建/更新 BD 手册
|
||||
- 新增 `docs/database/BD_Manual_tenant_admin_tables.md`:tenant_admins、excel_upload_log、salary_adjustments、3 张 staging 表的字段明细、约束、索引、验证 SQL
|
||||
- 更新 `docs/database/BD_Manual_retention_clue.md`(如存在):追加 is_hidden 字段说明
|
||||
- _规范: db-docs.md_
|
||||
|
||||
- [ ] 17.2 更新后端 API 参考文档
|
||||
- 在 `apps/backend/docs/API-REFERENCE.md` 新增 4 个 tenant 路由模块文档
|
||||
- 更新 `apps/backend/README.md` 路由模块摘要
|
||||
- _需求: 17.1-17.4_
|
||||
|
||||
- [ ] 17.3 更新文档地图
|
||||
- 在 `docs/DOCUMENTATION-MAP.md` 新增 NS4 模块条目(tenant-admin-web spec、BD 手册、前端项目)
|
||||
- _规范: doc-map.md_
|
||||
|
||||
- [ ] 18. 最终检查点 — 全量验证
|
||||
- 确保所有后端测试通过(单元测试 + 属性测试)
|
||||
- 确保前端所有页面连接真实后端运行正常
|
||||
- 确保 DDL 迁移已合并到主基线,BD 手册已同步更新
|
||||
- 确保 API 文档、后端 README、文档地图均已更新
|
||||
- ask the user if questions arise.
|
||||
|
||||
## 备注
|
||||
|
||||
- 标记 `*` 的子任务为可选,可跳过以加速 MVP 交付
|
||||
- 每个任务引用了具体的需求编号以确保可追溯性
|
||||
- 属性测试覆盖 16 个正确性属性(Property 1-16),单元测试覆盖具体边界条件
|
||||
- 检查点任务确保增量验证,避免问题累积
|
||||
- 后端使用 Python(FastAPI + Pydantic + Hypothesis),前端使用 TypeScript(React + Vite + Ant Design + fast-check)
|
||||
- 会员信息一律通过 `member_id` JOIN `v_dim_member`(`scd2_is_current=1`),不使用结算单冗余字段(DQ-6 规则)
|
||||
- 多店铺 FDW 查询采用逐 site_id 查询后合并结果的策略
|
||||
File diff suppressed because one or more lines are too long
@@ -1,9 +1,66 @@
|
||||
{
|
||||
"audit_required": false,
|
||||
"db_docs_required": false,
|
||||
"reasons": [],
|
||||
"changed_files": [],
|
||||
"change_fingerprint": "",
|
||||
"marked_at": "2026-03-20T03:26:08.198013+08:00",
|
||||
"audit_required": true,
|
||||
"db_docs_required": true,
|
||||
"reasons": [
|
||||
"dir:backend",
|
||||
"dir:miniprogram",
|
||||
"dir:db",
|
||||
"db-schema-change",
|
||||
"root-file"
|
||||
],
|
||||
"changed_files": [
|
||||
"apps/DEMO-miniprogram/.gitignore",
|
||||
"apps/DEMO-miniprogram/.gitkeep",
|
||||
"apps/DEMO-miniprogram/README.md",
|
||||
"apps/DEMO-miniprogram/doc/ABANDON_MODAL_COMPONENT.md",
|
||||
"apps/DEMO-miniprogram/doc/KEYBOARD_INTERACTION_FIX.md",
|
||||
"apps/DEMO-miniprogram/doc/TASK_ABANDON_IMPROVEMENTS.md",
|
||||
"apps/DEMO-miniprogram/doc/TASK_ABANDON_QUICK_REFERENCE.md",
|
||||
"apps/DEMO-miniprogram/doc/progress-bar-animation.md",
|
||||
"apps/DEMO-miniprogram/doc/useless/ABANDON_MODAL_COMPONENT.md",
|
||||
"apps/DEMO-miniprogram/doc/useless/KEYBOARD_INTERACTION_FIX.md",
|
||||
"apps/DEMO-miniprogram/doc/useless/TASK_ABANDON_IMPROVEMENTS.md",
|
||||
"apps/DEMO-miniprogram/doc/useless/TASK_ABANDON_QUICK_REFERENCE.md",
|
||||
"apps/DEMO-miniprogram/doc/useless/progress-bar-animation.md",
|
||||
"apps/DEMO-miniprogram/i18n/base.json",
|
||||
"apps/DEMO-miniprogram/jest.config.js",
|
||||
"apps/DEMO-miniprogram/miniprogram/app.json",
|
||||
"apps/DEMO-miniprogram/miniprogram/app.miniapp.json",
|
||||
"apps/DEMO-miniprogram/miniprogram/app.ts",
|
||||
"apps/DEMO-miniprogram/miniprogram/app.wxss",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/ai-robot-sm.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/ai-robot-title.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/arrow-left.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/chart.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/chat-gray.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/chat.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/check-bold.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/check-circle.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/clock.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/forbidden.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/help-circle.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/icon-ai-float.png",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/icon-ai-inline.png",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/info-circle.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/info-error.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/info-warning.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/logout.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/tab-board-active.png",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/tab-board.png",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/tab-my-active.png",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/tab-my.png",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/tab-task-active.png",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/tab-task.png",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/task.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/_archived/wechat.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/ai-robot-badge.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/ai-robot-inline.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/ai-robot.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/ball-black.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/ball-gray.svg",
|
||||
"apps/DEMO-miniprogram/miniprogram/assets/icons/feature-ai.svg"
|
||||
],
|
||||
"change_fingerprint": "c347e0fb24548a8427f63a65a48dbf7df0b4a734",
|
||||
"marked_at": "2026-03-20T09:01:30.178895+08:00",
|
||||
"last_reminded_at": null
|
||||
}
|
||||
@@ -1,41 +1,13 @@
|
||||
{
|
||||
"needs_check": true,
|
||||
"scanned_at": "2026-03-20T03:26:03.443110+08:00",
|
||||
"new_migration_sql": [
|
||||
"db/zqyy_app/migrations/2026-03-20_rebuild_rls_view_gift_breakdown.sql",
|
||||
"db/zqyy_app/migrations/2026-03-20_refresh_fdw_finance_recharge_summary.sql"
|
||||
],
|
||||
"new_or_modified_sql": [
|
||||
"db/zqyy_app/migrations/2026-03-20_rebuild_rls_view_gift_breakdown.sql",
|
||||
"db/zqyy_app/migrations/2026-03-20_refresh_fdw_finance_recharge_summary.sql",
|
||||
"docs/database/ddl/etl_feiqiu__app.sql",
|
||||
"scripts/ops/verify_gift_card_breakdown.sql"
|
||||
],
|
||||
"code_without_docs": [
|
||||
{
|
||||
"file": "apps/backend/app/services/fdw_queries.py",
|
||||
"expected_docs": [
|
||||
"apps/backend/docs/API-REFERENCE.md",
|
||||
"apps/backend/README.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "apps/etl/connectors/feiqiu/tasks/dws/finance_recharge_task.py",
|
||||
"expected_docs": [
|
||||
"apps/etl/connectors/feiqiu/docs/etl_tasks/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "apps/miniprogram/miniprogram/pages/board-finance/board-finance.ts",
|
||||
"expected_docs": [
|
||||
"apps/miniprogram/README.md"
|
||||
]
|
||||
}
|
||||
],
|
||||
"needs_check": false,
|
||||
"scanned_at": "2026-03-20T08:32:06.937993+08:00",
|
||||
"new_migration_sql": [],
|
||||
"new_or_modified_sql": [],
|
||||
"code_without_docs": [],
|
||||
"new_files": [],
|
||||
"has_bd_manual": false,
|
||||
"has_audit_record": false,
|
||||
"has_ddl_baseline": true,
|
||||
"has_ddl_baseline": false,
|
||||
"api_changed": false,
|
||||
"openapi_spec_stale": false
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"prompt_id": "P20260320-032608",
|
||||
"at": "2026-03-20T03:26:08.198013+08:00"
|
||||
"prompt_id": "P20260320-090130",
|
||||
"at": "2026-03-20T09:01:30.178895+08:00"
|
||||
}
|
||||
@@ -36,7 +36,7 @@ DWS(Data Warehouse Summary)层从 DWD 明细层按业务维度聚合计算
|
||||
| 任务代码 | 目标表 | 粒度 | 核心指标 |
|
||||
|----------|--------|------|----------|
|
||||
| `DWS_FINANCE_DAILY` | `dws_finance_daily_summary` | 日期 | 发生额、优惠合计、确认收入、现金流入/流出/净变动、卡消费、充值统计 |
|
||||
| `DWS_FINANCE_RECHARGE` | `dws_finance_recharge_summary` | 日期 | 充值笔数/总额、首充/续充拆分、去重会员数、全店卡余额快照 |
|
||||
| `DWS_FINANCE_RECHARGE` | `dws_finance_recharge_summary` | 日期 | 充值笔数/总额、首充/续充拆分、去重会员数、全店卡余额快照、赠送卡按卡类型拆分(酒水卡/台费卡/抵用券 × 余额+新增) |
|
||||
| `DWS_FINANCE_INCOME_STRUCTURE` | `dws_finance_income_structure` | 日期+收入类型 | 按收入类型(台费/商品/助教基础课/附加课)和区域分析 |
|
||||
| `DWS_FINANCE_DISCOUNT_DETAIL` | `dws_finance_discount_detail` | 日期+折扣类型 | 折扣类型拆分(GROUPBUY/VIP/ROUNDING/GIFT_CARD_*/BIG_CUSTOMER/OTHER) |
|
||||
|
||||
@@ -241,7 +241,7 @@ DWS(Data Warehouse Summary)层从 DWD 明细层按业务维度聚合计算
|
||||
`dws_assistant_daily_detail`、`dws_assistant_monthly_summary`、`dws_assistant_customer_stats`、`dws_assistant_salary_calc`、`dws_assistant_finance_analysis`、`dws_assistant_order_contribution`、`dws_member_consumption_summary`、`dws_member_visit_detail`、`dws_finance_daily_summary`、`dws_finance_recharge_summary`、`dws_finance_income_structure`、`dws_finance_discount_detail`、`dws_goods_stock_daily_summary`、`dws_goods_stock_weekly_summary`、`dws_goods_stock_monthly_summary`、`dws_order_summary`
|
||||
|
||||
### 指数表
|
||||
`dws_member_winback_index`、`dws_member_newconv_index`、`dws_member_recall_index`、`dws_member_assistant_relation_index`、`dws_member_assistant_intimacy`、`dws_member_spending_power_index`、`dws_index_percentile_history`
|
||||
`dws_member_winback_index`、`dws_member_newconv_index`、`dws_member_assistant_relation_index`、`dws_member_assistant_intimacy`、`dws_member_spending_power_index`、`dws_index_percentile_history`
|
||||
|
||||
### 其他表
|
||||
`dws_platform_settlement`、`dws_ml_manual_order_source`、`dws_ml_manual_order_alloc`、`dws_assistant_recharge_commission`、`dws_assistant_project_tag`、`dws_member_project_tag`
|
||||
|
||||
@@ -38,6 +38,7 @@ NeoZQYY Monorepo — 面向台球门店业务的全栈数据平台。多门店
|
||||
| `ods.assistant_cancellation_records` | ODS 表 | 2026-02-22 | 不再需要独立链路 |
|
||||
| `ODS_ASSISTANT_ABOLISH` / `ASSISTANT_ABOLISH` | ETL/调度任务 | 2026-02-22 | 无 |
|
||||
| `BILLIARD_VIP` | cfg_area_category 分类代码 | 2026-03-07 | V1-V4 归入 `BILLIARD`,V5 归入 `SNOOKER` |
|
||||
| `dws_member_recall_index` / `v_dws_member_recall_index` | DWS 表 + RLS 视图 | 2026-03-20 | `dws_member_winback_index`(WBI)+ `dws_member_newconv_index`(NCI) |
|
||||
|
||||
所有 `_archived/` 目录存放已废弃内容,除非用户明确要求,禁止读取或参考。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user