# InsuSnap **Repository Path**: sjq04/insu-snap ## Basic Information - **Project Name**: InsuSnap - **Description**: InsuSnap! - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-03-16 - **Last Updated**: 2026-03-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Insu-Snap 后端 Insu-Snap 是一个基于 FastAPI 的后端服务,用于糖尿病用户信息管理、血糖历史记录查询和餐食识别。 ## 技术栈 - Python 3.10+ - FastAPI - SQLAlchemy Async - MySQL - httpx - LogMeal API ## 项目结构 ```text insu-snap/ |- config/ # 数据库与外部服务配置 |- crud/ # 数据库访问逻辑 |- models/ # SQLAlchemy 数据模型 |- routers/ # FastAPI 路由定义 |- schemas/ # Pydantic 请求和响应模型 |- services/ # 外部服务集成 |- sql/ | `- init_db.sql # 数据库初始化脚本 |- main.py # 应用入口 |- requirements.txt `- .env.example ``` ## 快速开始 ### 1. 创建虚拟环境 ```powershell python -m venv .venv .venv\Scripts\activate ``` ### 2. 安装依赖 ```powershell pip install -r requirements.txt ``` ### 3. 创建 `.env` 把 `.env.example` 复制为 `.env`,然后按实际环境填写配置。 示例: ```env DB_HOST=localhost DB_PORT=3306 DB_NAME=fastapi DB_USER=root DB_PASSWORD=your-mysql-password DB_CHARSET=utf8 DB_ECHO=true LOGMEAL_API_KEY=your-logmeal-token LOGMEAL_BASE_URL=https://api.logmeal.com LOGMEAL_TIMEOUT=30 LOGMEAL_NUTRITION_MODEL_VERSION= WECHAT_APPID=your-wechat-appid WECHAT_SECRET=your-wechat-secret OSS_ACCESS_KEY_ID=your-oss-ak OSS_ACCESS_KEY_SECRET=your-oss-sk OSS_BUCKET=your-bucket-name OSS_REGION=cn-beijing OSS_DIR_PREFIX=meals OSS_EXPIRE_SECONDS=300 # 可选:如果你走 CDN,请填 CDN 域名,例如 https://img.example.com OSS_PUBLIC_BASE_URL= # 可选:若使用 STS 临时令牌,请填写 OSS_STS_TOKEN= ``` ### 4. 初始化数据库 执行 [sql/init_db.sql](E:\comp\2026software\insu-snap\sql\init_db.sql)。 如果你的终端里已经能直接使用 `mysql`: ```powershell mysql -u root -p < sql/init_db.sql ``` 如果本机没有 `mysql` 命令,也可以用 MySQL Workbench 或其他 SQL 客户端手动执行该脚本。 ### 5. 启动服务 ```powershell uvicorn main:app --reload ``` 默认访问地址: - Swagger UI:`http://127.0.0.1:8000/docs` - ReDoc:`http://127.0.0.1:8000/redoc` ## 配置说明 ### 数据库配置 后端会从 [config/db_conf.py](E:\comp\2026software\insu-snap\config\db_conf.py) 读取数据库配置。 - `DB_HOST`:MySQL 主机地址 - `DB_PORT`:MySQL 端口 - `DB_NAME`:数据库名 - `DB_USER`:MySQL 用户名 - `DB_PASSWORD`:MySQL 密码 - `DB_CHARSET`:MySQL 字符集,默认 `utf8` - `DB_ECHO`:是否打印 SQLAlchemy SQL 日志,`true` 或 `false` ### LogMeal 配置 后端会从 [config/logmeal_conf.py](E:\comp\2026software\insu-snap\config\logmeal_conf.py) 读取 LogMeal 配置。 - `LOGMEAL_API_KEY`:LogMeal 的 token - `LOGMEAL_BASE_URL`:LogMeal 接口基础地址 - `LOGMEAL_TIMEOUT`:请求超时时间,单位秒 - `LOGMEAL_NUTRITION_MODEL_VERSION`:可选的营养模型版本 如果 `LOGMEAL_API_KEY` 为空,当前餐食识别逻辑会退回默认 mock 数据,而不是真实识别结果。 ### 微信小程序配置 后端会从 `config/wechat_conf.py` 读取微信小程序配置: - `WECHAT_APPID`:小程序 AppID - `WECHAT_SECRET`:小程序 Secret 用于后端在登录时将 `wx.login()` 返回的 `code` 换取 `openid`。 ## 接口概览 ### 用户相关 - `GET /user/parameters?user_id=1` - `PUT /user/parameters?user_id=1` - `PUT /user/guardian?user_id=1` ### 微信认证相关 - `POST /auth/wechat/register` - `POST /auth/wechat/login` 说明: - 小程序侧先调用 `wx.login()` 获取 `code`,再把 `code` 提交给后端。 - `register`:首次注册,后端创建 `users` 并绑定该 `openid`,成功后返回 `user_id`。 - `login`:已注册后直接返回 `user_id`;若未注册则提示“未注册,请先注册”。 - 返回的 `user_id` 需要继续作为查询参数传给其它接口(例如 `GET /meal/dose?user_id=...`)。 请求体示例(注册): ```json { "code": "JSCODE_FROM_WX_LOGIN", "username": "alice", "password": "your-password", "phone": "13800138000", "weight": 55.0, "type": 1, "icr": 10.0, "isf": 2.5, "tdd": 30.0, "guardian_phone": "13800138001" } ``` 请求体示例(登录): ```json { "code": "JSCODE_FROM_WX_LOGIN" } ``` ### 血糖相关 - `GET /glucose/history?user_id=1&range=12h` - `range` 目前支持:`3h`、`6h`、`12h`、`24h` ### 餐食相关 - `POST /meal/analyze?user_id=1` - `GET /upload/credential?user_id=1` - `POST /meal/dose?user_id=1` - `POST /meal/correct` - `POST /meal/insulin/injection?user_id=1` `/meal/analyze` 目前只保留以下字段: - `image`:单张图片路径字符串,可传本地路径或 OSS 图片 URL - `images`:多张图片路径字符串,可传本地路径或 OSS 图片 URL 单张图片路径示例: ```json { "image": "https://comp-software.oss-cn-beijing.aliyuncs.com/multiple_items_meal.jpg" } ``` 多张图片路径示例: ```json { "images": [ "https://example.oss-cn-beijing.aliyuncs.com/meal-1.jpg", "https://example.oss-cn-beijing.aliyuncs.com/meal-2.jpg" ] } ``` #### 说明:`/meal/analyze` 会写入 `current_glucose` 当 `/meal/analyze` 成功识别并创建 `meal_records` 后,后端会从该用户的 `glucose_records` 中取“最近一条血糖”写入 `meal_records.current_glucose`。 - 若用户没有任何 `glucose_records`,则 `current_glucose` 为 `null` - `MealAnalyzeResponse.data` 中也会返回 `current_glucose` #### 建议剂量:`POST /meal/dose?user_id=...` - 请求 body 仅包含: ```json { "meal_record_id": 1 } ``` - 不需要前端传 `current_glucose` - 服务端优先使用 `meal_records.current_glucose`;若为空,则自动回退到该用户 `glucose_records` 的最近一次 `value` - 返回(外层为 `ApiResponse`)包含:`iob`、`meal_dose`、`correction_dose`、`suggested_dose`、`calculation_basis` #### 录入注射:`POST /meal/insulin/injection?user_id=...` - 请求 body 示例: ```json { "timestamp": "2025-03-19T08:30:00", "dose": 12.0 } ``` - `dose` 单位为 `U` - 成功后会返回 `id`、`timestamp`、`dose` 餐食修正示例: ```json { "meal_record_id": 1, "foods": [ { "id": 1, "weight": 180 } ] } ``` #### 餐食修正:`POST /meal/correct` - 请求 body 字段: - `meal_record_id: int` - `foods: [{ id: int, weight: float }]` - 响应的 `data` 结构(外层仍为 `ApiResponse`): - `meal_record_id: int` - `total_carb: float` - `foods: [{ id, name, weight, carb }]` ## 数据表说明 初始化脚本会创建以下数据表: - `users` - `glucose_records` - `meal_records` - `meal_foods` - `insulin_injections` 当前后端接口实际使用到的表有: - `users` - `glucose_records` - `meal_records` - `meal_foods` `insulin_injections` 已通过 `POST /meal/insulin/injection` 录入,并参与 `POST /meal/dose` 与 `GET /feedback/weekly` 的计算。 ## 说明 - 当前项目使用 SQL 脚本初始化数据库,没有引入 Alembic 之类的迁移工具。 - `meal_foods` 通过 `ON DELETE CASCADE` 关联 `meal_records`。 - 修改 `.env` 之后,需要重启 `uvicorn` 才会生效。 ## AI Chat ### LLM 配置 聊天接口当前使用 `SiliconFlow`,走 OpenAI 兼容接口。 请在 `.env` 中补充以下字段: ```env LLM_API_KEY=your-llm-api-key LLM_BASE_URL=https://api.siliconflow.cn/v1 LLM_MODEL=Qwen/Qwen2.5-7B-Instruct LLM_TIMEOUT=30 LLM_MAX_HISTORY=12 ``` ### 聊天接口 - `POST /chat/message?user_id=1` 请求体示例: ```json { "messages": [ { "role": "user", "content": "我午饭吃了米饭和鸡胸肉,那我晚饭应该怎么调整?" } ] } ``` `role` 当前支持: - `user` - `assistant` - `system` ### 多轮对话方式 后端当前不存储聊天记录。 多轮对话由前端保存最近几轮消息,并在每次请求时通过 `messages` 一起传给后端。 推荐做法: - 前端保留最近 8 到 12 条消息 - 每次请求都把最近这段历史完整传回后端 - 不要无限追加历史,避免 token 消耗过高 多轮示例: ```json { "messages": [ { "role": "user", "content": "我午饭吃了米饭和鸡胸肉。" }, { "role": "assistant", "content": "这顿主食不算少,建议关注餐后血糖。" }, { "role": "user", "content": "那我晚饭应该怎么调整?" } ] } ``` ### 当前回答策略 聊天功能目前采用“规则回答 + LLM 回答”的混合策略。 规则回答: - 餐次调整类问题,例如 `晚饭怎么调整` - 复测时机类问题,例如 `饭后多久测血糖` LLM 回答: - 开放分析类问题,例如 `最近血糖为什么波动` - 通识科普类问题,例如 `糖尿病分为几类` - 需要自然追问的多轮对话 ### LLM 会使用的上下文 当请求走 LLM 路径时,后端可能会补充以下上下文: - 用户基础资料 - 最近血糖记录 - 最近餐食记录 - 距离最近一餐过去了多久 - 是否存在接近餐后 2 小时的血糖记录 - 前端传来的多轮对话历史 如果是通识科普问题,后端不会注入最近餐食和最近血糖上下文,避免回答被个体化建议干扰。 ### 测试示例 LLM 通识科普: ```json { "messages": [ { "role": "user", "content": "糖尿病的诱因有什么" } ] } ``` LLM 开放分析: ```json { "messages": [ { "role": "user", "content": "我最近血糖有点波动,帮我看看可能是什么原因?" } ] } ``` 规则回答,下一餐调整: ```json { "messages": [ { "role": "user", "content": "我午饭吃了米饭和鸡胸肉,那我晚饭应该怎么调整?" } ] } ``` 规则回答,复测时机: ```json { "messages": [ { "role": "user", "content": "这顿饭后多久测血糖更合适?" } ] } ``` ## 当前完整接口清单 ### 用户相关 - `GET /user/parameters?user_id=1` - `PUT /user/parameters?user_id=1` - `PUT /user/guardian?user_id=1` ### 微信认证相关 - `POST /auth/wechat/register` - `POST /auth/wechat/login` ### 血糖相关 - `GET /glucose/history?user_id=1&range=12h` ### 餐食相关 - `POST /meal/analyze?user_id=1` - `POST /meal/correct` - `POST /meal/dose?user_id=1` - `POST /meal/insulin/injection?user_id=1` ### 周反馈相关 - `GET /feedback/weekly?user_id=1` 说明(与产品文档差异): - 查询参数 **`user_id`**:与项目其它接口一致;产品文档若写「无参数」,需后续接入登录态后再改为从 token 取用户。 - 统计窗口:默认 **`meal_time` 在最近 7 天内**(含当前时刻)。 - **红黑榜条数**:由 `services/weekly_feedback_service.py` 中常量 **`TOP_N`(默认 3)** 控制。 - **推荐剂量用于覆盖比**:周复盘内使用 **未做四舍五入** 的 `recommended_dose`(与 `POST /meal/dose` 返回里保留 1 位小数的值可能略有差异,仅影响是否落入 0.8–1.2 覆盖区间)。 - IOB 计算口径:默认 **DIA=4 小时**;对每一顿饭,取 `injection_time` 落在 `[meal_time-4h, meal_time]` 的注射,remaining_ratio = `max(1 - (meal_time - injection_time)/4h, 0)`,`IOB = Σ dose * remaining_ratio`。 - 推荐剂量(理论值):`meal_dose = total_carb / icr`;`correction_dose = max((current_glucose - target_glucose) / isf, 0)`;`recommended_dose = max(meal_dose + correction_dose - IOB, 0)`。 - 实际注射剂量:取 `meal_time ± 30 分钟` 内的注射剂量求和为 `actual_dose`。 - 有效餐次过滤:coverage_ratio = `actual_dose / recommended_dose`,仅纳入 `0.8 <= coverage_ratio <= 1.2` 的餐次。 - 餐后指标计算:餐后 2h 血糖从 `[meal_time+2h-30m, meal_time+2h+30m]` 里取最接近 `meal_time+2h` 的记录;峰值血糖取 `[meal_time, meal_time+3h]` 内最大值;`delta_bg = peak_bg - current_glucose`。 - 达标/高风险阈值:达标 `bg_2h <= 10.0`;高风险 `peak_bg >= 11.1` 或 `delta_bg >= 4.0`。 - 食物归因:对该餐 `meal_foods` 中每个 **distinct `name`**,把这顿餐的结果都累计一次(同一餐内同名食物只计一次)。 - 最小样本与红黑排序:仅当 `appear_count >= 3` 才进入候选;红榜排序 `target_rate` 降序,其次 `avg_delta_bg` 升序、`avg_peak_bg` 升序;黑榜排序相反(`target_rate` 升序,`avg_delta_bg` 与 `avg_peak_bg` 降序)。 - 无有效餐次:`red_list`/`black_list` 为空,`analysis` 返回“近期数据不足,暂无法生成饮食复盘。” 响应结构补全: - `data.red_list` / `data.black_list`:列表,每个元素结构为 `{ name: string, impact: string }` - `data.analysis`:字符串(饮食复盘总结) ### 对话相关 - `POST /chat/message?user_id=1`