
如果你正在找一套能真正练手、能看懂企业级开发流程的完整项目代码那这套“企业级免税商品优选购物商城管理系统”值得你花点时间琢磨。技术栈就是SpringBoot Vue MyBatis MySQL前后端分离业务链路完整从商品管理、购物车、订单流转到会员权益都有落地实现不是那种只有CRUD的玩具项目。这类源码最大的价值不在于“能跑起来”而在于它把企业开发里那些你没在教科书上见过的东西串起来了——动态SQL怎么写、订单金额怎么用BigDecimal算、权限控制怎么做到接口级别、前端路由守卫怎么拦未登录用户。下文我按实际开发视角把这套系统的设计思路、核心实现细节、部署过程和踩坑记录都拆开讲一遍希望能帮你少走弯路。1. 项目蓝图企业级免税商城到底在做什么1.1 为什么是SpringBoot Vue这套组合先聊技术选型。很多人在拿到一套源码时习惯性直接开跑但理解“为什么这样选”才是收获最大的一件事。后端用SpringBoot核心原因是生态成熟、约定大于配置。免税商城这种业务场景背后需要的是稳定的依赖管理、事务控制、接口安全、第三方对接支付、清关、物流SpringBoot的starter机制能把这些能力快速集成进来而且社区资料丰富遇到问题搜得到解决方案。相比传统SSHStruts Spring Hibernate那套XML配置地狱SpringBoot几乎把开发体验提升了一个量级。前端Vue的选型逻辑也很清晰。商城类系统的特点是页面交互多、状态变化频繁——商品列表要筛选排序、购物车要实时计算总价、订单状态要动态刷新。Vue的响应式数据绑定天然适合这种场景组件化开发又让页面复用变得简单商品卡片、订单条目、地址选择都能抽成独立组件。而且Vue对后端工程师友好学习曲线比React平缓Javaer转型做全栈时通常首选就是它。MyBatis在这套体系里的角色是持久层框架它和SpringBoot、MySQL的组合在我看来是当前中小型电商项目最稳妥的方案。MyBatis对SQL的掌控力极强像免税商城这种价格计算规则复杂、报表统计维度多的业务手写SQL能精确控制每一条查询的性能和逻辑这是JPAJava Persistence API那种“自动生成SQL”的方式比不了的。MySQL就不多说了开源、稳定、主流云厂商都支持商城业务的数据量级商品几万条、订单几十万条完全在MySQL的能力范围内配合索引和缓存优化性能完全够用。1.2 免税商城业务的特殊性在哪里普通电商系统的套路是商品上架、用户下单、支付、发货。免税商城在这个基础上多了几个关键差异点这也是这套源码真正有含金量的地方。第一是价格计算模型。免税商品涉及完税价格、税费减免、会员等级折扣、汇率折算等多重规则一个商品展示给用户的价格可能是多个因子叠加计算后的结果。普通商城“单价×数量”就完事免税商城需要在订单里区分商品原价、税费、优惠金额、实付金额并且每一步都要保留计算痕迹方便对账和审计。第二是购买资格校验。免税购物通常有购买额度限制系统需要在用户下单时检查其历史购买记录加上当前订单的金额不允许超过额度上限。这就是为什么订单模块不仅要有基本的CRUD还要有资格校验、额度扣减、超限拦截这一系列逻辑。第三是会员体系的重要性被放大了。免税商城属于高客单价场景用户复购和忠诚度是核心运营指标。源码里实现了会员等级、积分累计、等级折扣等功能不同等级对应不同折扣比例订单创建时自动应用。如果你只是把这套源码当成“又一个电商项目”那确实看不出什么特别。但从业务建模的角度看它教会你的是如何在通用电商架构之上通过扩展设计去适配特定领域的业务规则。1.3 源码整体分层结构这套源码采用标准的前后端分离结构这是目前企业级项目的主流形态。duty-free-server/ # 后端 SpringBoot 工程 ├── src/main/java/com/example/dutyfree/ │ ├── controller/ # 接口层接收请求、返回结果 │ ├── service/ # 业务逻辑层处理具体业务 │ ├── mapper/ # MyBatis接口层定义数据库操作 │ ├── entity/ # 实体类对应数据库表结构 │ ├── dto/ # 数据传输对象接口请求/响应封装 │ ├── config/ # 配置类跨域、拦截器、静态资源映射 │ ├── common/ # 公共类统一返回结果、异常处理、常量 │ └── utils/ # 工具类JWT工具、金额计算工具 └── src/main/resources/ ├── mapper/ # MyBatis XML文件 └── application.yml # 配置文件 duty-free-web/ # 前端 Vue 工程 ├── src/ │ ├── api/ # 接口请求模块 │ ├── router/ # 路由配置 │ ├── store/ # 状态管理 │ ├── views/ # 页面组件 │ ├── components/ # 公共组件 │ └── utils/ # 请求封装、工具方法 └── package.json后端采用经典的三层架构Controller接收请求 - Service处理业务 - Mapper操作数据库职责清晰每一层只做自己该做的事。实体类、DTO、VO分离避免数据库字段直接暴露到前端接口。前端按业务模块划分views目录商品浏览、购物车、订单中心、个人中心各司其职公共组件抽离可复用的UI块比如商品卡片、数量选择器、价格展示组件。API层集中管理所有接口请求方便统一处理请求头、错误码和异常提示。2. 后端核心SpringBoot MyBatis的设计细节2.1 SpringBoot事务与异常处理在订单模块中的实践订单创建是商城系统里最典型的需要强一致性的业务。一个完整下单动作涉及多个数据库操作插入订单主表、批量插入订单明细、扣减商品库存、校验并扣减购买额度。任何一个步骤失败必须全部回滚否则就会出现“订单生成了但库存没扣”这种数据不一致问题。源码在订单Service层使用了Transactional注解实现声明式事务默认遇到RuntimeException就回滚。这里有个细节很多新手容易忽略Transactional只对public方法生效而且在同一个类内部通过this调用事务方法是失效的。如果你在OrderService里写了一个私有方法然后由另一个public方法调用它事务不会生效。正确做法是把事务边界放在最好的粒度上——通常是Service层的public方法。异常处理方面源码提供了一个全局异常处理器用RestControllerAdvice统一捕获异常并返回规范的JSON格式。这样做的好处是前端不用关心异常堆栈只需要判断返回码和消息。常用的几个异常类型——业务异常、参数校验异常、系统异常——分别封装了不同的错误码方便日志排查和前端提示。企业级项目还有一个标配统一的返回结果封装。源码里有个Result类所有接口都返回Result.success(data)或Result.error(code, msg)格式。千万别嫌麻烦直接返回实体类后续如果要加签名、加时间戳、做统一加密有了这层封装你会感谢当初的自己。2.2 MyBatis动态SQL的实战用法MyBatis在这套系统里最出彩的地方是动态SQL。商城商品列表页的筛选条件是典型的动态场景——用户可能按分类筛选可能按价格区间筛选可能按品牌筛选也可能是多条件组合。用原生JDBC写这类查询你得手动拼接SQL字符串还容易引发SQL注入风险用MyBatis的where和if标签逻辑清晰且参数自动预编译。商品列表查询大致长这样select idselectProductList resultTypeProductVO SELECT p.id, p.name, p.price, p.category_id, p.brand_id, p.main_image FROM product p where if testcategoryId ! null AND p.category_id #{categoryId} /if if testbrandId ! null AND p.brand_id #{brandId} /if if testbrandId ! null AND p.brand_id #{brandId} /if if testminPrice ! null AND p.price gt; #{minPrice} /if if testmaxPrice ! null AND p.price lt; #{maxPrice} /if if testkeyword ! null and keyword ! AND (p.name LIKE CONCAT(%, #{keyword}, %) OR p.description LIKE CONCAT(%, #{keyword}, %)) /if /where choose when testsortRule price_asc ORDER BY p.price ASC /when when testsortRule price_desc ORDER BY p.price DESC /when otherwise ORDER BY p.sales DESC /otherwise /choose /selectwhere标签会自动处理第一个AND前缀的问题条件都不满足时不会生成多余的WHERE条件满足时会正确拼接。choose标签相当于Java里的switch用于排序规则的动态选择。这里分享一个踩过的坑MyBatis比较单个数字字符时有个经典问题。比如你想判断某个状态字段是否等于某个字符1写成if teststatus 1.toString()或者if teststatus 1都可能出问题——当status是Character类型时和String比较会产生类型不一致或ASCII码比较错误。最稳妥的方式是后端先把参数转换成字符串或使用整数类型或者在SQL里用CAST(status AS CHAR) 1这种显式转换。总之数值比较统一用整数字符串比较统一用字符串混用就是在给自己埋雷。2.3 MyBatis缓存机制在商城场景下的取舍MyBatis有一级缓存和二级缓存很多初学者在项目里盲目开启结果碰到数据不一致的问题后又开始怀疑人生。搞清楚这两个缓存的机制你才知道在商城系统里应该怎么用。一级缓存是SqlSession级别的同一个SqlSession内执行相同的查询会命中缓存。但在Spring整合MyBatis的环境中每次Mapper操作通常都是独立的SqlSession默认每次请求都会创建新的一级缓存基本等于没用。而且一级缓存有个著名的坑在Spring事务环境下如果事务内先查了数据再执行了更新操作一级缓存会失效并清空但如果没提交事务再查时缓存可能返回旧数据。二级缓存是namespace级别的也就是Mapper级别的跨SqlSession共享。理论上看开启二级缓存能减少数据库查询压力但商城这种业务场景我强烈建议关闭它原因有三个第一商品和订单数据更新极其频繁新增、修改、删除操作都会导致对应namespace的缓存失效缓存命中率其实很低反而增加了缓存维护的开销。第二商城系统一旦需要多人协作开发很容易出现一个Mapper查出来的对象被另一个Mapper更新二级缓存不一致的概率很高。第三真正的缓存重任应该交给Redis而不是MyBatis的本地缓存。这套源码的做法是默认关闭MyBatis二级缓存重点依赖MySQL索引优化和Redis做热点数据缓存比如首页热销商品、分类导航。这个思路才是企业级项目的正确姿势。2.4 JWT权限控制与会话管理前后端分离架构下Session的跨域共享是个麻烦事源码采用JWTJSON Web Token做无状态登录认证。用户登录成功后后端生成一个包含用户ID、用户名、过期时间的Token返回给前端前端存到本地localStorage或Pinia状态之后每次请求都在HTTP Header里带上Authorization: Bearer token。后端通过一个拦截器统一校验TokenSpringBoot里实现HandlerInterceptor接口在preHandle方法里解析Token、验证有效性、把用户信息塞到ThreadLocal里供后续业务使用。接口权限通过RequirePermission之类的自定义注解实现注解里标注需要的角色或权限码拦截器里判断当前用户是否拥有对应权限。这个方案的好处是全链路无状态后端不存Session方便水平扩容多台服务器之间无需同步会话数据。缺点也有——Token过期时间不好把握设太短影响体验设太长增加安全风险。源码的做法是Token有效期设24小时同时前端在请求拦截器里检测到Token即将过期时跳转登录页。项目上线时一般会优化成双Token机制短效AccessToken 长效RefreshToken但作为学习项目单Token方案逻辑更清晰更容易理解。3. 数据库设计MySQL表结构与落地实践3.1 核心业务表拆解这套源码的数据库设计遵循了电商系统经典的“SPU SKU 订单明细”模型。核心表大致有这些表名作用关键字段category商品分类多级id、parent_id、name、level、sortproduct商品SPU表id、category_id、brand_id、name、price、main_imageproduct_sku商品SKU表规格库存id、product_id、specs、stock、price、barcodecart_item购物车条目id、user_id、sku_id、quantity、checkedorder_info订单主表id、order_no、user_id、total_amount、status、pay_timeorder_item订单明细表id、order_id、sku_id、product_name、price、quantitymember会员表id、user_id、level、points、total_spentuser_address收货地址表id、user_id、receiver、phone、province、detailSPU和SKU为什么要分开简单解释SPU是“商品”比如“某品牌保湿精华液50ml”SKU是“具体可销售的规格”比如同一款精华的“单瓶装”和“双瓶装礼盒”价格和库存都可能不同。商品列表页展示的是SPU加入购物车和下单的是SKU。这个设计在真实商城里是标配理解了它你对商品建模就有了基本概念。订单主表和订单明细表是“一对多”关系设计成两张表的原因是为了减少数据冗余。订单主表只存订单级别信息订单编号、总金额、状态、收货信息订单明细表负责存下单时的商品快照信息商品名称、价格、数量。注意商品快照和商品表是解耦的——下单后即使商品改名、改价该订单还是要保留当时的成交信息这也是对账和售后的依据。3.2 订单金额计算的精度问题商城系统里金额计算是绝对不能马虎的环节。看到用double存价格的项目我一般直接判死刑——浮点数在二进制里无法精确表示0.1 0.2的结果是0.30000000000000004在价格计算中这种误差是不可接受的。正确的做法是全链路使用BigDecimal。数据库金额字段用DECIMAL(10, 2)Java实体类用BigDecimal所有金额计算都用BigDecimal的方法。订单金额的计算逻辑大致是// 订单金额计算 BigDecimal productAmount orderItems.stream() .map(item - item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); // 会员折扣 BigDecimal discountAmount productAmount.multiply(BigDecimal.valueOf(memberDiscountRate)); // 税费计算 BigDecimal taxAmount (productAmount.subtract(discountAmount)) .multiply(taxRate) .setScale(2, RoundingMode.HALF_UP); // 应付金额 BigDecimal payableAmount productAmount .subtract(discountAmount) .add(taxAmount) .setScale(2, RoundingMode.HALF_UP);每一笔金额的保留位数和舍入规则必须固定。这里有个容易被忽视的细节不要在中途setScale要在最终结果统一处理精度否则中间步骤的舍入误差会累积放大。另外税费、折扣率这些参数尽量做成数据库配置表不要硬编码因为活动配置经常变。3.3 MySQL索引优化与慢查询排查商品列表页和订单列表页是商城系统里查询压力最大的两个页面索引设计直接决定了接口的响应时间。源码里比较有价值的几个索引设计我列一下订单表按用户查询的场景最多所以必建(user_id, create_time)联合索引这样一次查询既过滤用户又按时间排序避免文件排序。订单状态查询场景也用得多status字段加单列索引即可。商品表按分类筛选category_id加索引如果经常按价格区间筛选可以考虑(category_id, price)联合索引覆盖“分类 价格”的组合筛选。排查SQL性能问题第一步是开启MySQL慢查询日志-- 查看当前慢查询配置 SHOW VARIABLES LIKE slow_query_log; SHOW VARIABLES LIKE long_query_time; -- 开启慢查询日志可以动态开启 SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;设置之后执行时间超过1秒的SQL会被记录到日志文件中。然后针对慢SQL执行EXPLAIN查看执行计划重点看type字段——如果是ALL全表扫描或者rows非常大说明索引没走对需要调整。分页深翻页是另一个经典坑。比如LIMIT 100000, 20即使有索引MySQL也得先扫描10万行再丢弃性能极差。优化方式是改成“基于游标”的分页WHERE id 上一次查询的最小id ORDER BY id DESC LIMIT 20或者使用延迟关联先用覆盖索引查出主键再关联回表查完整数据。3.4 主键策略选择自增ID还是雪花ID这是一个在商城系统里必须提前想清楚的问题。单机部署、数据量不大的场景自增主键最省心占用空间小、写入性能好、索引维护成本低。但企业级项目一旦面临分库分表自增ID就会产生冲突这时候需要全局唯一的ID生成策略。这套源码使用的是雪花算法Snowflake生成的分布式ID。雪花ID是64位长整型由时间戳 机器ID 序列号组成趋势递增且全局唯一。它在Java里通过IdWorker工具类实现生成速度极快每毫秒可生成数千个不重复ID。唯一的缺点是ID长度比自增主键长存储空间略大而且不直观但考虑到未来可能的分布式扩展这个取舍是值得的。如果你用雪花ID订单号通常是ID的变体或结合业务规则生成的唯一编号。源码里订单号用yyyyMMddHHmmss 用户ID后四位 随机数的格式生成既保证唯一性又方便按时间回溯。4. 前端实现Vue构建商城界面的实战细节4.1 Vue项目搭建与路由设计Vue工程是标准的前后端分离结构入口文件、路由配置、状态管理、API模块一应俱全。拿到源码后先执行npm install安装依赖然后看src/main.js的挂载逻辑和vue.config.js的配置——特别是开发环境的代理配置它能解决前后端分离开发的跨域问题// vue.config.js module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:9090, // 后端服务地址 changeOrigin: true, pathRewrite: { ^/api: } } } } };配置了代理之后前端请求/api/product/list会自动转发到后端http://localhost:9090/product/list开发环境就不需要后端处理CORS了。但生产环境前后端分域部署后端还是得配CORS跨域配置或者统一用Nginx反向代理做成同域访问。路由设计上商城系统通常分为两套路由客户端用户页面首页、商品列表、商品详情、购物车、订单确认、个人中心和管理后台页面商品管理、订单管理、会员管理、数据统计。源码里用动态路由的方式根据用户角色动态添加可访问的路由避免普通用户直接访问后台管理页面。4.2 Vue Router路由守卫实现登录拦截前端权限控制的核心是路由守卫。商城系统里购物车、订单确认、个人中心这些页面必须登录后才能访问前端不能只靠隐藏入口来防因为用户可以直接在浏览器地址栏输入URL。Vue Router的全局前置守卫在每次路由跳转前执行逻辑是检查目标路由的meta信息里是否标记了requiresAuth如果标记了就校验本地Token是否存在不存在就跳转登录页并携带redirect参数登录成功后跳回原页面router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.matched.some(record record.meta.requiresAuth)) { if (!token) { next({ path: /login, query: { redirect: to.fullPath } }); } else { next(); } } else { next(); } });这里强调一个容易被忽略的点前端守卫只是提升用户体验真正的安全防线必须在后端。接口层的Token校验才是核心前端路由守卫的作用是“挡掉未登录用户的多余请求”而不是“保护数据安全”。4.3 Axios封装与接口请求规范前端所有HTTP请求都走统一的Axios封装这一步做得好的项目后期维护会轻松很多。源码里的Axios实例做了三层处理请求拦截器从localStorage取出Token拼接到请求头Authorization字段。响应拦截器统一处理后端返回的Result结构业务码为200时返回data业务码非200时弹出错误提示遇到HTTP 401时清掉本地Token并跳转登录页。// 响应拦截器简化版 service.interceptors.response.use( response { const res response.data; if (res.code 200) { return res.data; } else { Message.error(res.message); if (res.code 401) { router.push(/login); } return Promise.reject(res); } }, error { Message.error(网络异常请稍后重试); return Promise.reject(error); } );统一的请求封装带来两个直接好处第一所有接口的错误处理逻辑一致不会出现“这个接口弹了提示、那个接口白屏”的体验差异第二如果要加Token刷新、请求埋点、全局Loading等能力只需要在拦截器里改一处。4.4 商品详情的视频播放与静态资源处理商品详情页除了图片轮播还需要支持视频展示。这里涉及到一个前后端配合的细节视频文件不会塞进数据库而是上传到服务器指定目录或对象存储数据库只保存视频路径。前端播放时通过URL访问。如果视频格式是常见的MP4直接用HTML5的video标签就行。如果是流媒体格式m3u8需要在视频组件里引入hls.js或video.js来处理。源码里的做法是使用video.js配合videojs-contrib-hls插件组件初始化时判断视频格式m3u8就走HLS播放器MP4就原生播放。静态资源上传这块也有讲究。后端需要配置静态资源映射否则前端访问上传的图片视频会404。SpringBoot里的配置是这样的Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 将 /upload/** 映射到本地磁盘目录 registry.addResourceHandler(/upload/**) .addResourceLocations(file:/data/duty-free/upload/); } }这样上传文件保存到服务器/data/duty-free/upload/前端可以通过/upload/xxx.jpg访问。生产环境更推荐配合Nginx直接映射静态资源目录减少后端压力。4.5 前端打包后布局异常的排查思路源码部署阶段最容易遇到的问题是本地开发一切正常npm run build打包上线后出现布局错乱、样式丢失、白屏或路由404。布局错乱优先检查CSS是否全局污染。如果组件样式没有加scoped打包后样式会被合并覆盖。另外如果用了Element UI等第三方组件库要注意按需引入还是全量引入——全量引入没问题但按需引入时漏了样式文件就会导致组件样式错乱。白屏问题优先检查publicPath配置。Vue CLI默认的publicPath是/如果部署在域名子路径下比如http://example.com/mall/就必须改成相对路径./或者子路径/mall/否则静态资源请求路径不对整个页面起不来。路由404则是history模式下典型的Nginx配置问题需要在Nginx里配置try_files回退到index.htmllocation / { try_files $uri $uri/ /index.html; }5. 环境搭建与部署上线5.1 本地开发环境准备与启动步骤花点时间把环境串起来。拿到源码后我建议的启动顺序是“先数据库、再后端、最后前端”这样每一步都能验证依赖是否就绪。数据库环境安装MySQL 8.x版本太老可能遇到兼容性问题。用Navicat或命令行执行源码根目录下的dutyfree.sql这会创建数据库、业务表结构和初始化数据。应用连接MySQL时确保数据库账号密码和application.yml里的一致重点检查这几项spring: datasource: url: jdbc:mysql://localhost:3306/dutyfree?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你自己的密码 driver-class-name: com.mysql.cj.jdbc.DriverJDBC连接串里的characterEncodingutf8不写中文乱码问题几乎逃不掉。serverTimezone不设置高版本MySQL会报时区错误。后端启动用IDEA导入Maven工程等待依赖下载完成启动DutyFreeApplication主类。如果pom.xml里的依赖下载慢换成阿里云Maven镜像能快不少。启动后访问http://localhost:9090能看到SpringBoot的默认页面或接口返回说明后端起来了。前端启动在duty-free-web目录下执行npm install接着npm run serve默认端口8080浏览器访问即可。Node环境建议用16版本以上太老的Node版本跑Vue CLI新项目会报错。5.2 打包部署项目上线最稳妥的姿势本地跑通后部署上线又是一套独立的流程很多源码从头到尾只在本地跑过没上过服务器这是不完整的。后端打包很直接mvn clean package然后target目录下会生成一个可执行的Jar包比如duty-free-1.0.0.jar。服务器上只要有JDK 8和MySQL就能直接运行nohup java -jar duty-free-1.0.0.jar --spring.profiles.activeprod dutyfree.log 21 前端打包npm run build打包产物在dist目录下是一堆纯静态文件。用Nginx托管这些文件把接口请求反向代理到后端Java服务。一个生产环境下比较完整的Nginx配置长这样server { listen 80; server_name your-domain.com; # 前端静态文件 root /data/duty-free/dist; index index.html; # history模式路由回退 location / { try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:9090/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 上传文件访问 location /upload/ { alias /data/duty-free/upload/; } }核心思路前端路由和接口请求都通过同一个域名访问Nginx根据URL前缀分发到静态文件或后端服务。/api/开头的请求走后端其他请求尝试匹配静态文件匹配不到就回退到index.html交给前端路由处理。6. 实战调试搞定那些让人头秃的坑6.1 高频问题排查速查表开发这种完整项目时遇到的问题很多是共通的。我把这套源码部署和二次开发中常见的坑整理成了表格直接按图索骥问题现象可能原因解决办法前端请求接口报跨域错误后端未配置CORS或代理未生效开发环境配vue.config.js代理生产环境用Nginx反代统一域名MyBatis查询返回字段全部为null实体类驼峰映射未开启application.yml配置mybatis.configuration.map-underscore-to-camel-case: true数据库中文乱码JDBC连接URL没配置编码或表字符集不对URL加characterEncodingutf8建表语句用UTF8字符集前端打包后刷新404路由模式history Nginx未配置try_filesNginx配置try_files $uri $uri/ /index.html;上传图片无法访问后端静态资源映射未配置实现WebMvcConfigurer添加资源映射或用Nginxalias指向上传目录订单金额偶尔差一两分钱浮点运算做了setScale中途舍入全程BigDecimal只在最终结果setScale统一舍入规则线上接口响应慢SQL没走索引或N1查询EXPLAIN分析慢SQL商品列表批量查询改用IN或JOIN页面文字乱码或编译报错Maven依赖冲突或JDK版本不对检查pom.xml依赖版本SpringBoot 2.x用JDK83.x用JDK176.2 高效调试技巧让问题定位快人一步最后分享几个调试技巧这些经验能让你的排查效率提升一个档次。MyBatis SQL日志必须开但要会看。在application.yml里配置mybatis.configuration.log-impl: org.apache.ibatis.logging.stdout.StdOutImpl控制台会打印每条SQL的预编译语句和参数。注意看 Preparing:和 Parameters:两行能直观发现参数没传对、类型不匹配的问题。更推荐安装IDEA插件MyBatis Log Plugin它会自动把预编译SQL和参数拼成完整可执行的SQL复制到Navicat里直接跑排查问题极快。接口调试用Postman或Apifox不要只依赖前端页面。有些问题在页面上看不出来是前端传参问题还是后端返回问题。直接用接口调试工具测试后端接口返回数据正常就说明后端没问题问题定位到前端返回数据异常再一步步往前推。启动报错先看堆栈第一行。很多新手看到控制台一大片红色就懵了其实SpringBoot启动失败的根因通常就在堆栈最上面几行——端口占用、数据库连接失败、Bean创建异常都有明确提示。端口占用用netstat -ano | findstr 9090Windows或lsof -i:9090Mac/Linux查看占用进程找到后杀掉或者改配置里的端口就行。备份是一切的底线。改数据库表结构、改线上配置前先备份。本地开发还好一旦动了线上环境一次误操作可能让你几个小时白干。MySQL备份就一行命令mysqldump -u root -p dutyfree dutyfree_backup.sql写在最后的一些体会把这套源码从头到尾跑通、读懂、再动手改一遍你会把SpringBoot、Vue、MyBatis、MySQL这套组合的完整协作链路理解得很透彻。我个人在实际操作中的感受是看源码最大的收益不是它的业务逻辑而是代码组织的规范性和边界划分意识。哪个逻辑放在Controller、哪个放在Service、哪个写到SQL里这套源码给出了一个相当合理的参考答案。最后再分享一个细节建议拿到任何一套开源商城源码第一件事别急着改功能先把“商品SKU怎么建模的、订单金额在哪里算的、权限是怎么校验的”这三个问题在代码里找到答案。这三个问题搞明白了你对这套系统的理解就超过大半只看过演示的人。后续如果要扩展功能——接入微信支付、对接物流接口、加一个秒杀模块——你就知道该往哪一层加代码了。