Files
smt/docs/api.md
T

592 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 戒烟/抽烟记录 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) 更新记录
`POST /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)
`POST /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` 分钟)。
- 间隔兜底:最终间隔会限制在 `5~240` 分钟之间。
- 若用户已补全作息时间,会自动规避睡眠区间:若计算出的时间落在睡眠区间,顺延到下一次起床时间。
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",
"last_smoke_at": "2026-01-05T09:30:00+08:00",
"today_count": 3,
"resisted_count": 1,
"reduced_from_yesterday": 2,
"exceeded_yesterday": false,
"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",
"last_smoke_at": "2026-01-05T09:30:00+08:00",
"today_count": 3,
"resisted_count": 1,
"reduced_from_yesterday": 2,
"exceeded_yesterday": false,
"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"
}
}
}
```
字段说明(新增首页字段):
- `last_smoke_at`:上次“实际抽烟”时间(忽略忍住记录),格式 `RFC3339`(含时区)。
- `today_count`:今日抽烟支数(累加 `num`)。
- `resisted_count`:今日克制次数(`level=0 && num=0`)。
- `reduced_from_yesterday`:较昨日减少的支数(允许为负数;为负时表示“今天超出昨日”)。
- `exceeded_yesterday`:是否超出昨日(`true` 表示今天超出昨日,前端可用作单独标识)。
## 14) 获取健康与储蓄统计(合并接口)
`GET /api/v1/smoke/health_savings?period=week&start=2026-01-01&end=2026-01-07`
说明:
- 合并了"肺部功能恢复"和"节省金额"两个指标,专用于统计页面展示。
- 支持按周/月/年查询,通过 `period` 参数指定时间范围类型。
参数:
- `period`(必填):时间范围类型,可选值:
- `week`:周(默认本周一至本周日)
- `month`:月(默认本月1日至本月最后一日)
- `year`:年(默认本年1月1日至本年12月31日)
- `start`(可选):起始日期(格式 `YYYY-MM-DD`),不传则根据 `period` 自动计算
- `end`(可选):截止日期(格式 `YYYY-MM-DD`),不传则根据 `period` 自动计算
**计算逻辑**
1. **肺部功能恢复百分比**
- 基于用户最后一次实际抽烟时间(忽略 `level=0 && num=0` 的忍住记录)
- 计算无烟时长(分钟)
- 根据医学研究恢复时间线计算恢复百分比(详见 `docs/ALGORITHM.md`
- 公式:
```
无烟天数 = (当前时间 - 最后抽烟时间) / (24 * 60) 分钟
恢复百分比计算:
- < 14天: (天数 / 14) * 15%
- 14-30天: 15% + ((天数 - 14) / 16) * 15%
- 30-90天: 30% + ((天数 - 30) / 60) * 20%
- > 90天: 50% + ((天数 - 90) / 275) * 50%,最高 100%
```
2. **节省金额**
- 基于用户在统计周期内的实际抽烟量与基线对比
- 计算公式:
```
预期总支数 = baseline_cigs_per_day * 周期天数
实际总支数 = 周期内所有记录的 num 累加(排除 level=0 && num=0
节省支数 = 预期总支数 - 实际总支数
节省包数 = 节省支数 / 20(假设每包20支)
节省金额(分)= 节省包数 * pack_price_cent
```
- 若实际总支数 > 预期总支数,则节省金额为 0(不显示负数)
3. **目标金额**
- 默认目标:`baseline_cigs_per_day * 周期天数 / 20 * pack_price_cent`(即完全戒烟节省的金额)
- 或使用用户设定的目标金额(如有)
成功响应示例:
```json
{
"code": 200,
"message": "success",
"data": {
"period": "week",
"start": "2026-01-01",
"end": "2026-01-07",
"days": 7,
"health": {
"lung_recovery_percent": 40.5,
"smoke_free_minutes": 11520,
"smoke_free_days": 8,
"last_smoke_at": "2025-12-24T14:30:00+08:00",
"recovery_stage": "early", // early(0-14天) / mid(14-30天) / late(30-90天) / advanced(>90天)
"next_milestone": {
"days_until": 6,
"milestone": "2周 - 肺功能提升15%",
"percent_at_milestone": 15
}
},
"savings": {
"saved_amount_cent": 14500, // 已节省金额(分)
"saved_amount_yuan": 145.00, // 已节省金额(元,前端展示用)
"target_amount_cent": 20000, // 目标金额(分)
"target_amount_yuan": 200.00, // 目标金额(元)
"saved_cigs": 58, // 节省支数
"expected_cigs": 140, // 预期总支数
"actual_cigs": 82, // 实际总支数
"progress_percent": 72.5, // 完成进度百分比
"pack_price_cent": 2500, // 单包价格(分)
"cigs_per_pack": 20 // 每包支数
},
"profile": {
"baseline_cigs_per_day": 20,
"pack_price_cent": 2500,
"smoking_years": 8
},
"calculated_at": "2026-01-08T10:00:00+08:00"
}
}
```
字段说明:
**health(健康数据)**
- `lung_recovery_percent`:肺部功能恢复百分比(0-100
- `smoke_free_minutes`:无烟时长(分钟)
- `smoke_free_days`:无烟天数(保留1位小数)
- `last_smoke_at`:最后一次实际抽烟时间(RFC3339格式)
- `recovery_stage`:恢复阶段标识
- `next_milestone`:下一个里程碑信息
**savings(储蓄数据)**
- `saved_amount_cent`:已节省金额(分,用于计算)
- `saved_amount_yuan`:已节省金额(元,用于前端展示)
- `target_amount_cent`:目标金额(分)
- `target_amount_yuan`:目标金额(元)
- `saved_cigs`:节省的支数
- `expected_cigs`:预期总支数(基线 × 天数)
- `actual_cigs`:实际总支数
- `progress_percent`:完成进度百分比(saved_amount / target_amount * 100
- `pack_price_cent`:单包价格(分)
- `cigs_per_pack`:每包支数(默认20
**period 参数说明**
- `period=week`(周):
- 默认:本周一 00:00:00 至 本周日 23:59:59
- 若传 `start`,则 `end = start + 6 天`
- `period=month`(月):
- 默认:本月1日 00:00:00 至 本月最后一日 23:59:59
- 若传 `start`,则 `end = start 所在月的最后一日`
- `period=year`(年):
- 默认:本年1月1日 00:00:00 至 本年12月31日 23:59:59
- 若传 `start`,则 `end = start 所在年的12月31日`
curl 示例:
```bash
# 查询本周数据
curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/health_savings?period=week' \
-H 'Authorization: Bearer wx-session-key'
# 查询本月数据
curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/health_savings?period=month' \
-H 'Authorization: Bearer wx-session-key'
# 查询本年数据
curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/health_savings?period=year' \
-H 'Authorization: Bearer wx-session-key'
# 查询指定日期范围
curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/health_savings?period=week&start=2026-01-01&end=2026-01-07' \
-H 'Authorization: Bearer wx-session-key'
```
**注意事项**
1. 若用户尚未补全 `profile`(缺少 `baseline_cigs_per_day` 或 `pack_price_cent`),相关计算字段可能为 0 或使用默认值
2. 若用户从未抽烟(无历史记录),`last_smoke_at` 可能不存在,肺部恢复百分比按最大恢复计算
3. 节省金额计算时,若实际支数 > 预期支数,`saved_amount_cent` 为 0(不显示负数)
4. 时间范围计算时,使用用户所在时区(后端需根据用户配置或默认使用 UTC+8)