每年到这个时间段总有人私信问我有没有那种能跑通的完整项目源码最好是SpringBootVue的组合后端要Java数据库要MySQL持久层最好是MyBatis——一问一个准基本都是手机销售网站管理系统、商城系统、后台管理系统这类题目。今天就把这套我做过的基于SpringBootVue的手机销售网站管理系统完整梳理一遍从需求拆解、技术选型、数据库设计、前后端实现到部署运行把源码背后那些文档里不会写的思路和踩过的坑一并讲清楚。这套系统不算大但链路是完整的Java后端负责业务逻辑SpringBoot提供接口服务MyBatis处理数据库操作MySQL存数据Vue做前端页面。做毕业设计、课程设计或者刚学完Java Web想看看真实项目长什么样的都可以拿它做参考。文章里我会按实际开发顺序来写——不是按代码目录从上到下讲一遍而是按照为什么这么做、怎么做、碰到什么问题的逻辑来讲。1. 这个项目解决的是什么问题手机销售网站的需求拆解1.1 现实场景里的手机销售到底需要管什么手机销售看起来就是卖手机但落到系统层面实际牵扯的事情比想象中多。你去线下手机店观察一圈就会发现店员要记住各种品牌机型的库存顾客问一句有没有华为Mate系列512G的黑色款就得去翻台账开单之后要记录客户信息、订单金额、提货状态老板月底要统计哪些机型卖得好、哪些积压了库存。这些在Excel里也能做但数据一多查询麻烦、多人协作容易出错、权限也管控不住。这套系统要解决的就是把手机销售的核心业务线上化用户在网站上注册登录、浏览手机、按品牌或价格筛选、搜索机型、加入购物车、下单管理员在后台管理商品上下架、调整库存、查看订单、管理用户。说白了它是一个简化但五脏俱全的B2C商城。做之前一定要明确边界——我们做的是教学演示级系统不需要像京东那样做秒杀、推荐、复杂支付但核心交易链路必须闭环从商品展示到订单生成每一步都有数据落库能查能改。1.2 功能清单怎么定哪些做进去哪些坚决不做功能不是越多越好尤其是毕设或者练手项目范围控制住才能把质量做上去。我当时列功能清单时坚持的原则是用户端一条完整购物链路 管理端一套完整数据维护链路。用户端注册、登录、退出登录。注册时校验用户名唯一性密码加密存储。商品浏览。按品牌分类展示支持关键字搜索、价格区间筛选、按销量或价格排序。商品详情。展示大图、参数型号、内存、颜色、价格、库存量。购物车。加入、修改数量、删除、批量结算。下单与订单管理。确认购物车生成订单、查看订单列表、取消未支付订单。管理端商品管理。新增、编辑、上下架、修改库存、删除。订单管理。查看所有订单、按状态筛选、修改订单状态例如发货。用户管理。查看注册用户列表禁用/启用账号。那段时间很多人问我为什么不做在线支付、不做物流对接。原因很简单真实支付需要商户号、证书、回调地址而且涉及资金安全练手项目用下单即视为支付成功或者货到付款来模拟即可把支付回调接口留出来后面想接再扩展。这也是这类项目最常见的做法不丢人。1.3 项目结构和模块划分系统拆成前台用户端和后台管理端前端用Vue来做通过路由区分页面后端统一提供RESTful接口前端用Axios调用。前后端完全分离后端不关心页面长什么样只返回JSON数据。这种模式现在已经是主流也是SpringBootVue这套组合最典型的落地方式。前端分两个入口思路用户端页面放在/路径下管理端页面放在/admin路径下。后端接口统一以/api开头用户端接口和管理端接口通过路径区分比如/api/user/**和/api/admin/**拦截器按路径判断是否需要管理员权限。这个设计在开发联调时非常省心接口一眼就能看出归属。2. 技术选型不是拍脑袋为什么偏偏是SpringBoot、Vue、MySQL、MyBatis2.1 SpringBoot把SSM时代那些繁琐配置收进抽屉里很多教材还在从SSM讲起但实际做项目时强烈建议直接用SpringBoot。SSMSpringSpringMVCMyBatis本身没问题但配置实在太碎web.xml、spring-mvc.xml、spring-mybatis.xml、applicationContext.xml一个文件没配对项目起不来报错还看不懂。SpringBoot把这些自动配置掉了内嵌Tomcat一个mvn spring-boot:run就能启动。这背后是约定优于配置的思想——你遵循它的默认规则比如配置文件叫application.yml、放在resources目录下它就不需要你写一堆XML。对于做毕设的同学来说最直观的好处是环境的复杂度降低了你可以把精力放在业务代码上而不是和配置死磕。我用它做这个项目时最大的感受是写接口就是写Java方法加个RestController注解返回对象自动序列化成JSON前后端对接非常顺利。2.2 MyBatis为什么不用JPA偏偏选它选MyBatis而不是Spring Data JPA主要有两个原因。第一是SQL可控手机销售这种业务里商品列表要支持关键字、品牌、价格区间、排序方式的动态组合用MyBatis的XML动态SQL写起来非常直观每一段条件都看得见摸得着用JPA的话动态查询要写Specification或者QueryDSL学习曲线陡执行SQL长什么样还得猜。第二是贴近国内企业现状国内Java项目用MyBatis的比例很高面试也爱问学了这个以后工作更容易上手。MyBatis的核心理解起来不难你写接口方法比如ListProduct searchProducts(Param(keyword) String keyword)然后在XML里写对应的SQLMyBatis负责把接口和SQL绑定起来查询结果自动映射到Java对象。不用像JDBC那样手动写ResultSet循环取值也不用像Hibernate那样完全让框架生成SQL。2.3 Vue组件化开发带来的实在好处前端选择Vue看中的是它的组件化开发方式。页面拆成组件后每个组件只负责一件事商品卡片是一个组件购物车列表是一个组件分页器是一个组件。组件之间通过props传数据、通过事件通信思路清晰不会出现一个几百行的HTML文件改一处崩全盘的情况。配合Vue Router做路由管理、Axios做HTTP请求、Element UI做后台管理界面组件库前端部分的开发效率很高。这里有个小建议如果是从零学Vue不要一上来就抱着Vuex/Pinia状态管理不放先搞清楚组件和路由就够了。我们这个项目的购物车状态就不复杂放组件里管理完全够用强行上状态管理反而多一层理解成本。2.4 MySQL的定位稳定可靠的关系型数据底座MySQL在这个项目里负责所有数据的持久化。用户信息、商品信息、订单信息、购物车信息都是结构化数据用关系型数据库存储再合适不过。MySQL的InnoDB存储引擎支持事务和外键约束对订单这种需要保证数据一致性的场景来说非常重要。实际项目里我建议用MySQL 5.7或8.0版本。8.0的窗口函数和性能优化更好但5.7的资料更多、踩坑经验更丰富。这个项目用到的SQL都比较常规两个版本都能跑。需要注意的坑是驱动版本要和数据库版本匹配用8.0数据库就别拿5.x的驱动去连会报SSL连接错误。3. 从业务到表结构数据库建模的完整思考过程3.1 核心表有六张彼此之间的关系要理顺我设计时画了大概十几分钟的草图最终确定六张核心表用户表user、品牌分类表category、商品表product、购物车表cart、订单表orders、订单明细表order_item。表名这里有个小坑order是MySQL的关键字直接用会报语法错误所以用orders。用户表字段字段类型说明idint主键自增usernamevarchar用户名唯一passwordvarchar密码BCrypt加密存储nicknamevarchar昵称roletinyint角色0用户1管理员statustinyint状态0禁用1正常create_timedatetime注册时间商品表核心字段字段类型说明idint主键自增category_idint所属品牌分类namevarchar商品名称imagevarchar商品图片URLpricedecimal(10,2)售价original_pricedecimal(10,2)原价stockint库存数量salesint销量statustinyint上架状态descriptiontext商品描述specsvarchar规格如8GB256GBcolorvarchar颜色订单表的核心字段是订单号、用户ID、总金额、状态。状态我定义成0待支付、1已支付、2已发货、3已完成、4已取消。订单明细表记录每一件商品的快照信息——商品名称、购买时的单价、数量、小计。这里有个容易忽略的设计点订单明细里不能只存商品ID因为商品信息后续会变比如改价、改名如果只存ID历史订单展示时就对不上了。所以要冗余一份商品名称和购买时价格。3.2 表关系和外键约束怎么处理六张表的关系不难理清用户和购物车是一对多商品和购物车是多对一用户和订单是一对多订单和订单明细是一对多商品和订单明细是一对多。外键方面我的建议是建表时可以不建物理外键逻辑外键就够用。原因是在代码里控制数据一致性更灵活而且物理外键在插入、删除时要额外检查对性能有一些影响。比如删除一个分类时你要先检查分类下有没有商品这个检查放在Service层做效果一样还能返回友好的提示信息而不是数据库报错。索引设计上商品表的category_id加索引订单表的user_id加索引这是查询频率最高的两个维度。商品表的name字段如果做模糊搜索数据量小的时候不用管等到数据量大了再考虑全文索引或搜索引擎现阶段没必要过度设计。3.3 初始化SQL脚本里要准备什么一套能直接跑起来的SQL脚本至少包括三部分建库语句、建表语句、初始化数据。初始化数据特别重要没有数据的系统前端页面一片空白根本没法演示。我当时准备了一个管理员账号admin/admin123BCrypt加密存储、一个普通用户账号、6到8部手机商品信息。品牌覆盖华为、小米、苹果、荣耀几个常见分类价格从一千多到六千多不等这样列表页的分页、筛选效果才直观。每个商品配一张图片URL前端直接展示不用本地存图片文件。4. 后端主链路开发登录、商品、购物车、订单是怎么串起来的4.1 后端分层Entity、Mapper、Service、Controller各司其职后端代码严格分层这个习惯非常重要。实体类Entity对应数据库表结构只做数据载体Mapper接口加XML负责SQL操作Service层写业务逻辑比如下单扣库存、注册时校验用户名唯一性Controller层只做参数接收、调用Service、返回结果。这样一层层下来问题定位非常快。Controller返回统一的JSON格式也很重要。我定义了一个Result类包含code、msg、data三个字段。前端收到结果后先看code是不是200是就取data不是就弹错误提示。这套约定前后端联调时会非常顺畅不会出现这次返回成功下次成功结果怎么是一个字符串这种尴尬。登录接口返回的核心信息是用户ID、用户名、昵称和角色。会话保持用Session实现登录成功把用户对象放进Session拦截器里判断Session是否存在来决定是否放行。管理端接口额外检查Session里用户的角色是不是管理员。这种方案在单体应用里够了不需要引入Redis和JWT。想升级的话把Session换token登录接口返回token前端请求头带token后端拦截器解析token思路是一样的。4.2 商品查询的难点动态SQL是怎么帮你省事的商品列表是前端访问量最大的接口也是最需要仔细设计的查询。用户可能按品牌筛选也可能搜索华为还可能设置了价格区间或者想按销量排序——这些条件不是每次都有用MyBatis的动态SQL处理起来非常舒服。select idsearchProducts resultTypecom.phone.entity.Product SELECT * FROM product where if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if /where ORDER BY choose when testsort salessales DESC/when when testsort price_ascprice ASC/when when testsort price_descprice DESC/when otherwisecreate_time DESC/otherwise /choose /selectwhere标签会自动处理掉第一个条件前面的AND这个细节非常实用。如果不用动态SQL你就得在Java代码里拼接SQL字符串写起来繁琐还容易漏空格、漏引号。分页的话直接引入PageHelper插件一行代码PageHelper.startPage(pageNum, pageSize)下面的查询自动带上LIMIT返回结果拿到总条数前端就能渲染分页器了。4.3 下单和扣库存事务是底线不能省购物车里可能有多件商品下单时要一次性完成生成订单主表、生成订单明细、扣减库存、清空购物车这几步操作。任何一步失败都要回滚不能让用户钱付了订单没生成更不能让库存扣了订单却失败。SpringBoot里做事务非常简单Service方法上加Transactional注解就行。但我当时排查过一个小问题事务没生效。原因是自己没有注意自调用——同一个类里的一个方法调用另一个Transactional方法事务不生效必须通过Controller调用Service方法或者把事务方法放到另一个Service类里。这个坑很经典。扣库存的时候还有个并发问题超卖。真正的做法是扣库存SQL写成UPDATE product SET stock stock - 1 WHERE id ? AND stock 0通过数据库的行锁保证库存不为负。如果先SELECT stock再在Java代码里判断并发请求下会出现两人同时买到最后一个库存的情况。这个点面试官也很爱问做项目时顺手就用了正确方案后面讲起来都是加分项。Transactional public Order createOrder(Long userId, ListCartItemVO items) { // 1. 生成订单主表计算总金额 // 2. 遍历明细扣减库存带stock 0条件 // 3. 批量插入订单明细 // 4. 清空该用户购物车 }订单号我用的方案是日期加时间戳加随机数比如20240601153012345。不要用数据库自增ID直接当订单号暴露给用户会让别人猜到你的订单量而且看起来不专业。4.4 密码安全千万别用明文存注册和登录模块里密码存储是安全底线。早期的项目喜欢用MD5但MD5已经被彩虹表破解得差不多了直接存MD5值和存明文差别不大。我用的方案是BCrypt加密SpringSecurity里自带BCryptPasswordEncoder单独抽出来用也行。它的特点是每次加密同一个密码得到的密文都不一样因为内部混入了随机盐验证时用matches方法比对。就算数据库泄露攻击者很难通过密文反推出原始密码。5. 前端Vue工程和后端接口的对接细节5.1 Vite构建Vue项目目录结构怎么组织前端建议用Vite来创建Vue项目npm create vitelatest一条命令搞定相比Webpack启动速度快很多开发体验好不少。目录组织上src/api目录放所有接口请求方法按模块拆文件user.js、product.js、cart.js、order.jssrc/views目录放页面组件比如Home.vue、ProductList.vue、ProductDetail.vue、Cart.vue、OrderList.vuesrc/router目录配置路由。src/api/product.js里这样写import request from ../utils/request export function getProducts(params) { return request({ url: /api/product/list, method: get, params }) }5.2 Axios封装和跨域问题这两个地方最容易卡住Axios是前后端对接的核心工具建议在utils/request.js里统一封装一个实例配置基础URL和拦截器。请求拦截器里带上登录凭证响应拦截器里统一处理错误码比如登录过期就跳转登录页。这样不用在每个页面重复写错误处理代码。跨域问题几乎是所有前后端分离项目都会碰到的坎。开发环境下前端跑在8080端口后端跑在9090端口浏览器会拦截跨域请求。解决办法两个一是后端配置CORS写一个CorsConfig类二是前端配代理Vite的vite.config.js里加server.proxy配置把/api前缀的请求代理到后端地址。我当时用的是前端代理方案好处是生产部署时可以用同源策略不用额外处理跨域。// vite.config.js server: { port: 3000, proxy: { /api: { target: http://localhost:9090, changeOrigin: true } } }5.3 核心页面的交互逻辑怎么落地商品列表页的交互核心是筛选与分页。筛选条件变化时重新调接口分页器切换页码时更新查询参数列表数据放到响应式变量里Vue自动重新渲染。为了方便查询参数我维护成一个对象每次变化后调用一次fetchProducts。购物车页面有个细节值得一说购物车数据是存后端还是存前端我的方案是存后端登录后加入购物车就调接口这样换设备、清缓存数据都还在。购物车列表里的数量加减每次变化调后端更新接口拿到最新的购物车数据刷新页面。谨慎的同学可能会问这样会不会请求太频繁演示项目量级完全没问题。商品详情页从列表页跳转时通过路由传递商品IDthis.$route.params.id取到之后调详情接口。这里不建议在列表页把整个商品对象通过路由传过去用户刷新详情页时参数会丢失必须通过ID再查一次。6. 完整源码跑起来的详细指南与目录说明6.1 后端源码的目录结构拿到源码后第一步不要急着运行先花5分钟把目录结构过一遍。后端项目的结构是phone-sale-backend/ ├── pom.xml └── src/main/java/com/phone/ ├── PhoneSaleApplication.java # 启动类 ├── controller/ # 接口层 ├── service/ # 业务逻辑层 ├── mapper/ # MyBatis接口 ├── entity/ # 实体类 ├── config/ # 配置类跨域、拦截器 └── common/ # 公共类Result、异常处理 src/main/resources/ ├── application.yml # 配置文件 └── mapper/ # MyBatis XML文件看到这个结构任何一个地方报错都能快速定位到对应层。比如接口返回500先看ControllerSQL语法错误看XML文件业务逻辑不对看Service。6.2 从0到1把项目跑起来的每一步第一步准备环境。JDK 1.8或更高版本、Maven 3.6、MySQL 5.7或8.0、Node.js 16以上。这几个版本匹配要注意JDK版本和SpringBoot版本要兼容如果是SpringBoot 2.x系列JDK 8完全没问题如果是SpringBoot 3.x需要JDK 17。第二步导入数据库。用Navicat或命令行执行项目附带的sql/phone_sale.sql执行完检查一下有没有六张表和数据。第三步修改后端配置。打开application.yml改数据库地址、用户名、密码。这里最大的坑是时区设置URL上建议加serverTimezoneAsia/ShanghaiuseSSLfalse不然会报时区错误或SSL连接错误。spring: datasource: url: jdbc:mysql://localhost:3306/phone_sale?serverTimezoneAsia/ShanghaiuseSSLfalsecharacterEncodingutf8 username: root password: 你的密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true第四步启动后端。用IDEA打开Maven项目等依赖下载完运行启动类。看到SpringBoot启动成功的日志后用Postman或者直接浏览器访问http://localhost:9090/api/product/list能返回JSON数据就说明后端通了。第五步启动前端。npm install安装依赖然后npm run dev。如果npm install速度慢可以临时换淘宝镜像源。前端默认跑在3000端口浏览器访问就能看到系统首页。6.3 运行时常见问题速查启动时最常碰到的几个问题我这里直接列出来。端口被占用改配置文件里的端口号MySQL驱动版本不匹配pom.xml里调整mysql-connector-j的版本前端跨域报错检查代理配置接口能通但数据是null检查MyBatis的map-underscore-to-camel-case配置。这些我都遇到过都是配置层面的小问题但排查起来挺费时间提前知道能省半小时。7. 开发过程踩过的坑排查记录比功能代码更值钱7.1 MyBatis返回字段全是null花了一个下午才发现是驼峰映射问题第一次写完商品列表接口兴冲冲跑起来前端展示的商品名称、价格这些字段全是null仔细看数据库明明有数据。后来翻日志才发现SQL执行没问题问题出在结果映射上——数据库字段category_id是下划线风格实体类属性categoryId是驼峰风格MyBatis默认不会自动转换。解决办法很简单在application.yml里加一行map-underscore-to-camel-case: true或者用Results注解手动映射。我后来建议身边的同学都提前加上这个配置免得踩同样的坑。7.2 前端页面时间显示成时间戳后端没配置JSON格式订单列表页面展示下单时间时显示一串数字时间戳很难看。原因是后端返回的Date对象序列化成JSON时默认变成长整型。解决办法是在SpringBoot配置文件的jackson配置里指定日期格式。spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这个配置加完之后所有接口返回的时间字段自动变成可读格式不用在每个实体类字段上加JsonFormat注解。7.3 分类删除失败报外键约束错误开发管理端删除分类功能时测试删一个还有商品的分类MyBatis执行DELETE时报了外键约束错误。虽然建表时没加物理外键但MyBatis的SQL里写了DELETE FROM category WHERE id ?而product表里还有category_id引用它数据库层面报错。我的处理思路是在Service层做业务校验删除分类前先查一下该分类下有没有商品有商品就返回提示该分类下还有商品请先处理商品。数据库报错对用户太不友好业务层要把错误转换成看得懂的提示。7.4 关于复制源码的正确姿势拿到一套源码最忌讳的事情是打开就跑、跑通就改、改坏就懵。正确的姿势是先把项目跑起来走一遍用户买手机的完整流程然后停下来画一张请求路径图从点击按钮开始前端调了什么接口、后端执行了什么方法、SQL查了哪张表最后再动手改需求。比如想把手机销售改成图书销售把商品字段调整一下、初始化数据换一批就行核心逻辑完全不需要动。我见过太多人上来就想删代码重写结果越改越乱。实际上这套系统的价值不在代码多复杂而在于它展示了一个典型全栈项目的完整结构——你把这个结构吃透了换成任何一个业务场景都能套上去。做这个项目的过程中我最大的体会是前后端分离项目的难点不在单个技术而在串联。数据库字段怎么设计的、接口返回什么格式、前端怎么取数、异常怎么传递这些约定在动手前定好开发时就会非常顺畅。如果只盯着某一个技术点死磕很容易陷入局部忘了整条链路。后期如果想继续扩展可以往这个方向走把购物车和Session换掉引入Redis做缓存和Token登录商品搜索加Elasticsearch订单状态机完善一下加上支付回调接口。但只要核心链路跑通了这个项目的骨架就已经真正属于你了。
阅读完成 · 觉得有帮助?