谷粒商城Second阶段实战:商品分类树形结构与前后端联调避坑指南
我从基础篇First啃到Second的时候最大的感受是这阶段不再只是把Spring Boot、Spring Cloud这些框架跑通而是真正开始做“商城后台该有的业务功能”。如果你正在跟谷粒商城项目会发现Second阶段的信息量一下子变大了——前端要起后台管理界面后端要拆业务模块还要搞定文件上传、参数校验、跨域这些工程化问题。这篇文章我不打算复述视频里的操作步骤而是把我实际做这个阶段时踩过的坑、反复确认过的设计、以及后来复盘觉得应该早点知道的经验一次说清楚。1. 这个阶段到底在解决什么问题1.1 Second阶段在整个项目里的位置谷粒商城这套项目是一路从无到有搭起来的。基础篇First我理解是在解决“骨架”问题注册中心、配置中心、网关、公共模块这些东西都准备到位以后商城还是一个只有框架的空壳。到了基础篇Second重心转向了商品系统的基础数据维护比如商品分类、品牌、属性、规格参数这些后台管理功能。这一步的意义不只是“写几个CRUD接口”。商品分类是整个商城SPU、SKU体系的源头如果分类树没建好后面搞商品发布、检索、首页推荐都会很难受。所以Second阶段的本质是把后台管理端的地基立起来让后续所有业务模块都有可依附的数据结构。我做的时候给自己定的目标是三件事第一前后端能连起来跑通完整的增删改查第二商品分类能以树形结构展示和维护第三后端接口不再裸奔参数校验和异常处理要有个统一规范。有了这三个目标再看视频内容就不会觉得琐碎。1.2 Second阶段涉及的技术栈这阶段会出现一个明显的前后端分界。后端主力是Spring Boot 2.x配合MyBatis-Plus数据库还是MySQL注册和配置继续用Nacos。前端则从零初始化一个Vue后台工程通常搭配Element UI来做管理界面。如果你把整个项目拆开看Second阶段的后端部分没有特别高深的技术最难的反而是那些“不起眼”的工程细节跨域怎么配、长整型传到前端为什么精度会丢、代码生成出来的实体类为什么不能直接用。这些问题不解决功能逻辑再对联调的时候照样卡壳。前端这边我建议不要觉得“我不是搞前端的”就跳过。这阶段至少要把Vue Router的路由嵌套、axios拦截器、Element UI的表格和树组件用熟因为后面做商品上架、SKU管理时前端交互复杂度会比分类维护高得多。1.3 环境准备里最容易被卡住的点我记得第一次跑前端工程时Node版本不对导致依赖一直装不上。后来习惯是先固定一套稳定的版本组合Node 16左右、npm源切换成国内镜像、Vue CLI用4.x或5.x。别小看这些细节版本错一个一个下午就没了。数据库方面我会强烈建议先把项目提供的最新SQL脚本完整导入别自己手动删字段。商品分类、品牌、属性这些表之间有逻辑关联如果只导入部分表后面写SQL关联查询时会莫名其妙缺字段。另一个容易踩的是Maven的依赖冲突。Second阶段会引入MyBatis-Plus和Spring Cloud Alibaba相关依赖如果你本地Maven仓库里缓存过旧版本很容易出现ClassNotFound或者Bean创建失败。我在动手前会把整个仓库的gulimall相关依赖先clean一遍再重新构建公共模块能省掉很多排查时间。2. 前端工程化Vue脚手架和Element UI的整合细节2.1 初始化后台管理前端创建前端工程用的是Vue CLI这一步没什么花活。运行创建命令后选上Router和Vuex其他暂时不需要。装Element UI时要注意版本我用的Vue 2版本下装的是element-ui而不是element-plus两者API有些差异混用会直接白屏。初始化完成后第一件事不是写业务页面而是先把目录结构搭好views放页面组件router放路由配置api放接口请求文件utils放axios封装。这个习惯越早养成越好后面几十个业务模块涌进来时靠的就是这些约定。我见过不少人直接把所有页面堆在views下面刚开始很爽到Second阶段后半段就乱了。实际上商品分类、品牌、属性这些后台页面都有相似性建议按业务模块建子目录比如views/product/category.vue、views/product/brand.vue这样维护起来一目了然。2.2 路由和菜单布局Element UI的经典后台布局是左侧菜单、顶部栏、右侧内容区。对应的路由设计要做成嵌套路由——外层是一个Layout组件里面配各个业务页面。我在Second阶段最常犯的错是路由path写错层级导致页面在内容区显示不出来实际上只是父路由没有配子路由占位。如果项目里用到了动态菜单就要把后端返回的菜单字段和前端路由字段对应好。但这个阶段一般不会做得太重先把静态路由写清楚、嵌套层级理顺已经足够支撑商品维护页面的跳转。有一点值得注意分类页面里Element UI的el-tree组件和路由关系不大但它和级联选择器一样依赖树形数据格式。所以前端真正吃力的不是“页面跳转”而是“把后端给的数据变成组件需要的结构”。这个我会在后面分类树部分再展开。2.3 axios封装与统一错误处理axios封装是前端工程化里最值得做的一件事。我在utils里维护一个request实例统一拿到baseURL加上请求拦截器和响应拦截器。请求拦截器做的事情很简单把本地存的登录token加到header上。响应拦截器则统一处理返回码200以外的状态码直接弹错误提示不让每个页面重复写。实际联调中我发现响应拦截器里最容易被忽略的是“后端返回的是R对象但HTTP状态码永远是200”。也就是说你不能只判断HTTP状态还要判断响应体里的业务状态码。这个设计如果前后端没有对齐前端会拿不到真正的错误信息排查问题时全靠猜。还有一个小坑axios默认的application/json请求如果后端接口接收的是application/x-www-form-urlencoded会直接导致参数取不到。我在写登录接口时就遇到过一次后来统一了Content-Type的约定这种问题才算绝迹。3. 后端代码生成MyBatis-Plus Generator与分层规范3.1 为什么选择代码生成器Second阶段的业务表非常多动辄几十张手写实体类、Mapper接口和维护XML会让你在做核心业务前就耗尽精力。MyBatis-Plus Generator的价值就在于它能根据数据库表一次性生成实体、Mapper、Service和Controller至少省掉一半机械重复工作。但我要提醒一句生成器生成的代码不等于能直接用。它生成的Controller往往太朴素Service里的方法也基本都是单表操作真正的业务逻辑还是得自己写。把它当“脚手架”而不是“答案”心态上会舒服很多。我当时做的是先执行生成器跑完以后把生成的Controller删掉或重写因为生成器里的Controller直接暴露了Mapper的CRUD这在实际项目中既不安全也不合理。Service和Mapper保留再在上层重新设计接口。3.2 Generator的典型配置Generator的配置核心是数据库连接、包名、策略和模板。包名我会按项目结构设计比如com.example.mall.product下面再分entity、dao、service、controller。实体类生成策略里最关键的是把IdType设成ASSIGN_ID或AUTO避免主键生成策略和数据库不一致。另一个容易翻车的是字段命名映射。MyBatis-Plus默认会把数据库下划线字段转成驼峰属性这个看起来很省心但生成器在生成实体时如果配置不对会把parent_cid映射成parentCid字段名倒是没错可一旦你手写XML里面的resultMap就得特别小心。我之后的习惯是生成完实体先做三件手工修正。第一逻辑删除字段加TableLogic注解第二创建时间、更新时间加自动填充注解第三把不必要的TableField注解清理干净。这三点做到了后面写业务时才不会出现“更新完数据时间没变”“删除变假删”这种莫名问题。3.3 分层的职责边界Second阶段的代码结构看起来很简单但如果一开始职责划不清楚越往后越痛苦。我的经验是Controller只负责接收参数和返回结果Service里写真正的业务逻辑Mapper只做数据访问。至于DTO和VO在跨层传输数据的时候一定要分开别直接拿实体类往外抛。这样做的好处很直观商品分类的服务里要判断“父节点是否存在”“子节点能否删除”这些逻辑写在Controller里代码会迅速膨胀而且没法复用。放在Service里一个方法可以被多个Controller调用后续做API版本迭代也容易。我记得有一次重构分类删除逻辑因为在Service层统一封装了方法只改一处就完成了。如果是乱写的项目估计得同时改五六个接口。分层不是形式主义它是为了让你在加班的时候少掉头发。4. 商品分类维护树形结构从查询到级联4.1 商品分类表的结构设计商品分类表是Second阶段最有代表性的表。核心字段包括分类ID、分类名称、父分类ID、层级、排序和是否显示。这里最关键的就是parent_cid它把每条分类挂到另一个分类下面形成一棵树。实际设计里还会冗余一个cat_level字段比如一级分类level为1二级分类level为2。这个字段的代价是需要维护好处是查询这条分类时不用递归数层级。我第一次做的时候偷懒没设置level后面要做分类级联选择器时还得现算烦得很。另外一个容易忽略的是show_status。它不是简单的展示开关还会影响前台商品分类是否可被选中。所以在新增、修改分类时状态字段一定要跟着一起维护不能只改名称。4.2 树形查询的常见实现查询整棵分类树最直观的是递归查询先查一级分类再逐层查子分类。但这样会发出很多次SQL数据量大时压力不小。我用的方案是一次性查出全表然后在Java内存里组装树。组装树的核心思路是先把所有分类按parent_cid分组从根节点开始往下挂子节点。如果根节点不固定就把parent_cid 0的节点当成一级分类。代码不复杂但要注意避免用很深的递归否则容易栈溢出。前端拿到的树形数据通常是一个多层级JSON节点里包含children数组。这个结构和数据库里的扁平行记录完全不同所以后端要多做一次转换。我会把转换逻辑放在Service内部Controller返回的永远是前端需要的树形结构而不是裸表数据。4.3 新增、修改、删除的边界问题新增分类时要根据父分类的level计算出新分类的level同时把parent_cid设置对。这里最常见的错误是只设置了父ID忘记设置level导致前端级联选择器展示层级错乱。修改分类时最麻烦的是修改父分类。比如把一个二级分类改成一级分类它的子分类层级是否需要同步调整这Stage通常不会做得很重但你要想清楚如果允许修改父分类就必须连带着修改所有子孙分类的level。我做的时候选择了“允许修改父分类但同步更新子孙节点”这需要在事务里递归更新。删除分类更多是校验问题。如果一个分类下面还有子分类直接删会导致数据悬挂。所以删除之前必须先统计子分类数量有子节点就拒绝删除或者改成逻辑删除。我在实际项目中倾向逻辑删除因为物理删除接口一旦上线恢复数据的成本极高。树形结构还有一个隐藏坑循环依赖。如果数据库里出现了A的parent是B、B的parent是A组装树的时候就会死循环。稳妥的做法是在组装逻辑里加一个已访问集合遇到循环就中断。虽然这个场景概率低但一旦出现接口就是灾难。5. 跨域、文件上传和前后端联调5.1 跨域问题的两种解决方式本地开发时前端跑在8080后端跑在8080之外必然出现跨域。最常见的解法是前端配置devServer代理把/api开头的请求转发到后端地址。这样做的好处是请求看起来还是同源不需要后端额外处理CORS。另一种是后端配置CORS过滤器允许指定来源跨域访问。这种方式在生产环境用得更多。但要注意如果项目里已经挂了网关网关要负责跨域处理后端服务就不该再配一遍否则会出现重复响应头浏览器直接报错。我在Second阶段踩过的是前端代理配好了但请求路径里带了多余的/api前缀后端接口实际不带这个前缀结果404。后来约定好前端代理只做转发不重写路径后端接口路径以前端请求为准。两边各退一步问题就解决了。5.2 对象存储上传的实现方式后台管理系统里免不了要传图片。上传方案我建议用对象存储服务而不是把图片保存到应用服务器本地。本地存储有两个问题一是应用重启或扩容后图片丢失二是文件访问路径与API服务耦合。上传过程有两种做法。一种是把文件上传到后端后端再把文件流推到对象存储流程简单但多一次转发大文件效率低。另一种是后端生成一个临时上传凭证前端拿着凭证直接把文件传到对象存储效率更高也更常用。我在这阶段选了更稳的方案后端返回一个临时上传地址前端把文件PUT或POST过去。这样后端只负责鉴权和生成路径真正的文件流不经过业务服务器带宽压力小很多。要注意的是上传成功后要回调或返回文件路径前端再把路径提交给业务接口保存时序一定不能错。5.3 联调里最容易忽略的数据格式问题联调时最常见的问题不是404而是数据类型不匹配。比如数据库里分类ID是bigint后端实体用Long传到前端后JavaScript的Number类型会丢精度。服务端返回的分类ID比如123456789012345678前端拿到的可能已经变成123456789012345670。这个问题在商品分类这种ID很长的情况下极为致命。解决办法有两个要么把ID序列化成字符串要么在前端把ID当字符串处理。我倾向后端在返回JSON时对Long类型的ID做统一转字符串处理可以配置全局的Jackson序列化器一劳永逸。另一个坑是时间格式。后端返回的LocalDateTime默认序列化出来是一串数组而不是字符串前端根本没法直接用。需要在配置里统一指定时间格式用yyyy-MM-dd HH:mm:ss。这个问题不解决每次联调都要现改太影响心情。6. JSR303参数校验与全局异常处理6.1 参数校验到底值不值得做很多人在Second阶段会觉得反正前端都做了必填判断后端再校验一遍是重复劳动。但实际项目里前端可以做限制可接口本身是公开的别人绕过前端直接调接口你的程序扛不扛得住全靠后端校验。所以我的看法是后端参数校验不是“加分项”而是“安全底线”。用JSR303的Validated配合注解比如NotBlank、NotNull、Min直接在实体类属性上声明规则Controller参数前面加校验注解框架就会自动拦截非法请求。从这个阶段的代码量看加注解的成本很低但能省掉一大部分“手动if判断参数为空”的重复代码。我自己写的时候很不喜欢Controller里塞一长串if那种代码既难看又难测。用注解声明后Controller看起来干净很多。6.2 自定义校验注解与分组校验JSR303自带校验注解只能覆盖基础场景像“分类显示状态只能取值0或1”这种枚举校验就得自己写。思路是自定义一个注解再实现一个校验器在注解里指定校验器的class。另外还要理解分组校验。比如新增分类时catId可以为空修改分类时catId必须存在同一个实体就不能只有一套规则。解决办法是在注解上指定分组Controller方法上用Validated(UpdateGroup.class)来激活对应规则。这个在Second阶段的分类功能里非常实用。分组校验也是我后来才补上的。一开始所有字段共用一套校验规则结果新增时总提示“分类ID不能为空”明明新增分类时ID根本不需要传。发现了分组校验后这个问题才彻底解决。官方文档看着枯燥但真用起来才知道它是干什么用的。6.3 统一返回结构和全局异常处理约定一个统一的返回对象很重要。我用的是R对象包含code、msg、data这三个字段。业务成功时code为0失败时code为非0。前端响应拦截器根据code判断是否需要弹错误不必关心HTTP状态。全局异常处理则用RestControllerAdvice。里面定义一个捕获MethodArgumentNotValidException的异常处理方法把参数校验的错误信息统一提取出来包装成R对象返回。另一个是兜底异常处理捕获所有未被识别的异常避免系统返回一堆堆栈信息给前端。这里有个细节抛异常时异常信息不要直接透传给前端。返回给用户的提示语和日志里记录的详细信息应该分开。我在实现时把详细的堆栈打到了日志里给前端只返回“系统繁忙请稍后重试”既安全又不泄露内部信息。7. Second阶段复盘新人最容易踩的五个坑7.1 依赖版本必须整体对齐Spring Boot、Spring Cloud Alibaba、MyBatis-Plus这些版本是联动的不能随便升也不能随便降。我之前遇到过一个诡异问题RefreshScope失效查了半天最后发现是Spring Cloud版本和Boot版本不匹配。如果你用的是教学项目给的版本组合就老老实实照着用不要觉得“新版更好”。前端也一样Vue CLI、Element UI、axios版本之间的兼容性问题很常见。建议锁定一个能跑通的版本组合把package.json保存好后面换机器时直接按这个版本安装。7.2 数据库字段命名别太随缘数据库字段喜欢用下划线Java属性用驼峰这没问题。但千万别把字段名起成MySQL的关键字比如desc、rank、order。我刚开始建表时把分类排序字段叫sort一开始没事后面写SQL时被坑了一次加了反引号才解决。这种问题很无趣但确实会浪费一晚上。字段命名一定要统一风格。比如父分类ID就叫parent_cid不要一会儿parentId一会儿pid。代码生成器是根据字段名生成属性的命名不统一会导致实体属性名乱七八糟。后期维护的时候你会在不同表之间找同一个含义的字段找到怀疑人生。7.3 先跑通主链路再优化细节Second阶段的内容多很容易陷入“这个功能我要做得完美”的心态。我自己的经验是先把“商品分类查询一棵树”这条主链路完整跑通再做增删改再做校验和异常处理。每一步确保能看到效果再进下一个。如果一上来就纠结前端样式、后端分层、异常码设计很可能两三天都看不到一个完整的功能人会非常挫败。做项目不是写论文先有个能跑的东西再一点点变好这样效率高得多也更容易坚持下去。7.4 日志和断点要用好排查问题时日志比断点更可靠。断点只能查本地的一次调用而日志能看到线上环境、测试环境里真实用户触发的数据。我在分类删除接口里加了几行关键日志入参、查到的子节点数量、删除结果。后面自查时一看日志就知道是哪一步出了问题。日志不是写得越多越好而是要在关键的决策点打。比如新增分类时计算出新的level后打一条日志后端返回给前端的数据结构里到底有没有children也打一条。这些日志在联调阶段就是救命稻草能在前端报错时快速定位是后端数据问题还是前端渲染问题。7.5 前端别跳过后面联调会加倍还债这是我最想提醒的。很多人学后端出身一看Vue就头大遇到前端页面就跳过直接看视频里的后端代码。但Second阶段的核心挑战恰恰是前后端配合如果前端完全没接触后面做商品发布、购物车、订单时你会发现自己连数据都调不通。我并不是让你去精通前端而是至少要能看懂Vue单文件组件的基本结构、axios请求怎么发、Element UI的表格和树组件的属性怎么用。能做到这些你在学习项目时就不再是“半盲人”前后端联调的问题也能自己排查。这一点在我后来的项目实践中帮了大忙。老实说Second阶段是整个项目里最能锻炼“工程感”的环节。它没有特别炫酷的算法也不会遇到高并发难题但就是这些细碎的配置、校验、联调逼着你像一个真正的开发者那样去思考。如果你也在啃这个阶段别急着往后赶把每一条报错都当作一次学习机会把每一个组件都亲手点一遍。等你把分类管理这套功能完整跑通回头看一定比刚开始时明白很多。