简介:这是一份基于SpringBoot与uniapp的商城项目源码包,面向具有一定Java基础、想系统练习全栈或跨端开发的学习者。项目后端以SpringBoot为主,前端采用uniapp,整体参考linjiashop开源商城设计,覆盖用户注册登录、商品分类展示、购物车、订单管理等典型电商流程,并包含商品查询、提交订单等接口示例。资源共67个文件,压缩包仅64KB;其中60个java文件构成业务核心,体现控制器、服务层与数据访问层的分层实现,3个xml和2个yml用于MyBatis/JPA映射与端口等环境配置,1个md说明文档便于快速了解项目结构,另有.gitignore辅助版本管理。目前已有1938人浏览学习。通过学习源码可掌握RESTful API注解开发、uniapp跨端页面与请求封装、前后端JSON交互、数据库字段设计及基础安全与性能优化思路,适合作为商城类全栈项目的入门参考和二次开发模板。 去年团队接了一个商城项目的开发任务,产品经理扔过来一句话:小程序、App、H5 一个都不能少,后台管理再补一套 PC 端。这句话基本就给技术选型定了调。最终我们选择的后端是 SpringBoot,前端是 uniapp,项目代号就叫 shop。整套系统从订单流程到多端打包发布都踩了一遍坑,这篇把其中真正值得参考的细节写出来,给同样想用 SpringBoot + uniapp 做商城项目的朋友一个完整参考。
1. 商城项目为什么选 SpringBoot + uniapp:一次多端交付的现实决策
1.1 技术选型的核心逻辑:谁在什么时候用什么端
很多人讨论技术选型爱从"哪个框架更先进"出发,但我建议先看交付面。商城类项目有两个典型角色:C 端消费者逛商品、下单、查订单,用的设备极其分散,有微信小程序、有安卓 App、有苹果 App、还有人直接开手机浏览器访问 H5;B 端运营和客服基本只坐在电脑前,用 PC 后台管理商品、订单、用户数据。
如果 C 端四个平台各写一套原生代码,一个前端小组根本交付不过来。uniapp 的核心价值就在这里:一套 Vue 语法代码,编译到小程序、App、H5 三端,覆盖了商城项目绝大部分终端场景。而后台管理这种强交互、重表格、重权限的系统,单独用 Vue 或 React 写 PC 端是更稳的做法,没必要硬塞进 uniapp。
SpringBoot 作为后端也同样是"随便招一个 Java 开发就能上手"的务实选择:生态成熟、集成第三方支付和物流接口的资料多、团队踩坑成本低。一个商城项目最怕的不是技术不够新,而是问题没人解决过。
1.2 为什么不选 Flutter 或 React Native
聊选型绕不开 Flutter 和 React Native。我在项目立项时专门做了一次对比,最终放弃它们不是因为技术差,而是"跨端范围"不匹配:
| 对比项 | uniapp | Flutter | React Native |
|---|---|---|---|
| 小程序支持 | 原生支持,直接编译 | 需额外套一个编译层,维护成本高 | 需额外方案,成熟度一般 |
| H5 支持 | 支持较完善 | 支持较弱,常用于 Web 实验 | 支持一般 |
| App 性能 | 接近原生,复杂动画吃亏 | 自绘引擎,性能强 | 原生组件桥接,性能较好 |
| 前端上手成本 | Vue 语法,门槛低 | 需要学 Dart | 需要学 React 与 RN 生态 |
| 社区与招聘 | 国内电商类项目多,生态热度高 | 热度高但偏移动端 | 热度下降,招人难度上升 |
商城项目不是重动画、重计算的 App,性能瓶颈主要在接口和图片加载上。用 uniapp 换来的开发效率和全端覆盖,远比那一点性能差距值钱。当然如果你的核心场景是大规模列表滚动、复杂手势交互,Flutter 确实更有优势,但那是另一个故事了。
1.3 项目模块怎么划分
后端我用了 SpringBoot 多模块结构,避免所有代码堆在一个工程里:
shop ├── shop-common // 公共工具类、统一返回结果、异常码 ├── shop-framework // 配置、安全、拦截器、文件存储 ├── shop-api // C 端接口:商品、购物车、订单、支付 ├── shop-admin // B 端接口:商品管理、订单管理、用户管理 └── shop-job // 定时任务:超时关单、库存回补前端 uniapp 这边按业务分包,商城核心页面放主包,活动页、售后流程这类低频页面用分包加载,能显著缩小首包体积,这对小程序的审核和首屏速度都有帮助。这个划分方式在项目中期被验证是对的:C 端和 B 端接口权限模型完全不同,拆开之后互相不干扰,单独发布迭代也方便。
2. SpringBoot 后端落地:版本矩阵、自动装配与订单核心链路
2.1 先把版本矩阵定死:SpringBoot 2.7 还是 3.x 不只是一个选择题
搜索热度里"springboot版本太高"出现得很多,这背后是真实的团队困境。SpringBoot 3.x 要求 JDK 17,而很多商城项目团队现有的基础设施、运维脚本、依赖包还停留在 JDK 8 生态。如果贸然升级,第一个撞上的就是包名变更:javax.servlet变成了jakarta.servlet。我的一个朋友从 2.7 升 3.x 时,光是把代码里import javax.annotation.*全部替换成jakarta.annotation.*就花了一天,后来还发现某个支付对接的第三方 SDK 根本不支持 JDK 17。
我给商城项目的版本矩阵建议是:
<properties> <java.version>1.8</java.version> <spring-boot.version>2.7.18</spring-boot.version> <mybatis-plus.version>3.5.3.2</mybatis-plus.version> <hutool.version>5.8.25</hutool.version> <jjwt.version>0.11.5</jjwt.version> </properties>SpringBoot 2.7 是 2.x 的最终版本,官方维护周期长,社区资料最全,JDK 8 环境下非常稳。如果你是新项目、团队又全员熟悉 JDK 17,那直接上 3.2+ 没问题,但前提是第三方依赖都确认过兼容性,尤其是支付、短信、对象存储这类商城绕不开的中间件 SDK。另外补充一点,spring-boot-maven-plugin在做镜像构建时,3.x 的build-image功能更完善,但那是部署环节的事,别因为想用这个功能就把整个后端架构推倒重来。
2.2 自动装配原理:为什么引入依赖就能用,不生效时又该查哪里
很多做商城项目的开发被面试官问"SpringBoot 自动装配原理"时能说个大概,但实际排错时就抓瞎。简单说,@SpringBootApplication注解里有个@EnableAutoConfiguration,它在启动时会读取 META-INF 下的自动装配文件:
- SpringBoot 2.7 及之前读取的是
spring.factories - SpringBoot 3.x 改成了
AutoConfiguration.imports
文件里列了一堆配置类,每个配置类通常配合@ConditionalOnClass、@ConditionalOnProperty、@ConditionalOnMissingBean等条件注解生效。举例来说,你在 pom 里引入spring-boot-starter-data-redis,自动配置类检测到 classpath 里有 RedisTemplate 相关类,就会自动创建连接工厂和模板对象。所以当你的 Redis 配置不生效时,第一反应不应该是盲目写@Bean,而是去检查:
- classpath 是否真的引入了对应 starter
- 配置文件中是否有关键开关被误关
- 是否自己定义了同名 Bean 覆盖了默认的自动配置
我在一个订单模块里就踩过这个坑:自定义了一个RedisTemplate,但没有指定序列化器,结果把LocalDateTime直接存进去,反序列化时全部报错。当时一直怀疑是版本问题,最后翻源码才发现是自动配置被自定义 Bean 覆盖了。建议在项目初期就把常用自动配置类的源码打开看一眼,比出了问题再找效率高很多。
2.3 订单状态机、库存扣减与超时关单
商城项目与普通 CRUD 项目最大的区别在订单链路。一个订单从创建到完成通常经历:待支付、已支付、待发货、已发货、已完成、已关闭、售后中。这些状态之间不是随便跳的,比如"已发货"不能直接变成"待支付","已关闭"也不能变成"已完成"。我习惯在代码里用一个枚举加一个状态流转表来管理,而不是到处写魔法数字:
public enum OrderStatus { PENDING_PAYMENT(0, "待支付"), PAID(1, "已支付"), SHIPPED(2, "已发货"), COMPLETED(3, "已完成"), CLOSED(4, "已关闭"); private final int code; private final String desc; }库存扣减是另一个重灾区。简单做法是在下单时直接UPDATE product_sku SET stock = stock - #{count} WHERE id = #{skuId} AND stock >= #{count},靠数据库行锁保证不会超卖。但秒杀、高并发场景下,这个方案会把数据库打爆。更常见的方案是先把库存预热到 Redis,用 Lua 脚本原子扣减,再异步同步回数据库。商城项目如果预期流量不大,数据库扣减足够;如果要做秒杀,Redis 预热加异步落库才是比较稳的路径。
订单超时未支付需要自动关闭,这也是商城必备功能。我们盯上了定时任务——用 PowerJob 配置了一个每日扫描任务,每过 30 秒扫一次待支付订单,超过 30 分钟没支付的自动关单并把预占库存释放。用定时任务要注意幂等,任务重复执行也不能影响订单状态。
2.4 最容易漏的三项配置:上传、资源映射、日期序列化
商城项目绕不开图片上传、商品详情富文本里的图片访问、订单时间的格式化。这三个配置看着基础,但一旦漏了,联调阶段就是连环炸。
大文件上传需要先调大限制,默认只有 1MB 左右,肯定不够用:
spring: servlet: multipart: max-file-size: 50MB max-request-size: 100MB图片传到本地磁盘后,用户访问不到,因为 SpringBoot 默认不映射任意目录。需要加一个资源映射:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + uploadPath + "/"); } }日期格式问题更隐蔽。后端返回LocalDateTime时,Jackson 默认序列化出来是"2024-05-20T10:15:30",前端小程序端显示起来特别别扭。我在配置里统一处理掉:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8还有一点容易忽略:SpringBoot 2.6 开始默认禁止循环依赖,如果项目里出现A -> B -> A这种依赖,启动时直接报错。网上很多老教程建议在配置里设置spring.main.allow-circular-references=true强行放行,我的态度是:新代码一律通过构造器注入加合理拆分解决,不改配置绕过,不然以后根本不敢动那两个类。
2.5 单元测试别只测 Service,接口层的测试价值更大
商城项目涉及金额计算、库存变化,出 bug 的代价比普通系统高。接手项目之初我就定了一条规矩:每个订单接口都要有单元测试。但注意,SpringBoot 的单元测试最佳实战不是拿@SpringBootTest把整个容器拉起来再跑,那样速度慢且依赖环境。正确的姿势是:
- 纯业务逻辑(价格计算、状态流转)用 JUnit 5 + Mockito,不启动容器
- 涉及数据库操作的用
@SpringBootTest加 H2 内存库或 Testcontainers - 涉及第三方接口的用 MockWebServer 模拟
我见过太多团队单元测试写成了"会亮的绿灯":测试代码把数据库连着、Redis 连着,跑一次要两分钟,最后 CI 上总是超时,大家就约定俗成不跑了。这种测试不如不写。把测试控制成秒级,开发才会愿意在每次改动后跑一遍。
3. uniapp 端最容易翻车的点:登录态、滚动冲突、视频播放与分享
3.1 manifest.json 是打包前的第一道关卡
uniapp 项目的manifest.json作用比很多人以为的大得多。它不只是个应用信息配置文件,还直接决定你打包出来的 App 能调用哪些原生模块。商城项目里常见的几个场景:
- 要用地图选收货地址,必须勾选 Maps 模块
- 要用扫码功能,必须勾选 Barcode(二维码扫码)模块
- 要用消息推送,必须勾选 Push 模块并配置厂商通道
- 要使用蓝牙连接打印机,必须勾选 Bluetooth 模块
这些模块在 HBuilderX 云打包时会写进原生工程。如果没勾选,代码里调用uni.chooseLocation或uni.scanCode时,App 端要么直接报错要么没反应,小程序端却一切正常。这种"开发时好好的,打包后失效"的问题,九成出在 manifest 配置上。另外,H5 端跨域问题也需要额外注意,开发时用 devServer 配置 proxy,生产环境则靠后端网关解决。
3.2 多端登录态的差异处理
商城 C 端的登录链路比想象中分裂:微信小程序走uni.login获取 code,由后端调微信接口换 openid;App 端可能是手机号一键登录;H5 端则可能是账密登录。三端不能共用同一套前端逻辑,但后端返回的登录凭证可以统一成 token。
前端这层我用了一个简单的请求拦截器:
// request.js const request = (options) => { return new Promise((resolve, reject) => { const token = uni.getStorageSync('token') uni.request({ url: BASE_URL + options.url, data: options.data, header: { 'Authorization': 'Bearer ' + token }, success: (res) => { if (res.data.code === 401) { uni.removeStorageSync('token') uni.navigateTo({ url: '/pages/login/login' }) } else { resolve(res.data) } }, fail: reject }) }) }这里的 401 统一处理要注意一点:商城页面跳转链路可能很深,用户在订单确认页登录超时被弹回登录页,登录成功后最好有一个回跳逻辑,否则用户得重新一步步点进来,流失率很高。我在项目中用了一个全局变量记录登录前页面路径,登录成功后再重新跳转。
3.3 下拉刷新与页面滚动的冲突:问题根源与解法
"uniapp 下拉如何触动滚动屏而不触发页面下拉刷新"这个问题被搜了很多次,说明它确实困扰了大量开发者。问题场景是:页面里有一个scroll-view纵向滚动,你期望列表滚动到顶部后继续下拉,触发列表自己的刷新逻辑,而不是把整个页面拽下来触发onPullDownRefresh。
冲突的根源在于:页面级下拉刷新和scroll-view滚动是两套手势系统。默认情况下scroll-view滚动到顶后继续下拉,手势会被页面层接管。
我试验后比较稳的方案是:页面配置文件里关掉enablePullDownRefresh,在scroll-view上监听@scrolltoupper事件,配合自己写的下拉刷新动画组件。这样刷新逻辑完全收敛在列表组件内部,不跟页面手势打架。代价是要自己实现刷新态和动画,但体验是可控的。另外注意scroll-view需要设置固定高度或flex: 1,否则列表不会出现滚动条,事件也就不会触发。
3.4 renderjs 播放 mp4 失败:问题大概率出在编码
项目里有一个商品详情视频,用户手机拍摄上传后,在 App 端能播放,但在 H5 端怎么都放不出来。一开始怀疑是 renderjs 的兼容问题,花了不少时间排查,最后发现根因是视频编码格式:手机录制的 mp4 很多是 HEVC(H.265)编码,而大部分浏览器只支持 AVC(H.264)编码。
renderjs 解决的是"视图层与逻辑层通信"的问题,它没法把一个浏览器解不出来的编码格式变出来。解决办法是在上传视频链路里增加一道转码服务,用 FFmpeg 把上传视频统一转成 H.264 + AAC 编码的 mp4:
ffmpeg -i input.mp4 -c:v libx264 -profile:v main -crf 23 -c:a aac output.mp4如果是商城项目,建议在服务端上传接口就做转码,而不是把压力丢到前端。这个坑属于典型的"非 uniapp 的问题但 uniapp 项目一定会遇到",排查的时候先确认编码,再谈其他。
3.5 自定义分享:微信小程序和 App 的逻辑完全不同
"uniapp 自定义分享好友"是商城运营的刚需,但三端实现方式差异很大。微信小程序在页面里写onShareAppMessage和onShareTimeline,可以自定义标题、图片和路径;App 端则要调用plus.share或者使用 uni 的分享组件,配置微信 SDK、QQ SDK 等;H5 端最简单,本质是复制链接。
我踩过的坑是:在小程序端自定义分享卡片时,imageUrl必须是小程序白名单域名下的 HTTPS 图片,且分享路径path需要带参数。如果imageUrl填的是本地相对路径,分享卡片会显示空白。当时为了这个问题,最后把所有分享图统一放到 CDN 域名下,才彻底解决。线上商城如果有拼团、砍价这类裂变玩法,分享参数的正确性直接决定活动效果,值得早点设计好。
4. 从开发机到应用市场:打包、上架与 Docker 部署全链路
4.1 HBuilderX 云打包与离线打包的取舍
uniapp 打包 App 有两种方式:HBuilderX 云打包和离线打包。云打包的优势是省事,界面点点就出 apk 或 ipa,缺点是受平台网络影响,而且自定义原生插件的空间有限。离线打包则需要下载官方 SDK、用 Android Studio 自己组装工程,适合需要集成原生 SDK 或做深度定制的项目。
离线打包最大的坑是 SDK 版本必须和 HBuilderX 版本严格对应。有一次我们 HBuilderX 更新后发现 App 一打开就闪退,查了很久才发现是离线打包 SDK 还在用旧版本,和新版运行时不一致。这个问题的排查链路是:先看崩溃日志中是否有UniSDK相关异常,再对比 HBuilderX 版本和 SDK 版本。官方文档里有一个版本对应表,开发前第一件事就是核对它。另外,离线打包的 apk 在 Android 13 以上设备上如果没做相关权限适配,打开相册、定位都会异常,manifest 里的权限声明要同步处理。
4.2 安卓应用市场上架与 iOS TestFlight
应用市场审核是商城项目逃不掉的痛苦环节。安卓主流市场要求的材料包括:软件著作权证书、隐私政策、应用备案信息。隐私政策尤其关键,小程序端要求收集用户信息前必须弹窗说明,App 端上架时也必须在应用内展示隐私政策,涉及读取通讯录、定位、相册的权限,还需要逐项说明用途。
iOS 端上架前测试用的是 TestFlight,流程相对固定:在 App Store Connect 上传构建版本,添加测试员,然后通过 TestFlight 安装测试。商城的支付功能只能用实物购买,不能用虚拟商品支付,这一点在审核时很容易被卡,如果是虚拟商品项目,要提前想好替代方案。
4.3 SpringBoot 打包到 Docker:JDK 版本必须前后一致
搜索词里的"springboot jdk1.8打包到 docker desktop"说明很多人在这块栽过跟头。SpringBoot 项目用 JDK 8 编译,Docker 基础镜像却用了openjdk:17-jre,启动时直接报UnsupportedClassVersionError。这是一个很无语但非常常见的错误。
商城后端我用的 Dockerfile 长这样:
FROM openjdk:8-jdk-alpine LABEL maintainer="shop-team" ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone VOLUME /tmp COPY target/shop-api.jar app.jar ENTRYPOINT ["java","-Xms512m","-Xmx512m","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]这里有几个细节:
- 基础镜像必须和编译 JDK 大版本一致,JDK 8 项目就选
openjdk:8-*,不要选 17 - 设置
TZ=Asia/Shanghai,否则容器内时间差 8 小时,订单创建时间会集体漂移 - 磁盘上的上传文件目录要挂载 Volume,否则容器重建后用户图片全丢
Docker Desktop 本地调试时,如果项目是从 Windows 共享目录里挂载出来的,要注意文件监听性能问题,开发阶段直接用spring-boot:run更省事。
4.4 商城环境的运维细节:多环境配置、日志与备份
上线前我会把 SpringBoot 的配置拆成多环境文件:
application.yml application-dev.yml application-prod.ymlapplication.yml里只放公共配置,环境相关的数据库、Redis、对象存储地址放到对应 profile 文件里,启动时通过SPRING_PROFILES_ACTIVE指定。这样能避免开发连生产库这种事故。另一个容易被忽略的是日志,商城项目至少要把登录日志、订单操作日志单独记录,出了问题能够追踪是谁在什么时间改了什么订单状态。数据库备份用任务计划或者平台自带的自动备份功能,每天一次全量,每次大版本发布前再做一次手动备份。
5. 这套项目里的真实踩坑清单与顺序优化建议
5.1 联调阶段最常见的三个问题
第一个是跨域。管理后台开发时直接访问 C 端接口,浏览器跨域报错,配置好 CORS 后还要注意allowCredentials和allowedOrigin不能同时使用通配符。第二个是参数命名风格不统一,后端习惯orderId,前端写成了order_id,被这种问题浪费的时间比技术难点多得多。从项目第一天起就约定接口文档规范,查询参数用驼峰还是下划线,一旦定了就不要改。第三个是日期时区问题,后端new Date()存储的是 UTC,前端拿到后如果不指定时区,显示出来的时间就差了 8 小时。前后端约定统一用时间戳或者带时区的 ISO 字符串,能省掉大量扯皮。
5.2 如果重来一次,我会把这些事前置
做成这个项目后我复盘过,有几个环节如果能前置,整体周期至少缩短两成。
接口文档先行。我们的问题是边开发边补接口文档,前端等后端接口时经常靠猜。如果一开始用 Apifox 或 YApi 把接口定义好,前后端并行开发根本没有阻碍。
订单状态机先设计。商城项目的核心不是"增删改查",而是订单流程、库存扣减、支付回调这三件事。这三件事不提前讨论清楚,后期每个接口都在为状态错乱打补丁。
测试用例先写。不是说 TDD 那种严格流程,而是核心订单流程的单元测试要在开发的同时就写。这个项目最贵的 bug 出现在支付回调重复执行上,如果第一次写支付逻辑时就配上幂等校验和测试,后面根本不用改。
5.3 为什么这类项目会频繁出现在面试题里
打开招聘网站搜"SpringBoot"和"uniapp"相关的面试题,商城项目是出现频率极高的案例。原因很简单:商城项目覆盖的知识面足够宽。SpringBoot 考自动装配原理、循环依赖、单元测试、集成 Redis;uniapp 考多端差异、打包配置、生命周期、自定义组件;后端考订单状态机、库存扣减、幂等性、定时任务。这些点恰好对应了中小团队实际开发中最高频的技术场景,能完整讲清楚一个商城项目的人,通常意味着他真实经历过联调、上架、排查线上问题的全过程。
这也是我坚持用 SpringBoot + uniapp 做商城项目的原因。它不炫技,但每一个模块都是将来开发其他业务系统能复用的底子。真正做完一遍,你的收获会比看十篇教程都大。
最后分享一个我个人的小习惯:在 uniapp 项目里尽量用条件编译处理端差异,比如#ifdef MP-WEIXIN、#ifdef APP-PLUS,把不同端的特殊逻辑隔离在小代码块里,公共代码保持干净。这个习惯在后端多环境配置里也一样适用。商城项目表面上是技术问题,本质上是对业务边界的理解和执行顺序的把握,把这层想清楚,你的代码会少很多补丁。
本文还有配套的精品资源,点击获取