拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue3+MyBatis商城系统源码拆解与实战

SpringBoot+Vue3+MyBatis商城系统源码拆解与实战 1. 开发前先想明白这个商城项目的核心难点到底在哪坦白说市面上叫在线商城系统的开源项目没有一千也有八百但绝大多数你拉下来跑一遍就会发现要么是单体架构前后端糊在一起要么是只有 CRUD 没有业务闭环要么压根跑不起来。Java SpringBootVue3MyBatis 这套前后端分离的 ONLY 在线商城系统算是我见过比较适合拿来深入学习、改造、甚至写进简历的一套完整源码。它能解决的问题很明确从零搭一个可上线的电商整套骨架包含商品展示、购物车、下单、用户登录、后台管理等核心业务线。这篇文章不是给你讲看代码而是把我自己拆解这个项目源码、二次开发、踩坑补漏的完整过程记录下来。适合谁看一个是准备做毕设或项目实训的学生一个是刚转 Java 后端想接触完整业务系统的同学还有就是打算在 Vue3 SpringBoot 技术栈上做商城二次开发、想快速定位改造点的人。先说一个反直觉的结论这个项目表面上是SpringBootVue3MyBatisMySQL四个技术点的组合但你真正动手跑起来之后会发现最花时间的不是写代码而是理清数据表之间的关系、订单状态怎么流转、前后端接口字段怎么对齐。代码只是最后落地的那一层。2. 技术选型的底层逻辑SpringBootVue3MyBatis 为什么能撑起一个商城2.1 SpringBoot 的价值在于省掉配置但不省掉架构思维很多人一看 SpringBoot 就觉得它是自动配置的微服务框架这个理解没错但放到商城项目里它的真正价值是让你把精力集中到业务上。就拿 ONLY 商城里的商品模块来说涉及商品基本信息、轮播图、分类、库存、上下架状态如果用传统 SSM 去写光 XML 配置、数据源配置、事务配置就要堆一大片SpringBoot 靠 Starter 机制把这些全部封装掉你只需要关心Service、Mapper、Controller这些注解怎么组织。但这里有一个容易踩的坑SpringBoot 的自动配置是约定优于配置一旦你的目录结构不合约定或者依赖版本冲突排查起来比写代码痛苦得多。我在跑这个项目时就遇到一个经典的坑后面会单独说。2.2 Vue3 对比 Vue2 的差异在商城前端哪里体现商城前端的特点页面多、状态多、组件复用度高。Vue3 带来的 Composition API 最大优势就是把同一业务逻辑的代码聚到一起。比如购物车模块在 Vue2 的 Options API 里data、computed、methods是分离的改一个购物车数量逻辑要上下来回跳Vue3 里用ref、computed、watch包在一起整个流程顺下来非常舒服。另外组合式函数Composable在商城项目里特别好用。我把登录态、购物车数量、搜索关键词这几个跨页面共享的状态抽成useUserStore、useCart这样的自定义函数之后代码复用率高了很多。这一点如果你只是照着源码跑可能体会不深但一旦你自己动手加一个收藏夹功能就能感受到这种设计的好处。2.3 MyBatis 在商城场景里比 JPA 更好用的地方商城系统的 SQL 有一个特点查询条件组合多变。商品列表要按分类筛选、按价格区间筛选、按关键字模糊搜索、按销量或上架时间排序这些条件用户在前端勾一勾就会拼出好几种组合。MyBatis 的动态 SQLwhere、if、choose就是为这种场景设计的。对比 JPA 的Specification或QueryDSLMyBatis 的 XML 写起来更直白SQL 本身就是程序员一眼能看懂的。而且商城报表类的复杂统计 SQL比如按天统计订单销售额统计热销商品Top10在 XML 里写起来完全没有语法适配的成本。这个项目用 MyBatis 而不是 JPA我认为是选对了的。3. 数据库表设计逐张拆解这些字段为什么不能省商城系统的表设计是这个项目最值得抄作业的部分。我数了一下核心业务表大概八张左右我把每张表的字段设计逻辑拆开讲方便你二次开发时知道哪些能加、哪些不能动。3.1 用户表和地址表账号体系与收货信息的边界用户表user必备字段id、username、password加密存储、nickname、avatar、phone、email、status是否禁用、create_time、update_time。这里一个容易被忽略的关键点password字段必须存加密后的密文。项目里用了 BCrypt 加密不是简单的 MD5。MD5 现在用彩虹表破解成本太低了商城涉及真实用户数据密码安全必须用加盐哈希。我见过很多自己写的商城项目直接存明文密码这在简历上属于是送命题。地址表address的核心字段是user_id归属用户、receiver_name、receiver_phone、province、city、district、detail_address、is_default。is_default这个字段很多人喜欢用0/1表示但默认地址的逻辑不是简单的置1而是当用户设置新默认地址时要先把该用户所有地址的is_default置为0再把当前地址置为1。这个操作必须放在一个事务里否则并发情况下会出现两个默认地址。3.2 商品表和分类表SPU/SKU 的简化方案商品表product字段设计得好不好直接决定前台展示和后台管理的复杂度。这个项目采用了相对简化的模型一个商品记录对应一个 SKU最小库存单元。基础字段包括product_name、sub_title副标题、main_image、detail_html详情富文本、price、stock、sales销量、category_id、status上架/下架/草稿、create_time、update_time。这里我要多说一句真正的大型商城比如京东、天猫的商品模型是 SPU标准化产品单元和 SKU 分离的同一个商品有不同的颜色、尺码、套餐每个组合对应不同的价格和库存。但这个项目开发定位是可上线的独立站商城用单表 SKU 模型是合理的简化——你自己开个小商城卖几十上百个商品没必要把模型搞成京东那样。如果你想升级成 SKU 模型改造点主要在商品表拆成spu和sku两张表购物车和订单项表要关联sku_id而不是product_id前端的商品详情页要增加规格选择交互。3.3 购物车设计存数据库还是存 Redis项目里购物车表cart的核心字段是id、user_id、product_id、quantity、checked是否选中、create_time、update_time。这个设计有一个明显的好处用户换设备登录购物车数据不丢。如果你用 localStorage 存购物车虽然实现简单但用户一清缓存全没了而且后台完全看不到用户加购了什么。不过这里有个性能优化的点如果用户量大购物车频繁读写每次都走 MySQL 压力会很大。常规优化方案是 Redis 的 Hash 结构key为user_idfield为product_idvalue为数量。这个项目用 MySQL 是为了降低架构复杂度如果你要做高并发版本把购物车迁到 Redis 是一个很好的练手点。3.4 订单表和订单项表为什么要拆成两张表这是整个数据库设计里最核心的部分。订单表order存的是订单维度信息order_no订单编号、user_id、total_price、pay_status、delivery_status、receive_status、create_time、pay_time、delivery_time。订单项表order_item存的是商品维度信息order_id、product_id、product_name下单时商品名称的快照、product_image、price下单时价格快照、quantity、subtotal。为什么要拆因为一次下单可能包含多个商品如果只建一张订单表那每个商品一行会导致同一个订单的信息收货人、支付状态、下单时间重复存储状态更新时需要同时改多行容易出错。快照字段product_name、price是电商库存设计里特别重要但新手最容易漏的。商品名称和价格是会在后台被修改的如果订单项里不存快照你后续查历史订单时会发现商品名称和当时下单时不一样了价格对不上账。这也是我在实际做这个项目时特别注意到的。订单状态流转是整个系统最核心的业务逻辑待支付 - 已支付/待发货 - 已发货 - 已收货 - 已完成 \- 已关闭超时或用户取消这个状态机在实际场景里还有各种中间态退款/售后但这个项目的定位是基础版状态流转做得比较清晰适合在这个基础上去扩展退款流程。4. SpringBoot 后端的工程结构与 MyBatis 实战细节4.1 包结构按业务模块分包还是按技术层次分包我看这个项目的源码结构用的是经典的按技术层次分包controller、service、mapper、entity、common、config。对于商城这个体量的项目这个分包是合理的简单直白新人好上手。如果是更大规模的项目我更推荐按业务域分包比如order、product、user各成一套 controller/service/mapper这样模块之间能在代码层面隔离开。你自己改造的时候如果新增模块建议按业务域建包不然所有类都堆在controller下面会越来越乱。4.2 MyBatis 动态 SQL商品列表查询的经典写法商品列表查询是这个项目 MyBatis 用得最漂亮的地方。我摘一段典型的动态 SQL 写法去掉了部分无关字段select idselectProductList resultTypecom.example.entity.Product SELECT * FROM product where if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (product_name LIKE CONCAT(%, #{keyword}, %) OR sub_title LIKE CONCAT(%, #{keyword}, %)) /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if if teststatus ! null AND status #{status} /if /where choose when testsort sales ORDER BY sales DESC /when when testsort price_asc ORDER BY price ASC /when when testsort price_desc ORDER BY price DESC /when otherwise ORDER BY create_time DESC /otherwise /choose /select这个写法的好处是Java 代码里不用拼一大堆if-else去构造 SQL 字符串MyBatis 的where标签会自动处理掉AND多余的问题——这个细节很贴心当你所有条件都不成立时where会直接去掉WHERE关键字查全表。4.3 分页插件 PageHelper 的正确用法与隐藏坑商品列表和订单列表都需要分页。项目里用了 PageHelper用法非常简单PageHelper.startPage(pageNum, pageSize); ListProduct list productMapper.selectProductList(query); PageInfoProduct pageInfo new PageInfo(list);但这里有两个坑我必须提醒你第一个坑PageHelper.startPage只能作用于紧接着的第一个查询。如果你在调用startPage之后、执行selectProductList之前中间插入了任何其他 SQL 操作比如一次日志查询、一次计数查询分页就会作用到错误的查询上。所以最佳实践是startPage和 Mapper 调用必须紧挨着中间不要有任何其他操作。第二个坑PageHelper 和动态 SQL 一起用时查询总数的方式可能不符合预期。它默认会生成一条SELECT COUNT(0)来统计总数如果产品表数据量到了几十万这个 count 查询如果没有走合适的索引会拖慢整体响应。优化方式是给商品表的category_id、status建联合索引并尽量避免在WHERE条件里对字段做函数操作比如WHERE DATE(create_time) ...这种写法会让索引失效。4.4 MyBatis 缓存问题为什么你改了数据库前端还是旧数据这里要重点说因为很多人栽在这上面。MyBatis 有一级缓存SqlSession 级别和二级缓存Mapper 级别。在 SpringBoot 整合环境下一级缓存默认开启但每次请求都会创建新的 SqlSession所以几乎感知不到。真正容易出问题的是二级缓存。一旦你开启了二级缓存但没有处理好缓存刷新策略后台改了上下架状态前台读到的还是缓存里的旧数据。排查这个问题的时候我建议先做三件事检查是否在cache中配置了flushInterval刷新间隔检查所有增删改操作是否都执行了flushCachetrueMyBatis 默认为true除非你改过配置最简单的排查方法是先关掉二级缓存看问题是否消失。如果你改动了数据但前端还是旧状态很大概率是缓存没刷掉而不是代码逻辑没生效。5. Vue3 前端组件拆分、登录态管理与接口联调细节5.1 Vite Vue3 的项目组织和路由架构前端部分是基于 Vite Vue3 Vue Router Pinia 搭建的。目录组织比较常规views放页面级组件components放复用组件api放接口请求封装router放路由配置store放 Pinia 状态。路由设计上这个项目区分了前台展示和后台管理的路由。前台路由/home、/product/:id、/cart、/order是用户能访问的后台路由/admin/product、/admin/order专门给管理员用通过路由守卫拦截权限。5.2 登录状态与权限控制Token 存在哪、路由守卫怎么写这个项目用的是 JWT 方案用户登录成功后后端返回一个 Token前端把 Token 存起来每次请求通过拦截器放到Authorization请求头里。关键细节有两个。第一Token 存localStorage还是sessionStorage这个看你对持久登录的需求。存 localStorage用户关掉浏览器再打开会话还在商城场景大多数人希望这样存 sessionStorage关闭标签页就失效更安全但体验差一点。我一般是存 localStorage但会在路由守卫里做过期判断。第二路由守卫不只是判断有没有 Token还要判断这个页面是否需要登录。Vue3 里的写法router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })这里redirect参数很关键用户没登录点进购物车被踢到登录页登录成功后要能自动跳回购物车而不是回到首页。5.3 Axios 封装统一处理 Token、错误码和 HTTP 状态码前后端分离的项目Axios 封装是所有联调的基础。我在这个项目里做了三层处理第一层是请求拦截器每次请求前从 localStorage 取 Token放到请求头。同时注意处理上传文件接口multipart/form-data不需要手动设置Content-Type让浏览器根据FormData自动生成带 boundary 的请求头如果你手动设置了反而可能出错。第二层是响应拦截器正常情况HTTP 200下后端返回格式通常是{ code: 200, data: ..., message: success }。但 HTTP 401未认证和 403无权限是例外要在拦截器里单独判断遇到 401 直接清掉本地 Token 并跳登录页。第三层是错误处理网络超时、500 错误、后端业务异常比如库存不足都要给用户一个明确的提示而不是页面白屏或者控制台报错就完事。5.4 组件通信与状态管理Pinia 在购物车中的使用购物车数据在导航栏数量角标、购物车页面、结算页都要用跨页面共享状态必须用全局状态管理。项目用 Pinia 而不是 Vuex这是 Vue3 时代的正确选择。简单示意一下购物车 store 的结构export const useCartStore defineStore(cart, { state: () ({ items: [], totalCount: 0, totalPrice: 0 }), actions: { async fetchCart() { const res await getCart() this.items res.data this.calc() }, calc() { this.totalCount this.items.filter(i i.checked).reduce((sum, i) sum i.quantity, 0) this.totalPrice this.items.filter(i i.checked).reduce((sum, i) sum i.quantity * i.price, 0) }, async updateQuantity(productId, quantity) { await updateCartItem(productId, quantity) this.fetchCart() } } })这里有一个前后端职责划分的问题合计金额totalPrice到底该前端算还是后端算我的做法是前端计算仅用于展示最终订单金额以后端下单接口返回为准。因为前端算的金额可以被篡改后端必须在创建订单时重新从数据库取价格计算这才是安全的。6. 联调阶段踩过的坑从跨域到金额精度的完整排查链路这一章的内容是我觉得最值得分享的。跑这个项目、前后端联调的时候我踩过几个坑每一个都花了不少时间排查写出来帮你省时间。6.1 跨域问题前后端分离的第一个拦路虎前端跑在http://localhost:5173后端跑在http://localhost:8080端口不同必定跨域。解决跨域的方式有三种后端加CrossOrigin注解仅适合单个 Controller太零散后端写 CORS 配置类全局放行用 Nginx 反向代理前端请求/api转发到后端。项目里用的是 SpringBoot 全局 CORS 配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这个配置里有个细节allowedOriginPatterns(*)不是allowedOrigins(*)。原因是当allowCredentials(true)时Spring 不允许allowedOrigins使用*必须用allowedOriginPatterns才能配合通配符。6.2 金额精度问题BigDecimal 和 double 之间的一步之遥商城项目涉及钱这是最不能妥协的地方。前端 JS 的Number类型处理小数时存在精度问题比如0.1 0.2 0.30000000000000004如果前端直接用浮点数传给后端后端再转成大类型存储累计起来就会出现分毫差错。这个项目的正确处理方式是后端接收金额的字段一律用BigDecimal不要用double或float数据库金额字段用DECIMAL(10,2)这个类型在 MySQL 中本身就是精确的定点数前端传给后端的金额尽量用整数分单位是分或者在后端解析时将元转成分在最后展示再除回去。我在联调时遇到过一次问题创建订单时总金额显示是对的但数据库里存的值总是差个几分钱。排查到最后发现是前端把购物车里每一项quantity * price之和算出来了传给了后端后端直接把前端传的totalPrice存入数据库——这等于拿前端算的钱当最终金额属于在后端代码里就能避免的设计缺陷。正确做法是后端自己遍历订单项用数据库里的价格重新计算总金额。6.3 时间格式问题JSON 序列化和 MySQL 的时区陷阱还有一个我经常碰到的问题前端传2025-01-12 10:30:00这样格式的时间后端存入 MySQL 后查出来变成了null或者前后端显示的时间差了 8 个小时。这通常是两个原因叠加第一个是 SpringBoot 的 JSON 序列化格式不匹配。默认的 Jackson 序列化 LocalDateTime 时输出的是数组格式需要加统一配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8第二个是 MySQL 连接 URL 里的时区参数jdbc:mysql://localhost:3306/mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiserverTimezoneAsia/Shanghai这个参数不加JDBC 驱动和你本机时区不一致就会出现时间错乱。另外数据库表字段里如果是datetime类型Java 实体对应LocalDateTime这套组合用起来最顺手。6.4 图片上传的坑前端 FormData 和后端 MultipartFile 的配合商城商品肯定要传图片。前端上传图片的正确姿势是const formData new FormData() formData.append(file, file) axios.post(/api/upload, formData)这里一个注意点不要手动设置Content-Type: multipart/form-data。你一旦手动设置axios 不会自动帮你生成带 boundary 的 Content-Type后端解析 MultipartFile 时会直接报找不到文件。让浏览器自动生成这个头是最稳妥的。后端的接收方式PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file) { // 校验文件大小、类型 // 存储到本地或OSS // 返回可访问的URL }如果你部署在服务器上还要考虑图片存储路径的问题。我一般建议图片存到独立的静态目录或者直接上云存储OSS/COS不要存在项目的src/main/resources下因为代码重新打包会覆盖掉上传的文件。7. 部署上线从 MySQL 初始化到前后端打包的完整流程7.1 环境准备清单跑这个项目之前我先列一下需要准备好的环境每一条都是我自己踩过坑总结出来的JDK 8 或 11推荐 8兼容性最好如果你要用新特性再上 17Maven 3.6用来构建后端Node.js 14Vue3 项目构建需要推荐 16 或 18 LTS 版本MySQL 5.7 或 8.0推荐 8.0注意认证插件坑见下文Nginx生产环境部署前端静态资源和对接口做反向代理Redis如果需要跑购物车缓存、验证码、单点登录这些扩展功能才需要基础版 MySQL 存储够用的话可以先不装7.2 MySQL 初始化最容易出问题的三个细节初始化数据库通常是导入项目自带的 SQL 文件。这个环节三个常见问题第一个MySQL 8.0 的认证插件问题。如果你用的是 MySQL 8.0而项目里的 JDBC 驱动版本比较老比如 5.1.x启动后端时可能会报Public Key Retrieval is not allowed或认证失败。解决办法是把 JDBC 驱动升级到mysql-connector-java8.0.x 版本并在连接 URL 上加上allowPublicKeyRetrievaltrue。第二个SQL 脚本的编码问题。如果 SQL 文件里有中文注释或中文字段默认值导入时报错或中文乱码原因多半是文件编码不是 UTF-8。用命令行导入前先执行mysql -uroot -p --default-character-setutf8mb4 mall.sql第三个数据库名和用户名对应不上。导入后启动项目如果出现Unknown database或Access denied for user先检查application.yml里的连接信息是否和实际数据库完全一致。这个排查优先级最高因为配置错了代码再对也白搭。7.3 后端打包Maven 的 packaged 与 jar 直接运行细节后端是标准 SpringBoot 项目打包命令很简单mvn clean package -DskipTests打出来的 jar 在target目录下。启动方式java -jar mall-admin.jar --spring.profiles.activeprod这里要提醒一个常见问题如果你在本地运行正常部署到服务器后访问数据库超时或者连不上先检查服务器的防火墙和安全组是否放行了 3306 端口。很多时候并不是代码问题而是网络出不去。配置文件方面我强烈建议把不同环境的配置拆开application-dev.yml和application-prod.yml用spring.profiles.active切换。不要把线上数据库密码写在application.yml里然后整个传到公开仓库。7.4 前端构建与 Nginx 配置一个 301 重定向的坑Vue3 项目构建npm install npm run build生成dist目录里面是静态文件。把dist里的文件上传到服务器某个目录比如/var/www/mall。然后配置 Nginxserver { listen 80; server_name your-domain.com; root /var/www/mall; index index.html; # 前端路由的 history 模式需要这个配置 location / { try_files $uri $uri/ /index.html; } # 接口反向代理到后端 location /api/ { proxy_pass http://127.0.0.1:8080/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; } }这里有两个坑。第一个try_files $uri $uri/ /index.html;这行必不可少。Vue Router 如果用的 history 模式刷新某个子路由页面比如/admin/product时Nginx 如果找不到对应的真实文件会返回 404try_files会让所有前端路由请求最终落到index.html由前端路由接管刷新就不挂了。第二个proxy_pass 的uri部分不能瞎配。我用的是http://127.0.0.1:8080/api/;这种带尾部/api/的写法意思是将原始请求/api/xxx转发到http://127.0.0.1:8080/api/xxx。如果你写成http://127.0.0.1:8080;不带路径那么/api/xxx会被转发成http://127.0.0.1:8080/api/xxx两个效果一样。但如果写成http://127.0.0.1:8080/;带根路径则/api/xxx会被转成/xxx丢掉api前缀后端接口全部 404。这个区别经常有人搞混。7.5 上线前的安全检查清单这部分是我个人的习惯每次商城项目上线前都会过一遍后台管理页面的接口是否做了权限校验还是说只有前端隐藏了入口、后端接口裸奔商品详情页的用户输入内容是否做了 XSS 过滤富文本尤其需要用户密码是否全部为 BCrypt 密文数据库里有没有明文关键接口比如下单、支付回调是否有防重复提交机制是否记录了关键操作日志后台改价格、上下架这类必须留痕MySQL 是否定期自动备份别等数据丢了才想起来。这个项目的后端接口权限用的是拦截器加角色判断的方式算是能用的状态。但如果是生产环境我建议引入 Spring Security 或者 Sa-Token 做更细粒度的权限管理。8. 这套源码之后还可以往哪些方向改如果你已经把 ONLY 商城系统跑通了我觉得可以按下面的优先级做二次开发每一步都能学到东西加一个商品 SKU 规格模型。这是最有价值的改造。把商品表和订单项表的关联字段从product_id改成sku_id前端详情页加规格选择器这能让你彻底理解电商商品模型的核心。加一个简单的支付回调模拟。不用真接支付宝或微信支付你可以模拟支付回调接口把订单状态从待支付改成已支付。重点理解为什么支付回调要用签名验签为什么回调要做幂等处理。加一个后台数据统计面板。用 ECharts 展示近30天销售额趋势、热销商品 Top10、订单状态分布。这一步能让你熟练写出 MyBatis 的聚合 SQL对做报表类需求很有帮助。把购物车迁到 Redis。保持前端接口不变把后端的购物车增删改查持久化层换到 Redis再对比一下性能。你会发现接口响应时间明显下降但代码复杂度也上来了。这种对比经验写进项目描述里是实打实的亮点。我在跑完这套商城源码之后最大的体会是开源项目拉下来能跑只是起点你要敢拆、敢改、敢把原来的表结构推倒重来一遍才算真正把这个项目变成你自己的东西。希望这篇拆解能帮你少走一些弯路。
返回列表