# 后端架构文档 — FastAPI 服务层
> 更新日期:2026-03-19(RNS1.3 三看板接口 + CONFIG-1 技能类型)
> 位置:`apps/backend/`
> 框架:FastAPI + psycopg2(同步连接池)
> 数据库:`zqyy_app`(业务库)+ `etl_feiqiu`(ETL 库,直连只读访问)
---
## 1. 模块交互总览
```mermaid
graph TB
subgraph "微信小程序"
MP[miniprogram pages]
end
subgraph "管理后台"
ADMIN[admin-web]
end
subgraph "FastAPI 后端"
subgraph "路由层 (routers/)"
R_AUTH[xcx_auth]
R_TASKS[xcx_tasks]
R_NOTES[xcx_notes]
R_PERF[xcx_performance]
R_CUST[xcx_customers
CUST-1, CUST-2]
R_COACH[xcx_coaches
COACH-1]
R_CHAT[xcx_ai_chat]
R_CACHE[xcx_ai_cache]
R_ADMIN[admin_*]
end
subgraph "服务层 (services/)"
S_TM[task_manager
任务 CRUD + 列表扩展]
S_PS[performance_service
绩效概览 + 明细]
S_NS[note_service
备注 CRUD + 评分]
S_CS[customer_service
客户详情 + 服务记录]
S_CO[coach_service
助教详情]
S_FDW[fdw_queries
FDW 查询集中封装]
S_TG[task_generator
任务自动生成]
S_TE[task_expiry
过期检测]
S_RD[recall_detector
召回完成检测]
S_WX[wechat
微信登录/Token]
end
subgraph "中间件 + 认证"
MW[ResponseWrapperMiddleware]
AUTH[JWT + require_approved]
end
end
subgraph "数据库"
BIZ[(zqyy_app
biz / auth / public)]
ETL[(etl_feiqiu
直连 app.v_*)]
end
MP --> R_AUTH & R_TASKS & R_NOTES & R_PERF & R_CUST & R_COACH & R_CHAT
ADMIN --> R_ADMIN
R_TASKS --> S_TM
R_PERF --> S_PS
R_NOTES --> S_NS
R_CUST --> S_CS
R_COACH --> S_CO
R_CHAT --> S_TM
S_TM --> S_FDW
S_PS --> S_FDW
S_CS --> S_FDW
S_CO --> S_FDW
S_TM --> BIZ
S_PS --> BIZ
S_NS --> BIZ
S_CS --> BIZ
S_CO --> BIZ
S_FDW --> ETL
style S_FDW fill:#9f9,stroke:#333
style S_PS fill:#9f9,stroke:#333
style R_PERF fill:#9f9,stroke:#333
style S_CS fill:#9f9,stroke:#333
style S_CO fill:#9f9,stroke:#333
style R_CUST fill:#9f9,stroke:#333
style R_COACH fill:#9f9,stroke:#333
```
## 2. 路由模块清单
| 路由文件 | 前缀 | 说明 |
|----------|------|------|
| `xcx_auth.py` | `/api/xcx/auth` | 微信登录、Token 刷新、入驻申请 |
| `xcx_tasks.py` | `/api/xcx/tasks` | 任务列表(TASK-1)、详情(TASK-2)、pin/unpin、abandon/restore |
| `xcx_notes.py` | `/api/xcx/notes` | 备注 CRUD(含 score 评分) |
| `xcx_performance.py` | `/api/xcx/performance` | 绩效概览(PERF-1)、明细(PERF-2) |
| `xcx_customers.py` | `/api/xcx/customers` | 客户详情(CUST-1)、客户服务记录(CUST-2)(RNS1.2 新增) |
| `xcx_coaches.py` | `/api/xcx/coaches` | 助教详情(COACH-1)(RNS1.2 新增) |
| `xcx_board.py` | `/api/xcx/board` | 助教看板(BOARD-1)、客户看板(BOARD-2)、财务看板(BOARD-3)(RNS1.3 新增) |
| `xcx_config.py` | `/api/xcx/config` | 技能类型配置(CONFIG-1)(RNS1.3 新增) |
| `xcx_ai_chat.py` | `/api/xcx/ai` | AI 对话(SSE 流式) |
| `xcx_ai_cache.py` | `/api/xcx/ai-cache` | AI 缓存查询 |
| `admin_applications.py` | `/api/admin/applications` | 入驻审核 |
| `auth.py` | `/api/auth` | 管理后台登录 |
| `member_retention_clue.py` | `/api/member-retention-clue` | 维客线索 |
| `ops_panel.py` | `/api/ops` | 运维面板 |
| `business_day.py` | `/api/business-day` | 营业日配置 |
## 3. 服务层模块清单
| 服务文件 | 职责 | 数据源 |
|----------|------|--------|
| `task_manager.py` | 任务 CRUD、`get_task_list_v2()`、`get_task_detail()` | biz.coach_tasks + FDW |
| `performance_service.py` | `get_overview()`、`get_records()`,绩效汇总与明细 | FDW |
| `note_service.py` | 备注创建(含 score)、AI 占位、回访触发 | biz.notes |
| `customer_service.py` | 客户详情(CUST-1)、客户服务记录(CUST-2)(RNS1.2 新增) | biz.coach_tasks + biz.ai_cache + biz.notes + public.member_retention_clue + FDW |
| `coach_service.py` | 助教详情(COACH-1):绩效/收入/任务分组/TOP客户/历史月份(RNS1.2 新增) | biz.coach_tasks + biz.notes + FDW |
| `fdw_queries.py` | ETL RLS 视图查询集中封装,直连 ETL 库 + `SET LOCAL app.current_site_id` 门店隔离 | app.v_* (ETL 直连) |
| `task_generator.py` | 定时任务:基于 WBI/NCI/RS 指数自动生成助教任务 | biz + FDW |
| `task_expiry.py` | 定时任务:检测过期任务并标记 inactive | biz.coach_tasks |
| `recall_detector.py` | 事件驱动:ETL 数据更新后检测召回完成 | biz + FDW |
| `note_reclassifier.py` | 事件驱动:召回完成后回溯重分类备注 | biz.notes |
| `wechat.py` | 微信 code2session、Token 管理 | 外部 API |
| `role.py` | 角色权限查询 | auth.* |
| `scheduler.py` | 触发器调度引擎 | biz.trigger_jobs |
| `board_service.py` | 三看板编排:`get_coach_board()`、`get_customer_board()`、`get_finance_board()`,含日期范围/环比/排序/分页/降级(RNS1.3 新增) | FDW + biz.coach_tasks |
| `application.py` | 入驻申请处理 | auth.applications |
## 4. FDW 查询封装(fdw_queries.py)
所有跨库查询集中在此模块,确保 DWD-DOC 强制规则统一实施。
⚠️ 架构变更(2026-03-18):不再使用 zqyy_app 的 `fdw_etl.*` foreign table,改为通过 `get_etl_readonly_connection(site_id)` 直连 ETL 库查询 `app.v_*` RLS 视图。原因:`postgres_fdw` 不传递自定义 GUC 参数到远端连接,导致 RLS 视图的 `current_setting('app.current_site_id')` 在远端未设置而报错。
所有函数通过 `_fdw_context()` 上下文管理器:创建 ETL 直连 → `SET LOCAL app.current_site_id` → 执行查询 → 关闭连接。
| 函数 | 用途 | DWD-DOC 规则 |
|------|------|-------------|
| `get_member_info()` | 批量查询会员姓名/手机号 | DQ-6:JOIN v_dim_member |
| `get_member_balance()` | 批量查询储值卡余额 | DQ-7:JOIN v_dim_member_card_account |
| `get_last_visit_days()` | 批量查询距上次到店天数 | 废单排除:is_delete = 0 |
| `get_salary_calc()` | 助教绩效/档位/收入 | 收入用 gross_salary;费用用 base_income + bonus_income |
| `get_service_records()` | 服务记录明细(分页) | 收入用 ledger_amount + DQ-6 + 废单排除 |
| `get_service_records_for_task()` | 特定客户服务记录 | 同上 |
| `get_consumption_60d()` | 客户近 60 天消费金额(RNS1.2 新增) | 收入用 ledger_amount(items_sum)+ 废单排除 |
| `get_relation_index()` | 客户与助教关系指数列表(RNS1.2 新增) | 来源 v_dws_member_assistant_relation_index |
| `get_consumption_records()` | 客户消费记录嵌套查询(RNS1.2 新增) | items_sum + DWD-DOC 规则 2 + 废单排除 |
| `get_total_service_count()` | 客户累计服务总次数(RNS1.2 新增) | 废单排除:is_delete = 0 |
| `get_coach_60d_stats()` | 助教对客户近 60 天统计(RNS1.2 新增) | 废单排除:is_delete = 0 |
| `get_assistant_info()` | 助教基本信息(RNS1.2 新增) | 来源 v_dim_assistant |
| `get_salary_calc_multi_months()` | 批量多月绩效数据(RNS1.2 新增) | 来源 v_dws_assistant_salary_calc |
| `get_monthly_customer_count()` | 各月不重复客户数(RNS1.2 新增) | 废单排除:is_delete = 0 |
| `get_coach_top_customers()` | 助教 TOP 客户(RNS1.2 新增) | DQ-6 + DQ-7 + items_sum + 废单排除 |
| `get_customer_service_records()` | 客户按月服务记录(RNS1.2 新增) | DQ-6 + items_sum + 废单排除 |
| `get_all_assistants()` | BOARD-1:按技能筛选助教列表(RNS1.3 新增) | v_dim_assistant |
| `get_salary_calc_batch()` | BOARD-1:批量查询当期/上期绩效(RNS1.3 新增) | items_sum + assistant_pd/cx_money |
| `get_top_customers_for_coaches()` | BOARD-1:按亲密度 Top3 客户 + 四级 emoji(RNS1.3 新增) | DQ-6 + v_dws_member_assistant_relation_index |
| `get_coach_sv_data()` | BOARD-1:助教客源储值数据(RNS1.3 新增) | v_dws_assistant_monthly_summary |
| `get_customer_board_recall()` | BOARD-2:召回维度(WBI 降序)(RNS1.3 新增) | v_dws_member_winback_index + DQ-6 |
| `get_customer_board_potential()` | BOARD-2:潜力维度(SPI 降序)(RNS1.3 新增) | v_dws_member_spending_power_index |
| `get_customer_board_balance()` | BOARD-2:余额维度(RNS1.3 新增) | v_dim_member_card_account + DQ-7 |
| `get_customer_board_recharge()` | BOARD-2:充值维度(RNS1.3 新增) | v_dwd_recharge_order |
| `get_customer_board_recent()` | BOARD-2:最近到店维度(RNS1.3 新增) | v_dws_member_visit_detail |
| `get_customer_board_spend60()` | BOARD-2:60 天消费维度(RNS1.3 新增) | items_sum 口径 |
| `get_customer_board_freq60()` | BOARD-2:60 天频次维度 + weeklyVisits(RNS1.3 新增) | v_dws_member_consumption_summary |
| `get_customer_board_loyal()` | BOARD-2:忠诚度维度(RS 降序)(RNS1.3 新增) | v_dws_member_assistant_relation_index |
| `get_customer_assistants()` | BOARD-2:批量查询客户关联助教(RNS1.3 新增) | 含亲密度 + 当前跟进置顶 |
| `get_finance_overview()` | BOARD-3:经营一览 8 项指标(RNS1.3 新增) | v_dws_finance_daily_summary + items_sum |
| `get_finance_recharge()` | BOARD-3:预收资产(储值卡 + 赠送卡矩阵)(RNS1.3 新增) | v_dws_finance_recharge_summary |
| `get_finance_revenue()` | BOARD-3:应计收入结构(RNS1.3 新增) | v_dws_finance_income_structure + discount_detail |
| `get_finance_cashflow()` | BOARD-3:现金流入(RNS1.3 新增) | v_dws_finance_daily_summary + 互斥规则 7 |
| `get_finance_expense()` | BOARD-3:现金流出 4 子分组(RNS1.3 新增) | v_dws_finance_expense_summary + platform_settlement |
| `get_finance_coach_analysis()` | BOARD-3:助教分析(basic + incentive)(RNS1.3 新增) | v_dws_assistant_salary_calc |
| `get_skill_types()` | CONFIG-1:技能类型配置(RNS1.3 新增) | ETL cfg 表 |
### 列名映射(design.md 理想名 → 实际 FDW 视图列名)
RLS 视图直接暴露 DWD/DWS 原始列名,后端代码在 SQL 中使用 AS 别名转换。
**v_dwd_assistant_service_log**(基于 `dwd.dwd_assistant_service_log`,非 _ex 表):
| 代码语义 | 实际列名 | 说明 |
|----------|----------|------|
| id | `assistant_service_id` | 服务记录主键 |
| assistant_id (WHERE) | `site_assistant_id` | 与 salary_calc.assistant_id 同源 |
| member_id | `tenant_member_id` | 会员 ID |
| is_trash = false | `is_delete = 0` | 整数类型,0=正常 |
| settle_time | `create_time` | 结算时间 |
| start_time | `start_use_time` | 开始使用时间 |
| end_time | `last_use_time` | 最后使用时间 |
| service_hours | `income_seconds / 3600.0` | 折算工时(计算字段) |
| service_hours_raw | `real_use_seconds / 3600.0` | 原始工时(计算字段) |
| income (items_sum) | `ledger_amount` | 收入金额 |
| course_type | `skill_name` | 课程类型 |
| table_name | `site_table_id` | 仅有台桌 ID,无名称 |
**v_dws_assistant_salary_calc**:
| 代码语义 | 实际列名 | 说明 |
|----------|----------|------|
| calc_month | `salary_month` | date 类型,存储为 YYYY-MM-01 |
| coach_level | `assistant_level_name` | 档位名称 |
| tier_index | `tier_id` | 档位 ID |
| basic_hours | `base_hours` | 基础课时 |
| total_hours | `effective_hours` | 有效总工时 |
| total_income | `gross_salary` | 总收入 |
| basic_rate | `base_course_price` | 基础课单价 |
| incentive_rate | `bonus_course_price` | 激励课单价 |
| bonus_money | `sprint_bonus` | 冲刺奖金 |
| assistant_pd_money_total | `base_income` | 基础课总收入 |
| assistant_cx_money_total | `bonus_income` | 激励课总收入 |
> 注意:`tier_nodes`、`total_customers`、`next_tier_*`、`tier_completed` 在视图中不存在,后端使用默认值或从其他数据源推算。
## 5. 数据流向
```
小程序请求
→ JWT 认证 (require_approved)
→ 路由层 (routers/)
→ 服务层 (services/)
→ 业务库 (zqyy_app) 直连
→ ETL 库 (etl_feiqiu) 直连只读(app.v_* RLS 视图)
→ 响应包装 (ResponseWrapperMiddleware)
→ { code: 0, data: ..., message: "ok" }
```
## 6. 关键设计决策
- **ETL 直连**:所有 ETL 查询封装在 `fdw_queries.py`,通过 `_fdw_context()` 直连 ETL 库查询 `app.v_*` RLS 视图(不使用 FDW foreign table,因 postgres_fdw 不传递自定义 GUC)
- **优雅降级**:扩展字段(lastVisitDays/balance/aiSuggestion)查询失败返回 null,不影响核心响应
- **camelCase 转换**:所有小程序端响应通过 `CamelModel` 自动转换为 camelCase
- **门店隔离**:业务库通过 `site_id` 参数过滤,ETL 查询通过 `SET LOCAL app.current_site_id` + RLS 视图隔离