# uniapp **Repository Path**: sjsjssj/uniapp ## Basic Information - **Project Name**: uniapp - **Description**: No description available - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-29 - **Last Updated**: 2026-10-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 食遇校园 · AI 智能食堂 > 解决「每天不知道吃啥」的校园食堂 AI 推荐应用。 > 输入口味偏好(爱吃辣 / 清淡 / 不吃香菜 / 预算多少),调用大模型 API,从食堂窗口菜品里智能匹配出今天该吃的那几道菜,并给出推荐理由。 本项目为 uni-app(Vue3)**纯前端 + uniCloud 云函数**架构,无需自建服务器,在 HBuilderX 里关联服务空间即可运行与发行。 --- ## 一、核心功能 | 模块 | 说明 | | --- | --- | | **AI 智能推荐** | 一句话描述("想吃辣的,20 块以内,不要香菜")+ 精细筛选(用餐时段 / 口味 / 忌口 / 预算 / 食堂),云端调用大模型返回 3~5 道菜、推荐理由与匹配度 | | **忌口硬过滤** | 忌口食材在**服务端二次校验**,即使大模型产生幻觉推荐了含忌口食材的菜,也会被校验环节剔除 | | **降级兜底** | 未配置 Key / 大模型超时 / 云空间不可用 → 自动降级为本地规则引擎,功能始终可用(首页与推荐页会标注当前数据来源) | | **菜品浏览** | 菜品列表(搜索 / 食堂 / 分类 / 排序 / 分页)、菜品详情、榜单(热门 / 高分 / 新品) | | **评价体系** | 星级 + 文字评价,需管理员审核通过后公开展示 | | **口味档案** | 自动记住上次偏好,下次进入推荐页一键复用,并保留最近推荐历史 | | **管理后台** | 数据看板、菜品增删改、评价审核、**AI 配置(大模型 Key / 模型 / 中转地址)**、菜品库初始化 | --- ## 二、技术架构 ``` ┌─────────────────────────── uni-app 前端(Vue3 + script setup) ───────────────────────────┐ │ 首页 / AI推荐 / 菜品列表 / 菜品详情 / 评价 / 我的 / 管理后台 │ │ ↓ 只依赖统一适配层 │ │ api/index.js(统一 API 适配层) │ └───────────────────────────────────┬──────────────────────────────────────────────────────┘ │ ① 主链路:uniCloud 云函数 ▼ ┌───────────────────────────────────────────────────────────────┐ │ shiyu-recommend AI 推荐(Prompt 构建 → 大模型 → 严格校验) │ │ shiyu-dish 菜品查询 / 榜单 / 食堂分类 / 增删改 │ │ shiyu-config AI 配置读写(Key 不下发客户端)+ 连通性测试 │ │ shiyu-review 评价发布 / 查询 / 审核 │ │ shiyu-seed 菜品库初始化(sync 补齐 / reset 重置) │ └───────────────────────────────┬───────────────────────────────┘ │ HTTPS(OpenAI 兼容协议) ▼ 大模型 API(DeepSeek / 通义千问 / Kimi / 智谱 / 任意中转) ② 兜底链路(云函数不可用时) common/local-dishes.js(40 道本地菜谱)+ common/recommend-engine.js(客户端规则引擎) ``` ### 为什么这样设计 - **主链路走云函数**:`uniCloud` 由 DCloud 托管,免运维、免备案,答辩演示不需要现场启动任何后端进程;同时大模型 API Key 只存在于云端,符合"密钥不出服务端"的安全要求。 - **兜底链路走本地规则引擎**:把推荐能力做成"可降级"的,网络异常或未配置 Key 时应用依然完整可用,避免演示翻车。 - **统一适配层**:页面只依赖 `api/index.js` 的语义化方法,不关心数据来自云函数还是本地,后续更换后端实现(例如换成自建 FastAPI)不需要改动任何页面。 --- ## 三、目录结构 ``` ├── api/ │ └── index.js # 统一 API 适配层(云函数优先 + 本地兜底) ├── common/ │ ├── constants.js # 口味/忌口/预算/服务商等常量 │ ├── local-dishes.js # 40 道本地菜品数据(离线兜底) │ ├── recommend-engine.js # 客户端推荐规则引擎 + 自然语言解析 │ └── utils.js # 通用工具 ├── pages/ │ ├── index/index.vue # 首页(AI 推荐入口 + 榜单) │ ├── recommend/recommend.vue # ★ AI 智能推荐(核心页) │ ├── dish-list/dish-list.vue # 菜品列表 │ ├── dish/dish.vue # 菜品详情 │ ├── review/review.vue # 发布评价 │ ├── user/user.vue # 我的(口味档案 / 推荐历史 / 我的评价) │ ├── login/login.vue # 身份设置(本地身份,无后端登录) │ └── admin/ # 管理后台 │ ├── index.vue # 后台首页 │ ├── dashboard.vue # 数据看板 │ ├── dishes.vue # 菜品管理 │ ├── dish-edit.vue # 菜品编辑 │ ├── reviews.vue # 评价审核 │ ├── ai-config.vue # ★ AI 大模型配置 │ └── users.vue # 评价数据管理 ├── uniCloud-aliyun/ │ ├── cloudfunctions/ │ │ ├── shiyu-recommend/ # ★ AI 推荐云函数 │ │ ├── shiyu-dish/ # 菜品查询云函数 │ │ ├── shiyu-config/ # AI 配置云函数 │ │ ├── shiyu-review/ # 评价云函数 │ │ └── shiyu-seed/ # 菜品库初始化云函数 │ └── database/ │ ├── dishes.schema.json # 菜品表 │ ├── shiyu-config.schema.json # AI 配置表(权限全 false,仅云函数可读写) │ └── reviews.schema.json # 评价表 └── backend/ # 可选:自建 FastAPI 备用链路(默认不启用,详见其 README) ``` --- ## 四、快速开始 ### 方式 A:纯本地运行(无需云空间,立刻可看效果) 1. 用 **HBuilderX 3.7+** 打开本项目目录 2. 运行 → 运行到浏览器 / 微信开发者工具 / 手机模拟器 3. 进入「AI 推荐」页,输入口味与预算,点「✨ 生成推荐」 此时走的是**本地规则引擎**(40 道内置菜品),页面底部会提示"当前使用本地离线数据"。想体验真实大模型,按方式 B 配置。 ### 方式 B:接入大模型(完整链路) 1. **开通 uniCloud 服务空间**:HBuilderX 左侧项目树 → `uniCloud-aliyun` 右键 → 关联云服务空间(阿里云,免费空间足够) 2. **上传云函数**:右键 `uniCloud-aliyun/cloudfunctions` 下每个 `shiyu-*` 目录 → 上传部署 3. **上传数据库 Schema**:右键 `uniCloud-aliyun/database` 下 3 个 `*.schema.json` → 上传 DB Schema (`shiyu-config` 的权限为全 `false`,这是有意为之:Key 只允许云函数读写) 4. **初始化菜品库**:运行项目 → 我的 → (用管理员身份进入)管理后台 → 数据初始化 → 「同步菜品库」 或在云函数控制台直接运行 `shiyu-seed`,参数 `{"action":"reset"}` 5. **配置大模型 Key**:管理后台 → AI 配置 → 选择服务商(DeepSeek / 通义千问 / Kimi / 智谱 / 自定义中转)→ 填入 `api_key` → 保存 → 点「测试连接」 6. 回到 AI 推荐页重新生成,结果卡片会显示 `大模型 deepseek-chat` 标签 > 也可以在 uniCloud 云函数控制台给 `shiyu-recommend` 配置环境变量 `AI_API_KEY` / `AI_BASE_URL` / `AI_MODEL`, > 云函数会优先读取数据库配置,读取不到时回退到环境变量。 ### 管理员入口 管理员身份**不能自行选择**,必须通过管理口令认证: 1. 登录页(`pages/login/login`)选「管理员」,输入**管理口令**(只有口令,没有用户名) 2. 云函数 `shiyu-config` 的 `verify` 动作比对 SHA-256 摘要,通过后把本机口令作为凭证保存 3. 管理后台各页进入时会再向云端验签一次,不通过就退出管理员身份 口令以 SHA-256 哈希保存在云端 `shiyu-config` 集合的 `admin_token` 字段(不存明文)。 首次使用时会用你输入的口令完成初始化(即第一次输入什么,后续就是什么)。 > 忘记口令的恢复办法:uniCloud 控制台 → 云数据库 → `shiyu-config` → 删掉 `admin_token` 字段, > 下次在登录页输入的口令就会成为新的管理口令。 普通同学不需要注册账号:昵称 / 头像 / 口味档案 / 推荐历史都保存在本机,仅用于评价与收藏归属。 ### 我的收藏 - **收藏**:菜单页(食堂菜单)每张菜品卡片**右上角的心形**,点一下即收藏,再点取消 - **查看**:「我的」→「**我的收藏**」(`pages/favorites/favorites`),带收藏数量 - 只显示**在架**的菜;被下架/删除的收藏会自动从列表里消失(对应的 id 顺手清掉,收藏数不会虚高) - 可以逐条取消收藏、也可以一次清空(有二次确认) - 取不到云端时用本机菜谱兜底,**离线也看得到收藏** - **存储**:收藏只是本机存储里的一串菜品 id(`common/favorites.js`,键 `shiyu_favorites`), **不上云、换设备不同步** —— 与「本机 uid + 无服务端会话」的轻量身份方案保持一致。 将来要做多端同步,只要保持 `getFavorites / addFavorite / removeFavorite` 的行为即可平滑迁移。 > 收藏页一次请求取回所有收藏菜品:云函数 `shiyu-dish` 的 `byIds` 动作(保序、只返上架、上限 200), > 而不是逐个调 `detail` 发 N 次请求。 > 回归测试:`node _ui/check-favorites-page.mjs`(含「收藏 → 读取 → 取消 → 清空」端到端) ### 微信一键登录(可选能力) 登录页的「微信一键登录」只做一件事:拿 `openid` 当本机 uid。它**不是认证**,也不会授予管理员权限; 拿不到时页面会告诉你具体原因,并让你手动填写昵称进入(功能完全一样)。 要让它在微信小程序里真正可用,需要同时满足**三个条件**(缺一个都会失败): | # | 条件 | 在哪配 | |---|---|---| | 1 | 小程序有真实 AppID | `manifest.json` → `mp-weixin.appid`(当前是 `wx77b975b02cb7f1aa`) | | 2 | 云函数 `wechat-login` 配了 `WECHAT_APPID` 与 `WECHAT_SECRET` 环境变量 | uniCloud 控制台 → 云函数 → `wechat-login` → 环境变量(AppSecret 在微信公众平台 → 开发管理 → 开发设置) | | 3 | `wechat-login` 云函数已上传部署 | HBuilderX 右键该函数 → 上传部署 | 改完环境变量**必须重新部署**该函数才会生效。 > 排查口诀:`wechat-login` 返回的 `message` 会直接显示在弹窗里。 > 看到「尚未配置」= 第 2 条没做;看到「AppID 无效」= 第 2 条的 AppID 与第 1 条不一致; > 看到「code 无效或已过期」= 正常现象(code 只能用一次),重新点一次即可。 非微信端(H5 / App)不会渲染这个按钮 —— `uni.login` 在那里拿不到微信授权码,渲染出来只会白白报错。 相关实现:`api/index.js` 的 `userApi.wechatLogin()`、`pages/login/login.vue` 的 `wxLogin()`、 云函数 `uniCloud-aliyun/cloudfunctions/wechat-login/index.js`。 回归测试:`node _ui/check-wechat-login.mjs`(会把云函数真跑一遍,覆盖缺配置 / 各类微信错误码 / 网络异常)。 --- ## 五、AI 推荐链路详解 `uniCloud-aliyun/cloudfunctions/shiyu-recommend/index.js` 的完整流程: 1. **参数归一**:接收 `pref`(结构化偏好)与 `text`(一句话描述),服务端再次解析 `text` 并合并,显式传入的 `pref` 优先 2. **候选集裁剪**:只取 `status == 1` 的菜品,按预算与食堂过滤;再在 JS 层**剔除 tags 命中忌口的菜品**(第一道硬过滤) 3. **读取配置**:`shiyu-config` 集合 → 环境变量,两级回退;没有 Key 直接走本地规则引擎 4. **Prompt 构建**:把同学需求 + 候选菜品 JSON 列表拼成中文 Prompt,明确要求"只输出 JSON"、"严禁推荐含忌口食材的菜"、"dish_id 必须来自候选列表" 5. **调用大模型**:OpenAI 兼容协议 `POST {base_url}/chat/completions`,超时 30s 6. **输出严格校验**(关键工程点): - 容忍 ```json 代码块包裹、前后多余文字 - `dish_id` 必须存在于候选集,否则丢弃 - 菜品 tags 不得含忌口词,否则丢弃(防模型幻觉) - 价格不得超过预算,否则丢弃 - 校验后为空 → 降级本地规则 7. **埋点与返回**:命中菜品 `recommend_count` 累加(失败不影响主流程),返回结果并标注 `engine: 'ai' | 'local'` --- ## 六、数据来源标识 接口返回都会带 `engine` 字段,页面据此提示用户: | engine | 含义 | 页面提示 | | --- | --- | --- | | `ai` | 大模型成功生成并通过校验 | 标签「大模型 xxx」,本次结果由大模型生成 | | `cloud` | 云函数规则引擎(未配置 Key 或模型调用失败) | 提示可在管理后台配置 Key | | `local` | 云空间不可用,走前端本地数据 | 当前为本地离线数据 | --- ## 八、菜品配图 图片有两种存法,`DishImage` 组件两种都支持: | 存法 | image 字段长什么样 | 说明 | |---|---|---| | uniCloud 云存储 | `cloud://xxx.yyy/dishes/seed-d001.jpg` | **当前采用**:不占小程序包体,加载走网络 | | 小程序包内静态图 | `static/dishes/d001.jpg` | 早期做法:41 张图共 2.6MB,跟着主包发布 | > **当前状态**:菜品图已迁移到云存储,本机 `static/dishes/` 已删除(`static/` 从 2.72MB 降到 **0.11MB**)。 > 本机图片的**署名信息**保留在 `scripts/dish-image-credits.json`(来源 Wikimedia Commons,含每张图的作者与协议)—— > 图片版权义务不会因为换了存储位置而消失,答辩材料里的来源说明用它。 ### 一键把菜品图转到云存储 微信小程序主包上限 **2MB**,而 `static/dishes` 里的 41 张图就有 **2.6MB** —— 真机预览/上传体验版会报包体积超限。 管理后台 → **菜品管理** → 顶部「**转云存储**」按钮,一次点击自动完成: 1. 读取全部菜品(自动翻页,云函数单页上限 100),挑出 image 还指向 `static/` 的那些 2. 逐张上传到云存储(`getImageInfo` 换临时路径,失败则退回读文件内容上传) 3. 收集 fileID,**一次性批量写回**云端菜品(云函数 `shiyu-dish` 的 `setImages` 动作,只改 `image` 字段) 单张失败不会中断整批,结束后会列出成功/失败数量与失败原因。 **这个按钮不会删除本机原图** —— 删不删由你决定。 > 相关实现:`common/cloud-images.js`(迁移逻辑)、`pages/admin/dishes.vue`(入口)、 > `uniCloud-aliyun/cloudfunctions/shiyu-dish/index.js` 的 `handleSetImages`。 > 回归测试:`node _ui/check-cloud-images.mjs` ### ⚠️ 迁移后不要对云端执行「重置菜品库」 `shiyu-seed` 的种子数据里 image 写的是本机 `static/dishes/*.jpg`,而「**重置菜品库**」会清空后按种子重新写入 —— 等于把云端图片地址**改回已经删掉的本机路径**,所有菜品会退化成 emoji 占位。 「数据初始化」(sync 模式)只补缺失、不覆盖已有记录,**不受影响,可以放心用**。 真发生了也不难救:`git checkout -- static/dishes` 从 git 恢复本机图 → 再点一次「转云存储」即可。 ### 后台上传(单张,新增/编辑菜品时) **管理后台 → 菜品管理 → 编辑(或新增)→ 菜品图片 → 📷 上传图片** - 从手机相册选图或直接拍照,图片上传到 **uniCloud 云存储** - 菜品记录里存的是云存储的 `fileID`(`cloud://` 开头),**不是**临时地址,所以不会过期 - 也可以手填图片地址(`static/dishes/d001.jpg` 这类本地路径,或一个 `https://` 外链) - 点「清除」即可去掉图片,回退成 emoji 占位 ### 一个容易踩的坑:fileID 不能直接塞进 `` `cloud://` 的 fileID 必须先用 `uniCloud.getTempFileURL()` 换成临时地址才能显示 (DCloud 自己的 `uni-id-pages/components/cloud-image` 就是这么做的)。 本项目把这件事收在 **`components/DishImage/DishImage.vue`** 里:它会自动识别 `cloud://` 并换取临时地址,还带一层跨组件缓存(首页榜单同一张图出现三次只会解析一次), 解析失败或图片被删时自动回退成 emoji 方块,不会留破图。 因为首页、菜品列表、菜品详情、菜品管理、数据看板**全部**通过这个组件渲染图片, 所以只要改这一个地方,所有界面都支持上传的图了。 ### 图片缺失时 统一回退为 emoji 渐变方块(优先用菜品自己的 emoji 字段,其次取菜名前两字),不会出现破图。 ### 真机上传失败? 微信的 **`uploadFile` 合法域名**和 **`request` 合法域名**是**两个不同的列表** —— 云函数调用需要前者,上传图片需要后者里的 `uploadFile` 那一项。 真机上传报 `url not in domain list` 时,到微信公众平台 → 开发管理 → 开发设置 → 服务器域名 → **uploadFile 合法域名** 里补上云存储的域名。 > 说明:早期版本这里写过 `scripts/dish-image-map.json`、`scripts/download-dish-images.mjs`、 > `scripts/self-check.mjs` 三个脚本,但它们**从未提交进仓库**,已按实际情况删除说明。 > 现在配图只需要上面的后台上传。 --- ## 九、常见问题 **Q:运行后没有任何菜品数据?** 先执行「管理后台 → 数据初始化 → 同步菜品库」。若未关联云服务空间,前端会自动使用内置的 40 道本地菜品,功能不受影响。 **Q:首页能看到「第一食堂」,点进去却一道菜都没有?** 这是**云端菜品库里残留了旧版本数据**导致的:旧数据没有 `status` 字段,而菜品列表查询带 `where({status: 1})`,于是查不出来;食堂列表不带这个条件,所以仍然显示。 修复方式(任选其一): 1. 管理后台 → 数据初始化 → **同步菜品库**(会自动把缺失 `status` 的记录补为 `status: 1`,并提示修复条数) 2. 或直接执行 **重置菜品库** 全量重建 3. 或到 uniCloud 控制台把 `dishes` 集合清空后重新同步 另外适配层已加保护:当检测到云端菜品库为空或含有本地不认识的食堂(说明数据陈旧)时,会整体退回本地 40 道菜谱,不会再出现"有食堂没菜"的空列表。 **Q:点「生成推荐」提示未配置 API Key?** 到「管理后台 → AI 配置」填写。也可以在 uniCloud 云函数控制台配置 `AI_API_KEY` 环境变量。Key 不会下发到客户端。 **Q:测试连接返回 401 / 404?** 401 = Key 无效或已欠费;404 = `base_url` 写错(注意要带 `/v1` 这类版本路径)或 `model` 名称不存在;超时 = 网络受限或中转地址不可达。 **Q:小程序真机访问不了?** 云函数不需要配置域名白名单;若使用自建后端(`backend/`)则需在微信后台配置 request 合法域名。 **Q:`backend/` 目录是必须的吗?** 不是。那是早期自建 FastAPI 后端的备用实现,默认不参与运行,前端已不再依赖它。保留它是为了说明"同一套前端可以对接不同后端实现"的可扩展性,详见 `backend/README.md`。 --- ## 十、说明 - 菜品图缺失时统一回退为 emoji 占位,避免破图;配图后自动切换为照片。 - 身份信息(昵称 / 头像 / 口味档案 / 推荐历史)保存在本机 Storage,不上传服务器;评价内容会写入云端并需管理员审核。 - 图片抓取脚本仅下载 Wikimedia Commons 上的**自由授权**作品,并生成署名清单,请勿替换为来源不明的图片。