# 后端架构文档 — FastAPI 服务层

> 更新日期：2026-03-23（DevTrace 全链路日志模块）
> 位置：`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` | 入驻审核 |
| `admin_dev_trace.py` | `/api/admin/dev-trace` | 开发调试全链路日志（日期/请求/详情/清理/设置/覆盖率，8 端点） |
| `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-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_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-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" }
```

## 7. Trace 全链路日志模块（`app/trace/`）

开发调试专用的全链路请求追踪模块，覆盖 HTTP 请求、SSE 流式响应、WebSocket 连接、后台 Job、异常/错误、数据库连接生命周期和中间件层。日志以 JSON Lines 格式写入本地文件系统，仅用于开发调试。

### 模块结构

| 文件 | 职责 |
|------|------|
| `config.py` | `TraceConfig` 配置类，从环境变量读取（`DEV_TRACE_*`），支持运行时动态修改（API 更新后即时生效，重启回退 .env 值） |
| `context.py` | `TraceContext` + `TraceSpan` 数据模型，基于 `contextvars.ContextVar` 实现请求级隔离，23 种 span_type |
| `writer.py` | JSON Lines 日志写入器，按日期分目录（`YYYY-MM-DD/`）、按小时分文件（`trace_YYYY-MM-DD_HH.jsonl`），单文件 10MB 自动轮转，维护 `_index.json` 索引 |
| `cleanup.py` | 日志自动清理，按 `DEV_TRACE_LOG_RETENTION_DAYS` 保留天数删除过期目录，每日凌晨自动执行 |
| `middleware.py` | `TraceMiddleware` ASGI 中间件，拦截 `xcx_*` 路由前缀请求，创建 TraceContext，记录 HTTP_IN/HTTP_OUT/MIDDLEWARE span，写入响应头（X-Request-ID 等） |
| `decorators.py` | `@trace_service` 装饰器，Service 层函数追踪（模块名、函数名、参数、返回值摘要、耗时） |
| `db_wrapper.py` | 数据库连接生命周期追踪，包装 cursor.execute()（DB_QUERY）、get_connection()（DB_CONN）、连接关闭（DB_CONN_RELEASE）、异常（DB_ERROR） |
| `error_handler.py` | 异常/错误全链路追踪，集成全局异常处理器，记录 ERROR span（HTTPException + 未捕获异常 + 堆栈摘要） |
| `sse_wrapper.py` | SSE 流式响应追踪（SSE_START/SSE_EVENT/SSE_END/AI_CALL/AI_ERROR），每 10 token 记录一次避免 span 爆炸 |
| `ws_wrapper.py` | WebSocket 连接追踪（WS_CONNECT/WS_MESSAGE/WS_DISCONNECT），trace_type 为 "ws" |
| `job_wrapper.py` | 后台 Job 执行追踪（JOB_START/JOB_END/JOB_ERROR），包装 lifespan 中注册的 4 个 job handler |
| `coverage.py` | 覆盖率扫描器，通过 AST 解析检测路由/Service/Job/SSE/WS 五个维度的追踪覆盖情况，启动时自动扫描，支持手动刷新 |

### 数据流

```
xcx_* 请求 / SSE / WebSocket / 后台 Job
  → TraceMiddleware / sse_wrapper / ws_wrapper / job_wrapper
  → 创建 TraceContext（contextvars）
  → AUTH span（鉴权层）
  → @trace_service span（Service 层）
  → DB_QUERY / DB_CONN span（数据库层）
  → ERROR span（异常处理器）
  → TraceWriter 异步写入 .jsonl 文件
  → admin_dev_trace API 读取并展示
```

### 管理 API

`admin_dev_trace.py` 路由（`/api/admin/dev-trace`，8 个端点，admin 角色鉴权）提供日志查询、手动清理、运行时设置修改和覆盖率扫描能力。前端 DevTrace 页面（`apps/admin-web/src/pages/DevTrace.tsx`）提供可视化操作界面。

## 8. 关键设计决策

- **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 视图隔离
