资讯动态

SpringBoot+Vue3+MyBatis实战:爱心公益商城系统开发复盘

发布时间:2026/10/23 9:27:35 来源:尧图企业网站定制
做后端这些年接手过不少业务系统但今年上半年这个爱心商城系统算是我完整走完的一套偏电商的项目SpringBoot Vue3 MyBatisMySQL 存数据前后端完全分离。表面看就是一个商城但业务上有不一样的地方——它不是一个单纯的买卖平台而是围绕公益义卖场景来设计用户买商品的同时能看到自己产生的公益贡献商城后台还要维护公益项目、义卖商品、订单和贡献流水。这套系统做完从数据库建模到后端接口、前端页面再到服务器部署踩了不少坑也攒了不少经验今天就把整个过程完整拆给你。我假设看这篇东西的人有两种一种是刚学完 SpringBoot 和 Vue 但没做过完整前后端分离项目的新手想找一套可以复刻的实战参考另一种是已经做过类似商城系统、想看看别人怎么处理订单、事务、权限、联调这些问题。不管哪种这篇文章都尽量写到我当时的真实决策和操作细节能直接当开发手册用。1. 项目定位与整体架构设计1.1 商城系统到底要解决什么问题先把这个系统的业务逻辑说清楚。爱心商城不是普通电商核心业务围绕义卖 公益贡献展开。普通用户注册登录后可以浏览义卖商品、加入购物车、下单支付支付完成后系统会根据订单金额按一定比例生成公益贡献值关联到用户选择支持的公益项目上。管理端则要维护公益项目信息、上架下架商品、处理订单状态、查看贡献汇总。这里有个关键点订单不只是订单它还是贡献数据的来源。所以数据库设计时我必须把订单、订单明细、贡献记录分开每笔订单的状态流转要清楚贡献记录要能追溯到具体订单。这个业务模型直接决定了后面所有表结构的设计方向。我一开始也想过要不要做复杂的积分体系后来和需求方反复确认第一版只做消费即贡献这一条主链路保证核心流程先跑通。这也是我后来反复和做系统的人强调的一句话第一版永远不要贪多把主链路做扎实。1.2 技术选型为什么是这套组合技术栈选型这件事很多新人以为是哪个火选哪个实际上背后全是成本和团队熟悉度的考量。后端选 SpringBoot 2.7.x理由很朴素生态成熟招人好招遇到问题随便一搜就有答案。如果选一些小众框架文档少不说出了奇怪的问题你连排查方向都没有。SpringBoot 内置的自动配置能把项目搭建成本压到最低我本地从初始化到跑起来一个空工程不超过十分钟。持久层用 MyBatis 而不是 JPA是因为商城这类系统的查询太灵活了。商品列表要按分类、价格区间、关键字动态组合筛选订单列表要按状态、时间、用户多种条件叠加查MyBatis 动态 SQL 处理这种场景得心应手。虽然很多人觉得写 XML 麻烦但我更宁愿 SQL 掌握在自己手上SQL 好不好优化、能不能加索引提示心里都有数。前端选 Vue3 配 Vite组合态 API 写起来逻辑内聚响应式系统比 Vue2 的 Object.defineProperty 方案在处理深层对象时更省心。加上 Element Plus 做后台管理界面开发效率提升非常明显。数据库就是 MySQL 8.0InnoDB 引擎。MySQL 在中小型项目里的平衡性最好配套工具多团队都熟没必要为了高大上去上 PostgreSQL 或者其他重型方案。这套选型放到市场上看就是一套非常稳妥、不会出大错的主流组合对个人学习或者团队交付都很合适。1.3 前后端分离架构如何落地前后端分离这四个字说起来简单真正落地时涉及一套约定。我这个项目的物理结构是这样前端一个独立工程负责页面渲染和用户交互后端一个独立工程只提供 JSON 接口两者之间通过 HTTP 通信前端开发时走代理转发生产环境用 Nginx 把静态页面和接口请求指向同一个域名下避免跨域。后端工程内部没有再拆微服务这个体量的项目拆微服务纯属给自己找事。模块化分层就够了Controller 层只做参数接收和响应封装Service 层写业务逻辑Mapper 层通过 MyBatis 访问数据库。每层职责清晰出了问题能快速定位是接口问题还是业务逻辑问题还是 SQL 问题。接口规范上用 RESTful 风格资源用名词操作靠 HTTP 方法区分。比如 GET /api/goods 查商品列表POST /api/orders 创建订单。约定好统一的响应体结构前端拿到后不用每个接口单独判断格式。我用的响应体结构是{ code, message, data }code 为 0 表示成功非 0 表示业务异常前端 axios 拦截器统一处理。前端路由用 Vue Router页面按用户端和管理端划分用户端是商城前台包括首页、商品列表、商品详情、购物车、订单确认、个人中心管理端是后台包括商品管理、订单管理、公益项目管理、数据看板。两个端共用一套接口只是不同角色能访问的接口权限不同。2. 数据库设计与核心表结构2.1 三个核心业务域怎么拆商城系统再怎么复杂核心逃不出三个域用户域、交易域、商品域。我这套系统因为带公益贡献的业务还要加一个贡献域但本质上它依附于交易域靠订单金额触发。用户域处理的是账号体系包括用户基本信息、登录凭证、收货地址。交易域是订单和订单明细这是整个系统数据一致性要求最高的地方。商品域包括商品基本信息、分类、库存、上下架状态。贡献域记录每次购物产生的贡献流水关联公益项目和订单。域与域之间的关联我尽量用外键约束吗没有。生产环境中我很少用物理外键逻辑关联就够了。外键在项目初期看着规整到后面做分表、做数据归档、做批量删除时全是坑。订单表里的 userId、goodsId 这种字段靠应用层保证逻辑正确数据库层面只建索引。2.2 几张关键表的定义直接看我实际用的建表语句以订单主表为例CREATE TABLE t_order ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 订单ID, order_no varchar(32) NOT NULL COMMENT 订单编号, user_id bigint(20) NOT NULL COMMENT 下单用户ID, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 订单状态0待支付 1已支付 2已发货 3已完成 4已取消, receiver_name varchar(50) NOT NULL COMMENT 收货人姓名, receiver_phone varchar(20) NOT NULL COMMENT 收货人电话, receiver_address varchar(255) NOT NULL COMMENT 收货地址, charity_project_id bigint(20) DEFAULT NULL COMMENT 支持的公益项目ID, contribution_amount decimal(10,2) DEFAULT NULL COMMENT 产生的公益贡献金额, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, pay_time datetime DEFAULT NULL COMMENT 支付时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;这里几个细节值得说一下。订单编号我不用自增ID直接展示因为自增ID会暴露订单量而且多表合并或数据迁移时容易冲突。我用的是时间戳 随机数拼接的订单号格式类似20240515103012xxxxxx在 Service 层生成唯一索引兜底防止重复。金额字段一律用decimal(10,2)绝不使用 float 或 double。浮点数在二进制里无法精确表示商城涉及钱一点点精度误差都可能引发对账问题。这个道理我在培训新人时反复强调。订单状态用 tinyint 存数字不存字符串。原因很简单数字占用空间小查询快而且状态枚举在代码里是明确的不会因为文案改动影响数据。前端展示时再映射成对应文字。订单明细表t_order_item记录每个商品的快照信息商品名称、单价、数量、小计金额。这里必须把商品名称和价格冗余进来不能只存 goodsId。因为商品可能后续改名、调价甚至下架如果订单明细只关联 goodsId历史订单显示的信息就全都乱了。快照思维在电商系统里非常重要。2.3 字段设计要点和索引规划索引这块我的原则是按查询需求建索引不为建而建。开发时先想清楚有哪些查询路径再决定索引不要一上来把所有字段都建上索引。订单表的索引就是上面的设计订单号建唯一索引因为订单号有唯一性约束userId 建普通索引因为个人中心要查我的订单status 建索引因为后台管理经常按状态过滤订单。注意联合索引的使用如果常见查询是按用户 状态查订单那建(user_id, status)联合索引比分别建两个索引效率更高。商品表、商品分类表、购物车表、用户表、会员地址表、公益项目表、贡献记录表的建表逻辑类似都在高频查询字段上建索引。比如商品表的上下架状态和分类ID贡献记录表的用户ID和订单ID。一个容易忽略的点是字符集和排序规则。MySQL 8.0 默认字符集已经改成 utf8mb4比 utf8 能多存 emoji 等四字节字符。但如果你用的还是 MySQL 5.7建表时一定要显式指定DEFAULT CHARSETutf8mb4否则表情符号和生僻字会存不进去前端输入个昵称带个 emoji 就直接报错。排序规则我用utf8mb4_general_ci对中文场景够用性能也好。还有一个我后来才意识到的所有表必须带create_time和update_time两个时间字段能自动填充就自动填充。排查问题时这两个字段是救命稻草比如用户说我下单后订单消失了你一看create_time和update_time就能判断是不是状态被更新了。3. 后端关键实现与业务逻辑3.1 SpringBoot 工程结构怎么组织后端的包结构这样的com.example.charitymall ├── controller # 接口层 │ ├── UserController.java │ ├── GoodsController.java │ ├── CartController.java │ ├── OrderController.java │ └── AdminController.java ├── service # 业务层 │ ├── UserService.java │ ├── OrderService.java │ ├── CartService.java │ └── GoodsService.java ├── mapper # MyBatis Mapper接口 ├── entity # 数据库实体 ├── dto # 接口传输对象 ├── common # 统一响应、异常、工具类 └── config # 配置类安全、跨域、拦截器这个结构看起来常规但有几个值得讲清楚的细节。实体类和数据传输对象必须分开。entity对应数据库表结构字段和列一一映射dto是接口层的输入输出载体字段可以自由裁剪组合。很多人图省事直接用 entity 接收前端参数返回接口数据也用 entity短期没问题一旦业务变化就会很痛。比如用户表的密码字段如果直接用 entity 返回给前端密码哈希就泄露了。我用一个UserInfoVO专门做脱敏返回。Controller 层只负责三件事接收参数、调用 Service、封装响应。所有业务校验逻辑放 ServiceController 里不要出现 if else 判断业务规则。一方面是为了复用同一个校验规则可能多个接口都要用另一方面是可测试性Service 层的业务逻辑可以脱离 HTTP 层单独写单元测试。3.2 MyBatis 动态 SQL 是查询灵活性的关键商品列表页的筛选条件在不同页面差异很大首页要按销量排序分类页要按分类 ID 筛选搜索页要按关键字模糊查询并支持价格区间。写死 SQL 无法覆盖这么多组合MyBatis 的动态 SQL 正好处理这个。看一个商品条件查询的 Mapper XMLselect idselectGoodsByCondition resultTypecom.example.charitymall.entity.Goods SELECT id, goods_name, price, stock, cover_url, sales_count, status FROM t_goods where if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND goods_name LIKE CONCAT(%, #{keyword}, %) /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if AND status 1 /where ORDER BY choose when testsortType salessales_count DESC/when when testsortType price_ascprice ASC/when when testsortType price_descprice DESC/when otherwisecreate_time DESC/otherwise /choose LIMIT #{offset}, #{pageSize} /select注意where标签的用法它会自动去掉条件块开头的 AND同时如果没有条件成立整个 WHERE 子句也不会出现避免语法错误。聚合查询用 Map 接收还是建一个独立 VO我的做法是建独立统计 VO比如ContributionStatVO包含 charityProjectId、projectName、totalContribution 字段。用 Map 接收虽然省事但字段名不明确前端联调时容易对不上、找不着数据含义。建 VO 能让接口的返回结构清清楚楚。动态 SQL 的调试是个难点。我强烈建议在application.yml里把 Mapper 的日志级别设为 DEBUGlogging: level: com.example.charitymall.mapper: debug这样 MyBatis 会把最终执行的真实 SQL 打出来排查 SQL 拼接问题特别高效。我记得一开始调商品条件查询时有个if条件判断了错误的参数名SQL 里直接缺了一块条件就是靠日志把真实 SQL 打出来一眼看穿的。3.3 下单流程与事务控制下单是整个系统最核心、最容易出问题的环节完整链路是校验购物车或直接购买的商品 → 校验库存 → 计算金额 → 生成订单 → 扣减库存 → 生成订单明细 → 生成贡献记录。这条链路涉及多张表的写入操作任何一步失败都必须全部回滚。我用Transactional解决Transactional(rollbackFor Exception.class) public OrderVO createOrder(Long userId, CreateOrderRequest request) { // 1. 查询购物车数据 ListCartItem cartItems cartMapper.selectCheckedItems(userId); if (cartItems.isEmpty()) { throw new BizException(请选择要结算的商品); } // 2. 循环校验库存 for (CartItem item : cartItems) { Goods goods goodsMapper.selectByIdForUpdate(item.getGoodsId()); if (goods.getStock() item.getQuantity()) { throw new BizException(商品[ goods.getGoodsName() ]库存不足); } } // 3. 生成订单号、计算总金额、插入订单主表 // 4. 插入订单明细表 // 5. 扣减库存 // 6. 计算贡献值、生成贡献记录 return orderVO; }事务这里有两个关键点。第一rollbackFor Exception.class必须写。Spring 的Transactional默认只在 RuntimeException 时回滚如果业务抛的是受检异常比如 IOException、自定义的 Checked Exception事务不会回滚数据就写了一半进数据库。我习惯所有业务异常都继承 RuntimeException同时显式声明 rollbackFor双保险。第二库存校验和扣减必须防并发超卖。我用的方式是查库存时加FOR UPDATE行锁selectByIdForUpdate在事务里锁住商品行这样两个用户同时对同一商品下单时第二个用户的查询会阻塞等第一个用户事务提交后再执行重新读到最新的库存值。这是悲观锁方案并发量不大的商城系统完全够用。想再优化可以改成乐观锁 重试用UPDATE t_goods SET stock stock - #{num} WHERE id #{id} AND stock #{num}利用受影响行数判断是否超卖。这个方案我第一次没采用因为涉及重试机制代码复杂度上来了第一版先保证正确性。还有一个很容易踩的事务失效场景我放在后面第六节专门说。3.4 登录鉴权与接口安全用户端和管理端的鉴权我统一用 JWT。用户登录成功后后端生成一个携带用户ID和角色信息的 token 返回前端存在 localStorage 或 sessionStorage 里每次请求在请求头带上Authorization: Bearer token后端用一个拦截器校验 token 有效性。具体实现是写一个AuthInterceptor注册进 Spring MVC 的拦截器链放行登录注册接口其余接口全部校验 token。管理端接口额外校验角色必须是管理员否则返回 403。这里有个安全细节 token 里不要放敏感信息只放 userId、userName、role 这些非敏感字段。密码哈希、手机号、身份证这种绝对不放。因为 JWT 是明文编码的base64 解码就能看到内容。我的 token 用的是 HS256 签名密钥放在配置文件里通过Value注入不会写死在代码里。密码存储用 BCrypt 加密不用 MD5。MD5 加盐虽然也能用但 BCrypt 是专门为密码设计的自适应哈希算法计算成本可调抗暴力破解能力比 MD5 强一个量级。Spring Security 的BCryptPasswordEncoder直接用就行用起来也就两行代码的事。越权问题也要防。比如用户只能查询自己的订单接口层光靠 token 还不够必须从 token 解析出 userId再和查询条件里的 userId 做比对。不能只依赖前端传入的 userId 参数那样随便改个参数就能看别人的订单。我在 OrderController 里统一用CurrentUserHolder.getUserId()获取当前登录用户ID前端传的 userId 一律忽略。4. Vue3 前端项目实现与联调细节4.1 前端工程结构和状态管理前端用的 Vite 脚手架创建 Vue3 项目目录结构按业务模块划分src ├── api # 接口请求封装 │ ├── goods.js │ ├── cart.js │ ├── order.js │ └── user.js ├── assets # 静态资源 ├── components # 公共组件 ├── router # 路由配置 ├── store # Pinia 状态管理 │ ├── user.js │ └── cart.js ├── views # 页面组件 │ ├── home │ ├── goods │ ├── cart │ ├── order │ ├── user │ └── admin ├── utils │ ├── request.js # axios 封装 │ └── auth.js # token 存取 └── App.vueaxios 封装是整个前端基础设施的核心我统一在utils/request.js里做了请求拦截和响应拦截。请求拦截器统一加 token响应拦截器统一处理 code 非 0 的情况弹出错误提示并把用户踢到登录页。// utils/request.js import axios from axios import { ElMessage } from element-plus import { getToken, clearToken } from ./auth import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token getToken() if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 0) { ElMessage.error(res.message || 请求失败) if (res.code 401) { clearToken() router.push(/login) } return Promise.reject(new Error(res.message)) } return res.data }, error { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default request状态管理用 Pinia比 Vuex 简单不少。购物车的数据结构和后端保持一致一个数组每项包含 goodsId、goodsName、price、quantity、checked 标记。购物车的数据我选择存在后端数据库里不存本地这样用户换设备购物车还能同步。商品列表页的筛选条件用 URL query 传递这样用户可以分享链接、刷新页面后筛选条件不丢。4.2 核心页面和交互怎么实现商品列表页是一个典型的筛选 列表组合。我用onMounted里调用getGoodsList筛选条件改变时重新拉数据。需要注意的地方是搜索按钮和重置按钮的处理搜索重新拉第一页重置清空所有条件再拉数据。分页用 Element Plus 的el-pagination切换页码时带着当前筛选条件请求。购物车页面有个典型的交互陷阱商品数量和选中状态都是立即生效的操作我会在用户点击加号、减号、勾选框时立刻调用后端接口而不是等用户点结算才一次性提交。一开始我图省事把购物车数据全量提交结果用户改数量后不结算就走了再回来发现数量和上次不一样体验很糟。改成即时同步后数据一致性问题就没了。下单确认页面要展示三块信息收货地址、商品明细、支持哪个公益项目。公益项目选择用单选卡片组默认选中一个公益项目用户也可以切换。这样下单时把 charityProjectId 传给后端后端生成订单时写进t_order表。路由守卫必须提一句。没有登录的用户访问我的订单购物车结算这些页面要直接跳转到登录页登录成功后再回到之前的页面。我在 router 的beforeEach里判断目标路由的meta.requiresAuth和 token 是否存在。router.beforeEach((to, from, next) { if (to.meta.requiresAuth !getToken()) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })4.3 前后端联调时的接口约定联调阶段出问题最多的地方是接口返回结构和字段命名不一致。前端用的字段是驼峰命名goodsName后端 Java 实体也是驼峰。但 MyBatis 查询出来映射到实体没问题转成 JSON 后也保持驼峰所以这里天然一致前提是后端不要再写一大堆 getter setter 搞乱。时间格式是联调最常见的坑。Java 侧 LocalDateTime 序列化成 JSON 默认格式是2024-05-15T10:30:12中间带个 T前端显示很不友好。我在配置里全局统一了序列化格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8时区也要显式指定 GMT8否则部署到服务器上如果系统时区不是中国时区用户看到的时间就会差 8 个小时。这个问题我在测试环境踩过明明存的正确查出来却少了 8 小时差点以为数据库写错了。联调时另一个建议是开发环境用 Vite 代理解决跨域。在vite.config.js里这样配server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }前端所有请求走/api前缀Vite 把请求代理到后端 8080 端口。这样浏览器里看不到跨域后端也不用额外配 CORS等部署到 Nginx 时再用反向代理处理生产环境天然同源。5. 环境搭建与部署上线5.1 本地开发环境怎么搭后端的运行环境JDK 1.8 或 17 都行Maven 3.8MySQL 8.0IDEA。前端环境Node.js 16npm 或 pnpm。搭建过程中最容易卡住的是依赖下载国内网络环境下 Maven 仓库建议配阿里云镜像npm 配淘宝镜像否则下载 Spring Boot 依赖和 Vite 依赖能让人怀疑人生。后端启动步骤先在 MySQL 里执行初始化 SQL 脚本建库建表然后修改application.yml里的数据库连接信息最后运行主启动类。启动时会加载 Mapper XML如果 XML 写错了会直接启动报错所以第一次启动看到报错先不要慌看日志里提示的是哪个 mapper 文件。前端启动步骤npm install安装依赖npm run dev启动开发服务器默认 5173 端口。Vite 的冷启动速度很快基本秒开。如果npm install报错先把node_modules删了重新装很多莫名其妙的依赖问题这样都能解决。5.2 打包与生产部署前端打包用npm run build产物会生成到dist目录。强调的是构建前要把baseURL配置成相对路径或同域下的/api不能是http://localhost:8080否则部署到服务器后请求地址就写死了。后端打包用 Maven 的mvn clean package -DskipTests打出来的 jar 包直接扔服务器java -jar charity-mall-1.0.0.jar --spring.profiles.activeprod生产环境我用 Nginx 做静态文件服务和反向代理。Nginx 配置有两块关键内容把/路径指向dist目录做前端静态资源服务把/api/路径反向代理到后端服务端口server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; 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; } }try_files $uri $uri/ /index.html这行很重要否则刷新 Vue Router 的 history 路由页面时会报 404。因为浏览器请求的是/goods/1服务器上并没有这个物理文件必须让 Nginx 把所有路由都回退到index.html再由前端路由接管。数据库初始化用 SQL 脚本文件导入。我建议把建表和初始化数据分成两个脚本schema.sql只负责结构data.sql放入初始分类、管理员账号、公益项目等基础数据。分工清楚后续测试环境重置也方便。5.3 性能和并发上的优化实践上线之后用压测工具简单跑了一下发现商品列表接口响应偏慢排查下来是商品详情查询里有个循环查询分类名称的问题一次性查出 50 条商品然后每条商品单独查一次分类表产生 50 条 SQL。优化方案很简单——先查出商品列表再用WHERE category_id IN (...)一次性查出所有分类代码里做内存映射。SQL 数量从 51 条降到 2 条接口响应时间从 800ms 左右降到 150ms。这种 N1 查询问题在 MyBatis 项目里非常常见一旦发现循环里查数据库就要警惕。数据库连接池我用的 HikariCPSpring Boot 2.x 默认集成。连接池配置里maximum-pool-size我设置在 20minimum-idle设置在 5。有个经验是连接池不是越大越好MySQL 默认连接数上限有限连接池设太大反而拖垮数据库。并发量上来了优先加缓存、优化 SQL不要盲目调大连接池。商品详情页加了一层本地缓存用 Caffeine 做的缓存键是 goodsId缓存过期时间 5 分钟。商品信息属于读多写少的数据缓存收益很大。订单这类写操作频繁、数据一致性要求高的数据不做缓存直接走数据库。6. 常见问题与排查实录6.1 跨域问题是不是都怪后端开发阶段遇到跨域报错先别急着改后端代码。前后端分离项目开发环境最推荐的方式就是 Vite 代理前端配置server.proxy即可。但如果前端直接调用后端地址比如axios.get(http://localhost:8080/api/goods)就一定会跨域后端就得配 CORS。我通常两套方案都保留开发环境优先代理后端也配一个 CORS 配置类万一有人直接用 IP 调试时不至于蒙圈。后端 CORS 配置要注意一个问题如果同时用了拦截器并且拦截器里直接返回了 401 错误OPTIONS 预检请求也会被拦截。必须把预检请求放行否则前端实际请求发不过去。我见过几次前端报跨域后端查日志发现 OPTIONS 请求被拦截了调了半天 CORS 配置都没用实际上问题在拦截器里。6.2 Transactional 失效的几种情况这个坑必须单独说因为我第一次做商城项目时就在这里栽过。写了下单的Transactional测试时故意让库存扣减抛异常结果订单表里还是写进去了数据。排查半天发现我的事务方法在同一个类里被另外一个方法调用了而 Spring 的Transactional是基于代理的同类内部调用不会经过代理对象事务注解直接失效。解决方案有两个把事务方法移到另一个 Service 类里调用或者注入自身代理。我用的第一种把下单逻辑单独抽到一个OrderTransactionService写完后再调。这样结构也更清晰事务边界更明确。另一个事务失效场景是方法不是 public 的事务注解不生效。Spring 官方文档明确说Transactional只能作用于 public 方法。当时我为了代码少写几个修饰符把方法写成默认访问权限结果事务同样失效了。还有一个环境Transactional和异步方法组合时异步调用的事务是独立的报错不会回滚主线程的事务。这个场景在生成贡献记录时差点踩到如果用了Async发通知、记日志记得这些异步操作不参与主事务。6.3 MyBatis 参数和 ResultMap 的坑MyBatis 传参时如果 Mapper 接口方法有多个参数必须用Param注解给每个参数起名否则 XML 里只能按param1、param2这种名字取写着写着就乱了。我在商品条件查询接口里用了Param(categoryId)、Param(pageSize)这种命名方式XML 里一眼能看出对应关系。还有Integer和int的区别。XML 里if testprice ! null判断的是封装类型如果参数是基本类型 int默认值是 0不可能为 nullif永远成立就会拼上price 0这种错误条件。所以前端传参时Spring MVC 会把缺失参数封装成 nullMapper 参数那侧用到封装类型 Integer、Long 而不是基本类型这个细节很值得留意。ResultMap 我基本都显式定义用autoMapping反而容易出问题。当数据库字段是下划线命名goods_nameJava 实体是驼峰goodsName如果项目全局开启了map-underscore-to-camel-case: true大多数普通字段能自动映射。但遇到 JOIN 查询结果中两个表都有create_time字段映射就会互相覆盖。显式 ResultMap 能明确每个字段的来源排查问题时看得清清楚楚。6.4 前端联调里的几个小问题第一个是 token 过期后请求还在继续。我在 axios 响应拦截器里判断 code 为 401 后清 token 并跳转登录页但如果同时有多个请求都返回 401会跳转多次。可以加一个标记位控制只跳转一次。第二个是购物车数量同步问题。用户开两个标签页操作同一个账号后端购物车数据会被覆盖。这个属于单机版系统的固有局限第一版没做多端实时同步记录下来作为后续优化项。如果想做可以用 WebSocket 推送或轮询方案但这在第一版确实没必要。第三个是图片上传。商城商品需要上传图片我第一版用了本地磁盘存储把图片存到服务器的/data/images目录并做了静态资源映射。这种做法简单但要注意两点生产环境 Nginx 要额外配一个/images/路径指向这个目录服务器重启或迁移时图片文件要单独备份。如果图片量大后面应该接对象存储但第一版先跑通功能不提前引入复杂度。7. 后续还能怎么扩展整个系统做完我自己的感觉是主链路已经稳了但离商业化系统还有距离。如果这个项目还要继续往深处走有几个方向值得考虑商品模块可以加多规格 SKU、优惠券和营销活动订单模块可以对接支付网关把模拟支付替换成真实支付贡献体系可以加用户累计贡献等级、公益项目透明度展示性能上可以引入 Redis 缓存商品热点数据、用消息队列处理下单后的贡献计算和通知。这些扩展方向和现有表结构没有冲突因为我设计的订单表、订单明细表、贡献记录表在建模时就已经给后续扩展留了余地。最后分享一个我做项目时的个人习惯每做完一个模块我会把当时踩的坑和排查思路记录在一个文档里哪怕只是两三行字。这个项目做到后期这份踩坑笔记已经积累了上百条很多问题看一眼现象就能直接定位。等到再做一个类似系统时前期的坑基本不会再踩第二遍开发速度快了不止一倍。这种沉淀比代码本身更有价值建议你也试试。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑