简介一套微信小程序在线点餐商城源码面向新手开发者适合用于学习小程序电商项目的完整搭建流程覆盖菜品展示、购物车、订单确认等核心模块。压缩包共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 联动有个印象要纠正小程序页面不是 HTMLWXML 也不是模板字符串它是一套与 JS 逻辑层绑定的声明式渲染方案。你 setData 什么页面就渲染什么数据驱动视图所以点餐链路的核心不是写页面而是管理好数据。菜品列表、购物车、订单提交这条链路每一步都是数据在不同层的流动。3.1 菜品列表WXML 渲染 wx:forJS 里 wx.request 拉数据菜品列表页是用户进入点餐系统的第一屏源码里通常长这样view classgoods-list view classgoods-item wx:for{{goodsList}} wx:keyid bindtaponTapGoods>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 后面的查询参数比如从首页带过来categoryId3这里就能读到。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 ResultT { private int code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 0; r.message ok; r.data data; return r; } public static T ResultT error(int code, String message) { ResultT 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 登录与 tokenwx.login 换 openid再换业务 token点餐系统的登录流程与账号密码无关核心是微信的静默登录RestController RequestMapping(/api/auth) public class AuthController { Autowired private AuthService authService; PostMapping(/login) public ResultString 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 建议用 JWTpayload 里放 openid 和过期时间有效期设 2 小时就够点餐不是长期在线的业务token 太长反而增加被盗风险。这里有一个容易被忽略的问题如果同时接入了多个小程序同一个用户在不同小程序下 openid 不同数据库表设计时要把 appid 和 openid 一起作为唯一索引否则登录态会串。有些工程没考虑这点切到另一个小程序就查出别人的订单排查起来相当头疼。4.3 数据表设计菜品、订单、订单明细的关键字段后端接口背后是数据库表点餐系统核心四张表就够了表名关键字段说明categoryid, name, sort菜品分类sort 控制排序goodsid, category_id, name, price, image, statusprice 用 decimal(10,2)status 0下架1上架ordersid, order_no, openid, total_amount, status, create_timestatus 0待支付1已支付2制作中3已完成4已取消order_itemid, 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 做金额运算往下传的时候各种精度奇怪问题就出来了。最稳的做法是数据库层 decimalJava 层用 BigDecimal接口返回字符串。4.4 下单接口Controller、入参校验与状态流转订单创建是点餐系统最核心的后端接口前面提到的金额计算必须在这里完成PostMapping(/api/order/create) public ResultOrderVO 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 出发但顺着这个思路改下去它就能长成一套能接外卖平台、能出日结报表的完整业务系统。希望这条二开路径能帮你在自己的项目里少走几步弯路。本文还有配套的精品资源点击获取