
前后端分离的电商项目我这些年带学生、自己练手加起来做过不下五个版本最常被问到、也最适合拿来完整讲清楚的就是这套基于 SpringBoot Vue MyBatis MySQL 的米家商城模仿米家生态链商品风格的 B2C 商城项目。它不算新潮却是目前就业市场里出现频率最高的技术组合之一也是把 Java 后端、前端工程化和数据库设计串成闭环的经典样本。这套项目做的事情很明确用户端能注册登录、逛商品、加购、下单、模拟支付运营端也就是项目里叫 ABO 系统的那套后台能维护商品分类、上下架商品、处理订单和用户。它不是什么高并发秒杀系统但电商主链路完整接口设计、状态流转、权限控制这些核心环节都踩到了。源码是完整的一套数据库脚本、前后端工程、部署文档都齐适合两类人一是刚学完 Spring Boot 和 Vue 基础、想通过完整项目把技术串起来的人二是准备毕业设计或者求职项目、需要一个可演示可扩展的商城系统的人。但话说在前面项目能跑通只是起点真正值钱的是你理解了多少“为什么”。这篇东西我就按从 0 到 1 的设计思路、数据库、后端、前端、部署到排错一条线讲透。1. 项目定位与整体架构设计思路1.1 为什么偏偏是 SpringBoot Vue MyBatis MySQL 这套组合先说结论这套组合不是技术选型里最优的但它是就业市场里最大公约数。你打开招聘网站搜 Java 后端或者全栈开发Spring Boot 出现概率极高搜前端岗位Vue 在国内中小团队里普及率依然靠前MyBatis 和 MySQL 更是老搭档。用一套大家都在用的技术栈做项目别人面试时就不用先花十分钟理解你的冷门框架沟通成本低很多。从学习角度讲这套组合也足够顺滑。Spring Boot 默认内嵌 Tomcat不用单独配置外部容器一个 main 方法就能把后端拉起来MyBatis 是半自动 ORMSQL 自己写虽然比 MyBatis-Plus 这类全自动工具麻烦但恰恰因为这个“麻烦”你能把 SQL 执行过程、参数映射、结果集映射这些底层逻辑弄明白Vue 组件化开发让前端不再是一堆零散页面而是可以把头部、商品卡片、购物车项这些公共部分拆出来复用。相比之下如果你一上来就搞 Spring Cloud 微服务、Redis、RabbitMQ 全套光环境搭建就能劝退大半人主线业务反而没精力写。这里多说一句经常有人问“为什么不直接用若依这类现成框架”。若依确实把权限、代码生成、用户管理都封装好了但它是给“快速交付内部管理系统”准备的如果你是为了学商城业务、理解前后端交互从零搭建反而能逼你走一遍完整流程。我通常建议可以拿若依做参考但不要上来就套壳否则出了问题连日志都不知道去哪看。1.2 ABO系统在米家商城项目里扮演什么角色看到项目名里的“abo”有人会往奇怪的方向想其实它在这个项目里就是后台管理系统的代号你可以理解为 Admin Back Office即管理后台。为什么单独给它起个名字因为商城项目天然是双端的面向 C 端用户的商城页面负责浏览、下单、支付面向 B/O 端运营人员的 ABO 系统负责商品、分类、订单管理两者共用同一套后端 API只是前端路由和页面权限不同。这种命名的实际好处是沟通方便。比如你们团队说“改一下 ABO 的商品列表”所有人都知道改的是管理后台说“用户商城看不到这个按钮”就知道是 C 端的问题。很多商业项目里也会用类似叫法区分端。ABO 系统在这个项目里承担的具体工作包括管理员登录、商品分类维护、商品上下架与库存调整、订单列表和状态修改、用户信息管理。它不需要太炫酷的页面但 CRUD 稳定、权限清晰是底线。1.3 前后端分离的职责边界到底怎么划前后端分离这个词被说了很多年但很多项目其实只是“物理分离、逻辑没分”。比如后端模板里还藏着 HTML或者前端页面里直接拼 SQL这些都是没划清责任田的表现。在这个项目里边界是硬性的后端只输出 JSON所有页面渲染都由 Vue 负责前端所有数据都通过 HTTP 接口获取绝不直连数据库认证信息通过 Token 在请求头传递后端通过拦截器统一校验。具体到目录结构上后端工程里没有任何 JSP、FreeMarker、Thymeleaf 这类视图模板controller 层只返回 Result 对象前端工程里所有数据请求都走统一封装的 Axios 实例页面组件里看不到裸的 fetch 或者 Ajax 调用。这样做的好处是前后端可以并行开发前端用 mock 数据就能先把页面搭起来后端用 Postman 就能把接口测完最后联调时只要对字段名和状态码就可以了。2. 数据库设计商城核心表结构与 MyBatis 落地细节2.1 九张核心表设计与关联关系商城系统的数据库设计不会太复杂但每张表都有讲究。我这个项目里一共 9 张业务表用户表、收货地址表、商品分类表、商品表、购物车表、订单表、订单明细表、支付流水表和管理员表。分类表我做成自关联的parent_id 为 0 表示一级分类其余挂在父分类下面这样可以支持“手机数码 - 手机”这种两级甚至多级分类前端做 tab 联动时很自然。商品表是整个系统的信息中枢字段包括 title、subtitle、main_image、detail富文本详情、price、stock、sales、status。这里有两个关键点price 用 decimal(10,2) 而不是 float/double避免浮点精度问题status 用 tinyint 表示上下架状态1 为上架、0 为下架。购物车表需要同时存 user_id 和 product_id再加一个 quantity 和 checked 字段checked 用来记录用户是否勾选该商品参与结算这样刷新页面后购物车选中状态也不会丢。订单表和订单明细表是核心链路的两张表。订单表存订单号和总金额订单明细表存商品维度的快照。管理员表独立于用户表权限级别完全分开防止有人拿普通用户账号去后台搞事情。这 9 张表的关系落到 MyBatis 里通常是一个 mapper 对应一张主表再通过嵌套 ResultMap 或者关联查询组装出聚合对象比如查询订单详情时需要把订单头、明细列表、地址信息一次查出来。2.2 订单明细为什么要存“商品快照”这是个非常经典的设计点。商品表里的名称、图片、价格是“活数据”运营管理员随时可能改价、改标题、下架商品。如果订单明细只存一个 product_id后续再查订单时去关联商品表拿信息就会面临一个问题用户下单时商品是 3999 元一周后运营把价格改成 4299 元这时候订单却跟着显示 4299 元财务和客服绝对会疯掉。所以订单明细表里必须冗余一份商品快照下单那一刻的商品标题、商品主图、单价、数量原样拷贝进来。之后不管商品表怎么变订单明细始终是当时的真实情况。这个做法在订单售后、对账、统计报表里特别重要。顺带一提同步库存时只更新商品表的 stock不更新明细里的快照因为明细是历史事实不能动。2.3 MyBatis 动态 SQL、TypeHandler 与缓存使用讲究MyBatis 在这个项目里最常用的能力是动态 SQL。商品列表页需要多条件筛选按分类、按价格区间、按关键字还要分页。如果每个条件都写一个接口那组合条件会爆炸。正确做法是用动态 SQL 拼 SQL比如select idselectProductList resultTypecom.mall.entity.Product SELECT id, title, subtitle, price, main_image, sales, stock FROM product where if testcategoryId ! null AND category_id #{categoryId} /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR subtitle LIKE CONCAT(%, #{keyword}, %)) /if AND status 1 /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /select其中 和 是 XML 里对大于号小于号的转义新手经常在这里翻车直接写 会报 XML 解析错误。分页我建议手写 LIMIT不要图省事把所有数据查回来在 Java 内存里分页数据量一大内存就撑不住。TypeHandler 平时用得不多但商品图片列表这种 JSON 字段会用到。它做的事就是 Java 类型和 JDBC 类型之间的双向转换写入时把 List 转成 JSON 字符串塞进数据库列读取时把 JSON 字符串转回 List 。自定义 TypeHandler 需要继承 BaseTypeHandler在 setNonNullParameter 里做序列化在 getNullableResult 里做反序列化然后在 MyBatis 配置里注册或者字段上指定。它工作的完整流程是ORM 拿到 Java 对象的属性值调用 TypeHandler 的 setParameter 转成 JDBC 能识别的类型执行 SQL查询时 JDBC 返回的 ResultSet 再经过 TypeHandler 的 getResult 转回 Java 对象。MyBatis 缓存这块要非常谨慎。一级缓存默认开启作用范围是同一个 SqlSession说白了就是同一次会话里重复查询相同 SQL 会直接走缓存。二级缓存作用范围是 namespace 级别需要在 mapper XML 里配置cache evictionLRU flushInterval60000 size512 readOnlytrue/但电商项目数据变动极其频繁商品价格、库存一旦更新缓存里的旧数据就会造成脏读。我的建议是项目里只依赖数据库本身MyBatis 二级缓存不作为业务正确性的依赖顶多给商品分类这种极少变化的字典数据开一下。面试时能说清楚“一级缓存是 SqlSession 级、二级缓存是 namespace 级、为什么电商场景对缓存要谨慎”比单纯背概念管用得多。2.4 MySQL 初始化、字符集与连接池配置数据库脚本建议直接用一个 mall.sql 文件把建库、建表、初始数据都放在一起。创建表时字符集要统一指定 utf8mb4因为它能存 emoji 和生僻字比 utf8 更稳。一个常见的默认值问题商品 status 字段如果没给值希望默认是 0下架可以用ALTER TABLE product ALTER COLUMN status SET DEFAULT 0这样后台添加商品时忘了选上下架它也不会默认上架。连接池我用的是 Spring Boot 2.x 默认的 HikariCP性能好配置也简单。application.yml 里的关键配置是spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mall?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 hikari: maximum-pool-size: 10 minimum-idle: 5 connection-timeout: 30000这里有两个高频坑。第一个驱动类名。MySQL 5.x 用 com.mysql.jdbc.DriverMySQL 8.x 必须换成 com.mysql.cj.jdbc.Driver写错启动直接报 ClassNotFound。第二个时区。MySQL 8.x 默认时区跟本地不一致不指定 serverTimezone 会报 “The server time zone value” 的异常useSSLfalse 则能避免本地开发时 SSL 握手失败的问题部署到生产再根据实际需要开启 SSL。3. 后端实现登录鉴权、商品查询到订单流转3.1 分层结构与统一返回结果约定后端工程我按 controller、service、mapper 三层来拆另外单独建了几个支撑包common 放统一返回结果和常量config 放跨域和拦截器配置utils 放 JWT 等工具类exception 放全局异常处理器。分层的核心目的是让每一层只干一件事controller 收参数、service 写业务、mapper 操作数据库。层与层之间传递数据的格式必须提前定死。我在项目里统一用 Result 对象public class ResultT { private Integer code; private String message; private T data; }约定 code200 表示成功500 表示业务失败401/10001 表示未登录或 Token 失效。前端 Axios 的响应拦截器拿到响应后先看 code不是 200 就弹出 message。这个约定必须在动手写第一个接口前定好否则前后端各写各的联调时全在扯皮。全局异常处理器也要跟上Service 层抛出的自定义异常要能被统一捕获并转成 Result 返回否则代码里到处 try-catch业务逻辑会糊成一团。3.2 基于 JWT 的登录认证与拦截器设计登录认证我用 JWT而不是 Session。原因很简单前后端分离后后端接口是无状态的Token 放在请求头里跟着走就行不需要服务器保存会话状态也不受跨域 Cookie 限制。JWT 结构是三段式Header声明算法、Payload携带用户信息、Signature签名。服务端在用户登录成功后签发 Token返回给前端前端存到 localStorage每次请求在拦截器里自动加上 Authorization 头。后端校验在拦截器里做。实现 HandlerInterceptor 的 preHandle 方法Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || !jwtUtil.validate(token)) { response.setStatus(401); response.getWriter().write(JSON.toJSONString(Result.error(10001, 未登录或登录已过期))); return false; } return true; }有几个细节要注意。第一个OPTIONS 请求要直接放行否则跨域预检请求会死在拦截器里。第二个拦截器注册时排除登录、注册、商品列表、商品详情这些公开接口。第三个注册拦截器的配置类要注入到 Spring 容器别 new 一个拦截器就用那样拦截器里没法用 Spring 管理的 Bean。密码存储一定要用 BCrypt而不是 MD5 或 SHA。BCrypt 是加盐哈希并且计算速度故意设计得慢能极大提高暴力破解成本。每次加密同一个密码得到的 hash 都不同但 matches 方法能正确校验。Spring Security 里的 BCryptPasswordEncoder 可以直接拿来用不需要引入整个 Security 框架。另外有人问“怎么把 Spring Boot 的 jar 反编译成项目”说实话只有拿到部署包、没有源码时才有必要折腾反编译。这个项目本身源码完整直接打开工程就能看不用走那条弯路。3.3 商品分页搜索接口的动态 SQL 实现商品列表接口是 C 端访问量最大的接口之一。参数一般有 pageNum、pageSize、categoryId、keyword、sort 这些返回结果要包含 total总条数和 list当前页数据。实现方式就是前面那条动态 SQL再配一个 count 查询把总数查出来。排序这块我建议不要把排序规则写死在 SQL 里而是让前端传一个 sort 字段后端白名单映射sortdefault 按创建时间倒序sortprice_asc 按价格升序sortsales_desc 按销量降序。为什么用白名单因为直接拼接前端传过来的排序字段很容易被注入恶意的 SQL 片段。虽然是内部项目但好习惯从第一天就该有。还有一点容易被忽略商品列表只返回列表页需要的字段比如 id、标题、副标题、价格、主图、销量不要把富文本详情这种大字段带出来。列表页用不到 detail查出来纯属浪费带宽也拖慢 MySQL 查询。3.4 购物车合并与订单库存扣减的并发控制购物车接口的逻辑看起来简单但有一个隐藏场景同一个用户、同一个商品连续点击两次“加入购物车”应该合并成一条记录数量累加而不是插入两条。所以新增前要先查 cart 表有没有同 user_id 和 product_id 的记录有就 UPDATE quantity quantity 1没有才 INSERT。下单流程是整个项目里对“并发”要求最高的地方。超卖问题是电商的老大难处理的核心不是先查库存够不够再扣而是用一条原子 SQL 完成“扣减且校验库存”UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}这条 UPDATE 的受影响行数就是判断依据返回 1 说明扣减成功返回 0 说明库存不足。不要分成两步“先 SELECT stock再 UPDATE stock stock - quantity”并发情况下两个请求同时读到库存剩 1各自扣 1就会把库存扣成负数。下单、扣库存、生成订单明细、清空购物车这些操作要在同一个事务里用 Transactional 注解保证原子性任何一个环节抛异常就整体回滚。订单状态我用状态机管理0 待付款、1 待发货、2 待收货、3 已完成、4 已取消。状态流转有严格方向只有 0 能变成 1支付或 4取消只有 1 能变成 2发货只有 2 能变成 3确认收货。后台如果直接接收一个 “status3” 就把订单改成已完成那整个链路就乱套了。所以更新订单状态时都从支付、发货、收货、取消这四个动作触发而不是开放一个通用的改状态接口。商品图片上传用的是 Spring MVC 的 MultipartFile存到本地配置好的 upload 目录数据库里只存相对路径访问时通过一个映射配置把路径暴露成静态资源 URL。学习项目用本地存储足够但如果以后要部署到生产环境建议换成对象存储。热搜里提到的 MinIO 就是个不错的自建方案它可以当成私有化的对象存储服务Spring Boot 集成的时候用它的 Java SDK上传后返回一个可访问的 URL 存到数据库与本地存储的逻辑一致只是上传代码换掉。4. 前端实现Vue 环境搭建与页面交互逻辑4.1 开发环境准备与项目初始化前端环境就两样东西Node.js 和 npm或者用 cnpm 镜像。Node.js 版本我建议装 LTS 版本比如 14 或 16 都行不建议上太新的版本因为某些老依赖可能还没适配。安装完成后用node -v和npm -v确认一下。如果你在国内可以把 npm 镜像切换到淘宝源装依赖会快很多npm config set registry https://registry.npmmirror.com项目初始化我用 Vue CLI命令是vue create mall-admin。创建过程中勾选 Router、Vuex 和 CSS 预处理器。这里要注意 Vue 2 和 Vue 3 的区别Vue 2 配 Element UIVue 3 配 Element Plus本项目用的是 Vue 2 Element UI 这套成熟组合官方文档齐全坑也相对少。工程目录按约定划分views 放页面级组件components 放公共组件api 放接口封装router 放路由配置store 放全局状态。开发时还可以配上 Vue Devtools 插件调试组件状态、查看 Vuex 变化、检查路由跳转都很直观排查问题效率能提升一大截装它是前端开发最基本的准备工作。4.2 Axios 统一封装与请求拦截页面里不能到处 new XMLHttpRequest那会让代码没法维护。我用 Axios 封装了一个 request.js导出统一的请求函数。核心逻辑是创建实例时指定 baseURL 和超时时间然后通过拦截器把 Token 附加到请求头同时统一处理响应。import axios from axios const request axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { if (res.code 10001) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(new Error(res.message || 请求失败)) } return res }, error { return Promise.reject(error) } )baseURL 是 VUE_APP_BASE_API 这种环境变量开发环境指向代理地址生产环境指向 Nginx 地址不要写死。这样换环境部署时只要改 .env 文件就行。4.3 路由、权限守卫与页面组织路由表分成两块商城端和管理后台。商城端路由包括首页、商品列表、商品详情、购物车、结算页、个人中心管理后台路由包括仪表盘、分类管理、商品管理、订单管理、用户管理。后台页面必须在登录后访问这就靠路由守卫实现router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path.startsWith(/admin) !token) { next(/login) } else { next() } })细粒度的后台按钮级权限需要动态路由来实现但学习项目做到路由级权限就够用了再复杂就是在堆概念。商城端和管理后台是两个 Layout商城端 Layout 有公共头部和底部后台 Layout 有侧边栏菜单和顶栏两边通过嵌套路由区分。把公共导航菜单放在 Layout 里切换页面时 Layout 不刷新体验上更像一个完整系统。4.4 核心页面实现从商品列表到结算支付商品列表页是 C 端交互最复杂的页面。顶部分类 tab 从分类接口拉取点击分类切换时重置 pageNum 并重新请求商品列表右侧排序 tab 控制 sort 参数分页组件绑定 pageNum 和 pageSize。这里注意 Vue 的 watch 用法监听路由参数变化后重新加载数据否则用户从商品详情点“相似商品”回到列表页时数据不会自动刷新。商品详情页把商品参数、价格、主图展示出来选择数量后点“加入购物车”调用后端接口。购物车页面要支持勾选、全选、修改数量、删除并且实时联动计算总价。计算总价的纯前端逻辑放在 computed 里派发到 Vuex 或者用事件抛给父组件。结算页要选择一个收货地址推荐地址列表接口单选后提交订单接口生成订单后跳转到模拟支付页。管理后台的商品管理页面是表格加对话框的组合列表用分页表格展示编辑时弹出对话框表单图片上传用 Element UI 的 Upload 组件提交时把表单数据和图片路径一起发给后端。订单管理页面按状态 Tab 过滤订单列表每个订单展开查看明细和收货地址支持发货、取消操作。做这些界面时你会发现后端接口字段定得好前端写起来就很顺字段命名不统一前端就要到处兼容这也是为什么前期约定联合返回结果那么重要。5. 本地联调与部署上线的完整流程5.1 本地启动顺序与常见环境坑本地开发的标准启动顺序是先启动 MySQL 并执行 mall.sql 建库建表再启动后端 Spring Boot 应用最后启动前端 npm run serve。这个顺序别乱先保证数据库通再验接口最后开前端页面。前端开发环境的跨域问题是第一个拦路虎。浏览器里 http://localhost:8081 的页面去请求 http://localhost:8080 的接口会被同源策略拦住。解决方式推荐用 Vue CLI 的 devServer 代理在 vue.config.js 里配置module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这样前端请求 /api/v1/product/list代理会转发到后端 8080 端口的对应接口。Axios 的 baseURL 写成 /api 就行浏览器只看到同源的请求跨域问题在代理层就消化掉了。后端同样也要配置 CORS双保险的做法避免某些场景下代理不生效直接被浏览器拦截。5.2 前端打包与 Nginx 部署前后端分离项目本地联调通过后部署阶段要换一套思路。前端的产物是静态文件执行npm run build生成 dist 目录里面是打好的 HTML、JS、CSS。这些纯静态文件用 Nginx 托管就行Tomcat 反而没必要。完整 Nginx 配置是关键它同时要解决两个问题托管静态文件、代理后端接口。配置如下server { listen 80; server_name localhost; location / { root /usr/share/nginx/html/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; 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 是 Vue Router history 模式的生命线。如果前端路由用 history 模式访问商城的 /product/123 时服务器并没有这个物理文件不配 try_files 就会 404配置后它会回退到 index.html由前端路由接管渲染。如果前端用了 hash 模式URL 里带 #没有 try_files 也能跑但 URL 丑我一般用 history 加 try_files。后端打包则是 Maven 项目执行mvn clean package -DskipTests生成一个可执行 jar。把 jar 传到服务器配合 MySQL 账号密码修改 application.yml 后重新打包然后启动nohup java -jar mall-admin-server.jar --spring.profiles.activeprod app.log 21 启动后先看 app.log 有没有异常再用 curl 测一个公开接口确认服务真的起来了。把 Nginx 里 /api/ 的 proxy_pass 指到这台机器的 8080 端口整个前后端分离部署就闭环了。5.3 Tomcat 部署的另一种选择虽然 Spring Boot 默认内嵌 Tomcat很多人还是习惯用外部 Tomcat 部署网上检索“tomcat 部署前后端分离项目”的需求一直很高。两种方式各有适合场景jar 方式启动简单、一个进程搞定适合云服务器war 方式适合公司里已经统一运维了 Tomcat 集群的环境。要把 Spring Boot 项目改成 war 包需要三步pom.xml 的 packaging 改成 war排除内嵌 Tomcat 依赖启动类继承 SpringBootServletInitializer 并重写 configure 方法。改完后把 war 包丢进 Tomcat 的 webapps 目录启动 Tomcat 就会自动解压部署。前端也能一起塞进 Tomcat把 dist 内容放到 webapps/ROOT 下就能跟后端同域部署。但我不太推荐把前端静态资源放进 Tomcat。原因还是耦合前端资源应该交给 Nginx 这类 Web 服务器管压缩、缓存、静态文件处理都更专业Tomcat 干这个活属于“能跑但别扭”。如果要对外提供服务Nginx jar 的组合是主流。5.4 部署阶段 MySQL 问题速查部署环境换到 Linux 上MySQL 相关的坑会集中冒出来我把高频的几个列成速查表现象原因解决办法Navicat 连接不上MySQL 服务没启动或 root 用户 host 是 localhost先systemctl status mysqld确认服务执行 SQL 授权远程访问JDBC 报 SSL 连接错误MySQL 8.x 默认要求 SSLURL 加useSSLfalseallowPublicKeyRetrievaltrueerror 2002 cant connect through socket /tmp/mysql.sockmysql.sock 路径不对或服务没启动确认 mysqld 是否在跑检查 socket 路径中文乱码数据库/表/连接 URL 字符集不一致建库建表统一 utf8mb4URL 加characterEncodingutf8某字段希望默认值为 0建表时没指定默认值ALTER TABLE product ALTER COLUMN status SET DEFAULT 0Linux 上安装 MySQLCentOS 用 yum 装 MySQL 官方 yum 源Ubuntu 用 apt 装 mysql-server装完执行systemctl enable mysqld让它开机自启。新装的 MySQL 8.x 默认 root 密码是临时生成的在日志里找或者用mysqld --initialize-insecure初始化成空密码再自己设置。6. 常见问题排查清单与我的实操心得6.1 后端接口报错排查清单后端启动失败、接口 500这类问题我总结了一份排查清单。Spring 容器启动时报 “Consider defining a bean of type xxxMapper”十有八九是启动类没加 MapperScan或者 mapper 接口上没加 MapperMyBatis 扫描不到。加了注解还报错就检查 mapper XML 的 namespace 是不是跟接口全限定名一致resultType 是不是写成了实体类的简单类名。数据库字段下划线和 Java 属性驼峰不一致时要么在 SQL 里写别名要么开启 map-underscore-to-camel-case 配置二选一别混着来。接口返回的时间是一串数字那是 Jackson 把 Date 序列化成了时间戳。如果你希望返回 “2024-06-01 12:00:00” 这样的格式全局配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8跨域请求报 “Access-Control-Allow-Origin”先分清是开发环境还是生产环境开发环境走 devServer 代理就不会有跨域生产环境走 Nginx 就不会有跨域。如果直接前端地址访问后端地址那再看后端 CORS 配置是否加了允许的 origin以及拦截器是否拦住了 OPTIONS 预检请求。6.2 前端联调与打包报错排查清单npm install 经常报错常见原因是网络问题和依赖版本冲突。先删掉 node_modules 和 package-lock.json 重新装一次多数问题能解决还不行再查看报错里具体是哪个依赖不兼容手动指定版本号。前端控制台报 “Cannot find module vue” 一般是依赖没装全确认 node_modules 里有没有 vue 包没有就重新 npm install。打包完成后部署到服务器刷新页面 404这个我之前已经强调过就是 Nginx 缺了 try_files 配置。前端控制台报接口 404要检查打包后的环境变量 VUE_APP_BASE_API 是不是对的以及 Nginx 的 /api/ 代理规则是否生效。请求返回了但页面数据不对先用 Network 面板看接口返回的 JSON 跟后端接口文档对不对得上字段名不一致就得改前端取值逻辑。调试前端页面时Vue Devtools 和浏览器 Network 面板配合使用Network 定位请求层的错误Vue Devtools 看组件数据和 Vuex 状态。我见过很多人盯着页面代码找半天问题结果打开 Network 发现接口根本没发出去。排查顺序永远是网络请求 - 接口响应 - 页面渲染按这条链路走问题定位快很多。6.3 用一条完整业务链路检验系统是否可用项目做完部署好怎么证明它真的没问题别东点一下西点一下用一条完整的电商链路把系统跑通才算数。我的建议测试顺序是注册新用户 - 登录 - 浏览商品列表 - 搜索商品 - 查看商品详情 - 加入购物车 - 修改购物车数量 - 勾选结算 - 填写收货地址 - 提交订单 - 模拟支付 - 后台订单列表看到新订单 - 后台发货 - C 端确认收货 - 订单完成。这条路走完用户、商品、分类、购物车、订单、明细、支付、地址、库存这些核心表和接口基本都覆盖到了。测试过程中要故意制造一些异常场景库存只留 1 件两个账号同时去买看会不会超卖商品下架后还能不能加购未登录访问购物车接口会不会被拦截器拦下来订单未支付时去取消订单库存有没有加回来。这些边界场景出了问题恰恰是项目里最值钱的细节也是面试时你能讲得比别人深的地方。6.4 几个真正值钱的实践建议这个项目如果只是照着跑通一周就能搞定但如果你想让它成为简历上拿得出手的东西我有几个实在的建议。第一不要一开始就追微服务和高并发架构先把单体的完整链路跑顺。很多人上来就谈“得用 Redis 缓存商品”“得拆订单服务”但单体都没写好就拆只会制造更多分布式问题。先让系统正确再谈优化。第二MyBatis 写 SQL 时不要用 SELECT *明确列出需要的字段这既是性能习惯也是代码可读性的一部分。第三日志要提前规划登录、下单、支付这几个关键节点一定打日志生产环境出问题时没日志等于瞎猜。第四接一个接口约定类的活儿时统一返回结果和异常机制必须在设计文档里定好否则前后端并行开发到最后必然互相甩锅。前端说“接口报错了”后端说“你参数传错了”这种扯皮我已经见过太多次。第五如果想给项目加分选一两个点做深就行订单超时未支付自动取消可以用延迟任务、热门商品展示缓存引入 Redis、库存扣减性能优化引入消息队列削峰任何一个都足够在面试时聊二十分钟。最后再分享一个实战小技巧本地联调时把后端日志级别调成 DEBUG前端同时打开 Network 面板和 Vue Devtools。出问题时先回答三个问题——请求发出去了吗后端返回了什么页面有没有按照返回数据渲染一步步排除你会发现绝大多数问题都卡在边界条件和字段约定上而不是复杂的技术原理。这套定位方法比你会背一百个框架 API 都管用。