Files
Neo-ZQYY/docs/architecture/backend-architecture.md

235 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 后端架构文档 — FastAPI 服务层
> 更新日期2026-03-19RNS1.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<br/>CUST-1, CUST-2]
R_COACH[xcx_coaches<br/>COACH-1]
R_CHAT[xcx_ai_chat]
R_CACHE[xcx_ai_cache]
R_ADMIN[admin_*]
end
subgraph "服务层 (services/)"
S_TM[task_manager<br/>任务 CRUD + 列表扩展]
S_PS[performance_service<br/>绩效概览 + 明细]
S_NS[note_service<br/>备注 CRUD + 评分]
S_CS[customer_service<br/>客户详情 + 服务记录]
S_CO[coach_service<br/>助教详情]
S_FDW[fdw_queries<br/>FDW 查询集中封装]
S_TG[task_generator<br/>任务自动生成]
S_TE[task_expiry<br/>过期检测]
S_RD[recall_detector<br/>召回完成检测]
S_WX[wechat<br/>微信登录/Token]
end
subgraph "中间件 + 认证"
MW[ResponseWrapperMiddleware]
AUTH[JWT + require_approved]
end
end
subgraph "数据库"
BIZ[(zqyy_app<br/>biz / auth / public)]
ETL[(etl_feiqiu<br/>直连 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 + ETL 直连 |
| `performance_service.py` | `get_overview()``get_records()`,绩效汇总与明细 | ETL 直连 |
| `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 + ETL 直连 |
| `coach_service.py` | 助教详情(COACH-1):绩效/收入/任务分组/TOP客户/历史月份RNS1.2 新增) | biz.coach_tasks + biz.notes + ETL 直连 |
| `fdw_queries.py` | ETL RLS 视图查询集中封装,直连 ETL 库 + `SET LOCAL app.current_site_id` 门店隔离 | app.v_* (ETL 直连) |
| `task_generator.py` | 定时任务:基于 WBI/NCI/RS 指数自动生成助教任务 | biz + ETL 直连 |
| `task_expiry.py` | 定时任务:检测过期任务并标记 inactive | biz.coach_tasks |
| `recall_detector.py` | 事件驱动ETL 数据更新后检测召回完成 | biz + ETL 直连 |
| `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 新增) | ETL 直连 + 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-6JOIN v_dim_member |
| `get_member_balance()` | 批量查询储值卡余额 | DQ-7JOIN 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_amountitems_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 客户 + 四级 emojiRNS1.3 新增) | DQ-6 + v_dws_member_assistant_relation_index |
| `get_coach_sv_data()` | BOARD-1助教客源储值数据RNS1.3 新增) | v_dws_assistant_recharge_commission |
| `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-260 天消费维度RNS1.3 新增) | items_sum 口径 |
| `get_customer_board_freq60()` | BOARD-260 天频次维度 + weeklyVisitsRNS1.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 + incentiveRNS1.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 视图隔离