资讯详情

资讯详情

SpringBoot+Vue+MyBatis:全栈商城系统从零搭建实战

对于准备做电商类毕业设计或者想入门前后端分离开发的朋友来说SpringBoot Vue MyBatis MySQL 这套组合拳几乎是绕不开的经典搭配。我当初为了完成自己的商城系统前前后后折腾了快两个月从网上买过那种烂大街的“完整源码”结果被坑得不轻——代码里全是“毕业设计”水印数据库脚本导入就报错所谓的秒杀逻辑根本是摆设核心的表结构还缺东少西。后来一怒之下推倒重写完全按照企业级项目的标准从零手撸了一个仿米家商城的全栈项目涵盖了商品、购物车、订单、支付模拟、后台管理全链路最终顺利答辩通过还把它打造成了可以离线部署的单体 Jar 包。这篇文章我会把整个项目从架构设计、表结构规划、核心接口实现、典型踩坑记录到部署上线的完整过程都梳理出来希望能给正在做同类项目的人一些真正可落地的参考。1. 项目基础架构搭建为什么选择这套技术栈1.1 技术选型的核心考量前后端分离这个词这几年已经被说烂了但真正动手做的时候才发现选对组合能省下一半的精力。我最终定下的方案是后端 SpringBoot 2.3.x JDK 1.8ORM 层用 MyBatis数据库 MySQL 5.7前端 Vue 2.6.14 Vue CLI 4.xUI 组件库 Element UI 2.x接口调试用 Swagger2。为什么这么选我个人的判断标准是“够用、稳定、资料多”。SpringBoot 2.x 在 2023 年之前依然是各大企业生产环境的主流版本网上能搜到的踩坑帖和解决方案多到数不清遇到报错基本不会无解。JDK 1.8 更不用说了虽然 17 和 21 都出来很久了但很多学校机房和云服务器上预装的还是 1.8而且 1.8 对 SpringBoot 2.x 的兼容性是最完美的。MyBatis 这块我纠结过一段时间到底是原生 MyBatis 还是 MyBatis-Plus后面我会单独讲最终项目里是两者都用了复杂查询手写 SQL单表 CRUD 交给 MyBatis-Plus 的 BaseMapper。前端这块选择 Vue 2 而不是 Vue 3主要是因为 Element UI 对 Vue 2 的支持最成熟组件全文档多而且网上优秀的开源商城 Admin 模板基本都是基于 Vue 2 Element UI 开发的抄作业也方便。Vue 3 虽然很香但对应的组件库生态当时还不够统一与其把时间花在版本兼容性上不如把精力放在业务逻辑上。1.2 工程结构设计前后端源码目录规划整个项目我在 GitHub 上分成两个大目录backend 和 frontend。后端是一个标准的 Maven SpringBoot 工程包结构如下com.mall.abo ├── controller // 控制层接收前端请求 ├── service // 业务逻辑层 │ └── impl // 业务实现类 ├── mapper // MyBatis 数据访问层接口 ├── entity // 数据库实体类 ├── vo // 前端交互的视图对象 ├── config // 配置类跨域、Swagger、JWT拦截器 ├── utils // 工具类JWT工具、Redis工具等 ├── common // 统一返回结果封装、异常处理 └── MallApplication.java // 启动类前端是 Vue CLI 创建的标准工程核心目录为src ├── api // 封装 Axios 请求按模块拆分product.js, cart.js, order.js ├── assets // 静态资源 ├── components // 公共组件商品卡片、分页组件等 ├── router // 路由配置含路由守卫 ├── store // Vuex管理用户登录状态、购物车数据 ├── views // 页面组件首页、商品列表、详情、购物车、结算、后台管理 ├── utils // 请求封装、身份验证工具 └── main.js模块划分的原则是前端按页面拆后端按功能拆前后端调用关系一一对应。这样做的最大好处是后面写毕业论文的时候功能模块图直接照着包结构画就行每个模块的代码量和工作量一目了然。1.3 数据库表结构设计18张表的ER图规划数据库是整个项目的地基表结构设计得好不好直接决定后期写代码是享受还是受罪。我最初的版本是参考别人的商城数据库表建得乱七八糟比如订单表直接叫 order字段里有 order_count看起来没什么问题但 order 在 MySQL 里是保留字Linux 服务器上部署时直接报语法错误后来改成了 orders 和 order_num 才消停。最终版的数据库一共 18 张表核心表和用途如下表名用途说明users用户表存储账号、密码BCrypt加密、昵称、头像、手机号、邮箱product商品表包含商品名称、副标题、主图、详情图片、价格、库存、销量、上下架状态product_category商品分类表单级分类存储分类名称和图标cart_item购物车表以用户ID加商品ID作为唯一维度orders订单主表订单号、总金额、订单状态、收货信息快照order_item订单明细表商品快照、购买数量、单价payment_record支付记录表模拟支付时生成的流水shipping_address收货地址表含默认地址标识banner首页轮播图表favorite商品收藏表product_stock_log库存变动日志表用于排查超卖问题设计数据库时有三条经验非常关键。第一所有涉及钱的字段统一用 DECIMAL(10,2)千万别用 FLOAT 或 DOUBLE不然会出现 0.1 0.2 0.30000000000000004 这种经典问题。第二所有表的主键统一用 BIGINT 自增ID逻辑删除字段统一叫 deleted创建时间和更新时间统一叫 create_time 和 update_time这是很多企业级项目的建表规范提前养成习惯没坏处。第三订单表和订单明细表中一定要存储收货人和商品信息的“快照”而不是通过外键去关联用户表和商品表。因为用户下单后如果改了地址或者商品下架了订单历史数据不能被牵连改动这个设计在答辩时也是加分项。2. 后端接口实现与核心业务拆解2.1 统一返回结构与 JWT 身份认证机制后端接口开发的第一步我封装了一个统一的返回结果类 Result所有接口的返回格式都长这样{ code: 200, message: 操作成功, data: {} }约定 code 为 200 表示成功401 表示未登录或 Token 过期403 表示无权限500 表示服务器内部错误。前端 Axios 的响应拦截器里统一处理拿到 401 就清空本地存储并跳转登录页拿到 500 就弹出错误提示。这个统一封装非常有必要否则前端每个页面都要自己判断返回格式代码会冗余到爆炸。身份认证我采用的是 JWTJSON Web Token方案。用户登录成功后后端根据用户ID和用户名生成一个 Token设置有效期 24 小时返回给前端。前端把 Token 存在 localStorage 里每次请求在拦截器中自动加到请求头 Authorization 字段。后端通过一个自定义的 JwtInterceptor 拦截器统一校验放行登录、注册、商品浏览等公开接口购物车、订单等敏感接口必须携带合法 Token 才能访问。这里有个细节JWT 的密钥必须足够长至少 32 位而且不要硬编码在代码里放在 application.yml 中用 jwt.secret 属性配置。有个同学图省事用了 123456 当密钥结果 Token 被人伪造直接绕过登录访问了后台接口这种低级错误千万不要犯。2.2 商品浏览链路分类、列表、搜索、详情商品模块是商城对外展示的门面。用户端提供分类树查询、商品列表分页查询、按关键词模糊搜索、商品详情查询四个接口。商品列表接口支持按分类ID过滤、按价格区间过滤、按销量或价格排序这些参数通过 ProductQuery 对象接收分页用 MyBatis-Plus 的 Page 对象实现。讲一下 MyBatis 这块的最初实现。项目早期我用的是纯注解方式Select 注解里直接写 SQL配合 Results 做结果映射。但数据库字段是下划线风格create_timeJava 实体是驼峰风格createTime如果不在 application.yml 里配置 map-underscore-to-camel-case: true那么查出 create_time 字段映射到实体时就会变成 null前端拿到 createTime 就是 undefined。这个坑我印象太深了因为排查了半天才找到原因。后来项目重构时引入了 MyBatis-Plus单表的 CRUD 直接继承 BaseMapper一行 SQL 都不用写逻辑删、自动填充时间戳这些功能都能通过注解自动实现。多表关联的复杂查询比如查询商品列表时需要连分类表拿分类名称我继续手写 SQL。为什么要这么混合用核心原因是提升开发效率——MyBatis-Plus 能覆盖 60% 以上的单表操作剩下的复杂查询用原生 SQL 更灵活可控两者结合既保证了开发速度又在关键业务上保留了完全的手写掌控力。2.3 购物车设计以商品维度合并不产生重复条目购物车是最容易写出逻辑混乱的模块所以我把边界控制得特别清晰。购物车的核心设计是同一用户对同一商品只保留一条购物车记录重复添加同一个商品时只累加数量购物车列表按创建时间倒序排列已勾选商品的总价和总件数在计算属性中实时计算。后端购物车接口总共五个加入购物车、修改商品数量、删除购物车条目、切换勾选状态、查询购物车列表。加购接口的伪代码如下public Result addCart(Integer productId, Integer userId, Integer quantity) { LambdaQueryWrapperCartItem wrapper new LambdaQueryWrapper(); wrapper.eq(CartItem::getUserId, userId); wrapper.eq(CartItem::getProductId, productId); CartItem item cartMapper.selectOne(wrapper); if (item null) { // 新建购物车记录价格为商品当前价格快照 CartItem newItem new CartItem(userId, productId, quantity, product.getPrice()); cartMapper.insert(newItem); } else { // 数量累加但不能超过库存 item.setQuantity(item.getQuantity() quantity); cartMapper.updateById(item); } return Result.success(); }有个问题值得注意价格到底存在购物车里还是下单时实时查商品表我的做法是加购时存一个价格快照但下单时重新从商品表读取最新价格计算总价以数据库实时价格为准。这样做的原因是防止用户加购后商品调价结算时引发争议。虽然模拟商城不用太较真但答辩时能讲出这个设计理由会让老师觉得你考虑过真实业务场景。2.4 下单事务链库存扣减与订单生成的原子性下单接口是整套系统最复杂的接口没有之一。它在一个事务里同时完成四件事校验商品库存是否充足。扣减库存采用条件更新防止超卖。生成订单主表记录和订单明细记录。清空购物车中已勾选的商品。核心代码逻辑如下Transactional(rollbackFor Exception.class) public OrderVO createOrder(Long userId, Long addressId, ListLong cartItemIds) { // 1. 读取收货地址快照 ShippingAddress address addressMapper.selectById(addressId); // 2. 查询购物车中的商品信息 ListCartItem cartItems cartMapper.selectBatchIds(cartItemIds); BigDecimal totalAmount BigDecimal.ZERO; ListOrderItem orderItems new ArrayList(); for (CartItem cartItem : cartItems) { Product product productMapper.selectById(cartItem.getProductId()); // 3. 条件更新stock 0 防止超卖 int rows productMapper.deductStock(product.getId(), cartItem.getQuantity()); if (rows 0) { throw new RuntimeException(商品 product.getName() 库存不足); } // 4. 组装订单明细快照 totalAmount totalAmount.add(product.getPrice().multiply(new BigDecimal(cartItem.getQuantity()))); } // 5. 插入订单主表 Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setTotalAmount(totalAmount); order.setStatus(0); // 0待支付 order.setReceiverName(address.getReceiverName()); order.setReceiverPhone(address.getReceiverPhone()); order.setReceiverAddress(address.getDetailAddress()); orderMapper.insert(order); // 6. 批量插入订单明细 for (OrderItem item : orderItems) { item.setOrderId(order.getId()); orderItemMapper.insert(item); } // 7. 清空已勾选的购物车 cartMapper.deleteBatchIds(cartItemIds); return orderToVO(order); }这段代码里有一个极其重要的点Transactional 注解必须显式指定 rollbackFor Exception.class。Spring 默认情况下只会在抛出 RuntimeException 时回滚事务如果业务代码里抛出的是受检异常Exception事务是不会回滚的数据就会出现“库存扣了但订单没生成”这种严重不一致的情况。很多新手源码里都栽在这上面我特意加了干扰处理。库存扣减的 SQL 是UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}返回受影响行数如果为 0 说明库存不足直接抛异常。这个方案利用了数据库行锁和条件更新的原子性单机部署、并发量不高时完全够用。真到了秒杀场景还是得引入 Redis Lua 脚本或者消息队列来削峰但那是另一个复杂度级别的问题了。2.5 订单状态机与模拟支付流程订单状态我定义成 0 待支付、1 已支付、2 已发货、3 已收货、4 已取消。由于没有接入真实的支付宝或微信支付我设计了一个模拟支付页面订单创建后跳转过去点击“确认支付”按钮前端向后端发送支付请求后端生成一条支付流水记录同时把订单状态从 0 改为 1。整个流程完全模拟真实支付的信任链答辩时讲清楚思路即可没人会真要求你掏出商户号对接支付宝。为了防止用户下了单不支付导致库存被长时间占用我设计了订单超时自动取消逻辑用 Spring 的 Scheduled 注解实现Scheduled(fixedDelay 60000) public void autoCancelExpiredOrders() { ListOrders expiredOrders orderMapper.selectExpiredOrders(15); for (Orders order : expiredOrders) { order.setStatus(4); orderMapper.updateById(order); // 恢复库存 ListOrderItem items orderItemMapper.selectByOrderId(order.getId()); for (OrderItem item : items) { productMapper.restoreStock(item.getProductId(), item.getQuantity()); } } }每分钟扫描一次订单表把创建时间超过 15 分钟且状态还是待支付的订单改成已取消顺便把库存加回去。这个定时任务在单机部署下没问题但如果将来部署了多个后端实例会导致任务重复执行就必须引入分布式锁了。这个点可以在论文的“系统优化方向”里提一下是个不错的加分思路。3. 前端核心页面与交互逻辑实现3.1 路由设计懒加载与全局前置守卫前端路由是整个前端骨架我按照商城用户习惯拆分成首页、全部商品、商品详情、购物车、结算页、个人中心、登录注册、后台管理这几大模块。所有路由采用懒加载模式const routes [ { path: /, name: Home, component: () import(/views/Home.vue), meta: { title: 首页 } }, { path: /product/:id, name: ProductDetail, component: () import(/views/ProductDetail.vue), meta: { title: 商品详情 } }, { path: /cart, name: Cart, component: () import(/views/Cart.vue), meta: { title: 购物车, requireAuth: true } }, { path: /checkout, name: Checkout, component: () import(/views/Checkout.vue), meta: { title: 确认订单, requireAuth: true } } ];用动态 import 语法把每个页面拆成独立 chunk首屏只加载首页需要的代码进入其他页面时才异步加载对应文件这样打包体积和首屏性能都会好不少。登录控制这块我用的是全局前置守卫这个功能必须单独拿出来讲。核心代码如下router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (!token to.meta.requireAuth) { next({ path: /login, query: { redirect: to.fullPath } }); } else { next(); } });未登录用户可以直接浏览商品列表和详情一旦点击购物车、个人中心这些标记了 requireAuth 的页面就会被重定向到登录页并且带上 redirect 参数。登录成功后跳回之前想去的页面这是一个非常符合直觉的交互。登录页里用 Element UI 的 el-form rules 做了表单校验邮箱手机号和密码格式先在前端校验一遍再提交给后端减少无效请求。3.2 Axios 封装拦截器与请求配置前端所有 HTTP 请求我都走了一个统一的 Axios 实例而不是每个页面单独写 axios.get。封装的目的是统一处理基础 URL、超时时间、Token 注入和错误提示。const service axios.create({ baseURL: process.env.VUE_APP_BASE_API || /api, timeout: 15000 }); // 请求拦截器自动注入Token service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] token; } return config; }); // 响应拦截器统一处理业务错误码 service.interceptors.response.use( response { const res response.data; if (res.code 401) { localStorage.removeItem(token); location.href /login; } else if (res.code ! 200) { Message.error(res.message); } return res; }, error { Message.error(网络请求失败请稍后重试); return Promise.reject(error); } );baseURL 用的相对路径 /api这是为后面 Nginx 反向代理准备的统一前缀生产环境通过 Nginx 把 /api 转发到后端的 8080 端口前端开发时则用 vue.config.js 里的 devServer.proxy 把 /api 代理到本地后端服务这样前后端联调阶段就不需要关心跨域问题。这个设计是我在踩了跨域的坑之后总结出来的最佳实践。3.3 购物车与结算页的状态管理购物车数据我用 Vuex 管理因为购物车在页面刷新后需要保留数据而且多个页面导航栏购物车角标、购物车页、结算页都要共享同一份数据。用户登录后进入网站时从后端拉取购物车列表存入 Vuex操作购物车加购、改数量、删除时先调用后端接口成功后再重新拉取列表更新 Vuex。总价总件数这类汇总逻辑我用的是 Vuex 的 gettersgetters: { cartCount: state { return state.cartList.reduce((total, item) total item.quantity, 0); }, cartTotalPrice: state { return state.cartList .filter(item item.checked) .reduce((total, item) total item.price * item.quantity, 0); } }结算页的设计比较简洁左侧显示收货地址列表可以新增、编辑、设置默认右侧是商品清单和总计金额确认无误后点击“提交订单”按钮跳转到模拟支付页。地址管理模块有一件事值得注意设置默认地址时需要先把该用户所有地址的非默认字段更新为 0再把目标地址的默认字段更新为 1两步操作必须在同一个事务里完成否则会出现两个默认地址并存的问题。3.4 后台管理页面权限控制与核心功能后台管理这块我单独设计了一套 Layout侧边栏包括商品管理、订单管理、用户管理、轮播图管理和分类管理。后台的路由单独挂在一个父路由下并且加了一个 admin 权限标志。判断逻辑朴素直接用户表里有一个 role 字段普通用户是 0管理员是 1登录时把 role 也存进 localStorage路由守卫里判断访问后台路由的用户 role 是否为 1不是就重定向到首页。商品管理页是最典型的管理端 CRUD 页面表格用 el-table搜索区有商品名称关键字、分类下拉框、上下架状态筛选操作列有编辑、删除、上下架切换。删除是逻辑删除把 deleted 字段改为 1列表查询默认过滤掉已删除的数据。上传商品主图用的是 Element UI 的 el-upload 组件接口是后端提供的 /api/admin/upload 文件上传接口文件存储到服务器本地指定目录并把访问路径返回给前端拼接到图片地址上。后台权限这块我只做了一个简单的角色字段判断因为它毕竟是毕业设计级别的项目够用就好。如果真做企业级后台RBAC 用户-角色-权限模型才算正规军那又是一套复杂的东西。4. 真刀真枪的踩坑记录从跨域到数据库保留字4.1 数据库字段大小写与保留字问题这个坑是我在从 Windows 开发机往 Linux 服务器部署时踩的。Windows 上 MySQL 对表名大小写不敏感我在本地建表随便写 Product 也能查到但 Linux 上 MySQL 默认区分大小写配置了 lower_case_table_names0 时用 Product 查询直接报 table doesnt exist。后来我统一把所有表名、字段名设置为小写加下划线SQL 语句里也全部用小写彻底告别了这类问题。保留字问题则更隐蔽。我有一个订单表第一版命名为 order字段里还有 order_count本地 Windows MySQL 5.7 能建表成功但到了 Linux 上某些版本直接报语法错误。排查了很久才发现 order 是 MySQL 8.0 的保留字建表时必须用反引号包裹或者改名。我把订单表改为 orders查询用 order_num从源头规避。这个经验对你的项目同样适用建表之前先过一遍 MySQL 保留字列表千万别踩 order、group、desc、key 这些雷。4.2 跨域配置的三种方案对比前后端分离必然涉及跨域我先后试过三种方案最终选定全局 CorsFilter。先说 CrossOrigin 注解方案加在 Controller 类或方法上能生效但每个 Controller 都要加繁琐且容易漏。再看 Spring Cloud Gateway 或 Nginx 层转发方案适合生产环境但本地联调时配置起来略麻烦。最后我采用了最直接的 CorsFilter 全局配置Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOrigin(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }有一个细节必须注意addAllowedOrigin(*) 和 setAllowCredentials(true) 不能同时使用浏览器会报错。如果前端需要携带 Cookie就必须显式指定具体来源域名而不能用通配符。我的项目里用的是 Token 认证不依赖 Cookie所以前端联调时用 * 没问题但生产环境为了安全我改成了具体的域名。4.3 MyBatis XML 扫描不到与 SQL 日志排查这个坑是 MyBatis 新手最容易卡住的。项目里如果用了 XML 映射文件并且放在 src/main/resources/mapper 目录下那么 application.yml 必须显式指定mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.mall.abo.entity同时 pom.xml 里的 resources 配置也要把 mapper 目录包含进去否则 Maven 打包时不会把 XML 文件复制到 target/classes 目录运行时就报 Invalid bound statement (not found)。由于我在最终版项目中全部改为注解 SQL 和 MyBatis-Plus这个坑就自动绕开了但如果你是照抄别人的 XML 方案这两处配置缺一不可。排查 SQL 问题还得学会看日志。SpringBoot 默认集成 Logback在 yml 里把 mapper 包下的日志级别调成 DEBUGlogging: level: com.mall.abo.mapper: debug这样每个 Mapper 方法执行的 SQL 语句和参数值都会打印到控制台拼接的 SQL 是啥、参数传的什么一眼就能看明白。调接口时发现数据不对先看日志再猜代码效率能提升十倍。4.4 前端接口路径404与字段名undefined的联调问题前后端联调阶段最常见的两类问题就是路径对不上和字段名映射不上。路径问题大多数是大小写不一致后端的 RequestMapping(/api/product/list)前端写成 /api/Product/list浏览器请求就变成了 404。为了解决这个低级问题我引入 Swagger2 生成接口文档前后端各拿一份所有接口的请求方法、路径、参数、返回值在文档里列得明明白白完全按照文档调用就不会出岔子。字段名 undefined 的问题前面提过是 Java 驼峰转下划线导致的。我最初的后端实体用 createTimeMySQL 字段是 create_time前端的 Table 组件通过 propcreateTime 取值但后端返回的 JSON 里字段名却可能是 create_time前端拿到 undefined。最后统一在 yml 配置 map-underscore-to-camel-case: true并且要求后端所有实体字段严格遵循驼峰命名前端统一按驼峰取值就再没出现过这个问题。5. 部署上线从开发环境到 Linux 服务器全流程5.1 环境准备JDK 与 MySQL 的初始化配置部署的第一步是准备一台干净的 Linux 服务器。JDK 安装我用的是 openjdk-8-jdk安装完后用 java -version 验证版本。MySQL 用的是 5.7安装后需要执行 mysql_secure_installation 设置 root 密码和删除匿名用户。这里有个重要细节如果 MySQL 是 8.0 版本驱动必须用 mysql-connector-java 8.0.x如果服务器是 5.7用 5.1.49 或 8.0.17 都可以。驱动版本与 MySQL 版本不匹配时常见报错是 The server time zone value 或 SSL 连接错误处理方法是给 JDBC URL 加上时区参数和关闭 SSLjdbc:mysql://localhost:3306/mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse5.2 前端打包与后端 Jar 制作两种部署形态详解部署方案我整理了两条路线一条是纯传统前后端分离另一条是把前端静态资源塞进后端 Jar 包做成单体部署。方案一Nginx 反向代理是生产环境最标准的做法前端在项目根目录执行 npm run build生成 dist 目录。把 dist 下的所有文件上传到服务器 Nginx 的 html 根目录例如 /usr/share/nginx/html。后端执行 mvn clean package -DskipTests生成 mall-0.0.1-SNAPSHOT.jar。将 jar 放在 /opt/mall 目录执行 nohup java -jar mall.jar mall.log 21 启动。Nginx 配置文件中加一段转发规则location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }方案二单体 Jar适用于学校答辩这种网络不稳定的场景把前端 dist 目录直接复制进后端项目的 src/main/resources/static 下重新打包 Jar。SpringBoot 的内置 Tomcat 会自动把 static 目录下的 index.html 作为首页访问 http://服务器IP:8080 就能直接打开前端页面前端相对路径的 API 请求也会落到同一个服务上不需要 Nginx 也能完整跑通。两者之间用 Nginx 更符合实际工作习惯但答辩演示时一个 Jar 搞定一切更省心。5.3 安全组、防火墙与数据库远程连接的排查清单部署完成后最常遇到的问题不是代码问题而是网络层面的问题。我第一次部署到腾讯云时后端 jar 起来了Swagger 页面就是打不开折腾了半天发现是云服务器安全组没有放行 8080 端口。安全组入方向规则里必须添加 TCP 端口 80、8080、3306 的放行规则。如果用的是宝塔面板还要检查系统防火墙firewall-cmd --zonepublic --add-port8080/tcp --permanent firewall-cmd --reload数据库远程连接也是很多人的痛点。本地开发时连接服务器 MySQLroot 用户默认只允许 localhost 登录需要在服务器上执行CREATE USER mall% IDENTIFIED BY yourpassword; GRANT ALL PRIVILEGES ON mall.* TO mall%; FLUSH PRIVILEGES;同时 MySQL 配置文件里确保 bind-address 设置为 0.0.0.0而不是默认的 127.0.0.1否则远程连接会被拒绝。这个问题的表现是 Navicat 连接时报 Cant connect to MySQL server很多人误以为是密码错误其实是 bind-address 的问题。5.4 数据库脚本交付干净可复现的SQL备份最后一步也是容易被忽视的一步生成一份可在任何环境直接运行的 SQL 脚本。项目调试完成后用 mysqldump 导出mysqldump -uroot -p mall mall.sql把这份文件放进项目的 sql 目录同时在 README 里写清楚导入命令mysql -uroot -p -e CREATE DATABASE mall DEFAULT CHARACTER SET utf8mb4; mysql -uroot -p mall mall.sql为什么强调 utf8mb4因为老的 utf8 编码存不了 emoji 表情如果用户昵称里带了 emoji插入数据库时会报 Incorrect string value 错误。utf8mb4 才是 MySQL 上真正的“完整 UTF-8”这也是 5.7 之后的主流选择。我特别叮嘱一句这份 SQL 必须在干净的机器上实际测试过能导入成功再发布。我见过太多人发的源码SQL 是从自己本地库导出的里面有本地测试的数据垃圾或者依赖了某些自定义函数别人一导入就各种报错体验直接崩塌。干净到只包含建表语句和基础演示数据的 SQL 才是合格交付物。6. 项目文档、答辩演示与开源反思6.1 数据库设计文档从ER图到表结构说明如果这个项目是拿来当毕业设计用的数据库设计文档是论文里的重头戏。我写文档时按这四章展开第一章项目背景与需求分析第二章系统设计架构图、功能模块图、ER图、数据库表结构第三章系统实现核心代码加页面截图第四章系统测试测试用例表格加测试结论。整体篇幅在 12000 字左右全部自己写没有抄网上的模板。画 ER 图时有一个重要的建议不要用系统截图代替矢量图。代码截图放大后会很模糊答辩时投影上根本看不清。我用的 ProcessOn 和 Visio 重画了所有架构图和 ER 图打印到论文里使用矢量图非常清晰。ER 图至少要覆盖用户-订单、商品-分类、订单-订单明细这三组核心关系这是数据库设计题答辩必问的内容。6.2 演示环境的离线部署与演示脚本答辩现场网络状况总是不可预测我强烈建议采用前面说的单体 Jar 方案然后提前在答辩用的电脑上装好 JDK 1.8 和 MySQL 5.7把 SQL 导入好jar 包启动好全程断网也能完成演示。演示脚本我建议按这条线走首页展示轮播图、商品分类、新品推荐让老师看到整体 UI。进入商品详情页展示商品图片、价格、库存、加入购物车操作。去购物车修改数量、勾选结算展示前端交互逻辑。提交订单并模拟支付展示订单状态从待支付变为已支付。演示后台管理登录管理员账号展示商品上下架和订单管理功能。最后打开 Swagger 文档现场调用一个接口展示前后端分离的接口设计。这条演示链路由浅入深覆盖前端、后端、数据库、接口文档四个层面时间控制在 8 到 10 分钟比较合适。如果老师追问“你这个项目有什么亮点”回答的切入点就是 JWT 认证、事务保障库存一致性、超时自动取消订单、统一异常处理以及文档齐全这些真正花过功夫的点。6.3 代码重构带来的成长从“能用”到“工程化”整个项目改造完成后我最大的感受是不要迷信网上那份“完整源码”。我最初花钱买的那份源码代码里有大量复制粘贴的痕迹注释和类名对不上接口返回的数据结构混乱数据库缺少外键关系导入就报错。最终版项目虽然也是从模仿开始的但经过完整的推倒重写我把 SpringBoot 自动装配原理、MyBatis 执行流程、JWT 加密过程、Vue 生命周期钩子这些基础理论彻底吃透了答辩时老师问到的每一个扩展问题我都能从实践的角度给出自己的理解而不是背概念。建议所有想认真做一个商城项目的朋友可以先去 GitHub 上把 star 数高的开源商城项目源码下载下来读比如 newbee-mall、mall 这些经典项目照着它们的表结构设计、接口风格、部署文档一步一步走然后把核心代码用自己的思路重写一遍。即使达不到完全从 0 手写也要保证每一行代码都看得懂、改得动。这也是我这套“米家商城”从粗糙、残废的“完整源码”逐步演变进化为一份结构清晰、可运行、可部署、可复现的项目的完整过程。最后再分享一个小技巧项目根目录一定放一个 README 文件把环境要求、数据库导入步骤、后端启动命令、前端启动命令、默认账号密码这五件事写清楚。这个 README 不只能帮别人快速跑通项目也是你毕业答辩时展示工程素养的加分项。我见过一些同学代码写得挺好但连 README 都没有老师打开项目一脸懵印象分大打折扣。把基础功做扎实整个项目的完整度远不只是代码层面的事情。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →