diff --git a/docs/smoke/ALGORITHM.md b/docs/smoke/ALGORITHM.md index 4b54bb5..30ea753 100644 --- a/docs/smoke/ALGORITHM.md +++ b/docs/smoke/ALGORITHM.md @@ -21,7 +21,7 @@ | 参数 | 来源 | 默认值 | 说明 | |------|------|--------|------| -| base_interval | profile.baseline_interval_minutes | 60 分钟 | 用户初始平均抽烟间隔 | +| base_interval | profile.baseline_interval_minutes | 60 分钟 | 用户初始平均抽烟间隔(为空/0 时默认 60) | | resisted_7d | 近7天忍住次数 | 0 | level=0,num=0 的记录数 | | bonus_interval | 计算得出 | 0 | 奖励延长时间 | @@ -31,7 +31,7 @@ ``` bonus_interval = min(floor(resisted_7d / 5) * 5, 60) -final_interval = base_interval + bonus_interval +final_interval = clamp(base_interval + bonus_interval, 5, 240) ``` **示例**: @@ -41,17 +41,22 @@ final_interval = base_interval + bonus_interval ### 2.4 睡眠规避 -若计算出的时间落在睡眠区间,顺延到次日起床时间: +若计算出的时间落在睡眠区间,顺延到**下一次**起床时间(可能是当天也可能是次日): ``` if suggested_time in [sleep_time, wake_up_time]: suggested_time = next_day_wake_up_time ``` -### 2.5 算法流程图 +### 2.5 边界与兜底 + +- 若没有历史记录,则以“当前时间”作为 `last_smoke_at` 参与计算。 +- 若生成未来日期计划(如明天),默认建议不早于该日起床时间;未配置作息时按 `07:00` 处理。 + +### 2.6 算法流程图 ``` -获取上次抽烟时间 (last_smoke_at) +获取上次抽烟时间 (last_smoke_at, 若无记录则取当前时间) ↓ 获取用户基础间隔 (base_interval_minutes) ↓ @@ -59,7 +64,7 @@ if suggested_time in [sleep_time, wake_up_time]: ↓ 计算奖励间隔: bonus = min(floor(resisted_7d / 5) * 5, 60) ↓ -计算建议时间: suggested = last_smoke_at + base + bonus +计算建议时间: suggested = last_smoke_at + base + bonus (并限制在 5~240 分钟区间) ↓ 检查是否在睡眠时间? ├── 是 → 顺延到起床时间 @@ -91,20 +96,14 @@ if suggested_time in [sleep_time, wake_up_time]: ### 3.2 AI 建议内容 -AI 会生成以下内容: +AI 会生成以下内容(实际接口格式): ```json { - "advice": "昨天你的吸烟量比限额少了2支,这是一个巨大的胜利!数据显示你的烟瘾在下午2点左右达到顶峰——今天试着那个时候去散散步。", - "time_nodes": [ - { "time": "09:30", "type": "suggested", "note": "第一支,早餐后" }, - { "time": "12:30", "type": "suggested", "note": "午餐后" }, - { "time": "15:30", "type": "suggested", "note": "下午茶时间" }, - { "time": "19:00", "type": "suggested", "note": "晚餐后" }, - { "time": "22:00", "type": "suggested", "note": "睡前最后一支" } - ], - "daily_target": 5, - "tips": ["2点是你的高峰期,准备一颗薄荷糖", "试着用深呼吸替代"] + "not_before_at": "2026-01-05T10:18:00+08:00", + "suggested_at": "2026-01-05T10:28:00+08:00", + "time_nodes": ["09:30", "12:30", "15:30", "19:00", "22:00"], + "advice": "昨天你的吸烟量比限额少了2支,这是一个巨大的胜利!数据显示你的烟瘾在下午2点左右达到顶峰——今天试着那个时候去散散步。" } ``` @@ -299,6 +298,7 @@ function getMotivationMessage(context) { | 忍住成功率 | 忍住次数 / (忍住+抽烟次数) | 意志力评估 | | 平均间隔 | 总时长 / 抽烟次数 | 递减效果 | | 最长无烟时长 | 最大间隔记录 | 成就激励 | +| 较昨日减少 | 昨日支数 - 今日支数(可为负) | 若为负,表示今天超出昨日 | ### 8.2 周报数据结构 diff --git a/docs/smoke/API.md b/docs/smoke/API.md index ac3f689..fa83871 100644 --- a/docs/smoke/API.md +++ b/docs/smoke/API.md @@ -360,6 +360,7 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \ 默认策略(不使用 AI): - 基础间隔:优先使用 `GET /api/v1/smoke/profile` 返回的 `baseline_interval_minutes`;若不存在则默认 `60` 分钟。 - 阶梯式延时:最近 7 天内每累计 `5` 条“忍住记录(level=0,num=0)”,在基础间隔上 `+5` 分钟(最多 `+60` 分钟)。 +- 间隔兜底:最终间隔会限制在 `5~240` 分钟之间。 - 若用户已补全作息时间,会自动规避睡眠区间:若计算出的时间落在睡眠区间,顺延到下一次起床时间。 AI 生成说明: @@ -373,22 +374,23 @@ AI 生成说明: "message": "success", "data": { "source": "default", - "not_before_at": "2026-01-05 10:18:00", - "suggested_at": "2026-01-05 10:18:00", - "last_smoke_at": "2026-01-05 09:30:00", + "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-05 09:30:00", - "next_smoke_at": "2026-01-05 10:18:00", + "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-05 10:00:00" + "as_of": "2026-01-05T10:00:00+08:00" } } } @@ -401,19 +403,20 @@ AI 生成说明: "message": "success", "data": { "source": "ai", - "not_before_at": "2026-01-05 10:18:00", - "suggested_at": "2026-01-05 10:28:00", - "last_smoke_at": "2026-01-05 09:30:00", + "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-05 10:18:00", - "suggested_at": "2026-01-05 10:28:00", + "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", @@ -425,7 +428,8 @@ AI 生成说明: ``` 字段说明(新增首页字段): -- `last_smoke_at`:上次“实际抽烟”时间(忽略忍住记录),格式 `YYYY-MM-DD HH:MM:SS`。 +- `last_smoke_at`:上次“实际抽烟”时间(忽略忍住记录),格式 `RFC3339`(含时区)。 - `today_count`:今日抽烟支数(累加 `num`)。 - `resisted_count`:今日克制次数(`level=0 && num=0`)。 -- `reduced_from_yesterday`:较昨日减少的支数(`max(昨日支数 - 今日支数, 0)`)。 +- `reduced_from_yesterday`:较昨日减少的支数(允许为负数;为负时表示“今天超出昨日”)。 +- `exceeded_yesterday`:是否超出昨日(`true` 表示今天超出昨日,前端可用作单独标识)。 diff --git a/docs/smoke/PRD.md b/docs/smoke/PRD.md index f43169b..1a60d7a 100644 --- a/docs/smoke/PRD.md +++ b/docs/smoke/PRD.md @@ -28,10 +28,10 @@ |------|------|----------| | 问候语 | 根据时段显示(早上好/下午好等) + 用户昵称 | 本地计算 + profile | | AI 提示卡片 | 发现的抽烟规律/建议(可关闭) | `GET /ai/advice` 缓存 | -| 计时环 | 距上次抽烟时间(时:分:秒) | `dashboard.minutes_since_last` | +| 计时环 | 距上次抽烟时间(时:分:秒) | `GET /next_smoke_time` 的 `last_smoke_at`(前端计时) | | 下次建议时间 | 显示建议的下次抽烟时间点 | `GET /next_smoke_time` | -| 今日已抽 | X / 目标数,较昨日 ±N | `dashboard.today_count` | -| 烟瘾发作已抵抗 | 忍住次数统计 | 筛选 `level=0,num=0` 记录 | +| 今日已抽 | X / 目标数,较昨日 ±N | `next_smoke_time.today_count` + `next_smoke_time.reduced_from_yesterday`(可为负) + `next_smoke_time.exceeded_yesterday`(标识“超出昨日”) | +| 烟瘾发作已抵抗 | 忍住次数统计 | `next_smoke_time.resisted_count` | | 记录抽烟按钮 | 快速记录一次抽烟 | `POST /logs` | | 想抽忍住了按钮 | 记录成功抵抗 | `POST /logs/resisted` | @@ -81,7 +81,7 @@ | 筛选 Tabs | 全部 / 已抽烟 / 已忍住 | 前端筛选 | | 时间线 | 按日期分组展示 | `GET /logs` | | 记录卡片 | 类型、时间、原因标签、间隔时间 | logs 数据 | -| 左滑操作 | 编辑 / 删除 | `PUT/DELETE /logs/:id` | +| 左滑操作 | 编辑 / 删除 | `POST/DELETE /logs/:id` | | 新增按钮 | 浮动按钮快速新增 | 跳转记录流程 | ### 2.5 个人中心 (profile_&_settings) @@ -92,11 +92,11 @@ |------|------|----------| | 用户信息 | 头像、昵称 | 微信授权 | | 目标展示 | 目标戒烟日期、连续天数 | profile | -| 目标设定 | 调整每日限额与戒烟日期 | `PUT /profile` | +| 目标设定 | 调整每日限额与戒烟日期 | `POST /profile` | | AI 计划调整 | 个性化辅导风格设置 | profile 扩展 | | 通知设置 | 提醒时间、频率 | 本地存储 | | 会员解锁 | PRO 功能 / 广告解锁 | 会员系统 | -| 基础设置 | 作息时间等 | `PUT /profile` | +| 基础设置 | 作息时间等 | `POST /profile` | | 隐私与数据 | 数据导出、账号注销 | 待扩展 | --- @@ -126,7 +126,7 @@ Step 5: 作息时间 (wake_up_time, sleep_time) ↓ Step 6: 设置目标 (目标日期、每日限额) ↓ -提交 profile (PUT /profile) +提交 profile (POST /profile) ↓ 进入首页 ``` @@ -186,8 +186,8 @@ Step 6: 设置目标 (目标日期、每日限额) ``` [并行请求] ├── GET /profile (用户信息,判断是否需引导) -├── GET /dashboard (今日统计,计时器数据) -└── GET /next_smoke_time (下次建议时间) +├── GET /next_smoke_time (首页汇总 + 下次建议时间) +└── GET /dashboard (看板数据,可延迟) [延迟加载] └── GET /ai/advice (AI提示卡片,非关键) @@ -195,8 +195,8 @@ Step 6: 设置目标 (目标日期、每日限额) **缓存策略**: - profile: 登录后缓存,变更时更新 -- dashboard: 每次进入刷新,后台定时更新 -- next_smoke_time: 缓存至下次记录 +- next_smoke_time: 每次进入刷新,下一次记录后刷新 +- dashboard: 进入看板时刷新 - ai/advice: 按天缓存 ### 5.2 数据预加载 diff --git a/docs/smoke/PRODUCT.md b/docs/smoke/PRODUCT.md index dbbbb92..dd7cba4 100644 --- a/docs/smoke/PRODUCT.md +++ b/docs/smoke/PRODUCT.md @@ -22,7 +22,7 @@ ### 2.2 典型场景 - 抽完烟立刻记一笔(含时间/原因/数量)。 - 想抽但忍住:快速一键记录(`level=0,num=0`)。 -- 首页查看:下次建议时间、距离上次实际抽烟的间隔、今天累计。 +- 首页查看:下次建议时间、上次实际抽烟时间、今日累计、今日克制、较昨日增减。 - 每天晚上/第二天查看 AI 总结与行动建议(解锁后)。 - 生成“今天/明天时间节点计划”(解锁后),帮助前端展示时间线/日程。 @@ -34,7 +34,8 @@ ### 3.2 “最后一次抽烟” - 定义为“最后一次实际抽烟事件”(忽略忍住记录)。 -- 后端看板 `minutes_since_last` 已按该口径计算。 +- 首页口径使用 `next_smoke_time` 返回的 `last_smoke_at` 作为计时起点(前端可自行计算间隔)。 +- 看板的 `minutes_since_last` 仍按该口径计算。 ### 3.3 默认“下次建议时间”(不使用 AI) - 基础间隔来自 `GET /api/v1/smoke/profile` 的 `baseline_interval_minutes`(若无则默认 60 分钟)。 @@ -71,6 +72,7 @@ - 若已存在 AI 时间节点:返回 `source=ai` - 否则:返回 `source=default` - 生成 AI 节点(需要解锁):`GET /api/v1/smoke/next_smoke_time?date=today&mode=ai` +- 接口会同时返回首页汇总字段:`last_smoke_at`、`today_count`、`resisted_count`、`reduced_from_yesterday`(可为负,负数代表“超出昨日”)、`exceeded_yesterday`(前端用于标识“超出昨日”)。 ### 4.4 AI 解锁(按天) - 看广告完成后回调:`POST /api/v1/smoke/ai/advice_unlocks {date}` @@ -78,8 +80,8 @@ - 每日 AI 建议:`GET /api/v1/smoke/ai/advice?date=...` - AI 时间节点计划:`GET /api/v1/smoke/next_smoke_time?date=...&mode=ai` -## 5. 页面能力清单(对应 UI 文档) -- 首页:今日累计、距上次实际抽烟、下次建议时间(默认/AI)、时间节点列表、快速入口(抽烟/忍住)。 +## 5. 页面能力清单 +- 首页:上次实际抽烟时间(用于计时)、今日累计、今日克制、较昨日增减(可为负并标识“超出昨日”)、下次建议时间(默认/AI)、时间节点列表、快速入口(抽烟/忍住)。 - 记录页:快速添加抽烟、快速忍住、补录真实时间 `smoke_at`。 - 列表页:按日期筛选、分页、区分“抽烟/忍住”标签、支持编辑/删除。 - 看板页:周视图/区间视图,展示每日支数与 `minutes_since_last`。 @@ -94,13 +96,12 @@ ### 6.2 Smoke 模块 API - 详见:`docs/smoke/API.md` -- 时序图:`docs/smoke/SEQUENCE.md` ## 7. 权限与错误处理(产品口径) - 未登录:统一提示“请先登录”,引导重新登录。 - AI 未解锁(403):弹窗/页内提示“观看广告解锁当天生成”,提供“去观看”按钮。 - AI 服务不可用(503):提示“稍后再试”,不阻断默认策略展示。 -- 没有记录:列表/看板显示空态;首页 `minutes_since_last` 不显示或显示“暂无数据”。 +- 没有记录:列表/看板显示空态;首页 `last_smoke_at` 不显示或展示“暂无数据”。 ## 8. 埋点与指标(建议) - D1/D7 留存:进入首页、完成补全、记录次数(抽烟/忍住)、打开看板、生成 AI 建议。 diff --git a/docs/smoke/README.md b/docs/smoke/README.md index 6cb4a00..3877105 100644 --- a/docs/smoke/README.md +++ b/docs/smoke/README.md @@ -2,14 +2,10 @@ 本小程序用于记录抽烟情况(日期、原因、烟瘾程度、数量等)。 -## 时序图(前端流程) - -见:`docs/smoke/SEQUENCE.md` - -## 产品与 UI 文档 +## 产品与流程文档 - 产品说明(PRD):`docs/smoke/PRODUCT.md` -- UI/UX 设计说明:`docs/smoke/UI.md` +- 算法与 AI 策略:`docs/smoke/ALGORITHM.md` ## 依赖 diff --git a/docs/smoke/SEQUENCE.md b/docs/smoke/SEQUENCE.md deleted file mode 100644 index 05e9332..0000000 --- a/docs/smoke/SEQUENCE.md +++ /dev/null @@ -1,143 +0,0 @@ -# 戒烟/抽烟记录小程序:时序图 - -以下时序图使用 Mermaid(在支持 Mermaid 的 Markdown 渲染器中可直接预览)。 - -## 1) 登录与鉴权(获取 Bearer Token) - -```mermaid -sequenceDiagram - autonumber - participant MP as 小程序(前端) - participant API as 后端API(Gin) - participant WX as 微信 jscode2session - participant DB as MySQL - participant Redis as Redis(可选) - - MP->>WX: wx.login() 获取 code - MP->>API: POST /api/v1/auth/login {mini_program_id, code, ...可选资料} - API->>DB: 读取 mini_programs(app_id/app_secret) - API->>WX: code 换取 openid/session_key - API->>DB: upsert users(mini_program_id+open_id) - alt 启用 Redis session cache - API->>Redis: 写入 session_key -> user 缓存 - end - API-->>MP: {session_key} 作为后续 Authorization: Bearer -``` - -## 2) 首次进入:基础信息补全(Profile) - -```mermaid -sequenceDiagram - autonumber - participant MP as 小程序(前端) - participant API as 后端API - participant DB as MySQL - - MP->>API: GET /api/v1/smoke/profile (Bearer) - API->>DB: 查询 fa_smoke_user_profile(uid) - API-->>MP: exists/is_completed/baseline_interval_minutes/作息等 - alt 需要补全 - MP->>API: POST /api/v1/smoke/profile {...基础烟量/动机/动力/作息...} - API->>DB: upsert fa_smoke_user_profile(uid) - API-->>MP: 返回最新 profile + baseline_interval_minutes - end -``` - -## 3) 记录抽烟 / 记录“忍住” - -```mermaid -sequenceDiagram - autonumber - participant MP as 小程序(前端) - participant API as 后端API - participant DB as MySQL - - alt 实际抽烟 - MP->>API: POST /api/v1/smoke/logs {smoke_at?, remark?, level?, num?} (Bearer) - API->>DB: INSERT fa_smoke_log(uid, smoke_time, smoke_at, remark, level, num) - API-->>MP: 返回记录 - else 想抽但忍住 - MP->>API: POST /api/v1/smoke/logs/resisted {smoke_at?, remark?} (Bearer) - API->>DB: INSERT fa_smoke_log(uid, ..., level=0, num=0) - API-->>MP: 返回记录(用于列表展示) - end -``` - -## 4) 首页:获取“下次抽烟记录时间”(默认 + AI 自动切换) - -说明:统一使用 `GET /api/v1/smoke/next_smoke_time`。 - -```mermaid -sequenceDiagram - autonumber - participant MP as 小程序(前端) - participant API as 后端API - participant DB as MySQL - - MP->>API: GET /api/v1/smoke/next_smoke_time?date=today&mode=auto (Bearer) - API->>DB: 查 fa_smoke_ai_advice(type=next_smoke_time, advice_date=today) 是否存在 - alt 已存在 AI 建议且 time_nodes 非空 - API->>DB: 查 fa_smoke_ai_next_smoke(ai_advice_id) 取 not_before/suggested/nodes - API-->>MP: source=ai + 时间节点 + advice + default(兜底信息) - else 不存在 AI 建议 - API->>DB: 查 fa_smoke_user_profile(uid) 取 baseline_interval/作息 - API->>DB: 查 fa_smoke_log 取最后一次实际抽烟 + 最近7天忍住次数 - API-->>MP: source=default + default.next_smoke_at - end -``` - -## 5) 生成 AI “下次抽烟时间节点”(需要每日广告解锁) - -说明:前端先“看广告解锁”,再用 `mode=ai` 生成当天/明天的 AI 时间节点;生成结果缓存“一天一份”。 - -```mermaid -sequenceDiagram - autonumber - participant MP as 小程序(前端) - participant API as 后端API - participant DB as MySQL - participant AI as AI Provider(OpenAI-compatible) - - MP->>API: POST /api/v1/smoke/ai/advice_unlocks {date: "YYYY-MM-DD"} (Bearer) - API->>DB: upsert fa_smoke_ai_advice_unlocks(uid, unlock_date=date) - API-->>MP: unlocked=true - - MP->>API: GET /api/v1/smoke/next_smoke_time?date=YYYY-MM-DD&mode=ai (Bearer) - API->>DB: 校验是否解锁(或会员) - alt 未解锁 - API-->>MP: 403 需要观看广告解锁 - else 已解锁 - API->>DB: 读取最近3天 fa_smoke_log(含忍住) + profile + 默认策略 - API->>AI: /chat/completions(严格JSON输出) - API->>DB: 写 fa_smoke_ai_advice(type=next_smoke_time, advice_date=date, ...) - API->>DB: 写 fa_smoke_ai_next_smoke(每个时间点一条:not_before/suggested/node) - API-->>MP: source=ai + 时间节点 - end -``` - -## 6) 每日 AI 戒烟建议(会员/广告解锁) - -```mermaid -sequenceDiagram - autonumber - participant MP as 小程序(前端) - participant API as 后端API - participant DB as MySQL - participant AI as AI Provider(OpenAI-compatible) - - MP->>API: GET /api/v1/smoke/ai/advice?date=YYYY-MM-DD (Bearer) - API->>DB: 查 fa_smoke_ai_advice(type=daily_advice, advice_date=date) 是否已缓存 - alt 已缓存 - API-->>MP: advice - else 未缓存 - API->>DB: 校验会员或广告解锁(unlock_date=date) - alt 未解锁/非会员 - API-->>MP: 403 need vip_or_ad - else 已解锁/会员 - API->>DB: 读取当天 fa_smoke_log(节点/总量) - API->>AI: /chat/completions 生成建议 - API->>DB: 写 fa_smoke_ai_advice(type=daily_advice,...) - API-->>MP: advice - end - end -``` diff --git a/docs/smoke/UI.md b/docs/smoke/UI.md deleted file mode 100644 index 9f9930e..0000000 --- a/docs/smoke/UI.md +++ /dev/null @@ -1,200 +0,0 @@ -# 戒烟/抽烟记录小程序:UI/UX 设计说明 - -本文档面向前端实现,包含信息架构、页面结构、组件与交互细节,以及与后端 API 的映射。 - -## 1. 信息架构(IA) - -推荐 4 个 Tab: -1) 首页 -2) 记录 -3) 看板 -4) 我的 - -页面路由(建议): -- 首页:NextTimeCard + QuickActions + TodaySummary + RecentList -- 记录:新增抽烟、忍住、历史列表入口 -- 看板:周视图/区间视图 -- 我的:基础信息、AI/解锁入口、帮助与隐私 - -## 2. 视觉与组件规范(简版) - -### 2.1 视觉风格 -- 主色:清爽、偏健康(绿色/蓝绿)+ 强对比强调色(橙/红用于“抽烟”风险提示,但避免羞辱语气)。 -- 卡片化布局:关键指标卡片(下次时间/今日累计/间隔)。 - -### 2.2 通用组件 -- `AppHeader`:标题 + 日期切换(today/tomorrow) -- `MetricCard`:标题/主数值/辅助说明/状态标识(AI/默认) -- `PrimaryButton` / `SecondaryButton` -- `Tag`:抽烟/忍住/动机标签 -- `Timeline`:时间节点列表(AI time_nodes) -- `EmptyState` / `ErrorState` / `Skeleton` -- `ModalPaywall`:广告解锁提示(403) - -### 2.3 文案口径 -- 避免指责:使用“建议/可以尝试/你已经在进步”。 -- 忍住记录:用“忍住了”/“成功延后”作为正反馈。 - -## 3. 页面详设 - -## 3.1 首页(核心) - -### 目标 -- 让用户一眼看到:下一次建议时间、今天累计、距离上次实际抽烟的间隔、快速记录入口。 - -### 布局(从上到下) -1) 顶部:日期选择(默认 today,可切 tomorrow) -2) `NextTimeCard` -3) `QuickActions`(两按钮:抽烟/忍住) -4) `TodaySummary`(今日累计、minutes_since_last) -5) `AITipsCard`(可选:每日 AI 建议入口) -6) `RecentList`(最近记录 10~20 条) - -### NextTimeCard(关键) -数据来源: -- `GET /api/v1/smoke/next_smoke_time?date=today|tomorrow&mode=auto` -展示字段: -- `source`: - - `ai`:展示 `not_before_at`、`suggested_at`、`time_nodes`(Timeline),并展示一句 `advice` - - `default`:展示 `default.next_smoke_at`(同时映射为 `not_before_at/suggested_at`) -交互: -- “生成AI计划”按钮:调用同接口但 `mode=ai` - - 403:弹 `ModalPaywall`,引导先看广告解锁 - - 503:提示 AI 暂不可用,仍保留默认卡片 - -解锁流程(按天): -1) 前端看广告完成 -2) `POST /api/v1/smoke/ai/advice_unlocks {date: 计划日期}` -3) 再调 `GET /api/v1/smoke/next_smoke_time?date=...&mode=ai` - -### QuickActions(抽烟/忍住) -- 抽烟:进入“新增抽烟”轻表单(默认 `num=1, level=2`) -- 忍住:一键记录(可选弹小输入框写 remark),调用 `POST /api/v1/smoke/logs/resisted` - -### TodaySummary -数据来源: -- `GET /api/v1/smoke/dashboard`(默认本周)+ 也可在首页只展示 `today_count` 与 `minutes_since_last` -展示: -- 今日支数(sum num) -- 距上次实际抽烟分钟(后端已忽略忍住记录) - -### RecentList -数据来源: -- `GET /api/v1/smoke/logs/latest?limit=20` -展示规则: -- `num==0 && level==0`:标记为“忍住了”,颜色更积极(如绿色),不展示“支数” -- 其它:展示 `num` + `level` + `remark` + `smoke_at`(优先)/创建时间 -交互: -- 点击进入详情(编辑/删除) - -## 3.2 记录页(新增/补录) - -### 页面目标 -- 让记录成本极低(3 秒内完成一条)。 - -### 模块 -1) 快速新增抽烟(表单) -2) 快速忍住(按钮 + 可选 remark) -3) 历史记录入口(跳列表) - -### 新增抽烟表单 -字段: -- `smoke_at`:默认当前时间,可手动改 -- `num`:Stepper(1~10) -- `level`:1~5(或 0~5,但 0 仅用于忍住) -- `remark`:常用标签(压力/无聊/社交/提神)+ 自定义输入 -API: -- `POST /api/v1/smoke/logs` - -## 3.3 历史列表页 - -### 目标 -- 可回溯、可筛选、可编辑。 - -UI: -- 顶部筛选:日期范围(start/end)、分页加载 -- 列表项:时间、类型(抽烟/忍住)、num/level、remark -API: -- `GET /api/v1/smoke/logs?page=&page_size=&start=&end=` - -空态: -- “还没有记录,先从右下角按钮开始” - -## 3.4 记录详情页(编辑/删除) -展示: -- smoke_time、smoke_at、remark、level、num -编辑: -- `POST /api/v1/smoke/logs/:id`(支持清空 smoke_at/smoke_time 传空字符串) -删除: -- `DELETE /api/v1/smoke/logs/:id` - -## 3.5 看板页 - -### 目标 -- 展示趋势:周视图/区间视图,给用户反馈与成就感。 - -UI: -- 顶部:日期范围选择(默认本周) -- 柱状图:weekly.count -- 指标:today_count、minutes_since_last -API: -- `GET /api/v1/smoke/dashboard?start=&end=` - -## 3.6 AI 建议页(每日) - -入口: -- 首页卡片/我的页面 - -展示: -- Markdown 渲染(建议内容可能包含列表) -API: -- `GET /api/v1/smoke/ai/advice?date=YYYY-MM-DD`(默认昨天) -解锁: -- 若 403,弹引导看广告 -> `POST /api/v1/smoke/ai/advice_unlocks {date}` - -## 3.7 基础信息(Profile)页 - -目标: -- 获取默认策略与 AI 个性化所需信息;尽量简短可跳过但提示价值。 - -字段建议(分组) -1) 基础烟量:日均支数 `baseline_cigs_per_day` -2) 烟龄/价格:`smoking_years`、`pack_price_cent` -3) 抽烟动机(多选):压力/无聊/社交/提神 + 自定义 -4) 戒烟动力(多选):健康/家人/省钱 + 自定义 -5) 作息:起床/入睡(HH:MM) - -API: -- `GET /api/v1/smoke/profile` -- `POST /api/v1/smoke/profile` - -## 4. 关键交互与状态 - -### 4.1 AI 生成(按天一次) -推荐状态机: -- `idle` -> `generating` -> `success`/`locked(403)`/`failed(5xx)` - -403(未解锁): -- 弹窗:说明“观看一次广告解锁当天生成” -- 按钮:`去观看`(前端完成广告后调用 unlock 接口) - -### 4.2 日期选择(today/tomorrow) -- 首页切 tomorrow 时: - - `GET /api/v1/smoke/next_smoke_time?date=tomorrow&mode=auto` - - 仅在用户主动点“生成AI计划”时才 `mode=ai`(且 unlock date=tomorrow) - -### 4.3 忍住记录的视觉反馈 -- 提交成功 toast:`已记录:忍住了` -- 列表项显示“忍住”Tag,并用绿色/正向文案。 - -## 5. API 映射速查表 - -- 登录:`POST /api/v1/auth/login`(返回 session_key) -- Profile:`GET/POST /api/v1/smoke/profile` -- 新增抽烟:`POST /api/v1/smoke/logs` -- 忍住:`POST /api/v1/smoke/logs/resisted` -- 列表:`GET /api/v1/smoke/logs`、`GET /api/v1/smoke/logs/latest` -- 看板:`GET /api/v1/smoke/dashboard` -- 下次时间:`GET /api/v1/smoke/next_smoke_time?date=&mode=` -- 每日 AI 建议:`GET /api/v1/smoke/ai/advice?date=` -- 广告解锁:`POST /api/v1/smoke/ai/advice_unlocks {date}` diff --git a/internal/smoke/handler/smoke_next_handler.go b/internal/smoke/handler/smoke_next_handler.go index f89b26a..5a91c53 100644 --- a/internal/smoke/handler/smoke_next_handler.go +++ b/internal/smoke/handler/smoke_next_handler.go @@ -21,6 +21,7 @@ type nextSmokeTimeUnifiedResponse struct { TodayCount int `json:"today_count"` Resisted int `json:"resisted_count"` Reduced int `json:"reduced_from_yesterday"` + Exceeded bool `json:"exceeded_yesterday"` TimeNodes []string `json:"time_nodes,omitempty"` Advice string `json:"advice,omitempty"` Default nextSmokeDefaultResponse `json:"default"` @@ -84,7 +85,7 @@ func (h *SmokeHandler) GetNextSmokeTime(c *gin.Context) { if t == nil { return "" } - return t.In(time.Local).Format(dateTimeLayout) + return t.In(time.Local).Format(time.RFC3339) } formatTimeString := func(value string) string { @@ -93,10 +94,10 @@ func (h *SmokeHandler) GetNextSmokeTime(c *gin.Context) { return "" } if t, err := time.Parse(time.RFC3339, value); err == nil { - return t.In(time.Local).Format(dateTimeLayout) + return t.In(time.Local).Format(time.RFC3339) } if t, err := time.ParseInLocation(dateTimeLayout, value, time.Local); err == nil { - return t.In(time.Local).Format(dateTimeLayout) + return t.In(time.Local).Format(time.RFC3339) } return value } @@ -137,6 +138,7 @@ func (h *SmokeHandler) GetNextSmokeTime(c *gin.Context) { TodayCount: homeSummary.TodayCount, Resisted: homeSummary.ResistedCount, Reduced: homeSummary.ReducedFromYesterday, + Exceeded: homeSummary.ExceededYesterday, Default: formatDefault(defaultSuggestion), } diff --git a/internal/smoke/service/smoke_log_service.go b/internal/smoke/service/smoke_log_service.go index 1a8c3a9..6dc6132 100644 --- a/internal/smoke/service/smoke_log_service.go +++ b/internal/smoke/service/smoke_log_service.go @@ -123,6 +123,7 @@ type SmokeHomeSummary struct { TodayCount int ResistedCount int ReducedFromYesterday int + ExceededYesterday bool } // DashboardWeeklyStat 表示某一天的抽烟支数以及是否为今天。 @@ -290,9 +291,7 @@ func (s *SmokeLogService) HomeSummary(ctx context.Context, uid int, asOf time.Ti } reduced := int(yesterdayCount - todayCount) - if reduced < 0 { - reduced = 0 - } + exceeded := reduced < 0 var lastSmokeAt *time.Time var last smokemodel.SmokeLog @@ -315,6 +314,7 @@ func (s *SmokeLogService) HomeSummary(ctx context.Context, uid int, asOf time.Ti TodayCount: int(todayCount), ResistedCount: int(resistedCount), ReducedFromYesterday: reduced, + ExceededYesterday: exceeded, }, nil }