# 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