美食打卡小程序开发实录:个人主体也能上线的小程序实战

美食打卡小程序开发实录:个人主体也能上线的小程序实战

📅 2026-08-17
🏷️ uni-app / Express / SQLite / 微信小程序
⏱️ 阅读约 18 分钟

gh_0647d1f31cb4_258

 

一、为什么做这个小程序

事情起因很简单:我想有个地方记录自己吃过的餐厅。

大众点评是给所有人看的,我想要一个只给自己看的版本——我的评分是我的真实感受,我的招牌菜是我真的推荐的,我的心愿单是我想去但还没去的。市面上的 App 都太重了,打开就要定位、要推送、要好友、要社区,我只是想打个卡而已。

作为一个程序员,遇到工具不顺手,当然是自己造一个。本文记录这个小程序从 0 到 1 的完整开发过程,重点是:这是一个个人主体也能上线的小程序——不需要企业资质、不需要类目审核、不需要云开发额度。如果你也持个人账号想做小程序,这篇文章应该能帮到你。

💡 一句话定位:美食打卡小程序 = 我的私人美食地图。记录我去过的每一家餐厅,配合我的评分、招牌菜推荐、人均消费、回访提醒,长期积累成一份属于自己的美食数据资产。

二、功能一览

📝 打卡记录
店名/地址/评分/招牌菜/人均/点评
⭐ 双评分对照
我的评分 vs 大众点评评分
🗺️ 地图选点
选点后自动识别城市区县
💚 心愿单
想去 + 已打卡的”想再去”提醒
📊 美食足迹
总数/平均分/消费/城市/标签云
🔄 三状态管理
已打卡 / 想去 / 不再去
🔍 多维筛选
关键词、城市、状态、标签、排序
👤 多用户
每个账号独立数据空间

特别说一下“想再去”功能——这是用了一段时间后自己加上去的。发现有些店去过一次不够,过段时间想去第二次,但没有提醒就会忘。于是给已打卡的店加了一个独立的”想再去”标记,可以设一个回访计划日期,到期前会在心愿单顶部红色提醒。这个标记和”已打卡”状态是独立的,不会互相覆盖。

三、技术架构总览

选型 说明
前端 uni-app (Vue3) 一套代码编译微信小程序 / H5
后端 Node.js + Express 极简,单文件 460 行
数据库 SQLite (better-sqlite3) 零配置,文件即数据库
鉴权 JWT + 微信 code2session 账号密码 + 微信登录双模式
地图 wx.chooseLocation + 腾讯地图 个人主体可用,无需类目
部署 VPS + Nginx + pm2 甲骨文永久免费机即可

选这套栈的理由很简单:

  • uni-app:写过 Vue 的人零成本上手,编译微信小程序没问题
  • Express + SQLite:不需要任何云数据库,一个文件搞定,迁移备份只要拷贝文件
  • 无云开发依赖:所有逻辑都在自己服务器上,不被任何厂商锁定

这套架构同样适用于任何轻量级小程序——笔记、记账、健身、读书、宠物记录……只要数据形态是”用户 + 用户私有数据”,都能直接套用。

四、后端设计:单文件 Express 的优雅

4.1 数据模型

两张表搞定:

users       用户表:username / password / display_name / openid
user_data   数据表:username 主键 + data 字段(JSON 字符串)

关键设计在 user_data 表——每个用户一行,所有业务数据存成一个 JSON 字符串。这样做的好处:

  • 不需要为每个业务实体建表(餐厅、心愿单、统计快照全都塞 JSON)
  • 改字段不用改数据库结构,前端要什么就往 JSON 里加什么
  • 数据库迁移只要拷贝一个 .db 文件

4.2 统一入口:POST /api/mp + action 分发

所有业务接口都走同一个 URL:POST /api/mp,请求体里带一个 action 字段区分业务。这是一个我从党建小程序延续过来的极简套路:

app.post('/api/mp', (req, res) => {
  const { action } = req.body;
  // JWT 鉴权
  const user = jwt.verify(token, JWT_SECRET);
  // 按 action 分发到对应 handler
  const handler = mpHandlers[action];
  const result = handler(user.username, body);
  res.json({ code: 0, data: result });
});

所有业务逻辑挂在 mpHandlers 对象上,按命名空间组织:

const mpHandlers = {
  'stats:summary'    (username) { /* 统计工作台 */ },
  'restaurant:list'  (username, event) { /* 店铺列表 */ },
  'restaurant:create'(username, event) { /* 新增店铺 */ },
  'restaurant:update'(username, event) { /* 更新店铺 */ },
  'restaurant:delete'(username, event) { /* 删除店铺 */ },
  'restaurant:status' (username, event) { /* 切换状态 */ },
  'restaurant:revisit'(username, event) { /* 切换想再去 */ },
  'location:reverse'  (username, event) { /* 逆地址解析 */ },
  // ...
};

这种模式的优势:

  • 前后端契约清晰:所有 action 列表就是全部 API 文档
  • 鉴权集中:所有业务接口自动走 JWT 校验
  • 支持异步:handler 返回 Promise 时自动等待(如调用外部地图 API)
  • 错误统一:handler 里 return { error: '...' } 自动转成 400 响应

4.3 微信登录

微信登录走标准的 code2session 流程,前后端配合:

// 前端
uni.login({
  provider: 'weixin',
  success: (res) => {
    request('/api/wx-login', { code: res.code });
  }
});

// 后端
app.post('/api/wx-login', async (req, res) => {
  const { code } = req.body;
  // 用 code 换 openid
  const r = await fetch(`https://api.weixin.qq.com/sns/jscode2session?appid=${appid}&secret=${secret}&js_code=${code}`);
  const { openid } = await r.json();
  // 根据 openid 查用户
  const user = db.prepare('SELECT * FROM users WHERE openid = ?').get(openid);
  if (!user) return res.json({ code: 0, data: { needBind: true, openid } });
  // 已绑定,签发 JWT
  const token = jwt.sign({ username: user.username }, JWT_SECRET, { expiresIn: '30d' });
  res.json({ code: 0, data: { token, user } });
});
⚠️ 注意:微信登录返回 needBind: true 时,说明这个微信号还没绑定账号——这时引导用户去注册或登录页,把 openid 一起带上,登录成功后自动绑定。

五、前端设计:uni-app 六页搞定

前端只有六个页面:

页面 路径 功能
登录页 pages/login 账号登录 + 微信一键登录
打卡列表 pages/index 主页,餐厅卡片列表 + 筛选
店铺详情 pages/detail 评分快速调整 + 地图导航跳转
新增/编辑 pages/edit 表单 + 地图选点
心愿单 pages/want 想去 + 想再去的回访提醒
美食足迹 pages/stats 统计仪表盘

5.1 请求层封装

前端所有 API 调用统一走 utils/request.js,屏蔽掉 uni.request 的细节:

// 业务接口示例
const restaurants = await request.getRestaurants({
  keyword: '火锅',
  city: '上海',
  status: 'visited',
  sortBy: 'rating'
});

// 底层就是统一的 callCloud
function callCloud(action, data = {}) {
  return new Promise((resolve, reject) => {
    uni.request({
      url: API_BASE + '/mp',
      method: 'POST',
      data: Object.assign({ action }, data),
      header: { Authorization: 'Bearer ' + token },
      success: (res) => {
        if (res.data.code === 0) resolve(res.data.data);
        else reject(new Error(res.data.error));
      }
    });
  });
}

5.2 页面配色

用了一个暖色调主题(橙色 #FF6B35),符合美食场景的食欲感。tabBar 选中色、导航栏背景、按钮主色都统一用这一个色值,视觉一致性很容易做到。

六、地图选点:个人主体的避坑之路

这是整个项目最有挑战的部分,写在这里给同样持个人账号的开发者避雷。

6.1 坑一:getLocation 不能用

❌ 问题:原本想用 wx.getLocation 获取用户位置,结果发现这个 API 需要类目审核,个人主体的小程序根本通不过审核。
✅ 解决:改用 wx.chooseLocation(地图选点)。这个 API 是让用户主动在地图上选一个点,属于”用户主动授权”的范畴,不需要类目审核,个人主体就能用。返回结果里直接带店名、地址、经纬度,反而比 getLocation 更好用。
uni.chooseLocation({
  success: (res) => {
    // res 直接带 name/address/latitude/longitude
    // 完全是新增打卡需要的字段
  }
});

6.2 坑二:requiredPrivateInfos 配置报错

配置 requiredPrivateInfos 时一开始填了 openLocation,结果上传时报”非法值”。查文档才发现这个字段只在特定 API 列表里合法:getLocationchooseLocationchooseAddress 等少数几个,openLocation 不在内。

// manifest.json
"requiredPrivateInfos": ["chooseLocation"]  // 只保留这一个

6.3 坑三:逆地址解析 403

地图选点返回的是经纬度,但我想自动填充城市和区县,需要做逆地址解析。前端调用腾讯地图 API 直接 403:

❌ 原因:腾讯地图 WebServiceAPI 有两层限制——一是必须在控制台手动启用 WebServiceAPI 权限,二是 Key 必须配置Referer 白名单
✅ 解决:把逆地址解析挪到后端做,请求时带上 Referer: https://你的域名/ 头。前端只把经纬度传给后端,后端代理调腾讯地图 API 返回城市/区县/详细地址。顺便绕开了小程序对第三方域名的种种限制。
// 后端 server.js 的 location:reverse handler
'location:reverse'(username, event) {
  const { latitude, longitude } = event;
  const key = process.env.TENCENT_MAP_KEY;
  return fetch(`https://apis.map.qq.com/ws/geocoder/v1/?location=${latitude},${longitude}&key=${key}`, {
    headers: { Referer: 'https://your-domain.com/' }
  })
    .then(r => r.json())
    .then(json => ({
      city: json.result.address_component.city,
      district: json.result.address_component.district
    }));
}

6.4 兜底方案:前端正则猜城市

考虑到不是所有人都会去申请腾讯地图 Key,前端还做了一个兜底——如果后端没配置 Key 导致逆地址解析失败,就从地图选点返回的地址字符串里正则提取:

function guessCityFromAddress(address) {
  const m = address.match(/([^\s省市]{2,8}?[省市区]市|[^\s]{2,6}?自治区)/);
  return m ? m[1] : '';
}

这样即使完全没配地图 Key,小程序也能正常打卡,只是城市要手动确认一下而已。

七、踩过的五个坑及解决方案

症状 解决方案
getLocation 不可用 上传被拒,提示需类目 改用 wx.chooseLocation
requiredPrivateInfos 非法值 上传报错”非法值” 只保留 ["chooseLocation"]
腾讯地图 403 前端调用返回 403/199 启用 WebServiceAPI + 加 Referer 白名单 + 后端代理
编译报 summer-compiler miss js file 开发者工具一编译就崩 固定 libVersion: "3.16.2" + es6: false
pm2 restart 后 env 不生效 改了环境变量重启服务还是旧值 pm2 delete && pm2 start ecosystem.config.js

第五个坑尤其值得展开——pm2 的 restart 命令不会重新加载环境变量,它只是重启进程并继承之前的 env。改了 ecosystem.config.js 之后必须用 pm2 delete <name> && pm2 start ecosystem.config.js,否则你以为生效了其实没生效,调试半天不知道为啥行为不对。

八、部署到生产

8.1 Nginx 反代配置

假设后端跑在 3200 端口,用 Nginx 反代到 /food/api/ 路径下:

location /food/api/ {
    proxy_pass http://127.0.0.1:3200/api/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

8.2 pm2 ecosystem 配置

// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'food-server',
    script: './server.js',
    env: {
      PORT: 3200,
      JWT_SECRET: 'your-random-secret-32-chars',
      WX_APPID: 'your-appid',
      WX_SECRET: 'your-secret',
      TENCENT_MAP_KEY: 'your-map-key'
    }
  }]
};

8.3 一键部署脚本

# 本地推送后端到服务器
rsync -avz --exclude 'node_modules' --exclude 'food.db*' \
  web-server/ oracle:~/food-server-upload/

# 服务器同步并重启
ssh oracle "sudo rsync -a ~/food-server-upload/ /opt/food-server/ && \
  sudo chown -R www-data:www-data /opt/food-server && \
  sudo pm2 restart food-server"

8.4 微信公众平台配置

  1. 登录 微信公众平台
  2. 开发 → 开发管理 → 服务器域名 → request 合法域名添加你的后端域名
  3. 本地调试时在开发者工具「详情 → 本地设置」勾选「不校验合法域名」

九、写在最后

做完这个小程序,最大的感受是:个人开发者做小程序没那么难

很多人被”个人主体限制”劝退,觉得没有企业资质什么都做不了。实际上只要你愿意绕一下路(比如用 chooseLocation 替代 getLocation),大部分功能都能实现。微信对个人主体的限制主要在支付、直播、客服消息这类强商业化场景,普通的工具型、记录型小程序完全没问题。

技术上,这套”uni-app + Express + SQLite + 单文件后端“的组合,几乎可以复用到任何轻量级小程序。我之前做过党建小程序用的也是同一套架构,这次只是换了业务逻辑——前后端骨架基本没动,主要工作量在前端页面。

把这个套路沉淀下来的价值在于:以后再有”给自己做个小工具”的想法,从 0 到上线可能就一个周末的事。

🎯 本文总结:

  • 个人主体小程序完全可以做,避开几个 API 限制即可
  • uni-app + Express + SQLite 是轻量级小程序的黄金组合
  • “POST /api/mp + action 分发”的后端模式值得复用
  • 地图选点用 chooseLocation,逆地址解析放后端做

如果你也在做类似的小程序,或者对这套架构有疑问,欢迎在评论区交流。希望这篇文章对你有帮助 🙌


📌 相关阅读:
智慧党建小程序源码解析:从云开发迁移到自建服务器的完整实现

© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容