简介面向移动电商开发者的 Uniapp 购物商城移动端源码基于 lilishop 电商系统构建可编译打包为 iOS、Android 及各主流小程序适合需要快速搭建商城前端、学习跨端开发流程或二次开发的中高级开发者。压缩包共 1591 个文件约 35.89MB以 Java 后端源码为主1500 个辅以 JPG/PNG 素材、XML/YML 配置、SQL 数据库脚本及 Dockerfile、Shell 脚本等部署文件整体覆盖商城业务后端、接口配置与部署运维所需内容。预览中可见订单服务、商品索引、支付宝支付等核心模块可帮助理解电商订单流转、商品搜索引擎索引构建和支付对接实现。已有 507 人学习下载目录结构清晰适合对照 lilishop 开源体系梳理由 Uniapp 移动端到后端服务的调用链路作为电商项目实战参考。1. 一套 uniapp 商城源码同时交付小程序、H5 和 App值得吗当电商项目的排期表上同时出现微信小程序、H5 商城、iOS/Android App 三个入口而后端接口还没完全定稿时很多团队的习惯是“先写小程序后面再套壳”。实际上从移动终端项目的落地经验看这套思路往往走不通小程序里能顺畅跑的页面App 端并不一定兼容H5 里顺手用的 window 对象小程序编译阶段直接报错。与其维护三套代码、三套发布节奏不如在源头就统一技术栈uniapp 商城源码解决的就是这个矛盾——业务代码收敛到同一份 Vue 单文件组件里平台差异只通过条件编译暴露。本文适合手上已有后端接口、想用最短路径交付多端商城的前端工程师读完你不仅能搭起一套可复用的商城源码结构还能避开打包、登录、支付这几个最容易翻车的配置点。2. 商城源码的工程结构设计uniapp 的编译边界、目录划分与状态管理选型2.1 为什么 uniapp 商城源码能跨端关键在于编译期而不是运行期先澄清一个常见误解uniapp 不是运行时把一套 JS 翻译成多端代码而是在编译期根据目标平台执行不同的转换流程。写在小程序端编译器把 template 转成 WXML、style 转成 WXSS、script 里的 Vue 生命周期映射到小程序的生命周期写到 H5 端产出标准 DOM 页面写到 App 端非 nvue 页面走 webview 渲染nvue 页面则走原生渲染。这意味着在商城源码里只要不调用端特有的 API就能被编译到任意平台一旦图省事直接调用 wx.getSystemInfoSync 这类原生 API小程序上没问题App 端编译时却未必报错真机运行就直接白屏。这就是商城源码里最该遵守的第一条规则所有端能力调用必须走 uni.* 统一 API或放入条件编译分支。比如获取用户信息老版本小程序用 uni.getUserProfileApp 端有独立的授权弹窗流程如果在小程序里直接写 wx.loginH5 端调试时根本不报错可真机一跑就挂。一条典型的 uniapp 商城源码分层如下层级放什么典型文件页面层 pages路由页面pages/index、pages/goods/detail组件层 components可复用 UI 与业务组件sku 选择器、商品卡片、订单状态条状态层 store全局共享数据购物车、登录态、收货地址接口层 api请求封装与模块接口api/goods.js、api/order.js工具层 utils纯函数与格式处理价格计算、日期格式化、防抖2.2 商城源码目录怎么摆订单模块和售后模块才不会互相踩我一般建议把订单、商品、用户三个主流程拆成独立 api 文件而不是把所有请求都堆在一个 request.js 里。商城需求迭代最快的往往是订单状态和售后流程独立文件能降低互相改动的风险。参考结构src/ ├── pages/ │ ├── index/index.vue │ ├── goods/detail.vue │ ├── cart/cart.vue │ ├── order/ │ │ ├── list.vue │ │ ├── confirm.vue # 确认订单页 │ │ ├── pay-success.vue │ │ └── detail.vue │ └── user/ │ ├── index.vue │ ├── coupon.vue │ └── address/list.vue ├── components/ │ ├── sku-picker.vue │ └── goods-card.vue ├── store/ │ ├── index.js │ ├── cart.js │ └── user.js ├── api/ │ ├── goods.js │ ├── cart.js │ ├── order.js │ └── user.js ├── utils/ │ └── price.js ├── static/ └── pages.jsonpages 底下的目录名直接对应 pages.json 里的路由 path不要额外加一层 views 包裹。uniapp 编译到小程序时会把路径原样映射成分包路径层数越少越不容易在“主包体积超 2MB”时手忙脚乱地改目录结构。组件层只放真正跨页复用的东西像 sku-picker 这种只有商品详情页用到的组件直接放在 pages/goods/components 下连注册都省了。2.3 购物车和登录态用 Vuex 还是 Pinia取决于你的 Vue 版本老项目的 uniapp 商城源码大多基于 Vue2配 Vuex 3新脚手架默认 Vue3 Pinia。关键不在选哪个库而在统一 store 的写入和持久化策略。商城场景里购物车最怕“多处修改不同步”加购在详情页、改数量在购物车页、清空在订单生成后三处对同一份数据操作必须走同一个 mutation/action// store/cart.js —— Vue2 Vuex 3 的写法 import Vue from vue import Vuex from vuex Vue.use(Vuex) export default new Vuex.Store({ state: { cartList: uni.getStorageSync(cart) || [] }, mutations: { UPDATE_COUNT(state, { skuId, count }) { const item state.cartList.find(i i.skuId skuId) if (!item) return item.count count uni.setStorageSync(cart, state.cartList) // 本地缓存App 杀掉进程后购物车还在 }, CLEAR_CART(state) { state.cartList [] uni.removeStorageSync(cart) } } })这段代码有两个容易踩坑的位置。第一mutation 里直接修改了已存在对象的 count 属性Vuex 的响应式能追踪到但如果换成整体替换数组视图同样更新问题只出在本地缓存写入频率——每次 mutation 都同步 setStorageSync对商城这种高频加购操作开销偏大常见做法是加个 debounce或只在页面隐藏时写一次。第二setStorageSync 在小程序端有总量限制购物车对象里不要塞商品轮播图 base64只存 skuId、数量、缩略图路径、价格这几个结算必需字段。如果项目从 Vue2 迁到 Vue3网络热词里高频出现的“uniapp vue2 转 vue3”最大工作量并不是模板语法而是全局状态挂载方式变了。Vue2 里通过 Vue.prototype 注入Vue3 Pinia 改成 app.use(pinia)想在组件外拿到 store必须显式引入// 在 utils 或 api 模块里使用购物车 store import { useCartStore } from /store/cart export function clearCartOutsideComponent() { const store useCartStore() store.clear() }注意 useCartStore() 必须在 pinia 实例 install 完成之后调用否则会报 getActivePinia was called but there was no active Pinia。老项目转新框架时这个报错往往出现在 api 层拦截器里调试起来第一眼根本看不出是时序问题。3. 用 uniapp 商城源码跑通商品列表与购物车请求封装、状态联动和价格精度3.1 商品列表页的请求封装与加载态直接 uni.request 还是二次封装在每个页面里直接写 uni.request 不是不行但商城接口通常有统一的 code 约定、token 注入和错误提示散落各处后一旦后端把业务 code 从 0 改成 200就得全局替换。我一般维护一个 utils/request.js 做统一出口// utils/request.js const BASE_URL https://api.example.com export function request(options) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: uni.getStorageSync(token) ? Bearer ${uni.getStorageSync(token)} : }, success: (res) { if (res.statusCode ! 200) { uni.showToast({ title: 服务异常, icon: none }) reject(new Error(HTTP ${res.statusCode})) return } const body res.data if (body.code 0) { resolve(body.data) } else if (body.code 401) { // token 失效清除本地登录态并跳转登录页 uni.removeStorageSync(token) uni.navigateTo({ url: /pages/login/login }) reject(new Error(登录态失效)) } else { uni.showToast({ title: body.msg || 请求失败, icon: none }) reject(new Error(body.msg)) } }, fail: (err) { uni.showToast({ title: 网络异常请检查网络, icon: none }) reject(err) } }) }) } // api/goods.js import { request } from /utils/request export const fetchGoodsList (data) request({ url: /goods/list, method: POST, data })成功回调里判断 body.code 而不是只看 HTTP 状态码这是商城接口联调最常见的分水岭。网关返回 200 不代表业务成功后端做服务降级时 code 可能是 5001此时页面不应继续渲染商品列表。401 分支跳转登录页之前要先清 token否则会出现登录页闪一下又被拦截器拉回详情页的循环。3.2 商品详情进购物车的联动SKU 选择器与 store 的通信边界购物车的入口通常是商品详情页用户选了 SKU 规格点加入购物车这时不要把整个 SKU 选择器塞进全局 store。商城场景里 SKU 数据量可能很大颜色加尺寸加套餐组合上百条放进 Vuex 后再做本地缓存序列化非常浪费。常见做法是页面内部维护 currentSku 局部变量只有确认加购后才把 skuId、数量、价格、缩略图交给 store// pages/goods/detail.vue节选 import { useCartStore } from /store/cart export default { data() { return { skuList: [], currentSku: null, quantity: 1 } }, methods: { onSkuChange(sku) { // sku 选择器每次切换规格都会触发这里只更新局部变量 this.currentSku sku }, addToCart() { if (!this.currentSku) { uni.showToast({ title: 请先选择规格, icon: none }) return } const cartStore useCartStore() cartStore.add({ skuId: this.currentSku.id, count: this.quantity, price: this.currentSku.price, thumb: this.currentSku.thumb }) uni.showToast({ title: 已加入购物车, icon: success }) } } }如果项目还停留在 Vue2 Vuex把 useCartStore 换成 mapMutations 或 this.$store.commit 即可通信边界不变。SKU 规格联动留在页面内部数据流单向页面切走自动释放真正进 store 的只有结算字段购物车角标订阅 store 后任意页面加购都能即时刷新。3.3 价格累计用整数分而不是浮点数商城金额计算的三条硬规则商城源码里金额相关的高频 bug 几乎都来自浮点运算。JS 里 0.1 0.2 等于 0.30000000000000004商品数量一多总价就差几分钱。不加依赖的稳妥做法是转分计算// utils/price.js // 元转分先乘 100 再 Math.round避免 parseFloat 的精度陷阱 export function yuanToFen(yuan) { return Math.round(parseFloat(yuan) * 100) } export function fenToYuan(fen) { return (fen / 100).toFixed(2) } export function calcTotal(list) { // list: [{ price: 19.9, count: 3 }] const totalFen list.reduce((sum, item) { return sum yuanToFen(item.price) * item.count }, 0) return fenToYuan(totalFen) }场景推荐单位原因后端下发价格分字符串或整型避免 JSON 里浮点精度丢失前端参与计算分加减乘除都在整数域内完成界面展示元仅最后一步除以 100 再 toFixed(2)三条硬规则所有下发到前端的金额后端统一给分前端展示时才转元优惠券、运费、满减全部在整数分域内计算。另外注意 toFixed 在 JS 里是四舍六入五成双并非严格四舍五入。总价分转元后如果想四舍五入先用 Math.round 再除以 100不要直接写 (1.005).toFixed(2) 这类代码返回结果可能和预期差一分钱。4. 小程序端与 App 端的差异怎么处理条件编译、登录支付与 manifest 配置4.1 商城源码里的平台差异化代码用 #ifdef 条件编译隔离而不是运行时判断商城源码最典型的差异点在支付微信小程序只能用 wx.requestPaymentApp 端走 uni.requestPayment 的 App 支付通道H5 可能对接支付宝网页支付或微信 JSAPI。如果写 if (platform weixin) 这种运行时判断所有分支的代码都会被编译进同一个包而且微信小程序的 API 在 App 端未必存在一调用就报错。正确解法是条件编译// api/pay.js function payByWxMiniProgram(orderId, amount) { return new Promise((resolve, reject) { uni.requestPayment({ provider: wxpay, orderInfo: { orderId, amount }, success: (res) resolve(res), fail: (err) reject(err) }) }) } export function payOrder(orderId, amount) { // #ifdef MP-WEIXIN return payByWxMiniProgram(orderId, amount) // #endif // #ifdef APP-PLUS return uni.requestPayment({ provider: wxpay, orderInfo: { orderId, amount } }) // #endif // #ifdef H5 return payByH5(orderId, amount) // #endif }条件编译的注释不是普通注释uniapp 编译器在预处理阶段会删除不匹配平台下的整块代码所以其他端即使没有对应平台的声明也不会报错。调试时最容易让人困惑的是 IDE 报错提示找不到 wx 变量这通常是编辑器没识别条件编译把文件重新保存或确认文件后缀是 .vue提示就消失了。4.2 登录态在微信小程序和 App 上的差异code 换 token 的流程不一样微信小程序登录是 wx.login 拿 code后端拿 code 换 openid 和自定义 tokenApp 端可能是微信开放平台授权、Apple 登录或手机号加验证码。uniapp 商城源码里如果共用同一个登录组件平台登录按钮区域建议用条件编译隔离!-- pages/login/index.vue 节选 -- view classlogin-form input v-modelphone typenumber placeholder手机号 / input v-modelcode typenumber placeholder验证码 / button taploginByPhone登录/button /view !-- #ifdef MP-WEIXIN -- button open-typegetPhoneNumber getphonenumberwxPhoneLogin 微信一键登录 /button !-- #endif --open-typegetPhoneNumber 是微信小程序专有的按钮属性App 端没有这个类型。如果不做条件编译H5 端会把 getPhoneNumber 当普通 button 属性忽略真机小程序上也没反应。把差异藏在编译期各端拿到的是干净的模板。最近折腾商城源码的团队常问 uniapp 扫码怎么接。小程序端扫码入口是 wx.scanCodeApp 端是 uni.scanCode回调参数略有不同。我的做法是全部走 uni.scanCode 统一封装只有识别小程序码这类特殊场景才走条件编译普通商品码、订单码用统一 API 足够。4.3 manifest.json 不是摆设微信小程序 AppID、App 包名与支付模块都要在这里配好打开 uniapp 项目的 manifest.json在小程序配置里填微信 AppID在 App 模块配置里勾选 OAuth、Payment、Share、Push。很多新手问 uniapp 怎么打包卡点就在这一步manifest 里没勾选 Payment真机调用 uni.requestPayment 会直接返回 API_NOT_FOUND。还有 iOS 的 URL Scheme 和 Android 的包名签名如果要在 App 端拉起同主体的微信小程序或分享到微信微信开放平台后台必须与 manifest 里的包名签名保持一致。// manifest.json —— 关键字段节选 { name: 商城演示项目, appid: , mp-weixin: { appid: wx1234567890abcdef, setting: { urlCheck: false }, usingComponents: true }, app-plus: { modules: { OAuth: {}, Payment: {}, Share: {} }, distribute: { android: { packagename: com.example.mall, permissions: [ uses-permission android:name\android.permission.INTERNET\/ ] } } } }manifest.json 修改后不是每次打包都会自动生效。小程序端改 AppID 后要重新编译App 端改包名或勾选模块后建议在 HBuilderX 里执行重新编译运行。常见报错“请先在 manifest.json 里配置 appid”指的就是这里的 mp-weixin.appid 字段为空。另外App 端拉起微信小程序用的是 uni.navigateToMiniProgram传入目标小程序的原始 ID前提是当前 App 与目标小程序在同一微信开放平台账号下这个绑定关系不在 manifest 里而在微信开放平台后台。5. 商城源码上线前最后一步分包、缓存与多端打包验收清单5.1 微信小程序主包超 2MB 时用分包把下单流程拆出去微信对主包体积限制是 2MB整个小程序上限 20MB。商城源码光商品图就可能占掉大半常规做法是把“下单、支付、售后”拆成分包在 pages.json 的 subPackages 字段声明// pages.json 节选 { pages: [ { path: pages/index/index }, { path: pages/goods/detail }, { path: pages/cart/cart } ], subPackages: [ { root: pagesOrder, pages: [ { path: confirm/confirm }, { path: pay/pay }, { path: detail/detail } ] } ], preloadRule: { pages/cart/cart: { network: all, packages: [pagesOrder] } } }分包的限制在于 tabBar 页面不能放分包里分包之间不能互相跳转。把确认订单、收银台、订单详情放同一个分包再配合 preloadRule 做预下载用户进购物车时就开始加载订单分包点击结算跳转不白屏兼顾首屏速度和下单路径体验。5.2 动态标题与图片缓存移动终端上被忽视的两个体验细节商城源码里要把导航栏标题改成商品名方法是用 uni.setNavigationBarTitle它同时支持小程序、H5 和 App。但 onLoad 里如果页面还没完成挂载就调用偶发会被页面原标题覆盖稳妥做法是放在 onReady 之后或 nextTick 里。App 端如果想自定义原生导航栏方式不是 setNavigationBarTitle而是把 pages.json 里对应页面的 navigationStyle 设为 custom再自己渲染头部。图片缓存方面商品图优先走 CDN 并开启懒加载。App 端可用 uni.getImageInfo 预取下一屏商品图H5 端在 manifest.json 的 h5.publicPath 里配置 CDN 域名小程序端则把 static 目录里图片压缩到最小。这三个端各做一次首屏速度差的不是一点半点。5.3 多端打包验收清单H5、微信小程序、Android 各查一遍检查项H5微信小程序App登录手机号微信一键登录 手机号第三方登录 手机号支付支付宝 / 微信 JSAPIwx.requestPaymentuni.requestPayment分享生成海报转发按钮微信 SDK / 系统分享导航栏网页标题原生导航栏原生或自定义导航栏包体积Gzip 后 1MB主包 2MB按应用市场要求验收时逐端真机测试尤其支付回调微信小程序支付成功后拿到的 errMsg 要包含 requestPayment:ok 再跳转App 端支付回调路径不同写死同一套代码过不了原生验收。最后还有一处最容易提升成交率的改进在封装好的 request 层里定期刷新 token或在每次响应后重写本地 token 的过期时间避免用户算好满减准备付款时突然被登出。本文还有配套的精品资源点击获取