Files
Neo-ZQYY/docs/guides/FRONTEND-BACKEND-INTEGRATION.md
Neo 6f8f12314f feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更:
- backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔
- admin-web: ETL 状态页、任务管理、调度配置、登录优化
- miniprogram: 看板页面、聊天集成、UI 组件、导航更新
- etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强
- tenant-admin: 项目初始化
- db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8)
- packages/shared: 枚举和工具函数更新
- tools: 数据库工具、报表生成、健康检查
- docs: PRD/架构/部署/合约文档更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 00:03:48 +08:00

248 lines
13 KiB
Markdown
Raw Permalink 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.
# 前后端联调规范手册
> 最后更新2026-04-01
> 适用范围:小程序(`apps/miniprogram/`)与 FastAPI 后端(`apps/backend/`)的联调
> 约束力:高度建议参考。特殊需求可偏离但需注明原因。
> 速查索引:`.kiro/steering/frontend-backend-integration.md`fileMatch 自动加载)
---
## 一、数据契约
### 1.1 ResponseWrapper
后端 `ResponseWrapperMiddleware` 自动包装所有 2xx JSON 响应为 `{"code": 0, "data": <原始body>}`。脚本/测试调用 API 时必须从 `resp.json()["data"]` 取实际数据,不能直接用 `resp.json()`(踩坑记录 2026-03-28
### 1.2 字段命名与 CamelCase 转换
- 后端统一 `snake_case`Pydantic `CamelModel` 自动转 `camelCase` 输出
- 前端用 camelCase 读取,兼容写法:`rec.memberId ?? rec.member_id ?? 0`
- 踩坑:`me.avatar` 应为 `me.avatarUrl``avatar_url` 转换后)
### 1.3 条件显示放后端
字段需要根据业务条件决定显示/隐藏时,后端满足条件返回值、不满足返回 `null`。前端只判 `wx:if="{{field}}"`,禁止做数值比较。
组件 property 收到 `null` 不走默认值,传入前必须清洗:`r.durationRaw ?? 0``r.drinks ?? ''`
### 1.4 Schema 与 Service 同步规则
Pydantic response schema 必须与 service 返回字段严格一致,否则 500。排查 500 优先检查:
1. 数据库迁移是否已执行
2. Schema 字段是否与 service 一致
3. 环境变量是否缺失
4. 鉴权依赖是否匹配
#### 踩坑记录汇总Pydantic 系列)
| 问题 | 日期 | 表现 | 规则 |
|------|------|------|------|
| 静默丢弃未声明字段 | 2026-03-28 | 新增字段(`desc``discount_total`)前端收不到 | 新增后端返回字段时必须同步更新 Schema |
| `list[dict]` 不转换内部 key | 2026-03-28 | 前端拿到 snake_case key | 用 CamelModel 子类替代 `dict`,或 service 层手动递归转换 |
| Optional 与 None 不同步 | 2026-03-29 | service 返回 None 但 Schema 非 Optional → 500 | 修改 service 返回 None 时必须同步 Schema 为 `XxxModel | None` |
| response_model 类型不匹配 | 2026-03-29 | Schema 期望 `list[CoachSkillItem]` 但返回 `list[str]` → 500 | 修改 service 返回字段时必须同步检查 Schema 类型 |
| 嵌套 CamelModel 字段名不匹配 | 2026-03-29 | dict key 与 Schema 字段名不一致 → 500 | `_build_*` 方法必须逐字段对照 Schema |
| 含数字字段名转换 | 2026-03-29 | `consumption_60d``consumption60D`(大写 D | 前端用 `d.consumption60D ?? d.consumption60d` 兼容 |
### 1.5 接口返回结构
```json
// 列表:{ "records": [...], "total_count": 42, "has_more": true }
// 概览:{ "coach_name": "...", "income_items": [...], "this_month_records": [...] }
```
收入明细始终返回所有项(即使为 0前端不做过滤。
---
## 二、展示映射
### 2.1 头像颜色
后端只传 `member_id` + `avatar_char`,前端 `nameToAvatarColor(String(memberId))` 计算颜色 key。散客`member_id ≤ 0`)→ `'default'`(灰色),`avatar_char` 返回 `"?"`
### 2.2 课程标签
后端返回数据库原始 `skill_name`中文WXSS 不支持中文类名,前端映射:
```typescript
const COURSE_TAG_MAP: Record<string, string> = {
'陪打': 'basic', '基础课': 'basic',
'包厢': 'room', '包厢课': 'room',
'超休': 'incentive', '激励课': 'incentive', '打赏课': 'incentive',
}
```
### 2.3 角色标签
后端返回英文 code`coach`/`staff`/`head_coach`/`manager`),前端 `ROLE_LABELS` 映射中文。助教拼接等级:`${coachLevel}助教`
### 2.4 助教姓名显示规则2026-03-28
所有助教必须使用昵称/花名(`nickname`),不得显示真实姓名(`real_name`。SQL 中统一用 `COALESCE(da.nickname, da.real_name, '')`nickname 优先。
### 2.5 助教离职过滤2026-03-29
所有查询助教的 SQL 必须加 `da.leave_status = 0` 过滤离职助教。LEFT JOIN 场景用 `(da.leave_status IS NULL OR da.leave_status = 0)` 兼容无匹配行。`dim_assistant``leave_status=1` 为离职(占 74%),不过滤会展示大量已离职助教。
### 2.6 预估规则(全小程序统一)
当月且当前日期 ≤ 5号时显示"预估"标签。业务含义:每月 1-5 日为工资审核期。
### 2.7 WXSS 类名拼接
后端值用于 `class="xxx-{{value}}"` 时必须是合法 CSS 标识符(纯英文),禁止中文/hex/特殊字符。
### 2.8 前端筛选枚举同步(踩坑记录 2026-03-28
前端下拉/筛选组件的 `value` 必须与后端 Enum 值完全一致(大小写、命名)。修改后端枚举时,必须全局搜索所有使用该枚举的前端页面同步更新。
### 2.9 WXS 格式化规范
- WXS 数值方法防护:`format.wxs` 中所有调用 `.toFixed()` 的函数必须先 `parseFloat` 转数字
- TS 与 WXS 格式化互斥2026-03-27WXML 用 `{{fmt.money(field)}}`TS 层 `setData` 必须传原始数字(`?? 0`),禁止 `formatMoney()` 预格式化
- 后端 service 层同理2026-03-29前端用 WXS 格式化的字段后端必须返回原始数字float/int禁止预格式化为字符串。Schema 类型需同步从 `str` 改为 `float/int`
- 环比显示统一规范2026-03-28所有环比文本必须使用 `fmt.compareText(compare, isDown)` + `fmt.compareClass(compare, isDown, size)` WXS 函数
- WXS 零值语义2026-03-27`value === 0` 不一定是空值。`days(0)` 表示"今天到店"是有效值。新增 WXS 函数时必须区分"零值有业务含义"和"零值等同空值"
---
## 三、SQL 查询规范
### 3.1 ETL 连接复用(必须)
同一 service 方法内创建一个 `etl_conn` 传给所有 `fdw_queries.*`
### 3.2 多页面复用组件时 SQL 统一
后端为同一组件提供数据的查询必须同口径(共享 JOIN/聚合),各页面只负责过滤和分页。
### 3.3 一对多明细用 LATERAL 避免行膨胀
```sql
LEFT JOIN LATERAL (
SELECT string_agg(gs.ledger_name || '×' || gs.total_count, '') AS drinks
FROM (...GROUP BY ledger_name) gs
) gs_agg ON true
```
### 3.4 助教收入口径
- `sl.ledger_amount`:行级毛收入,不是到手
- 到手:`hours × (base_course_price - base_deduction)`
- `sh.assistant_pd_money + cx_money`:整单级汇总,禁止 JOIN 到行级
### 3.5 快照值 vs 流量值2026-03-27
- 流量值(充值/消费/收入):可以 SUM
- 快照值(卡余额 `cash_card_balance`/`gift_card_balance`/`total_card_balance`):日末快照,多天聚合取最后一天的值,禁止 SUM
### 3.6 `dim_member_card_account` 多卡膨胀2026-03-29
同一 `tenant_member_id``scd2_is_current=1` 下可能有多条记录(多张卡),必须先 `GROUP BY tenant_member_id` + `SUM(balance)` 聚合后再 JOIN。
### 3.7 台费原价推算2026-03-29
台费正价 = `table_charge_money + adjust_amount``adjust_amount > 0` 时显示原价删除线。
### 3.8 环比同期对比规则2026-03-28
当前周期 end_date cap 到今天,上期取同类周期的同期天数。已完成周期取完整周期对比再上一个完整周期。
### 3.9 优惠拆分恒等式2026-03-28
`discount_total = discount_groupbuy + discount_manual + discount_other + discount_vip + discount_gift_card + discount_rounding`。拆分展示必须包含全部 6 个子字段。
### 3.10 团购金额双口径2026-03-28
DWS `groupbuy_pay_amount` 是团购交易金额,`daily_revenue_report.py``sale_price × 0.75` 估算回款。财务看板用 DWS 口径,经营报告用估算回款口径。
### 3.11 助教财务三列口径2026-03-28
pay/share/hourly 全部从 DWS `dws_assistant_salary_calc` 计算。禁止从 DWD `ledger_amount` 取客户支付再 JOIN DWS 等级(同一助教同月可有多等级记录导致行膨胀)。
### 3.12 客源储值口径(纠正 2026-03-29
客源储值 = 该助教关联客户的卡余额合计,不是充值提成。数据源:`v_dws_member_assistant_relation_index``session_count > 0`+ `v_dim_member_card_account`(先 GROUP BY 聚合)。
### 3.13 `DATE_TRUNC` 返回 timestamp 不是 date2026-03-29
用作 dict key 时必须先 `.date()` 转换。
### 3.14 关系指数表字段名2026-03-29
`dws_member_assistant_relation_index` 的字段是 `session_count`(非 service_count`total_duration_minutes`(非 total_hours`ml_allocated_amount`(非 total_income
### 3.15 `dim_table` 主键列名2026-03-29
主键是 `table_id`(不是 `site_table_id`。JOIN 时用 `ON s.site_table_id = dt.table_id`
---
## 四、数据库操作陷阱
### 4.1 psycopg2 Windows GBK 编码2026-03-29
`psycopg2.connect()` 用关键字参数时libpq 拼接系统 locale 信息触发 `UnicodeDecodeError`。解决:用显式 DSN 字符串 + `client_encoding=UTF8` + `os.environ.setdefault("PGCLIENTENCODING", "UTF8")`
### 4.2 软删除 + ON CONFLICT
`ON CONFLICT DO UPDATE SET is_removed=false`,禁止 `DO NOTHING`
### 4.3 ON CONFLICT 精确匹配 partial unique index
列列表和 WHERE 条件必须与索引定义完全对应。
### 4.4 跨库过滤分页
先构建排除列表SQL 层统一过滤,禁止内存过滤修正 total。
### 4.5 共享连接子流程
需事务隔离时用独立连接,避免异常被外层 except 吞掉。
### 4.6 FDW 查询禁止 N+1 串行2026-03-29
必须改为 `WHERE assistant_id = ANY(%s)` 批量查询 + Python 层 dict 映射。
### 4.7 FDW 查询必须传 etl_conn2026-03-29
所有 `_build_*` 子函数都应接收并传递 `etl_conn`,否则 `_fdw_context` 在业务库连接上设置 RLS 导致空结果。
---
## 五、前端交互规范
### 5.1 页面加载态2026-03-29
统一用 `wx.showLoading` / `wx.hideLoading` 原生遮罩,禁止自定义加载组件。
### 5.2 跨页面跳转
URL 参数必须包含可查询 ID`memberId`/`taskId`),不能只传展示字段。
### 5.3 dataset 同步
TS `e.currentTarget.dataset.xxx` 必须与 WXML `data-xxx` 属性同步。
### 5.4 自定义组件静默不渲染2026-03-29
未在页面 JSON `usingComponents` 中注册的组件不报错但完全不渲染。
### 5.5 多状态列表页
后端按 status 过滤,前端需并行请求所有需要展示的状态并合并。
### 5.6 微信头像
`chooseAvatar` 组件 + `wx.uploadFile` 上传服务器,临时路径会过期。
### 5.7 Vite 代理
跨前缀接口必须在 `vite.config.ts` proxy 中添加规则。
### 5.8 权限守卫
fallback 目标页必须对当前用户无条件可访问,避免跳转死循环。
### 5.9 前后端权限一致性
后端 `/api/xcx/me` 返回 `permissions[]`,前端根据权限码动态控制可见性。新增权限码只需更新数据库 `auth.role_permissions`
### 5.10 custom-tab-bar 异步刷新
更新 `globalData.visibleTabs` 后必须主动调用 `tabBar._refreshTabs()`。看板 boardTabs 刷新必须放在 `checkPageAccess().then()` 回调中。
### 5.11 禁止页面滚动2026-03-28
`wx.setPageScrollEnabled` 不存在。用 `<page-meta page-style="overflow:hidden" />` 控制。
### 5.12 登录与 /me 接口字段差异2026-03-28
`/api/xcx/login` 返回 `WxLoginResponse`(不含 permissions`/api/xcx/me` 返回 `UserStatusResponse`(含 permissions。登录后需权限码时必须额外请求 `/me`
### 5.13 分页接口 API 函数必须返回完整响应2026-03-29
必须返回完整 `data`(含 `items`/`total`/`page`/`pageSize`),禁止只返回 `data.items`
### 5.14 懒加载分页追加必须去重 + 双条件判底2026-03-29
① 追加数据按 id 去重(`Set` 过滤);② `hasMore` 用双条件:`items.length >= pageSize && merged.length < total`
---
## 六、事件驱动触发器
- `biz.trigger_jobs``trigger_condition='event'` 的任务必须有明确的 `fire_event(event_name)` 发射端
- 任务引擎全流程由 ETL 任务 `DWS_TASK_ENGINE` 编排,通过 HTTP 调用后端 `POST /api/internal/run-job`
- `task_expiry_check` 的 interval 触发器保留(每小时),`DWS_TASK_ENGINE` 是补充而非替代
- `recall_detector` 直接生成回访任务CHANGE 2026-03-31
- 任务生成器使用客户级别升级/转移CHANGE 2026-03-31基于 `t_v / ideal_interval` ratio 决定分配范围
- 任务统计写入CHANGE 2026-03-31`task_generator.run()` 末尾调用 `_update_task_stats()`
---
## 七、ETL 调用后端 API 注意事项
- ETL 的 `AppConfig` 不设置 `os.environ`ETL 任务中用 `os.environ.get()` 前必须先 `load_dotenv`
- 后端 `ResponseWrapperMiddleware` 包装响应ETL 调用时需从 `resp.json()["data"]` 取数据
---
## 八、列表与分页
- 客户列表:默认 5 条,展开最多 20 条
- 服务记录按日期分组DateGroup默认 2 组,可展开
- 新客:本月有服务但之前无历史
- 常客:本月服务 ≥ 2 次,展示近 90 天聚合