# 小程序接口契约文档

<!-- AI_CHANGELOG
- 2026-03-20 | Prompt: 小程序文档过期检查与落盘 | 状态标记从"联调前草案"更新为
  "后端已实现（26 个端点全部就绪），前端联调中"；新增最后更新日期。
-->

> 创建时间：2026-03-18
> 最后更新：2026-03-20
> 状态：后端已实现（26 个端点全部就绪），前端联调中
> 基础路径：`/api/xcx`

---

## 〇、约定

- 所有接口需认证（`Authorization: Bearer <token>`），除 login/dev-login 外
- 响应统一 JSON，成功 `{ code: 0, data: ... }`，失败 `{ code: number, message: string }`
- 分页统一 `{ items: T[], total: number, page: number, pageSize: number }`
- 金额单位：元（number，保留 2 位小数）
- 时间格式：ISO 8601（`2026-03-18T10:30:00`）
- 排序参数：`sort=field:asc|desc`

---

## 一、认证模块（已实现 ✅）

### AUTH-1: 微信登录
```
POST /api/xcx/login
Body: { code: string }
Response: { access_token, refresh_token, user: { user_id, status, nickname } }
```

### AUTH-2: 开发模式登录
```
POST /api/xcx/dev-login
Body: { openid: string }
Response: 同 AUTH-1
⚠️ 生产环境必须禁用此接口（REQ-3）
```

### AUTH-3: 查询当前用户
```
GET /api/xcx/me
Response: {
  user_id: number,
  status: 'new' | 'pending' | 'approved' | 'rejected' | 'disabled',
  nickname: string,
  role: string,           // 新增（REQ-4）
  store_name: string,     // 新增（REQ-4）
  coach_level?: string,   // 新增（REQ-4），仅助教角色
  avatar?: string         // 新增（REQ-4）
}
```

### AUTH-4: 刷新 Token
```
POST /api/xcx/refresh
Body: { refresh_token: string }
Response: { access_token, refresh_token }
```

### AUTH-5: 提交入驻申请
```
POST /api/xcx/apply
Body: { name: string, phone: string, store_id: string }
Response: { user_id: number, status: 'pending' }
```

---

## 二、任务模块

### TASK-1: 任务列表

> 数据源：`zqyy_app.coach_tasks`（任务）、`etl_feiqiu.dws.*`（绩效聚合，via FDW）
> 金额口径：收入金额使用 `items_sum`（DWD-DOC 强制规则 1）

```
GET /api/xcx/tasks?status={pending|completed|abandoned}&page=1&pageSize=20
```

#### 响应结构

```typescript
interface TaskListResponse {
  items: TaskItem[]
  total: number
  page: number
  pageSize: number

  // 绩效概览（附带在任务列表响应中，避免额外请求）
  performance: PerformanceSummary
}
```

#### 子类型定义

```typescript
/** 任务项 */
interface TaskItem {
  id: string
  customerName: string
  customerAvatar: string
  taskType: 'callback' | 'priority_recall' | 'relationship' | 'high_priority'
  taskTypeLabel: string
  deadline: string                 // ISO 8601
  heartScore: number               // 0-10
  hobbies: string[]
  isPinned: boolean
  hasNote: boolean
  status: 'pending' | 'completed' | 'abandoned'

  // ── 可选扩展字段（需求 5.7.2） ──
  lastVisitDays?: number           // 距上次到店天数
  balance?: number                 // 客户余额（元）
  aiSuggestion?: string            // AI 建议摘要
}

/** 绩效概览（扩展版，需求 5.7.1）
 *  从原 4 字段扩展为 15+ 字段，供任务列表页 Banner 进度条组件使用 */
interface PerformanceSummary {
  // ── 原有字段 ──
  totalHours: number               // 本月总工时（折算后）
  totalIncome: number              // 本月总收入
  totalCustomers: number           // 本月服务客户数
  monthLabel: string               // 月份标签，如 '3月'

  // ── 扩展字段 ──
  tierNodes: number[]              // 档位节点数组，如 [0, 100, 130, 160, 190, 220]
  basicHours: number               // 基础课时
  bonusHours: number               // 激励课时
  currentTier: number              // 当前档位索引（0-based，对应 tierNodes）
  nextTierHours: number            // 下一档位工时阈值
  tierCompleted: boolean           // 是否已达最高档
  bonusMoney: number               // 升档奖金（元）
  incomeTrend: string              // 收入趋势描述，如 '↓368'
  incomeTrendDir: 'up' | 'down'    // 趋势方向
  prevMonth: string                // 上月标签，如 '1月'
  currentTierLabel: string         // 当前档位名称，如 '初级'
}
```

#### 字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| `performance.tierNodes` | number[] | 档位节点数组，前端用于绘制进度条刻度 |
| `performance.basicHours` | number | 基础课工时 |
| `performance.bonusHours` | number | 激励课工时 |
| `performance.currentTier` | number | 当前所在档位索引（0=未入档，1=初级...） |
| `performance.nextTierHours` | number | 下一档位的工时阈值 |
| `performance.tierCompleted` | boolean | 是否已达最高档位 |
| `performance.bonusMoney` | number | 达到下一档位可获得的奖金 |
| `performance.incomeTrend` | string | 与上月对比的收入变化，含方向符号 |
| `performance.incomeTrendDir` | string | `'up'` 或 `'down'`，前端用于控制颜色 |
| `performance.prevMonth` | string | 上月标签，用于对比展示 |
| `items[].lastVisitDays` | number? | 距上次到店天数，可选 |
| `items[].balance` | number? | 客户余额，可选 |
| `items[].aiSuggestion` | string? | AI 建议摘要，可选 |

### TASK-2: 任务详情
```
GET /api/xcx/tasks/{taskId}
Response: {
  // 基础信息（同 TASK-1 item）
  id, customer_name, customer_avatar, task_type, task_type_label,
  deadline, heart_score, hobbies, is_pinned, has_note, status,
  // 扩展信息
  last_visit_date?: string,
  last_spend_amount?: number,
  days_absent?: number,
  spend_trend?: 'up' | 'down' | 'flat',
  churn_risk?: 'high' | 'medium' | 'low',
  callback_reason?: string,
  preferences?: string[],
  consumption_habits?: string,
  social_preference?: string,
  // 维客线索
  retention_clues: Array<{
    tag: string,
    tag_color: 'primary' | 'success' | 'purple' | 'error',
    emoji: string,
    text: string,
    source: string,
    desc?: string
  }>,
  // 话术参考
  talking_points: string[],
  // 近期服务记录摘要
  service_summary: { total_hours: number, total_income: number, avg_income: number },
  service_records: Array<{
    table: string,
    type: string,
    type_class: 'basic' | 'vip' | 'tip' | 'recharge' | 'incentive',
    record_type?: 'course' | 'recharge',
    duration: number,
    duration_raw?: number,
    income: number,
    is_estimate?: boolean,
    drinks: string,
    date: string
  }>,
  // AI 分析
  ai_analysis: { summary: string, suggestions: string[] },
  // 备注（最近 N 条）
  notes: Array<{
    id: string,
    content: string,
    tag_type: 'customer' | 'coach' | 'system',
    tag_label: string,
    created_at: string,
    ai_score?: number,
    manual_score?: number
  }>
}
```

### TASK-3: 放弃任务
```
POST /api/xcx/tasks/{taskId}/abandon
Body: { reason: string }
Response: { status: 'abandoned' }
```

### TASK-4: 取消放弃
```
POST /api/xcx/tasks/{taskId}/restore
Response: { status: 'pending' }
```

---

## 三、备注模块

### NOTE-1: 备注列表
```
GET /api/xcx/notes?page=1&pageSize=20
Response: {
  items: Array<{
    id: string,
    content: string,
    tag_type: 'customer' | 'coach' | 'system',
    tag_label: string,
    created_at: string,
    ai_score?: number,
    manual_score?: number
  }>,
  total, page, pageSize
}
```

### NOTE-2: 新增备注
```
POST /api/xcx/notes
Body: { content: string, task_id?: string, customer_id?: string }
Response: { id: string, created_at: string }
```

### NOTE-3: 删除备注
```
DELETE /api/xcx/notes/{noteId}
Response: { success: true }
```

---

## 四、绩效模块

### PERF-1: 绩效概览

> 数据源：`etl_feiqiu.dws.*`（绩效/收入聚合，via FDW）、`zqyy_app.coaches`（助教信息）
> 金额口径：所有收入金额使用 `items_sum`（DWD-DOC 强制规则 1）；助教费用使用 `assistant_pd_money` + `assistant_cx_money` 拆分（DWD-DOC 强制规则 2）

```
GET /api/xcx/performance?year={year}&month={month}
```

#### 响应结构

```typescript
interface PerformanceOverviewResponse {
  // ── Banner 数据 ──
  coachName: string                // 助教姓名
  coachRole: string                // 角色，如 '助教'
  storeName: string                // 门店名
  monthlyIncome: string            // 本月收入（格式化），如 '¥6,206'
  lastMonthIncome: string          // 上月收入（格式化），如 '¥16,880'

  // ── 收入档位 ──
  currentTier: {
    basicRate: number              // 当前档基础课时费率（元/h）
    incentiveRate: number          // 当前档激励课时费率（元/h）
  }
  nextTier: {
    basicRate: number              // 下一档基础课时费率
    incentiveRate: number          // 下一档激励课时费率
  }
  upgradeHoursNeeded: number       // 距升档所需工时
  upgradeBonus: number             // 升档奖金（元）

  // ── 本月业绩明细（incomeItems） ──
  incomeItems: IncomeItem[]
  monthlyTotal: string             // 本月收入合计（格式化），如 '¥6,950.5'

  // ── 本月服务记录（thisMonthRecords） ──
  // 按日期分组的 DateGroup 结构
  thisMonthRecords: DateGroup[]

  // ── 新客列表（newCustomers） ──
  newCustomers: Array<{
    name: string
    avatarChar: string             // 姓氏首字
    avatarColor: string            // 头像渐变色
    lastService: string            // 最后服务日期，如 '2月7日'
    count: number                  // 服务次数
  }>

  // ── 常客列表（regularCustomers） ──
  regularCustomers: Array<{
    name: string
    avatarChar: string
    avatarColor: string
    hours: number                  // 总工时
    income: string                 // 总收入（格式化），如 '¥960'
    count: number                  // 服务次数
  }>
}
```

#### 子类型定义

```typescript
/** 业绩明细项 */
interface IncomeItem {
  icon: string                     // emoji 图标，如 '🎱'、'⭐'、'💰'、'🏆'
  label: string                    // 标签，如 '基础课'、'激励课'、'充值激励'、'TOP3 销冠奖'
  desc: string                     // 费率×工时描述，如 '80元/h × 75h'
  value: string                    // 金额（格式化），如 '¥6,000'；或文字如 '继续努力'
}

/** 按日期分组的服务记录 */
interface DateGroup {
  date: string                     // 日期标签，如 '2月7日'
  totalHours: string               // 当日总工时，如 '4.0h'
  totalIncome: string              // 当日总收入，如 '¥350'
  records: Array<{
    customerName: string
    avatarChar: string             // 姓氏首字
    avatarColor: string            // 头像渐变色
    timeRange: string              // 时间段，如 '20:00-22:00'
    hours: string                  // 工时，如 '2.0h'
    courseType: string             // 课程类型，如 '基础课'、'包厢课'
    courseTypeClass: string        // 样式类：tag-basic / tag-vip / tag-tip
    location: string               // 台桌/包厢，如 '3号台'、'VIP1号房'
    income: string                 // 收入（格式化），如 '¥160'
  }>
}
```

#### 字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| `lastMonthIncome` | string | 上月收入，用于 Banner 对比展示 |
| `currentTier` / `nextTier` | object | 当前/下一档位的基础课和激励课费率 |
| `upgradeHoursNeeded` | number | 距升档还需多少工时 |
| `upgradeBonus` | number | 升档后可获得的奖金 |
| `incomeItems[].desc` | string | 费率×工时的拆分描述，如 `"80元/h × 75h"` |
| `newCustomers[].lastService` | string | 最后服务日期 |
| `newCustomers[].count` | number | 服务次数 |
| `regularCustomers[].hours` | number | 总工时 |
| `regularCustomers[].income` | string | 总收入（`items_sum` 口径） |

### PERF-2: 绩效明细（按月）
```
GET /api/xcx/performance/records?year={year}&month={month}&page=1&pageSize=20
Response: {
  // 月度汇总
  summary: {
    total_count: number,
    total_hours: number,
    total_hours_raw: number,
    total_income: number
  },
  // 按日期分组
  date_groups: Array<{
    date: string,           // "3月18日"
    total_hours: number,
    total_income: number,
    records: Array<{
      id: string,
      customer_name: string,
      time_range: string,   // "20:00-22:00"
      hours: number,
      hours_raw?: number,
      course_type: string,
      course_type_class: string,
      location: string,
      income: number
    }>
  }>,
  has_more: boolean
}
```

---

## 五、客户模块（已实现 ✅）

### CUST-1: 客户详情

> 客户详情页完整数据，包含 Banner 概览、AI 洞察、关联助教任务、最亲密助教、消费记录（嵌套结构）和备注列表。
> 数据源：`biz.members`（会员基础信息）、`fdw_etl.dwd.dim_member`（手机号/昵称，DQ-6）、`fdw_etl.dwd.dim_member_card_account`（会员卡，DQ-7）、`fdw_etl.dwd.dwd_settlement_head`（消费记录）、`fdw_etl.dws.*`（聚合指标）、`biz.ai_cache`（AI 洞察）、`biz.tasks`（助教任务）、`biz.customer_notes`（备注）
> DWD-DOC 强制规则：金额字段使用 `items_sum` 口径（规则 1）；助教费用使用 `assistant_pd_money` + `assistant_cx_money` 拆分（规则 2）；会员信息通过 `member_id` JOIN `dim_member` 获取（规则 DQ-6），禁止直接使用 `settlement_head.member_phone`；会员卡信息通过 `member_id` JOIN `dim_member_card_account` 获取（规则 DQ-7）

```
GET /api/xcx/customers/{customerId}
```

#### 请求参数

| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `customerId` | string (path) | 是 | 客户唯一 ID |

#### 响应结构（TypeScript 类型定义）

```typescript
/** 顶层响应 */
interface CustomerDetailResponse {
  // ─── 基础信息 ───

  /** 客户唯一 ID */
  id: string
  /** 客户姓名（⚠️ 通过 member_id JOIN dim_member.nickname 获取，DQ-6） */
  name: string
  /** 脱敏手机号，如 "139****5678"（⚠️ 通过 member_id JOIN dim_member.mobile 获取，DQ-6） */
  phone: string
  /** 完整手机号（点击查看时用）（⚠️ 同上，DQ-6） */
  phoneFull: string
  /** 头像 URL */
  avatar: string
  /** 会员等级（⚠️ 通过 member_id JOIN dim_member_card_account 获取，DQ-7） */
  memberLevel: string
  /** 关系指数，如 "0.85" */
  relationIndex: string
  /** 客户标签列表 */
  tags: string[]

  // ─── Banner 概览字段 ───

  /** 客户余额（元），⚠️ 使用 `items_sum` 口径 */
  balance: number
  /** 近 60 天消费金额（元），⚠️ 使用 `items_sum` 口径 */
  consumption60d: number
  /** 理想到店间隔（天） */
  idealInterval: number
  /** 距上次到店天数 */
  daysSinceVisit: number

  // ─── AI 洞察模块 ───

  /** AI 分析洞察，数据源：biz.ai_cache（cache_type=app4_analysis） */
  aiInsight: AiInsight

  // ─── 关联助教任务列表 ───

  /** 关联助教任务 */
  coachTasks: CoachTask[]

  // ─── 最亲密助教 ───

  /** 最亲密助教列表 */
  favoriteCoaches: FavoriteCoach[]

  // ─── 维客线索 ───

  /** 维客线索列表（同 TASK-2 格式） */
  retentionClues: RetentionClue[]

  // ─── 消费记录（嵌套结构） ───

  /** 消费记录列表，⚠️ 所有金额使用 `items_sum` 口径 */
  consumptionRecords: ConsumptionRecord[]

  // ─── 备注列表 ───

  /** 客户备注 */
  notes: CustomerNote[]
}

// ─── AI 洞察 ───

interface AiInsight {
  /** AI 分析摘要文本 */
  summary: string
  /** 策略建议列表 */
  strategies: Array<{
    /** 标签颜色（CSS 类或色值） */
    color: string
    /** 策略建议文本 */
    text: string
  }>
}

// ─── 关联助教任务 ───

interface CoachTask {
  /** 助教姓名 */
  name: string
  /** 助教等级：star / senior / middle / junior */
  level: string
  /** 等级对应颜色（前端样式用） */
  levelColor: string
  /** 任务类型，如 "回访"、"召回" */
  taskType: string
  /** 任务类型对应颜色 */
  taskColor: string
  /** 背景样式类 */
  bgClass: string
  /** 任务状态，如 "进行中"、"已完成" */
  status: string
  /** 最后服务日期，如 "2026-03-01" */
  lastService: string
  /**
   * 指标列表（近 60 天统计）
   * 含：近60天次数 / 总时长 / 次均时长
   */
  metrics: MetricItem[]
}

// ─── 最亲密助教 ───

interface FavoriteCoach {
  /** 亲密度 emoji，如 "💖"、"🧡"、"💛"、"💙" */
  emoji: string
  /** 助教姓名 */
  name: string
  /** 关系指数，如 "0.92" */
  relationIndex: string
  /** 关系指数对应颜色 */
  indexColor: string
  /** 背景样式类 */
  bgClass: string
  /**
   * 统计指标列表
   * 含：基础课时（assistant_pd_money 对应）/ 激励课时（assistant_cx_money 对应）/ 上课次数 / 充值金额
   * ⚠️ 课时费用使用 assistant_pd_money + assistant_cx_money 拆分（DWD-DOC 规则 2）
   */
  stats: MetricItem[]
}

// ─── 通用指标项 ───

interface MetricItem {
  /** 指标标签，如 "近60天次数"、"基础课时" */
  label: string
  /** 指标值，如 "12次"、"45.5h"、"¥3,200" */
  value: string
  /** 可选颜色标记（高亮/警告等） */
  color?: string
}

// ─── 维客线索（同 TASK-2 格式） ───

interface RetentionClue {
  /** 线索类型 */
  type: string
  /** 线索描述 */
  text: string
}

// ─── 消费记录（嵌套结构） ───

interface ConsumptionRecord {
  /** 记录唯一 ID */
  id: string
  /** 消费类型：table（台桌消费）/ shop（酒水/商品消费）/ recharge（充值） */
  type: 'table' | 'shop' | 'recharge'
  /** 消费日期，如 "2026-03-01" */
  date: string
  /** 台桌名称（type=table 时有值） */
  tableName?: string
  /** 开始时间，如 "14:00"（type=table 时有值） */
  startTime?: string
  /** 结束时间，如 "16:30"（type=table 时有值） */
  endTime?: string
  /** 时长（分钟）（type=table 时有值） */
  duration?: number
  /**
   * 台费金额（元），⚠️ 使用 `items_sum` 口径中的 `table_charge_money`
   * type=table 时有值
   */
  tableFee?: number
  /** 台费原价（元，优惠前），type=table 时有值 */
  tableOrigPrice?: number
  /**
   * 助教服务列表
   * ⚠️ fee 使用 assistant_pd_money（基础/陪打）+ assistant_cx_money（激励/超休）拆分（DWD-DOC 规则 2）
   */
  coaches: CoachServiceItem[]
  /** 酒水/商品金额（元），⚠️ 使用 `items_sum` 口径中的 `goods_money` */
  foodAmount?: number
  /** 酒水/商品原价（元，优惠前） */
  foodOrigPrice?: number
  /**
   * 消费总金额（元）
   * ⚠️ 使用 `items_sum` 口径 = table_charge_money + goods_money + assistant_pd_money + assistant_cx_money + electricity_money
   */
  totalAmount: number
  /** 消费总原价（元，优惠前） */
  totalOrigPrice: number
  /** 支付方式，如 "储值卡"、"微信支付"、"现金" */
  payMethod: string
  /** 充值金额（元），type=recharge 时有值 */
  rechargeAmount?: number
}

/** 助教服务明细（嵌套在消费记录中） */
interface CoachServiceItem {
  /** 助教姓名 */
  name: string
  /** 助教等级：star / senior / middle / junior */
  level: string
  /** 等级对应颜色 */
  levelColor: string
  /** 课程类型，如 "基础课"、"激励课" */
  courseType: string
  /** 服务时长（小时） */
  hours: number
  /**
   * 折算工时（小时），可选
   * 激励课按系数折算后的工时
   */
  perfHours?: number
  /**
   * 服务费用（元）
   * ⚠️ 基础课 = assistant_pd_money，激励课 = assistant_cx_money（DWD-DOC 规则 2）
   */
  fee: number
}

// ─── 客户备注 ───

interface CustomerNote {
  /** 备注唯一 ID */
  id: string
  /** 标签文本，如 "跟进"、"重要" */
  tagLabel: string
  /** 创建时间，ISO 8601 格式 */
  createdAt: string
  /** 备注内容 */
  content: string
}
```

#### 字段说明表

##### 基础信息字段（8 个）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `id` | string | 客户唯一 ID | `biz.members.id` |
| `name` | string | 客户姓名 | ⚠️ `member_id` JOIN `fdw_etl.dwd.dim_member.nickname`（`scd2_is_current=1`），DQ-6 |
| `phone` | string | 脱敏手机号，如 `"139****5678"` | ⚠️ `member_id` JOIN `fdw_etl.dwd.dim_member.mobile`（`scd2_is_current=1`），DQ-6，后端脱敏 |
| `phoneFull` | string | 完整手机号（点击查看时用） | 同上，不脱敏 |
| `avatar` | string | 头像 URL | `biz.members.avatar` |
| `memberLevel` | string | 会员等级 | ⚠️ `member_id` JOIN `fdw_etl.dwd.dim_member_card_account`（`tenant_member_id=member_id`，`scd2_is_current=1`），DQ-7 |
| `relationIndex` | string | 关系指数，如 `"0.85"` | 后端根据服务频率/消费金额/亲密度综合计算 |
| `tags` | `string[]` | 客户标签列表 | `biz.customer_tags` |

##### Banner 概览字段（4 个）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `balance` | number | 客户余额（元），⚠️ `items_sum` 口径 | `fdw_etl.dws.*` 聚合，储值卡余额 |
| `consumption60d` | number | 近 60 天消费金额（元），⚠️ `items_sum` 口径 | `fdw_etl.dwd.dwd_settlement_head`，按 `settle_time` 过滤近 60 天，`SUM(items_sum)` |
| `idealInterval` | number | 理想到店间隔（天） | 后端根据历史到店频率计算 |
| `daysSinceVisit` | number | 距上次到店天数 | `fdw_etl.dwd.dwd_settlement_head`，`MAX(settle_time)` 与当前日期差值 |

##### aiInsight 模块（2 个字段）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `summary` | string | AI 分析摘要文本 | `biz.ai_cache`，`cache_type='app4_analysis'`，`target_id=customerId` |
| `strategies` | `Array<{ color, text }>` | 策略建议列表，`color` 为标签颜色，`text` 为建议文本 | 同上，JSON 解析 `cache_value` |

##### coachTasks 模块（每项 9 个字段）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `name` | string | 助教姓名 | `biz.users.nickname` |
| `level` | string | 助教等级：`star`/`senior`/`middle`/`junior` | `fdw_etl.v_dws_assistant_salary_calc.coach_level` |
| `levelColor` | string | 等级对应颜色 | 后端根据 `level` 映射 |
| `taskType` | string | 任务类型，如 `"回访"`、`"召回"` | `biz.tasks.task_type` |
| `taskColor` | string | 任务类型对应颜色 | 后端根据 `taskType` 映射 |
| `bgClass` | string | 背景样式类 | 后端根据 `level` 映射 |
| `status` | string | 任务状态，如 `"进行中"`、`"已完成"` | `biz.tasks.status` |
| `lastService` | string | 最后服务日期 | `fdw_etl.dwd.dwd_settlement_head`，该助教对该客户的 `MAX(settle_time)` |
| `metrics` | `MetricItem[]` | 近 60 天指标：次数/总时长/次均时长 | `fdw_etl.dwd.dwd_settlement_head` + `dwd_assistant_service_log_ex`，按助教+客户聚合近 60 天 |

##### favoriteCoaches 模块（每项 6 个字段）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `emoji` | string | 亲密度 emoji（💖(>8.5) / 🧡(>7) / 💛(>5) / 💙(≤5)，后端 compute_heart_icon() 根据 rs_display 映射） | 后端根据 `relationIndex` 映射 |
| `name` | string | 助教姓名 | `biz.users.nickname` |
| `relationIndex` | string | 关系指数，如 `"0.92"` | 后端根据服务频率/消费金额综合计算 |
| `indexColor` | string | 关系指数对应颜色 | 后端根据 `relationIndex` 阈值映射 |
| `bgClass` | string | 背景样式类 | 后端根据排名映射 |
| `stats` | `MetricItem[]` | 统计指标：基础课时（`assistant_pd_money`）/ 激励课时（`assistant_cx_money`）/ 上课次数 / 充值金额 | `fdw_etl.dwd.dwd_settlement_head` + `dwd_assistant_service_log_ex`，⚠️ 课时费用使用 `assistant_pd_money` + `assistant_cx_money` 拆分（DWD-DOC 规则 2） |

##### consumptionRecords 模块（每项 16 个字段）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `id` | string | 记录唯一 ID | `fdw_etl.dwd.dwd_settlement_head.id` |
| `type` | enum | 消费类型：`table`/`shop`/`recharge` | 后端根据 `settle_type` 映射（`IN (1,3)` 为正向交易） |
| `date` | string | 消费日期 | `dwd_settlement_head.settle_time`，格式化为日期 |
| `tableName` | string? | 台桌名称 | `dwd_settlement_head.table_name` |
| `startTime` | string? | 开始时间 | `dwd_settlement_head.start_time`，格式化为 `"HH:mm"` |
| `endTime` | string? | 结束时间 | `dwd_settlement_head.end_time`，格式化为 `"HH:mm"` |
| `duration` | number? | 时长（分钟） | 后端根据 `startTime`/`endTime` 计算 |
| `tableFee` | number? | 台费金额（元），⚠️ `table_charge_money` | `dwd_settlement_head.table_charge_money` |
| `tableOrigPrice` | number? | 台费原价（元，优惠前） | `dwd_settlement_head.table_charge_money` + 对应折扣 |
| `coaches` | `CoachServiceItem[]` | 助教服务列表 | `fdw_etl.dwd.dwd_assistant_service_log_ex`，关联 `settlement_id` |
| `foodAmount` | number? | 酒水/商品金额（元），⚠️ `goods_money` | `dwd_settlement_head.goods_money` |
| `foodOrigPrice` | number? | 酒水/商品原价（元，优惠前） | `dwd_settlement_head.goods_money` + 对应折扣 |
| `totalAmount` | number | 消费总金额（元），⚠️ `items_sum` 口径 | `dwd_settlement_head.items_sum` |
| `totalOrigPrice` | number | 消费总原价（元，优惠前） | `items_sum` + `adjust_amount` |
| `payMethod` | string | 支付方式 | 后端根据 `pay_type` / `balance_amount` 等字段映射 |
| `rechargeAmount` | number? | 充值金额（元），`type=recharge` 时有值 | `biz.recharge_records.amount` |

##### coaches 子数组（CoachServiceItem，每项 7 个字段）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `name` | string | 助教姓名 | `biz.users.nickname` |
| `level` | string | 助教等级 | `fdw_etl.v_dws_assistant_salary_calc.coach_level` |
| `levelColor` | string | 等级对应颜色 | 后端根据 `level` 映射 |
| `courseType` | string | 课程类型：`"基础课"` / `"激励课"` | 后端根据 `dwd_assistant_service_log_ex` 服务类型映射 |
| `hours` | number | 服务时长（小时） | `dwd_assistant_service_log_ex.service_hours` |
| `perfHours` | number? | 折算工时（小时），激励课按系数折算 | `dwd_assistant_service_log_ex`，后端按激励系数计算 |
| `fee` | number | 服务费用（元），⚠️ 基础课 = `assistant_pd_money`，激励课 = `assistant_cx_money`（DWD-DOC 规则 2） | `dwd_settlement_head.assistant_pd_money` / `assistant_cx_money` |

##### notes 模块（每项 4 个字段）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `id` | string | 备注唯一 ID | `biz.customer_notes.id` |
| `tagLabel` | string | 标签文本，如 `"跟进"`、`"重要"` | `biz.customer_notes.tag` |
| `createdAt` | string | 创建时间，ISO 8601 格式 | `biz.customer_notes.created_at` |
| `content` | string | 备注内容 | `biz.customer_notes.content` |

#### 业务规则

| 规则 | 说明 |
|------|------|
| `items_sum` 口径（DWD-DOC 规则 1） | 所有金额字段（`balance`/`consumption60d`/`tableFee`/`foodAmount`/`totalAmount` 等）使用 `items_sum = table_charge_money + goods_money + assistant_pd_money + assistant_cx_money + electricity_money`，禁止使用 `consume_money` |
| 助教费用拆分（DWD-DOC 规则 2） | `coaches[].fee` 中基础课 = `assistant_pd_money`（陪打），激励课 = `assistant_cx_money`（超休）；`favoriteCoaches[].stats` 中基础课时/激励课时同理。禁止使用 `service_fee` |
| 会员信息 JOIN（DWD-DOC 规则 DQ-6） | `name`/`phone`/`phoneFull` 必须通过 `member_id` LEFT JOIN `dwd.dim_member`（取 `scd2_is_current=1`）获取，禁止直接使用 `settlement_head.member_phone`/`member_name`（自 2025-12 起全为 NULL） |
| 会员卡 JOIN（DWD-DOC 规则 DQ-7） | `memberLevel` 必须通过 `member_id` LEFT JOIN `dwd.dim_member_card_account`（`tenant_member_id=member_id`，`scd2_is_current=1`）获取，禁止直接使用 `member_card_type_name`（自 2025-07-21 起全为 NULL） |
| `settle_type` 过滤 | 消费记录取正向交易 `settle_type IN (1, 3)`，本表无 `is_delete` 字段 |
| 废单排除 | 使用 `dwd_assistant_service_log_ex.is_trash` 排除废单，`dwd_assistant_trash_event` 已废弃 |
| `type` 枚举映射 | `table`（台桌消费）/ `shop`（酒水/商品消费）/ `recharge`（充值），后端根据 `settle_type` 和业务逻辑映射 |
| 亲密度 emoji | 💖(>8.5) / 🧡(>7) / 💛(>5) / 💙(≤5) |

#### 数据源映射表

| 模块 | 主数据源 | 辅助数据源 |
|------|---------|-----------|
| 基础信息 | `biz.members` | `fdw_etl.dwd.dim_member`（DQ-6）、`fdw_etl.dwd.dim_member_card_account`（DQ-7） |
| Banner 概览 | `fdw_etl.dwd.dwd_settlement_head` | `fdw_etl.dws.*`（余额聚合） |
| AI 洞察 | `biz.ai_cache`（`cache_type='app4_analysis'`） | — |
| 关联助教任务 | `biz.tasks` | `biz.users`、`fdw_etl.dwd.dwd_settlement_head`、`dwd_assistant_service_log_ex` |
| 最亲密助教 | `fdw_etl.dwd.dwd_assistant_service_log_ex` | `biz.users`、`fdw_etl.dwd.dwd_settlement_head` |
| 消费记录 | `fdw_etl.dwd.dwd_settlement_head` | `fdw_etl.dwd.dwd_assistant_service_log_ex`、`biz.recharge_records` |
| 备注 | `biz.customer_notes` | — |

### CUST-2: 客户服务记录
```
GET /api/xcx/customers/{customerId}/records?year={year}&month={month}&table={tableId}
Response: {
  customer_name: string,
  customer_phone: string,
  relation_index: string,
  tables: Array<{ id: string, name: string }>,
  records: Array<{
    id: string,
    date: string,
    time_range: string,
    table: string,
    type: string,
    type_class: string,
    duration: number,
    duration_raw?: number,
    income: number,
    drinks: string
  }>,
  has_more: boolean
}
```

### 客户模块错误码（CUST-1 / CUST-2 共用）

| HTTP 状态码 | 场景 | 响应示例 |
|:-----------:|------|---------|
| 403 | 用户未通过审核（`require_approved()` 拦截） | `{ "code": 403, "message": "用户未通过审核，无法访问此资源" }` |
| 404 | 客户不存在（`customerId` 无对应记录） | `{ "code": 404, "message": "客户不存在" }` |
| 422 | 请求参数校验失败（year/month/page/pageSize 不合法） | Pydantic `HTTPValidationError` |

> CUST-1 扩展模块（aiInsight/coachTasks/favoriteCoaches/consumptionRecords）查询失败时不返回错误码，而是对该模块返回空默认值（空数组或空对象），整体响应仍为 HTTP 200。

---

## 六、看板模块

### BOARD-1: 助教看板

> 助教排行看板，支持排序×技能×时间三重筛选，4 种维度卡片（定档业绩/工资/客源储值/任务）。
> 数据源：`fdw_etl.v_dws_assistant_salary_calc`（工资/定档）、`fdw_etl.v_dws_recharge_summary`（储值）、`biz.tasks`（任务）
> DWD-DOC 强制规则：金额字段使用 `items_sum` 口径（规则 1）；助教费用使用 `assistant_pd_money` + `assistant_cx_money` 拆分（规则 2）

```
GET /api/xcx/board/coaches?sort={sort}&skill={skill}&time={time}
```

#### 请求参数

| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `sort` | enum | 是 | 排序维度，6 种枚举：`perf_desc`（定档业绩最高）/ `perf_asc`（定档业绩最低）/ `salary_desc`（工资最高）/ `salary_asc`（工资最低）/ `sv_desc`（客源储值最高）/ `task_desc`（任务完成最多） |
| `skill` | enum | 是 | 技能筛选，5 种枚举：`all`（不限）/ `chinese`（🎱 中式/追分）/ `snooker`（斯诺克）/ `mahjong`（🀄 麻将/棋牌）/ `karaoke`（🎤 团建/K歌） |
| `time` | enum | 是 | 时间范围，6 种枚举：`month`（本月）/ `quarter`（本季度）/ `last_month`（上月）/ `last_3m`（前3个月不含本月）/ `last_quarter`（上季度）/ `last_6m`（最近6个月不含本月） |

#### 交叉约束

| 约束 | 说明 |
|------|------|
| `time=last_6m` + `sort=sv_desc` | ⛔ 不兼容，后端返回 HTTP 400 `{ code: 400, message: "最近6个月不支持客源储值排序" }` |

> 原因：储值数据跨 6 个月聚合开销过大，且业务意义有限。前端 `TIME_OPTIONS` 中已在 `last_6m` 选项文案标注"不支持客源储值最高"。

#### 响应结构（TypeScript 类型定义）

```typescript
/** 顶层响应 */
interface BoardCoachesResponse {
  items: CoachBoardItem[]
}

/** 助教卡片 — 基础字段 + 4 维度专属字段 */
interface CoachBoardItem {
  // ─── 基础字段（所有维度共享） ───

  /** 助教 ID */
  id: string
  /** 助教姓名 */
  name: string
  /** 姓名首字（用于头像占位符） */
  initial: string
  /** 头像渐变色标识（blue/green/pink/amber/violet/cyan 等） */
  avatarGradient: string
  /** 等级英文 key：star / senior / middle / junior */
  level: string
  /**
   * 技能标签列表
   * - text: 技能 emoji 或文字（如 '🎱'、'斯'、'🀄'、'🎤'）
   * - cls: 前端样式类（如 'skill--chinese'、'skill--snooker'）
   */
  skills: Array<{ text: string; cls: string }>
  /**
   * Top 客户列表（含亲密度 emoji 前缀）
   * 示例：['💖 王先生', '🧡 李女士', '💛 赵总', '💙 新客户']
   * 💖(>8.5) / 🧡(>7) / 💛(>5) / 💙(≤5)
   */
  topCustomers: string[]

  // ─── perf 维度专属（sort=perf_desc / perf_asc 时展示） ───

  /** 当期定档工时（小时） */
  perfHours: number
  /** 上期定档工时（小时），无上期数据时不返回 */
  perfHoursBefore?: number
  /** 距升档差距描述，如 '距升档 13.8h'；已达标时不返回 */
  perfGap?: string
  /** 是否已达标当前档位 */
  perfReached: boolean

  // ─── salary 维度专属（sort=salary_desc / salary_asc 时展示） ───

  /** 预估工资总额（元） */
  salary: number
  /** 工资维度-定档工时（小时） */
  salaryPerfHours: number
  /** 工资维度-上期定档工时（小时），无上期数据时不返回 */
  salaryPerfBefore?: number

  // ─── sv 维度专属（sort=sv_desc 时展示） ───

  /** 客源储值总额（元） */
  svAmount: number
  /** 储值客户数 */
  svCustomerCount: number
  /** 储值消耗额（元） */
  svConsume: number

  // ─── task 维度专属（sort=task_desc 时展示） ───

  /** 召回任务完成数 */
  taskRecall: number
  /** 回访任务完成数 */
  taskCallback: number
}
```

#### 字段说明表

##### 基础字段（7 个，所有维度共享）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `id` | string | 助教唯一 ID | `biz.users.id` |
| `name` | string | 助教姓名 | `biz.users.nickname` |
| `initial` | string | 姓名首字，用于头像占位符渲染 | 后端从 `name` 提取 |
| `avatarGradient` | string | 头像渐变色标识（`blue`/`green`/`pink`/`amber`/`violet`/`cyan`） | 后端根据 ID 哈希分配 |
| `level` | string | 等级英文 key：`star`/`senior`/`middle`/`junior` | `fdw_etl.v_dws_assistant_salary_calc.coach_level` |
| `skills` | `Array<{ text: string, cls: string }>` | 技能标签，`text` 为 emoji 或文字，`cls` 为前端样式类 | `biz.coach_skills` 或配置表 |
| `topCustomers` | `string[]` | Top 客户列表，含亲密度 emoji 前缀（💖/🧡/💛/💙） | 后端按亲密度排序取 Top 3，拼接 emoji + 姓名 |

##### perf 维度专属字段（4 个）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `perfHours` | number | 当期定档工时（小时） | `fdw_etl.v_dws_assistant_salary_calc`，按 `time` 参数聚合 |
| `perfHoursBefore` | number? | 上期定档工时（小时），无上期数据时不返回 | 同上，取上一周期 |
| `perfGap` | string? | 距升档差距描述，如 `'距升档 13.8h'`；已达标时不返回 | 后端根据档位阈值计算 |
| `perfReached` | boolean | 是否已达标当前档位 | 后端根据档位阈值判断 |

##### salary 维度专属字段（3 个）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `salary` | number | 预估工资总额（元），⚠️ 使用 `items_sum` 口径 | `fdw_etl.v_dws_assistant_salary_calc` |
| `salaryPerfHours` | number | 工资维度-定档工时（小时） | 同上 |
| `salaryPerfBefore` | number? | 工资维度-上期定档工时（小时），无上期数据时不返回 | 同上，取上一周期 |

##### sv 维度专属字段（3 个）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `svAmount` | number | 客源储值总额（元） | `fdw_etl.v_dws_recharge_summary`，按助教关联客户聚合 |
| `svCustomerCount` | number | 储值客户数 | 同上，`COUNT(DISTINCT customer_id)` |
| `svConsume` | number | 储值消耗额（元） | 同上，消耗金额聚合 |

##### task 维度专属字段（2 个）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `taskRecall` | number | 召回任务完成数 | `biz.tasks`，`task_type='priority_recall' AND status='completed'` |
| `taskCallback` | number | 回访任务完成数 | `biz.tasks`，`task_type='callback' AND status='completed'` |

#### 维度与排序的关系

前端根据 `sort` 参数决定展示哪组维度字段（卡片模板切换）：

| sort 值 | 维度类型 | 展示字段 |
|---------|---------|---------|
| `perf_desc` / `perf_asc` | perf | `perfHours` / `perfHoursBefore` / `perfGap` / `perfReached` |
| `salary_desc` / `salary_asc` | salary | `salary` / `salaryPerfHours` / `salaryPerfBefore` |
| `sv_desc` | sv | `svAmount` / `svCustomerCount` / `svConsume` |
| `task_desc` | task | `taskRecall` / `taskCallback` |

> 后端始终返回所有维度字段（避免多次请求），前端根据 `sort` 选择性渲染对应卡片模板。

#### 业务规则

| 规则 | 说明 |
|------|------|
| `time=last_6m` + `sort=sv_desc` → HTTP 400 | 储值数据跨 6 个月聚合开销过大，后端拒绝此组合 |
| `items_sum` 口径（DWD-DOC 规则 1） | `salary` 等金额字段使用 `items_sum` 口径，禁止使用 `consume_money` |
| 助教费用拆分（DWD-DOC 规则 2） | 工资计算中基础课 = `assistant_pd_money`，激励课 = `assistant_cx_money` |
| `levelClass` 前端计算 | 后端仅返回 `level`（英文 key），前端通过 `LEVEL_CLASS` 映射生成样式类 |
| `perfGap` 后端计算 | 后端根据档位阈值计算差距描述字符串，已达标时不返回该字段 |
| `topCustomers` emoji 规则 | 💖(>8.5) / 🧡(>7) / 💛(>5) / 💙(≤5)，基于 rs_display 值域 0-10 |

### BOARD-2: 客户看板

> 客户排行看板，支持 8 个维度查看前 100 名客户，每个维度展示不同的专属字段卡片。
> 数据源：`fdw_etl.v_dws_customer_*` 系列视图（召回/潜力/余额/充值/到店/消费/频率/专一度）、`biz.tasks`（任务关联）、`fdw_etl.dwd.dim_member`（会员信息）
> DWD-DOC 强制规则：金额字段使用 `items_sum` 口径（规则 1）；会员信息通过 `member_id` JOIN `dim_member` 获取（规则 DQ-6/DQ-7）

```
GET /api/xcx/board/customers?dimension={dim}&project={proj}&page=1&pageSize=20
```

#### 请求参数

| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `dimension` | enum | 是 | 维度，8 种枚举：`recall`（最应召回）/ `potential`（最大消费潜力）/ `balance`（最高余额）/ `recharge`（最近充值）/ `recent`（最近到店）/ `spend60`（最高消费近60天）/ `freq60`（最频繁近60天）/ `loyal`（最专一近60天） |
| `project` | enum | 是 | 项目筛选，5 种枚举：`all`（全部）/ `chinese`（🎱 中式/追分）/ `snooker`（斯诺克）/ `mahjong`（🀄 麻将/棋牌）/ `karaoke`（🎤 团建/K歌） |
| `page` | number | 否 | 页码，默认 `1` |
| `pageSize` | number | 否 | 每页条数，默认 `20` |

#### 响应结构（TypeScript 类型定义）

```typescript
/** 顶层响应 */
interface BoardCustomersResponse {
  items: CustomerBoardItem[]
  total: number
  page: number
  pageSize: number
}

// ─── 助教信息（嵌套在客户卡片中） ───

interface AssistantInfo {
  /** 助教姓名 */
  name: string
  /**
   * 前端样式类
   * - 'assistant--assignee'：当前跟进助教
   * - 'assistant--abandoned'：已放弃的助教
   * - 'assistant--normal'：普通关联助教
   */
  cls: string
  /** 亲密度分数 0-10，供 heart-icon 组件渲染 */
  heartScore: number
  /** 角标文字，如 '跟'（跟进中）/ '弃'（已放弃） */
  badge?: string
  /** 角标样式类，如 'assistant-badge--follow' / 'assistant-badge--drop' */
  badgeCls?: string
}

/** 客户卡片 — 基础字段 + 8 维度专属字段 */
interface CustomerBoardItem {
  // ─── 基础字段（所有维度共享，5 个） ───

  /** 客户唯一 ID */
  id: string
  /** 客户姓名 */
  name: string
  /** 姓名首字（用于头像占位符） */
  initial: string
  /** 头像样式类（avatar--amber / avatar--pink / avatar--blue 等） */
  avatarCls: string
  /**
   * 关联助教列表
   * 按亲密度降序排列，含跟进状态角标
   */
  assistants: AssistantInfo[]

  // ─── recall 维度专属（dimension=recall 时展示） ───

  /** 理想到店间隔（天） */
  idealDays?: number
  /** 已过天数（距上次到店） */
  elapsedDays?: number
  /** 超期天数（elapsedDays - idealDays，> 0 表示超期） */
  overdueDays?: number
  /** 近 30 天到店次数 */
  visits30d?: number
  /** 客户余额（格式化字符串，如 '¥2,680'） */
  balance?: string
  /** 召回指数（0-10 评分，如 '9.2'） */
  recallIndex?: string

  // ─── potential 维度专属（dimension=potential 时展示） ───

  /**
   * 潜力标签列表
   * - text: 标签文字（如 '高频'、'高客单'、'高余额'、'低频'）
   * - theme: 标签主题色（'primary' / 'warning' / 'success' / 'gray'）
   */
  potentialTags?: Array<{ text: string; theme: string }>
  /** 近 30 天消费（格式化字符串，如 '¥4,200'） */
  spend30d?: string
  /** 平均到店频率（如 '6.2次'） */
  avgVisits?: string
  /** 平均客单价（格式化字符串，如 '¥680'） */
  avgSpend?: string

  // ─── balance 维度专属（dimension=balance 时展示） ───

  // balance 字段复用上方 recall 维度的 balance（格式化字符串）
  /** 最后到店时间（如 '3天前'、'12天前'） */
  lastVisit?: string
  /** 月均消费（格式化字符串，如 '¥3,500'） */
  monthlyConsume?: string
  /** 可用月数（如 '约0.8个月'、'约4.6个月'） */
  availableMonths?: string

  // ─── recharge 维度专属（dimension=recharge 时展示） ───

  /** 最近充值日期（如 '2月15日'） */
  lastRecharge?: string
  /** 充值金额（格式化字符串，如 '¥5,000'） */
  rechargeAmount?: string
  /** 近 60 天充值次数（如 '2次'） */
  recharges60d?: string
  /** 当前余额（格式化字符串，如 '¥2,680'） */
  currentBalance?: string

  // ─── recent 维度专属（dimension=recent 时展示） ───

  /** 距上次到店天数 */
  daysAgo?: number
  /** 到店频率（如 '6.2次/月'） */
  visitFreq?: string
  // idealDays 复用 recall 维度字段
  // visits30d 复用 recall 维度字段
  // avgSpend 复用 potential 维度字段

  // ─── spend60 维度专属（dimension=spend60 时展示） ───

  /** 近 60 天消费（格式化字符串，如 '¥8,400'） */
  spend60d?: string
  /** 近 60 天到店次数（如 '12'） */
  visits60d?: string
  /** 是否高消费标记（true 时前端显示高消费标签） */
  highSpendTag?: boolean
  // avgSpend 复用 potential 维度字段

  // ─── freq60 维度专属（dimension=freq60 时展示） ───

  // visits60d 复用 spend60 维度字段
  /** 平均到店间隔（天，如 '5.0天'） */
  avgInterval?: string
  /**
   * 最近 8 周到店柱状图数据
   * - val: 该周到店次数
   * - pct: 百分比高度（0-100，相对于最大值）
   * 数组长度固定为 8，从最早一周到最近一周排列
   */
  weeklyVisits?: Array<{ val: number; pct: number }>
  // spend60d 复用 spend60 维度字段

  // ─── loyal 维度专属（dimension=loyal 时展示） ───

  /** 专一度指数（0-100，如 '92'） */
  intimacy?: string
  /** Top 1 助教姓名 */
  topCoachName?: string
  /** Top 1 助教亲密度（heart-icon 分数，0-10） */
  topCoachHeart?: number
  /** Top 1 助教关系指数（如 '9.2'） */
  topCoachScore?: string
  /** 主助教姓名（占比最高的助教） */
  coachName?: string
  /** 主助教服务占比（如 '78%'） */
  coachRatio?: string
  /**
   * 助教明细列表（按服务占比降序）
   * 展示每位助教的服务统计和关系指数
   */
  coachDetails?: Array<{
    /** 助教姓名 */
    name: string
    /** 前端样式类（同 AssistantInfo.cls） */
    cls: string
    /** 亲密度分数 0-10 */
    heartScore: number
    /** 角标文字（'跟' / '弃'），无角标时不返回 */
    badge?: string
    /** 平均服务时长（如 '2.3h'） */
    avgDuration: string
    /** 服务次数（如 '14'） */
    serviceCount: string
    /** 该助教关联消费金额（格式化字符串，如 '¥4,200'） */
    coachSpend: string
    /** 关系指数（0-10） */
    relationIdx: number
  }>
}
```

#### 字段说明表

##### 基础字段（5 个，所有维度共享）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `id` | string | 客户唯一 ID | `fdw_etl.dwd.dim_member.member_id` |
| `name` | string | 客户姓名 | `fdw_etl.dwd.dim_member.nickname`（通过 `member_id` JOIN，DQ-6） |
| `initial` | string | 姓名首字，用于头像占位符渲染 | 后端从 `name` 提取 |
| `avatarCls` | string | 头像样式类（`avatar--amber`/`avatar--pink`/`avatar--blue` 等） | 后端根据 ID 哈希分配 |
| `assistants` | `AssistantInfo[]` | 关联助教列表，含亲密度和跟进状态 | `biz.tasks` + `fdw_etl.dws` 亲密度计算 |

##### recall 维度专属字段（6 个）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `idealDays` | number | 理想到店间隔（天），基于历史到店频率计算 | `fdw_etl.v_dws_customer_recall` |
| `elapsedDays` | number | 距上次到店已过天数 | 同上，`CURRENT_DATE - last_visit_date` |
| `overdueDays` | number | 超期天数（`elapsedDays - idealDays`） | 后端计算 |
| `visits30d` | number | 近 30 天到店次数 | `fdw_etl.v_dws_customer_recall` |
| `balance` | string | 客户余额（格式化），⚠️ 使用 `items_sum` 口径 | `fdw_etl.dwd.dim_member_card_account` |
| `recallIndex` | string | 召回指数（0-10），综合超期天数、余额、频率加权 | 后端算法计算 |

##### potential 维度专属字段（4 个）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `potentialTags` | `Array<{ text, theme }>` | 潜力标签（高频/高客单/高余额/低频等），`theme` 取值 `primary`/`warning`/`success`/`gray` | 后端根据消费特征生成 |
| `spend30d` | string | 近 30 天消费（格式化），⚠️ 使用 `items_sum` 口径 | `fdw_etl.v_dws_customer_potential` |
| `avgVisits` | string | 平均到店频率（如 '6.2次'） | 同上 |
| `avgSpend` | string | 平均客单价（格式化），⚠️ 使用 `items_sum` 口径 | 同上，`spend / visits` |

##### balance 维度专属字段（4 个）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `balance` | string | 客户余额（格式化），与 recall 维度共用字段 | `fdw_etl.dwd.dim_member_card_account` |
| `lastVisit` | string | 最后到店时间描述（如 '3天前'） | `fdw_etl.v_dws_customer_balance` |
| `monthlyConsume` | string | 月均消费（格式化），⚠️ 使用 `items_sum` 口径 | 同上 |
| `availableMonths` | string | 可用月数（`balance / monthlyConsume`，如 '约0.8个月'） | 后端计算 |

##### recharge 维度专属字段（4 个）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `lastRecharge` | string | 最近充值日期（如 '2月15日'） | `fdw_etl.v_dws_customer_recharge` |
| `rechargeAmount` | string | 最近充值金额（格式化） | 同上 |
| `recharges60d` | string | 近 60 天充值次数（如 '2次'） | 同上 |
| `currentBalance` | string | 当前余额（格式化） | `fdw_etl.dwd.dim_member_card_account` |

##### recent 维度专属字段（5 个）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `daysAgo` | number | 距上次到店天数 | `fdw_etl.v_dws_customer_recent` |
| `visitFreq` | string | 到店频率（如 '6.2次/月'） | 同上 |
| `idealDays` | number | 理想到店间隔（天），与 recall 维度共用字段 | `fdw_etl.v_dws_customer_recall` |
| `visits30d` | number | 近 30 天到店次数，与 recall 维度共用字段 | 同上 |
| `avgSpend` | string | 平均客单价，与 potential 维度共用字段 | `fdw_etl.v_dws_customer_potential` |

##### spend60 维度专属字段（4 个）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `spend60d` | string | 近 60 天消费（格式化），⚠️ 使用 `items_sum` 口径 | `fdw_etl.v_dws_customer_spend60` |
| `visits60d` | string | 近 60 天到店次数 | 同上 |
| `highSpendTag` | boolean | 是否高消费标记（`true` 时前端显示高消费标签） | 后端根据阈值判断 |
| `avgSpend` | string | 平均客单价，与 potential 维度共用字段 | `fdw_etl.v_dws_customer_potential` |

##### freq60 维度专属字段（4 个）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `visits60d` | string | 近 60 天到店次数，与 spend60 维度共用字段 | `fdw_etl.v_dws_customer_spend60` |
| `avgInterval` | string | 平均到店间隔（如 '5.0天'） | `fdw_etl.v_dws_customer_freq60` |
| `weeklyVisits` | `Array<{ val, pct }>` | 最近 8 周到店柱状图，`val` 为次数，`pct` 为百分比高度（0-100） | 同上，按周聚合 |
| `spend60d` | string | 近 60 天消费，与 spend60 维度共用字段 | `fdw_etl.v_dws_customer_spend60` |

##### loyal 维度专属字段（7 个）

| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `intimacy` | string | 专一度指数（0-100） | `fdw_etl.v_dws_customer_loyal` |
| `topCoachName` | string | Top 1 助教姓名 | 同上，按服务占比排序取第一 |
| `topCoachHeart` | number | Top 1 助教亲密度（0-10，heart-icon 用） | 同上 |
| `topCoachScore` | string | Top 1 助教关系指数（如 '9.2'） | 同上 |
| `coachName` | string | 主助教姓名（占比最高） | 同上 |
| `coachRatio` | string | 主助教服务占比（如 '78%'） | 同上 |
| `coachDetails` | `Array<{ name, cls, heartScore, badge?, avgDuration, serviceCount, coachSpend, relationIdx }>` | 助教明细列表，按服务占比降序 | 同上，逐助教聚合 |

#### 维度与展示字段的关系

前端根据 `dimension` 参数决定展示哪组维度字段（卡片模板切换）：

| dimension 值 | 维度类型 | 展示字段 |
|-------------|---------|---------|
| `recall` | 召回 | `idealDays` / `elapsedDays` / `overdueDays` / `visits30d` / `balance` / `recallIndex` |
| `potential` | 潜力 | `potentialTags` / `spend30d` / `avgVisits` / `avgSpend` |
| `balance` | 余额 | `balance` / `lastVisit` / `monthlyConsume` / `availableMonths` |
| `recharge` | 充值 | `lastRecharge` / `rechargeAmount` / `recharges60d` / `currentBalance` |
| `recent` | 到店 | `daysAgo` / `visitFreq` / `idealDays` / `visits30d` / `avgSpend` |
| `spend60` | 消费60天 | `spend60d` / `visits60d` / `highSpendTag` / `avgSpend` |
| `freq60` | 频率60天 | `visits60d` / `avgInterval` / `weeklyVisits` / `spend60d` |
| `loyal` | 专一度 | `intimacy` / `topCoachName` / `topCoachHeart` / `topCoachScore` / `coachName` / `coachRatio` / `coachDetails` |

> 后端按 `dimension` 参数仅返回对应维度的专属字段（减少传输量），前端根据 `dimension` 选择性渲染对应卡片模板。

#### 业务规则

| 规则 | 说明 |
|------|------|
| `items_sum` 口径（DWD-DOC 规则 1） | `balance`、`spend30d`、`avgSpend`、`monthlyConsume`、`spend60d`、`coachSpend` 等金额字段使用 `items_sum` 口径，禁止使用 `consume_money` |
| 会员信息 JOIN（DWD-DOC 规则 DQ-6） | 客户姓名通过 `member_id` JOIN `dim_member`（`nickname`，`scd2_is_current=1`），禁止直接使用 `member_phone`/`member_name` |
| 会员卡余额 JOIN（DWD-DOC 规则 DQ-7） | 余额通过 `member_id` JOIN `dim_member_card_account`（`scd2_is_current=1`），禁止直接使用 `member_card_type_name` |
| `assistants` 排序规则 | 按亲密度（`heartScore`）降序排列；当前跟进助教（`cls='assistant--assignee'`）置顶 |
| `weeklyVisits` 固定 8 周 | 数组长度固定为 8，从最早一周到最近一周排列；`pct` 为相对于 8 周最大值的百分比 |
| `coachDetails` 排序规则 | 按服务占比降序排列；`badge` 仅在跟进（'跟'）或放弃（'弃'）状态时返回 |
| 分页上限 | 单次最多返回 100 条（`pageSize` 上限 100），超出截断 |

#### 数据源映射

| 维度 | FDW 视图 | 说明 |
|------|---------|------|
| recall | `fdw_etl.v_dws_customer_recall` | 召回指数、理想间隔、超期天数 |
| potential | `fdw_etl.v_dws_customer_potential` | 潜力标签、消费频率、客单价 |
| balance | `fdw_etl.v_dws_customer_balance` | 余额、月均消费、可用月数 |
| recharge | `fdw_etl.v_dws_customer_recharge` | 充值记录、充值频率 |
| recent | `fdw_etl.v_dws_customer_recent` | 到店天数、到店频率 |
| spend60 | `fdw_etl.v_dws_customer_spend60` | 60 天消费、到店次数 |
| freq60 | `fdw_etl.v_dws_customer_freq60` | 到店间隔、周柱状图 |
| loyal | `fdw_etl.v_dws_customer_loyal` | 专一度、助教明细 |
| 会员基础 | `fdw_etl.dwd.dim_member` | 姓名、手机号（DQ-6） |
| 会员卡 | `fdw_etl.dwd.dim_member_card_account` | 余额（DQ-7） |
| 助教关联 | `biz.tasks` + `biz.users` | 跟进状态、角标 |

### BOARD-3: 财务看板

> ⚠️ 全项目最复杂的单个接口，6 个独立板块嵌套结构。
> 数据源：`fdw_etl.v_dws_finance_*` 系列视图 + `fdw_etl.v_dws_assistant_salary_calc`
> DWD-DOC 强制规则：所有金额字段使用 `items_sum` 口径（规则 1）；助教费用使用 `assistant_pd_money` + `assistant_cx_money` 拆分（规则 2）

```
GET /api/xcx/board/finance?time={time}&area={area}&compare={0|1}
```

#### 请求参数

| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `time` | enum | 是 | 时间范围，8 种枚举：`month`（本月）/ `lastMonth`（上月）/ `week`（本周）/ `lastWeek`（上周）/ `quarter3`（前3个月不含本月）/ `quarter`（本季度）/ `lastQuarter`（上季度）/ `half6`（最近6个月不含本月） |
| `area` | enum | 是 | 区域筛选，7 种枚举：`all`（全部区域）/ `hall`（大厅）/ `hallA`（A区）/ `hallB`（B区）/ `hallC`（C区）/ `mahjong`（麻将房）/ `teamBuilding`（团建房） |
| `compare` | `0` \| `1` | 否 | 是否返回环比数据，默认 `0`。`compare=0` 时所有 `xxxCompare` / `isDown` / `isFlat` 字段不返回（减少查询开销） |

#### 响应结构（TypeScript 类型定义）

```typescript
/** 顶层响应 */
interface BoardFinanceResponse {
  overview: OverviewSection
  recharge: RechargeSection | null  // area ≠ all 时为 null（储值卡数据不按区域拆分）
  revenue: RevenueSection
  cashflow: CashflowSection
  expense: ExpenseSection
  coachAnalysis: CoachAnalysisSection
}

// ─── 环比字段通用模式 ───
// 每个核心指标附带 3 个环比字段：
//   xxxCompare: string    — 环比百分比，如 "12.5%"、"持平"
//   isDown: boolean       — 是否下降（true = 负向变化）
//   isFlat: boolean       — 是否持平（true 时 compare 显示 "持平"）
// compare=0 时这 3 个字段不返回

// ─── 1. 经营一览 overview ───

interface OverviewSection {
  /** 发生额/正价 — 所有消费项目按标价计算的总金额 */
  occurrence: string                    // 格式化金额，如 "¥823,456"
  occurrenceCompare?: string
  occurrenceDown?: boolean
  occurrenceFlat?: boolean

  /** 总优惠 — 会员折扣 + 赠送卡抵扣 + 团购差价等 */
  discount: string                      // 负值，如 "-¥113,336"
  discountCompare?: string
  discountDown?: boolean
  discountFlat?: boolean

  /** 折扣率 — discount / occurrence */
  discountRate: string                  // 百分比，如 "13.8%"
  discountRateCompare?: string
  discountRateDown?: boolean
  discountRateFlat?: boolean

  /** 成交/确认收入 — occurrence - discount */
  confirmedRevenue: string
  confirmedCompare?: string
  confirmedDown?: boolean
  confirmedFlat?: boolean

  /** 实收/现金流入 — 实际到账现金（消费直接支付 + 储值充值） */
  cashIn: string
  cashInCompare?: string
  cashInDown?: boolean
  cashInFlat?: boolean

  /** 现金支出 — 人工 + 房租 + 水电 + 进货等 */
  cashOut: string
  cashOutCompare?: string
  cashOutDown?: boolean
  cashOutFlat?: boolean

  /** 现金结余 — cashIn - cashOut */
  cashBalance: string
  cashBalanceCompare?: string
  cashBalanceDown?: boolean
  cashBalanceFlat?: boolean

  /** 结余率 — cashBalance / cashIn */
  balanceRate: string                   // 百分比，如 "32.4%"
  balanceRateCompare?: string
  balanceRateDown?: boolean
  balanceRateFlat?: boolean
}

// ─── 2. 预收资产 recharge ───
// ⚠️ area ≠ all 时整个板块返回 null（储值卡数据不按区域拆分）

interface RechargeSection {
  /** 储值卡充值实收 */
  actualIncome: string
  actualCompare?: string
  actualDown?: boolean
  actualFlat?: boolean

  /** 首充 */
  firstCharge: string
  firstChargeCompare?: string
  firstChargeDown?: boolean
  firstChargeFlat?: boolean

  /** 续费 */
  renewCharge: string
  renewChargeCompare?: string
  renewChargeDown?: boolean
  renewChargeFlat?: boolean

  /** 消耗 — 会员使用储值卡消费的金额 */
  consumed: string
  consumedCompare?: string
  consumedDown?: boolean
  consumedFlat?: boolean

  /** 储值卡总余额 */
  cardBalance: string
  cardBalanceCompare?: string
  cardBalanceDown?: boolean
  cardBalanceFlat?: boolean

  /**
   * 赠送卡 3×4 矩阵
   * 行：新增 / 消费 / 余额
   * 列：合计 / 酒水卡 / 台费卡 / 抵用券
   * 每个单元格含值和环比字段
   */
  giftRows: GiftRow[]

  /** 全类别会员卡余额合计 — 储值卡 + 赠送卡（酒水卡 + 台费卡 + 抵用券） */
  allCardBalance: string
  allCardBalanceCompare?: string
  allCardBalanceDown?: boolean
  allCardBalanceFlat?: boolean
}

interface GiftRow {
  label: string               // "新增" | "消费" | "余额"
  total: string               // 合计金额
  totalCompare?: string
  wine: string                // 酒水卡
  wineCompare?: string
  table: string               // 台费卡
  tableCompare?: string
  coupon: string              // 抵用券
  couponCompare?: string
}

// ─── 3. 应计收入确认 revenue ───

interface RevenueSection {
  /**
   * 收入结构表 — 9 行含子行标记
   * 主行：开台与包厢 / 助教(基础课) / 助教(激励课) / 食品酒水
   * 子行（isSub=true）：A区 / B区 / C区 / 团建区 / 麻将区（属于"开台与包厢"的子行）
   *
   * ⚠️ DWD-DOC 规则 2：助教行金额来自 assistant_pd_money（基础课/陪打）和 assistant_cx_money（激励课/超休）
   */
  structureRows: RevenueStructureRow[]

  /** 正价明细 — 4 项 */
  priceItems: RevenueDetailItem[]

  /** 发生额合计 */
  totalOccurrence: string
  totalOccurrenceCompare?: string

  /** 优惠明细 — 4 项 */
  discountItems: RevenueDetailItem[]

  /** 确认收入合计 — occurrence - discount */
  confirmedTotal: string
  confirmedTotalCompare?: string
  confirmedTotalDown?: boolean
  confirmedTotalFlat?: boolean

  /** 渠道明细 — 3 项（储值卡结算冲销 / 现金线上支付 / 团购核销确认收入） */
  channelItems: RevenueDetailItem[]
}

interface RevenueStructureRow {
  id: string                  // 行标识，如 "table" / "area-a" / "coach-basic" / "food"
  name: string                // 行名称，如 "开台与包厢" / "A区" / "助教" / "食品酒水"
  desc?: string               // 行描述，如 "基础课" / "激励课"
  amount: string              // 发生额
  discount: string            // 优惠金额（无优惠时为 "-"）
  booked: string              // 入账金额
  bookedCompare?: string      // 入账环比
  isSub?: boolean             // 是否为子行（缩进展示）
}

interface RevenueDetailItem {
  name: string                // 项目名称
  desc?: string               // 补充说明，如 "台桌卡+酒水卡+抵用券"
  value: string               // 金额
  compare?: string            // 环比
}

// ─── 4. 现金流入 cashflow ───

interface CashflowSection {
  /** 消费收款 — 3 项（纸币现金 / 线上收款 / 团购平台） */
  consumeItems: CashflowItem[]

  /** 充值收款 — 1 项（会员充值到账） */
  rechargeItems: CashflowItem[]

  /** 合计 */
  total: string
  totalCompare?: string
  totalDown?: boolean
  totalFlat?: boolean
}

interface CashflowItem {
  name: string                // 项目名称
  desc: string                // 补充说明，如 "柜台现金收款"
  value: string               // 金额
  compare?: string            // 环比
  isDown?: boolean            // 是否下降
}

// ─── 5. 现金流出 expense ───

interface ExpenseSection {
  /** 经营支出 — 3 项（食品饮料 / 耗材 / 报销） */
  operationItems: ExpenseItem[]

  /** 固定支出 — 4 项（房租 / 水电 / 物业 / 人员工资） */
  fixedItems: ExpenseItem[]

  /**
   * 助教分成 — 4 项（基础课分成 / 激励课分成 / 充值提成 / 额外奖金）
   * ⚠️ DWD-DOC 规则 2：基础课分成来自 assistant_pd_money，激励课分成来自 assistant_cx_money
   */
  coachItems: ExpenseItem[]

  /** 平台服务费 — 3 项（汇来米 / 美团 / 抖音） */
  platformItems: ExpenseItem[]

  /** 合计 */
  total: string
  totalCompare?: string
  totalDown?: boolean
  totalFlat?: boolean
}

interface ExpenseItem {
  name: string                // 项目名称
  value: string               // 金额
  compare?: string            // 环比，如 "4.5%" 或 "持平"
  isDown?: boolean            // 是否下降
  isFlat?: boolean            // 是否持平
}

// ─── 6. 助教分析 coachAnalysis ───

interface CoachAnalysisSection {
  /** 基础课（陪打）— assistant_pd_money */
  basic: CoachAnalysisTable

  /** 激励课（超休）— assistant_cx_money */
  incentive: CoachAnalysisTable
}

interface CoachAnalysisTable {
  /** 汇总行 */
  totalPay: string                    // 总课时费
  totalPayCompare?: string
  totalPayDown?: boolean
  totalShare: string                  // 总分成
  totalShareCompare?: string
  totalShareDown?: boolean
  avgHourly: string                   // 平均时薪，如 "¥25/h"
  avgHourlyCompare?: string
  avgHourlyFlat?: boolean

  /** 按等级分行 — 初级 / 中级 / 高级 / 星级 */
  rows: CoachAnalysisRow[]
}

interface CoachAnalysisRow {
  level: string               // "初级" | "中级" | "高级" | "星级"
  pay: string                 // 课时费
  payCompare?: string
  payDown?: boolean
  share: string               // 分成
  shareCompare?: string
  shareDown?: boolean
  hourly: string              // 时薪，如 "¥20/h"
  hourlyCompare?: string      // 如 "持平"
  hourlyFlat?: boolean        // 是否持平
}
```

#### 业务规则

| 规则 | 说明 |
|------|------|
| `area ≠ all` 时 `recharge = null` | 储值卡数据不按区域拆分，非全部区域时预收资产板块不返回 |
| `items_sum` 口径（DWD-DOC 规则 1） | 所有金额字段使用 `items_sum = table_charge_money + goods_money + assistant_pd_money + assistant_cx_money + electricity_money`，禁止使用 `consume_money` |
| 助教费用拆分（DWD-DOC 规则 2） | 基础课 = `assistant_pd_money`（陪打），激励课 = `assistant_cx_money`（超休），禁止使用 `service_fee` |
| 支付渠道恒等式（DWD-DOC 规则 3） | `balance_amount = recharge_card_amount + gift_card_amount`，三者不可重复计算 |
| 折扣互斥（DWD-DOC 规则 6） | `discount_manual`（大客户优惠）与 `discount_other` 互斥，两者之和 = `adjust_amount` |
| 现金流互斥（DWD-DOC 规则 7） | `platform_settlement_amount` 和 `groupbuy_pay_amount` 互斥 |
| 环比计算 | 后端根据 `time` 参数计算当期和上期日期范围，分别查询后计算百分比；`compare=0` 时不计算 |

#### 数据源映射

| 板块 | FDW 视图 | 说明 |
|------|---------|------|
| overview | `fdw_etl.v_dws_finance_overview` | 8 指标 + 8 环比 |
| recharge | `fdw_etl.v_dws_finance_recharge` | 储值卡 + 赠送卡矩阵 |
| revenue | `fdw_etl.v_dws_finance_revenue` | 收入结构表 + 明细 |
| cashflow | `fdw_etl.v_dws_finance_cashflow` | 消费收款 + 充值收款 |
| expense | `fdw_etl.v_dws_finance_expense` | 4 子分组 |
| coachAnalysis | `fdw_etl.v_dws_assistant_salary_calc` | 按等级分行聚合 |

#### 字段说明表

##### overview 经营一览（8 指标）

| 字段 | 类型 | 说明 | 环比字段 |
|------|------|------|---------|
| `occurrence` | string | 发生额/正价 | `occurrenceCompare` + `occurrenceDown` + `occurrenceFlat` |
| `discount` | string | 总优惠（负值） | `discountCompare` + `discountDown` + `discountFlat` |
| `discountRate` | string | 折扣率（百分比） | `discountRateCompare` + `discountRateDown` + `discountRateFlat` |
| `confirmedRevenue` | string | 成交/确认收入 | `confirmedCompare` + `confirmedDown` + `confirmedFlat` |
| `cashIn` | string | 实收/现金流入 | `cashInCompare` + `cashInDown` + `cashInFlat` |
| `cashOut` | string | 现金支出 | `cashOutCompare` + `cashOutDown` + `cashOutFlat` |
| `cashBalance` | string | 现金结余 | `cashBalanceCompare` + `cashBalanceDown` + `cashBalanceFlat` |
| `balanceRate` | string | 结余率（百分比） | `balanceRateCompare` + `balanceRateDown` + `balanceRateFlat` |

##### recharge 预收资产

| 字段 | 类型 | 说明 |
|------|------|------|
| `actualIncome` | string | 储值卡充值实收 |
| `firstCharge` | string | 首充 |
| `renewCharge` | string | 续费 |
| `consumed` | string | 消耗 |
| `cardBalance` | string | 储值卡总余额 |
| `giftRows` | GiftRow[] | 赠送卡 3×4 矩阵（新增/消费/余额 × 合计/酒水卡/台费卡/抵用券） |
| `allCardBalance` | string | 全类别会员卡余额合计 |

##### revenue 应计收入确认

| 字段 | 类型 | 说明 |
|------|------|------|
| `structureRows` | RevenueStructureRow[] | 收入结构表（9 行含子行） |
| `priceItems` | RevenueDetailItem[] | 正价明细（4 项：开台消费/酒水商品/包厢费用/助教服务） |
| `totalOccurrence` | string | 发生额合计 |
| `discountItems` | RevenueDetailItem[] | 优惠明细（4 项：团购优惠/手动调整+大客户优惠/赠送卡抵扣/其他优惠） |
| `confirmedTotal` | string | 确认收入合计 |
| `channelItems` | RevenueDetailItem[] | 渠道明细（3 项：储值卡结算冲销/现金线上支付/团购核销确认收入） |

##### coachAnalysis 助教分析

| 子表 | 数据源字段 | 说明 |
|------|-----------|------|
| `basic` | `assistant_pd_money` | 基础课（陪打）按初级/中级/高级/星级分行 |
| `incentive` | `assistant_cx_money` | 激励课（超休）按初级/中级/高级/星级分行 |

---

## 七、助教模块（已实现 ✅）

### COACH-1: 助教详情

> 数据源：`zqyy_app.coaches`（基本信息）、`etl_feiqiu.dws.*`（绩效/收入，via FDW）、`zqyy_app.coach_tasks`（任务）、`zqyy_app.coach_notes`（备注）
> 金额口径：所有消费金额使用 `items_sum`（DWD-DOC 强制规则 1）；助教费用使用 `assistant_pd_money` + `assistant_cx_money` 拆分（DWD-DOC 强制规则 2）
> 会员信息：通过 `member_id` JOIN `dim_member` 获取（DWD-DOC 强制规则 DQ-6），禁止直接使用 `member_phone`

```
GET /api/xcx/coaches/{coachId}
```

#### 响应结构

```typescript
interface CoachDetailResponse {
  // ── 基本信息 ──
  id: string
  name: string
  avatar: string
  level: string                    // 初级/中级/高级/星级
  skills: string[]                 // 技能标签，如 ['中🎱', '🎯斯诺克']
  workYears: number                // 工龄（年）
  customerCount: number            // 客户数
  hireDate: string                 // 入职日期，如 '2023-03-15'

  // ── 绩效指标（performance） ──
  performance: {
    monthlyHours: number           // 本月定档工时
    monthlySalary: number          // 本月工资（预估）
    customerBalance: number        // 客源储值余额合计
    tasksCompleted: number         // 本月任务完成数
    perfCurrent: number            // 当前绩效值
    perfTarget: number             // 绩效目标值
  }

  // ── 收入明细（income） ──
  // thisMonth / lastMonth 各含 4 项收入分类
  income: {
    thisMonth: IncomeItem[]
    lastMonth: IncomeItem[]
  }

  // ── 档位节点（tierNodes） ──
  // 供前端绩效进度条组件使用，如 [0, 100, 130, 160, 190, 220]
  tierNodes: number[]

  // ── 任务分组 ──
  visibleTasks: TaskItem[]         // 可见任务
  hiddenTasks: TaskItem[]          // 隐藏任务
  abandonedTasks: AbandonedTask[]  // 已放弃任务

  // ── Top 客户（topCustomers） ──
  topCustomers: TopCustomer[]

  // ── 近期服务记录（serviceRecords） ──
  serviceRecords: ServiceRecord[]

  // ── 历史月份统计（historyMonths） ──
  // 最近 5+ 个月的汇总数据
  historyMonths: HistoryMonth[]

  // ── 备注（notes） ──
  notes: NoteItem[]
}
```

#### 子类型定义

```typescript
/** 收入分项 */
interface IncomeItem {
  label: string                    // 如 '基础课时费'、'激励课时费'、'充值提成'、'酒水提成'
  amount: string                   // 格式化金额，如 '¥3,500'
  color: string                    // 主题色：primary / success / warning / purple
}

/** 任务项（visibleTasks / hiddenTasks） */
interface TaskItem {
  typeLabel: string                // 任务类型标签，如 '高优先召回'、'客户回访'
  typeClass: string                // 样式类：high-priority / priority / callback / relationship
  customerName: string             // 客户姓名
  customerId?: string              // 客户 ID（用于跳转 customer-detail）
  noteCount: number                // 备注数量
  pinned: boolean                  // 是否置顶
  notes?: Array<{                  // 备注列表（可选）
    pinned?: boolean               // 是否置顶备注
    text: string                   // 备注内容
    date: string                   // 日期，如 '2026-02-06'
  }>
}

/** 已放弃任务 */
interface AbandonedTask {
  customerName: string
  reason: string                   // 放弃原因，如 '客户拒绝'、'超时未响应'
}

/** Top 客户（扩展版，含关系指数和余额） */
interface TopCustomer {
  id: string
  name: string
  initial: string                  // 姓氏首字，如 '王'
  avatarGradient: string           // 头像渐变色：pink / amber / green / blue / violet / teal
  heartEmoji: string               // 关系 emoji（P6 AC3 四级映射）：💖 / 🧡 / 💛 / 💙
  relationScore: string            // 关系指数（0-10），如 '9.5'
  scoreColor: string               // 分数颜色：#FF6B6B / #FF8C00 / #FFA726 / #5B9BD5
  serviceCount: number             // 服务次数
  balance: string                  // 余额（格式化），如 '¥8,600'
  consume: string                  // 消费总额（格式化，items_sum 口径），如 '¥12,800'
}

/** 近期服务记录 */
interface ServiceRecord {
  customerId?: string              // 客户 ID（用于跳转 customer-detail）
  customerName: string
  initial: string
  avatarGradient: string
  type: string                     // 课程类型：'基础课' / '激励课'
  typeClass: string                // 样式类：basic / incentive
  table: string                    // 台桌名，如 'A12号台'
  duration: string                 // 时长，如 '2.5h'
  income: string                   // 收入（格式化），如 '¥200'
  date: string                     // 日期时间，如 '2026-02-07 21:30'
  perfHours?: string               // 折算工时（可选），如 '2h'
}

/** 历史月份统计 */
interface HistoryMonth {
  month: string                    // 月份标签：'本月' / '上月' / '4月' 等
  estimated: boolean               // 是否为预估数据
  customers: string                // 客户数（格式化），如 '22人'
  hours: string                    // 工时（格式化），如 '87.5h'
  salary: string                   // 工资（格式化），如 '¥6,950'
  callbackDone: number             // 回访任务完成数
  recallDone: number               // 召回任务完成数
}

/** 备注项 */
interface NoteItem {
  id: string
  content: string                  // 备注内容
  timestamp: string                // ISO 时间戳，用于排序
  aiScore?: number                 // AI 应用 6 评分（1-10，展示用，星数 = aiScore ÷ 2）
  manualScore?: number             // 用户手动评分（1-5 星，输入用）
  customerName: string             // 关联客户/操作人
  tagLabel: string                 // 标签
  createdAt: string                // 格式化时间，如 '2026-03-05 14:30'
}
```

#### 字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| `performance.monthlyHours` | number | 本月定档工时（折算后） |
| `performance.monthlySalary` | number | 本月工资预估（含基础+激励+提成） |
| `performance.customerBalance` | number | 该助教所有客户的储值余额合计 |
| `performance.tasksCompleted` | number | 本月已完成任务数 |
| `performance.perfCurrent` | number | 当前绩效进度值 |
| `performance.perfTarget` | number | 绩效目标值 |
| `tierNodes` | number[] | 档位节点数组，如 `[0, 100, 130, 160, 190, 220]`，前端用于绘制进度条刻度 |
| `income.thisMonth` / `lastMonth` | IncomeItem[] | 各含 4 项：基础课时费 / 激励课时费 / 充值提成 / 酒水提成 |
| `topCustomers[].consume` | string | 消费总额，使用 `items_sum` 口径 |
| `serviceRecords[].perfHours` | string? | 折算工时，仅激励课有值 |
| `serviceRecords[].customerId` | string? | 客户 ID，用于从服务记录跳转到客户详情 |
| `visibleTasks[].customerId` | string? | 客户 ID，用于从任务项跳转到客户详情 |
| `historyMonths` | HistoryMonth[] | 最近 5+ 个月，第一条为本月（`estimated: true`） |

### 助教模块错误码（COACH-1）

| HTTP 状态码 | 场景 | 响应示例 |
|:-----------:|------|---------|
| 403 | 用户未通过审核（`require_approved()` 拦截） | `{ "code": 403, "message": "用户未通过审核，无法访问此资源" }` |
| 404 | 助教不存在（`coachId` 无对应记录） | `{ "code": 404, "message": "助教不存在" }` |
| 422 | 请求参数校验失败（coachId 类型不合法） | Pydantic `HTTPValidationError` |

> COACH-1 扩展模块（income/topCustomers/serviceRecords/historyMonths/notes）查询失败时不返回错误码，而是对该模块返回空默认值，整体响应仍为 HTTP 200。

---

## 八、对话模块（已实现 ✅ — RNS1.4）

> 路由前缀：`/api/xcx/chat`（RNS1.4 从 `/api/ai/*` 迁移，旧路径已移除）
> 数据源：`zqyy_app.biz.ai_conversations`（对话会话）、`zqyy_app.biz.ai_messages`（消息）
> 时间字段统一使用 `createdAt`（camelCase），替代前端 `timestamp` 和旧契约 `created_at`
> 所有端点需 JWT（approved 状态），使用 `require_approved()` 权限检查

### CHAT-1: 对话历史列表

```
GET /api/xcx/chat/history?page=1&pageSize=20
```

#### 请求参数

| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `page` | number (query) | 否 | 页码，默认 1 |
| `pageSize` | number (query) | 否 | 每页条数，默认 20，最大 100 |

#### 响应结构

```typescript
interface ChatHistoryResponse {
  items: Array<{
    id: number                     // 对话 ID（即 chatId）
    title: string                  // 对话标题（自定义 > 客户姓名 > 首条消息前20字）
    customerName?: string          // 关联客户姓名（仅 contextType=customer 时有值）
    lastMessage?: string           // 最后一条消息摘要
    timestamp: string              // 最后消息时间（ISO 8601）
    unreadCount: number            // 未读消息数
  }>
  total: number
  page: number
  pageSize: number
}
```

#### 排序规则

按 `last_message_at` 倒序（最新对话在前）。

### CHAT-2a: 通过 chatId 查询消息

```
GET /api/xcx/chat/{chatId}/messages?page=1&pageSize=50
```

#### 请求参数

| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `chatId` | number (path) | 是 | 对话 ID |
| `page` | number (query) | 否 | 页码，默认 1 |
| `pageSize` | number (query) | 否 | 每页条数，默认 50，最大 100 |

### CHAT-2b: 通过上下文查询消息

> 后端根据 `contextType` + `contextId` 自动查找或创建对话，返回对应的 `chatId` 和消息列表。
> 对话复用规则：`task` 入口同一 taskId 始终复用（无时限）；`customer`/`coach` 入口 ≤ 3 天复用、> 3 天新建；`general` 入口始终新建。

```
GET /api/xcx/chat/messages?contextType={type}&contextId={id}&page=1&pageSize=50
```

#### 请求参数

| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `contextType` | string (query) | 是 | 上下文类型：`task` / `customer` / `coach` / `general` |
| `contextId` | string (query) | 是 | 上下文 ID（taskId / customerId / coachId，general 时为空） |
| `page` | number (query) | 否 | 页码，默认 1 |
| `pageSize` | number (query) | 否 | 每页条数，默认 50，最大 100 |

#### 响应结构（CHAT-2a / CHAT-2b 共用）

```typescript
interface ChatMessagesResponse {
  chatId: number                   // 对话 ID（上下文入口时由后端返回，供后续发送消息使用）
  items: Array<{
    id: number
    role: 'user' | 'assistant'
    content: string
    createdAt: string              // 统一时间字段名，ISO 8601

    // 引用卡片（可选，AI 回复涉及特定客户时附带）
    referenceCard?: ReferenceCard
  }>
  total: number
  page: number
  pageSize: number
}
```

#### 排序规则

消息按 `created_at` 正序（最早的消息在前）。

#### referenceCard 结构定义

```typescript
interface ReferenceCard {
  type: 'customer' | 'record'      // 引用类型
  title: string                    // 卡片标题，如 '张伟 — 消费概览'
  summary: string                  // 摘要文字，如 '余额 ¥5,200，近30天消费 ¥2,380'
  data: Record<string, string>     // 键值对详情
}
```

示例：
```json
{
  "type": "customer",
  "title": "张伟 — 消费概览",
  "summary": "余额 ¥5,200，近30天消费 ¥2,380",
  "data": {
    "余额": "¥5,200",
    "近30天消费": "¥2,380",
    "到店次数": "8次",
    "最近到店": "3天前"
  }
}
```

> referenceCard 中金额使用 `items_sum` 口径（DWD-DOC 强制规则 1），会员信息通过 `member_id` JOIN `dim_member` 获取（DWD-DOC 规则 DQ-6）。

### CHAT-3: 发送消息（同步回复）

```
POST /api/xcx/chat/{chatId}/messages
Content-Type: application/json
Body: { content: string }
```

#### 请求参数

| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `chatId` | number (path) | 是 | 对话 ID（归属验证：不属于当前用户返回 403） |
| `content` | string (body) | 是 | 消息内容（不能为空） |

#### 响应结构

```typescript
interface SendMessageResponse {
  userMessage: {
    id: number
    content: string
    createdAt: string              // ISO 8601
  }
  aiReply: {
    id: number
    content: string
    createdAt: string              // ISO 8601
  }
}
```

#### AI 失败降级

AI 服务调用失败时，用户消息仍保存，`aiReply.content` 返回错误提示消息（如 `"抱歉，AI 助手暂时无法回复，请稍后重试"`），HTTP 状态码保持 200。

### CHAT-4: SSE 流式端点

> 用于 AI 流式回复，前端通过 SSE 接收逐 token 输出。
> chatId 归属验证在 SSE 流开始前完成，失败时返回普通 HTTP 403（非 SSE）。

```
POST /api/xcx/chat/stream
Content-Type: application/json
Body: { chatId: number, content: string }
Response: text/event-stream
```

#### SSE 事件类型定义

三种事件类型：`message`（逐 token）、`done`（完成）、`error`（错误）。

```
event: message
data: {"token": "根据"}

event: message
data: {"token": "数据分析"}

event: done
data: {"messageId": 123, "createdAt": "2026-03-20T14:30:00+08:00"}

event: error
data: {"message": "AI 服务暂时不可用"}
```

| 事件类型 | data 结构 | 说明 |
|----------|----------|------|
| `message` | `{ "token": string }` | 逐 token 输出，`token` 为文本片段 |
| `done` | `{ "messageId": number, "createdAt": string }` | 流结束，返回完整消息 ID 和创建时间 |
| `error` | `{ "message": string }` | 错误，返回错误描述 |

> 注意：SSE 端点的响应不经过 ResponseWrapperMiddleware 包装（`text/event-stream` 自动跳过，RNS1.0 已实现）。

#### 对话模块错误码

| HTTP 状态码 | 场景 | 响应示例 |
|:-----------:|------|---------|
| 403 | 用户未通过审核 | `{ "code": 403, "message": "用户未通过审核，无法访问此资源" }` |
| 403 | chatId 不属于当前用户 | `{ "code": 403, "message": "无权访问此对话" }` |
| 404 | 对话不存在 | `{ "code": 404, "message": "对话不存在" }` |
| 422 | 消息内容为空 | `{ "code": 422, "message": "消息内容不能为空" }` |

#### 字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| `CHAT-1.items[].title` | string | 对话标题（自定义 > 客户姓名 > 首条消息前20字） |
| `CHAT-1.items[].timestamp` | string | 最后消息时间（ISO 8601） |
| `CHAT-2.items[].createdAt` | string | 统一时间字段名，替代 `created_at` 和 `timestamp` |
| `CHAT-2.items[].referenceCard` | ReferenceCard? | 引用卡片，AI 回复涉及客户时附带结构化上下文数据 |
| `CHAT-2b` `contextType` 参数 | query | 上下文类型：`task` / `customer` / `coach` / `general` |
| `CHAT-2b` `contextId` 参数 | query | 上下文 ID（taskId / customerId / coachId） |
| `CHAT-4` SSE | text/event-stream | 三种事件：`message`（token）/ `done`（完成）/ `error`（错误） |

---

## 九、配置模块

### CONFIG-1: 技能类型列表（REQ-1）
```
GET /api/xcx/config/skill-types
Response: {
  skills: Array<{ value: string, text: string, icon?: string }>
}
```

---

## 附：接口与页面映射

| 页面 | 依赖接口 | 优先级 |
|------|----------|--------|
| login | AUTH-1, AUTH-2 | ✅ 已实现 |
| app.ts | AUTH-3, AUTH-4 | ✅ 已实现 |
| apply | AUTH-5 | ✅ 已实现 |
| reviewing | AUTH-3 | ✅ 已实现 |
| no-permission | AUTH-3 | ✅ 已实现 |
| task-list | TASK-1 | P0 |
| task-detail | TASK-2, TASK-3, TASK-4, NOTE-2, NOTE-3 | P0 |
| notes | NOTE-1, NOTE-3 | P1 |
| performance | PERF-1 | P1 |
| performance-records | PERF-2 | P1 |
| customer-detail | CUST-1 | ✅ 后端已实现 |
| customer-service-records | CUST-2 | ✅ 后端已实现 |
| coach-detail | COACH-1 | ✅ 后端已实现 |
| board-coach | BOARD-1, CONFIG-1 | P2 |
| board-customer | BOARD-2 | P2 |
| board-finance | BOARD-3 | P2 |
| chat-history | CHAT-1 | ✅ 后端已实现 |
| chat | CHAT-2a, CHAT-2b, CHAT-3, CHAT-4 | ✅ 后端已实现 |
| my-profile | AUTH-3 | ✅ 已实现（读 globalData） |
