# 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
