资讯详情

手机商城系统实战:SpringBoot+Vue前后端分离从数据库到联调

📅 2026/10/22 8:33:21 | 华诺云谱 👁 阅读
手机商城系统实战:SpringBoot+Vue前后端分离从数据库到联调
又有人问我练手项目该怎么选了。上周一位读者发消息说自己简历上写了熟悉SpringBoot、熟悉Vue但被问到完整项目的业务闭环时支支吾吾讲不出个所以然。我给他推荐了一套手机商城系统技术栈正好是SpringBootVue前后端分离持久层用MyBatis数据库用MySQL——这套组合在2025年依然是国内市场占有率最高的Web开发配置之一。今天我就以这个名为“欢迪迈”的手机商城项目为例把一套商城系统从设计到落地、从数据库到前后端联调、再到最后跑起来的完整链路拆开讲一遍。无论你是准备毕设、想补项目经验还是第一次接触前后端分离项目这篇都值得你花二十分钟看完。先提前说一句项目名“欢迪迈”并不重要你可以把它理解成一个虚构的手机电商品牌。重要的是它背后的系统设计思路是可以直接复用到任何商城类项目里的。用户注册登录、商品浏览、购物车、下单、订单管理这些东西听着普通但真做起来每一步都有讲究。接下来我按我实际开发这类项目时的思考顺序一层一层给你讲透。1. 为什么练手项目我首推手机商城一套系统覆盖电商全链路1.1 这个系统到底能做什么先给还没见过商城源码的同学一个整体印象欢迪迈手机商城这类项目通常包含两个端的应用一个面向普通消费者一个面向运营管理员。消费者端做的事情很直观注册账号、登录、在首页看推荐商品、按分类浏览手机、在搜索框里搜机型、点进商品详情页看大图和参数、把商品加入购物车、勾选购物车里的商品生成订单、填写收货地址、然后模拟支付一般没有真实支付只是把订单状态从“待付款”改成“待发货”。部分做得细的还会有个人中心里的订单列表、取消订单、确认收货。管理端则是另一套界面登录后台之后管理员可以维护商品分类可以新增编辑手机商品信息包括手机名称、品牌、颜色、存储版本、价格、库存、上下架状态、商品主图可以处理订单把已付款的订单标记为发货可以管理注册用户比如禁用某个异常账号。有些版本还带数据统计看板展示每天的订单量和销售额。1.2 它不只是“增删改查”那么简单很多人对这类项目的第一反应是这不就是增删改查吗这话对也不对。表面看确实是各种表的新增、删除、修改、查询但把这些操作串起来的业务逻辑才是真正值钱的部分。举一个最简单的例子下单这个动作。用户从前端点了“提交订单”后端要做的事情至少包括查询商品当前价格、校验库存是否足够、扣减库存、生成订单主表记录、生成订单明细记录、清空购物车里对应的商品。如果哪个环节失败了前面做的一半操作必须全部撤销否则就会出现“库存扣了但订单没生成”的脏数据。这种连贯的业务操作才是项目中真正需要动脑子的地方。所以我的建议一直很明确商城系统是练手项目里的“黄金标准”因为它的功能复杂度刚好卡在一个很好的位置——太少学不到东西太多又会让新手直接放弃。一套做下来前端、后端、数据库、接口设计、部署全部过了一遍写在简历上也比“学生管理系统”有说服力得多。2. 2025年的技术栈怎么选SpringBoot、Vue、MyBatis、MySQL各司其职2.1 后端SpringBoot把复杂度藏起来把业务露出来技术选型这件事得先说清楚每个组件到底解决了什么问题别稀里糊涂套上去。SpringBoot在后端里的角色是“基础设施提供者”。以前的SSM项目光配置Spring、SpringMVC就能折腾好几天各种XML配置、依赖版本冲突把人耐心全磨没了。SpringBoot用一套自动配置机制把这些东西都收拾干净了你可以把全部精力放在控制器怎么写、业务逻辑怎么组织、数据怎么存储这些真正要紧的地方。选SpringBoot而不是别的框架还有个很现实的原因市场认可度。国内中小公司的Java岗位SpringBoot基本是标配招人看简历第一个就是看这个。用这套技术栈练手学的每一样东西出来后都用得上。2.2 持久层MyBatis为什么比JPA更被国内项目偏爱持久层这块我见过很多人在MyBatis和JPA之间犹豫。欢迪迈手机商城这类项目用的是MyBatis是有道理的。JPA走的是“全自动”路线你定义好实体类框架自动帮你生成SQL。而MyBatis走的是“半自动”路线SQL自己写框架只负责帮你把参数传进去、把结果集映射成对象。这两种风格没有绝对的好坏但在国内电商类项目里SQL基本都是高度定制化的比如商品列表要按价格排序、按分类过滤、还要做模糊搜索这种复杂查询用MyBatis的XML文件写起来非常直观出了问题也好排查。另外一个原因是团队协作。MyBatis的SQL是显式写出来的负责数据库的人可以直接看XML文件审核SQL效率而JPA在复杂查询场景下生成的SQL有时会超出你的预期性能问题很难定位。这也是为什么很多老项目的持久层一直在用MyBatis没动过。2.3 数据库MySQL承担了什么MySQL在整套系统里的角色就是“最终真相”。所有商品信息、用户信息、订单记录最后都得落到它里面。选择MySQL没什么可争议的开源免费、社区成熟、遇到问题一搜就有答案。对于这套体量的商城项目InnoDB存储引擎加事务支持完全够用。实际开发中要注意的倒是另一件事字符集必须用utf8mb4这样才能正确存储用户输入的生僻字和Emoji建表字段要用合理的类型比如价格别用float老老实实用DECIMAL(10,2)不然会出现0.1加0.2不等于0.3这种经典的浮点数误差问题。2.4 前端Vue的生态决定了开发效率前端选Vue最大的理由是其生态在国内太成熟了。配一个Element UI或者Element Plus组件库后台管理页面几乎不需要自己从头写样式表格、表单、弹窗、消息提示全都是现成的组件拖上去就能用。商城用户端可以用Vue Router做页面路由用Vuex或Pinia管理用户登录状态再配一个axios封装前后端联调的工作量能肉眼可见地降下来。如果你拿到的源码还停留在Vue 2也不用慌核心阅读思路是一样的无非是把生命周期和部分API的写法换成新版。层级技术选型在这套系统里负责的事前端框架Vue页面渲染、组件化、路由控制UI组件库Element UI / Element Plus后台表格、表单、弹窗、消息提示后端框架SpringBoot提供接口、处理业务逻辑、管理Bean持久层MyBatis编写SQL、结果映射数据库MySQL持久化存储业务数据3. 功能模块拆解之前先想清楚商城系统有哪些角色在做哪些事3.1 用户端从注册到下单的一条主链路我在梳理功能的时候习惯先找一条“主链路”也就是用户使用这个系统时最核心的一条路径。对于商城来说这条路径就是注册登录 → 浏览商品 → 加购物车 → 提交订单 → 支付 → 确认收货。这条链路走通这个项目就已经算完成百分之七八十了。围绕它再拓展出来的都是辅助功能注册要先校验用户名是否重复登录成功后前端要保存token浏览商品要区分分类和搜索购物车要支持数量修改和删除订单要有状态的变化流转。很多新手写项目时习惯拿到需求就开写结果写到一半发现某个表漏了字段、某个接口设计得挑三拣四来回返工。我建议你先画一张简单的功能清单不需要多美观把“谁用什么角色、执行什么操作、操作哪个数据”写清楚就行。3.2 管理端商品、订单、用户三大后台管理端的核心就三个大模块。商品管理管理员维护分类一个分类下挂着若干手机商品。商品本身有上下架状态下架的商品用户端是看不到的。商品信息在后台以表格展示支持关键词搜索、按分类筛选。订单管理管理员查看所有订单按状态筛选订单处理“发货”操作。这一步会修改订单状态从“已付款”变成“已发货”同时记录发货时间。这里还涉及权限控制普通用户只能看自己的订单管理员能看到全部订单改状态的操作只能管理员做。用户管理查看注册用户列表支持禁用账号。被禁用的用户登录时会被拦截。这个模块虽然代码量不大但让系统看起来更完整——项目答辩时面试官最喜欢问的就是“你怎么处理用户权限”这个模块就是最好的答案。3.3 权限与登录JWT方案为什么在这个场景是首选前后端分离项目里登录状态不能依赖传统的Session和Cookie因为前端跑在8080端口后端跑在8081端口跨域情况下Cookie不好使。所以欢迪迈这类项目普遍采用JWT方案。JWT做的事情可以这样理解用户登录成功后后端生成一串加密的字符串里面包含用户ID、用户名、过期时间等关键信息然后把这串字符串返回给前端。前端拿到后存进localStorage之后每次请求都在HTTP头里带上它。后端每次收到请求先从字符串里取出用户信息校验有效期和签名通过就放行不通过就返回401。这个方案的好处是无状态后端不用存Session集群部署时也不用考虑Session共享问题。我在项目里一般用一个拦截器统一解析token把当前登录用户信息放进一个ThreadLocal变量里这样任何Service层方法里都能方便地拿到“当前是谁在操作”。4. 数据库设计是这整套系统最容易返工的部分4.1 六张核心表字段不是随便定的数据库设计这一步几乎所有做过电商项目的人都有过返工经历。我先说结论再用欢迪迈项目的表结构来验证。一个标准商城系统至少需要六张核心表用户表、商品分类表、商品表、购物车表、订单表、订单明细表。如果要做收货地址管理还可以加一张地址表如果考虑手机的颜色、存储版本这类规格就要考虑SPU和SKU的拆分。用户表通常包含这些字段用户名、密码存的是加密后的密文绝不可能明文、昵称、头像地址、手机号、状态正常或禁用、创建时间。密码加密我推荐用BCrypt同一密码每次生成的密文都不同安全性比MD5和SHA好不少。商品分类表比较简单就三个核心字段分类名称、上级分类ID用于做一级二级分类、排序值。商品表的字段设计就要结合手机这个品类的特点了比如品牌、型号、机身颜色、存储容量这些都可以单独作为字段存储方便做筛选。关键字段就是价格DECIMAL(10,2)、库存INT、销量INT、上下架状态TINYINT/INT。我见过最典型的新手错误是让商品表用一个redundant字段把所有参数拼成字符串塞进去比如颜色:黑色;存储:128G;内存:8G。这样做当时省事后面做筛选、统计订单数据时会非常痛苦。4.2 订单相关表快照的设计思路订单主表和订单明细表是整套数据库设计里最见功力的一部分。先看订单主表核心字段有订单号、用户ID、订单总金额、订单状态、收货人姓名、收货人电话、收货地址、下单时间、支付时间、发货时间、完成时间。订单号建议不要用自增ID直接展示给用户而是用时间戳加随机数或用户ID生成一串业务号比如ORD202501151530001234。这样做既避免暴露系统订单总量也让订单号看起来更像真实的电商订单。订单明细表要和商品表做关联但有个关键点明细表里的商品名称、商品图片、商品价格必须是下单那一刻的“快照”不能直接去商品表关联查询。为什么因为用户下单三个月后再来看订单商品可能已经下架、改名、涨价了如果动态关联商品表历史订单显示的价格就全乱了。正确做法是下单时把商品名称、单价、图片都复制一份存进订单明细表。4.3 金额与状态字段的取舍金额字段国内项目有两种流派一种用DECIMAL比如DECIMAL(10,2)简单直观另一种把所有金额换算成“分”存成整数比如99.9元存9990。这两种方案都能用但同一个项目里必须统一千万别一会儿用元一会儿用分。订单状态字段我用过字符串和整数两种方案最终更推荐整数。一张订单的状态流转是固定的待付款、待发货、待收货、已完成、已取消。我在代码里会定义一个订单状态枚举比如0表示待付款、1表示待发货、2表示待收货、3表示已完成、4表示已取消枚举里定义好状态名称和描述代码里引用枚举数据库里只存数字。这里还有一个容易踩的坑取消订单的语义要设计清楚。待付款订单用户可以取消已付款订单一般不允许用户直接取消只能走申请退款流程。毕设项目通常简化掉退款流程但你至少要明确“什么时候能取消”这个判断逻辑。5. 后端落地MyBatis写SQL的时候我建议你注意这几个地方5.1 三层架构一个请求的完整路径后端代码的组织方式主流是Controller、Service、Mapper三层。一个请求进来之后的路径是这样的前端请求打到ControllerController负责接收参数和做参数校验然后调用Service把参数带到业务方法里Service处理业务逻辑比如校验、计算、判断状态需要数据时调用MapperMapper通过MyBatis执行SQL从MySQL取回数据再一层层返回给前端。分层最重要的价值是隔离变化。比如某个SQL写复杂了查询很慢你只需要优化Mapper里的SQL不需要动Controller和Service的代码。再比如多个地方都要用订单状态判断逻辑把它写在Service层统一复用比散落在各个Controller里强多了。5.2 动态SQL商品列表查询是怎么做出来的欢迪迈这类手机商城的商品列表查询条件通常是变化的用户可能选择了某个分类可能输入了关键词还可能按价格排序。这种需求写SQL时不能写死必须用MyBatis的动态SQL。看这段XMLselect idselectProductPage resultTypecom.mall.entity.Product SELECT id, category_id, name, price, stock, main_image, status FROM mall_product where if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if AND status 1 /where ORDER BY id DESC /selectwhere标签会自动处理多余的ANDif标签实现条件拼接。这样写的好处是有分类按分类查有关键词做模糊搜索都没有就返回全部在售商品同一个Mapper方法应对多种场景不用为每种条件组合单独写一个方法。分页这块原生MyBatis通常配合PageHelper插件使用。PageHelper的原理是在你执行查询前自动把SQL改写成带LIMIT的分页SQL同时再去查一条COUNT统计总条数。你只需要在Service层写PageHelper.startPage(pageNum, pageSize)紧接着的Mapper查询就会被自动分页。5.3 事务是下单逻辑的生命线刚才我强调过的下单流程在代码层面必须用事务包住。SpringBoot里做事务很简单Service方法上加上Transactional注解即可Transactional public Long createOrder(OrderCreateRequest request) { Product product productMapper.findById(request.getProductId()); if (product null) { throw new BusinessException(商品不存在); } if (product.getStock() request.getQuantity()) { throw new BusinessException(库存不足); } int rows productMapper.deductStock(product.getId(), request.getQuantity()); if (rows 0) { throw new BusinessException(扣减库存失败请重试); } // 创建订单主表记录 // 创建订单明细记录 // 清空购物车中该商品 return order.getId(); }这个注解做了什么方法执行前开启数据库事务方法正常执行完提交事务方法抛出任何RuntimeException就回滚整个事务。也就是说只要任何一步抛异常前面扣掉的库存、插入的订单记录全部撤销数据库回到调用前的状态。细心的话会发现我写了deductStock这个方法而且特别注意了它返回的影响行数。这是因为并发场景下两个用户同时下单可能都读到库存为1如果不加条件扣减库存可能变成负数。这个SQL写法是这样的UPDATE mall_product SET stock stock - 1 WHERE id #{id} AND stock 0影响行数为0就说明库存已经不足事务随即回滚。这种“乐观扣减”的方式比先查再扣更安全也是我自己在项目里一定会注意的细节。5.4 统一返回结构与其他公共设施前后端对接时接口返回的数据格式最好统一否则前端每个接口都要单独处理返回格式麻烦不说还容易出bug。常见的统一返回结构是这样的public class ResultT { private Integer code; // 200表示成功401表示未登录500表示业务错误 private String msg; // 提示信息 private T data; // 实际数据 }前端axios收到后先看code等于200就取data不等于200就弹msg提示用户。这比让前端判断HTTP状态码靠谱因为HTTP层面的200只代表网络通了不代表业务一定成功。比如扣库存失败HTTP状态照样是200但返回体里的code可能是500。除了统一返回体还可以在Controller层做统一异常处理。用一个RestControllerAdvice注解的类捕获所有Service层抛出的业务异常统一转成Result返回。这样Service层可以放心地throw new BusinessException(库存不足)不用每个方法里都写try-catch。6. 前端 Vue 联调从页面骨架到接口对接的完整过程6.1 路由和状态管理登录态怎么守住前端这部分我按欢迪迈项目实际用到的技术来拆解。Vue工程创建后第一件事是配置路由。用户端页面和管理端页面通常分别放在不同的路由前缀下比如/开头的是商城页面/admin开头的是后台页面。路由守卫是前端守登录态的第一道防线router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })这段代码的意思凡是标记了requiresAuth的页面都必须有token才能访问没有就去登录页。但是要注意前端的路由守卫只能防君子不能防小人真正的权限校验必须以后端为准。也就是说后端每个需要登录的接口都要在拦截器里验证token双保险。6.2 axios 封装请求和响应拦截器axios的封装水平直接决定前后端联调时舒服不舒服。我一般在项目里建一个request.js做两件事请求拦截器自动带token响应拦截器统一处理业务码。axios.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) axios.interceptors.response.use(resp { const { code, msg, data } resp.data if (code 401) { localStorage.removeItem(token) location.href /login return Promise.reject(msg) } if (code ! 200) { Message.warning(msg) return Promise.reject(msg) } return data })加了这套逻辑之后业务代码里调用接口就变得非常清爽const res await getProductList(params)拿到的res直接就是业务数据不用每次都在页面里判断code。6.3 管理端页面表格表单弹窗的标准套路管理端的开发有了Element UI系列组件库之后效率是成倍提升的。商品管理页面的做法几乎是固定的套路页面上方放搜索栏中间放数据表格再放一个分页组件。点“新增”按钮弹出表单弹窗提交时调新增接口成功后刷新表格数据。以商品管理为例组件结构大致是el-table :dataproductList border el-table-column propname label商品名称 / el-table-column propprice label价格 / el-table-column propstock label库存 / el-table-column label状态 template #defaultscope el-tag :typescope.row.status 1 ? success : info {{ scope.row.status 1 ? 在售 : 下架 }} /el-tag /template /el-table-column el-table-column label操作 template #defaultscope el-button clickopenEditDialog(scope.row)编辑/el-button /template /el-table-column /el-table这里可以学到的一个重要思想是“视图和数据分离”。表格只负责展示数据操作按钮触发的只是事件具体的数据变更全部走接口交给后端。前端不要自作主张去修改商品状态应该调用后端接口拿到最新的数据再刷新页面。6.4 联调阶段的高频问题前后端联调最常遇到的问题有三个。第一个是跨域。开发环境下前端在8080端口后端在8081端口浏览器会拦截跨域请求。解决方案是在后端的WebMvcConfigurer里配置允许跨域更推荐的做法是在前端的Vite或Vue CLI配置文件里设置proxy代理让前端把/api开头的请求转发到后端地址从根上避开跨域问题。第二个是字段名不一致。后端习惯用驼峰命名productName前端也习惯用驼峰但如果后端返回的字段是下划线风格product_name而前端没有配置转换拿到的数据就是undefined。MyBatis可以设置map-underscore-to-camel-casetrue或在SQL里用别名让它回归驼峰。第三个是时间格式。后端返回的日期默认可能是时间戳或带T的ISO格式前端如果直接用字符串展示会很难看。要么后端在Jackson配置里统一格式化时间要么前端写一个格式化函数处理。总之前后端一开始就要约定好格式别等联调时才发现两边对时间的理解不在一个频道上。7. 拿到源码到跑起来环境配置和最容易卡住人的部署细节7.1 环境清单与版本搭配不管你是自己从零写还是拿到一份欢迪迈类似的项目源码第一步都是把环境补齐。2025年这个时间点我建议的搭配是这样的组件推荐版本说明JDK8或17有些老源码必须用8新写的用17Maven3.6及以上管理后端依赖MySQL5.7或8.08.0需要配置时区Node.js14及以上跑前端工程前端依赖按package.json建议用npm install个别老项目要加--legacy-peer-deps这份清单有一个容易踩的坑JDK版本和SpringBoot版本不匹配。SpringBoot 3.x要求JDK17如果你用了JDK8还硬跑SpringBoot 3.x编译直接报错。拿到源码第一步永远是把pom.xml打开看一眼确认SpringBoot的版本号再从本地JDK清单里选合适的。7.2 启动顺序先数据库再后端最后前端拿到源码之后的运行顺序我的经验是这样的。先把MySQL跑起来用Navicat或命令行执行项目里的sql文件把数据库和表建好。执行的时候注意如果你用的MySQL是8.0以上版本记得检查SQL文件开头的字符集设置尽量改成utf8mb4避免中文乱码。然后配置后端。打开application.yml或application.properties修改三样东西数据库连接地址、数据库账号密码、文件上传保存路径。数据库地址最容易写错localhost:3306里面的3306如果被本机占用改过端口这里必须同步改。后端启动成功后再启动前端。在项目目录下执行npm install这个步骤可能耗时较长然后执行npm run serve或npm run dev。前端起来了浏览器访问前端地址先不要急着点功能打开浏览器开发者工具的网络面板随便触发一个接口看看请求能不能正确到达后端。7.3 部署时的常见坑最后说几个我在部署环节至少遇到过一次、而且一查就是几小时的问题。MySQL连接报时区错误。错误信息类似于The server time zone value Öйú±ê׼ʱ¼ä is unrecognized原因是MySQL 8.0之后时区配置变了。解决办法是在JDBC连接串里加上serverTimezoneAsia/Shanghaijdbc:mysql://localhost:3306/mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai上传的图片前端访问不到。商品图片上传成功后明明能打开但前端页面显示不出来大概率是后端没有做静态资源映射。SpringBoot默认只处理请求路径到Controller/upload/这种路径不会直接返回磁盘上的文件。需要自己加一个配置Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file:D:/upload/); }这里的D:/upload/要替换成你自己配置的上传目录。很多项目源码里图片上传功能写了但这个映射配置漏了或者配置路径和实际保存路径不一致。端口冲突。后端默认8080端口如果被本机其他程序占用了启动会直接失败。这时候改application.yml里的server.port改完记得前端代理配置里的目标地址也要同步改不然前端转发还是打到老的8080上去。还有就是数据库初始化MySQL8的认证插件问题caching_sha2_password老驱动版本不认识会报认证失败。解决方式有二选一驱动换成mysql-connector-java8.0以上版本或者建用户时指定mysql_native_password。最后分享一点我自己的调试习惯遇到报错先别急着问人把报错日志里那行Caused by找出来。后端日志是一层一层的最下面那个Caused by才是真正的根因。前端的报错则优先看Network面板如果接口返回了看返回体的code和msg如果接口没返回看请求URL是不是405、404。把这两个基本功练好百分之八九十的bug都能自己解决掉。这套手机商城的思路核心就一句话先想清楚链路再设计数据结构然后层层实现。拿到任意一份类似源码先看数据库脚本、再看接口列表、最后看前端页面项目很快就能吃透。以后无论是要在这个基础上加秒杀、加优惠券、加评论系统你都知道往哪里加、改哪些表、动哪些接口。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑