# 戒烟/抽烟记录 API

所有接口前缀：`/api/v1/smoke`  
除登录外都需要：`Authorization: Bearer <session_key>`（见：`docs/common/auth.md`）

## 1) 新增记录

`POST /api/v1/smoke/logs`

请求体：

```json
{
  "smoke_time": "2025-12-31",
  "smoke_at": "2025-12-31 08:30:00",
  "remark": "压力大",
  "level": 2,
  "num": 3
}
```

说明：
- `smoke_time` 可选；不传则默认“当天”。
- `smoke_at` 可选；真实抽烟时间（格式 `YYYY-MM-DD HH:MM:SS`）。用于“按时间节点分析/AI 建议”；不传则可用 `createtime` 近似。
- `level/num` 可选；不传时后端会按 `1` 处理。
- 如果要记录“想抽但忍住了”，请传 `level=0` 且 `num=0`（会在 `fa_smoke_log` 中展示为一条记录，但不会影响看板的支数累加）。

curl 示例：

```bash
curl -X POST 'http://127.0.0.1:8080/api/v1/smoke/logs' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer wx-session-key' \
  -d '{"smoke_time":"2025-12-31","smoke_at":"2025-12-31 08:30:00","remark":"压力大","level":2,"num":3}'
```

成功响应示例（字段以实际为准）：

```json
{
  "code": 200,
  "message": "success",
  "data": {
    "id": 5202,
    "smoke_time": "2025-12-31T00:00:00+08:00",
    "smoke_at": "2025-12-31T08:30:00+08:00",
    "remark": "压力大",
    "createtime": 1735600000,
    "updatetime": 1735600000,
    "deletetime": null,
    "level": 2,
    "num": 3
  }
}
```

## 2) 获取单条记录

`GET /api/v1/smoke/logs/:id`

curl 示例：

```bash
curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \
  -H 'Authorization: Bearer wx-session-key'
```

## 3) 列表查询（分页）

`GET /api/v1/smoke/logs?page=1&page_size=20&start=2025-12-01&end=2025-12-31`

参数：
- `page`：页码，默认 `1`
- `page_size`：每页数量，默认 `20`，最大 `200`
- `start/end`：可选，按 `smoke_time` 过滤（格式 `YYYY-MM-DD`）

成功响应示例：

```json
{
  "code": 200,
  "message": "success",
  "data": {
    "items": [],
    "total": 0,
    "page": 1,
    "page_size": 20
  }
}
```

## 4) 获取看板概览

`GET /api/v1/smoke/dashboard?start=2026-01-01&end=2026-01-07`

参数：
- `start`：起始日期（含，格式 `YYYY-MM-DD`），默认“本周一”
- `end`：截止日期（含，格式 `YYYY-MM-DD`），默认“本周日”。若只传 `start`，`end` 默认为 `start + 6 天`。

成功响应示例：

```json
{
  "code": 200,
  "message": "success",
  "data": {
    "today_count": 6,
    "minutes_since_last": 42,
    "weekly": [
      { "date": "2026-01-01", "count": 2, "is_today": false },
      { "date": "2026-01-02", "count": 1, "is_today": false },
      { "date": "2026-01-03", "count": 0, "is_today": false },
      { "date": "2026-01-04", "count": 0, "is_today": false },
      { "date": "2026-01-05", "count": 3, "is_today": true },
      { "date": "2026-01-06", "count": 0, "is_today": false },
      { "date": "2026-01-07", "count": 0, "is_today": false }
    ]
  }
}
```

字段说明：
- `today_count`：当天吸烟总支数（累加 `num`）
- `minutes_since_last`：距最后一次“实际抽烟”（忽略 `level=0 && num=0` 的忍住记录）的分钟数，通过最近一条 `smoke_at/smoke_time/createtime` 计算；若历史为空则字段不存在
- `weekly`：起止日期内每日汇总，`count` 为当日总支数，`is_today` 标记当前日期（即便不在 `start/end` 范围内也会标记为 `false`）

## 5) 最近记录列表（轻量版）

`GET /api/v1/smoke/logs/latest?limit=20`

参数：
- `limit`：返回条数，默认 `20`，最大 `100`

成功响应示例：

```json
{
  "code": 200,
  "message": "success",
  "data": {
    "items": [
      {
        "id": 123,
        "smoke_time": "2026-01-05T00:00:00+08:00",
        "smoke_at": "2026-01-05T09:12:00+08:00",
        "remark": "压力大",
        "level": 3,
        "num": 2,
        "createtime": 1736049120
      }
    ]
  }
}
```

## 6) 更新记录

`PUT /api/v1/smoke/logs/:id`

请求体（字段可选，按需传）：

```json
{
  "smoke_time": "2026-01-01",
  "smoke_at": "2026-01-01 21:10:00",
  "remark": "聚会",
  "level": 3,
  "num": 1
}
```

注意：
- 如果你想“清空 smoke_time”，请传空字符串：`{"smoke_time":""}`。
- 如果你想“清空 smoke_at”，请传空字符串：`{"smoke_at":""}`。
- 如果传 `null` 或者不传 `smoke_time`，后端会认为你没有修改该字段。

## 7) 删除记录（软删除）

`DELETE /api/v1/smoke/logs/:id`

成功响应：

```json
{
  "code": 200,
  "message": "success",
  "data": {
    "deleted": true
  }
}
```

## 8) 获取 AI 戒烟建议（会员 + 广告解锁并行）

`GET /api/v1/smoke/ai/advice?date=2026-01-02`

说明：
- `date` 可选，默认“昨天”（建议针对哪一天的数据）。
- 权限：会员用户直接可用；非会员需要先对该 `date` 完成“看广告解锁”（见下一个接口）。
- 建议结果会按 `uid + date + prompt_version` 缓存（表：`fa_smoke_ai_advice`）。

未满足权限时的建议响应（示例）：
```json
{
  "code": 403,
  "message": "需要会员或观看广告解锁后才可生成建议",
  "data": {
    "date": "2026-01-02",
    "need": "vip_or_ad"
  }
}
```

成功响应（示例）：
```json
{
  "code": 200,
  "message": "success",
  "data": {
    "date": "2026-01-02",
    "advice": "..."
  }
}
```

## 9) 看广告解锁（用于非会员）

`POST /api/v1/smoke/ai/advice_unlocks`

请求体：
```json
{
  "date": "2026-01-02",
  "ad_watched_at": "2026-01-03 09:00:00"
}
```

说明：
- 该接口用于记录“已完成观看广告”，落库到 `fa_smoke_ai_advice_unlocks`（`uid + unlock_date` 唯一）。
- `ad_watched_at` 可由后端取当前时间；如需审计/对账可保留前端上报并做校验。
- 解锁是“按天”的：观看一次广告解锁一天内的 AI 生成功能（可用于「每日 AI 建议」以及「AI 下次抽烟时间节点」）。
- 如果你要生成“明天”的 AI 时间节点，请把 `date` 传为明天日期（例如 `2026-01-06`）。

## 10) 获取用户基础信息（首次进入：判断是否需要补全）

`GET /api/v1/smoke/profile`

说明：
- 首次进入小程序建议先调用该接口：若 `exists=false` 或 `is_completed=false`，前端进入“信息补全”流程。
- `baseline_interval_minutes` 用于建立初始基准：在用户清醒时段内的“平均间隔（分钟）”。计算：`awake_minutes / baseline_cigs_per_day`。
- 若未提供作息时间（起床/入睡），后端会用默认清醒时长 `16*60=960` 分钟参与计算。

成功响应（示例）：

```json
{
  "code": 200,
  "message": "success",
  "data": {
    "exists": true,
    "profile": {
      "id": 1,
      "created_at": "2026-01-05T10:00:00+08:00",
      "updated_at": "2026-01-05T10:00:00+08:00",
      "baseline_cigs_per_day": 20,
      "smoking_years": 8,
      "pack_price_cent": 2500,
      "smoke_motivations": ["压力大", "社交"],
      "quit_motivations": ["身体健康", "省钱"],
      "wake_up_time": "07:30",
      "sleep_time": "23:30",
      "onboarding_completed_at": "2026-01-05T10:00:00+08:00"
    },
    "is_completed": true,
    "awake_minutes": 960,
    "baseline_interval_minutes": 48
  }
}
```

当 `exists=false`（尚未补全）时，响应示例：

```json
{
  "code": 200,
  "message": "success",
  "data": {
    "exists": false,
    "is_completed": false,
    "awake_minutes": 960,
    "baseline_interval_minutes": 0
  }
}
```

字段用途（补全页面可参考）：
- `baseline_cigs_per_day`（基础烟量/日均抽烟支数）：建立初始基准，计算初始建议间隔时长。
- `smoking_years`（烟龄/年）+ `pack_price_cent`（单包价格/分）：用于看板计算“已省金额”和“恢复时长”等指标（公式可在看板端实现）。
- `smoke_motivations`（抽烟动机）：如 `压力大/无聊/社交/提神`，用于 AI 在分析 remark 时更有针对性。
- `quit_motivations`（戒烟动力）：如 `身体健康/家人孩子/省钱`，当用户产生动摇时 AI 可用这些信息做“情感阻断/自我提醒”。
- `wake_up_time` + `sleep_time`（作息时间）：用于自动规避睡眠时间，防止在用户睡觉时提醒其“坚持”。

## 11) 补全/更新用户基础信息（Upsert）

`PUT /api/v1/smoke/profile`

说明：
- 字段按需传；首次进入建议一次性补全。
- 作息时间格式：`HH:MM`（24 小时制），例如 `07:30`、`23:10`。
- `pack_price_cent` 为“分”；若前端用“元”，请乘以 100。

请求体（示例）：

```json
{
  "baseline_cigs_per_day": 20,
  "smoking_years": 8,
  "pack_price_cent": 2500,
  "smoke_motivations": ["压力大", "社交"],
  "quit_motivations": ["身体健康", "省钱"],
  "wake_up_time": "07:30",
  "sleep_time": "23:30"
}
```

成功响应：同 `GET /api/v1/smoke/profile`（返回最新 `profile` + `is_completed` + `baseline_interval_minutes`）。

## 12) 想抽但忍住了（写入一条 level=0,num=0 的记录）

`POST /api/v1/smoke/logs/resisted`

请求体（示例）：
```json
{
  "smoke_time": "2026-01-05",
  "smoke_at": "2026-01-05 10:20:00",
  "remark": "压力大，想抽但忍住了"
}
```

说明：
- 该接口会在 `fa_smoke_log` 中新增一条记录：`level=0` 且 `num=0`，用于更直观记录“想抽/忍住”的过程。
- 这类记录不会影响 `today_count/weekly.count` 的支数统计（因为 `num=0`）。

## 13) 获取“下次抽烟记录时间”（默认 + AI 自动切换）

`GET /api/v1/smoke/next_smoke_time`

说明：
- 用于首页展示“建议的下次记录时间”。
- 如果指定日期存在 AI 给出的时间节点（`time_nodes` 不为空），则优先使用 AI 的建议；否则使用默认策略。
- 可选参数：
  - `date`：计划日期（默认今天），支持 `YYYY-MM-DD` 或 `today/tomorrow`。
  - `mode`（默认 `auto`）
    - `auto`：只在已存在 AI 时间节点时使用 AI（不主动生成）
    - `ai`：生成该 `date` 的 AI 时间节点（需要先看广告解锁；生成一次缓存一天）
    - `default`：永远返回默认策略

默认策略（不使用 AI）：
- 基础间隔：优先使用 `GET /api/v1/smoke/profile` 返回的 `baseline_interval_minutes`；若不存在则默认 `60` 分钟。
- 阶梯式延时：最近 7 天内每累计 `5` 条“忍住记录（level=0,num=0）”，在基础间隔上 `+5` 分钟（最多 `+60` 分钟）。
- 若用户已补全作息时间，会自动规避睡眠区间：若计算出的时间落在睡眠区间，顺延到下一次起床时间。

AI 生成说明：
- 当 `mode=ai` 时，会把最近 3 天的抽烟数据（含“忍住记录”）作为输入提供给 AI，用于更贴合近期模式生成时间节点。
- 未解锁时会返回 `403`：提示需要观看广告解锁。

成功响应（示例：回落到默认）：
```json
{
  "code": 200,
  "message": "success",
  "data": {
    "source": "default",
    "not_before_at": "2026-01-05T10:18:00+08:00",
    "suggested_at": "2026-01-05T10:18:00+08:00",
    "default": {
      "last_smoke_at": "2026-01-05T09:30:00+08:00",
      "next_smoke_at": "2026-01-05T10:18:00+08:00",
      "base_interval_minutes": 48,
      "interval_minutes": 48,
      "stage": 0,
      "resisted_7d": 3,
      "sleep_adjusted": false,
      "algorithm": "staircase_delay_v1",
      "as_of": "2026-01-05T10:00:00+08:00"
    }
  }
}
```

当存在 AI 建议且包含 `time_nodes` 时，响应会是（示例）：
```json
{
  "code": 200,
  "message": "success",
  "data": {
    "source": "ai",
    "not_before_at": "2026-01-05T10:18:00+08:00",
    "suggested_at": "2026-01-05T10:28:00+08:00",
    "time_nodes": ["10:30", "11:10", "14:00", "16:30"],
    "advice": "先把这次冲动延后到10:28，期间做一次5分钟快走+喝水，压力场景用深呼吸替代。",
    "default": { "algorithm": "staircase_delay_v1" },
    "ai": {
      "plan_date": "2026-01-05",
      "not_before_at": "2026-01-05T10:18:00+08:00",
      "suggested_at": "2026-01-05T10:28:00+08:00",
      "time_nodes": ["10:30", "11:10", "14:00", "16:30"],
      "advice": "先把这次冲动延后到10:28，期间做一次5分钟快走+喝水，压力场景用深呼吸替代。",
      "prompt_version": "v1",
      "model": "gpt-4.1-mini",
      "provider": "openai-compatible"
    }
  }
}
```
