简介:一套微信小程序在线点餐商城源码,面向新手开发者,适合用于学习小程序电商项目的完整搭建流程,覆盖菜品展示、购物车、订单确认等核心模块。压缩包共60个文件,总体积仅1.18MB,以WXML页面结构、WXSS样式、JavaScript逻辑和JSON配置为主,另含26张界面图片和1份说明文档,工程组织清晰,便于按页面和功能对照阅读。目前已有1148人浏览学习,适合作为入门练手项目。这份源码虽小但结构完整,通过阅读页面与逻辑代码,可掌握WXML/WXSS语法、小程序API调用以及前端数据交互的基本思路;结合界面图片预览,能直观理解在线点餐系统的运作方式,也为后续对接后端接口、微信支付等真实业务扩展打下基础。
1. 微信小程序商城源码:拿到手先别急着跑,先把这套点餐系统的底细摸清
“微信小程序商城源码”听起来像是一个完整的电商系统,但实际解压之后你会发现,它就是前端工程、配套的 imgs 资源和一个 README,后端 Java 接口得自己对应起来调试。我拆过的这套在线点餐源码,前端是标准的 WXML、WXSS、JavaScript 结构,启动页、菜品列表、购物车、订单确认这些页面都在 pages 目录里躺着,后端则用 Java 提供登录、菜品、下单这一类 RESTful 接口。它能解决的事情很明确:顾客在微信里打开小程序,浏览菜品、加购物车、提交订单,后端把订单落库并推进状态流转。适合两类人——刚学微信小程序想找一个完整工程做参照的新手,以及需要快速搭一套点餐业务、没空从零写前后端的开发者。
2. 拆解工程结构:app.js、pages、utils 各管什么,小程序端骨架怎么长
拿到 weapp-store-master 这份工程,第一件事不是看 README,而是先把顶层文件过一遍。app.js 管全局生命周期,app.json 管页面注册和窗口配置,app.wxss 是全局样式,utils 里一般放着请求封装和工具函数,pages 目录下面才是真正能看到的页面。很多人一上来就翻 app.js,其实应该先开 app.json,因为它是整个小程序的“注册表”,所有页面必须在这里登记,漏了页面编译直接报错。
2.1 app.json 全局配置:页面注册、导航栏与窗口样式
打开 app.json,你会看到类似下面这样的结构:
{ "pages": [ "pages/index/index", "pages/goods/goods", "pages/cart/cart", "pages/order/order", "pages/user/user" ], "window": { "navigationBarBackgroundColor": "#ff6633", "navigationBarTextStyle": "white", "navigationBarTitleText": "在线点餐" }, "style": "v2", "sitemapLocation": "sitemap.json" }pages 数组里第一项对应的是小程序启动后加载的第一个页面,点餐场景里一般把店铺首页或菜品分类页放第一位。window 字段控制导航栏样式,navigationBarTitleText 会直接显示在顶部,如果你想改成自己店铺名字,改这一行就行。style 设置为 v2 表示使用新版基础组件样式,老项目如果发现组件渲染怪怪的,可以先检查这里。
这套配置里常见遗漏是networkTimeout,我没见过太多人主动配,但点餐场景网络波动大,建议补一段:
"networkTimeout": { "request": 10000, "connectSocket": 10000, "uploadFile": 10000 }request 超时设 10 秒,用户下单时如果后端慢,前端至少能快速失败并提示,而不是傻等。这个字段在源码里未必有,我一般拿到手会先在 app.json 里加上,省得后面线上出问题再回来补。
2.2 pages 目录:点餐流程的页面栈与跳转关系
pages 目录下的页面是这套系统的核心资产。按照点餐业务,页面之间的关系大致如下:
| 页面路径 | 承担功能 | 典型交互 |
|---|---|---|
| pages/index/index | 店铺首页/分类入口 | 展示分类、推荐菜品 |
| pages/goods/goods | 菜品列表 | 按分类加载菜品、加入购物车 |
| pages/cart/cart | 购物车 | 修改数量、清空、结算 |
| pages/order/order | 订单确认 | 填写备注、提交订单 |
| pages/user/user | 我的订单 | 查看历史订单、支付入口 |
页面之间跳转的选型值得注意。index 到 goods 通常用wx.navigateTo,因为用户看了一圈不想要,还得能返回首页继续逛。goods 到 cart 也走navigateTo,但如果 cart 被设计成了底部 tabBar 页面,就必须用wx.switchTab,这两种 API 的页面栈行为完全不同,混用容易把层级搞乱,这个坑后面单独讲。
2.3 app.js 与 utils:全局数据、登录态与请求封装
app.json 管注册,app.js 管全局生命周期。点餐场景里 app.js 常做的事包括三件:启动时检查登录态、把用户信息和购物车数量放到 globalData、在需要登录时调用 wx.login。
App({ globalData: { userInfo: null, openid: '', cartCount: 0 }, onLaunch() { const token = wx.getStorageSync('token') if (token) { this.globalData.cartCount = wx.getStorageSync('cartCount') || 0 } else { this.login() } }, login() { wx.login({ success: (res) => { // res.code 由后端换取 openid 并返回业务 token wx.request({ url: 'https://api.example.com/api/auth/login', method: 'POST', data: { code: res.code }, success: (r) => { wx.setStorageSync('token', r.data.data.token) } }) } }) } })这段逻辑里最关键的是存储键名约定:token 统一用wx.setStorageSync('token')写入,下面所有请求都从同一个键取值。我不建议把用户 openid 直接存全局变量,因为小程序冷启动时 globalData 会被重置,而同步缓存不会,存缓存是更稳的做法。onLaunch 里先读 token,如果存在就直接用,不存在才走 wx.login,这样能避免每次打开小程序都让微信弹授权。
请求封装一般放在 utils 目录下,常见做法是封装成一个 Promise 方法,页面里用 async/await 写起来像同步逻辑:
const BASE_URL = 'https://api.example.com' function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method, data, header: { 'Content-Type': 'application/json', 'Authorization': wx.getStorageSync('token') || '' }, success: (res) => { if (res.statusCode === 200 && res.data.code === 0) { resolve(res.data.data) } else { reject(res) } }, fail: reject }) }) } module.exports = { request }BASE_URL 是唯一需要改的地方,拿到源码后第一件事就是把它换成你自己的 HTTPS 域名。header 里带 Authorization 是后端鉴权的通用做法,后端从 token 里解析 openid,而不是每个接口都传 openid 参数。res.data.code 判断依赖后端统一返回格式,如果后端返回的 code 不是 0,请求会被 reject,调用方就能在 catch 里统一弹错误提示,不用每个页面重复写。
提示:如果源码里没有 utils/request.js,不用怀疑工程完整性,自己按这个结构补一个,属于点餐小程序的标配。
3. 点餐核心链路:菜品列表、购物车、订单提交的 WXML 与 JS 联动
有个印象要纠正:小程序页面不是 HTML,WXML 也不是模板字符串,它是一套与 JS 逻辑层绑定的声明式渲染方案。你 setData 什么,页面就渲染什么,数据驱动视图,所以点餐链路的核心不是写页面,而是管理好数据。菜品列表、购物车、订单提交这条链路,每一步都是数据在不同层的流动。
3.1 菜品列表:WXML 渲染 wx:for,JS 里 wx.request 拉数据
菜品列表页是用户进入点餐系统的第一屏,源码里通常长这样:
<view class="goods-list"> <view class="goods-item" wx:for="{{goodsList}}" wx:key="id" bindtap="onTapGoods">Page({ data: { goodsList: [], categoryId: 0 }, onLoad(options) { this.setData({ categoryId: options.categoryId || 0 }) this.fetchGoods() }, async fetchGoods() { const list = await request(`/api/goods/list?categoryId=${this.data.categoryId}`) this.setData({ goodsList: list }) }, onTapGoods(e) { const id = e.currentTarget.dataset.id wx.navigateTo({ url: `/pages/detail/detail?id=${id}` }) }, addToCart(e) { const id = e.currentTarget.dataset.id // 这里只跳转加购逻辑,具体见 3.2 } })onLoad 里options参数是页面跳转时 URL 后面的查询参数,比如从首页带过来categoryId=3,这里就能读到。async/await 能工作的前提是 request 方法返回 Promise,如果源码里用的是回调风格,我会建议顺手改成 Promise,后面维护省心很多。事件对象里取参数的姿势容易踩坑:e.currentTarget.dataset.id拿的是当前监听事件的元素上的>const CART_KEY = 'cart' Page({ data: { cart: [], totalCount: 0, totalPrice: '0.00' }, onLoad() { const cart = wx.getStorageSync(CART_KEY) || [] this.setData({ cart }) this.calc() }, addToCart(e) { const goods = e.currentTarget.dataset.goods const cart = this.data.cart.slice() const idx = cart.findIndex(i => i.id === goods.id) if (idx > -1) { cart[idx].count += 1 } else { cart.push({ ...goods, count: 1 }) } wx.setStorageSync(CART_KEY, cart) this.setData({ cart }) this.calc() }, calc() { let totalCount = 0 let totalPrice = 0 this.data.cart.forEach(i => { totalCount += i.count totalPrice += i.count * Number(i.price) }) this.setData({ totalCount, totalPrice: totalPrice.toFixed(2) }) } })
三个值得琢磨的细节。第一,wx.setStorageSync(CART_KEY, cart)放在 setData 之前,因为 setData 只更新内存中的 data,页面切走再回来还是会走 onLoad 读缓存,先存缓存能保证数据不丢。第二,findIndex判断菜品是否已在购物车中,已存在则 count 加 1,不存在则拼一个新的对象进去,注意这里不能直接 push 原 goods 对象,否则后续修改 count 会污染列表页的数据源。第三,Number(i.price)是必要的防御,后端接口返回的金额可能是字符串,直接做乘法会得到字符串拼接结果,比如 8.6 乘 3 变成 “8.666”,这个坑相当隐蔽。
3.3 提交订单:请求参数拼接与支付流程的占位实现
购物车结算到订单提交,是最能看出一套源码质量的地方。常见做法是前端把购物车条目映射成后端需要的结构,后端下单成功后再跳转支付。
async submitOrder() { const cart = wx.getStorageSync(CART_KEY) || [] if (!cart.length) { wx.showToast({ title: '购物车为空', icon: 'none' }) return } const payload = { items: cart.map(i => ({ goodsId: i.id, count: i.count })), remark: this.data.remark } try { const order = await request('/api/order/create', 'POST', payload) wx.setStorageSync(CART_KEY, []) this.setData({ cart: [], totalCount: 0, totalPrice: '0.00' }) wx.navigateTo({ url: `/pages/pay/pay?orderId=${order.id}&amount=${order.amount}` }) } catch (err) { wx.showToast({ title: '下单失败', icon: 'none' }) } }payload 里不要传单价和总价,后端必须自己根据菜品表算金额。如果前端把价格一起提交,用户改一下请求参数就能以任意价格下单,这是点餐系统最致命的漏洞。下单成功后才清空购物车缓存,避免后端没收到订单、前端购物车却空了的情况。跳转支付页时,orderId 和 amount 都来自后端返回,前端不自己拼金额,这一点从源头堵住篡改空间。
支付这块,源码如果没有接入微信支付,属于正常现象,很多教学工程都把支付留作占位。真正要落地时,后端需要统一下单、签名、回调验签,前端调用wx.requestPayment并把后端返回的 timeStamp、nonceStr、package、signType、paySign 五个参数原样传进去,少一个都不行。
4. Java 后端接口对接:登录态、菜品接口与订单处理的 RESTful 设计
前端页面再完整,后端接口没对上,这份源码就跑不起来。Java 后端的重点不在用了什么框架,而在接口约定是否清晰。下面按一套完整的点餐后端来拆。
4.1 统一返回体:code / message / data 三段式约定
前端 request 封装里判断过res.data.code === 0,后端就必须配合这个约定。Java 里最常见的写法是定义一个泛型 Result 类:
public class Result<T> { private int code; private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.code = 0; r.message = "ok"; r.data = data; return r; } public static <T> Result<T> error(int code, String message) { Result<T> r = new Result<>(); r.code = code; r.message = message; return r; } }code 字段是业务状态码,不要跟 HTTP 状态码混在一起。HTTP 200 + code 10001 表示“请求到达了但业务失败”,HTTP 500 + code 0 表示服务端异常,前端统一判断 code 就能区分。我看到过有人把 HTTP 500 直接映射进 code,前端 res.data 为 null 导致 NPE,这种翻车完全可以通过约定避免。message 是给用户看的提示文案,后端不要返回 “Internal Server Error” 这种英文,直接返回“购物车不能为空”更省事。
4.2 登录与 token:wx.login 换 openid,再换业务 token
点餐系统的登录流程与账号密码无关,核心是微信的静默登录:
@RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private AuthService authService; @PostMapping("/login") public Result<String> login(@RequestBody LoginReq req) { String openid = authService.code2openid(req.getCode()); String token = authService.createToken(openid); return Result.ok(token); } }前端 wx.login 拿到的 code 是一次性的,有效期只有几分钟,而且只能用一次。code2openid 内部实际是调微信的 jscode2session 接口,用 code 换 openid 和 session_key。openid 是用户在小程序维度下的唯一标识,后端用它关联购物车、订单,不要把用户的昵称头像当作身份依据。token 建议用 JWT,payload 里放 openid 和过期时间,有效期设 2 小时就够,点餐不是长期在线的业务,token 太长反而增加被盗风险。
这里有一个容易被忽略的问题:如果同时接入了多个小程序,同一个用户在不同小程序下 openid 不同,数据库表设计时要把 appid 和 openid 一起作为唯一索引,否则登录态会串。有些工程没考虑这点,切到另一个小程序就查出别人的订单,排查起来相当头疼。
4.3 数据表设计:菜品、订单、订单明细的关键字段
后端接口背后是数据库表,点餐系统核心四张表就够了:
| 表名 | 关键字段 | 说明 |
|---|---|---|
| category | id, name, sort | 菜品分类,sort 控制排序 |
| goods | id, category_id, name, price, image, status | price 用 decimal(10,2),status 0下架1上架 |
| orders | id, order_no, openid, total_amount, status, create_time | status 0待支付1已支付2制作中3已完成4已取消 |
| order_item | id, order_id, goods_id, goods_name, price, count | 冗余 goods_name 和 price 做订单快照 |
订单和明细拆两张表的原因是订单项是“当时购买时的快照”,菜品改价或下架都不能影响历史订单。order_item 里冗余 goods_name 和 price,就是看订单详情时不再去 join 菜品表,直接读明细即可。orders 表的 order_no 建议用yyyyMMddHHmmss + 随机数生成,不要用数据库自增 id 直接暴露给用户,不然竞争对手通过订单号就能推算你的日单量。
total_amount 字段类型用 decimal(10,2) 而不用 float/double,这是金额精度问题,后端如果用了 double 做金额运算,往下传的时候各种精度奇怪问题就出来了。最稳的做法是数据库层 decimal,Java 层用 BigDecimal,接口返回字符串。
4.4 下单接口:Controller、入参校验与状态流转
订单创建是点餐系统最核心的后端接口,前面提到的金额计算必须在这里完成:
@PostMapping("/api/order/create") public Result<OrderVO> create(@RequestBody OrderCreateReq req, @RequestHeader("Authorization") String token) { // 1. 从 token 解析 openid String openid = authService.parseToken(token); // 2. 校验购物车条目 if (req.getItems() == null || req.getItems().isEmpty()) { return Result.error(10001, "购物车不能为空"); } // 3. 后端计算金额并落库 OrderVO order = orderService.create(openid, req); return Result.ok(order); }@RequestHeader 从请求头里拿 token,前端 request 封装里已经注入了 Authorization 头,这里两端是对应的。所有参数不要从 request body 里取 openid,因为前端传来的 openid 是可以伪造的,必须以 token 解析结果为准。OrderCreateReq 里建议加上校验注解,比如@NotNull和@Min(1),这样空 items 和负数数量在进入 Service 之前就被拦截下来。
Service 层 create 方法内部要做三件事:遍历 items 查出菜品真实价格、累加 totalAmount、事务性写入 orders 和 order_item 两张表。事务必须加上,否则订单主表写了、明细没写,对账时少一条,排查到怀疑人生。支付回调那里,后端要在回调里验签,然后用 out_trade_no 反查本地订单核对金额,金额不一致不能改订单状态。
5. 避坑指南:从源码跑通到能上线,这五个坎绕不过去
这套源码拿到手到真的能跑上线,中间隔着好几个坑。有些坑不大,但第一次遇到会卡半天,下面五条都是我拆类似工程时反复见过的。
5.1 开发工具能跑、真机连不上:合法域名校验是第一道门槛
现象:微信开发者工具里勾了“不校验合法域名”后,接口数据一切正常,页面也能渲染。一传到手机预览,所有 wx.request 全部失败,控制台报request:fail url not in domain list。
原因:开发者工具默认帮你放开域名限制,真机小程序却强制校验 request 合法域名,二者行为不一致,这是最典型的“工具能跑真机白屏”。源码里接口地址写的是测试域名或 IP,没配置过小程序后台,真机自然拒绝请求。
解决:开发阶段,在开发者工具右上角“详情-本地设置-勾选不校验合法域名”。要上体验版和正式版,就必须在小程序管理后台“开发管理-开发设置-服务器域名”里把 HTTPS 接口域名加入 request 合法域名列表,域名必须备案且支持 HTTPS。我一般会先配好域名再联调,免得每次在真机和工具之间来回切。
5.2 页面栈超过 10 层:点几轮回退直接白屏
现象:用户在菜品列表、详情、购物车、订单之间反复跳转,操作到第五六个页面时突然白屏,控制台报navigateTo:fail webview count limit exceed。
原因:wx.navigateTo每次跳转都会新开一个页面实例压入页面栈,微信限制页面栈最多 10 层,超过就拒绝跳转。点餐流程里用户习惯来回逛,最容易触顶。
解决:控制页面栈深度。详情页返回列表用wx.navigateBack,不要再用navigateTo重新进入列表。tab 页面之间的切换用wx.switchTab,它不会增加栈深度。从订单支付完成页回首页,用wx.reLaunch直接清空栈重新加载首页。我一般会把点餐链路设计成“首页-列表-详情-购物车-订单”不超过五层,尽量把层级压扁。
5.3 浮点数算金额:前端显示正常,后端对账对不上
现象:单价 8.6 元、购买 3 份,前端算出来的总价是 25.799999,传给后端后库里存的是 25.79,日结对账差了几分钱,怎么都对不上。
原因:JS 的 Number 是 IEEE 754 双精度浮点数,0.1 + 0.2 这种运算天然有精度误差。Java 后端如果用 float 或 double 接收金额,同样会有精度丢失。金额这种以“分”为单位更精确的数据用浮点数本来就是错误选型。
解决:前端计算的金额只用于展示,接口传参不带。后端统一用 int 类型存储“分”或 decimal(10,2) 存储元,Java 层用 BigDecimal 运算。最简单的做法是后端接口收到购物车明细后,自己从数据库菜品表重新计算总价,前端传的 totalAmount 直接忽略。
5.4 登录态串号:用户A看到用户B的购物车
现象:测试手机A登录后,换手机B打开小程序,购物车里居然是A加过的菜,订单列表里也能看到A的订单。
原因:购物车和订单查询用了全局共享变量或固定的缓存键,没有按 openid 隔离。常见写法是把购物车放到 globalData 里,冷启动时又从缓存读,但缓存键没有区分用户,第二个用户登录后读到的是上一个用户留下的数据。
解决:购物车缓存键加上 openid 后缀,比如cart_${openid},订单查询接口必须从 token 解析 openid,然后WHERE openid = ?查询。登录成功时要先清掉旧用户的数据缓存,再写新用户的数据。我从那以后每次处理用户态都会强制检查一遍“这个键是不是用户维度唯一的”。
5.5 图片路径用相对路径:开发正常,上线后菜品图全挂
现象:本地调试时菜品图都正常,上传代码后图片全部裂开,开发者工具里也不报错。
原因:源码里图片写的是images/xxx.png这种相对路径,开发工具会按工程目录解析,真机和线上版本只认网络图片路径,本地相对路径在线上找不到文件。还有一种情况是图片不存在于工程目录,而是放在服务器上,代码却用了相对路径。
解决:菜品图统一走后端返回完整 URL,或者在前端拼接固定的 CDN 前缀。比如后端只返回goods1.png,前端请求时拼成https://cdn.example.com/goods/goods1.png。不要在代码里写死http://localhost或内网 IP,上线前全局搜一遍 BASE_URL 和图片路径,把调试地址替换干净。
6. 二开演练:把堂食点餐改成“外卖+自提”模式,需要动哪四个地方
源码头像里只有堂食点餐,想加自提或外卖场景,改动量其实不大。这个二开方向也最能检验你是否真的理解了这套工程。
6.1 数据层:orders 表加三个字段
orders 表新增 delivery_type 字段,tinyint 类型,0 表示堂食、1 表示外卖、2 表示自提;再加 delivery_address 和 delivery_time 两个 varchar 字段。加字段时注意 order_item 表不用动,但旧订单查询接口要兼容 delivery_type 为空的情况,Java 代码里用 getDeliveryType 判空后默认 0。
6.2 接口层:下单入参增加配送信息
OrderCreateReq 里新增private Integer deliveryType;和private String deliveryAddress;。Service 层做类型判断:deliveryType 为 1 时,deliveryAddress 必填;deliveryType 为 0 或 2 时地址可以忽略。这个逻辑必须放在后端,不能依赖前端来校验。
6.3 前端:订单确认页加选择器
订单确认页 WXML 加一个 picker 组件,选项是“堂食/外卖/自提”,外卖时展开显示地址输入框,自提时显示预计取餐时间。提交订单前判断 deliveryType 为 1 且 address 为空,弹 toast 拦截,带着错误信息联调时能少一半沟通成本。
6.4 验证:体验版走三组完整流程
把工程上传为体验版,在真机上分别测堂食、外卖、自提三组下单,用 vConsole 查看请求参数,确认 deliveryType 和 deliveryAddress 正确传到后端,再到管理后台查订单列表,确认字段已落库。
我每次做这类加场景的改造,都强制走一遍“表结构改字段、接口加入参、前端加选择器、真机三组验证”这个顺序,中间绝不停在“代码能编译就说明没问题”的错觉上。线上跑过半年的点餐系统告诉我,订单渠道字段漏了,后续对账和运营分析全都得补数据。这份源码虽然是从一个点餐 Demo 出发,但顺着这个思路改下去,它就能长成一套能接外卖平台、能出日结报表的完整业务系统。希望这条二开路径能帮你在自己的项目里少走几步弯路。
本文还有配套的精品资源,点击获取