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-board-apis/.config.kiro
Normal file
1
.kiro/specs/rns1-board-apis/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"specId": "a3f7c2d1-8e4b-4f6a-9c5d-2b1e8f3a7d9c", "workflowType": "requirements-first", "specType": "feature"}
|
||||
840
.kiro/specs/rns1-board-apis/design.md
Normal file
840
.kiro/specs/rns1-board-apis/design.md
Normal file
@@ -0,0 +1,840 @@
|
||||
# 设计文档 — RNS1.3:三看板接口
|
||||
|
||||
## 概述
|
||||
|
||||
本设计覆盖 RNS1.3 的全部后端接口(BOARD-1/2/3、CONFIG-1)和前端筛选修复。核心挑战在于 BOARD-3 财务看板的复杂度(6 板块、200+ 字段、60+ 环比数据点)以及跨多个 ETL RLS 视图的数据聚合。
|
||||
|
||||
设计遵循已有架构模式:
|
||||
- 路由层(`routers/`):参数校验 + 权限检查,委托 service 层
|
||||
- 服务层(`services/`):业务逻辑编排,调用 `fdw_queries` 查询 ETL 数据
|
||||
- Schema 层(`schemas/`):`CamelModel` 基类,自动 camelCase 转换
|
||||
- 中间件:`ResponseWrapperMiddleware` 统一包装 `{ code: 0, data: ... }`
|
||||
- FDW 查询:`_fdw_context` 上下文管理器,直连 ETL 库 + `SET LOCAL app.current_site_id`
|
||||
|
||||
### 设计决策
|
||||
|
||||
1. **BOARD-1 扁平返回 vs 按维度返回**:选择扁平返回(所有维度字段一次性返回),前端切换维度无需重新请求,减少网络往返。代价是单次响应略大,但助教数量有限(通常 < 50),可接受。
|
||||
2. **BOARD-2 按维度返回**:选择按 `dimension` 参数仅返回对应维度字段。客户数量可达数千,分页 + 维度专属字段可显著减少传输量和查询开销。
|
||||
3. **BOARD-3 单接口 6 板块**:单个 `GET /api/xcx/board/finance` 返回全部 6 个板块。各板块独立查询、独立降级,某板块失败不影响其他板块。`recharge` 板块在 `area≠all` 时返回 `null`。
|
||||
4. **环比计算后端统一处理**:`compare=1` 时后端计算所有环比字段,`compare=0` 时完全不返回环比字段(非返回 null,而是字段不存在),减少 JSON 体积。
|
||||
5. **FDW 查询集中封装**:所有新增 ETL 查询函数统一添加到 `fdw_queries.py`,保持 DWD-DOC 规则在单一模块实施。
|
||||
|
||||
## 架构
|
||||
|
||||
### 请求流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant MP as 小程序
|
||||
participant MW as ResponseWrapperMiddleware
|
||||
participant R as Router (xcx_board / xcx_config)
|
||||
participant P as Permission (require_permission)
|
||||
participant S as Service (board_service / config_service)
|
||||
participant FDW as fdw_queries (_fdw_context)
|
||||
participant ETL as ETL DB (app.v_*)
|
||||
participant BIZ as App DB (biz.*)
|
||||
|
||||
MP->>MW: GET /api/xcx/board/coaches?sort=perf_desc&skill=all&time=month
|
||||
MW->>R: 透传请求
|
||||
R->>P: require_permission("view_board_coach")
|
||||
P-->>R: CurrentUser (site_id)
|
||||
R->>S: get_coach_board(params, site_id)
|
||||
S->>FDW: 查询助教列表 + 绩效 + 客户
|
||||
FDW->>ETL: SET LOCAL app.current_site_id; SELECT FROM app.v_*
|
||||
ETL-->>FDW: 结果集
|
||||
S->>BIZ: 查询任务完成数 (biz.coach_tasks)
|
||||
BIZ-->>S: 结果集
|
||||
S-->>R: 组装响应 dict
|
||||
R-->>MW: JSON 响应
|
||||
MW-->>MP: { code: 0, data: {...} }
|
||||
```
|
||||
|
||||
### 模块结构
|
||||
|
||||
```
|
||||
apps/backend/app/
|
||||
├── routers/
|
||||
│ ├── xcx_board.py # 新增:BOARD-1/2/3 三个看板端点
|
||||
│ └── xcx_config.py # 新增:CONFIG-1 技能类型端点
|
||||
├── schemas/
|
||||
│ ├── xcx_board.py # 新增:三看板请求参数 + 响应 schema
|
||||
│ └── xcx_config.py # 新增:技能类型响应 schema
|
||||
├── services/
|
||||
│ ├── board_service.py # 新增:看板业务逻辑(3 个看板的编排函数)
|
||||
│ └── fdw_queries.py # 扩展:新增看板相关 FDW 查询函数
|
||||
└── main.py # 扩展:注册 xcx_board / xcx_config 路由
|
||||
|
||||
apps/miniprogram/miniprogram/
|
||||
├── pages/board-coach/ # 修改:筛选事件 → loadData()
|
||||
├── pages/board-customer/ # 修改:筛选事件 → loadData() + 分页
|
||||
├── pages/board-finance/ # 修改:筛选事件 → loadData() + 环比开关
|
||||
└── services/api.ts # 修改:函数签名扩展
|
||||
```
|
||||
|
||||
## 组件与接口
|
||||
|
||||
### 3.1 路由层 — `xcx_board.py`
|
||||
|
||||
三个端点共用一个路由文件,前缀 `/api/xcx/board`。
|
||||
|
||||
```python
|
||||
# GET /api/xcx/board/coaches
|
||||
@router.get("/coaches", response_model=CoachBoardResponse)
|
||||
async def get_coach_board(
|
||||
sort: CoachSortEnum = Query(default="perf_desc"),
|
||||
skill: SkillFilterEnum = Query(default="all"),
|
||||
time: BoardTimeEnum = Query(default="month"),
|
||||
user: CurrentUser = Depends(require_permission("view_board_coach")),
|
||||
): ...
|
||||
|
||||
# GET /api/xcx/board/customers
|
||||
@router.get("/customers", response_model=CustomerBoardResponse)
|
||||
async def get_customer_board(
|
||||
dimension: CustomerDimensionEnum = Query(default="recall"),
|
||||
project: ProjectFilterEnum = Query(default="all"),
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
user: CurrentUser = Depends(require_permission("view_board_customer")),
|
||||
): ...
|
||||
|
||||
# GET /api/xcx/board/finance
|
||||
@router.get("/finance", response_model=FinanceBoardResponse)
|
||||
async def get_finance_board(
|
||||
time: FinanceTimeEnum = Query(default="month"),
|
||||
area: AreaFilterEnum = Query(default="all"),
|
||||
compare: int = Query(default=0, ge=0, le=1),
|
||||
user: CurrentUser = Depends(require_permission("view_board_finance")),
|
||||
): ...
|
||||
```
|
||||
|
||||
### 3.2 路由层 — `xcx_config.py`
|
||||
|
||||
```python
|
||||
# GET /api/xcx/config/skill-types
|
||||
@router.get("/skill-types", response_model=list[SkillTypeItem])
|
||||
async def get_skill_types(
|
||||
user: CurrentUser = Depends(require_approved()),
|
||||
): ...
|
||||
```
|
||||
|
||||
### 3.3 服务层 — `board_service.py`
|
||||
|
||||
三个核心编排函数,各自独立处理参数解析、日期范围计算、数据查询和响应组装:
|
||||
|
||||
```python
|
||||
async def get_coach_board(sort, skill, time, site_id) -> dict:
|
||||
"""BOARD-1:助教看板。扁平返回所有维度字段。"""
|
||||
|
||||
async def get_customer_board(dimension, project, page, page_size, site_id) -> dict:
|
||||
"""BOARD-2:客户看板。按维度返回专属字段 + 分页。"""
|
||||
|
||||
async def get_finance_board(time, area, compare, site_id) -> dict:
|
||||
"""BOARD-3:财务看板。6 板块独立查询、独立降级。"""
|
||||
```
|
||||
|
||||
### 3.4 FDW 查询层扩展 — `fdw_queries.py`
|
||||
|
||||
新增函数(遵循已有 `_fdw_context` 模式):
|
||||
|
||||
| 函数 | 数据源视图 | 用途 |
|
||||
|------|-----------|------|
|
||||
| `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_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 维度 |
|
||||
| `get_customer_board_recharge` | `v_dwd_recharge_order` + `v_dim_member_card_account` | BOARD-2 recharge 维度(充值记录 + 当前余额) |
|
||||
| `get_customer_board_recent` | `v_dws_member_visit_detail` + `v_dim_member` | BOARD-2 recent 维度(ETL 已计算到店明细) |
|
||||
| `get_customer_board_spend60` | `v_dws_member_consumption_summary` | BOARD-2 spend60 维度(items_sum_60d 已在汇总表中) |
|
||||
| `get_customer_board_freq60` | `v_dws_member_consumption_summary` | BOARD-2 freq60 维度(visit_count_60d 已在汇总表中) |
|
||||
| `get_customer_board_loyal` | `v_dws_member_assistant_relation_index` | BOARD-2 loyal 维度 |
|
||||
| `get_finance_overview` | `v_dws_finance_daily_summary` | BOARD-3 经营一览(按日期范围聚合财务日报) |
|
||||
| `get_finance_recharge` | `v_dws_finance_recharge_summary` | BOARD-3 预收资产 |
|
||||
| `get_finance_revenue` | `v_dws_finance_income_structure` + `v_dws_finance_discount_detail` | BOARD-3 应计收入 |
|
||||
| `get_finance_cashflow` | `v_dws_finance_daily_summary` | BOARD-3 现金流入(复用财务日报中的收款字段) |
|
||||
| `get_finance_expense` | `v_dws_finance_expense_summary` + `v_dws_platform_settlement` | BOARD-3 现金流出 |
|
||||
| `get_finance_coach_analysis` | `v_dws_assistant_salary_calc` | BOARD-3 助教分析 |
|
||||
| `get_skill_types` | ETL cfg 表 | CONFIG-1 技能类型 |
|
||||
|
||||
### 3.5 日期范围计算
|
||||
|
||||
统一工具函数 `_calc_date_range(time_enum) -> (start_date, end_date)`,返回 `date` 对象:
|
||||
|
||||
| time 参数 | 当期范围 | 上期范围(compare=1 时) |
|
||||
|-----------|---------|------------------------|
|
||||
| `month` | 当月 1 日 ~ 末日 | 上月 1 日 ~ 末日 |
|
||||
| `lastMonth` / `last_month` | 上月 1 日 ~ 末日 | 上上月 |
|
||||
| `week` | 本周一 ~ 本周日 | 上周一 ~ 上周日 |
|
||||
| `lastWeek` | 上周一 ~ 上周日 | 上上周 |
|
||||
| `quarter` | 本季度首日 ~ 末日 | 上季度 |
|
||||
| `lastQuarter` / `last_quarter` | 上季度 | 上上季度 |
|
||||
| `quarter3` / `last_3m` | 前 3 个月(不含本月) | 再前 3 个月 |
|
||||
| `half6` / `last_6m` | 前 6 个月(不含本月) | 再前 6 个月 |
|
||||
|
||||
### 3.6 环比计算工具
|
||||
|
||||
```python
|
||||
def calc_compare(current: Decimal, previous: Decimal) -> dict:
|
||||
"""
|
||||
统一环比计算。
|
||||
|
||||
返回:
|
||||
- compare: str — "12.5%" / "新增" / "持平"
|
||||
- is_down: bool — 是否下降
|
||||
- is_flat: bool — 是否持平
|
||||
|
||||
规则:
|
||||
- previous=0, current≠0 → "新增", is_down=False, is_flat=False
|
||||
- previous=0, current=0 → "持平", is_down=False, is_flat=True
|
||||
- 正常计算: (current - previous) / previous × 100%
|
||||
- 正值 → is_down=False; 负值 → is_down=True; 零 → is_flat=True
|
||||
"""
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 4.1 请求参数枚举
|
||||
|
||||
```python
|
||||
# BOARD-1 排序维度
|
||||
class CoachSortEnum(str, Enum):
|
||||
perf_desc = "perf_desc"
|
||||
perf_asc = "perf_asc"
|
||||
salary_desc = "salary_desc"
|
||||
salary_asc = "salary_asc"
|
||||
sv_desc = "sv_desc"
|
||||
task_desc = "task_desc"
|
||||
|
||||
# BOARD-1 技能筛选
|
||||
class SkillFilterEnum(str, Enum):
|
||||
all = "all"
|
||||
chinese = "chinese"
|
||||
snooker = "snooker"
|
||||
mahjong = "mahjong"
|
||||
karaoke = "karaoke"
|
||||
|
||||
# BOARD-1 时间范围
|
||||
class BoardTimeEnum(str, Enum):
|
||||
month = "month"
|
||||
quarter = "quarter"
|
||||
last_month = "last_month"
|
||||
last_3m = "last_3m"
|
||||
last_quarter = "last_quarter"
|
||||
last_6m = "last_6m"
|
||||
|
||||
# BOARD-2 客户维度
|
||||
class CustomerDimensionEnum(str, Enum):
|
||||
recall = "recall"
|
||||
potential = "potential"
|
||||
balance = "balance"
|
||||
recharge = "recharge"
|
||||
recent = "recent"
|
||||
spend60 = "spend60"
|
||||
freq60 = "freq60"
|
||||
loyal = "loyal"
|
||||
|
||||
# BOARD-2 项目筛选
|
||||
class ProjectFilterEnum(str, Enum):
|
||||
all = "all"
|
||||
chinese = "chinese"
|
||||
snooker = "snooker"
|
||||
mahjong = "mahjong"
|
||||
karaoke = "karaoke"
|
||||
|
||||
# BOARD-3 时间范围
|
||||
class FinanceTimeEnum(str, Enum):
|
||||
month = "month"
|
||||
lastMonth = "lastMonth"
|
||||
week = "week"
|
||||
lastWeek = "lastWeek"
|
||||
quarter3 = "quarter3"
|
||||
quarter = "quarter"
|
||||
lastQuarter = "lastQuarter"
|
||||
half6 = "half6"
|
||||
|
||||
# BOARD-3 区域筛选
|
||||
class AreaFilterEnum(str, Enum):
|
||||
all = "all"
|
||||
hall = "hall"
|
||||
hallA = "hallA"
|
||||
hallB = "hallB"
|
||||
hallC = "hallC"
|
||||
mahjong = "mahjong"
|
||||
teamBuilding = "teamBuilding"
|
||||
```
|
||||
|
||||
### 4.2 BOARD-1 响应 Schema
|
||||
|
||||
```python
|
||||
class CoachSkillItem(CamelModel):
|
||||
text: str
|
||||
cls: str
|
||||
|
||||
class CoachBoardItem(CamelModel):
|
||||
"""助教看板单条记录(扁平结构,包含所有维度字段)。"""
|
||||
# 基础字段(所有维度共享)
|
||||
id: int
|
||||
name: str
|
||||
initial: str
|
||||
avatar_gradient: str
|
||||
level: str # star/senior/middle/junior
|
||||
skills: list[CoachSkillItem]
|
||||
top_customers: list[str] # ["💖 王先生", "💛 李女士"]
|
||||
|
||||
# perf 维度
|
||||
perf_hours: float = 0.0
|
||||
perf_hours_before: float | None = None
|
||||
perf_gap: str | None = None # "距升档 13.8h" 或 None
|
||||
perf_reached: bool = False
|
||||
|
||||
# salary 维度
|
||||
salary: float = 0.0
|
||||
salary_perf_hours: float = 0.0
|
||||
salary_perf_before: float | None = None
|
||||
|
||||
# sv 维度
|
||||
sv_amount: float = 0.0
|
||||
sv_customer_count: int = 0
|
||||
sv_consume: float = 0.0
|
||||
|
||||
# task 维度
|
||||
task_recall: int = 0
|
||||
task_callback: int = 0
|
||||
|
||||
class CoachBoardResponse(CamelModel):
|
||||
items: list[CoachBoardItem]
|
||||
dim_type: str # perf/salary/sv/task
|
||||
```
|
||||
|
||||
### 4.3 BOARD-2 响应 Schema
|
||||
|
||||
```python
|
||||
class CustomerAssistant(CamelModel):
|
||||
name: str
|
||||
cls: str
|
||||
heart_score: float
|
||||
badge: str | None = None
|
||||
badge_cls: str | None = None
|
||||
|
||||
class CustomerBoardItemBase(CamelModel):
|
||||
"""客户看板基础字段(所有维度共享)。"""
|
||||
id: int
|
||||
name: str
|
||||
initial: str
|
||||
avatar_cls: str
|
||||
assistants: list[CustomerAssistant]
|
||||
|
||||
# 各维度专属字段通过继承扩展
|
||||
class RecallItem(CustomerBoardItemBase):
|
||||
ideal_days: int
|
||||
elapsed_days: int
|
||||
overdue_days: int
|
||||
visits_30d: int
|
||||
balance: str
|
||||
recall_index: float
|
||||
|
||||
class PotentialTag(CamelModel):
|
||||
text: str
|
||||
theme: str
|
||||
|
||||
class PotentialItem(CustomerBoardItemBase):
|
||||
potential_tags: list[PotentialTag]
|
||||
spend_30d: float
|
||||
avg_visits: float
|
||||
avg_spend: float
|
||||
|
||||
class BalanceItem(CustomerBoardItemBase):
|
||||
balance: str
|
||||
last_visit: str # "3天前"
|
||||
monthly_consume: float
|
||||
available_months: str # "约0.8个月"
|
||||
|
||||
class RechargeItem(CustomerBoardItemBase):
|
||||
last_recharge: str
|
||||
recharge_amount: float
|
||||
recharges_60d: int
|
||||
current_balance: str
|
||||
|
||||
class RecentItem(CustomerBoardItemBase):
|
||||
days_ago: int
|
||||
visit_freq: str # "6.2次/月"
|
||||
ideal_days: int
|
||||
visits_30d: int
|
||||
avg_spend: float
|
||||
|
||||
class Spend60Item(CustomerBoardItemBase):
|
||||
spend_60d: float
|
||||
visits_60d: int
|
||||
high_spend_tag: bool
|
||||
avg_spend: float
|
||||
|
||||
class WeeklyVisit(CamelModel):
|
||||
val: int
|
||||
pct: int # 0-100
|
||||
|
||||
class Freq60Item(CustomerBoardItemBase):
|
||||
visits_60d: int
|
||||
avg_interval: str # "5.0天"
|
||||
weekly_visits: list[WeeklyVisit] # 固定长度 8
|
||||
spend_60d: float
|
||||
|
||||
class CoachDetail(CamelModel):
|
||||
name: str
|
||||
cls: str
|
||||
heart_score: float
|
||||
badge: str | None = None
|
||||
avg_duration: str
|
||||
service_count: int
|
||||
coach_spend: float
|
||||
relation_idx: float
|
||||
|
||||
class LoyalItem(CustomerBoardItemBase):
|
||||
intimacy: float
|
||||
top_coach_name: str
|
||||
top_coach_heart: float
|
||||
top_coach_score: float
|
||||
coach_name: str
|
||||
coach_ratio: str # "78%"
|
||||
coach_details: list[CoachDetail]
|
||||
|
||||
class CustomerBoardResponse(CamelModel):
|
||||
items: list[dict] # 实际类型取决于 dimension
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
```
|
||||
|
||||
### 4.4 BOARD-3 响应 Schema
|
||||
|
||||
```python
|
||||
class CompareField(CamelModel):
|
||||
"""环比字段三元组(仅 compare=1 时出现)。"""
|
||||
compare: str # "12.5%" / "新增" / "持平"
|
||||
down: bool
|
||||
flat: bool
|
||||
|
||||
class OverviewPanel(CamelModel):
|
||||
occurrence: float
|
||||
discount: float # 负值
|
||||
discount_rate: float
|
||||
confirmed_revenue: float
|
||||
cash_in: float
|
||||
cash_out: float
|
||||
cash_balance: float
|
||||
balance_rate: float
|
||||
# 环比字段(compare=1 时存在,compare=0 时整个字段不出现)
|
||||
occurrence_compare: str | None = None
|
||||
occurrence_down: bool | None = None
|
||||
occurrence_flat: bool | None = None
|
||||
# ... 其余 7 项指标各 3 个环比字段,结构相同
|
||||
|
||||
class GiftCell(CamelModel):
|
||||
value: float
|
||||
compare: str | None = None
|
||||
down: bool | None = None
|
||||
flat: bool | None = None
|
||||
|
||||
class GiftRow(CamelModel):
|
||||
"""赠送卡矩阵一行:合计 / 酒水卡 / 台费卡 / 抵用券。"""
|
||||
label: str # "新增" / "消费" / "余额"
|
||||
total: GiftCell
|
||||
liquor: GiftCell
|
||||
table_fee: GiftCell
|
||||
voucher: GiftCell
|
||||
|
||||
class RechargePanel(CamelModel):
|
||||
actual_income: float
|
||||
first_charge: float
|
||||
renew_charge: float
|
||||
consumed: float
|
||||
card_balance: float
|
||||
gift_rows: list[GiftRow] # 3 行
|
||||
all_card_balance: float
|
||||
# 各项环比字段(同 overview 模式)
|
||||
|
||||
class RevenueStructureRow(CamelModel):
|
||||
id: str
|
||||
name: str
|
||||
desc: str | None = None
|
||||
is_sub: bool = False
|
||||
amount: float
|
||||
discount: float
|
||||
booked: float
|
||||
booked_compare: str | None = None
|
||||
|
||||
class RevenueItem(CamelModel):
|
||||
label: str
|
||||
amount: float
|
||||
|
||||
class ChannelItem(CamelModel):
|
||||
label: str
|
||||
amount: float
|
||||
|
||||
class RevenuePanel(CamelModel):
|
||||
structure_rows: list[RevenueStructureRow]
|
||||
price_items: list[RevenueItem] # 4 项
|
||||
total_occurrence: float
|
||||
discount_items: list[RevenueItem] # 4 项
|
||||
confirmed_total: float
|
||||
channel_items: list[ChannelItem] # 3 项
|
||||
|
||||
class CashflowItem(CamelModel):
|
||||
label: str
|
||||
amount: float
|
||||
|
||||
class CashflowPanel(CamelModel):
|
||||
consume_items: list[CashflowItem] # 3 项
|
||||
recharge_items: list[CashflowItem] # 1 项
|
||||
total: float
|
||||
|
||||
class ExpenseItem(CamelModel):
|
||||
label: str
|
||||
amount: float
|
||||
compare: str | None = None
|
||||
down: bool | None = None
|
||||
flat: bool | None = None
|
||||
|
||||
class ExpensePanel(CamelModel):
|
||||
operation_items: list[ExpenseItem] # 3 项
|
||||
fixed_items: list[ExpenseItem] # 4 项
|
||||
coach_items: list[ExpenseItem] # 4 项
|
||||
platform_items: list[ExpenseItem] # 3 项
|
||||
total: float
|
||||
total_compare: str | None = None
|
||||
total_down: bool | None = None
|
||||
total_flat: bool | None = None
|
||||
|
||||
class CoachAnalysisRow(CamelModel):
|
||||
level: str
|
||||
pay: float
|
||||
share: float
|
||||
hourly: float
|
||||
pay_compare: str | None = None
|
||||
pay_down: bool | None = None
|
||||
share_compare: str | None = None
|
||||
share_down: bool | None = None
|
||||
hourly_compare: str | None = None
|
||||
hourly_flat: bool | None = None
|
||||
|
||||
class CoachAnalysisTable(CamelModel):
|
||||
total_pay: float
|
||||
total_share: float
|
||||
avg_hourly: float
|
||||
total_pay_compare: str | None = None
|
||||
total_pay_down: bool | None = None
|
||||
total_share_compare: str | None = None
|
||||
total_share_down: bool | None = None
|
||||
avg_hourly_compare: str | None = None
|
||||
avg_hourly_flat: bool | None = None
|
||||
rows: list[CoachAnalysisRow] # 4 行:初级/中级/高级/星级
|
||||
|
||||
class CoachAnalysisPanel(CamelModel):
|
||||
basic: CoachAnalysisTable # 基础课/陪打
|
||||
incentive: CoachAnalysisTable # 激励课/超休
|
||||
|
||||
class FinanceBoardResponse(CamelModel):
|
||||
overview: OverviewPanel
|
||||
recharge: RechargePanel | None # area≠all 时为 null
|
||||
revenue: RevenuePanel
|
||||
cashflow: CashflowPanel
|
||||
expense: ExpensePanel
|
||||
coach_analysis: CoachAnalysisPanel
|
||||
```
|
||||
|
||||
### 4.5 CONFIG-1 响应 Schema
|
||||
|
||||
```python
|
||||
class SkillTypeItem(CamelModel):
|
||||
key: str # chinese/snooker/mahjong/karaoke
|
||||
label: str # 中文标签
|
||||
emoji: str # 表情符号
|
||||
cls: str # 前端样式类
|
||||
```
|
||||
|
||||
### 4.6 数据库查询模式
|
||||
|
||||
所有 FDW 查询遵循已有模式:
|
||||
|
||||
```python
|
||||
def get_finance_overview(conn, site_id, start_date, end_date):
|
||||
"""查询经营一览 8 指标(从财务日报聚合)。"""
|
||||
with _fdw_context(conn, site_id) as cur:
|
||||
cur.execute("""
|
||||
SELECT SUM(occurrence) AS occurrence,
|
||||
SUM(discount) AS discount,
|
||||
...
|
||||
FROM app.v_dws_finance_daily_summary
|
||||
WHERE stat_date >= %s AND stat_date < %s
|
||||
""", (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` 手动聚合
|
||||
- BOARD-2 recall/potential/recent/recharge 维度使用 ETL 已计算好的专用指数视图,避免在后端重复计算
|
||||
|
||||
关键 SQL 模式:
|
||||
- **items_sum 口径**:所有金额字段使用 `ledger_amount`(对应 `items_sum`),禁止 `consume_money`
|
||||
- **助教费用拆分**:`base_income`(对应 `assistant_pd_money`)+ `bonus_income`(对应 `assistant_cx_money`),禁止 `service_fee`
|
||||
- **会员信息 DQ-6**:`LEFT JOIN app.v_dim_member ON tenant_member_id = member_id AND scd2_is_current = 1`
|
||||
- **会员卡 DQ-7**:`LEFT JOIN app.v_dim_member_card_account ON tenant_member_id AND scd2_is_current = 1`
|
||||
- **废单排除**:`WHERE is_delete = 0`(RLS 视图使用 `is_delete`)
|
||||
- **正向交易**:`WHERE settle_type IN (1, 3)`
|
||||
- **支付渠道恒等式**:`balance_amount = recharge_card_amount + gift_card_amount`
|
||||
- **现金流互斥**:`platform_settlement_amount` 和 `groupbuy_pay_amount` 互斥
|
||||
|
||||
### 4.7 BOARD-2 分页策略
|
||||
|
||||
```python
|
||||
# SQL 层面使用 LIMIT/OFFSET
|
||||
# 先执行 COUNT(*) 获取 total,再执行分页查询
|
||||
# pageSize 上限 100,默认 20
|
||||
|
||||
def get_customer_board_recall(conn, site_id, project, page, page_size):
|
||||
with _fdw_context(conn, site_id) as cur:
|
||||
# 1. 总数
|
||||
cur.execute("SELECT COUNT(*) FROM ... WHERE ...", params)
|
||||
total = cur.fetchone()[0]
|
||||
|
||||
# 2. 分页数据
|
||||
offset = (page - 1) * page_size
|
||||
cur.execute("SELECT ... LIMIT %s OFFSET %s",
|
||||
(*params, page_size, offset))
|
||||
items = cur.fetchall()
|
||||
|
||||
return {"items": items, "total": total, "page": page, "page_size": page_size}
|
||||
```
|
||||
|
||||
### 4.8 BOARD-3 环比字段条件输出
|
||||
|
||||
`compare=0` 时,响应 JSON 中不包含任何环比字段。实现方式:Schema 中环比字段设为 `Optional`,`model_dump(exclude_none=True)` 输出时自动排除 `None` 值。
|
||||
|
||||
```python
|
||||
class OverviewPanel(CamelModel):
|
||||
model_config = ConfigDict(
|
||||
alias_generator=to_camel,
|
||||
populate_by_name=True,
|
||||
# exclude_none 在序列化时排除 None 字段
|
||||
)
|
||||
|
||||
occurrence: float
|
||||
occurrence_compare: str | None = None # compare=0 时为 None → 不输出
|
||||
occurrence_down: bool | None = None
|
||||
occurrence_flat: bool | None = None
|
||||
```
|
||||
|
||||
路由层序列化时使用 `response_model_exclude_none=True`:
|
||||
|
||||
```python
|
||||
@router.get("/finance", response_model=FinanceBoardResponse,
|
||||
response_model_exclude_none=True)
|
||||
async def get_finance_board(...): ...
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 正确性属性
|
||||
|
||||
*属性(Property)是系统在所有合法执行路径上都应保持为真的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
|
||||
|
||||
### Property 1: 日期范围计算正确性
|
||||
|
||||
*对于任意*当前日期和任意 `time` 枚举值(BOARD-1 的 6 种 + BOARD-3 的 8 种),`_calc_date_range(time)` 返回的 `(start_date, end_date)` 应满足:`start_date <= end_date`,且当 `compare=1` 时计算的上期范围 `(prev_start, prev_end)` 应满足 `prev_end <= start_date` 且上期长度等于当期长度。
|
||||
|
||||
**Validates: Requirements 1.3, 3.2, 3.3**
|
||||
|
||||
### Property 2: BOARD-1 排序不变量
|
||||
|
||||
*对于任意* BOARD-1 响应列表和任意 `sort` 参数,列表中相邻两项的排序字段值应满足排序方向约束:`perf_desc` → `items[i].perfHours >= items[i+1].perfHours`,`salary_asc` → `items[i].salary <= items[i+1].salary`,以此类推。
|
||||
|
||||
**Validates: Requirements 1.15, 9.1, 9.2**
|
||||
|
||||
### Property 3: BOARD-2 分页不变量
|
||||
|
||||
*对于任意*相同筛选参数(`dimension`、`project`)的 BOARD-2 请求,(a) `items.length <= pageSize`,(b) 不同 `page` 值返回的 `total` 相同,(c) `page=1` 和 `page=2` 返回的 `items` 无交集(按 `id` 判断)。
|
||||
|
||||
**Validates: Requirements 2.2, 9.3, 9.4**
|
||||
|
||||
### Property 4: 亲密度 emoji 四级映射
|
||||
|
||||
*对于任意* `rs_display` 浮点数值(0-10 范围),亲密度 emoji 映射函数应返回确定的结果:`> 8.5` → 💖,`> 7` → 🧡,`> 5` → 💛,`≤ 5` → 💙,且映射结果与阈值边界一致(如 `rs_display=8.5` 应返回 🧡 而非 💖)。
|
||||
|
||||
**Validates: Requirements 1.6**
|
||||
|
||||
### Property 5: 环比计算公式正确性
|
||||
|
||||
*对于任意*两个非负 `Decimal` 值 `(current, previous)`,`calc_compare(current, previous)` 应满足:(a) `previous > 0` 时 `compare` 字符串等于 `abs((current - previous) / previous * 100)` 格式化为百分比,(b) `current > previous` 时 `is_down=False`,(c) `current < previous` 时 `is_down=True`,(d) `current == previous` 时 `is_flat=True`,(e) `previous=0, current>0` 时返回 `"新增"`,(f) `previous=0, current=0` 时返回 `"持平"`。
|
||||
|
||||
**Validates: Requirements 8.11, 8.12, 8.13, 8.14**
|
||||
|
||||
### Property 6: 环比开关一致性
|
||||
|
||||
*对于任意* BOARD-3 请求参数,当 `compare=0` 时,响应 JSON 序列化后的字符串中不应包含任何以 `Compare`、`Down`、`Flat` 结尾的 key(camelCase 格式)。
|
||||
|
||||
**Validates: Requirements 3.4, 9.8**
|
||||
|
||||
### Property 7: 预收资产区域约束
|
||||
|
||||
*对于任意* BOARD-3 请求,当 `area` 不等于 `all` 时,响应中 `recharge` 字段应为 `null`。
|
||||
|
||||
**Validates: Requirements 3.11, 9.7**
|
||||
|
||||
### Property 8: 经营一览收入确认恒等式
|
||||
|
||||
*对于任意* BOARD-3 响应中的 `overview` 数据,`confirmedRevenue` 应近似等于 `occurrence - abs(discount)`(在 ±0.01 浮点精度范围内)。
|
||||
|
||||
**Validates: Requirements 9.5**
|
||||
|
||||
### Property 9: 经营一览现金结余恒等式
|
||||
|
||||
*对于任意* BOARD-3 响应中的 `overview` 数据,`cashBalance` 应近似等于 `cashIn - cashOut`(在 ±0.01 浮点精度范围内)。
|
||||
|
||||
**Validates: Requirements 9.6**
|
||||
|
||||
### Property 10: 支付渠道恒等式
|
||||
|
||||
*对于任意*涉及支付渠道的数据记录,`balance_amount` 应精确等于 `recharge_card_amount + gift_card_amount`(DWD-DOC 强制规则 3)。
|
||||
|
||||
**Validates: Requirements 8.7, 9.9**
|
||||
|
||||
### Property 11: 参数互斥约束
|
||||
|
||||
*对于任意* BOARD-1 请求,当 `time=last_6m` 且 `sort=sv_desc` 时,API 应返回 HTTP 400 状态码,响应体包含错误信息。
|
||||
|
||||
**Validates: Requirements 1.2, 9.11**
|
||||
|
||||
### Property 12: BOARD-3 幂等性
|
||||
|
||||
*对于任意*相同参数的 BOARD-3 请求(相同 `time`、`area`、`compare`),在底层数据未变更的情况下,两次请求返回的 `overview.occurrence` 和 `overview.cashBalance` 值应完全相同。
|
||||
|
||||
**Validates: Requirements 9.10**
|
||||
|
||||
### Property 13: weeklyVisits 百分比范围
|
||||
|
||||
*对于任意* BOARD-2 `freq60` 维度响应中的 `weeklyVisits` 数组,(a) 数组长度固定为 8,(b) 每个元素的 `pct` 值在 0-100 范围内,(c) 如果存在非零 `val`,则 `max(pct)` 应等于 100。
|
||||
|
||||
**Validates: Requirements 2.20**
|
||||
|
||||
### Property 14: 优雅降级不变量
|
||||
|
||||
*对于任意* BOARD-3 请求,当某个板块(overview/recharge/revenue/cashflow/expense/coachAnalysis)的数据源查询抛出异常时,整体响应仍应返回 HTTP 200,失败板块返回空默认值(空对象或空数组),其他板块数据不受影响。
|
||||
|
||||
**Validates: Requirements 8.9, 8.10**
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 6.1 HTTP 错误码
|
||||
|
||||
| 场景 | 状态码 | 响应体 |
|
||||
|------|--------|--------|
|
||||
| 未认证(无 JWT / JWT 过期) | 401 | `{ code: 401, message: "无效的令牌" }` |
|
||||
| 未审核(status ≠ approved) | 403 | `{ code: 403, message: "用户未通过审核" }` |
|
||||
| 权限不足 | 403 | `{ code: 403, message: "权限不足" }` |
|
||||
| 参数互斥(last_6m + sv_desc) | 400 | `{ code: 400, message: "最近6个月不支持客源储值排序" }` |
|
||||
| 无效枚举值 | 422 | FastAPI 自动验证 |
|
||||
| 服务端异常 | 500 | `{ code: 500, message: "Internal Server Error" }` |
|
||||
|
||||
### 6.2 优雅降级策略
|
||||
|
||||
BOARD-3 财务看板采用板块级降级:
|
||||
|
||||
```python
|
||||
async def get_finance_board(time, area, compare, site_id):
|
||||
conn = _get_biz_connection()
|
||||
try:
|
||||
# 每个板块独立 try/except
|
||||
try:
|
||||
overview = _build_overview(conn, site_id, date_range, compare)
|
||||
except Exception:
|
||||
logger.warning("overview 查询失败,降级为空", exc_info=True)
|
||||
overview = _empty_overview()
|
||||
|
||||
try:
|
||||
recharge = _build_recharge(conn, site_id, date_range, compare) if area == "all" else None
|
||||
except Exception:
|
||||
logger.warning("recharge 查询失败,降级为 null", exc_info=True)
|
||||
recharge = None
|
||||
|
||||
# ... 其余板块同理
|
||||
|
||||
return { "overview": overview, "recharge": recharge, ... }
|
||||
finally:
|
||||
conn.close()
|
||||
```
|
||||
|
||||
BOARD-1 和 BOARD-2 采用整体降级:核心查询失败直接返回 500,扩展字段(如 topCustomers)失败降级为空。
|
||||
|
||||
CONFIG-1 采用空数组降级:ETL cfg 表查询失败返回 `[]`,前端使用硬编码回退。
|
||||
|
||||
### 6.3 FDW 查询异常处理
|
||||
|
||||
所有 `_fdw_context` 内的查询异常由调用方捕获。`fdw_queries.py` 中的函数不做异常吞没,让 service 层决定降级策略。
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 7.1 测试框架
|
||||
|
||||
- **属性测试**:`hypothesis`(Python),已在项目中使用(见 `.hypothesis/` 目录和 `test_site_isolation_properties.py`)
|
||||
- **单元测试**:`pytest`,mock 数据库交互
|
||||
- **集成测试**:`FastAPI TestClient`,mock FDW 连接
|
||||
|
||||
### 7.2 属性测试配置
|
||||
|
||||
每个属性测试使用 `@settings(max_examples=100)` 配置最少 100 次迭代。
|
||||
|
||||
每个测试函数的 docstring 中标注对应的设计属性:
|
||||
|
||||
```python
|
||||
@settings(max_examples=100)
|
||||
@given(...)
|
||||
def test_compare_formula(current, previous):
|
||||
"""Feature: rns1-board-apis, Property 5: 环比计算公式正确性"""
|
||||
...
|
||||
```
|
||||
|
||||
### 7.3 属性测试清单
|
||||
|
||||
| Property | 测试文件 | 测试策略 |
|
||||
|----------|---------|---------|
|
||||
| P1 日期范围 | `test_board_properties.py` | 生成随机日期 + time 枚举,验证 start <= end 和上下期关系 |
|
||||
| P2 排序不变量 | `test_board_properties.py` | 生成随机助教列表,调用排序函数,验证相邻元素顺序 |
|
||||
| P3 分页不变量 | `test_board_properties.py` | 生成随机客户列表 + page/pageSize,验证分页约束 |
|
||||
| P4 emoji 映射 | `test_board_properties.py` | 生成 0-10 范围浮点数,验证映射结果 |
|
||||
| P5 环比公式 | `test_board_properties.py` | 生成非负 Decimal 对,验证公式和边界条件 |
|
||||
| P6 环比开关 | `test_board_properties.py` | 生成 BOARD-3 mock 数据 + compare=0,验证 JSON 无环比 key |
|
||||
| P7 区域约束 | `test_board_properties.py` | 生成 area≠all 的请求,验证 recharge=null |
|
||||
| P8 收入恒等式 | `test_board_properties.py` | 生成 overview 数据,验证 confirmedRevenue ≈ occurrence - |discount| |
|
||||
| P9 现金结余 | `test_board_properties.py` | 生成 overview 数据,验证 cashBalance ≈ cashIn - cashOut |
|
||||
| P10 支付渠道 | `test_board_properties.py` | 生成支付渠道数据,验证恒等式 |
|
||||
| P11 参数互斥 | `test_board_properties.py` | 固定 time=last_6m + sort=sv_desc,验证 400 |
|
||||
| P12 幂等性 | `test_board_properties.py` | 相同参数两次调用,验证结果一致 |
|
||||
| P13 pct 范围 | `test_board_properties.py` | 生成 8 周到店数据,验证 pct 范围和最大值 |
|
||||
| P14 优雅降级 | `test_board_properties.py` | mock 板块查询抛异常,验证整体 200 + 空默认值 |
|
||||
|
||||
### 7.4 单元测试清单
|
||||
|
||||
| 测试目标 | 测试文件 | 覆盖内容 |
|
||||
|----------|---------|---------|
|
||||
| 日期范围边界 | `test_board_unit.py` | 月末、跨年、闰年等边界 |
|
||||
| 环比 "新增"/"持平" | `test_board_unit.py` | previous=0 的两种情况 |
|
||||
| BOARD-1 扁平结构 | `test_board_unit.py` | 验证所有维度字段都存在 |
|
||||
| BOARD-2 各维度排序 | `test_board_unit.py` | 8 个维度各一个排序示例 |
|
||||
| BOARD-3 recharge null | `test_board_unit.py` | area=hallA 时 recharge=null |
|
||||
| CONFIG-1 空数组降级 | `test_board_unit.py` | cfg 表查询失败返回 [] |
|
||||
| 权限检查 | `test_board_unit.py` | 无权限用户访问各看板返回 403 |
|
||||
|
||||
### 7.5 测试文件位置
|
||||
|
||||
```
|
||||
apps/backend/tests/
|
||||
├── test_board_properties.py # 属性测试(14 个 property)
|
||||
└── unit/
|
||||
└── test_board_unit.py # 单元测试
|
||||
```
|
||||
|
||||
335
.kiro/specs/rns1-board-apis/requirements.md
Normal file
335
.kiro/specs/rns1-board-apis/requirements.md
Normal file
@@ -0,0 +1,335 @@
|
||||
# 需求文档 — RNS1.3:三看板接口
|
||||
|
||||
## 简介
|
||||
|
||||
RNS1.3 是 NS1 小程序后端 API 补全项目的第四个子 spec,负责实现 3 个看板接口(BOARD-1 助教看板、BOARD-2 客户看板、BOARD-3 财务看板)、CONFIG-1 技能配置接口、以及前端看板筛选修复。看板是管理层视角的核心功能,其中 BOARD-3 财务看板是全项目最复杂的单个接口(6 个独立板块、200+ 字段、60+ 环比数据点)。
|
||||
|
||||
### 依赖
|
||||
|
||||
- RNS1.0(基础设施与契约重写)必须先完成:全局响应包装中间件(`ResponseWrapperMiddleware`)、camelCase 转换(`CamelModel`)、重写后的 API 契约
|
||||
- RNS1.1 / RNS1.2 可并行开发,无直接依赖
|
||||
- 后端已有 `fdw_queries.py`(FDW 查询集中封装)、`task_manager.py`、`note_service.py`
|
||||
- 前端已有 13 个页面(P5.2 交付),当前使用 mock 数据
|
||||
|
||||
### 来源文档
|
||||
|
||||
- `docs/prd/Neo_Specs/RNS1-split-plan.md` — 拆分计划主文档(RNS1.3 章节,T3-1 ~ T3-7)
|
||||
- `docs/miniprogram-dev/API-contract.md` — API 契约(BOARD-1/2/3、CONFIG-1 完整定义)
|
||||
- `docs/prd/Neo_Specs/NS1-xcx-backend-api.md` — 原始 spec(八¾ 看板筛选交叉矩阵为权威参考)
|
||||
- `docs/prd/Neo_Specs/miniprogram-storyboard-walkthrough-gaps.md` — 管理层视角走查报告(G1~G10 看板相关 Gap)
|
||||
- `docs/prd/Neo_Specs/storyboard-walkthrough-assistant-view.md` — 助教视角走查报告
|
||||
- `docs/reports/DWD-DOC/` — 金额口径与字段语义权威标杆文档
|
||||
|
||||
## 术语表
|
||||
|
||||
- **Backend**:FastAPI 后端应用,位于 `apps/backend/`
|
||||
- **Miniprogram**:微信小程序前端应用,位于 `apps/miniprogram/`
|
||||
- **BOARD_1_API**:助教看板接口 `GET /api/xcx/board/coaches`,返回助教排行列表(4 维度专属字段)
|
||||
- **BOARD_2_API**:客户看板接口 `GET /api/xcx/board/customers`,返回客户排行列表(8 维度专属字段)
|
||||
- **BOARD_3_API**:财务看板接口 `GET /api/xcx/board/finance`,返回 6 个板块的财务数据(overview/recharge/revenue/cashflow/expense/coachAnalysis)
|
||||
- **CONFIG_1_API**:技能类型列表接口 `GET /api/xcx/config/skill-types`,返回助教技能类型配置
|
||||
- **FDW**:PostgreSQL Foreign Data Wrapper,后端通过直连 ETL 库查询 `app.v_*` RLS 视图
|
||||
- **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 要求的拆分字段
|
||||
- **v_dws_assistant_salary_calc**:ETL RLS 视图,提供助教绩效/档位/收入/工资数据
|
||||
- **v_dws_assistant_monthly_summary**:ETL RLS 视图,提供助教月度汇总(客户数、储值额等)
|
||||
- **v_dim_assistant**:ETL RLS 视图,提供助教基本信息(姓名、技能、入职日期等)
|
||||
- **v_dim_member**:ETL RLS 视图,提供会员基本信息(nickname、mobile),通过 `member_id` 关联,取 `scd2_is_current=1`
|
||||
- **v_dim_member_card_account**:ETL RLS 视图,提供会员卡余额,通过 `tenant_member_id` 关联,取 `scd2_is_current=1`
|
||||
- **v_dws_member_assistant_relation_index**:ETL RLS 视图,提供会员与助教的关系指数
|
||||
- **v_dws_member_consumption_summary**:ETL RLS 视图,提供会员消费汇总
|
||||
- **v_dws_finance_daily_summary**:ETL RLS 视图,提供财务日报汇总数据(经营一览 8 指标 + 现金流入/流出),BOARD-3 overview/cashflow/expense 板块的主数据源
|
||||
- **v_dws_finance_income_structure**:ETL RLS 视图,提供收入结构表 + 正价/优惠/渠道明细,BOARD-3 revenue 板块数据源
|
||||
- **v_dws_finance_recharge_summary**:ETL RLS 视图,提供储值卡 + 赠送卡矩阵数据,BOARD-3 recharge 板块数据源
|
||||
- **v_dws_finance_discount_detail**:ETL RLS 视图,提供优惠明细(大客户优惠/其他优惠拆分),BOARD-3 revenue 板块辅助数据源
|
||||
- **v_dws_finance_expense_summary**:ETL RLS 视图,提供现金流出 4 子分组明细,BOARD-3 expense 板块数据源
|
||||
- **v_dws_platform_settlement**:ETL RLS 视图,提供平台结算数据(汇来米/美团/抖音),BOARD-3 expense.platformItems 数据源
|
||||
- **coach_tasks**:业务库 `biz.coach_tasks` 表,存储助教任务分配与状态
|
||||
- **user_assistant_binding**:认证库 `auth.user_assistant_binding` 表,映射小程序用户与助教身份
|
||||
- **dimType**:BOARD-1 中根据 `sort` 参数映射的维度类型(`perf`/`salary`/`sv`/`task`),决定卡片展示模板
|
||||
- **环比**:月环比,与上一个相同时间周期对比(本月 vs 上月、本周 vs 上周等),返回百分比字符串 + 方向标记
|
||||
- **GiftRow**:赠送卡 3×4 矩阵中的一行(新增/消费/余额),每行含 4 列(合计/酒水卡/台费卡/抵用券)
|
||||
- **RevenueStructureRow**:收入结构表中的一行(9 行含子行),含发生额、优惠、入账金额
|
||||
- **CoachAnalysisTable**:助教分析子表(基础课或激励课),含汇总行 + 按等级分行(初级/中级/高级/星级)
|
||||
|
||||
## 需求
|
||||
|
||||
### 需求 1:实现 BOARD-1 助教看板(T3-1)
|
||||
|
||||
**用户故事:** 作为管理者,我希望在助教看板中按不同维度(定档业绩/工资/客源储值/任务完成)查看助教排行,并支持技能筛选和时间范围切换,以便评估和对比各助教的工作表现。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
#### 1.1 请求参数与筛选
|
||||
|
||||
1. THE BOARD_1_API SHALL 接受 3 个查询参数:`sort`(排序维度,6 种枚举:`perf_desc`/`perf_asc`/`salary_desc`/`salary_asc`/`sv_desc`/`task_desc`)、`skill`(技能筛选,5 种枚举:`all`/`chinese`/`snooker`/`mahjong`/`karaoke`)、`time`(时间范围,6 种枚举:`month`/`quarter`/`last_month`/`last_3m`/`last_quarter`/`last_6m`)
|
||||
2. IF `time=last_6m` 且 `sort=sv_desc`,THEN THE BOARD_1_API SHALL 返回 HTTP 400 `{ code: 400, message: "最近6个月不支持客源储值排序" }`
|
||||
3. THE BOARD_1_API SHALL 根据 `time` 参数计算对应的日期范围(本月=当月1日~末日、上月=上月1日~末日、本季度=季度首日~末日、前3个月=不含本月的前3个月、上季度=上季度、最近6个月=不含本月的前6个月)
|
||||
|
||||
#### 1.2 基础字段(所有维度共享)
|
||||
|
||||
4. THE BOARD_1_API SHALL 为每个助教 item 返回基础字段:`id`(助教 ID)、`name`(助教姓名)、`initial`(姓名首字)、`avatarGradient`(头像渐变色标识)、`level`(等级 key:`star`/`senior`/`middle`/`junior`)、`skills`(技能列表,`Array<{ text: string, cls: string }>`)、`topCustomers`(Top 客户列表,含亲密度 emoji 前缀,如 `['💖 王先生', '💛 李女士']`)
|
||||
5. THE BOARD_1_API SHALL 从 `v_dim_assistant` 获取助教基本信息,从 `v_dws_assistant_salary_calc` 获取等级(`assistant_level_name`)
|
||||
6. THE BOARD_1_API SHALL 从 `v_dws_member_assistant_relation_index` 按亲密度降序取 Top 3 客户,拼接亲密度 emoji(P6 AC3 四级映射:`rs_display > 8.5` → 💖,`> 7` → 🧡,`> 5` → 💛,`≤ 5` → 💙)+ 客户姓名作为 `topCustomers`
|
||||
|
||||
#### 1.3 perf 维度专属字段
|
||||
|
||||
7. WHEN `sort` 为 `perf_desc` 或 `perf_asc` 时,THE BOARD_1_API SHALL 返回 perf 维度字段:`perfHours`(当期定档工时)、`perfHoursBefore`(上期定档工时,可选)、`perfGap`(距升档差距描述,如 `"距升档 13.8h"`,已达标时不返回)、`perfReached`(是否已达标)
|
||||
8. THE BOARD_1_API SHALL 从 `v_dws_assistant_salary_calc` 查询当期和上期的定档工时数据,根据档位阈值计算 `perfGap` 和 `perfReached`
|
||||
|
||||
#### 1.4 salary 维度专属字段
|
||||
|
||||
9. WHEN `sort` 为 `salary_desc` 或 `salary_asc` 时,THE BOARD_1_API SHALL 返回 salary 维度字段:`salary`(预估工资总额,元)、`salaryPerfHours`(定档工时)、`salaryPerfBefore`(上期定档工时,可选)
|
||||
10. THE BOARD_1_API SHALL 使用 `items_sum` 口径计算 `salary` 字段(DWD-DOC 强制规则 1)
|
||||
|
||||
#### 1.5 sv 维度专属字段
|
||||
|
||||
11. WHEN `sort` 为 `sv_desc` 时,THE BOARD_1_API SHALL 返回 sv 维度字段:`svAmount`(客源储值总额,元)、`svCustomerCount`(储值客户数)、`svConsume`(储值消耗额,元)
|
||||
12. THE BOARD_1_API SHALL 从 `v_dws_assistant_monthly_summary` 获取助教月度储值汇总数据(客源储值额、储值客户数、储值消耗额),该视图已按助教维度预聚合
|
||||
|
||||
#### 1.6 task 维度专属字段
|
||||
|
||||
13. WHEN `sort` 为 `task_desc` 时,THE BOARD_1_API SHALL 返回 task 维度字段:`taskRecall`(召回任务完成数)、`taskCallback`(回访任务完成数)
|
||||
14. THE BOARD_1_API SHALL 从 `biz.coach_tasks` 查询指定时间范围内 `status='completed'` 的任务,按 `task_type` 分类统计
|
||||
|
||||
#### 1.7 排序与返回策略
|
||||
|
||||
15. THE BOARD_1_API SHALL 根据 `sort` 参数对结果排序(`perf_desc` 按定档工时降序、`perf_asc` 按定档工时升序、`salary_desc` 按工资降序、`salary_asc` 按工资升序、`sv_desc` 按储值额降序、`task_desc` 按任务完成总数降序)
|
||||
16. THE BOARD_1_API SHALL 始终返回所有维度的字段(扁平结构),前端根据当前 `sort` 选择性渲染对应卡片模板,切换维度时无需重新请求
|
||||
|
||||
### 需求 2:实现 BOARD-2 客户看板(T3-2)
|
||||
|
||||
**用户故事:** 作为管理者,我希望在客户看板中按 8 个维度(最应召回/最大消费潜力/最高余额/最近充值/最近到店/最高消费60天/最频繁60天/最专一60天)查看客户排行,每个维度展示不同的专属字段卡片和关联助教信息,以便从多角度评估客户价值和服务需求。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
#### 2.1 请求参数与分页
|
||||
|
||||
1. THE BOARD_2_API SHALL 接受 4 个查询参数:`dimension`(维度,8 种枚举:`recall`/`potential`/`balance`/`recharge`/`recent`/`spend60`/`freq60`/`loyal`)、`project`(项目筛选,5 种枚举:`all`/`chinese`/`snooker`/`mahjong`/`karaoke`)、`page`(页码,默认 1)、`pageSize`(每页条数,默认 20,上限 100)
|
||||
2. THE BOARD_2_API SHALL 返回分页结构:`items`(客户列表)、`total`(总数)、`page`(当前页)、`pageSize`(每页条数),支持前端 20 条懒加载
|
||||
|
||||
#### 2.2 基础字段(所有维度共享)
|
||||
|
||||
3. THE BOARD_2_API SHALL 为每个客户 item 返回基础字段:`id`(客户 member_id)、`name`(客户姓名)、`initial`(姓名首字)、`avatarCls`(头像样式类)、`assistants`(关联助教列表,`Array<{ name, cls, heartScore, badge?, badgeCls? }>`)
|
||||
4. THE BOARD_2_API SHALL 通过 `member_id` LEFT JOIN `v_dim_member`(取 `scd2_is_current=1`)获取客户姓名(DQ-6)
|
||||
5. THE BOARD_2_API SHALL 从 `biz.coach_tasks` + 亲密度计算获取 `assistants` 列表,按亲密度降序排列,当前跟进助教(`cls='assistant--assignee'`)置顶
|
||||
|
||||
#### 2.3 recall 维度专属字段
|
||||
|
||||
6. WHEN `dimension=recall` 时,THE BOARD_2_API SHALL 返回:`idealDays`(理想到店间隔天数)、`elapsedDays`(已过天数)、`overdueDays`(超期天数 = elapsedDays - idealDays)、`visits30d`(近30天到店次数)、`balance`(余额,格式化字符串)、`recallIndex`(召回指数)
|
||||
7. THE BOARD_2_API SHALL 按 WBI(召回指数)降序排列 recall 维度结果
|
||||
|
||||
#### 2.4 potential 维度专属字段
|
||||
|
||||
8. WHEN `dimension=potential` 时,THE BOARD_2_API SHALL 返回:`potentialTags`(潜力标签列表,`Array<{ text, theme }>`)、`spend30d`(近30天消费)、`avgVisits`(月均到店)、`avgSpend`(次均消费)
|
||||
9. THE BOARD_2_API SHALL 按 SPI(消费潜力指数)降序排列 potential 维度结果
|
||||
|
||||
#### 2.5 balance 维度专属字段
|
||||
|
||||
10. WHEN `dimension=balance` 时,THE BOARD_2_API SHALL 返回:`balance`(当前余额)、`lastVisit`(最近到店描述,如 `"3天前"`)、`monthlyConsume`(月均消耗)、`availableMonths`(可用月数,如 `"约0.8个月"`)
|
||||
11. THE BOARD_2_API SHALL 按 `balance_amount` 降序排列 balance 维度结果
|
||||
|
||||
#### 2.6 recharge 维度专属字段
|
||||
|
||||
12. WHEN `dimension=recharge` 时,THE BOARD_2_API SHALL 返回:`lastRecharge`(最后充值日期)、`rechargeAmount`(充值金额)、`recharges60d`(近60天充值次数)、`currentBalance`(当前余额)
|
||||
13. THE BOARD_2_API SHALL 按 `last_recharge_date` 降序排列 recharge 维度结果
|
||||
|
||||
#### 2.7 recent 维度专属字段
|
||||
|
||||
14. WHEN `dimension=recent` 时,THE BOARD_2_API SHALL 返回:`daysAgo`(距今天数)、`visitFreq`(到店频率,如 `"6.2次/月"`)、`idealDays`(理想间隔)、`visits30d`(近30天到店)、`avgSpend`(次均消费)
|
||||
15. THE BOARD_2_API SHALL 按 `last_visit_date` 降序排列 recent 维度结果
|
||||
|
||||
#### 2.8 spend60 维度专属字段
|
||||
|
||||
16. WHEN `dimension=spend60` 时,THE BOARD_2_API SHALL 返回:`spend60d`(近60天消费总额)、`visits60d`(近60天到店次数)、`highSpendTag`(是否高消费标记)、`avgSpend`(次均消费)
|
||||
17. THE BOARD_2_API SHALL 使用 `items_sum` 口径计算 `spend60d`(DWD-DOC 强制规则 1),按 `items_sum_60d` 降序排列
|
||||
|
||||
#### 2.9 freq60 维度专属字段
|
||||
|
||||
18. WHEN `dimension=freq60` 时,THE BOARD_2_API SHALL 返回:`visits60d`(近60天到店次数)、`avgInterval`(平均到店间隔,如 `"5.0天"`)、`weeklyVisits`(最近8周到店柱状图,`Array<{ val: number, pct: number }>`,固定长度 8)、`spend60d`(近60天消费)
|
||||
19. THE BOARD_2_API SHALL 按 `visit_count_60d` 降序排列 freq60 维度结果
|
||||
20. THE BOARD_2_API SHALL 计算 `weeklyVisits` 中每周的 `pct` 为相对于 8 周最大值的百分比(0-100)
|
||||
|
||||
#### 2.10 loyal 维度专属字段
|
||||
|
||||
21. WHEN `dimension=loyal` 时,THE BOARD_2_API SHALL 返回:`intimacy`(专一度指数)、`topCoachName`(最亲密助教姓名)、`topCoachHeart`(最亲密助教亲密度分数)、`topCoachScore`(最亲密助教关系指数)、`coachName`(主助教姓名)、`coachRatio`(主助教占比,如 `"78%"`)、`coachDetails`(助教明细列表,`Array<{ name, cls, heartScore, badge?, avgDuration, serviceCount, coachSpend, relationIdx }>`)
|
||||
22. THE BOARD_2_API SHALL 按 `max_rs`(最大关系指数)降序排列 loyal 维度结果
|
||||
|
||||
#### 2.11 维度切换策略
|
||||
|
||||
23. THE BOARD_2_API SHALL 按 `dimension` 参数仅返回对应维度的专属字段(减少传输量和查询开销),切换维度时前端重新请求
|
||||
|
||||
### 需求 3:实现 BOARD-3 经营一览 + 预收资产(T3-3)
|
||||
|
||||
**用户故事:** 作为管理者,我希望在财务看板中查看经营一览(8 项核心指标及环比)和预收资产(储值卡 + 赠送卡矩阵),并支持时间范围、区域筛选和环比开关,以便全面掌握门店的经营状况和预收资产变动。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
#### 3.1 请求参数
|
||||
|
||||
1. THE BOARD_3_API SHALL 接受 3 个查询参数:`time`(时间范围,8 种枚举:`month`/`lastMonth`/`week`/`lastWeek`/`quarter3`/`quarter`/`lastQuarter`/`half6`)、`area`(区域筛选,7 种枚举:`all`/`hall`/`hallA`/`hallB`/`hallC`/`mahjong`/`teamBuilding`)、`compare`(环比开关,`0` 或 `1`,默认 `0`)
|
||||
2. THE BOARD_3_API SHALL 根据 `time` 参数计算当期日期范围(`month`=当月1日~末日、`lastMonth`=上月1日~末日、`week`=本周一~本周日、`lastWeek`=上周一~上周日、`quarter3`=前3个月不含本月、`quarter`=本季度首日~末日、`lastQuarter`=上季度、`half6`=最近6个月不含本月)
|
||||
3. WHEN `compare=1` 时,THE BOARD_3_API SHALL 计算上期日期范围(与当期相同长度的前一个周期),分别查询当期和上期数据后计算环比百分比
|
||||
4. WHEN `compare=0` 时,THE BOARD_3_API SHALL 不返回任何环比字段(`xxxCompare`/`isDown`/`isFlat`),减少查询开销
|
||||
|
||||
#### 3.2 经营一览 overview(8 指标 + 8 环比)
|
||||
|
||||
5. THE BOARD_3_API SHALL 返回 `overview` 板块,包含 8 项核心指标:`occurrence`(发生额/正价)、`discount`(总优惠,负值)、`discountRate`(折扣率)、`confirmedRevenue`(成交/确认收入)、`cashIn`(实收/现金流入)、`cashOut`(现金支出)、`cashBalance`(现金结余)、`balanceRate`(结余率)
|
||||
6. WHEN `compare=1` 时,THE BOARD_3_API SHALL 为 overview 每项指标返回 3 个环比字段:`xxxCompare`(环比百分比字符串,如 `"12.5%"` 或 `"持平"`)、`xxxDown`(是否下降,boolean)、`xxxFlat`(是否持平,boolean)
|
||||
7. THE BOARD_3_API SHALL 从 `v_dws_finance_daily_summary` 查询经营一览数据(按日期范围聚合),使用 `items_sum` 口径计算所有金额(DWD-DOC 强制规则 1)
|
||||
|
||||
#### 3.3 预收资产 recharge(储值卡 + 赠送卡矩阵)
|
||||
|
||||
8. WHEN `area=all` 时,THE BOARD_3_API SHALL 返回 `recharge` 板块,包含储值卡 5 项指标:`actualIncome`(充值实收)、`firstCharge`(首充)、`renewCharge`(续费)、`consumed`(消耗)、`cardBalance`(储值卡总余额)
|
||||
9. THE BOARD_3_API SHALL 返回 `recharge.giftRows`(赠送卡 3×4 矩阵),3 行(新增/消费/余额)× 4 列(合计/酒水卡/台费卡/抵用券),每个单元格含值和环比字段,共 24 个数据字段 + 24 个环比字段
|
||||
10. THE BOARD_3_API SHALL 返回 `recharge.allCardBalance`(全类别会员卡余额合计 = 储值卡 + 赠送卡)
|
||||
11. WHEN `area` 不等于 `all` 时,THE BOARD_3_API SHALL 将 `recharge` 板块返回 `null`(储值卡数据不按区域拆分,选中具体区域时无意义)
|
||||
12. THE BOARD_3_API SHALL 从 `v_dws_finance_recharge_summary` 查询预收资产数据
|
||||
|
||||
### 需求 4:实现 BOARD-3 应计收入 + 现金流入(T3-4)
|
||||
|
||||
**用户故事:** 作为管理者,我希望在财务看板中查看应计收入确认(收入结构表含区域子行、正价/优惠/渠道明细)和现金流入(消费收款 + 充值收款),以便分析收入构成和现金来源。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
#### 4.1 应计收入确认 revenue
|
||||
|
||||
1. THE BOARD_3_API SHALL 返回 `revenue.structureRows`(收入结构表),包含 9 行含子行标记:主行(开台与包厢、助教基础课、助教激励课、食品酒水)+ 子行(A区/B区/C区/团建区/麻将区,属于"开台与包厢"的子行,`isSub=true`),每行含 `id`、`name`、`desc`(可选)、`amount`(发生额)、`discount`(优惠金额)、`booked`(入账金额)、`bookedCompare`(入账环比,可选)
|
||||
2. THE BOARD_3_API SHALL 对收入结构表中助教行使用 `assistant_pd_money`(基础课/陪打)和 `assistant_cx_money`(激励课/超休)拆分(DWD-DOC 强制规则 2),禁止使用 `service_fee`
|
||||
3. THE BOARD_3_API SHALL 返回 `revenue.priceItems`(正价明细,4 项)、`revenue.totalOccurrence`(发生额合计)、`revenue.discountItems`(优惠明细,4 项)、`revenue.confirmedTotal`(确认收入合计)、`revenue.channelItems`(渠道明细,3 项:储值卡结算冲销/现金线上支付/团购核销确认收入)
|
||||
4. THE BOARD_3_API SHALL 从 `v_dws_finance_income_structure`(收入结构主表)+ `v_dws_finance_discount_detail`(优惠明细辅助)查询应计收入数据
|
||||
|
||||
#### 4.2 现金流入 cashflow
|
||||
|
||||
5. THE BOARD_3_API SHALL 返回 `cashflow` 板块,包含 `consumeItems`(消费收款,3 项:纸币现金/线上收款/团购平台)、`rechargeItems`(充值收款,1 项:会员充值到账)、`total`(现金流入合计)
|
||||
6. THE BOARD_3_API SHALL 确保 `consumeItems` 中 `platform_settlement_amount` 和 `groupbuy_pay_amount` 互斥(DWD-DOC 强制规则 8:现金流互斥)
|
||||
7. THE BOARD_3_API SHALL 从 `v_dws_finance_daily_summary` 查询现金流入数据(消费收款 + 充值收款字段均在财务日报中)
|
||||
|
||||
### 需求 5:实现 BOARD-3 现金流出 + 助教分析(T3-5)
|
||||
|
||||
**用户故事:** 作为管理者,我希望在财务看板中查看现金流出(经营/固定/助教分成/平台服务费 4 个子分组)和助教分析(基础课 + 激励课各按 4 等级分行),以便分析支出结构和助教成本。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
#### 5.1 现金流出 expense
|
||||
|
||||
1. THE BOARD_3_API SHALL 返回 `expense` 板块,包含 4 个子分组:`operationItems`(经营支出,3 项:食品饮料/耗材/报销)、`fixedItems`(固定支出,4 项:房租/水电/物业/人员工资)、`coachItems`(助教分成,4 项:基础课分成/激励课分成/充值提成/额外奖金)、`platformItems`(平台服务费,3 项:汇来米/美团/抖音)
|
||||
2. THE BOARD_3_API SHALL 返回 `expense.total`(现金流出合计)及其环比字段
|
||||
3. THE BOARD_3_API SHALL 对 `coachItems` 中基础课分成使用 `assistant_pd_money`,激励课分成使用 `assistant_cx_money`(DWD-DOC 强制规则 2)
|
||||
4. THE BOARD_3_API SHALL 从 `v_dws_finance_expense_summary`(支出明细 4 子分组,含助教分成)+ `v_dws_platform_settlement`(平台服务费:汇来米/美团/抖音)查询现金流出数据
|
||||
|
||||
#### 5.2 助教分析 coachAnalysis
|
||||
|
||||
5. THE BOARD_3_API SHALL 返回 `coachAnalysis` 板块,包含 `basic`(基础课/陪打)和 `incentive`(激励课/超休)两个子表,结构完全相同
|
||||
6. THE BOARD_3_API SHALL 为每个子表返回汇总行:`totalPay`(总课时费)、`totalShare`(总分成)、`avgHourly`(平均时薪),各含环比字段
|
||||
7. THE BOARD_3_API SHALL 为每个子表返回 `rows`(按等级分行,4 行:初级/中级/高级/星级),每行含 `level`(等级名)、`pay`(课时费)、`share`(分成)、`hourly`(时薪),各含环比字段和方向标记(`payDown`/`shareDown`/`hourlyFlat`)
|
||||
8. THE BOARD_3_API SHALL 从 `v_dws_assistant_salary_calc` 按 `assistant_level_name` 分组聚合助教分析数据
|
||||
|
||||
### 需求 6:实现 CONFIG-1 技能类型列表(T3-6)
|
||||
|
||||
**用户故事:** 作为管理者,我希望助教看板的技能筛选选项从后端配置表动态获取(而非前端硬编码),以便在新增技能类型时无需发版更新前端。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE CONFIG_1_API SHALL 实现 `GET /api/xcx/config/skill-types` 端点,返回技能类型列表,每项含 `key`(枚举值,如 `chinese`/`snooker`/`mahjong`/`karaoke`)、`label`(中文标签)、`emoji`(表情符号)、`cls`(前端样式类)
|
||||
2. THE CONFIG_1_API SHALL 从 ETL cfg 表读取技能类型配置数据
|
||||
3. IF ETL cfg 表查询失败或无数据,THEN THE CONFIG_1_API SHALL 返回空数组,前端 `api.ts` 中的硬编码列表作为 mock 回退
|
||||
4. THE CONFIG_1_API SHALL 对响应设置合理的缓存策略(技能类型变更频率极低)
|
||||
|
||||
### 需求 7:前端看板筛选修复(T3-7)
|
||||
|
||||
**用户故事:** 作为管理者,我希望在看板页面切换筛选条件时能立即看到更新后的数据(而非停留在旧数据),以便实时对比不同维度和时间范围的数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
#### 7.1 BOARD-1 筛选修复(F1, F6)
|
||||
|
||||
1. THE Miniprogram SHALL 修复 `board-coach` 页面的 `onSortChange`、`onSkillChange`、`onTimeChange` 事件处理函数,在更新 data 状态后调用 `this.loadData()` 重新请求 API
|
||||
2. THE Miniprogram SHALL 在 `board-coach` 页面实现 `time=last_6m` + `sort=sv_desc` 的互斥约束:选择 `last_6m` 时禁用 `sv_desc` 选项,或选择 `sv_desc` 时禁用 `last_6m` 选项
|
||||
|
||||
#### 7.2 BOARD-2 筛选修复 + 分页补充(F2, F3)
|
||||
|
||||
3. THE Miniprogram SHALL 修复 `board-customer` 页面的 `onDimensionChange`、`onProjectChange` 事件处理函数,在更新 data 状态后调用 `this.loadData()` 重新请求 API
|
||||
4. THE Miniprogram SHALL 为 `board-customer` 页面补充分页参数(`page`/`pageSize`)和"加载更多"懒加载逻辑(`onReachBottom` 触发加载下一页,`pageSize=20`)
|
||||
5. THE Miniprogram SHALL 修改 `services/api.ts` 中 `fetchBoardCustomers` 函数签名,增加 `page` 和 `pageSize` 参数
|
||||
|
||||
#### 7.3 BOARD-3 筛选修复 + 签名扩展(F4, F5)
|
||||
|
||||
6. THE Miniprogram SHALL 修改 `services/api.ts` 中 `fetchBoardFinance` 函数签名,从 `{ date?: string }` 扩展为 `{ time: string, area: string, compare: number }`
|
||||
7. THE Miniprogram SHALL 修复 `board-finance` 页面的 `onTimeChange`、`onAreaChange` 事件处理函数,在更新 data 状态后使用新参数调用 `fetchBoardFinance` 重新请求 API
|
||||
8. THE Miniprogram SHALL 修复 `board-finance` 页面的 `toggleCompare` 函数,切换环比开关后使用 `compare=0` 或 `compare=1` 参数重新请求 API
|
||||
9. WHEN `area` 不等于 `all` 时,THE Miniprogram SHALL 隐藏预收资产板块(`recharge` 为 `null` 时不渲染该 section)
|
||||
|
||||
### 需求 8:全局约束与数据隔离
|
||||
|
||||
**用户故事:** 作为系统管理员,我希望所有看板和配置接口都遵循统一的权限控制、数据隔离和数据质量规则,以确保数据安全和口径一致。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
#### 8.1 权限与认证
|
||||
|
||||
1. THE Backend SHALL 对所有 RNS1.3 接口(BOARD_1_API、BOARD_2_API、BOARD_3_API、CONFIG_1_API)执行 `require_approved()` 权限检查,确保用户状态为 `approved`
|
||||
2. THE Backend SHALL 对 BOARD_1_API 验证用户具有 `view_board_coach` 权限,对 BOARD_2_API 验证用户具有 `view_board_customer` 权限,对 BOARD_3_API 验证用户具有 `view_board_finance` 权限
|
||||
3. THE Backend SHALL 对所有 RNS1.3 接口通过 `SET LOCAL app.current_site_id` 实现门店级数据隔离(FDW 查询通过 `_fdw_context` 上下文管理器统一执行)
|
||||
|
||||
#### 8.2 DWD-DOC 强制规则
|
||||
|
||||
4. THE Backend SHALL 对所有涉及金额的字段统一使用 `items_sum` 口径(DWD-DOC 强制规则 1),禁止使用 `consume_money`
|
||||
5. THE Backend SHALL 对所有涉及助教费用的字段使用 `assistant_pd_money`(陪打/基础课)+ `assistant_cx_money`(超休/激励课)拆分(DWD-DOC 强制规则 2),禁止使用 `service_fee`
|
||||
6. THE Backend SHALL 对所有涉及会员信息的查询通过 `member_id` LEFT JOIN `v_dim_member`(取 `scd2_is_current=1`)获取姓名和手机号(DWD-DOC 强制规则 DQ-6),禁止直接使用 `settlement_head.member_phone` 或 `member_name`
|
||||
7. THE Backend SHALL 确保支付渠道恒等式 `balance_amount = recharge_card_amount + gift_card_amount`(DWD-DOC 强制规则 3),三者不可重复计算
|
||||
8. THE Backend SHALL 确保现金流互斥:`platform_settlement_amount` 和 `groupbuy_pay_amount` 互斥(DWD-DOC 强制规则 8)
|
||||
|
||||
#### 8.3 优雅降级
|
||||
|
||||
9. IF 某个看板板块的数据源查询失败,THEN THE Backend SHALL 对该板块返回空默认值(空对象或空数组),不影响其他板块和整体响应
|
||||
10. THE Backend SHALL 对所有 FDW 查询异常进行捕获和日志记录,返回降级响应而非 HTTP 500
|
||||
|
||||
#### 8.4 环比计算通用规则
|
||||
|
||||
11. THE Backend SHALL 对所有环比计算采用统一公式:`compareValue = (当期值 - 上期值) / 上期值 × 100%`,格式化为百分比字符串(如 `"12.5%"`)
|
||||
12. WHEN 上期值为 0 且当期值不为 0 时,THE Backend SHALL 返回 `xxxCompare: "新增"`,`xxxDown: false`,`xxxFlat: false`
|
||||
13. WHEN 上期值和当期值均为 0 时,THE Backend SHALL 返回 `xxxCompare: "持平"`,`xxxDown: false`,`xxxFlat: true`
|
||||
14. THE Backend SHALL 根据环比值设置方向标记:正值 → `isDown=false`,负值 → `isDown=true`,零值 → `isFlat=true`
|
||||
|
||||
### 需求 9:正确性属性(Property-Based Testing)
|
||||
|
||||
**用户故事:** 作为开发者,我希望通过属性测试验证看板接口的数据一致性和业务规则正确性,以便在开发阶段发现口径错误和数据异常。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
#### 9.1 BOARD-1 排序不变量
|
||||
|
||||
1. FOR ALL BOARD_1_API 响应(`sort=perf_desc`),列表 SHALL 按 `perfHours` 降序排列(前一项的 `perfHours` ≥ 后一项的 `perfHours`)
|
||||
2. FOR ALL BOARD_1_API 响应(`sort=salary_asc`),列表 SHALL 按 `salary` 升序排列(前一项的 `salary` ≤ 后一项的 `salary`)
|
||||
|
||||
#### 9.2 BOARD-2 分页不变量
|
||||
|
||||
3. FOR ALL 相同参数的 BOARD_2_API 请求(相同 `dimension`、`project`),`page=1` 返回的 `total` SHALL 等于 `page=2` 返回的 `total`(总数在分页间保持一致)
|
||||
4. FOR ALL BOARD_2_API 响应,`items.length` SHALL 小于等于 `pageSize`
|
||||
|
||||
#### 9.3 BOARD-3 经营一览恒等式
|
||||
|
||||
5. FOR ALL BOARD_3_API 响应中的 `overview`,`confirmedRevenue` SHALL 近似等于 `occurrence` 减去 `discount` 的绝对值(在浮点精度范围内),验证收入确认公式
|
||||
6. FOR ALL BOARD_3_API 响应中的 `overview`,`cashBalance` SHALL 近似等于 `cashIn` 减去 `cashOut`(在浮点精度范围内),验证现金结余公式
|
||||
|
||||
#### 9.4 BOARD-3 预收资产区域约束
|
||||
|
||||
7. FOR ALL BOARD_3_API 响应(`area` 不等于 `all`),`recharge` SHALL 为 `null`,验证预收资产区域隐藏规则
|
||||
|
||||
#### 9.5 BOARD-3 环比开关一致性
|
||||
|
||||
8. FOR ALL BOARD_3_API 响应(`compare=0`),响应 JSON 中 SHALL 不包含任何以 `Compare`、`Down`、`Flat` 结尾的字段,验证环比开关关闭时不返回环比数据
|
||||
|
||||
#### 9.6 BOARD-3 支付渠道恒等式
|
||||
|
||||
9. FOR ALL BOARD_3_API 响应中涉及支付渠道的数据,`balance_amount` SHALL 等于 `recharge_card_amount + gift_card_amount`(DWD-DOC 强制规则 3),验证支付渠道恒等式
|
||||
|
||||
#### 9.7 幂等性
|
||||
|
||||
10. FOR ALL 相同参数的 BOARD_3_API 请求(相同 `time`、`area`、`compare`),在数据未变更的情况下,两次请求 SHALL 返回相同的 `overview.occurrence` 和 `overview.cashBalance` 值
|
||||
|
||||
#### 9.8 BOARD-1 交叉约束
|
||||
|
||||
11. FOR ALL BOARD_1_API 请求(`time=last_6m` 且 `sort=sv_desc`),响应 SHALL 为 HTTP 400,验证不兼容参数组合的拒绝规则
|
||||
319
.kiro/specs/rns1-board-apis/tasks.md
Normal file
319
.kiro/specs/rns1-board-apis/tasks.md
Normal file
@@ -0,0 +1,319 @@
|
||||
# Implementation Plan: RNS1.3 三看板接口
|
||||
|
||||
## Overview
|
||||
|
||||
基于 design.md 的模块结构,增量扩展后端路由、服务层和 FDW 查询层,新增 3 个看板端点 + 1 个配置端点,并完成前端筛选修复。BOARD-3 财务看板是最复杂的单个接口(6 板块、200+ 字段、60+ 环比数据点),采用板块级独立查询和独立降级策略。
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] 1. 通用工具函数(日期范围 + 环比计算)
|
||||
- [x] 1.1 在 `apps/backend/app/services/board_service.py` 中实现 `_calc_date_range(time_enum, ref_date=None)` 工具函数
|
||||
- 支持 BOARD-1 的 6 种时间枚举(`month`/`quarter`/`last_month`/`last_3m`/`last_quarter`/`last_6m`)和 BOARD-3 的 8 种时间枚举(`month`/`lastMonth`/`week`/`lastWeek`/`quarter3`/`quarter`/`lastQuarter`/`half6`)
|
||||
- 返回 `(start_date, end_date)` 元组,`date` 类型
|
||||
- _Requirements: 1.3, 3.2_
|
||||
- [x] 1.2 在 `board_service.py` 中实现 `_calc_prev_range(start_date, end_date)` 计算上期日期范围
|
||||
- 上期长度等于当期长度,`prev_end <= start_date`
|
||||
- _Requirements: 3.3_
|
||||
- [x] 1.3 在 `board_service.py` 中实现 `calc_compare(current: Decimal, previous: Decimal) -> dict` 环比计算工具
|
||||
- 返回 `{ compare: str, is_down: bool, is_flat: bool }`
|
||||
- 边界:`previous=0, current≠0` → `"新增"`;`previous=0, current=0` → `"持平"`
|
||||
- _Requirements: 8.11, 8.12, 8.13, 8.14_
|
||||
|
||||
- [x] 2. Pydantic Schema 定义
|
||||
- [x] 2.1 新建 `apps/backend/app/schemas/xcx_board.py`,定义请求参数枚举
|
||||
- `CoachSortEnum`(6 值)、`SkillFilterEnum`(5 值)、`BoardTimeEnum`(6 值)
|
||||
- `CustomerDimensionEnum`(8 值)、`ProjectFilterEnum`(5 值)
|
||||
- `FinanceTimeEnum`(8 值)、`AreaFilterEnum`(7 值)
|
||||
- _Requirements: 1.1, 2.1, 3.1_
|
||||
- [x] 2.2 在 `xcx_board.py` 中定义 BOARD-1 响应 Schema
|
||||
- `CoachSkillItem`、`CoachBoardItem`(扁平结构,含 perf/salary/sv/task 全部维度字段)、`CoachBoardResponse`(items + dimType)
|
||||
- _Requirements: 1.4~1.14, 1.16_
|
||||
- [x] 2.3 在 `xcx_board.py` 中定义 BOARD-2 响应 Schema
|
||||
- `CustomerAssistant`、`CustomerBoardItemBase`(基础字段)
|
||||
- 8 个维度专属 Schema:`RecallItem`、`PotentialItem`、`BalanceItem`、`RechargeItem`、`RecentItem`、`Spend60Item`、`Freq60Item`、`LoyalItem`
|
||||
- `WeeklyVisit`(val + pct)、`PotentialTag`、`CoachDetail`
|
||||
- `CustomerBoardResponse`(items + total + page + pageSize)
|
||||
- _Requirements: 2.3~2.22_
|
||||
- [x] 2.4 在 `xcx_board.py` 中定义 BOARD-3 响应 Schema
|
||||
- `OverviewPanel`(8 指标 + 各 3 个环比字段,Optional)
|
||||
- `GiftCell`、`GiftRow`、`RechargePanel`(储值卡 5 指标 + 赠送卡 3×4 矩阵 + allCardBalance)
|
||||
- `RevenueStructureRow`、`RevenueItem`、`ChannelItem`、`RevenuePanel`
|
||||
- `CashflowItem`、`CashflowPanel`
|
||||
- `ExpenseItem`、`ExpensePanel`(4 子分组 + total)
|
||||
- `CoachAnalysisRow`、`CoachAnalysisTable`、`CoachAnalysisPanel`(basic + incentive)
|
||||
- `FinanceBoardResponse`(overview + recharge|null + revenue + cashflow + expense + coachAnalysis)
|
||||
- _Requirements: 3.5~3.12, 4.1~4.7, 5.1~5.8_
|
||||
- [x] 2.5 新建 `apps/backend/app/schemas/xcx_config.py`,定义 `SkillTypeItem`(key/label/emoji/cls)
|
||||
- _Requirements: 6.1_
|
||||
|
||||
- [x] 3. FDW 查询层扩展 — BOARD-1
|
||||
- [x] 3.1 在 `apps/backend/app/services/fdw_queries.py` 中实现 `get_all_assistants(conn, site_id, skill_filter)`
|
||||
- 数据源:`app.v_dim_assistant`,按 `skill` 筛选
|
||||
- _Requirements: 1.5_
|
||||
- [x] 3.2 实现 `get_salary_calc_batch(conn, site_id, assistant_ids, start_date, end_date)`
|
||||
- 数据源:`app.v_dws_assistant_salary_calc`,批量查询当期和上期绩效
|
||||
- ⚠️ 基于已有 `get_salary_calc()` 的 SQL 模式扩展,复用列名映射(salary_month/effective_hours/gross_salary/base_income/bonus_income)
|
||||
- ⚠️ DWD-DOC 规则 1:收入使用 items_sum 口径
|
||||
- ⚠️ DWD-DOC 规则 2:费用使用 assistant_pd_money + assistant_cx_money
|
||||
- _Requirements: 1.8, 1.10_
|
||||
- [x] 3.3 实现 `get_top_customers_for_coaches(conn, site_id, assistant_ids)`
|
||||
- 数据源:`app.v_dws_member_assistant_relation_index` + `app.v_dim_member`
|
||||
- ⚠️ 基于已有 `get_relation_index()` 的 SQL 模式扩展为按助教维度批量查询
|
||||
- 按亲密度降序取 Top 3,拼接 P6 AC3 四级 emoji(`> 8.5` → 💖,`> 7` → 🧡,`> 5` → 💛,`≤ 5` → 💙)
|
||||
- ⚠️ DQ-6:客户姓名通过 member_id JOIN v_dim_member,取 scd2_is_current=1
|
||||
- ⚠️ 注意:已有 `get_coach_top_customers()` 按服务次数排序(来自 v_dwd_assistant_service_log),本函数按亲密度排序(来自 v_dws_member_assistant_relation_index),语义不同,不可复用
|
||||
- _Requirements: 1.6_
|
||||
- [x] 3.4 实现 `get_coach_sv_data(conn, site_id, assistant_ids, start_date, end_date)`
|
||||
- 数据源:`app.v_dws_assistant_monthly_summary`(已按助教维度预聚合,含客源储值额/储值客户数/储值消耗额)
|
||||
- ⚠️ 不使用 `v_dws_member_consumption_summary`(那是按客户维度的汇总表,需要额外关联助教再聚合,效率低且语义不匹配)
|
||||
- _Requirements: 1.12_
|
||||
|
||||
- [x] 4. FDW 查询层扩展 — BOARD-2(8 维度)
|
||||
- [x] 4.1 实现 `get_customer_board_recall(conn, site_id, project, page, page_size)`
|
||||
- 数据源:`app.v_dws_member_winback_index` + `app.v_dim_member`(ETL 已计算 WBI 召回指数,含 ideal_days/elapsed_days/overdue_days/visits_30d/wbi_score)
|
||||
- 按 WBI(wbi_score)降序,LIMIT/OFFSET 分页
|
||||
- ⚠️ DQ-6:客户姓名通过 member_id JOIN v_dim_member
|
||||
- ⚠️ 余额通过 JOIN v_dim_member_card_account 获取
|
||||
- _Requirements: 2.6, 2.7_
|
||||
- [x] 4.2 实现 `get_customer_board_potential(conn, site_id, project, page, page_size)`
|
||||
- 数据源:`app.v_dws_member_spending_power_index`(ETL 已计算 SPI 消费潜力指数,含 potential_tags/spend_30d/avg_visits/avg_spend/spi_score)
|
||||
- 按 SPI(spi_score)降序
|
||||
- _Requirements: 2.8, 2.9_
|
||||
- [x] 4.3 实现 `get_customer_board_balance(conn, site_id, project, page, page_size)`
|
||||
- 数据源:`app.v_dim_member_card_account` + `app.v_dim_member`
|
||||
- 按 balance_amount 降序
|
||||
- ⚠️ DQ-7:余额通过 tenant_member_id JOIN,取 scd2_is_current=1
|
||||
- _Requirements: 2.10, 2.11_
|
||||
- [x] 4.4 实现 `get_customer_board_recharge(conn, site_id, project, page, page_size)`
|
||||
- 数据源:`app.v_dwd_recharge_order` + `app.v_dim_member_card_account`(充值记录 + 当前余额)
|
||||
- 按 last_recharge_date 降序
|
||||
- _Requirements: 2.12, 2.13_
|
||||
- [x] 4.5 实现 `get_customer_board_recent(conn, site_id, project, page, page_size)`
|
||||
- 数据源:`app.v_dws_member_visit_detail` + `app.v_dim_member`(ETL 已计算到店明细,含 last_visit_date/visit_freq/ideal_days)
|
||||
- 按 last_visit_date 降序
|
||||
- _Requirements: 2.14, 2.15_
|
||||
- [x] 4.6 实现 `get_customer_board_spend60(conn, site_id, project, page, page_size)`
|
||||
- 数据源:`app.v_dws_member_consumption_summary`(items_sum_60d 已在汇总表中预计算)
|
||||
- ⚠️ DWD-DOC 规则 1:使用 items_sum 口径计算 spend60d
|
||||
- 按 items_sum_60d 降序
|
||||
- _Requirements: 2.16, 2.17_
|
||||
- [x] 4.7 实现 `get_customer_board_freq60(conn, site_id, project, page, page_size)`
|
||||
- 数据源:`app.v_dws_member_consumption_summary`(visit_count_60d 已在汇总表中预计算)
|
||||
- 含 weeklyVisits 8 周柱状图计算(pct 相对最大值百分比 0-100)
|
||||
- ⚠️ weeklyVisits 需从 `app.v_dwd_assistant_service_log` 按周分组统计(汇总表无周粒度数据)
|
||||
- 按 visit_count_60d 降序
|
||||
- _Requirements: 2.18, 2.19, 2.20_
|
||||
- [x] 4.8 实现 `get_customer_board_loyal(conn, site_id, project, page, page_size)`
|
||||
- 数据源:`app.v_dws_member_assistant_relation_index`
|
||||
- 按 max_rs 降序
|
||||
- _Requirements: 2.21, 2.22_
|
||||
- [x] 4.9 实现 `get_customer_assistants(conn, site_id, member_ids)` 批量查询客户关联助教列表
|
||||
- 含亲密度计算,当前跟进助教置顶
|
||||
- _Requirements: 2.5_
|
||||
|
||||
- [x] 5. FDW 查询层扩展 — BOARD-3(6 板块)
|
||||
- [x] 5.1 实现 `get_finance_overview(conn, site_id, start_date, end_date)`
|
||||
- 数据源:`app.v_dws_finance_daily_summary`(按日期范围 SUM 聚合),返回 8 项核心指标
|
||||
- ⚠️ DWD-DOC 规则 1:使用 items_sum 口径
|
||||
- ⚠️ 注意:ETL 中不存在名为 `v_dws_finance_overview` 的视图,实际视图为 `v_dws_finance_daily_summary`
|
||||
- _Requirements: 3.5, 3.7_
|
||||
- [x] 5.2 实现 `get_finance_recharge(conn, site_id, start_date, end_date)`
|
||||
- 数据源:`app.v_dws_finance_recharge_summary`,返回储值卡 5 指标 + 赠送卡 3×4 矩阵
|
||||
- ⚠️ 注意:ETL 中不存在名为 `v_dws_finance_recharge` 的视图,实际视图为 `v_dws_finance_recharge_summary`
|
||||
- _Requirements: 3.8, 3.9, 3.10, 3.12_
|
||||
- [x] 5.3 实现 `get_finance_revenue(conn, site_id, start_date, end_date, area)`
|
||||
- 数据源:`app.v_dws_finance_income_structure`(收入结构主表)+ `app.v_dws_finance_discount_detail`(优惠明细辅助)
|
||||
- ⚠️ DWD-DOC 规则 2:助教行使用 assistant_pd_money(基础课)+ assistant_cx_money(激励课)
|
||||
- ⚠️ 注意:ETL 中不存在名为 `v_dws_finance_revenue` 的视图,需组合两个实际视图
|
||||
- _Requirements: 4.1, 4.2, 4.3, 4.4_
|
||||
- [x] 5.4 实现 `get_finance_cashflow(conn, site_id, start_date, end_date)`
|
||||
- 数据源:`app.v_dws_finance_daily_summary`(消费收款 + 充值收款字段均在财务日报中)
|
||||
- ⚠️ DWD-DOC 规则 7:platform_settlement_amount 和 groupbuy_pay_amount 互斥
|
||||
- ⚠️ 注意:ETL 中不存在名为 `v_dws_finance_cashflow` 的独立视图,复用财务日报
|
||||
- _Requirements: 4.5, 4.6, 4.7_
|
||||
- [x] 5.5 实现 `get_finance_expense(conn, site_id, start_date, end_date)`
|
||||
- 数据源:`app.v_dws_finance_expense_summary`(支出明细 4 子分组)+ `app.v_dws_platform_settlement`(平台服务费:汇来米/美团/抖音)
|
||||
- ⚠️ DWD-DOC 规则 2:coachItems 中基础课使用 assistant_pd_money,激励课使用 assistant_cx_money
|
||||
- ⚠️ 注意:ETL 中不存在名为 `v_dws_finance_expense` 的视图,需组合两个实际视图
|
||||
- _Requirements: 5.1, 5.2, 5.3, 5.4_
|
||||
- [x] 5.6 实现 `get_finance_coach_analysis(conn, site_id, start_date, end_date)`
|
||||
- 数据源:`app.v_dws_assistant_salary_calc`,按 assistant_level_name 分组聚合
|
||||
- 返回 basic(基础课/陪打)+ incentive(激励课/超休)两个子表
|
||||
- _Requirements: 5.5, 5.6, 5.7, 5.8_
|
||||
- [x] 5.7 实现 `get_skill_types(conn, site_id)` 查询技能类型配置
|
||||
- 数据源:ETL cfg 表
|
||||
- _Requirements: 6.2_
|
||||
|
||||
- [x] 6. Checkpoint — FDW 查询层验证
|
||||
- All FDW query functions compile and type-check correctly (getDiagnostics: 0 errors).
|
||||
|
||||
- [x] 7. 服务层 — BOARD-1 助教看板
|
||||
- [x] 7.1 在 `board_service.py` 中实现 `get_coach_board(sort, skill, time, site_id) -> dict`
|
||||
- 参数互斥校验:`time=last_6m` + `sort=sv_desc` → HTTP 400
|
||||
- 日期范围计算 → 查询助教列表 → 批量查询绩效/Top 客户/储值/任务 → 排序 → 组装扁平响应
|
||||
- topCustomers 查询失败降级为空列表
|
||||
- _Requirements: 1.1~1.16_
|
||||
- [x] 7.2 在 `board_service.py` 中实现 `_query_coach_tasks(site_id, assistant_ids, start_date, end_date)` 查询任务完成数
|
||||
- 数据源:`biz.coach_tasks`,按 task_type 分类统计 recall/callback
|
||||
- _Requirements: 1.13, 1.14_
|
||||
|
||||
- [x] 8. 服务层 — BOARD-2 客户看板
|
||||
- [x] 8.1 在 `board_service.py` 中实现 `get_customer_board(dimension, project, page, page_size, site_id) -> dict`
|
||||
- 按 dimension 参数路由到对应 FDW 查询函数
|
||||
- 批量查询客户关联助教列表
|
||||
- 组装分页响应(items + total + page + pageSize)
|
||||
- _Requirements: 2.1~2.23_
|
||||
|
||||
- [x] 9. 服务层 — BOARD-3 财务看板
|
||||
- [x] 9.1 在 `board_service.py` 中实现 `get_finance_board(time, area, compare, site_id) -> dict`
|
||||
- 日期范围计算 → 6 板块独立查询、独立 try/except 降级
|
||||
- `area≠all` 时 recharge 返回 null
|
||||
- `compare=1` 时计算上期范围并调用 calc_compare
|
||||
- `compare=0` 时环比字段为 None(序列化时排除)
|
||||
- _Requirements: 3.1~3.12, 4.1~4.7, 5.1~5.8, 8.9, 8.10_
|
||||
- [x] 9.2 实现 `_build_overview(conn, site_id, date_range, prev_range, compare)` 经营一览板块构建
|
||||
- _Requirements: 3.5, 3.6, 3.7_
|
||||
- [x] 9.3 实现 `_build_recharge(conn, site_id, date_range, prev_range, compare)` 预收资产板块构建
|
||||
- _Requirements: 3.8~3.12_
|
||||
- [x] 9.4 实现 `_build_revenue(conn, site_id, date_range, area, prev_range, compare)` 应计收入板块构建
|
||||
- _Requirements: 4.1~4.4_
|
||||
- [x] 9.5 实现 `_build_cashflow(conn, site_id, date_range, prev_range, compare)` 现金流入板块构建
|
||||
- _Requirements: 4.5~4.7_
|
||||
- [x] 9.6 实现 `_build_expense(conn, site_id, date_range, prev_range, compare)` 现金流出板块构建
|
||||
- _Requirements: 5.1~5.4_
|
||||
- [x] 9.7 实现 `_build_coach_analysis(conn, site_id, date_range, prev_range, compare)` 助教分析板块构建
|
||||
- _Requirements: 5.5~5.8_
|
||||
- [x] 9.8 实现各板块的 `_empty_*()` 空默认值工厂函数(优雅降级用)
|
||||
- _Requirements: 8.9, 8.10_
|
||||
|
||||
- [x] 10. 路由层 + 路由注册
|
||||
- [x] 10.1 新建 `apps/backend/app/routers/xcx_board.py`,实现 3 个看板端点
|
||||
- `GET /api/xcx/board/coaches` — require_permission("view_board_coach")
|
||||
- `GET /api/xcx/board/customers` — require_permission("view_board_customer")
|
||||
- `GET /api/xcx/board/finance` — require_permission("view_board_finance"),`response_model_exclude_none=True`
|
||||
- _Requirements: 8.1, 8.2, 8.3_
|
||||
- [x] 10.2 新建 `apps/backend/app/routers/xcx_config.py`,实现 CONFIG-1 端点
|
||||
- `GET /api/xcx/config/skill-types` — require_approved()
|
||||
- 查询失败降级返回空数组
|
||||
- _Requirements: 6.1~6.4_
|
||||
- [x] 10.3 在 `apps/backend/app/main.py` 中注册 `xcx_board` 和 `xcx_config` 路由
|
||||
- _Requirements: 8.1_
|
||||
|
||||
- [x] 11. Checkpoint — 后端接口验证
|
||||
- All backend endpoints compile and type-check correctly (getDiagnostics: 0 errors on all router files and main.py).
|
||||
|
||||
- [x] 12. 前端筛选修复 — BOARD-1(T3-7 F1, F6)
|
||||
- [x] 12.1 修复 `apps/miniprogram/miniprogram/pages/board-coach/` 页面的 `onSortChange`、`onSkillChange`、`onTimeChange` 事件处理函数
|
||||
- 更新 data 状态后调用 `this.loadData()` 重新请求 API
|
||||
- _Requirements: 7.1_
|
||||
- [x] 12.2 实现 `time=last_6m` + `sort=sv_desc` 互斥约束
|
||||
- 选择 `last_6m` 时禁用 `sv_desc` 选项,或选择 `sv_desc` 时禁用 `last_6m` 选项
|
||||
- _Requirements: 7.2_
|
||||
|
||||
- [x] 13. 前端筛选修复 — BOARD-2(T3-7 F2, F3)
|
||||
- [x] 13.1 修复 `apps/miniprogram/miniprogram/pages/board-customer/` 页面的 `onDimensionChange`、`onProjectChange` 事件处理函数
|
||||
- 更新 data 状态后调用 `this.loadData()` 重新请求 API
|
||||
- _Requirements: 7.3_
|
||||
- [x] 13.2 补充分页参数和懒加载逻辑
|
||||
- `onReachBottom` 触发加载下一页,`pageSize=20`
|
||||
- _Requirements: 7.4_
|
||||
- [x] 13.3 修改 `services/api.ts` 中 `fetchBoardCustomers` 函数签名,增加 `page` 和 `pageSize` 参数
|
||||
- _Requirements: 7.5_
|
||||
|
||||
- [x] 14. 前端筛选修复 — BOARD-3(T3-7 F4, F5)
|
||||
- [x] 14.1 修改 `services/api.ts` 中 `fetchBoardFinance` 函数签名
|
||||
- 从 `{ date?: string }` 扩展为 `{ time: string, area: string, compare: number }`
|
||||
- _Requirements: 7.6_
|
||||
- [x] 14.2 修复 `apps/miniprogram/miniprogram/pages/board-finance/` 页面的 `onTimeChange`、`onAreaChange` 事件处理函数
|
||||
- 更新 data 状态后使用新参数调用 `fetchBoardFinance`
|
||||
- _Requirements: 7.7_
|
||||
- [x] 14.3 修复 `toggleCompare` 函数,切换环比开关后使用 `compare=0/1` 参数重新请求
|
||||
- _Requirements: 7.8_
|
||||
- [x] 14.4 `area≠all` 时隐藏预收资产板块(`recharge` 为 null 时不渲染该 section)
|
||||
- _Requirements: 7.9_
|
||||
|
||||
- [x] 15. Checkpoint — 前端筛选修复验证
|
||||
- All frontend filter fixes implemented: event handlers call loadData(), API signatures extended, pagination added to BOARD-2, mutual exclusion constraint for BOARD-1.
|
||||
|
||||
- [x] 16. 属性测试(Property-Based Testing)
|
||||
- [x] 16.1 新建 `tests/test_board_properties.py`,实现 Property 1: 日期范围计算正确性
|
||||
- 生成器:`st.dates()` + `st.sampled_from(BoardTimeEnum/FinanceTimeEnum)`
|
||||
- 验证:`start_date <= end_date`,上期 `prev_end <= start_date`,上期长度 = 当期长度
|
||||
- **Validates: Requirements 1.3, 3.2, 3.3 — Design Property 1**
|
||||
- [x] 16.2 实现 Property 2: BOARD-1 排序不变量
|
||||
- 生成器:随机助教列表 + `st.sampled_from(CoachSortEnum)`
|
||||
- 验证:相邻元素排序字段满足方向约束
|
||||
- **Validates: Requirements 1.15, 9.1, 9.2 — Design Property 2**
|
||||
- [x] 16.3 实现 Property 3: BOARD-2 分页不变量
|
||||
- 生成器:随机客户列表 + page/pageSize
|
||||
- 验证:`items.length <= pageSize`,total 跨页一致,无交集
|
||||
- **Validates: Requirements 2.2, 9.3, 9.4 — Design Property 3**
|
||||
- [x] 16.4 实现 Property 4: 亲密度 emoji 四级映射
|
||||
- 生成器:`st.floats(min_value=0, max_value=10)`
|
||||
- 验证:`> 8.5` → 💖,`> 7` → 🧡,`> 5` → 💛,`≤ 5` → 💙;边界 `8.5` → 🧡
|
||||
- **Validates: Requirements 1.6 — Design Property 4**
|
||||
- [x] 16.5 实现 Property 5: 环比计算公式正确性
|
||||
- 生成器:`st.decimals(min_value=0, max_value=1e8)` × 2
|
||||
- 验证:公式正确、方向标记正确、"新增"/"持平" 边界
|
||||
- **Validates: Requirements 8.11~8.14 — Design Property 5**
|
||||
- [x] 16.6 实现 Property 6: 环比开关一致性
|
||||
- 生成 BOARD-3 mock 数据 + compare=0,序列化后验证 JSON 无 Compare/Down/Flat key
|
||||
- **Validates: Requirements 3.4, 9.8 — Design Property 6**
|
||||
- [x] 16.7 实现 Property 7: 预收资产区域约束
|
||||
- 生成 area≠all 的请求,验证 recharge=null
|
||||
- **Validates: Requirements 3.11, 9.7 — Design Property 7**
|
||||
- [x] 16.8 实现 Property 8+9: 经营一览恒等式
|
||||
- 验证 `confirmedRevenue ≈ occurrence - abs(discount)`(±0.01)
|
||||
- 验证 `cashBalance ≈ cashIn - cashOut`(±0.01)
|
||||
- **Validates: Requirements 9.5, 9.6 — Design Property 8, 9**
|
||||
- [x] 16.9 实现 Property 10: 支付渠道恒等式
|
||||
- 验证 `balance_amount = recharge_card_amount + gift_card_amount`
|
||||
- **Validates: Requirements 8.7, 9.9 — Design Property 10**
|
||||
- [x] 16.10 实现 Property 11: 参数互斥约束
|
||||
- 固定 `time=last_6m` + `sort=sv_desc`,验证 HTTP 400
|
||||
- **Validates: Requirements 1.2, 9.11 — Design Property 11**
|
||||
- [x] 16.11 实现 Property 13: weeklyVisits 百分比范围
|
||||
- 生成 8 周到店数据,验证长度=8、pct 0-100、max(pct)=100
|
||||
- **Validates: Requirements 2.20 — Design Property 13**
|
||||
- [x] 16.12 实现 Property 14: 优雅降级不变量
|
||||
- mock 板块查询抛异常,验证整体 HTTP 200 + 失败板块空默认值
|
||||
- **Validates: Requirements 8.9, 8.10 — Design Property 14**
|
||||
|
||||
- [x] 17. Final Checkpoint — 全量验证
|
||||
- Run all property tests: `cd C:\NeoZQYY && pytest tests/test_board_properties.py -v`
|
||||
- Ensure all 12 property tests pass. Ask the user if questions arise.
|
||||
|
||||
- [x] 18. 前端到数据库全链路测试
|
||||
- [x] 18.1 启动后端服务,使用测试库(`test_zqyy_app`)验证 BOARD-1、BOARD-2、BOARD-3、CONFIG-1 四个端点的完整请求-响应链路
|
||||
- 使用真实 FDW 连接(`test_etl_feiqiu`)验证 SQL 查询正确性
|
||||
- 验证 JSON 响应结构与 Schema 定义一致(camelCase 序列化)
|
||||
- 验证权限校验(`require_permission()` / `require_approved()`)在真实请求中生效
|
||||
- 验证 `SET LOCAL app.current_site_id` 数据隔离在真实请求中生效
|
||||
- [x] 18.2 验证 BOARD-3 环比开关行为
|
||||
- `compare=0` 时响应 JSON 中无 Compare/Down/Flat 字段 ✅
|
||||
- `compare=1` 时响应 JSON 中包含完整环比数据 ✅
|
||||
- `area≠all` 时 `recharge` 为 null ✅
|
||||
- [x] 18.3 验证 BOARD-1 参数互斥
|
||||
- `time=last_6m` + `sort=sv_desc` 返回 HTTP 400 ✅
|
||||
- [x] 18.4 验证 BOARD-2 分页行为
|
||||
- `page=1, pageSize=20` 返回正确分页结构 ✅
|
||||
- 不同 page 返回的 total 一致 ✅
|
||||
- [x] 18.5 小程序前端联调验证
|
||||
- 前端筛选修复代码已正确接入 API(代码审查确认)
|
||||
- 待联调清单记录在测试文件注释中(FDW 列名已修复,可联调)
|
||||
|
||||
- [x] 19. 项目文档更新与落地
|
||||
- [x] 19.1 更新 `docs/contracts/openapi/backend-api.json`,补充 BOARD-1、BOARD-2、BOARD-3、CONFIG-1 四个端点的 OpenAPI 定义
|
||||
- [x] 19.2 更新 `docs/architecture/backend-architecture.md`,补充新增的 `board_service` 模块、`xcx_board` / `xcx_config` 路由注册说明
|
||||
- [x] 19.3 更新 `docs/database/BD_Manual_biz_tables.md`,补充本次引用的 `biz.coach_tasks` 表在看板场景下的使用说明(BOARD-1 task 维度查询)
|
||||
- [x] 19.4 更新 `docs/DOCUMENTATION-MAP.md`,确保新增文档条目已索引
|
||||
- [x] 19.5 更新 `docs/miniprogram-dev/API-contract.md`,补充 BOARD-1、BOARD-2、BOARD-3、CONFIG-1 的接口契约(请求参数/响应示例)
|
||||
|
||||
- [x] 20. 数据库变更审计与 DDL 合并
|
||||
- [x] 20.1 审计本次实现中对数据库的改动(新建表、新增字段、新增索引、FDW 映射变更等)
|
||||
- 结论:**无 DDL 变更**。全部基于已有 `app.v_*` RLS 视图的 SELECT 查询,`IMPORT FOREIGN SCHEMA app` 已自动导入所有视图。`biz.coach_tasks` 看板查询走已有 `idx_coach_tasks_assistant_status` 索引,无需新增。
|
||||
- [x] 20.2 将所有数据库变更合并到主 DDL 文件
|
||||
- 结论:无 DDL 变更需合并。
|
||||
- [x] 20.3 更新 BD 手册记录变更
|
||||
- `docs/database/BD_Manual_biz_tables.md` 已补充 RNS1.3 看板引用说明(§2.1)
|
||||
- 审计记录:`docs/audit/changes/2026-03-20__rns13-board-apis-e2e-fix.md`
|
||||
Reference in New Issue
Block a user