feat: batch update - gift card breakdown spec, backend APIs, miniprogram pages, ETL finance recharge, docs & migrations
This commit is contained in:
1
.kiro/specs/rns1-task-performance-api/.config.kiro
Normal file
1
.kiro/specs/rns1-task-performance-api/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"specId": "a7e3c1d4-8f2b-4e6a-b5d9-3c1f7a2e8b4d", "workflowType": "requirements-first", "specType": "feature"}
|
||||
930
.kiro/specs/rns1-task-performance-api/design.md
Normal file
930
.kiro/specs/rns1-task-performance-api/design.md
Normal file
@@ -0,0 +1,930 @@
|
||||
# 技术设计文档 — RNS1.1:任务与绩效接口
|
||||
|
||||
## 概述
|
||||
|
||||
RNS1.1 实现助教日常使用频率最高的 4 个核心接口及配套前端适配,覆盖 6 个任务:
|
||||
|
||||
1. **T1-1 扩展 TASK-1**:任务列表 `performance` 从 4 字段扩展到 15+ 字段;`enrichTask` 补充 `lastVisitDays`/`balance`/`aiSuggestion`
|
||||
2. **T1-2 实现 TASK-2**:任务详情完整版(维客线索、话术、服务记录、AI 分析、备注)
|
||||
3. **T1-3 实现 PERF-1**:绩效概览(DateGroup 分组、收入档位、新客/常客列表)
|
||||
4. **T1-4 实现 PERF-2**:绩效明细(按月分页、DateGroup 分组)
|
||||
5. **T1-5 pin/unpin**:已有端点的响应格式对齐契约
|
||||
6. **T1-6 前端适配**:createNote 补 score、月份切换、avatarChar/avatarColor 前端计算
|
||||
|
||||
### 设计原则
|
||||
|
||||
- **增量扩展**:在现有 `xcx_tasks.py` 路由和 `task_manager.py` 服务基础上扩展,不重写
|
||||
- **FDW 查询集中化**:所有 FDW 查询封装在 service 层,路由层不直接操作数据库
|
||||
- **DWD-DOC 强制规则**:金额 `items_sum` 口径、助教费用 `assistant_pd_money` + `assistant_cx_money` 拆分、会员信息通过 `member_id` JOIN `dim_member`
|
||||
- **契约驱动**:响应结构严格遵循 `API-contract.md` 定义
|
||||
|
||||
### 依赖
|
||||
|
||||
- RNS1.0 已完成:`ResponseWrapperMiddleware`、`CamelModel`、前端 `request()` 解包
|
||||
- 现有代码:`xcx_tasks.py`(路由)、`task_manager.py`(服务)、`xcx_notes.py`(备注路由)
|
||||
|
||||
## 架构
|
||||
|
||||
### 模块交互
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "微信小程序"
|
||||
A[task-list 页面] --> B[services/api.ts]
|
||||
C[task-detail 页面] --> B
|
||||
D[performance 页面] --> B
|
||||
E[performance-records 页面] --> B
|
||||
end
|
||||
|
||||
subgraph "FastAPI 后端"
|
||||
F[routers/xcx_tasks.py<br/>TASK-1/2, PIN/UNPIN]
|
||||
G[routers/xcx_performance.py<br/>PERF-1/2 — 新增]
|
||||
H[services/task_manager.py<br/>任务 CRUD + 扩展]
|
||||
I[services/performance_service.py<br/>绩效查询 — 新增]
|
||||
J[services/fdw_queries.py<br/>FDW 查询封装 — 新增]
|
||||
end
|
||||
|
||||
subgraph "数据库"
|
||||
K[(zqyy_app<br/>biz.coach_tasks<br/>biz.ai_cache<br/>biz.notes<br/>auth.user_assistant_binding<br/>public.member_retention_clue)]
|
||||
L[(etl_feiqiu via FDW<br/>fdw_etl.v_dws_assistant_salary_calc<br/>fdw_etl.v_dwd_assistant_service_log<br/>fdw_etl.v_dim_member<br/>fdw_etl.v_dim_member_card_account)]
|
||||
end
|
||||
|
||||
B -->|HTTP JSON| F
|
||||
B -->|HTTP JSON| G
|
||||
F --> H
|
||||
G --> I
|
||||
H --> J
|
||||
I --> J
|
||||
H --> K
|
||||
I --> K
|
||||
J -->|FDW + SET LOCAL| L
|
||||
|
||||
style G fill:#9f9,stroke:#333
|
||||
style I fill:#9f9,stroke:#333
|
||||
style J fill:#9f9,stroke:#333
|
||||
```
|
||||
|
||||
### 请求流程(以 TASK-1 扩展为例)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant MP as 小程序
|
||||
participant R as xcx_tasks Router
|
||||
participant TM as task_manager
|
||||
participant FDW as fdw_queries
|
||||
participant BIZ as zqyy_app
|
||||
participant ETL as etl via FDW
|
||||
|
||||
MP->>R: GET /api/xcx/tasks?status=pending&page=1
|
||||
R->>TM: get_task_list(user_id, site_id, status, page, pageSize)
|
||||
TM->>BIZ: SELECT assistant_id FROM user_assistant_binding
|
||||
TM->>BIZ: SELECT tasks FROM coach_tasks
|
||||
TM->>FDW: enrich_tasks(member_ids, assistant_id, site_id)
|
||||
FDW->>ETL: SET LOCAL app.current_site_id
|
||||
FDW->>ETL: SELECT dim_member (姓名)
|
||||
FDW->>ETL: SELECT dim_member_card_account (余额)
|
||||
FDW->>ETL: SELECT dwd_settlement_head (lastVisitDays)
|
||||
FDW-->>TM: enriched data
|
||||
TM->>FDW: get_performance_summary(assistant_id, site_id)
|
||||
FDW->>ETL: SELECT v_dws_assistant_salary_calc (档位/收入)
|
||||
FDW-->>TM: PerformanceSummary
|
||||
TM->>BIZ: SELECT ai_cache (aiSuggestion)
|
||||
TM-->>R: TaskListResponse
|
||||
R-->>MP: { code: 0, data: TaskListResponse }
|
||||
```
|
||||
|
||||
## 组件与接口
|
||||
|
||||
### 组件 1:xcx_tasks Router 扩展
|
||||
|
||||
**位置**:`apps/backend/app/routers/xcx_tasks.py`
|
||||
|
||||
**改动**:
|
||||
- `GET /api/xcx/tasks` — 响应从 `list[TaskListItem]` 改为 `TaskListResponse`(含 `items` + `performance` + 分页)
|
||||
- 新增 `GET /api/xcx/tasks/{taskId}` — 任务详情端点
|
||||
- `POST .../pin` 和 `POST .../unpin` — 响应对齐契约格式 `{ isPinned: bool }`
|
||||
- 新增查询参数:`status`(筛选)、`page`、`pageSize`
|
||||
|
||||
```python
|
||||
@router.get("", response_model=TaskListResponse)
|
||||
async def get_tasks(
|
||||
status: str = Query("pending", regex="^(pending|completed|abandoned)$"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
user: CurrentUser = Depends(require_approved()),
|
||||
):
|
||||
"""获取任务列表 + 绩效概览。"""
|
||||
return await task_manager.get_task_list_v2(
|
||||
user.user_id, user.site_id, status, page, page_size
|
||||
)
|
||||
|
||||
@router.get("/{task_id}", response_model=TaskDetailResponse)
|
||||
async def get_task_detail(
|
||||
task_id: int,
|
||||
user: CurrentUser = Depends(require_approved()),
|
||||
):
|
||||
"""获取任务详情完整版。"""
|
||||
return await task_manager.get_task_detail(
|
||||
task_id, user.user_id, user.site_id
|
||||
)
|
||||
```
|
||||
|
||||
### 组件 2:xcx_performance Router(新增)
|
||||
|
||||
**位置**:`apps/backend/app/routers/xcx_performance.py`
|
||||
|
||||
**职责**:绩效概览和绩效明细两个端点。
|
||||
|
||||
```python
|
||||
router = APIRouter(prefix="/api/xcx/performance", tags=["小程序绩效"])
|
||||
|
||||
@router.get("", response_model=PerformanceOverviewResponse)
|
||||
async def get_performance_overview(
|
||||
year: int = Query(...),
|
||||
month: int = Query(..., ge=1, le=12),
|
||||
user: CurrentUser = Depends(require_approved()),
|
||||
):
|
||||
"""绩效概览(PERF-1)。"""
|
||||
return await performance_service.get_overview(
|
||||
user.user_id, user.site_id, year, month
|
||||
)
|
||||
|
||||
@router.get("/records", response_model=PerformanceRecordsResponse)
|
||||
async def get_performance_records(
|
||||
year: int = Query(...),
|
||||
month: int = Query(..., ge=1, le=12),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
user: CurrentUser = Depends(require_approved()),
|
||||
):
|
||||
"""绩效明细(PERF-2)。"""
|
||||
return await performance_service.get_records(
|
||||
user.user_id, user.site_id, year, month, page, page_size
|
||||
)
|
||||
```
|
||||
|
||||
**注册**(`main.py`):
|
||||
```python
|
||||
from app.routers import xcx_performance
|
||||
app.include_router(xcx_performance.router)
|
||||
```
|
||||
|
||||
### 组件 3:fdw_queries 服务(新增)
|
||||
|
||||
**位置**:`apps/backend/app/services/fdw_queries.py`
|
||||
|
||||
**职责**:封装所有 FDW 查询,统一 `SET LOCAL app.current_site_id` 隔离。所有 FDW 查询集中在此模块,避免 service 层散落 SQL。
|
||||
|
||||
**核心函数**:
|
||||
|
||||
```python
|
||||
def _fdw_context(conn, site_id: int):
|
||||
"""上下文管理器:BEGIN + SET LOCAL app.current_site_id。"""
|
||||
...
|
||||
|
||||
def get_member_info(conn, site_id: int, member_ids: list[int]) -> dict[int, MemberInfo]:
|
||||
"""
|
||||
批量查询会员信息。
|
||||
⚠️ DQ-6:通过 member_id JOIN fdw_etl.v_dim_member 获取 nickname/mobile,
|
||||
禁止使用 settlement_head.member_phone/member_name。
|
||||
"""
|
||||
# SELECT member_id, nickname, mobile
|
||||
# FROM fdw_etl.v_dim_member
|
||||
# WHERE member_id = ANY(%s) AND scd2_is_current = 1
|
||||
|
||||
def get_member_balance(conn, site_id: int, member_ids: list[int]) -> dict[int, Decimal]:
|
||||
"""
|
||||
批量查询会员储值卡余额。
|
||||
⚠️ DQ-7:通过 member_id JOIN fdw_etl.v_dim_member_card_account。
|
||||
"""
|
||||
|
||||
def get_last_visit_days(conn, site_id: int, member_ids: list[int]) -> dict[int, int | None]:
|
||||
"""
|
||||
批量查询客户距上次到店天数。
|
||||
来源:fdw_etl.v_dwd_assistant_service_log 或 dwd_settlement_head。
|
||||
"""
|
||||
|
||||
def get_salary_calc(conn, site_id: int, assistant_id: int, year: int, month: int) -> dict | None:
|
||||
"""
|
||||
查询助教绩效/档位/收入数据。
|
||||
来源:fdw_etl.v_dws_assistant_salary_calc。
|
||||
⚠️ DWD-DOC 规则 1:收入使用 items_sum 口径。
|
||||
⚠️ DWD-DOC 规则 2:费用使用 assistant_pd_money + assistant_cx_money。
|
||||
"""
|
||||
|
||||
def get_service_records(
|
||||
conn, site_id: int, assistant_id: int,
|
||||
year: int, month: int, limit: int, offset: int
|
||||
) -> list[dict]:
|
||||
"""
|
||||
查询助教服务记录明细。
|
||||
来源:fdw_etl.v_dwd_assistant_service_log。
|
||||
⚠️ 废单排除:WHERE is_trash = false。
|
||||
⚠️ DQ-6:客户姓名通过 member_id JOIN dim_member。
|
||||
"""
|
||||
|
||||
def get_service_records_for_task(
|
||||
conn, site_id: int, assistant_id: int, member_id: int, limit: int
|
||||
) -> list[dict]:
|
||||
"""查询特定客户的服务记录(TASK-2 用)。"""
|
||||
```
|
||||
|
||||
### 组件 4:task_manager 服务扩展
|
||||
|
||||
**位置**:`apps/backend/app/services/task_manager.py`
|
||||
|
||||
**新增函数**:
|
||||
|
||||
```python
|
||||
async def get_task_list_v2(
|
||||
user_id: int, site_id: int,
|
||||
status: str, page: int, page_size: int
|
||||
) -> dict:
|
||||
"""
|
||||
扩展版任务列表(TASK-1)。
|
||||
返回 { items, total, page, pageSize, performance }。
|
||||
"""
|
||||
|
||||
async def get_task_detail(
|
||||
task_id: int, user_id: int, site_id: int
|
||||
) -> dict:
|
||||
"""
|
||||
任务详情完整版(TASK-2)。
|
||||
返回基础信息 + retentionClues + talkingPoints + serviceSummary
|
||||
+ serviceRecords + aiAnalysis + notes + customerId。
|
||||
"""
|
||||
```
|
||||
|
||||
**`get_task_list_v2` 逻辑**:
|
||||
1. `_get_assistant_id()` 获取 `assistant_id`
|
||||
2. 查询 `biz.coach_tasks` 带分页(`LIMIT/OFFSET` + `COUNT(*)`)
|
||||
3. 调用 `fdw_queries` 批量获取会员信息、余额、lastVisitDays
|
||||
4. 调用 `fdw_queries.get_salary_calc()` 获取绩效概览
|
||||
5. 查询 `biz.ai_cache` 获取 aiSuggestion
|
||||
6. 组装 `TaskListResponse`
|
||||
|
||||
**`get_task_detail` 逻辑**:
|
||||
1. `_get_assistant_id()` + `_verify_task_ownership()` 权限校验
|
||||
2. 查询 `biz.coach_tasks` 基础信息
|
||||
3. 查询 `public.member_retention_clue` 维客线索
|
||||
4. 查询 `biz.ai_cache`(`app5_talking_points` → talkingPoints,`app4_analysis` → aiAnalysis)
|
||||
5. 调用 `fdw_queries.get_service_records_for_task()` 获取服务记录(最多 20 条)
|
||||
6. 查询 `biz.notes` 获取备注(最多 20 条)
|
||||
7. 组装 `TaskDetailResponse`
|
||||
|
||||
### 组件 5:performance_service(新增)
|
||||
|
||||
**位置**:`apps/backend/app/services/performance_service.py`
|
||||
|
||||
**核心函数**:
|
||||
|
||||
```python
|
||||
async def get_overview(
|
||||
user_id: int, site_id: int, year: int, month: int
|
||||
) -> dict:
|
||||
"""
|
||||
绩效概览(PERF-1)。
|
||||
1. 获取 assistant_id
|
||||
2. fdw_queries.get_salary_calc() → 档位/收入/费率
|
||||
3. fdw_queries.get_service_records() → 按日期分组为 DateGroup
|
||||
4. 聚合新客/常客列表
|
||||
5. 计算 incomeItems(含 desc 费率描述)
|
||||
6. 查询上月收入 lastMonthIncome
|
||||
"""
|
||||
|
||||
async def get_records(
|
||||
user_id: int, site_id: int,
|
||||
year: int, month: int, page: int, page_size: int
|
||||
) -> dict:
|
||||
"""
|
||||
绩效明细(PERF-2)。
|
||||
1. 获取 assistant_id
|
||||
2. fdw_queries.get_service_records() 带分页
|
||||
3. 按日期分组为 dateGroups
|
||||
4. 计算 summary 汇总
|
||||
5. 返回 { summary, dateGroups, hasMore }
|
||||
"""
|
||||
```
|
||||
|
||||
### 组件 6:Pydantic Schema(新增/扩展)
|
||||
|
||||
**位置**:`apps/backend/app/schemas/xcx_tasks.py`(扩展)+ `apps/backend/app/schemas/xcx_performance.py`(新增)
|
||||
|
||||
#### 任务相关 Schema 扩展
|
||||
|
||||
```python
|
||||
class PerformanceSummary(CamelModel):
|
||||
"""绩效概览(附带在任务列表响应中)。"""
|
||||
total_hours: float
|
||||
total_income: float
|
||||
total_customers: int
|
||||
month_label: str
|
||||
tier_nodes: list[float]
|
||||
basic_hours: float
|
||||
bonus_hours: float
|
||||
current_tier: int
|
||||
next_tier_hours: float
|
||||
tier_completed: bool
|
||||
bonus_money: float
|
||||
income_trend: str
|
||||
income_trend_dir: str # 'up' | 'down'
|
||||
prev_month: str
|
||||
current_tier_label: str
|
||||
|
||||
class TaskItem(CamelModel):
|
||||
"""任务列表项(扩展版)。"""
|
||||
id: int
|
||||
customer_name: str
|
||||
customer_avatar: str
|
||||
task_type: str
|
||||
task_type_label: str
|
||||
deadline: str | None
|
||||
heart_score: float
|
||||
hobbies: list[str]
|
||||
is_pinned: bool
|
||||
has_note: bool
|
||||
status: str
|
||||
last_visit_days: int | None = None
|
||||
balance: float | None = None
|
||||
ai_suggestion: str | None = None
|
||||
|
||||
class TaskListResponse(CamelModel):
|
||||
"""TASK-1 响应。"""
|
||||
items: list[TaskItem]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
performance: PerformanceSummary
|
||||
```
|
||||
|
||||
#### 任务详情 Schema
|
||||
|
||||
```python
|
||||
class RetentionClue(CamelModel):
|
||||
tag: str
|
||||
tag_color: str
|
||||
emoji: str
|
||||
text: str
|
||||
source: str # 'manual' | 'ai_consumption' | 'ai_note'
|
||||
desc: str | None = None
|
||||
|
||||
class ServiceRecord(CamelModel):
|
||||
table: str | None = None
|
||||
type: str
|
||||
type_class: str # 'basic' | 'vip' | 'tip' | 'recharge' | 'incentive'
|
||||
record_type: str | None = None # 'course' | 'recharge'
|
||||
duration: float
|
||||
duration_raw: float | None = None
|
||||
income: float
|
||||
is_estimate: bool | None = None
|
||||
drinks: str | None = None
|
||||
date: str
|
||||
|
||||
class AiAnalysis(CamelModel):
|
||||
summary: str
|
||||
suggestions: list[str]
|
||||
|
||||
class NoteItem(CamelModel):
|
||||
id: int
|
||||
content: str
|
||||
tag_type: str
|
||||
tag_label: str
|
||||
created_at: str
|
||||
score: int | None = None
|
||||
|
||||
class ServiceSummary(CamelModel):
|
||||
total_hours: float
|
||||
total_income: float
|
||||
avg_income: float
|
||||
|
||||
class TaskDetailResponse(CamelModel):
|
||||
"""TASK-2 响应。"""
|
||||
# 基础信息
|
||||
id: int
|
||||
customer_name: str
|
||||
customer_avatar: str
|
||||
task_type: str
|
||||
task_type_label: str
|
||||
deadline: str | None
|
||||
heart_score: float
|
||||
hobbies: list[str]
|
||||
is_pinned: bool
|
||||
has_note: bool
|
||||
status: str
|
||||
customer_id: int
|
||||
# 扩展模块
|
||||
retention_clues: list[RetentionClue]
|
||||
talking_points: list[str]
|
||||
service_summary: ServiceSummary
|
||||
service_records: list[ServiceRecord]
|
||||
ai_analysis: AiAnalysis
|
||||
notes: list[NoteItem]
|
||||
```
|
||||
|
||||
#### 绩效 Schema
|
||||
|
||||
```python
|
||||
class DateGroupRecord(CamelModel):
|
||||
customer_name: str
|
||||
avatar_char: str | None = None # PERF-1 返回,PERF-2 不返回
|
||||
avatar_color: str | None = None # PERF-1 返回,PERF-2 不返回
|
||||
time_range: str
|
||||
hours: str
|
||||
course_type: str
|
||||
course_type_class: str # 'basic' | 'vip' | 'tip'
|
||||
location: str
|
||||
income: str
|
||||
|
||||
class DateGroup(CamelModel):
|
||||
date: str
|
||||
total_hours: str
|
||||
total_income: str
|
||||
records: list[DateGroupRecord]
|
||||
|
||||
class TierInfo(CamelModel):
|
||||
basic_rate: float
|
||||
incentive_rate: float
|
||||
|
||||
class IncomeItem(CamelModel):
|
||||
icon: str
|
||||
label: str
|
||||
desc: str
|
||||
value: str
|
||||
|
||||
class CustomerSummary(CamelModel):
|
||||
name: str
|
||||
avatar_char: str
|
||||
avatar_color: str
|
||||
|
||||
class NewCustomer(CustomerSummary):
|
||||
last_service: str
|
||||
count: int
|
||||
|
||||
class RegularCustomer(CustomerSummary):
|
||||
hours: float
|
||||
income: str
|
||||
count: int
|
||||
|
||||
class PerformanceOverviewResponse(CamelModel):
|
||||
"""PERF-1 响应。"""
|
||||
coach_name: str
|
||||
coach_role: str
|
||||
store_name: str
|
||||
monthly_income: str
|
||||
last_month_income: str
|
||||
current_tier: TierInfo
|
||||
next_tier: TierInfo
|
||||
upgrade_hours_needed: float
|
||||
upgrade_bonus: float
|
||||
income_items: list[IncomeItem]
|
||||
monthly_total: str
|
||||
this_month_records: list[DateGroup]
|
||||
new_customers: list[NewCustomer]
|
||||
regular_customers: list[RegularCustomer]
|
||||
|
||||
class RecordsSummary(CamelModel):
|
||||
total_count: int
|
||||
total_hours: float
|
||||
total_hours_raw: float
|
||||
total_income: float
|
||||
|
||||
class PerformanceRecordsResponse(CamelModel):
|
||||
"""PERF-2 响应。"""
|
||||
summary: RecordsSummary
|
||||
date_groups: list[DateGroup]
|
||||
has_more: bool
|
||||
```
|
||||
|
||||
### 组件 7:前端适配
|
||||
|
||||
**涉及文件与改动**:
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `services/api.ts` | 新增 `pinTask()`、`unpinTask()`;`createNote()` 增加 `score` 参数;新增 `fetchTaskDetail()`、`fetchPerformanceOverview(year, month)`、`fetchPerformanceRecords(year, month, page)` |
|
||||
| `pages/task-list/task-list.ts` | 消费 `TaskListResponse` 新结构;`buildPerfData()` 使用 15+ 字段绩效数据 |
|
||||
| `pages/task-detail/task-detail.ts` | 调用 `fetchTaskDetail()`;`storageLevel`/`relationLevel` 前端本地计算 |
|
||||
| `pages/performance/performance.ts` | 添加月份切换控件;切换时重新调用 API |
|
||||
| `pages/performance-records/performance-records.ts` | 月份切换时重置 `page=1`;`avatarChar`/`avatarColor` 使用 `nameToAvatarColor()` 前端计算 |
|
||||
|
||||
|
||||
|
||||
## 数据模型
|
||||
|
||||
### FDW 查询模式
|
||||
|
||||
所有 FDW 查询遵循统一模式:
|
||||
|
||||
```python
|
||||
# 1. 使用业务库连接(get_connection() → zqyy_app)
|
||||
conn = get_connection()
|
||||
|
||||
# 2. 开启事务 + 设置门店隔离
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("BEGIN")
|
||||
cur.execute("SET LOCAL app.current_site_id = %s", (str(site_id),))
|
||||
|
||||
# 3. 通过 fdw_etl schema 访问 ETL 视图
|
||||
cur.execute("""
|
||||
SELECT ...
|
||||
FROM fdw_etl.v_dws_assistant_salary_calc
|
||||
WHERE assistant_id = %s AND calc_month = %s
|
||||
""", (assistant_id, f"{year}-{month:02d}"))
|
||||
|
||||
conn.commit()
|
||||
```
|
||||
|
||||
### 核心 SQL 查询设计
|
||||
|
||||
#### Q1: 绩效概览查询(TASK-1 performance + PERF-1)
|
||||
|
||||
```sql
|
||||
-- ⚠️ DWD-DOC 规则 1: 收入使用 items_sum 口径
|
||||
-- ⚠️ DWD-DOC 规则 2: 费用使用 assistant_pd_money + assistant_cx_money
|
||||
SELECT
|
||||
calc_month,
|
||||
coach_level, -- 当前档位名称
|
||||
tier_index, -- 当前档位索引
|
||||
tier_nodes, -- 档位节点数组 (JSON)
|
||||
basic_hours, -- 基础课时 (assistant_pd_money 对应)
|
||||
bonus_hours, -- 激励课时 (assistant_cx_money 对应)
|
||||
total_hours, -- 总工时
|
||||
total_income, -- 总收入 (items_sum 口径)
|
||||
total_customers, -- 服务客户数
|
||||
basic_rate, -- 基础课时费率
|
||||
incentive_rate, -- 激励课时费率
|
||||
next_tier_basic_rate, -- 下一档基础课时费率
|
||||
next_tier_incentive_rate, -- 下一档激励课时费率
|
||||
next_tier_hours, -- 下一档工时阈值
|
||||
tier_completed, -- 是否已达最高档
|
||||
bonus_money, -- 升档奖金
|
||||
assistant_pd_money_total, -- 基础课总费用
|
||||
assistant_cx_money_total -- 激励课总费用
|
||||
FROM fdw_etl.v_dws_assistant_salary_calc
|
||||
WHERE assistant_id = %s
|
||||
AND calc_month = %s
|
||||
```
|
||||
|
||||
#### Q2: 收入趋势计算(TASK-1 performance)
|
||||
|
||||
```sql
|
||||
-- 当月 vs 上月收入对比
|
||||
WITH months AS (
|
||||
SELECT total_income, calc_month
|
||||
FROM fdw_etl.v_dws_assistant_salary_calc
|
||||
WHERE assistant_id = %s
|
||||
AND calc_month IN (%s, %s) -- 当月, 上月
|
||||
)
|
||||
SELECT * FROM months ORDER BY calc_month
|
||||
```
|
||||
|
||||
后端计算逻辑:
|
||||
```python
|
||||
diff = current_income - prev_income
|
||||
income_trend = f"{'↑' if diff >= 0 else '↓'}{abs(diff):.0f}"
|
||||
income_trend_dir = "up" if diff >= 0 else "down"
|
||||
```
|
||||
|
||||
#### Q3: 服务记录查询(PERF-1 thisMonthRecords / PERF-2 dateGroups / TASK-2 serviceRecords)
|
||||
|
||||
```sql
|
||||
-- ⚠️ DWD-DOC 规则 DQ-6: 客户姓名通过 member_id JOIN dim_member
|
||||
-- ⚠️ 废单排除: is_trash = false
|
||||
SELECT
|
||||
sl.id,
|
||||
dm.nickname AS customer_name,
|
||||
sl.settle_time,
|
||||
sl.start_time,
|
||||
sl.end_time,
|
||||
sl.service_hours, -- 折算工时
|
||||
sl.service_hours_raw, -- 原始工时
|
||||
sl.course_type, -- 课程类型
|
||||
sl.table_name, -- 台桌名
|
||||
sl.items_sum AS income, -- ⚠️ items_sum 口径
|
||||
sl.is_estimate
|
||||
FROM fdw_etl.v_dwd_assistant_service_log sl
|
||||
LEFT JOIN fdw_etl.v_dim_member dm
|
||||
ON sl.member_id = dm.member_id
|
||||
AND dm.scd2_is_current = 1 -- ⚠️ SCD2 当前记录
|
||||
WHERE sl.assistant_id = %s
|
||||
AND sl.is_trash = false -- ⚠️ 废单排除
|
||||
AND sl.settle_time >= %s -- 月份起始
|
||||
AND sl.settle_time < %s -- 月份结束
|
||||
ORDER BY sl.settle_time DESC
|
||||
LIMIT %s OFFSET %s
|
||||
```
|
||||
|
||||
#### Q4: 任务项扩展字段查询
|
||||
|
||||
```sql
|
||||
-- lastVisitDays: 距上次到店天数
|
||||
SELECT member_id,
|
||||
CURRENT_DATE - MAX(settle_time::date) AS days_since_visit
|
||||
FROM fdw_etl.v_dwd_assistant_service_log
|
||||
WHERE member_id = ANY(%s)
|
||||
AND is_trash = false
|
||||
GROUP BY member_id
|
||||
|
||||
-- balance: 客户储值卡余额
|
||||
-- ⚠️ DQ-7: 通过 member_id JOIN dim_member_card_account
|
||||
SELECT tenant_member_id AS member_id,
|
||||
balance
|
||||
FROM fdw_etl.v_dim_member_card_account
|
||||
WHERE tenant_member_id = ANY(%s)
|
||||
AND scd2_is_current = 1
|
||||
```
|
||||
|
||||
#### Q5: 维客线索查询(TASK-2)
|
||||
|
||||
```sql
|
||||
SELECT id, tag, tag_color, emoji, text, source, description
|
||||
FROM public.member_retention_clue
|
||||
WHERE member_id = %s
|
||||
AND site_id = %s
|
||||
ORDER BY created_at DESC
|
||||
```
|
||||
|
||||
#### Q6: AI 缓存查询(TASK-2)
|
||||
|
||||
```sql
|
||||
-- aiAnalysis: cache_type = 'app4_analysis'
|
||||
-- talkingPoints: cache_type = 'app5_talking_points'
|
||||
SELECT cache_type, cache_value
|
||||
FROM biz.ai_cache
|
||||
WHERE target_id = %s
|
||||
AND site_id = %s
|
||||
AND cache_type IN ('app4_analysis', 'app5_talking_points')
|
||||
```
|
||||
|
||||
#### Q7: 新客/常客列表(PERF-1)
|
||||
|
||||
```sql
|
||||
-- 新客:本月首次服务的客户
|
||||
-- ⚠️ DQ-6: 客户姓名通过 member_id JOIN dim_member
|
||||
WITH month_records AS (
|
||||
SELECT sl.member_id,
|
||||
dm.nickname AS customer_name,
|
||||
COUNT(*) AS service_count,
|
||||
MAX(sl.settle_time) AS last_service
|
||||
FROM fdw_etl.v_dwd_assistant_service_log sl
|
||||
LEFT JOIN fdw_etl.v_dim_member dm
|
||||
ON sl.member_id = dm.member_id AND dm.scd2_is_current = 1
|
||||
WHERE sl.assistant_id = %s
|
||||
AND sl.is_trash = false
|
||||
AND sl.settle_time >= %s AND sl.settle_time < %s
|
||||
GROUP BY sl.member_id, dm.nickname
|
||||
),
|
||||
historical AS (
|
||||
SELECT DISTINCT member_id
|
||||
FROM fdw_etl.v_dwd_assistant_service_log
|
||||
WHERE assistant_id = %s
|
||||
AND is_trash = false
|
||||
AND settle_time < %s -- 本月之前
|
||||
)
|
||||
SELECT mr.member_id, mr.customer_name, mr.service_count, mr.last_service
|
||||
FROM month_records mr
|
||||
LEFT JOIN historical h ON mr.member_id = h.member_id
|
||||
WHERE h.member_id IS NULL -- 新客:历史无记录
|
||||
ORDER BY mr.last_service DESC
|
||||
|
||||
-- 常客:本月服务 ≥ 2 次的客户
|
||||
SELECT mr.member_id, mr.customer_name,
|
||||
SUM(sl.service_hours) AS total_hours,
|
||||
SUM(sl.items_sum) AS total_income, -- ⚠️ items_sum 口径
|
||||
COUNT(*) AS service_count
|
||||
FROM month_records mr
|
||||
JOIN fdw_etl.v_dwd_assistant_service_log sl
|
||||
ON mr.member_id = sl.member_id
|
||||
WHERE mr.service_count >= 2
|
||||
GROUP BY mr.member_id, mr.customer_name
|
||||
ORDER BY total_income DESC
|
||||
```
|
||||
|
||||
### courseTypeClass 枚举映射
|
||||
|
||||
后端根据 `v_dwd_assistant_service_log.course_type` 映射:
|
||||
|
||||
| 原始值 | courseTypeClass | courseType 中文 |
|
||||
|--------|----------------|----------------|
|
||||
| `basic` / `陪打` / `基础课` | `basic` | 基础课 |
|
||||
| `vip` / `包厢` / `包厢课` | `vip` | 包厢课 |
|
||||
| `tip` / `超休` / `激励课` | `tip` | 激励课 |
|
||||
| `recharge` / `充值` | `recharge` | 充值 |
|
||||
| `incentive` / `激励` | `incentive` | 激励 |
|
||||
|
||||
统一不带 `tag-` 前缀(契约要求)。
|
||||
|
||||
### DWD-DOC 强制规则实施位置
|
||||
|
||||
| 规则 | 实施位置 | 具体措施 |
|
||||
|------|---------|---------|
|
||||
| 规则 1: `items_sum` 口径 | `fdw_queries.py` 所有金额查询 | SQL 中使用 `items_sum` 字段,禁止 `consume_money` |
|
||||
| 规则 2: 助教费用拆分 | `fdw_queries.get_salary_calc()` | 使用 `assistant_pd_money` + `assistant_cx_money`,禁止 `service_fee` |
|
||||
| DQ-6: 会员信息 JOIN | `fdw_queries.get_member_info()` + 所有含客户姓名的查询 | `member_id JOIN fdw_etl.v_dim_member WHERE scd2_is_current=1` |
|
||||
| DQ-7: 会员卡 JOIN | `fdw_queries.get_member_balance()` | `member_id JOIN fdw_etl.v_dim_member_card_account WHERE scd2_is_current=1` |
|
||||
| 废单排除 | `fdw_queries` 所有服务记录查询 | `WHERE is_trash = false`,禁止引用 `dwd_assistant_trash_event` |
|
||||
|
||||
|
||||
|
||||
## 正确性属性
|
||||
|
||||
*属性(Property)是一个在系统所有合法执行路径上都应成立的特征或行为——本质上是对"系统应该做什么"的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
|
||||
|
||||
### Property 1: 收入趋势计算正确性
|
||||
|
||||
*For any* 两个非负收入值 `currentIncome` 和 `prevIncome`,计算 `incomeTrend` 和 `incomeTrendDir` 时:
|
||||
- `incomeTrendDir` 应为 `"up"` 当 `currentIncome >= prevIncome`,否则为 `"down"`
|
||||
- `incomeTrend` 应包含绝对差值 `abs(currentIncome - prevIncome)` 的整数表示
|
||||
- `incomeTrend` 应以 `"↑"` 或 `"↓"` 开头,与 `incomeTrendDir` 一致
|
||||
|
||||
**Validates: Requirements 1.4**
|
||||
|
||||
### Property 2: lastVisitDays 计算正确性
|
||||
|
||||
*For any* 有效日期 `lastVisitDate`(不晚于今天),`lastVisitDays` 应等于 `(current_date - lastVisitDate).days`,且结果为非负整数。
|
||||
|
||||
**Validates: Requirements 1.7**
|
||||
|
||||
### Property 3: 维客线索 tag 净化与 source 枚举
|
||||
|
||||
*For any* 维客线索记录,经过后端处理后:
|
||||
- `tag` 字段不应包含换行符 `\n`(多行标签使用空格分隔)
|
||||
- `source` 字段必须是 `manual`、`ai_consumption`、`ai_note` 三者之一
|
||||
|
||||
**Validates: Requirements 2.4, 2.5**
|
||||
|
||||
### Property 4: courseTypeClass 枚举映射
|
||||
|
||||
*For any* 服务记录的原始课程类型值,经过 `map_course_type_class()` 映射后:
|
||||
- 结果必须是 `basic`、`vip`、`tip`、`recharge`、`incentive` 五者之一
|
||||
- 结果不应包含 `tag-` 前缀
|
||||
|
||||
**Validates: Requirements 2.9, 4.4**
|
||||
|
||||
### Property 5: 列表分页与排序
|
||||
|
||||
*For any* 记录集合和分页参数 `(page, pageSize)`:
|
||||
- 返回的记录数 ≤ `pageSize`
|
||||
- `hasMore` 为 `true` 当且仅当总记录数 > `page * pageSize`
|
||||
- 服务记录在每个 DateGroup 内按时间倒序排列
|
||||
- 备注列表按 `created_at` 倒序排列
|
||||
|
||||
**Validates: Requirements 2.3, 4.2**
|
||||
|
||||
### Property 6: DateGroup 分组正确性
|
||||
|
||||
*For any* 服务记录集合,按日期分组后:
|
||||
- 每个 DateGroup 的 `date` 在结果中唯一
|
||||
- 每个 DateGroup 内的所有记录的日期部分相同
|
||||
- `totalHours` 等于该组内所有记录 `hours` 之和
|
||||
- `totalIncome` 等于该组内所有记录 `income` 之和
|
||||
- DateGroup 列表按日期倒序排列
|
||||
|
||||
**Validates: Requirements 3.3, 4.3**
|
||||
|
||||
### Property 7: incomeItems desc 格式化
|
||||
|
||||
*For any* 费率 `rate`(正数)和工时 `hours`(非负数),生成的 `desc` 字段应包含费率值和工时值,格式为 `"{rate}元/h × {hours}h"`。
|
||||
|
||||
**Validates: Requirements 3.9**
|
||||
|
||||
### Property 8: 月度汇总聚合正确性
|
||||
|
||||
*For any* 服务记录集合,`summary` 的聚合应满足:
|
||||
- `totalCount` 等于记录总数
|
||||
- `totalHours` 等于所有记录折算工时之和
|
||||
- `totalHoursRaw` 等于所有记录原始工时之和
|
||||
- `totalIncome` 等于所有记录收入之和(`items_sum` 口径)
|
||||
|
||||
**Validates: Requirements 4.6**
|
||||
|
||||
### Property 9: Pin/Unpin 状态往返
|
||||
|
||||
*For any* 有效任务,执行 `pin` 后 `isPinned` 应为 `true`,再执行 `unpin` 后 `isPinned` 应恢复为 `false`。即 `unpin(pin(task))` 的 `isPinned` 状态等于原始状态(假设原始为 `false`)。
|
||||
|
||||
**Validates: Requirements 5.1, 5.2, 5.3**
|
||||
|
||||
### Property 10: 权限与数据隔离
|
||||
|
||||
*For any* 请求,若用户状态非 `approved`、缺少 `view_tasks` 权限、或在 `user_assistant_binding` 中无绑定记录,则所有 RNS1.1 端点应返回 HTTP 403。若请求的 `taskId` 不属于当前助教的 `assistant_id`,也应返回 HTTP 403。
|
||||
|
||||
**Validates: Requirements 2.13, 8.1, 8.2, 8.4**
|
||||
|
||||
### Property 11: 前端派生字段计算
|
||||
|
||||
*For any* 客户姓名字符串 `name`(非空),`nameToAvatarColor(name)` 应返回:
|
||||
- `avatarChar` 等于 `name` 的第一个字符
|
||||
- `avatarColor` 为预定义颜色集合中的一个值
|
||||
- 相同 `name` 输入始终产生相同输出(确定性)
|
||||
|
||||
*For any* 余额值 `balance`(非负数),`computeStorageLevel(balance)` 应返回预定义等级之一(如 "非常多"/"较多"/"一般"/"较少"),且等级随余额单调递增。
|
||||
|
||||
*For any* 亲密度分数 `heartScore`(0-10),`computeRelationLevel(heartScore)` 应返回预定义等级之一,且等级随分数单调递增。
|
||||
|
||||
**Validates: Requirements 6.4, 6.5, 7.6**
|
||||
|
||||
### Property 12: 备注 score 输入验证
|
||||
|
||||
*For any* 整数 `score`,若 `score` 在 1-5 范围内,`POST /api/xcx/notes` 应接受并存储;若 `score` 超出范围(<1 或 >5),应返回 422 错误。`score` 为 `null` 时应正常创建备注(score 可选)。
|
||||
|
||||
**Validates: Requirements 6.3**
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 后端错误处理
|
||||
|
||||
| 错误场景 | HTTP 状态码 | 响应 | 触发位置 |
|
||||
|---------|:----------:|------|---------|
|
||||
| 用户未通过审核 | 403 | `{ code: 403, message: "用户未通过审核,无法访问此资源" }` | `require_approved()` |
|
||||
| 用户无 `view_tasks` 权限 | 403 | `{ code: 403, message: "权限不足" }` | `require_permission("view_tasks")` |
|
||||
| 用户未绑定助教身份 | 403 | `{ code: 403, message: "未绑定助教身份" }` | `_get_assistant_id()` |
|
||||
| 任务不属于当前助教 | 403 | `{ code: 403, message: "无权访问该任务" }` | `_verify_task_ownership()` |
|
||||
| 任务不存在 | 404 | `{ code: 404, message: "任务不存在" }` | `get_task_detail()` / `pin_task()` |
|
||||
| score 超出 1-5 范围 | 422 | `{ code: 422, message: "评分必须在 1-5 范围内" }` | `create_note()` |
|
||||
| 无效的 status 参数 | 422 | Pydantic 验证错误 | FastAPI 参数校验 |
|
||||
| year/month 参数无效 | 422 | Pydantic 验证错误 | FastAPI 参数校验 |
|
||||
| FDW 查询超时/失败 | 500 | `{ code: 500, message: "Internal Server Error" }` | `unhandled_exception_handler` |
|
||||
| 数据库连接失败 | 500 | `{ code: 500, message: "Internal Server Error" }` | `unhandled_exception_handler` |
|
||||
|
||||
### FDW 查询容错策略
|
||||
|
||||
任务列表扩展字段(`lastVisitDays`、`balance`、`aiSuggestion`)采用**优雅降级**策略:
|
||||
|
||||
```python
|
||||
# 单个扩展字段查询失败不影响整体响应
|
||||
try:
|
||||
last_visit_map = fdw_queries.get_last_visit_days(conn, site_id, member_ids)
|
||||
except Exception:
|
||||
logger.warning("FDW 查询 lastVisitDays 失败", exc_info=True)
|
||||
last_visit_map = {}
|
||||
```
|
||||
|
||||
核心字段(绩效概览、服务记录)查询失败则直接抛出 500。
|
||||
|
||||
### 前端错误处理
|
||||
|
||||
| 场景 | 处理方式 |
|
||||
|------|---------|
|
||||
| API 返回 403 | 显示"权限不足"提示,不跳转 |
|
||||
| API 返回 404 | 显示"任务不存在"提示,返回列表页 |
|
||||
| API 返回 500 | 显示"服务器错误"toast |
|
||||
| 月份切换期间网络失败 | 恢复上一月份状态,显示重试提示 |
|
||||
| 扩展字段为 null | 前端隐藏对应 UI 元素(如不显示余额标签) |
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 双轨测试方法
|
||||
|
||||
RNS1.1 采用属性测试(Property-Based Testing)+ 单元测试(Unit Testing)双轨并行:
|
||||
|
||||
- **属性测试**:验证计算逻辑、枚举映射、分页排序、聚合等通用规则在所有输入上的正确性
|
||||
- **单元测试**:验证具体的 API 端点行为、权限校验、FDW 查询集成、边界条件
|
||||
|
||||
### 属性测试配置
|
||||
|
||||
- **测试库**:[Hypothesis](https://hypothesis.readthedocs.io/)(Python,项目已使用)
|
||||
- **测试位置**:`tests/` 目录(Monorepo 级属性测试)
|
||||
- **最小迭代次数**:每个属性测试 100 次(`@settings(max_examples=100)`)
|
||||
- **标签格式**:每个测试函数的 docstring 中标注 `Feature: rns1-task-performance-api, Property {N}: {property_text}`
|
||||
- **每个正确性属性由一个属性测试实现**
|
||||
|
||||
### 属性测试清单
|
||||
|
||||
| Property | 测试函数 | 生成器 | 验证逻辑 |
|
||||
|----------|---------|--------|---------|
|
||||
| P1: 收入趋势计算 | `test_income_trend_computation` | `st.floats(min_value=0, max_value=1e6)` × 2 | 方向正确、差值正确、前缀符号一致 |
|
||||
| P2: lastVisitDays 计算 | `test_last_visit_days_computation` | `st.dates(max_value=date.today())` | 天数差 = (today - date).days,非负 |
|
||||
| P3: 维客线索净化 | `test_retention_clue_sanitization` | `st.text()` 生成含 `\n` 的 tag + `st.sampled_from` source | tag 无换行、source 在枚举内 |
|
||||
| P4: courseTypeClass 映射 | `test_course_type_class_mapping` | `st.sampled_from(ALL_COURSE_TYPES)` | 结果在枚举内、无 `tag-` 前缀 |
|
||||
| P5: 分页与排序 | `test_pagination_and_ordering` | `st.lists(st.fixed_dictionaries(...))` + `st.integers(1,10)` page/pageSize | 记录数 ≤ pageSize、hasMore 正确、排序正确 |
|
||||
| P6: DateGroup 分组 | `test_date_group_correctness` | `st.lists(st.fixed_dictionaries({date, hours, income}))` | 日期唯一、组内日期一致、汇总正确 |
|
||||
| P7: incomeItems desc 格式化 | `test_income_item_desc_format` | `st.floats(min_value=0.01)` rate × `st.floats(min_value=0)` hours | 包含费率和工时值 |
|
||||
| P8: 月度汇总聚合 | `test_monthly_summary_aggregation` | `st.lists(st.fixed_dictionaries({hours, hours_raw, income}))` | count/hours/hours_raw/income 聚合正确 |
|
||||
| P9: Pin/Unpin 往返 | `test_pin_unpin_roundtrip` | `st.booleans()` 初始状态 | pin→true、unpin→false、往返恢复 |
|
||||
| P10: 权限隔离 | `test_authorization_enforcement` | `st.sampled_from(INVALID_USER_SCENARIOS)` | 所有场景返回 403 |
|
||||
| P11: 前端派生字段 | `test_frontend_derived_fields` | `st.text(min_size=1)` name + `st.floats(0, 1e6)` balance + `st.floats(0, 10)` heartScore | avatarChar=name[0]、确定性、单调性 |
|
||||
| P12: score 输入验证 | `test_note_score_validation` | `st.integers()` | 1-5 接受、超范围拒绝、null 接受 |
|
||||
|
||||
### 单元测试清单
|
||||
|
||||
| 测试目标 | 测试文件 | 关键用例 |
|
||||
|---------|---------|---------|
|
||||
| TASK-1 端点 | `tests/unit/test_xcx_tasks_v2.py` | 正常返回 TaskListResponse 结构;performance 含 15+ 字段;扩展字段 null 降级 |
|
||||
| TASK-2 端点 | `tests/unit/test_task_detail.py` | 完整详情返回;customerId 正确;serviceRecords ≤ 20 条;notes ≤ 20 条 |
|
||||
| PERF-1 端点 | `tests/unit/test_performance_overview.py` | DateGroup 结构正确;收入档位数据完整;新客/常客列表 |
|
||||
| PERF-2 端点 | `tests/unit/test_performance_records.py` | 分页正确;summary 聚合正确;hasMore 标记 |
|
||||
| PIN/UNPIN | `tests/unit/test_pin_unpin.py` | 响应格式 `{ isPinned: bool }`;404 不存在;403 非本人任务 |
|
||||
| 权限校验 | `tests/unit/test_auth_rns11.py` | 未审核用户 403;无绑定 403;无权限 403 |
|
||||
| FDW 查询 | `tests/unit/test_fdw_queries.py` | DQ-6 JOIN 正确;is_trash 排除;items_sum 口径 |
|
||||
| AI 缓存降级 | `tests/unit/test_ai_cache_fallback.py` | 无缓存返回空;cache_type 映射正确 |
|
||||
| tierCompleted 边界 | `tests/unit/test_tier_completed.py` | tierCompleted=true 时 bonusMoney=0 |
|
||||
|
||||
### 测试执行命令
|
||||
|
||||
```bash
|
||||
# 属性测试(Hypothesis)
|
||||
cd C:\NeoZQYY && pytest tests/ -v -k "rns1_task_performance"
|
||||
|
||||
# 单元测试
|
||||
cd apps/backend && pytest tests/unit/ -v -k "xcx_tasks_v2 or task_detail or performance or pin_unpin or auth_rns11 or fdw_queries"
|
||||
```
|
||||
214
.kiro/specs/rns1-task-performance-api/requirements.md
Normal file
214
.kiro/specs/rns1-task-performance-api/requirements.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# 需求文档 — RNS1.1:任务与绩效接口
|
||||
|
||||
## 简介
|
||||
|
||||
RNS1.1 是 NS1 小程序后端 API 补全项目的第二个子 spec,负责实现助教日常使用频率最高的 4 个核心接口(任务列表扩展、任务详情、绩效概览、绩效明细)、pin/unpin 端点、以及对应的前端适配。本 spec 覆盖助教视角的核心工作流,是助教登录后最先接触的功能集合。
|
||||
|
||||
### 依赖
|
||||
|
||||
- RNS1.0(基础设施与契约重写)必须先完成:全局响应包装中间件、camelCase 转换、重写后的 API 契约
|
||||
- 前端已有 13 个页面(P5.2 交付),当前使用 mock 数据
|
||||
- 后端已有 `xcx_tasks.py`(需扩展)、`xcx_notes.py`、`xcx_auth.py`
|
||||
|
||||
### 来源文档
|
||||
|
||||
- `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-02~22)
|
||||
- `docs/reports/DWD-DOC/` — 金额口径与字段语义权威标杆文档
|
||||
|
||||
## 术语表
|
||||
|
||||
- **Backend**:FastAPI 后端应用,位于 `apps/backend/`
|
||||
- **Miniprogram**:微信小程序前端应用,位于 `apps/miniprogram/`
|
||||
- **TASK_1_API**:任务列表接口 `GET /api/xcx/tasks`,返回任务列表 + 绩效概览
|
||||
- **TASK_2_API**:任务详情接口 `GET /api/xcx/tasks/{taskId}`,返回单个任务的完整详情
|
||||
- **PERF_1_API**:绩效概览接口 `GET /api/xcx/performance`,返回助教当月绩效汇总
|
||||
- **PERF_2_API**:绩效明细接口 `GET /api/xcx/performance/records`,返回按日期分组的服务记录
|
||||
- **PIN_API**:任务置顶/取消置顶接口 `POST /api/xcx/tasks/{id}/pin` 和 `POST /api/xcx/tasks/{id}/unpin`
|
||||
- **PerformanceSummary**:任务列表响应中附带的绩效概览数据结构(15+ 字段)
|
||||
- **DateGroup**:按日期分组的数据结构,包含日期标签、当日汇总、记录列表
|
||||
- **FDW**:PostgreSQL Foreign Data Wrapper,用于从业务库 `zqyy_app` 访问 ETL 库 `etl_feiqiu` 的数据
|
||||
- **items_sum**:DWD-DOC 强制使用的消费金额口径,= `table_charge_money + goods_money + assistant_pd_money + assistant_cx_money + electricity_money`
|
||||
- **assistant_pd_money**:助教陪打费用(基础课),DWD-DOC 强制规则 2 要求的拆分字段
|
||||
- **assistant_cx_money**:助教超休费用(激励课),DWD-DOC 强制规则 2 要求的拆分字段
|
||||
- **enrichTask**:前端 `task-list` 页面中对原始任务数据进行扩展的函数,补充 `lastVisitDays`/`balance`/`aiSuggestion` 等字段
|
||||
- **buildPerfData**:前端 `task-list` 页面中构建绩效进度条数据的函数,消费 `PerformanceSummary` 的 15+ 字段
|
||||
- **courseTypeClass**:服务记录中课程类型的样式标识,统一使用 `basic`/`vip`/`tip` 枚举(不带 `tag-` 前缀)
|
||||
- **user_assistant_binding**:业务库中用户与助教身份的绑定关系表,用于数据隔离
|
||||
- **ai_cache**:业务库中 AI 分析结果的缓存表,按 `cache_type` 区分不同类型的 AI 输出
|
||||
|
||||
## 需求
|
||||
|
||||
### 需求 1:扩展 TASK-1 任务列表绩效概览(T1-1)
|
||||
|
||||
**用户故事:** 作为助教,我希望在任务列表页面看到完整的绩效进度条(含档位节点、基础/激励课时、升档奖金、收入趋势),以便快速了解本月绩效状态和距升档的差距。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE TASK_1_API SHALL 在响应的 `performance` 字段中返回以下扩展字段:`tierNodes`(档位节点数组,如 `[0, 100, 130, 160, 190, 220]`)、`basicHours`(基础课时)、`bonusHours`(激励课时)、`currentTier`(当前档位索引,0-based)、`nextTierHours`(下一档位工时阈值)、`tierCompleted`(是否已达最高档)、`bonusMoney`(升档奖金,元)、`incomeTrend`(收入趋势描述,如 `"↓368"`)、`incomeTrendDir`(`up` 或 `down`)、`prevMonth`(上月标签,如 `"1月"`)、`currentTierLabel`(当前档位名称,如 `"初级"`)
|
||||
2. THE TASK_1_API SHALL 从 `fdw_etl.v_dws_assistant_salary_calc` 查询当前助教的绩效和档位数据,通过 `user_assistant_binding` 获取 `assistant_id` 进行数据隔离
|
||||
3. THE TASK_1_API SHALL 使用 `items_sum` 口径计算所有收入金额(DWD-DOC 强制规则 1),使用 `assistant_pd_money`(基础课)和 `assistant_cx_money`(激励课)拆分助教费用(DWD-DOC 强制规则 2)
|
||||
4. THE TASK_1_API SHALL 通过对比当月和上月的收入数据计算 `incomeTrend` 和 `incomeTrendDir` 字段
|
||||
5. WHEN `tierCompleted` 为 `true` 时,THE TASK_1_API SHALL 将 `bonusMoney` 设为 0,`nextTierHours` 设为当前档位工时阈值
|
||||
|
||||
#### 1.2 任务项扩展字段(GAP-03)
|
||||
|
||||
6. THE TASK_1_API SHALL 为每个任务 item 返回以下可选扩展字段:`lastVisitDays`(距上次到店天数,integer)、`balance`(客户余额,number,元)、`aiSuggestion`(AI 建议摘要,string)
|
||||
7. THE TASK_1_API SHALL 从 `fdw_etl.dwd.dwd_settlement_head` 查询客户最后到店日期,计算 `lastVisitDays`(当前日期与 `MAX(settle_time)` 的天数差)
|
||||
8. THE TASK_1_API SHALL 从 `fdw_etl.dwd.dim_member_card_account` 查询客户储值卡余额作为 `balance` 字段值
|
||||
9. THE TASK_1_API SHALL 从 `biz.ai_cache` 查询 `cache_type` 对应的 AI 建议摘要作为 `aiSuggestion` 字段值
|
||||
10. IF 某个扩展字段的数据源查询失败或无数据,THEN THE TASK_1_API SHALL 对该字段返回 `null`,不影响其他字段和整体响应
|
||||
|
||||
### 需求 2:实现 TASK-2 任务详情完整版(T1-2)
|
||||
|
||||
**用户故事:** 作为助教,我希望点击任务后能看到完整的任务详情(包括服务记录、维客线索、AI 分析、备注),以便全面了解客户情况并制定跟进策略。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE TASK_2_API SHALL 返回完整的任务详情响应,包含基础信息、维客线索(`retentionClues`)、话术参考(`talkingPoints`)、服务记录摘要(`serviceSummary`)、服务记录列表(`serviceRecords`)、AI 分析(`aiAnalysis`)、备注列表(`notes`)
|
||||
2. THE TASK_2_API SHALL 在响应中包含 `customerId` 字段(客户唯一 ID),供前端跳转 chat 和 customer-service-records 页面使用
|
||||
3. WHEN 请求任务详情时,THE TASK_2_API SHALL 对服务记录(`serviceRecords`)和备注(`notes`)各返回最多 20 条,按时间倒序排列,支持前端懒加载
|
||||
|
||||
#### 2.2 维客线索格式统一(GAP-06~07)
|
||||
|
||||
4. THE TASK_2_API SHALL 返回维客线索的 `tag` 字段为纯文本字符串(不含换行符 `\n`),多行标签使用空格分隔
|
||||
5. THE TASK_2_API SHALL 返回维客线索的 `source` 字段为以下枚举值之一:`manual`(手动创建)、`ai_consumption`(AI 消费分析生成)、`ai_note`(AI 备注分析生成)
|
||||
6. THE TASK_2_API SHALL 从 `public.member_retention_clue` 查询维客线索数据,按 `created_at` 倒序排列
|
||||
|
||||
#### 2.3 AI 分析 cache_type 映射(GAP-08)
|
||||
|
||||
7. THE TASK_2_API SHALL 从 `biz.ai_cache` 查询 AI 分析数据,使用以下 `cache_type` 映射:`aiAnalysis.summary` 来自 `app4_analysis`,`talkingPoints` 来自 `app5_talking_points`
|
||||
8. IF `biz.ai_cache` 中无对应 `cache_type` 的缓存记录,THEN THE TASK_2_API SHALL 对 `aiAnalysis` 返回 `{ summary: "", suggestions: [] }`,对 `talkingPoints` 返回空数组
|
||||
|
||||
#### 2.4 服务记录字段(GAP-06)
|
||||
|
||||
9. THE TASK_2_API SHALL 为每条服务记录返回 `courseTypeClass` 字段,使用统一枚举值:`basic`(基础课)、`vip`(包厢课)、`tip`(激励课)、`recharge`(充值)、`incentive`(激励),不带 `tag-` 前缀
|
||||
10. THE TASK_2_API SHALL 为每条服务记录返回可选字段 `recordType`(`course` 或 `recharge`)和 `isEstimate`(是否预估数据,boolean)
|
||||
11. THE TASK_2_API SHALL 使用 `items_sum` 口径计算服务记录中的 `income` 字段(DWD-DOC 强制规则 1)
|
||||
12. THE TASK_2_API SHALL 使用 `dwd_assistant_service_log_ex.is_trash` 排除废单记录
|
||||
|
||||
#### 2.5 权限与数据隔离
|
||||
|
||||
13. THE TASK_2_API SHALL 验证请求的 `taskId` 属于当前登录助教(通过 `user_assistant_binding` 获取 `assistant_id`,校验 `coach_tasks.assistant_id` 匹配)
|
||||
14. IF 请求的 `taskId` 不属于当前助教,THEN THE TASK_2_API SHALL 返回 HTTP 403 `{ code: 403, message: "无权访问该任务" }`
|
||||
|
||||
### 需求 3:实现 PERF-1 绩效概览(T1-3)
|
||||
|
||||
**用户故事:** 作为助教,我希望查看本月绩效概览(包括收入明细、档位进度、服务记录按日期分组、新客和常客列表),以便了解本月工作成果和收入构成。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE PERF_1_API SHALL 接受 `year` 和 `month` 查询参数,返回指定月份的绩效概览数据
|
||||
2. THE PERF_1_API SHALL 通过 `user_assistant_binding` 获取当前助教的 `assistant_id`,仅查询该助教自己的绩效数据
|
||||
|
||||
#### 3.2 本月服务记录 DateGroup 结构(GAP-12)
|
||||
|
||||
3. THE PERF_1_API SHALL 将 `thisMonthRecords` 以 DateGroup 结构返回:每组包含 `date`(日期标签,如 `"2月7日"`)、`totalHours`(当日总工时,格式化字符串)、`totalIncome`(当日总收入,格式化字符串)、`records`(记录列表)
|
||||
4. THE PERF_1_API SHALL 为 DateGroup 中每条记录返回以下字段:`customerName`、`avatarChar`(姓氏首字)、`avatarColor`(头像渐变色)、`timeRange`(时间段,如 `"20:00-22:00"`)、`hours`(工时,格式化字符串)、`courseType`(课程类型,如 `"基础课"`)、`courseTypeClass`(样式标识:`basic`/`vip`/`tip`,不带 `tag-` 前缀)、`location`(台桌/包厢名)、`income`(收入,格式化字符串)
|
||||
5. THE PERF_1_API SHALL 从 `fdw_etl.v_dwd_assistant_service_log` 查询服务记录,按 `settle_time` 日期分组,每组内按时间倒序排列
|
||||
|
||||
#### 3.3 收入档位数据(GAP-13)
|
||||
|
||||
6. THE PERF_1_API SHALL 返回收入档位数据:`currentTier`(当前档,含 `basicRate` 和 `incentiveRate`)、`nextTier`(下一档,含 `basicRate` 和 `incentiveRate`)、`upgradeHoursNeeded`(距升档所需工时,number)、`upgradeBonus`(升档奖金,number,元)
|
||||
7. THE PERF_1_API SHALL 从 `fdw_etl.v_dws_assistant_salary_calc` 查询档位和费率数据
|
||||
|
||||
#### 3.4 上月收入与收入明细(GAP-14~15)
|
||||
|
||||
8. THE PERF_1_API SHALL 返回 `lastMonthIncome` 字段(上月收入,格式化字符串,如 `"¥16,880"`)
|
||||
9. THE PERF_1_API SHALL 为 `incomeItems` 每项返回 `desc` 字段(费率×工时的拆分描述,如 `"80元/h × 75h"`),由后端根据费率和工时数据计算生成
|
||||
10. THE PERF_1_API SHALL 使用 `items_sum` 口径计算所有收入金额(DWD-DOC 强制规则 1),使用 `assistant_pd_money` 和 `assistant_cx_money` 拆分助教费用(DWD-DOC 强制规则 2)
|
||||
|
||||
#### 3.5 新客与常客列表(GAP-16)
|
||||
|
||||
11. THE PERF_1_API SHALL 为 `newCustomers` 每项返回 `lastService`(最后服务日期,如 `"2月7日"`)和 `count`(服务次数,number)字段
|
||||
12. THE PERF_1_API SHALL 为 `regularCustomers` 每项返回 `hours`(总工时,number)和 `income`(总收入,格式化字符串)字段
|
||||
13. THE PERF_1_API SHALL 通过 `member_id` JOIN `fdw_etl.dwd.dim_member`(取 `scd2_is_current=1`)获取客户姓名(DWD-DOC 强制规则 DQ-6),禁止直接使用 `member_phone` 或 `member_name`
|
||||
|
||||
### 需求 4:实现 PERF-2 绩效明细(T1-4)
|
||||
|
||||
**用户故事:** 作为助教,我希望查看指定月份的绩效明细(按日期分组的服务记录列表),以便回顾每天的工作详情。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE PERF_2_API SHALL 接受 `year`、`month`、`page`、`pageSize` 查询参数,返回指定月份的绩效明细数据
|
||||
2. THE PERF_2_API SHALL 每页返回最多 20 条记录(默认 `pageSize=20`),按日期分组为 `dateGroups` 结构,并返回 `hasMore` 标记指示是否有更多数据
|
||||
|
||||
#### 4.2 按日期分组(GAP-19~20)
|
||||
|
||||
3. THE PERF_2_API SHALL 将服务记录按日期分组,每组包含 `date`(日期标签)、`totalHours`(当日总工时)、`totalIncome`(当日总收入)、`records`(记录列表)
|
||||
4. THE PERF_2_API SHALL 为每条记录返回 `courseTypeClass` 字段,使用统一枚举值 `basic`/`vip`/`tip`(不带 `tag-` 前缀)
|
||||
5. THE PERF_2_API SHALL 不返回 `avatarChar` 和 `avatarColor` 字段(前端通过 `nameToAvatarColor()` 工具函数从 `customerName` 自行计算)
|
||||
|
||||
#### 4.3 月度汇总
|
||||
|
||||
6. THE PERF_2_API SHALL 返回月度汇总数据 `summary`:`totalCount`(总记录数)、`totalHours`(总工时)、`totalHoursRaw`(原始工时,未折算)、`totalIncome`(总收入)
|
||||
7. THE PERF_2_API SHALL 使用 `items_sum` 口径计算 `totalIncome` 和每条记录的 `income`(DWD-DOC 强制规则 1)
|
||||
8. THE PERF_2_API SHALL 通过 `member_id` JOIN `fdw_etl.dwd.dim_member`(取 `scd2_is_current=1`)获取客户姓名(DWD-DOC 强制规则 DQ-6)
|
||||
|
||||
### 需求 5:实现 pin/unpin API 端点(T1-5)
|
||||
|
||||
**用户故事:** 作为助教,我希望将重要任务置顶或取消置顶,以便优先处理关键客户的跟进任务。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Backend SHALL 实现 `POST /api/xcx/tasks/{taskId}/pin` 端点,将指定任务的 `is_pinned` 字段设为 `true`
|
||||
2. THE Backend SHALL 实现 `POST /api/xcx/tasks/{taskId}/unpin` 端点,将指定任务的 `is_pinned` 字段设为 `false`
|
||||
3. WHEN pin 或 unpin 操作成功时,THE PIN_API SHALL 返回 `{ isPinned: true }` 或 `{ isPinned: false }`
|
||||
4. THE PIN_API SHALL 验证请求的 `taskId` 属于当前登录助教(通过 `user_assistant_binding` 校验),不属于时返回 HTTP 403
|
||||
5. IF 请求的 `taskId` 不存在,THEN THE PIN_API SHALL 返回 HTTP 404 `{ code: 404, message: "任务不存在" }`
|
||||
6. THE Miniprogram SHALL 在 `services/api.ts` 中新增 `pinTask(taskId: string)` 和 `unpinTask(taskId: string)` 函数,分别调用 pin 和 unpin 端点
|
||||
7. WHEN pin/unpin API 调用成功后,THE Miniprogram SHALL 更新本地任务列表状态(将任务移入/移出置顶分组),无需重新请求完整任务列表
|
||||
|
||||
### 需求 6:前端适配 — 任务页面(T1-6 任务部分)
|
||||
|
||||
**用户故事:** 作为助教,我希望任务列表和任务详情页面能正确展示后端返回的真实数据(替代当前的 mock 数据),以便看到真实的客户信息和绩效状态。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
#### 6.1 createNote 补充用户手动评分参数(GAP-05)
|
||||
|
||||
1. THE Miniprogram SHALL 修改 `services/api.ts` 中 `createNote` 函数的签名,增加可选参数 `manualScore?: number`(用户手动评分,1-5 星)
|
||||
2. WHEN 用户在备注弹窗中提交备注时,THE Miniprogram SHALL 将 `manualScore` 字段一并传递给 `POST /api/xcx/notes` 端点
|
||||
3. THE Backend SHALL 修改 `POST /api/xcx/notes` 端点,接受请求体中的可选 `manualScore` 字段(number,1-5),并存入 `biz.notes.score` 列。注意:此字段为用户手动评分(再次服务意愿 + 再来店可能性),与 AI 应用 6 的 `aiScore`(1-10 分,展示用)语义不同
|
||||
|
||||
#### 6.2 storageLevel/relationLevel 前端计算(GAP-11)
|
||||
|
||||
4. THE Miniprogram SHALL 在 task-detail 页面根据后端返回的 `balance` 字段值,在前端本地计算 `storageLevel`(储值等级,如 "非常多"/"较多"/"一般"/"较少")
|
||||
5. THE Miniprogram SHALL 在 task-detail 页面根据后端返回的 `heartScore` 字段值(0-10 范围),在前端本地计算 `relationLevel`、`relationLevelText`、`relationColor`,阈值遵循 P6 AC3 四级映射(>8.5 / >7 / >5 / ≤5)
|
||||
|
||||
### 需求 7:前端适配 — 绩效页面(T1-6 绩效部分)
|
||||
|
||||
**用户故事:** 作为助教,我希望绩效概览和绩效明细页面支持月份切换,以便查看历史月份的绩效数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
#### 7.1 绩效概览月份切换(F8, GAP-18)
|
||||
|
||||
1. THE Miniprogram SHALL 在 performance 页面添加月份切换控件(左右箭头 + 月份标签),允许用户在当前月和历史月份之间切换
|
||||
2. WHEN 用户切换月份时,THE Miniprogram SHALL 使用新的 `year`/`month` 参数重新调用 `fetchPerformanceOverview` 接口,加载对应月份的绩效数据
|
||||
3. THE Miniprogram SHALL 在月份切换期间显示加载状态,防止用户重复操作
|
||||
|
||||
#### 7.2 绩效明细月份切换重置分页(F9, GAP-21)
|
||||
|
||||
4. WHEN 用户在 performance-records 页面切换月份时,THE Miniprogram SHALL 将 `page` 重置为 1,清空已加载的记录列表,重新从第一页加载
|
||||
5. THE Miniprogram SHALL 修复 `switchMonth()` 函数中 `page` 未重置的 Bug
|
||||
|
||||
#### 7.3 avatarChar/avatarColor 前端计算(GAP-19 决策)
|
||||
|
||||
6. THE Miniprogram SHALL 在 performance 和 performance-records 页面,使用 `nameToAvatarColor()` 工具函数从 `customerName` 计算 `avatarChar`(姓氏首字)和 `avatarColor`(头像渐变色),不依赖后端返回这两个字段
|
||||
|
||||
### 需求 8:全局约束与数据隔离
|
||||
|
||||
**用户故事:** 作为系统管理员,我希望所有任务和绩效接口都遵循统一的权限控制和数据隔离规则,以确保每位助教只能访问自己的数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Backend SHALL 对所有 RNS1.1 接口(TASK-1、TASK-2、PERF-1、PERF-2、PIN/UNPIN)执行 `require_approved()` 权限检查,确保用户状态为 `approved`
|
||||
2. THE Backend SHALL 对所有 RNS1.1 接口验证用户具有 `view_tasks` 权限
|
||||
3. THE Backend SHALL 通过 `user_assistant_binding` 表获取当前用户对应的 `assistant_id`,所有数据查询均以该 `assistant_id` 作为过滤条件
|
||||
4. IF 当前用户在 `user_assistant_binding` 中无绑定记录,THEN THE Backend SHALL 返回 HTTP 403 `{ code: 403, message: "未绑定助教身份" }`
|
||||
5. THE Backend SHALL 对所有涉及金额的字段统一使用 `items_sum` 口径(DWD-DOC 强制规则 1),禁止使用 `consume_money`
|
||||
6. THE Backend SHALL 对所有涉及助教费用的字段使用 `assistant_pd_money`(陪打/基础课)+ `assistant_cx_money`(超休/激励课)拆分(DWD-DOC 强制规则 2),禁止使用 `service_fee`
|
||||
7. THE Backend SHALL 对所有涉及会员信息的查询通过 `member_id` LEFT JOIN `fdw_etl.dwd.dim_member`(取 `scd2_is_current=1`)获取姓名和手机号(DWD-DOC 强制规则 DQ-6),禁止直接使用 `settlement_head.member_phone` 或 `member_name`
|
||||
8. THE Backend SHALL 使用 `dwd_assistant_service_log_ex.is_trash` 排除废单记录,禁止使用已废弃的 `dwd_assistant_trash_event` 表
|
||||
168
.kiro/specs/rns1-task-performance-api/tasks.md
Normal file
168
.kiro/specs/rns1-task-performance-api/tasks.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Implementation Plan: RNS1.1 任务与绩效接口
|
||||
|
||||
## Overview
|
||||
|
||||
基于 design.md 的 7 个组件结构,增量扩展现有后端路由和服务层,新增 FDW 查询封装、绩效服务、Pydantic Schema,并完成前端适配。所有 FDW 查询集中在 `fdw_queries.py`,服务层分为 `task_manager.py`(扩展)和 `performance_service.py`(新增)。
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] 1. Pydantic Schema 定义(组件 6)
|
||||
- [x] 1.1 扩展 `apps/backend/app/schemas/xcx_tasks.py`,新增 `PerformanceSummary`(15+ 字段:tierNodes, basicHours, bonusHours, currentTier, nextTierHours, tierCompleted, bonusMoney, incomeTrend, incomeTrendDir, prevMonth, currentTierLabel 等)、`TaskItem`(扩展版,含 lastVisitDays, balance, aiSuggestion 可选字段)、`TaskListResponse`(items + total + page + pageSize + performance)
|
||||
- 新增 `RetentionClue`、`ServiceRecord`(含 courseTypeClass 枚举)、`AiAnalysis`、`NoteItem`(含 score 可选字段)、`ServiceSummary`、`TaskDetailResponse` 模型
|
||||
- _Requirements: 1.1, 1.6, 2.1, 2.2, 2.4, 2.9_
|
||||
- [x] 1.2 新建 `apps/backend/app/schemas/xcx_performance.py`,定义 `DateGroupRecord`(含可选 avatarChar/avatarColor)、`DateGroup`、`TierInfo`、`IncomeItem`(含 desc)、`CustomerSummary`、`NewCustomer`、`RegularCustomer`、`PerformanceOverviewResponse` 模型
|
||||
- 定义 `RecordsSummary`(totalCount, totalHours, totalHoursRaw, totalIncome)、`PerformanceRecordsResponse`(summary + dateGroups + hasMore)模型
|
||||
- _Requirements: 3.1, 3.3, 3.4, 3.5, 4.1, 4.2, 4.3_
|
||||
|
||||
- [x] 2. FDW 查询封装服务(组件 3 — 新增 fdw_queries.py)
|
||||
- [x] 2.1 新建 `apps/backend/app/services/fdw_queries.py`,实现 `_fdw_context(conn, site_id)` 上下文管理器(BEGIN + SET LOCAL app.current_site_id)和 `get_member_info(conn, site_id, member_ids)` 批量查询会员信息
|
||||
- ⚠️ DQ-6:通过 member_id JOIN fdw_etl.v_dim_member,取 scd2_is_current=1
|
||||
- _Requirements: 8.7_
|
||||
- [x] 2.2 实现 `get_member_balance(conn, site_id, member_ids)` 批量查询会员储值卡余额
|
||||
- ⚠️ DQ-7:通过 member_id JOIN fdw_etl.v_dim_member_card_account,取 scd2_is_current=1
|
||||
- _Requirements: 1.8, 8.7_
|
||||
- [x] 2.3 实现 `get_last_visit_days(conn, site_id, member_ids)` 批量查询客户距上次到店天数
|
||||
- 来源:fdw_etl.v_dwd_assistant_service_log,WHERE is_trash = false
|
||||
- _Requirements: 1.7_
|
||||
- [x] 2.4 实现 `get_salary_calc(conn, site_id, assistant_id, year, month)` 查询助教绩效/档位/收入数据
|
||||
- ⚠️ DWD-DOC 规则 1:收入使用 items_sum 口径
|
||||
- ⚠️ DWD-DOC 规则 2:费用使用 assistant_pd_money + assistant_cx_money
|
||||
- _Requirements: 1.2, 1.3, 3.7, 8.5, 8.6_
|
||||
- [x] 2.5 实现 `get_service_records(conn, site_id, assistant_id, year, month, limit, offset)` 查询助教服务记录明细
|
||||
- ⚠️ 废单排除:WHERE is_trash = false;DQ-6:客户姓名通过 member_id JOIN dim_member
|
||||
- _Requirements: 2.11, 2.12, 3.5, 4.7, 4.8, 8.8_
|
||||
- [x] 2.6 实现 `get_service_records_for_task(conn, site_id, assistant_id, member_id, limit)` 查询特定客户的服务记录(TASK-2 用)
|
||||
- _Requirements: 2.1, 2.3_
|
||||
|
||||
|
||||
- [x] 3. Checkpoint — Schema 与 FDW 查询层验证
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
- [x] 4. task_manager 服务扩展(组件 4 — 扩展现有 task_manager.py)
|
||||
- [x] 4.1 在 `apps/backend/app/services/task_manager.py` 中实现 `get_task_list_v2(user_id, site_id, status, page, page_size)`
|
||||
- 逻辑:_get_assistant_id() → 查询 coach_tasks 带分页 → fdw_queries 批量获取会员信息/余额/lastVisitDays → fdw_queries.get_salary_calc() 获取绩效概览 → 查询 ai_cache 获取 aiSuggestion → 组装 TaskListResponse
|
||||
- 扩展字段(lastVisitDays/balance/aiSuggestion)采用优雅降级:单个查询失败返回 null,不影响整体响应
|
||||
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 1.10_
|
||||
- [x] 4.2 在 `apps/backend/app/services/task_manager.py` 中实现 `get_task_detail(task_id, user_id, site_id)`
|
||||
- 逻辑:_get_assistant_id() + _verify_task_ownership() 权限校验 → 查询 coach_tasks 基础信息 → 查询 member_retention_clue 维客线索 → 查询 ai_cache(app5_talking_points → talkingPoints, app4_analysis → aiAnalysis)→ fdw_queries.get_service_records_for_task() 服务记录(最多 20 条)→ 查询 notes(最多 20 条)→ 组装 TaskDetailResponse
|
||||
- tag 字段净化:去除换行符 \n,多行标签使用空格分隔
|
||||
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10, 2.11, 2.12, 2.13, 2.14_
|
||||
- [x] 4.3 Write property test: 收入趋势计算正确性
|
||||
- **Property 1: 收入趋势计算正确性**
|
||||
- 生成器:st.floats(min_value=0, max_value=1e6) × 2
|
||||
- 验证:incomeTrendDir 方向正确、差值正确、前缀符号一致
|
||||
- **Validates: Requirements 1.4**
|
||||
- [x] 4.4 Write property test: lastVisitDays 计算正确性
|
||||
- **Property 2: lastVisitDays 计算正确性**
|
||||
- 生成器:st.dates(max_value=date.today())
|
||||
- 验证:天数差 = (today - date).days,非负
|
||||
- **Validates: Requirements 1.7**
|
||||
- [x] 4.5 Write property test: 维客线索 tag 净化与 source 枚举
|
||||
- **Property 3: 维客线索 tag 净化与 source 枚举**
|
||||
- 生成器:st.text() 生成含 \n 的 tag + st.sampled_from source
|
||||
- 验证:tag 无换行、source 在枚举内
|
||||
- **Validates: Requirements 2.4, 2.5**
|
||||
- [x] 4.6 Write property test: courseTypeClass 枚举映射
|
||||
- **Property 4: courseTypeClass 枚举映射**
|
||||
- 生成器:st.sampled_from(ALL_COURSE_TYPES)
|
||||
- 验证:结果在 {basic, vip, tip, recharge, incentive} 内、无 tag- 前缀
|
||||
- **Validates: Requirements 2.9, 4.4**
|
||||
|
||||
- [x] 5. performance_service 服务(组件 5 — 新增 performance_service.py)
|
||||
- [x] 5.1 新建 `apps/backend/app/services/performance_service.py`,实现 `get_overview(user_id, site_id, year, month)`
|
||||
- 逻辑:获取 assistant_id → fdw_queries.get_salary_calc() 档位/收入/费率 → fdw_queries.get_service_records() 按日期分组为 DateGroup → 聚合新客/常客列表 → 计算 incomeItems(含 desc 费率描述)→ 查询上月收入 lastMonthIncome
|
||||
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13_
|
||||
- [x] 5.2 实现 `get_records(user_id, site_id, year, month, page, page_size)`
|
||||
- 逻辑:获取 assistant_id → fdw_queries.get_service_records() 带分页 → 按日期分组为 dateGroups → 计算 summary 汇总 → 返回 { summary, dateGroups, hasMore }
|
||||
- PERF-2 不返回 avatarChar/avatarColor(前端自行计算)
|
||||
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8_
|
||||
- [x] 5.3 Write property test: 分页与排序
|
||||
- **Property 5: 列表分页与排序**
|
||||
- 生成器:st.lists(st.fixed_dictionaries(...)) + st.integers(1,10) page/pageSize
|
||||
- 验证:记录数 ≤ pageSize、hasMore 正确、排序正确
|
||||
- **Validates: Requirements 2.3, 4.2**
|
||||
- [x] 5.4 Write property test: DateGroup 分组正确性
|
||||
- **Property 6: DateGroup 分组正确性**
|
||||
- 生成器:st.lists(st.fixed_dictionaries({date, hours, income}))
|
||||
- 验证:日期唯一、组内日期一致、汇总正确、按日期倒序
|
||||
- **Validates: Requirements 3.3, 4.3**
|
||||
- [x] 5.5 Write property test: incomeItems desc 格式化
|
||||
- **Property 7: incomeItems desc 格式化**
|
||||
- 生成器:st.floats(min_value=0.01) rate × st.floats(min_value=0) hours
|
||||
- 验证:desc 包含费率值和工时值,格式为 "{rate}元/h × {hours}h"
|
||||
- **Validates: Requirements 3.9**
|
||||
- [x] 5.6 Write property test: 月度汇总聚合正确性
|
||||
- **Property 8: 月度汇总聚合正确性**
|
||||
- 生成器:st.lists(st.fixed_dictionaries({hours, hours_raw, income}))
|
||||
- 验证:totalCount/totalHours/totalHoursRaw/totalIncome 聚合正确
|
||||
- **Validates: Requirements 4.6**
|
||||
|
||||
- [x] 6. Checkpoint — 服务层验证
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
- [x] 7. xcx_tasks Router 扩展(组件 1)
|
||||
- [x] 7.1 修改 `apps/backend/app/routers/xcx_tasks.py`:将 `GET /api/xcx/tasks` 响应从 `list[TaskListItem]` 改为 `TaskListResponse`(含 items + performance + 分页),新增 status/page/pageSize 查询参数,调用 `task_manager.get_task_list_v2()`
|
||||
- _Requirements: 1.1, 1.2_
|
||||
- [x] 7.2 在 `routers/xcx_tasks.py` 中新增 `GET /api/xcx/tasks/{task_id}` 端点,调用 `task_manager.get_task_detail()`,含 require_approved() 权限校验
|
||||
- _Requirements: 2.1, 2.13, 2.14_
|
||||
- [x] 7.3 修改 `routers/xcx_tasks.py` 中 `POST .../pin` 和 `POST .../unpin` 端点,响应对齐契约格式 `{ isPinned: bool }`
|
||||
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5_
|
||||
- [x] 7.4 修改 `POST /api/xcx/notes` 端点(`xcx_notes.py`),接受请求体中的可选 `score` 字段(number, 1-5),存入 `biz.coach_notes.score` 列;超出 1-5 范围返回 422
|
||||
- _Requirements: 6.3_
|
||||
- [x] 7.5 Write property test: Pin/Unpin 状态往返
|
||||
- **Property 9: Pin/Unpin 状态往返**
|
||||
- 生成器:st.booleans() 初始状态
|
||||
- 验证:pin→true、unpin→false、往返恢复
|
||||
- **Validates: Requirements 5.1, 5.2, 5.3**
|
||||
- [x] 7.6 Write property test: 权限与数据隔离
|
||||
- **Property 10: 权限与数据隔离**
|
||||
- 生成器:st.sampled_from(INVALID_USER_SCENARIOS)
|
||||
- 验证:未审核/无绑定/无权限/非本人任务 → 所有端点返回 403
|
||||
- **Validates: Requirements 2.13, 8.1, 8.2, 8.4**
|
||||
- [x] 7.7 Write property test: 备注 score 输入验证
|
||||
- **Property 12: 备注 score 输入验证**
|
||||
- 生成器:st.integers()
|
||||
- 验证:1-5 接受、超范围拒绝 422、null 接受
|
||||
- **Validates: Requirements 6.3**
|
||||
|
||||
- [x] 8. xcx_performance Router(组件 2 — 新增)
|
||||
- [x] 8.1 新建 `apps/backend/app/routers/xcx_performance.py`,实现 `GET /api/xcx/performance` 端点(接受 year/month 参数),调用 `performance_service.get_overview()`,含 require_approved() 权限校验
|
||||
- _Requirements: 3.1, 3.2_
|
||||
- [x] 8.2 在 `routers/xcx_performance.py` 中实现 `GET /api/xcx/performance/records` 端点(接受 year/month/page/pageSize 参数),调用 `performance_service.get_records()`
|
||||
- _Requirements: 4.1, 4.2_
|
||||
- [x] 8.3 修改 `apps/backend/app/main.py`,导入并注册 `xcx_performance.router`
|
||||
- _Requirements: 3.1, 4.1_
|
||||
|
||||
- [x] 9. Checkpoint — 后端路由层验证
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
- [x] 10. 前端适配(组件 7)
|
||||
- [x] 10.1 修改 `apps/miniprogram/miniprogram/services/api.ts`:新增 `pinTask()`、`unpinTask()`、`fetchTaskDetail()`、`fetchPerformanceOverview(year, month)`、`fetchPerformanceRecords(year, month, page)` 函数;`createNote()` 增加可选 `score` 参数
|
||||
- _Requirements: 5.6, 6.1, 6.2, 7.1_
|
||||
- [x] 10.2 修改 `apps/miniprogram/miniprogram/pages/task-list/task-list.ts`:消费 `TaskListResponse` 新结构(items + performance),`buildPerfData()` 使用 15+ 字段绩效数据,移除 mock 数据
|
||||
- _Requirements: 6.4, 6.5_
|
||||
- [x] 10.3 修改 task-detail 页面:调用 `fetchTaskDetail()`;根据 `balance` 前端本地计算 `storageLevel`,根据 `heartScore` 计算 `relationLevel`/`relationLevelText`/`relationColor`
|
||||
- _Requirements: 6.4, 6.5_
|
||||
- [x] 10.4 修改 `apps/miniprogram/miniprogram/pages/performance/performance.ts`:添加月份切换控件(左右箭头 + 月份标签),实现 `switchMonth()` 调用 `fetchPerformanceOverview`,含加载状态管理
|
||||
- _Requirements: 7.1, 7.2, 7.3_
|
||||
- [x] 10.5 修改 `apps/miniprogram/miniprogram/pages/performance-records/performance-records.ts`:修复 `switchMonth()` 中 page 未重置的 Bug,切换月份时清空记录列表并重新加载(page 重置为 1)
|
||||
- _Requirements: 7.4, 7.5_
|
||||
- [x] 10.6 在 performance 和 performance-records 页面中,使用 `nameToAvatarColor()` 从 `customerName` 计算 `avatarChar`/`avatarColor`,不依赖后端
|
||||
- _Requirements: 7.6_
|
||||
- [x] 10.7 Write property test: 前端派生字段计算
|
||||
- **Property 11: 前端派生字段计算**
|
||||
- 生成器:st.text(min_size=1) name + st.floats(0, 1e6) balance + st.floats(0, 10) heartScore
|
||||
- 验证:avatarChar=name[0]、确定性、storageLevel/relationLevel 单调性
|
||||
- **Validates: Requirements 6.4, 6.5, 7.6**
|
||||
|
||||
- [x] 11. Final checkpoint — 全量验证
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
## Notes
|
||||
|
||||
- Tasks marked with `*` are optional and can be skipped for faster MVP
|
||||
- 组件 3(fdw_queries.py)是所有 FDW 查询的集中封装,组件 4/5 通过调用它访问 ETL 数据
|
||||
- 组件 4 扩展现有 task_manager.py(不新建 task_perf_service.py),组件 5 新建 performance_service.py
|
||||
- Schema 文件命名:`xcx_tasks.py`(扩展)+ `xcx_performance.py`(新增),与 design.md 一致
|
||||
- 所有 12 个正确性属性(P1-P12)均有对应的属性测试任务
|
||||
- DWD-DOC 强制规则在 fdw_queries.py 中统一实施:items_sum 口径、费用拆分、DQ-6/DQ-7 JOIN、废单排除
|
||||
Reference in New Issue
Block a user