272 lines
19 KiB
Markdown
272 lines
19 KiB
Markdown
# Implementation Plan: RNS1.2 客户与助教接口
|
||
|
||
## Overview
|
||
|
||
基于 design.md 架构,按 T2-1 ~ T2-6 任务结构增量实现 CUST-1、CUST-2、COACH-1 三个接口。先扩展 FDW 查询层,再逐步构建 service → router → 集成测试 → 属性测试。所有金额使用 `items_sum` 口径,助教费用使用 `assistant_pd_money` + `assistant_cx_money` 拆分,会员信息通过 `member_id` JOIN `v_dim_member`。
|
||
|
||
## Tasks
|
||
|
||
- [x] 1. Pydantic Schema 定义与项目结构搭建
|
||
- [x] 1.1 创建 `apps/backend/app/schemas/xcx_customers.py`,定义 CUST-1 和 CUST-2 所有响应模型
|
||
- `CustomerDetailResponse`、`CustomerRecordsResponse` 及所有嵌套模型(`AiInsight`、`AiStrategy`、`MetricItem`、`CoachTask`、`FavoriteCoach`、`CoachServiceItem`、`ConsumptionRecord`、`RetentionClue`、`CustomerNote`、`ServiceRecordItem`)
|
||
- 所有模型继承 `CamelModel`,确保 camelCase 序列化
|
||
- _Requirements: 1.1-1.7, 2.1-2.3, 3.1-3.2, 4.1-4.6, 5.1-5.4, 6.1-6.4, 7.1-7.9_
|
||
- [x] 1.2 创建 `apps/backend/app/schemas/xcx_coaches.py`,定义 COACH-1 所有响应模型
|
||
- `CoachDetailResponse` 及所有嵌套模型(`PerformanceMetrics`、`IncomeItem`、`IncomeSection`、`CoachTaskItem`、`AbandonedTask`、`TopCustomer`、`CoachServiceRecord`、`HistoryMonth`、`CoachNoteItem`)
|
||
- _Requirements: 8.1-8.5, 9.1-9.4, 10.1-10.7, 11.1-11.5, 12.1-12.6_
|
||
|
||
- [x] 2. FDW 查询层扩展(T2-1 基础)
|
||
- [x] 2.1 在 `apps/backend/app/services/fdw_queries.py` 新增客户相关查询函数
|
||
- `get_consumption_60d(conn, site_id, member_id)` — 近 60 天消费,使用 `ledger_amount`(items_sum),过滤 `is_delete=0`
|
||
- `get_relation_index(conn, site_id, member_id)` — 关系指数列表,来源 `v_dws_member_assistant_relation_index`,按 `relation_index` 降序
|
||
- `get_consumption_records(conn, site_id, member_id, limit, offset)` — 消费记录嵌套查询,JOIN `v_dim_assistant`,过滤 `settle_type IN (1,3)` + `is_delete=0`
|
||
- `get_total_service_count(conn, site_id, member_id)` — 累计服务总次数
|
||
- `get_coach_60d_stats(conn, site_id, assistant_id, member_id)` — 特定助教对特定客户近 60 天统计
|
||
- 所有 SQL 使用 AS 别名映射(design.md 列名映射表)
|
||
- _Requirements: 1.5, 1.6, 4.3-4.6, 5.3, 6.1, 7.4, 7.7-7.8, 13.2-13.7, 13.10_
|
||
- [x] 2.2 在 `apps/backend/app/services/fdw_queries.py` 新增助教相关查询函数
|
||
- `get_assistant_info(conn, site_id, assistant_id)` — 助教基本信息,来源 `v_dim_assistant`
|
||
- `get_salary_calc_multi_months(conn, site_id, assistant_id, months)` — 批量多月绩效数据
|
||
- `get_monthly_customer_count(conn, site_id, assistant_id, months)` — 各月不重复客户数,`COUNT(DISTINCT tenant_member_id)`,过滤 `is_delete=0`
|
||
- `get_coach_top_customers(conn, site_id, assistant_id, limit=20)` — TOP 客户,JOIN `v_dim_member`(DQ-6)+ `v_dim_member_card_account`(DQ-7),consume 使用 `ledger_amount`
|
||
- `get_customer_service_records(conn, site_id, member_id, year, month, table, limit, offset)` — 按月服务记录 + 月度统计汇总
|
||
- _Requirements: 8.2, 8.4, 9.3, 10.2-10.4, 10.6-10.7, 12.3, 12.6, 13.5-13.7, 13.10_
|
||
- [x] 2.3 为新增 FDW 查询函数编写单元测试
|
||
- 测试文件:`apps/backend/tests/unit/test_fdw_queries_rns12.py`
|
||
- 验证 DQ-6 JOIN 正确性、DQ-7 余额查询、`is_delete=0` 排除、`items_sum` 口径
|
||
- _Requirements: 13.2-13.7_
|
||
|
||
- [x] 3. CUST-1 客户详情 Service + Router(T2-1 ~ T2-3)
|
||
- [x] 3.1 创建 `apps/backend/app/services/customer_service.py`,实现 `get_customer_detail()`
|
||
- 核心字段:调用 `fdw_queries.get_member_info()` → 基础信息,`get_member_balance()` → balance,`get_consumption_60d()` → consumption60d,`get_last_visit_days()` → daysSinceVisit
|
||
- 手机号脱敏逻辑(`"139****5678"` 格式)
|
||
- Banner 字段查询失败返回 `null`(需求 1.7)
|
||
- _Requirements: 1.1-1.7, 13.1-13.2_
|
||
- [x] 3.2 在 `customer_service.py` 实现 `_build_ai_insight()` 和 `_build_retention_clues()` 和 `_build_notes()`
|
||
- aiInsight:查询 `biz.ai_cache` WHERE `cache_type='app4_analysis'` AND `target_id=customerId`,解析 `cache_value` JSON
|
||
- retentionClues:查询 `public.member_retention_clue`,按 `created_at` 倒序
|
||
- notes:查询 `biz.notes` WHERE `target_type='member'`,最多 20 条,按 `created_at` 倒序
|
||
- 每个模块独立 try/except 优雅降级
|
||
- _Requirements: 2.1-2.3, 3.1-3.2, 13.8-13.9_
|
||
- [x] 3.3 在 `customer_service.py` 实现 `_build_consumption_records()`
|
||
- 调用 `fdw_queries.get_consumption_records()` 获取结算单列表
|
||
- 构建 coaches 子数组:`fee` 使用 `assistant_pd_money`(基础课)/ `assistant_cx_money`(激励课)
|
||
- `totalAmount` 使用 `items_sum` 口径,`tableFee` 使用 `table_charge_money`,`foodAmount` 使用 `goods_money`
|
||
- 过滤 `settle_type IN (1, 3)` + `is_delete=0`
|
||
- _Requirements: 4.1-4.6, 13.3-13.4, 13.7_
|
||
- [x] 3.4 在 `customer_service.py` 实现 `_build_coach_tasks()`(T2-2)
|
||
- 查询 `biz.coach_tasks` WHERE `member_id=customerId`
|
||
- 对每位助教:`fdw_queries.get_salary_calc()` 获取等级,`get_coach_60d_stats()` 获取近 60 天统计
|
||
- 映射 `levelColor`/`taskColor`/`bgClass`
|
||
- metrics 返回:服务次数、总时长、次均时长
|
||
- _Requirements: 5.1-5.4_
|
||
- [x] 3.5 在 `customer_service.py` 实现 `_build_favorite_coaches()`(T2-3)
|
||
- 调用 `fdw_queries.get_relation_index()` 获取关系指数列表,按降序排列
|
||
- emoji 映射:`relationIndex >= 0.7` → `"💖"`,`< 0.7` → `"💛"`
|
||
- stats 4 项指标:基础课时(`assistant_pd_money`)、激励课时(`assistant_cx_money`)、上课次数、充值金额
|
||
- _Requirements: 6.1-6.4_
|
||
- [x] 3.6 创建 `apps/backend/app/routers/xcx_customers.py`,注册 CUST-1 端点
|
||
- `GET /{customer_id}` → `customer_service.get_customer_detail()`
|
||
- `Depends(require_approved())` 权限检查
|
||
- 在 `main.py` 注册 router
|
||
- _Requirements: 13.1_
|
||
- [x] 3.7 为 CUST-1 编写单元测试
|
||
- 测试文件:`apps/backend/tests/unit/test_customer_detail.py`
|
||
- 验证完整响应结构、Banner 字段、aiInsight 降级、consumptionRecords 嵌套、coachTasks metrics、favoriteCoaches 排序
|
||
- _Requirements: 1.1-6.4_
|
||
|
||
- [x] 4. Checkpoint — 确保 CUST-1 所有测试通过
|
||
- 确保所有测试通过,ask the user if questions arise.
|
||
|
||
- [x] 5. CUST-2 客户服务记录 Service + Router(T2-4)
|
||
- [x] 5.1 在 `customer_service.py` 实现 `get_customer_records()`
|
||
- 接受 `year`、`month`、`table`(可选)、`page`、`page_size` 参数
|
||
- 调用 `fdw_queries.get_member_info()` → customerName/customerPhone(DQ-6)
|
||
- 调用 `fdw_queries.get_customer_service_records()` → 按月分页记录
|
||
- 聚合 `monthCount`/`monthHours`
|
||
- 调用 `fdw_queries.get_total_service_count()` → totalServiceCount(跨月)
|
||
- 每条记录含 `recordType`(`course`/`recharge`)和 `isEstimate`
|
||
- income 使用 `items_sum` 口径,排除 `is_delete!=0`
|
||
- 按 `create_time` 倒序,返回 `hasMore`
|
||
- _Requirements: 7.1-7.9_
|
||
- [x] 5.2 在 `xcx_customers.py` router 注册 CUST-2 端点
|
||
- `GET /{customer_id}/records` → `customer_service.get_customer_records()`
|
||
- Query 参数:`year: int`、`month: int (ge=1, le=12)`、`table: str | None`、`page: int (ge=1)`、`page_size: int (ge=1, le=100)`
|
||
- `Depends(require_approved())` 权限检查
|
||
- _Requirements: 7.1, 13.1_
|
||
- [x] 5.3 为 CUST-2 编写单元测试
|
||
- 测试文件:`apps/backend/tests/unit/test_customer_records.py`
|
||
- 验证按月查询、monthCount/monthHours 汇总、totalServiceCount 跨月、hasMore 分页、recordType/isEstimate
|
||
- _Requirements: 7.1-7.9_
|
||
|
||
- [x] 6. COACH-1 助教详情 Service + Router(T2-5)
|
||
- [x] 6.1 创建 `apps/backend/app/services/coach_service.py`,实现 `get_coach_detail()`
|
||
- 基础信息:`fdw_queries.get_assistant_info()` → name/avatar/skills/workYears/hireDate
|
||
- 绩效:`fdw_queries.get_salary_calc()` → monthlyHours(`effective_hours`)/monthlySalary(`gross_salary`)/perfCurrent/perfTarget
|
||
- customerBalance:`fdw_queries.get_member_balance()` 聚合该助教所有客户余额
|
||
- tasksCompleted:`biz.coach_tasks` 当月 `status='completed'` 计数
|
||
- _Requirements: 8.1-8.5_
|
||
- [x] 6.2 在 `coach_service.py` 实现 `_build_income()` 和 `_build_tier_nodes()`
|
||
- income:`thisMonth`/`lastMonth` 各含 4 项(基础课时费 `assistant_pd_money`/`base_income`、激励课时费 `assistant_cx_money`/`bonus_income`、充值提成、酒水提成)
|
||
- 从 `v_dws_assistant_salary_calc` 分别查询当月和上月(`salary_month` 为 `YYYY-MM-01`)
|
||
- tierNodes:档位节点数组(如 `[0, 100, 130, 160, 190, 220]`)
|
||
- _Requirements: 9.1-9.4_
|
||
- [x] 6.3 在 `coach_service.py` 实现 `_build_top_customers()` 和 `_build_service_records()`
|
||
- topCustomers:调用 `fdw_queries.get_coach_top_customers()`,最多 20 条
|
||
- heartEmoji 三级映射:`score >= 0.7` → `"❤️"`,`0.3 <= score < 0.7` → `"💛"`,`score < 0.3` → `"🤍"`
|
||
- consume 使用 `items_sum` 口径,balance 通过 `v_dim_member_card_account`(DQ-7),客户姓名通过 `v_dim_member`(DQ-6)
|
||
- serviceRecords:近期服务记录,income 使用 `ledger_amount`,排除 `is_delete!=0`
|
||
- _Requirements: 10.1-10.7_
|
||
- [x] 6.4 在 `coach_service.py` 实现 `_build_task_groups()` 和 `_build_notes()`
|
||
- 查询 `biz.coach_tasks` WHERE `assistant_id=coachId`
|
||
- 按 status 分组:`active` → visibleTasks,`inactive` → hiddenTasks,`abandoned` → abandonedTasks
|
||
- visible/hidden:关联 `biz.notes` 获取备注列表(`task_id` 关联,按 `created_at` 倒序)
|
||
- abandoned:取 `abandon_reason`
|
||
- notes:助教相关备注,最多 20 条
|
||
- _Requirements: 11.1-11.5_
|
||
- [x] 6.5 在 `coach_service.py` 实现 `_build_history_months()`(T2-6)
|
||
- `fdw_queries.get_salary_calc_multi_months()` → 最近 6 个月工时/工资
|
||
- `fdw_queries.get_monthly_customer_count()` → 各月客户数
|
||
- `biz.coach_tasks` → 各月回访完成数(`task_type='follow_up_visit' AND status='completed'`)和召回完成数(`task_type IN ('high_priority_recall', 'priority_recall') AND status='completed'`)
|
||
- 本月 `estimated=True`,历史月份 `estimated=False`
|
||
- 格式化:customers → `"22人"`,hours → `"87.5h"`,salary → `"¥6,950"`
|
||
- _Requirements: 12.1-12.6_
|
||
- [x] 6.6 创建 `apps/backend/app/routers/xcx_coaches.py`,注册 COACH-1 端点
|
||
- `GET /{coach_id}` → `coach_service.get_coach_detail()`
|
||
- `Depends(require_approved())` 权限检查
|
||
- 在 `main.py` 注册 router
|
||
- _Requirements: 13.1_
|
||
- [x] 6.7 为 COACH-1 编写单元测试
|
||
- 测试文件:`apps/backend/tests/unit/test_coach_detail.py`
|
||
- 验证完整响应结构、performance 6 指标、income 本月/上月、topCustomers heartEmoji、historyMonths 排序与 estimated、任务分组
|
||
- _Requirements: 8.1-12.6_
|
||
|
||
- [x] 7. Checkpoint — 确保 CUST-1 + CUST-2 + COACH-1 所有测试通过
|
||
- 确保所有测试通过,ask the user if questions arise.
|
||
|
||
- [x] 8. 优雅降级与权限校验测试
|
||
- [x] 8.1 为优雅降级编写单元测试
|
||
- 测试文件:`apps/backend/tests/unit/test_degradation_rns12.py`
|
||
- 验证 aiInsight/coachTasks/favoriteCoaches/consumptionRecords/historyMonths 各模块查询失败时返回空默认值,不影响 HTTP 200
|
||
- _Requirements: 13.8-13.9_
|
||
- [x] 8.2 为权限校验编写单元测试
|
||
- 测试文件:`apps/backend/tests/unit/test_auth_rns12.py`
|
||
- 验证未审核用户 403、客户不存在 404、助教不存在 404
|
||
- _Requirements: 13.1_
|
||
|
||
- [x] 9. 属性测试(T2-6 PBT)
|
||
- [x] 9.1 编写属性测试:消费记录金额拆分不变量
|
||
- **Property 1: 消费记录金额拆分不变量**
|
||
- 测试文件:`tests/test_rns12_properties.py`
|
||
- 生成器:`st.floats(min_value=0, max_value=1e5)` 生成 tableFee/foodAmount/coachFees
|
||
- 验证:`abs(totalAmount - (tableFee + foodAmount + sum(fees))) < 0.01`
|
||
- **Validates: Requirements 14.1, 4.4**
|
||
- [x] 9.2 编写属性测试:废单排除一致性
|
||
- **Property 2: 废单排除一致性**
|
||
- 生成器:`st.lists(st.fixed_dictionaries({is_delete: st.integers(0,2), ...}))`
|
||
- 验证:过滤后结果中所有 `is_delete == 0`
|
||
- **Validates: Requirements 14.8, 4.6, 7.8, 10.7**
|
||
- [x] 9.3 编写属性测试:助教费用拆分正确性
|
||
- **Property 3: 助教费用拆分正确性**
|
||
- 生成器:`st.floats` 生成 pd_money/cx_money + `st.sampled_from(["基础课","激励课"])`
|
||
- 验证:基础课 → pd_money,激励课 → cx_money,两者之和 = 总额
|
||
- **Validates: Requirements 14.2, 4.3, 9.2**
|
||
- [x] 9.4 编写属性测试:favoriteCoaches 排序不变量
|
||
- **Property 4: favoriteCoaches 排序不变量**
|
||
- 生成器:`st.lists(st.floats(0, 1))` 生成 relationIndex 列表
|
||
- 验证:排序后每项 ≥ 下一项
|
||
- **Validates: Requirements 14.5, 6.1**
|
||
- [x] 9.5 编写属性测试:historyMonths 排序与预估标记
|
||
- **Property 5: historyMonths 排序与预估标记**
|
||
- 生成器:`st.lists(st.dates(), min_size=1)` 生成月份列表
|
||
- 验证:降序排列,首项 `estimated=True`,其余 `False`
|
||
- **Validates: Requirements 14.6, 12.5**
|
||
- [x] 9.6 编写属性测试:列表上限约束
|
||
- **Property 6: 列表上限约束**
|
||
- 生成器:`st.integers(0, 100)` 生成记录数
|
||
- 验证:notes ≤ 20,topCustomers ≤ 20
|
||
- **Validates: Requirements 3.2, 10.1, 11.5**
|
||
- [x] 9.7 编写属性测试:月度汇总聚合正确性
|
||
- **Property 7: 月度汇总聚合正确性**
|
||
- 生成器:`st.lists(st.fixed_dictionaries({hours: st.floats(0,10), income: st.floats(0,1e4)}))`
|
||
- 验证:count=len,monthHours=sum(hours)
|
||
- **Validates: Requirements 7.6, 5.3**
|
||
- [x] 9.8 编写属性测试:daysSinceVisit 计算正确性
|
||
- **Property 8: daysSinceVisit 计算正确性**
|
||
- 生成器:`st.dates(max_value=date.today())`
|
||
- 验证:days = (today - date).days,非负整数
|
||
- **Validates: Requirements 1.6**
|
||
- [x] 9.9 编写属性测试:emoji 映射正确性
|
||
- **Property 9: emoji 映射正确性**
|
||
- 生成器:`st.floats(0, 1)` 生成 relationIndex
|
||
- 验证:CUST-1 两级映射(≥0.7→💖,<0.7→💛);COACH-1 三级映射(≥0.7→❤️,0.3-0.7→💛,<0.3→🤍)
|
||
- **Validates: Requirements 6.4**
|
||
- [x] 9.10 编写属性测试:优雅降级
|
||
- **Property 10: 优雅降级**
|
||
- 生成器:`st.sampled_from(MODULES)` 选择失败模块
|
||
- 验证:失败模块返回空默认值,其他模块正常,HTTP 200
|
||
- **Validates: Requirements 1.7, 13.8**
|
||
- [x] 9.11 编写属性测试:任务分组正确性
|
||
- **Property 11: 任务分组正确性**
|
||
- 生成器:`st.lists(st.fixed_dictionaries({status: st.sampled_from(STATUSES)}))`
|
||
- 验证:active→visible,inactive→hidden,abandoned→abandoned,无交集,并集=原集合
|
||
- **Validates: Requirements 11.1**
|
||
- [x] 9.12 编写属性测试:数据隔离不变量
|
||
- **Property 12: 数据隔离不变量**
|
||
- 生成器:`st.integers(1, 1000)` 生成 customerId/coachId
|
||
- 验证:coachTasks 每条 member_id=customerId,serviceRecords 每条 assistant_id=coachId
|
||
- **Validates: Requirements 14.3, 14.4**
|
||
- [x] 9.13 编写属性测试:分页与 hasMore 正确性
|
||
- **Property 13: 分页与 hasMore 正确性**
|
||
- 生成器:`st.integers(1,100)` total + `st.integers(1,10)` page/pageSize
|
||
- 验证:记录数 ≤ pageSize,hasMore = (total > page*pageSize)
|
||
- **Validates: Requirements 7.9**
|
||
- [x] 9.14 编写属性测试:幂等性
|
||
- **Property 14: 幂等性**
|
||
- 生成器:`st.integers(1,12)` month + `st.integers(2020,2026)` year
|
||
- 验证:f(x) == f(x) 对 monthCount/monthHours
|
||
- **Validates: Requirements 14.7**
|
||
|
||
- [x] 10. Final checkpoint — 确保所有测试通过
|
||
- 确保所有测试通过,ask the user if questions arise.
|
||
|
||
- [x] 11. 前端到数据库全链路测试
|
||
- [x] 11.1 启动后端服务,使用测试库(`test_zqyy_app`)验证 CUST-1、CUST-2、COACH-1 三个端点的完整请求-响应链路
|
||
- 使用真实 FDW 连接(`test_etl_feiqiu`)验证 SQL 查询正确性
|
||
- 验证 JSON 响应结构与 Schema 定义一致(camelCase 序列化)
|
||
- 验证权限校验(`require_approved()`)在真实请求中生效
|
||
- [x] 11.2 小程序前端联调验证(如已有对应页面)
|
||
- 确认前端页面能正确调用新增 API 并渲染数据
|
||
- 验证空数据/降级场景下前端不崩溃
|
||
- 如前端页面尚未开发,记录待联调清单供后续 RNS 任务使用
|
||
|
||
- [x] 12. 项目文档更新与落地
|
||
- [x] 12.1 更新 `docs/contracts/openapi/backend-api.json`,补充 CUST-1、CUST-2、COACH-1 三个端点的 OpenAPI 定义
|
||
- [x] 12.2 更新 `docs/architecture/backend-architecture.md`,补充新增的 `customer_service`、`coach_service` 模块及路由注册说明
|
||
- [x] 12.3 更新 `docs/database/BD_Manual_biz_tables.md`,补充本次新增/引用的 `biz.coach_tasks`、`biz.notes`、`biz.ai_cache` 表的使用说明(如有新增字段或新用法)
|
||
- [x] 12.4 更新 `docs/DOCUMENTATION-MAP.md`,确保新增文档条目已索引
|
||
- [x] 12.5 更新 `docs/miniprogram-dev/API-contract.md`,补充 CUST-1、CUST-2、COACH-1 的接口契约(请求/响应示例)
|
||
|
||
- [x] 13. 数据库变更审计与 DDL 合并
|
||
- [x] 13.1 审计本次实现中对数据库的改动(新建表、新增字段、新增索引、FDW 映射变更等)
|
||
- 检查 `biz.coach_tasks`、`biz.notes`、`biz.ai_cache`、`public.member_retention_clue` 是否需要新建或变更
|
||
- 检查 FDW 外部表映射是否需要更新(新增视图引用等)
|
||
- [x] 13.2 将所有数据库变更合并到主 DDL 文件
|
||
- 业务库变更 → `db/zqyy_app/` 对应 DDL 文件
|
||
- FDW 变更 → `db/fdw/` 对应 DDL 文件
|
||
- 编写日期前缀迁移脚本(如有 schema 变更)
|
||
- [x] 13.3 更新 BD 手册记录变更
|
||
- 业务库 → `docs/database/BD_Manual_biz_tables.md`
|
||
- FDW → `docs/database/BD_Manual_fdw.md`(如有变更)
|
||
- 记录变更原因、影响范围、回滚 SQL
|
||
|
||
## Notes
|
||
|
||
- Tasks marked with `*` are optional and can be skipped for faster MVP
|
||
- 所有金额字段统一使用 `items_sum` 口径(DWD-DOC 强制规则 1),禁止 `consume_money`
|
||
- 助教费用使用 `assistant_pd_money` + `assistant_cx_money` 拆分(DWD-DOC 强制规则 2),禁止 `service_fee`
|
||
- 会员信息通过 `member_id` JOIN `v_dim_member`(DQ-6),余额通过 `v_dim_member_card_account`(DQ-7)
|
||
- 废单排除统一使用 `is_delete=0`,禁止引用已废弃的 `dwd_assistant_trash_event`
|
||
- Property tests validate universal correctness properties from design.md
|
||
- Checkpoints ensure incremental validation
|