Enhance smoking tracking API with new features and improvements
- Added a new API endpoint `GET /api/v1/smoke/home` to consolidate core modules for the home dashboard, reducing the need for multiple requests. - Updated the `smoke` routes to include the new home endpoint and improved user profile management with the addition of a `quit_date` field. - Enhanced the algorithm for calculating daily targets and next smoke suggestions, ensuring accurate future time handling and user-specific recommendations. - Improved API documentation to reflect new endpoints, response formats, and detailed field descriptions for better clarity and usability. - Refactored user authentication handling in various handlers to streamline the process and ensure consistent error responses.
This commit is contained in:
+18
-17
@@ -71,6 +71,11 @@ if suggested_time in [sleep_time, wake_up_time]:
|
||||
└── 否 → 返回建议时间
|
||||
```
|
||||
|
||||
### 2.7 过期/未来记录兜底
|
||||
|
||||
- 如果用户补录了“未来时间”的抽烟记录,为了避免前端出现负倒计时,服务端会把 `last_smoke_at` 限制在当前 `as_of` 时刻以内。
|
||||
- 当 `plan_date` 是“今天”且 `last_smoke_at + interval` 早于当前时间时,会根据已过去的分钟数计算需要补齐的间隔,向前跳跃若干次直到结果落在未来。这样首页和 `GET /next_smoke_time` 都能返回一个“未来的下一次建议时间”,不会出现提示已经过期的时间点。
|
||||
- 如果 `plan_date` 是将来日期(例如明天的日程),仍然按照指定日期的起床时间作为 `not_before_at`,不会使用上述补齐逻辑。
|
||||
---
|
||||
|
||||
## 3. AI 增强算法
|
||||
@@ -177,26 +182,22 @@ function calculateStage(startDate) {
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 每日目标计算
|
||||
### 4.3 每日目标计算(线性递减)
|
||||
|
||||
以 onboarding 完成日期为起点,到 `quit_date` 线性递减到 0:
|
||||
|
||||
```javascript
|
||||
// utils/target.js
|
||||
function calculateDailyTarget(baseline, stage, dayInStage) {
|
||||
if (stage === 1) {
|
||||
return baseline
|
||||
}
|
||||
|
||||
if (stage === 2) {
|
||||
const reduction = (dayInStage / 14) * 0.5
|
||||
return Math.max(Math.round(baseline * (1 - reduction)), 1)
|
||||
}
|
||||
|
||||
if (stage === 3) {
|
||||
const targetRate = 0.25 - (dayInStage / 9) * 0.25
|
||||
return Math.max(Math.round(baseline * targetRate), 0)
|
||||
}
|
||||
|
||||
return baseline
|
||||
function calculateDailyTarget(baseline, startDate, quitDate, today) {
|
||||
if (!baseline || !startDate || !quitDate) return baseline
|
||||
if (today >= quitDate) return 0
|
||||
|
||||
const totalDays = daysBetween(startDate, quitDate)
|
||||
if (totalDays <= 0) return baseline
|
||||
|
||||
const remainingDays = daysBetween(today, quitDate)
|
||||
const target = Math.round(baseline * (remainingDays / totalDays))
|
||||
return remainingDays > 0 ? Math.max(target, 1) : 0
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+143
-12
@@ -23,7 +23,7 @@
|
||||
- `smoke_time` 可选;不传则默认“当天”。
|
||||
- `smoke_at` 可选;真实抽烟时间(格式 `YYYY-MM-DD HH:MM:SS`)。用于“按时间节点分析/AI 建议”;不传则可用 `createtime` 近似。
|
||||
- `level/num` 可选;不传时后端会按 `1` 处理。
|
||||
- 如果要记录“想抽但忍住了”,请传 `level=0` 且 `num=0`(会在 `fa_smoke_log` 中展示为一条记录,但不会影响看板的支数累加)。
|
||||
- “想抽但忍住了”请传 `level=0` 且 `num=0`,系统以 `num=0` 作为“忍住”的判断条件(计入忍住次数,但不计入抽烟支数)。
|
||||
|
||||
curl 示例:
|
||||
|
||||
@@ -67,12 +67,16 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \
|
||||
|
||||
## 3) 列表查询(分页)
|
||||
|
||||
`GET /api/v1/smoke/logs?page=1&page_size=20&start=2025-12-01&end=2025-12-31`
|
||||
`GET /api/v1/smoke/logs?page=1&page_size=20&start=2025-12-01&end=2025-12-31&type=all`
|
||||
|
||||
参数:
|
||||
- `page`:页码,默认 `1`
|
||||
- `page_size`:每页数量,默认 `20`,最大 `200`
|
||||
- `start/end`:可选,按 `smoke_time` 过滤(格式 `YYYY-MM-DD`)
|
||||
- `type`:可选,默认 `all`;`smoke` 表示抽烟记录(`num>0`),`resisted` 表示忍住记录(`num=0`)
|
||||
|
||||
说明:
|
||||
- 列表按时间倒序返回(优先 `smoke_at`,其次 `createtime`,最后 `smoke_time`)。
|
||||
|
||||
成功响应示例:
|
||||
|
||||
@@ -199,6 +203,125 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \
|
||||
- 权限:会员用户直接可用;非会员需要先对该 `date` 完成“看广告解锁”(见下一个接口)。
|
||||
- 建议结果会按 `uid + date + prompt_version` 缓存(表:`fa_smoke_ai_advice`)。
|
||||
|
||||
## 9) 首页整合接口(Home Dashboard)
|
||||
|
||||
`GET /api/v1/smoke/home`
|
||||
|
||||
此接口把首页 UI 所需核心模块一次返回,避免前端串行请求多个接口。返回示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"greeting": {
|
||||
"title": "早安,Alex",
|
||||
"subtitle": "今天也是清爽的一天",
|
||||
"nickname": "Alex",
|
||||
"time_of_day": "morning",
|
||||
"avatar_url": "https://example.com/avatar.jpg"
|
||||
},
|
||||
"profile": {
|
||||
"exists": true,
|
||||
"is_completed": true,
|
||||
"awake_minutes": 900,
|
||||
"baseline_interval_minutes": 60,
|
||||
"profile": {
|
||||
"baseline_cigs_per_day": 10,
|
||||
"pack_price_cent": 3200,
|
||||
"wake_up_time": "07:20",
|
||||
"sleep_time": "23:30"
|
||||
}
|
||||
},
|
||||
"advice_card": {
|
||||
"title": "智能控烟建议",
|
||||
"date": "2026-01-04",
|
||||
"message": "根据你的习惯,下午2点是烟瘾高峰,可以试试短暂散步。",
|
||||
"status": "available"
|
||||
},
|
||||
"campaign_card": {
|
||||
"title": "绿色生活,从戒烟开始",
|
||||
"subtitle": "BRAND CAMPAIGN",
|
||||
"badge": "广告"
|
||||
},
|
||||
"timer": {
|
||||
"label": "距上次抽烟",
|
||||
"last_smoke_at": "2026-01-05T07:42:00+08:00",
|
||||
"seconds_since_last": 9900,
|
||||
"next_suggested_at": "2026-01-05T10:30:00+08:00",
|
||||
"next_suggested_clock": "10:30",
|
||||
"not_before_at": "2026-01-05T10:30:00+08:00",
|
||||
"suggestion_source": "default",
|
||||
"suggestion_algorithm": "staircase_delay_v1"
|
||||
},
|
||||
"summary": {
|
||||
"today_count": 3,
|
||||
"daily_target": 10,
|
||||
"resisted_count": 1,
|
||||
"reduced_from_yesterday": 2,
|
||||
"exceeded_yesterday": false,
|
||||
"profile_completed": true
|
||||
},
|
||||
"motivation": {
|
||||
"message": "太棒了!你刚刚成功抵抗了一次烟瘾",
|
||||
"type": "praise"
|
||||
},
|
||||
"quick_actions": [
|
||||
{ "type": "log_smoke", "title": "记录抽烟", "primary": false },
|
||||
{ "type": "resist", "title": "想抽忍住了", "primary": true }
|
||||
],
|
||||
"data_sources": {
|
||||
"ai_advice_date": "2026-01-04",
|
||||
"plan_date": "2026-01-05"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
- `greeting.title`:问候语 + 昵称(如“下午好,Alex”)。
|
||||
- `greeting.subtitle`:副标题/心情提示文案。
|
||||
- `greeting.nickname`:昵称(无昵称时使用“朋友”)。
|
||||
- `greeting.time_of_day`:时间段标识(`morning`/`noon`/`afternoon`/`evening`)。
|
||||
- `greeting.avatar_url`:头像 URL。
|
||||
- `profile.exists`:是否存在用户档案。
|
||||
- `profile.profile`:档案详情对象(可能为空)。
|
||||
- `profile.is_completed`:是否已完成 onboarding。
|
||||
- `profile.awake_minutes`:清醒时长(分钟)。
|
||||
- `profile.baseline_interval_minutes`:基准间隔(分钟)。
|
||||
- `advice_card.title`:AI 提示卡片标题。
|
||||
- `advice_card.date`:建议对应日期。
|
||||
- `advice_card.message`:AI 建议内容。
|
||||
- `advice_card.status`:`available`、`locked`(需解锁)、`unavailable`(AI 服务未配置)、`no_data`(所需日期没有记录)、`empty`(初始化)。
|
||||
- `advice_card.model`:模型名称(有则返回)。
|
||||
- `campaign_card.title`:活动标题。
|
||||
- `campaign_card.subtitle`:活动副标题。
|
||||
- `campaign_card.badge`:活动角标(如“广告”)。
|
||||
- `timer.label`:展示标题(如“距上次抽烟”)。
|
||||
- `timer.last_smoke_at`:最近一次实际抽烟时间(RFC3339)。
|
||||
- `timer.seconds_since_last`:距上次抽烟的秒数(无记录返回 `-1`)。
|
||||
- `timer.next_suggested_at`:建议下次抽烟时间(RFC3339)。
|
||||
- `timer.next_suggested_clock`:仅时分显示(如“16:30”)。
|
||||
- `timer.not_before_at`:不早于的时间点(当前与 `next_suggested_at` 一致)。
|
||||
- `timer.suggestion_source`:建议来源(`default`/`ai`)。
|
||||
- `timer.suggestion_algorithm`:算法版本(`staircase_delay_v1`)。
|
||||
- `timer` 说明:`seconds_since_last` 基于服务器当前时间计算,`last_smoke_at` 若补录未来时间会截断到 `as_of`;当 `plan_date=今天` 时会补齐过期间隔确保 `next_suggested_at` 在未来。
|
||||
- `summary.today_count`:今日吸烟支数累加。
|
||||
- `summary.daily_target`:每日目标(线性递减:以 `onboarding_completed_at` 为起点,按 `quit_date` 线性下降到 0)。
|
||||
- `summary.resisted_count`:今日忍住次数。
|
||||
- `summary.reduced_from_yesterday`:与昨日的绝对差值(非负)。
|
||||
- `summary.exceeded_yesterday`:是否比昨天多。
|
||||
- `summary.profile_completed`:是否已完成基础信息。
|
||||
- `motivation.message`:激励语文案。
|
||||
- `motivation.type`:激励语类型。
|
||||
- `quick_actions[].type`:动作类型(`log_smoke`/`resist`)。
|
||||
- `quick_actions[].title`:按钮文案。
|
||||
- `quick_actions[].primary`:是否主按钮。
|
||||
- `data_sources.ai_advice_date`:AI 建议日期。
|
||||
- `data_sources.plan_date`:当前计划日期。
|
||||
|
||||
如需 AI 时间节点完整版,可继续调用 `GET /ai/next_smoke_time`;首页接口只返回默认建议,避免额外的 AI 生成成本。
|
||||
|
||||
未满足权限时的建议响应(示例):
|
||||
```json
|
||||
{
|
||||
@@ -223,7 +346,7 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \
|
||||
}
|
||||
```
|
||||
|
||||
## 9) 看广告解锁(用于非会员)
|
||||
## 10) 看广告解锁(用于非会员)
|
||||
|
||||
`POST /api/v1/smoke/ai/advice_unlocks`
|
||||
|
||||
@@ -241,7 +364,7 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \
|
||||
- 解锁是“按天”的:观看一次广告解锁一天内的 AI 生成功能(可用于「每日 AI 建议」以及「AI 下次抽烟时间节点」)。
|
||||
- 如果你要生成“明天”的 AI 时间节点,请把 `date` 传为明天日期(例如 `2026-01-06`)。
|
||||
|
||||
## 10) 获取用户基础信息(首次进入:判断是否需要补全)
|
||||
## 11) 获取用户基础信息(首次进入:判断是否需要补全)
|
||||
|
||||
`GET /api/v1/smoke/profile`
|
||||
|
||||
@@ -269,6 +392,7 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \
|
||||
"quit_motivations": ["身体健康", "省钱"],
|
||||
"wake_up_time": "07:30",
|
||||
"sleep_time": "23:30",
|
||||
"quit_date": "2026-02-28T00:00:00+08:00",
|
||||
"onboarding_completed_at": "2026-01-05T10:00:00+08:00"
|
||||
},
|
||||
"is_completed": true,
|
||||
@@ -299,8 +423,9 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \
|
||||
- `smoke_motivations`(抽烟动机):如 `压力大/无聊/社交/提神`,用于 AI 在分析 remark 时更有针对性。
|
||||
- `quit_motivations`(戒烟动力):如 `身体健康/家人孩子/省钱`,当用户产生动摇时 AI 可用这些信息做“情感阻断/自我提醒”。
|
||||
- `wake_up_time` + `sleep_time`(作息时间):用于自动规避睡眠时间,防止在用户睡觉时提醒其“坚持”。
|
||||
- `quit_date`(目标戒烟日期):用于阶段规划或到期提醒。
|
||||
|
||||
## 11) 补全/更新用户基础信息(Upsert)
|
||||
## 12) 补全/更新用户基础信息(Upsert)
|
||||
|
||||
`POST /api/v1/smoke/profile`
|
||||
|
||||
@@ -319,13 +444,14 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \
|
||||
"smoke_motivations": ["压力大", "社交"],
|
||||
"quit_motivations": ["身体健康", "省钱"],
|
||||
"wake_up_time": "07:30",
|
||||
"sleep_time": "23:30"
|
||||
"sleep_time": "23:30",
|
||||
"quit_date": "2026-02-28"
|
||||
}
|
||||
```
|
||||
|
||||
成功响应:同 `GET /api/v1/smoke/profile`(返回最新 `profile` + `is_completed` + `baseline_interval_minutes`)。
|
||||
|
||||
## 12) 想抽但忍住了(写入一条 level=0,num=0 的记录)
|
||||
## 13) 想抽但忍住了(写入一条 level=0,num=0 的记录)
|
||||
|
||||
`POST /api/v1/smoke/logs/resisted`
|
||||
|
||||
@@ -334,7 +460,9 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \
|
||||
{
|
||||
"smoke_time": "2026-01-05",
|
||||
"smoke_at": "2026-01-05 10:20:00",
|
||||
"remark": "压力大,想抽但忍住了"
|
||||
"remark": "压力大,想抽但忍住了",
|
||||
"level": 0,
|
||||
"num": 0
|
||||
}
|
||||
```
|
||||
|
||||
@@ -342,7 +470,7 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \
|
||||
- 该接口会在 `fa_smoke_log` 中新增一条记录:`level=0` 且 `num=0`,用于更直观记录“想抽/忍住”的过程。
|
||||
- 这类记录不会影响 `today_count/weekly.count` 的支数统计(因为 `num=0`)。
|
||||
|
||||
## 13) 获取“下次抽烟记录时间”(默认 + AI 自动切换)
|
||||
## 14) 获取“下次抽烟记录时间”(默认 + AI 自动切换)
|
||||
|
||||
`GET /api/v1/smoke/next_smoke_time`
|
||||
|
||||
@@ -430,11 +558,11 @@ AI 生成说明:
|
||||
字段说明(新增首页字段):
|
||||
- `last_smoke_at`:上次“实际抽烟”时间(忽略忍住记录),格式 `RFC3339`(含时区)。
|
||||
- `today_count`:今日抽烟支数(累加 `num`)。
|
||||
- `resisted_count`:今日克制次数(`level=0 && num=0`)。
|
||||
- `resisted_count`:今日克制次数(`num=0`)。
|
||||
- `reduced_from_yesterday`:较昨日减少的支数(允许为负数;为负时表示“今天超出昨日”)。
|
||||
- `exceeded_yesterday`:是否超出昨日(`true` 表示今天超出昨日,前端可用作单独标识)。
|
||||
|
||||
## 14) 数据统计分析(趋势 + 健康 + 省钱)
|
||||
## 15) 数据统计分析(趋势 + 健康 + 省钱)
|
||||
|
||||
`GET /api/v1/smoke/stats?range=week|month|year&date=2026-01-07`
|
||||
|
||||
@@ -445,6 +573,7 @@ AI 生成说明:
|
||||
说明:
|
||||
- 用于“统计页”一屏数据整合(趋势、均值、环比、健康恢复、省钱、连续记录、已拒绝次数)。
|
||||
- `trend_unit`:`day` 或 `month`,用于前端图表横轴显示。
|
||||
- `trend`:最多返回 7 个 label,超出会等距抽样,方便绘图。
|
||||
|
||||
成功响应(示例):
|
||||
```json
|
||||
@@ -493,9 +622,11 @@ AI 生成说明:
|
||||
字段说明:
|
||||
- `change_percent`:与上一个同周期对比的变化比例(可为负)。
|
||||
- `money.available=false`:表示缺少 `baseline_cigs_per_day` 或 `pack_price_cent`。
|
||||
- `money.expected_total`:按“统计周期内有记录的天数”×`baseline_cigs_per_day` 计算;不统计无日志的天数。
|
||||
- `money.saved_cent`:按 `max(expected_total - actual_total, 0)` 计算,避免出现负值。
|
||||
- `health.available=false`:表示无历史记录。
|
||||
|
||||
## 15) 激励语(后端统一生成)
|
||||
## 16) 激励语(后端统一生成)
|
||||
|
||||
`GET /api/v1/smoke/motivation`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user