
这套商铺管理系统源码第一眼看上去确实像许多校园课程的指定配置SpringBoot Vue MyBatis MySQL标准的前后端分离组合附带完整源码。但如果你真把它当成普通作业看待会漏掉不少值钱的东西。它不是那种只有登录页和几个空白CRUD的演示工程而是按企业级后台的基本要求组织的有完整的角色权限体系、商铺与商品的多级关联、订单同库存的联动扣减、可切换的多环境部署配置甚至连数据落地时的并发扣库存、事务边界、逻辑删除这些细节都做了处理。这系统适合谁两类人最该顺手存一份一类是正在准备毕设或者求职作品的在校学生另一类是刚进公司需要快速上手企业级Java全栈开发的新人。前者可以拿走整套源码做二次开发补全自己的项目经验后者能借这套工程理解一个真实后台系统从表结构设计、后端分层、前端联调到打包上线的全链路。别嫌它技术栈“传统”恰恰因为足够主流你才能在里面看到一套可以直接复用到其他业务系统的组织方式。1. 项目定位与技术选型的真实逻辑1.1 为什么SpringBoot依然是后台系统的第一选择很多新人一提到SpringBoot就觉得“过时了”毕竟现在微服务、云原生这些概念满天飞。但落到企业级管理系统这个具体场景里SpringBoot依然是性价比最高的底座。原因很朴素它把Spring生态里那些繁琐的XML配置、Bean装配全部自动化了配合内嵌的Tomcat打一个Jar包就能跑起来开发和部署成本都极低。更重要的是SpringBoot的周边生态极其成熟。加了spring-boot-starter-web就是一套Web应用加spring-boot-starter-validation就能做参数校验加spring-boot-starter-aop就能搞日志切面几乎你能想到的企业级通用能力都有现成的Starter。这意味着团队招人容易、排查问题容易、后续扩展也有兜底。如果换成更激进的框架组合比如完全响应式编程或者Service Mesh对于这种以CRUD和业务流转为主的管理系统反而是杀鸡用牛刀徒增维护成本。这套源码里对SpringBoot的使用也是典型的规范路子启动类放在根包下保证组件扫描范围配置项按环境拆分AOP切面统一切入Controller层打印请求日志。这些细节看着简单但很多不规范的课程设计恰恰就是栽在“启动类包路径放错导致Bean找不到”“日志打成一团麻”这种基础问题上。1.2 Vue与SpringBoot为什么能配合得这么顺前后端分离这个架构前端选Vue不是偶然。Vue的核心优势是“渐进式”和“组件化”你不需要一次性引入全家桶只需要一个脚手架、一个路由、一个状态管理库就能把单页应用搭起来。对于商铺管理这类有大量列表、表单、弹窗交互相的系统Vue的组件复用能力极其好用比如一个商品表格组件封装好之后订单页面、库存页面都能引用。Vue和SpringBoot的配合关键在于开发流程上的天然对齐。开发阶段前端用Vite或Webpack起一个本地开发服务器通过proxy把/api开头的请求转发到后端的localhost:8080完全规避了开发环境跨域。生产阶段前端build出静态文件丢给Nginx后端只负责提供API两边互不干扰。这套源码里前端vue.config.js和项目根的Nginx配置都是现成的照着改几个IP和端口就能用。相比之下老式的JSP模板方案页面和后端代码耦合在一起前端改个按钮都要重新编译后端工程而纯React的方案虽然也能做但上手曲线和工程复杂度都比Vue高。对于中小型团队和独立开发者来说“SpringBoot Vue”就是公认的最优解不是因为它多先进而是因为它能最平稳地完成从开发到交付。1.3 MyBatis与MySQL的组合为什么比JPA更务实先说MySQL这没什么争议。开源、免费、稳定、性能足够强互联网大厂内部大量业务也是MySQL家族扛着。商铺管理系统这种体量单库单表加几个索引就能扛住绝大多数场景没必要一上来就铺开PostgreSQL或者分布式数据库。MyBatis的定位则在“SQL可控”四个字上。商铺管理这类系统业务查询非常复杂多表关联、动态条件筛选、报表统计MyBatis允许你手写SQL每一次查询的索引命中情况、关联顺序都尽在掌握。相比JPA/Hibernate那种对象关系映射虽然开发时写方法名就能查出数据但复杂查询一旦需要优化生成的SQL往往让人无从下手。这套源码里MyBatis的使用也很克制没有滥用select *多表关联都写了明确的字段列表分页查询用PageHelper插件优雅解决动态SQL用if标签做了条件拼接。这些习惯正是企业开发中对SQL性能和可维护性的基本要求。2. 系统模块设计与核心业务拆解2.1 商铺与商品管理的层级关系企业级管理系统最忌讳的就是“扁平化地堆菜单”。这套商铺系统的业务结构是分层的先有商铺再有商品商铺有经营分类、有店长、有营业状态商品挂靠在商铺下有商品类目、上下架状态、销售规格、库存余量。这种层级设计非常符合真实线下校园商铺的运营模型一个管理平台管理着若干个店铺每个店铺再管理自己的商品。在设计实体时核心是shop_info和goods_info两张表。shop_info里有shop_name、category_type快餐、超市、文印等、status营业/停业、owner_id关联用户表的店长goods_info里有shop_id外键、goods_name、price、stock、sales_count、status在售/下架。一张商品表通过shop_id和商铺表关联避免在商铺表里去存一个商品列表的冗余字段这是最基础但最重要的数据库范式思想。在代码实现上商铺列表页做了分页和条件筛选按名称、状态、经营分类商品列表页也支持按商铺维度筛选。这些筛选逻辑在后端都是通过MyBatis动态SQL实现的条件为空就拼一个恒真条件跳过条件非空就追加and status #{status}。这里面有个细节值得学习状态字段在SQL里使用Integer而不是字符串因为数字在数据库索引里走得更快也比较不容易出现大小写不一致的问题。提示实际二次开发时如果想加“商铺评分”或者“商品标签”这类新字段优先考虑加独立字段或关联表不要硬塞进remark字段里否则后面统计查询会非常难受。2.2 订单与库存的联动扣减逻辑订单是任何交易系统的核心商铺管理系统也不例外。用户在商铺下单之后系统要同时完成两件事创建订单记录、扣减库存。这两步必须在一个事务里完成否则会出现“订单建了库存没扣超卖”或者“库存扣了订单没建钱货对不上”的严重事故。这套源码的订单设计值得细看。订单主表order_info存订单号、商铺ID、用户ID、总金额、支付状态、订单状态订单明细表order_item存商品快照信息包括商品名、单价、数量、小计。为什么要存快照因为商品表里的价格可能随时调整如果只存goods_id将来商品改价了订单历史里的金额就对不上了。这是很多初学者容易忽略的设计订单是事实记录不能依赖未来会变化的基础数据。库存扣减这块源码里用了典型的乐观锁思路。核心SQL长这样UPDATE goods_info SET stock stock - #{count}, sales_count sales_count #{count} WHERE id #{goodsId} AND stock #{count}这段SQL的妙处在于用stock #{count}作为条件直接在数据库层面判断库存充足并且通过受影响行数update返回0表示库存不足来判断是否扣减成功而不是先把库存查出来在Java代码里比较再更新。后者在并发场景下必然出问题两个请求同时读到库存5同时扣3最后库存变成2而不是预期的-1或者0。前者则能保证在并发情况下只有一条扣减成功。这里的“为什么”理解了之后才算是真正读懂了这套源码。2.3 多角色权限体系的设计企业级系统和课设项目最大的分水岭就是有没有一套完整的权限模型。这套系统里有管理员、商铺店长、普通用户三类典型角色管理员可以管理所有商铺和系统配置店长只能管理自己名下的商铺和商品普通用户只能查看和下单。背后用的是经典RBAC模型基于角色的访问控制。数据库里是标准的五张表用户表、角色表、菜单/权限表、用户角色关联表、角色菜单关联表。用户登录后后端查询出该用户拥有的所有权限标识生成JWT token前端拿到token后再请求/api/user/info接口获得可访问的菜单和按钮权限。这样做的好处是新增一个角色时不需要改代码只要在后台给角色勾选菜单权限就行用户改角色后下一次登录权限自动生效。代码层面后端用一个拦截器校验接口权限。每个Controller方法的PreAuthorize(hasAuthority(shop:add))这类注解定义了接口所需权限。前端则配合this.$route.meta里的权限标识做按钮级的控制比如普通用户看不到“新增商铺”按钮不是简单地隐藏而是根本没渲染这个按钮防止直接改接口地址越权操作。2.4 数据统计与可视化实现这个系统的统计模块虽然不是最出彩的部分但逻辑上很值得参考。统计项包括商铺销售总额排行、商品热销榜单、普通用户消费频次、每日订单量趋势等。这些数据在后台通过聚合SQL得出比如查询商铺销售额排行核心SQL是SELECT s.shop_name, SUM(oi.amount) AS total_amount FROM order_info oi LEFT JOIN shop_info s ON oi.shop_id s.id WHERE oi.pay_status 1 GROUP BY oi.shop_id ORDER BY total_amount DESC LIMIT 10这个模块有一个设计心得值得记录统计查询一律走独立接口不去改动原本的列表查询逻辑。因为列表查询追求的是响应速度如果混入聚合计算SQL性能会急剧下降。统计接口还做了参数限制强制要求传startDate和endDate避免把整年的订单全捞出来做聚合导致数据库压力过大。前端的图表展示用ECharts折线图和柱状图分别呈现趋势和排行接口数据直接绑定到图表配置项里改动成本很低。3. 数据库设计核心细节3.1 核心表结构与字段设计思路一套管理系统的地基是数据库表设计表设计糟糕后面写多少代码都救不回来。这套商铺系统的表结构大致分为几个部分功能域核心表关键字段说明系统用户sys_userid, username, password, real_name, phone, status角色权限sys_role, sys_menu, sys_user_role, sys_role_menurole_code, role_name, perms, menu_name, parent_id商铺管理shop_infoshop_name, category_type, owner_id, status, address商品管理goods_info, goods_categoryshop_id, goods_name, price, stock, status, category_id订单交易order_info, order_itemorder_no, shop_id, user_id, amount, pay_status, order_status通用字段所有业务表create_time, update_time, deleted这里重点说三个细节第一是deleted逻辑删除字段。几乎所有业务表都有deleted默认0删除时执行UPDATE ... SET deleted 1查询时统一在SQL末尾带上AND deleted 0。为什么要这样因为物理删除会把历史数据彻底抹掉将来想统计过往订单、找回误删商品就无据可查。这也是企业级系统对数据留存的基本要求。第二是金额字段的统一处理。订单金额、商品原价全部用DECIMAL(10,2)绝不使用FLOAT或DOUBLE。这个坑很多新手踩过浮点数在计算机里是近似值0.1 0.2 会得出0.30000000000000004做金额运算越算越偏。DECIMAL是定点数以字符串形式存储精度可控才是金融数据的正确选择。第三是订单号的生成规则。源码里没有用数据库自增ID当订单号而是用了“时间戳 随机数 用户ID后缀”的方式比如202503081030450001。为什么不用自增ID因为订单号容易暴露业务量且多张表查询时需要一定程度上的唯一性。这个方案虽然没有分布式雪花ID那么严谨但对于单体系统完全够用。3.2 索引设计的关键原则数据库的索引设计直接决定系统在数据量上来之后是否还能流畅运行。这套系统的索引设计有些细节是照搬企业经验的。订单表是查询压力最大的表常见查询是“查某商铺的订单列表”“按状态筛选订单”“统计某段时间的订单数”。所以订单表建了(shop_id, create_time)联合索引很适合商铺维度的范围查询还单独建了(pay_status, order_status)索引覆盖状态筛选场景。商品表则建了(shop_id, status)联合索引因为商铺首页展示的就是“某个店铺所有在售商品”。索引也不是越多越好。每张表的每个索引写入时都会增加一次B树的维护成本。特别是商铺系统这种订单、库存高频更新的场景过度索引会导致写操作变慢严重时还会触发锁等待。经验法则是一个表的索引数量控制在5个以内每个索引都对应一个具体的高频查询场景不能被“顺手”添加。注意修改表结构或索引时如果表数据量已经很大使用ALTER TABLE ADD INDEX一定要评估耗时最好在业务低峰期执行避免长时间锁表把线上业务堵死。3.3 事务与并发一致性处理商铺系统的核心交易链路用户下单时要同时完成订单主表插入、订单明细插入、库存扣减、商品销量更新四件事。任何一步失败整体都应该回滚。源码里直接在Service层方法上加Transactional注解交给Spring管理事务边界。这里有一个值得展开的经验点Transactional有很多“看似没有生效”的场景。最常见的是同类内部调用比如OrderServiceImpl里的createOrder()方法没有加注解但内部调用了另一个加注解的updateStock()方法由于调用方式绕过了Spring代理事务注解不生效。解决方式是把事务方法放到独立的Bean里或者自己注入TransactionTemplate手动管理事务。这套源码里的事务都是在独立的OrderServiceImpl类里完成的避免了这个坑。并发一节提到的库存条件更新其实也是对事务的补充。单靠Transactional只能保证原子性没法解决超卖问题因为哪怕在事务里先select后update两个并发事务都可能读到同一个旧库存值。条件更新WHERE stock #{count}才是真正拦住超卖的那道闸门。这个案例很好地说明了“事务”和“并发控制”是两回事事务保证要么全成功要么全失败条件更新保证并发下数据不越界两者配合才是完整的方案。4. SpringBoot 后端实现中的核心经验4.1 分层结构与统一返回体这套源码的包结构一目了然com.xxx.shop ├── controller // 接收请求、参数校验、返回结果 ├── service // 业务逻辑层 ├── mapper // MyBatis接口 ├── entity // 数据库实体 ├── dto // 前端入参对象 ├── vo // 前端出参对象 ├── config // 配置类 ├── common // 统一返回体、异常处理、工具类 └── interceptor // 拦截器分层最大的好处是职责清晰。Controller只做参数接收和结果转发Service层承载业务逻辑Mapper层只做数据库交互。有个地方特别能体现工程规范Controller层的入参和出参用的是DTO和VO而不是直接把Entity暴露给前端。为什么因为Entity通常含有password、create_time这类内部字段直接返回会导致敏感信息泄露或者返回了前端用不上的冗余字段。这是一个“看着多写了几个类实际省了无数隐患”的做法。统一返回体设计得也很实用public class ResultT { private Integer code; // 200成功500失败401未授权 private String message; private T data; }所有Controller都返回这个结构前端Axios拦截器里统一判断code成功则返回data失败则弹出错误信息。这样前端处理逻辑就不用每个接口单独写一套。4.2 JWT认证与接口安全这套系统用JWT实现了无状态认证。用户登录成功后后端生成一个token返回给前端前端存储在localStorage后续通过请求头Authorization: Bearer token携带。后端拦截器每次从Header中取出token解析并校验过期时间再通过其中的用户ID查出用户权限。选JWT而不选传统Session是因为前后端分离场景下后端接口是无状态的存在Session里反而不方便水平扩展。JWT本身携带用户ID和过期时间验签通过就算通过。但JWT也有坑无法主动失效。如果用户的token泄露只能等待它自然过期。所以这套系统把过期时间设为2小时并且修改密码后会强制重新登录。安全这块还有几个加分细节密码存储使用了BCrypt加密每次登录校验都走BCryptPasswordEncoder.matches()不是明文比对登录接口做了简单的验证码校验即使是一个数学计算题也能拦住大量脚本暴力尝试拦截器放行了/api/login、静态资源等公开路径其余接口全部要求认证。4.3 MyBatis动态SQL与性能排查MyBatis是这项目的重点。先看一个商品列表的动态SQL模板select idlistGoods resultTypecom.xxx.shop.vo.GoodsVO SELECT id, goods_name, price, stock, status, shop_id FROM goods_info WHERE deleted 0 if testshopId ! null AND shop_id #{shopId} /if if teststatus ! null AND status #{status} /if if testkeyword ! null and keyword ! AND goods_name LIKE CONCAT(%, #{keyword}, %) /if ORDER BY create_time DESC /select这种写法的好处是一个Mapper方法可以应对不同组合的查询条件而不是每种查询都单独写一个SQL。执行时MyBatis会根据传入参数动态决定最终SQL条件是空就跳过条件非空才拼接。注意LIKE查询使用CONCAT(%, #{keyword}, %)而不是直接传%${keyword}%前者能防止SQL注入后者则是直接字符串拼接非常危险。排查SQL性能的经验也和MyBatis相关。调试阶段把application.yml里的Mapper日志级别调成DEBUG控制台会完整打印每条SQL及参数数据库执行慢时把SQL拿出来在MySQL客户端跑一遍EXPLAIN看type是不是ref或rangerows是不是扫描了大量行。这套系统里的所有列表查询都确保有对应的索引支持所以在数据量万级左右基本都能稳定在毫秒级响应。4.4 多环境配置与配置安全一个工程要跑在开发、测试、生产三个环境里配置必然不同。这套源码把配置拆成了application-dev.yml和application-prod.yml主配置文件用spring.profiles.active指定当前环境。开发环境数据库连本地日志级别DEBUG生产环境连线上库日志级别INFO、开启慢SQL记录。打包部署时启动命令里通过--spring.profiles.activeprod显式指定环境避免打包时改代码造成混乱。配置安全这块有个很实用的建议数据库密码不要硬编码在application-prod.yml里而是用环境变量引用比如datasource.password: ${DB_PASSWORD}部署时在服务器系统环境变量里设置。这样即使代码仓库不小心被别人克隆数据库口令也不会泄露。这是企业级项目最基本的安全要求。5. Vue 前端搭建与联调技巧5.1 前端目录结构与路由设计前端的目录结构基本对应后端的業務模块打开源码就能顺着名字找到页面src/ ├── api/ // 接口请求封装 ├── assets/ // 静态资源 ├── components/ // 公共组件 ├── router/ // 路由配置 ├── store/ // Vuex状态管理 ├── utils/ // 工具函数 └── views/ // 页面组件 ├── login/ ├── dashboard/ ├── shop/ ├── goods/ ├── order/ └── statistics/路由配置里每个页面都设置了meta字段存放title和roles允许访问的角色。beforeEach全局路由守卫里会判断当前用户角色是否在meta.roles中不在就重定向到403页面。这样即使有人手动在地址栏输入受保护的URL也无法访问页面。登录态的处理也在这块完成Vuex的state里存用户信息和tokenlocalStorage持久化。刷新页面后从localStorage恢复token再调用/api/user/info重新获取用户权限信息保证任何时候刷新都不会白屏。5.2 Axios请求封装与统一错误处理这一步特别值得推荐新手写前端时最常见的问题就是每个页面都单独写一遍axios.get(...)遇到错误弹一个乱七八糟的报错框。这套源码里所有请求都走一个统一定制的axios实例。请求拦截器注入token响应拦截器统一处理返回结构service.interceptors.response.use( (response) { const res response.data if (res.code 200) { return res.data } if (res.code 401) { // token失效跳转登录页 router.push(/login) } Message.error(res.message) return Promise.reject(new Error(res.message)) }, (error) { Message.error(网络异常请稍后重试) return Promise.reject(error) } )这样做的好处显而易见几十个接口错误处理逻辑完全一致后端只要保证code码规范前端就能用10行代码解决全局的异常反馈。api目录下每个文件按业务模块导出请求函数页面里调用shopApi.getShopList(params)即可既清晰又能复用。模板项目里这一个约定能减少至少三分之一的前端代码量。5.3 Element UI 表格与表单的实用细节这个项目的所有管理页面基本都基于 Element UI 的el-table、el-form、el-pagination搭建。有几个使用细节都是实际开发中踩过坑才总结出来的首先是表格的分页。el-pagination的current-page和page-size必须绑定数据改变分页就重新发起请求。同时分页参数还要同步到表单数据里这样筛选条件和分页状态才能一致。有些新手把分页仅仅做成前端裁切数据所有数据一次加载到页面上数据量小看不出问题数据一到几千条页面直接卡死。其次是表单编辑的深拷贝问题。处理“编辑商铺”这种弹窗表单时页面初始化会把当前行数据直接绑定到form对象上。如果用户点开弹窗但改了一半不想保存直接关闭此时列表里的原始数据其实已经被Form表单修改了。解决方法是打开弹窗时对原始数据做一次深拷贝this.form JSON.parse(JSON.stringify(row))这样就算取消关闭列表里的数据也不会被污染。第三是图片上传回显。商铺logo、商品图片用el-upload上传上传成功后后端返回图片URL前端要手动把URL拼到fileList里并且在el-image中预览。这里要记得处理上传组件的on-success回调把响应中的URL取出来存到表单字段中然后:file-listfileList用于回显两个数据源不能混为一谈。5.4 按钮级权限控制菜单能通过路由守卫控制但按钮级权限比菜单更细。比如“删除订单”只能管理员操作“编辑商品”只能店长操作普通用户连看到这些按钮的资格都没有。这套系统里实现按钮权限的方式很轻量Vuex里存了一个当前用户权限码数组permissions页面中通过一个方法判断// 判断是否有权限 hasPermission(perm) { if (!this.permissions || this.permissions.length 0) return false return this.permissions.some((p) p perm) }模板中使用时用v-ifhasPermission(shop:delete)来控制按钮渲染。这样做的核心逻辑不是让用户“点了之后提示无权限”而是直接让没有权限的用户连操作的入口都看不到。前后端双重校验的意义就在这里前端管体验后端管安全——哪怕有人绕过前端直接对接口发起请求后端拦截器那关也会拒绝。6. 实际部署落地时的关键坑6.1 前后端分离项目的Nginx部署方式项目开发完总要打包上线。这套系统的部署方案非常直白后端用Maven打成Jar包前端npm run build生成静态文件Nginx托管前端文件并把/api路径反向代理到后端。Nginx配置核心片段如下server { listen 80; server_name your_domain.com; client_max_body_size 20M; location / { root /opt/shop-web; index index.html; try_files $uri $uri/ /index.html; # 解决history路由刷新404 } 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模式后刷新/shop/detail/1这样的页面如果Nginx找不到对应的物理文件会直接返回404而这一行配置会把所有匹配不到的文件路径回退到首页由前端路由接管并渲染对应组件。很多新人部署完发现“一刷新就404”多半就是没加这一行。proxy_pass后面没有/的细节也很重要。proxy_pass http://127.0.0.1:8080;表示保持原始URI转发前端请求/api/shop/list到后端就是/api/shop/list如果写成proxy_pass http://127.0.0.1:8080/;则会把/api前缀去掉后端接收到的变成/shop/list。两种方式取决于后端context-path配置但这套系统后端接口本来就以/api开头所以直接用不带斜杠的写法最省事。6.2 数据库初始化与版本变更规范拿到源码后第一步不是急着启动而是先把sql目录下的初始化脚本执行一遍。这套系统的SQL脚本包含建库、建表、初始数据管理员账号、基础菜单、测试商铺商品三个部分按顺序执行即可。我建议使用MySQL的source命令或Navicat运行整个SQL文件不要手动一条条复制避免遗漏。到了项目迭代阶段SQL变更要有纪律。尽量不要“直接改旧表字段”而是新增一个升级脚本比如v1.1_add_goods_stock_warning.sql记录本次变更的背景和新字段。这样做的好处是测试环境和生产环境执行同一条脚本即可保持一致将来回滚也有据可查。即使不用Flyway这类工具靠这个命名约定也能管理住小团队的数据库变更。提示上线前一定要做好数据库备份。常用做法是mysqldump -uusername -p database backup.sql导出全部数据。首次启动生产环境时建议先拿一个备份库做一遍启动测试确认连接串、字符集、时区都没问题再正式切流。6.3 日志与监控的基础配置生产环境不可能总盯着控制台日志就是你的眼睛。这套系统的日志配置基于Logback规则比较实用按天和时间大小双重滚动同时输出INFO级别的应用日志、ERROR级别的错误日志。这样排查问题时可以直接看错误日志文件error.log而不是在几十MB的主日志里大海捞针。appender nameERROR_FILE classch.qos.logback.core.rolling.RollingFileAppender filter classch.qos.logback.classic.filter.LevelFilter levelERROR/level onMatchACCEPT/onMatch onMismatchDENY/onMismatch /filter !-- 滚动策略省略 -- /appender监控层面MySQL开启慢查询日志执行时间超过3秒的SQL写入slow_log表定期检查并优化。后端接口响应时间通过AOP切面打印在INFO日志里若发现某个接口超过500毫秒就要及时查看是不是SQL没走索引或者循环调用数据库。这套源码虽然没有接入完整的监控平台但这些本地化的日志和慢查询工具已经能帮你在项目初期发现大量隐患。7. 常见问题与排查实录7.1 跨域问题的三种表现与解法前后端分离项目跨域是最常见的拦路虎。这套源码里因为同时考虑了开发环境代理和生产环境Nginx跨域问题很少但如果你自己做二次开发可能会遇到以下几种情况现象原因解决方式浏览器报“CORS policy”后端没有返回跨域允许头后端配置CorsFilter或者加入CrossOrigin开发环境请求能通生产报404生产环境没配Nginx代理Nginxlocation /api/写反向代理请求带着Content-Type被拦截预检请求未通过后端CORS配置要允许OPTIONS请求和自定义Header这里最容易出问题的是“开发环境代理和后端CORS同时开启”。Vue的proxy转发请求时浏览器看到的是同源请求不会触发CORS但如果后端又开启了全局CrossOrigin在部分浏览器的处理逻辑下会出现重复的跨域响应头反而导致请求失败。正确的做法是开发环境只开启Vue代理生产环境只靠Nginx后端CORS配置只在前后端完全分离、接口需要被第三方调用时才启用。7.2 列表数据“消失了”的隐藏原因很多人做二次开发时会遇到一种诡异情况明明数据库里有数据但列表查不出来。这套系统80%的情况是出在逻辑删除上。所有查询SQL都带了deleted 0条件如果你在数据库里直接插入了一条测试数据但忘了给deleted字段赋默认值0比如建表脚本里漏了DEFAULT 0那么新插入的数据deleted可能是NULLNULL在deleted 0条件下不成立数据自然就“消失”了。还有两个相似的原因第一MyBatis里如果开启了下划线转驼峰的映射配置数据库字段goods_name映射到实体goodsName表字段和实体字段命名不一致时就容易查出空对象第二PageHelper分页插件如果和一些自定义SQL的返回结构冲突会导致数据查得出来但分页总数为0。排查这类问题最好的手段就是把MyBatis的DEBUG日志打开看真实执行的SQL和返回参数比翻半天的代码猜测要快得多。7.3 并发请求导致库存超卖如果把系统的并发压测跑起来或者正好赶上商铺搞秒杀活动库存超卖问题就会浮出水面。原因在前面已经分析过先查库存再判断再更新代码写起来最直白但并发场景下必然出错。正确做法就是源码里用的条件更新UPDATE goods_info SET stock stock - #{count} WHERE id #{goodsId} AND stock #{count}执行后判断受影响行数等于1说明扣减成功等于0说明库存不足。如果你的业务对超发非常敏感再加上一层“扣减前预占库存”的设计但总体上单体系统用条件更新就足够了。另外一个相关坑是前端重复提交用户手抖连点两次“提交订单”后端就会创建两笔订单。解决方式可以在下单接口加一个简易的幂等处理前端提交时带上生成好的requestId后端在Redis里检查是否已处理。不过单体项目不引Redis的话用数据库唯一索引约束(user_id, goods_id, order_time)也能兜住大部分。7.4 Vue中数据改了页面不更新最后说一个前端调优的典型场景在列表页用this.list[index].stock newStock控制台打印数据已经变了但页面数字纹丝不动。原因是Vue 2的响应式系统是基于Object.defineProperty实现的对数组下标赋值和新增对象属性这类操作无法自动触发视图更新。解决方式是用Vue提供的this.$set(this.list, index, newStock)或者使用数组的splice方法替换元素、用Object.assign()重新生成对象。这套源码的前端虽然用了Vue 2但它们在修改表格数据时基本都是直接替换整个数组项或调用$set就是这个原因。如果你准备把它升级到Vue 3那么响应式系统已经换成Proxy这类问题会大幅减少但依然要注意复杂对象嵌套触发视图更新时的性能消耗。如果让我总结这套源码里最值钱的部分我会说不是某个炫技的算法而是它对常规业务系统的规范处理。从数据库的字段取舍、事务并发控制到后端的统一返回结构、JWT权限体系再到前端的Axios封装、按钮权限、Nginx部署兜底这些每一环单独看都不复杂但串起来就是一个“能真正上线运行”的企业级项目和课程设计里那些“演示完就丢”的半成品有着本质区别。实际操作时我建议你拿到源码之后先从数据库脚本开始梳理表关系再顺着一条订单主链路去看Controller、Service和Mapper的调用逻辑有了主线条之后再去改前端页面。如果你想在原有基础上加功能从“给商铺加一个公告栏”这类独立小模块入手最合适既能体会完整的开发流程又不会破坏原系统结构。最后再分享一个小技巧本地启动这套系统之前先把日志级别调到DEBUG运行起来后观察MyBatis在控制台打印的SQL语句你会惊讶地发现排查数据对不上问题的速度能快上好几倍。源码这东西光看不练等于零自己敲一遍、跑一遍、改一遍踩过的坑才算真正吃透。