每个做毕设或课设的同学,大概率都见过这类标题:“基于Spring Boot的XX商城系统”。我的待办事项列表里常年躺着类似的需求,其中“springboot167基于Java的水产品海鲜烧烤商城购物网站系统设计与实现”算是比较典型的电商类题目。你第一眼看到它,可能会觉得就是个普通商城,但仔细拆开看,水产品海鲜和烧烤食材这个垂直品类,其实给系统设计带来了一些很有趣的、和普通服装数码商城不太一样的挑战。这篇文章把我实际做这类系统时的完整思路、数据库设计、核心代码逻辑,还有那些文档里查不到的坑,一次性给你盘清楚。这篇内容适合谁看?如果你正在为毕业设计选这个题目,或者刚工作不久想拿一个完整的电商系统练手,又或者你想了解一个“带生鲜/食材属性”的电商系统和普通电商系统在设计上有啥区别,那这篇文章能给你省下不少琢磨的时间。我会尽量说人话,把关键模块的代码逻辑和设计理由讲透,你可以直接对照着去改写。1. 系统整体设计与思路拆解1.1 这个系统到底在解决什么问题很多人会把这类商城系统简单理解为“商品展示 + 购物车 + 订单”,做完发现功能是有了,但答辩时老师一问“为什么这么设计”就卡壳。如果只看表面,系统确实是在做买卖,但站在实际业务角度,海鲜烧烤商城比标品电商更麻烦的地方在于几个特殊属性。第一个是保鲜和时效性。买衣服的订单晚两天发货问题不大,但海鲜或者烧烤食材放不起,所以订单状态流转、配送时效提示、售后期这些逻辑需要专门考虑。第二个是商品形态复杂。海鲜类商品既有按份卖的(比如一份三文鱼刺身),也有按重量称重卖的(比如大闸蟹按只、冻虾按斤)。这种规格差异直接影响库存扣减方式和购物车的后端设计。第三个是冷链配送的边界约束。很多做普通商城的同学完全没考虑配送范围的问题,但在海鲜烧烤商城里,远距离地区能不能配送,直接关系到下单流程。如果系统里不做一个简单的配送区域判断,后面做订单履约成本会特别高。所以,在真正动手写代码之前,我建议你先画清楚这个系统的边界。我最终落地的方案是:前台用户端做到商品浏览、按分类筛选、搜索、购物车、下单、在线支付模拟、订单管理;后台管理端做到商品管理、分类管理、库存管理、订单处理(发货)、用户管理。营销活动、优惠券这类东西属于锦上添花,如果时间不够,砍掉了也不影响主线功能完整度。系统技术栈用的是Spring Boot作为后端框架,前端用Thymeleaf模板引擎加Bootstrap构成服务端渲染方案,数据存储用MySQL,ORM层使用MyBatis-Plus。1.2 技术选型的理由和取舍很多同学会纠结:为什么不用Vue做前后端分离?为什么不用Redis?为什么不用微服务?我的看法是,这些技术本身没有好坏,关键是适不适合你的场景和你要展示的能力。如果你的项目定位是课程设计或者毕业设计,用Spring Boot + Thymeleaf这种服务端渲染方案,好处极其明显:代码量少、调式简单、部署方便,一个jar包扔服务器上就能跑。你不需要维护两套工程,不需要处理跨域问题,不需要为了联调接口而头疼,这对于单人开发来说,省下的时间是非常客观的。但也要说清楚,如果选择了服务端渲染,你在简历上就不能硬吹“前后端分离”。我见过不少同学,代码明明是Thymeleaf写的,项目描述里非要写“基于Vue的SPA应用”,这是答辩翻车重灾区。你完全可以在设计说明里换一种说法:“本项目采用服务端渲染架构,使用Thymeleaf模板引擎实现动态页面生成,保证首屏加载速度,便于SEO优化”,这个说法真实、合理、禁得起追问。至于Redis和MQ,我的建议是:如果只是为了让项目看起来高级而引入,但你连Redis在项目里到底缓存了什么都说不清楚,那不如不引入。你完全可以把精力放在订单状态机的严谨性、库存扣减的并发控制这些真正能讲出东西的地方。1.3 目录结构与项目分层项目我采用经典的四层结构:src/main/java/com/example/seafoodmall/ ├── controller/ # 控制层:接收请求、返回视图或JSON ├── service/ # 业务层:核心业务逻辑、事务控制 ├── mapper/ # 持久层:MyBatis-Plus的Mapper接口 ├── entity/ # 实体类:与数据库表字段对应 ├── config/ # 配置类:拦截器、WebMvc配置等 ├── common/ # 公共类:统一返回结果、异常处理、常量定义 └── utils/ # 工具类这种分层结构很常规,但它是后面所有逻辑能够清晰展开的基础。每个层只负责自己的事情:Controller只处理参数接收和视图跳转,不写业务代码;Service扛起所有业务规则和事务边界;Mapper只负责数据库操作。这样做的好处是,一旦你后面要换成前后端分离模式,只需要把Controller返回值从ModelAndView改成JSON,业务逻辑完全不用动。2. 核心功能模块与数据库设计2.1 功能模块梳理我把整个系统拆成了七个核心模块,每个模块都有明确的职责边界:用户模块:注册、登录、个人信息维护、收货地址管理商品模块:商品列表展示、分类筛选、关键字搜索、商品详情购物车模块:加入购物车、修改数量、删除条目、批量结算订单模块:提交订单、订单状态流转、订单列表查询、订单详情支付模块:模拟支付流程、支付回调处理、订单支付状态更新库存模块:商品库存扣减、库存回滚、库存不足校验管理后台:商品管理、分类管理、订单管理、用户管理、数据统计这里面,商品模块和订单模块是核心,购物车和支付是辅助,但说到难点,支付回调的幂等处理、库存扣减的并发安全,是真正能区分“会写”和“懂设计”的地方。2.2 数据库表结构设计数据库设计是这类系统里最能看出水平的部分。我见过很多同学的库表设计,就是简单地把页面上的字段堆成一张表,商品、订单、用户全塞进去,结果后面写代码时懊恼不已。我给出一个相对标准的设计,共九张核心表:第一张是用户表user,字段包括id、username、password(加密存储)、nickname、phone、avatar、create_time。密码必须用加密方式存储,明文是绝对不可接受的,这个在答辩时也常被问到。第二张是商品分类表category,字段包括id、category_name、parent_id、sort_order。加一个parent_id是为了支持二级分类,比如“海鲜水产”下挂“虾类”“蟹类”“贝类”,如果不支持层级,后面数据一多你就知道有多痛苦了。第三张是商品表product,核心字段有id、category_id、product_name、description、main_image、price、stock、unit(比如“份”“500g”“只”)、sales、status(上架/下架)。这里unit字段很重要,它解决了我前面提到的“按份卖”和“按重量卖”的展示问题。第四张是购物车表cart_item,字段包括id、user_id、product_id、quantity、checked。checked用于记录用户在结算时选了哪几项,这个字段很多初学者会漏掉,导致只能整单结算。第五张是订单表orders,字段包括id、order_no(订单编号)、user_id、total_amount、status、receiver_name、receiver_phone、receiver_address、remark、create_time、pay_time、delivery_time、finish_time。订单状态我用了整数表示,0代表待付款,1代表待发货,2代表待收货,3代表已完成,4代表已取消。用整数比用字符串要省空间,也更方便状态机的逻辑判断。第六张是订单明细表order_item,字段包括id、order_id、product_id、product_name、product_image、price、quantity、subtotal。这里有个设计细节:商品名称和图片在下单那一刻就要快照到订单明细里,而不是通过外键去关联商品表。原因很简单——万一商家以后修改了商品名称或者删除了商品,你的历史订单不能跟着变,否则就乱套了。这个思路在很多面试题里也出现过。第七张是收货地址表address,字段包括id、user_id、receiver_name、receiver_phone、province、city、district、detail_address、is_default。第八张和第九张分别是管理员表和轮播图表,字段比较简单:管理员表有id、username、password,轮播图表有id、image_url、link_url、sort_order。2.3 表关系与核心查询逻辑表关系其实不复杂:用户对购物车是一对多,用户对订单是一对多,订单对订单明细是一对多,商品对订单明细是一对多(从商品视角看)。这些关系搞清楚之后,写查询就顺了。举个例子,你要展示订单列表,需要同时看到每个订单的多个商品信息,这就是典型的一对多查询。用MyBatis-Plus,我的实现方式是先查出订单主记录,再根据订单ID批量查询订单明细,最后在Service层组装成视图对象,而不是直接用传统的嵌套ResultMap去映射。这样代码看起来更直观,也方便处理分页。嵌套映射在简单场景没问题,但一旦订单状态或者条件查询变复杂,SQL会变得非常难维护。3. 关键功能实现与核心代码逻辑3.1 用户登录与拦截器设计登录这块用Session就够了,不要一上来就搞JWT。服务端渲染模式下,用Session天然合适,简单不折腾。我的实现逻辑是:用户提交用户名和密码,Service层校验通过后,把用户对象放进Session。然后注册一个拦截器,专门拦截需要登录才能访问的路径,比如购物车、订单结算、个人中心。拦截器配置是我要重点提醒的地方。很多同学写了拦截器,结果发现静态资源全被拦了,页面样式全丢了。这是因为路径拦截规则没写好。我实践下来的标准配置是:@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/cart/**", "/order/**", "/user/**", "/checkout/**") .excludePathPatterns("/css/**", "/js/**", "/images/**", "/fonts/**", "/login", "/register", "/product/**", "/category/**", "/"); }登录拦截器内部判断Session里有没有用户,没有就重定向到登录页。要注意,重定向时要保留用户原本想访问的地址,登录成功后可以再跳回去,这一步体验上的细节很加分。3.2 购物车的核心实现购物车放在Session里确实简单,但有个坑:用户换设备或者Session失效,购物车就没了。我在实际项目中把购物车状态放进了数据库,也就是前面设计的cart_item表。这么做的好处是,用户在手机上看好的东西,换电脑登录还能继续结算,体验是完整的。购物车的增删改查接口,需要关注两个细节。第一个是幂等性:同一个用户往购物车里加同一个商品,应该是数量加一,而不是插入两条新记录。所以代码逻辑要先查一下user_id + product_id是否存在,存在则update数量,不存在才insert。第二个是数量上限:库存只有10件,你购物车不能加100件,这个校验虽然数据库层也可以做,但展示层的友好提示更重要。3.3 订单提交与事务管理下单是整个系统里最核心、最容易出并发问题的地方。我用的方案是“一锁二判三更新”。下单逻辑的第一步是防止重复提交。前端按钮点击后置灰只是辅助手段,真正的防重要靠后端在生成订单号时保证唯一性。我的订单号生成规则是:时间戳 + 用户ID后四位 + 随机数。之所以不用数据库自增ID做订单号,是因为订单号要兼顾可读性、唯一性和一定程度的防猜测。第二步是校验库存。这里要注意,判断库存不能只靠查一次数据库就完事,因为两个用户同时下单时可能都查到库存有5件,然后都买入,最后超卖。解决超卖的办法是“乐观锁 + 条件更新”,SQL逻辑类似于:UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}这句SQL是关键。它把“库存是否足够”和“扣减库存”合并成了一个原子操作。如果影响行数为0,说明库存不足,整个下单流程就回滚。第三步是事务边界。下单涉及的操作包括:写订单主表、写订单明细表、扣库存、清购物车,这些必须在一个事务里,只要任何一个失败,所有操作都要回滚。在Spring Boot里就是给Service方法加@Transactional注解。但要注意,这个注解默认只在RuntimeException下触发回滚,如果你的代码里用try-catch把异常吃掉了,事务是不会回滚的。我看到过不少同学在这里踩坑,辛苦调了一晚上bug,最后发现是缓存异常导致库存没回滚。3.4 支付模块与回调处理实打实接入支付宝或者微信支付,需要商户号,个人开发一般不现实,而且答辩现场演示也容易出幺蛾子。我建议做成“模拟支付”,也就是用户点击支付按钮后,跳到支付确认页,显示订单号、应付金额和支付方式,再点确认支付,后端直接把这个订单的状态改为已支付。但是,支付状态更新这里要防一个很典型的“并发问题”——如果用户快速点了两次确认支付,会不会把订单状态从待支付改成已支付,然后再改成已支付,导致订单状态机错乱?我的方案是使用compareAndSet语义的更新语句:UPDATE orders SET status = 1, pay_time = NOW() WHERE id = #{orderId} AND status = 0也就是只把“待支付”状态的订单改为“已支付”,影响行数为0时说明订单已经被处理过了,避免重复支付。3.5 库存管理中的另一个细节商品上下架和库存预警,这些是后台管理的基础功能。上架下架就是个状态字段,很简单,但库存预警值得提一下。我建议在商品管理列表里加一个查询条件:库存低于阈值(比如10件)的商品高亮显示。这个功能说起来简单,但实现的时候其实就是一个stock <= threshold的条件筛选,不过它能体现你对业务的理解,在答辩时是很好的加分点。3.6 图片上传的处理后台商品管理需要上传图片。在Spring Boot里用MultipartFile接收文件,存到本机磁盘的某个目录,然后把文件路径存到数据库。这里有个特别容易出的问题:本地开发时你存的是D:\images\1.jpg,等部署到Linux服务器后,这个路径失效,图片全挂。我建议的做法是:在application.yml里配置一个上传路径的变量,同时在WebMvcConfig里配置资源映射。部署到新环境时,只需要改配置文件里的路径,不用动代码。这种做法虽然不是专业对象存储OSS方案,但它足够应对你的项目场景,迁移环境也不头疼。4. 管理后台模块的设计侧重后台管理系统一般用独立的前缀/admin,功能模块我拆成四个核心子模块:商品管理不只是简单的增删改查。你需要支持商品图上传、商品描述编辑(UEditor或类似富文本组件)、库存的即时调整、上下架切换。价格和库存字段尽量用decimal来存储,避免浮点精度问题。订单管理是后台操作最多的模块。订单列表要支持多条件筛选:订单号模糊搜索、用户手机号搜索、订单状态筛选、下单时间范围筛选。这些条件叠加起来,SQL使用MyBatis-Plus的LambdaQueryWrapper动态拼接就非常合适。订单处理的核心操作有“发货”和“完成”。发货需要填写物流单号(或者模拟),状态从“待发货”变为“待收货”。这个操作同样要做状态条件校验,只允许“待发货”状态下执行。用户管理比较简单,主要是查看用户列表、禁用/启用用户。要注意的是,禁用用户之后,该用户下次登录请求要能正常被拒。我的做法是在登录校验时多查一次用户状态,比单纯靠Session失效要可靠得多。数据统计模块,我认为是能拉开差距的地方。我不建议只做个SQL count就完事,至少要有:每日订单量、每日销售额统计,做成近七日的柱状图。Spring Boot后端提供统计数据接口,前端用ECharts画图。ECharts引入很简单,就是一个JavaScript组件库,重点是把后端返回的数据结构设计好,不要只返回一个数字列表,至少包含日期和数值两个数组。5. 系统测试与性能优化5.1 测试用例设计思路很多人对写测试文档特别敷衍,随便写几个“点击按钮,页面正常显示”应付了事。其实测试是展示你逻辑思维的好机会。我的建议是重点写两类测试用例。一类是正常业务流,也就是用户从注册登录到搜索商品、加入购物车、提交订单、支付、管理员发货、用户确认收货的完整流程,这个流程必须全通。另一类是异常场景测试,这部分才是亮点:库存不足时下单是否提示、未登录用户直接访问结算页是否被拦截、订单在待支付状态下超时未支付如何处理。这里提一个值得做的功能:订单超时取消。下单后超过30分钟未支付自动取消,同时把库存加回去。如果你不想用消息队列延迟消息,可以用定时任务扫描create_time + 30分钟 < NOW() AND status = 0的订单,执行取消操作。这个功能实现成本很低,但实用性非常高,也避免了用户故意锁库存不下单导致其他用户买不到货的情况。5.2 SQL优化与常见性能瓶颈如果商品列表页每次刷新都要全表查询,那随着数据量增长肯定会变慢。我建议做两件简单的事:第一,分页查询必须用上,MyBatis-Plus的分页插件配置很简单,关键是要记得加@Configuration类配置PaginationInterceptor;第二,商品列表的主查询加索引,category_id和status这两个字段是高频筛选条件,建立联合索引会让查询效率明显改善。另外,访问商品详情页时,一个页面上要展示商品基本信息、同类推荐。写代码时要注意别用循环查数据库的方式,每次循环都发一条SQL是最常见的N+1问题。正确做法是先把商品详情查出来,同类商品用一个批次查询拉回,再在Service层做聚合。5.3 部署方案与环境配置这是很多人忽略的部分。我强烈建议你至少学会用mvn clean package打成可执行jar包,然后部署到你自己的云服务器上。Spring Boot内置Tomcat,你不需要单独安装Tomcat,这个动作本身就是亮点:一条命令完成部署,明显比传统方式更简洁。配置方面,为了让项目可移植性强,我把数据库连接、上传路径这些环境相关配置全部放到application.yml里,分别维护本地开发和服务器生产两个配置文件。这样你换环境只需要切换profile,而不是在代码里写死。6. 常见问题与排查技巧实录6.1 跨域与会话问题服务端渲染一般没有跨域问题,但如果你调试的时候用了Chrome的某些插件,或者前端资源是从CDN加载的,可能会出现Session不一致的情况。排查思路是:用浏览器的隐身模式,或者在Network面板里看Cookie是否正常携带了JSESSIONID。如果发现Cookie没有设置,检查一下后端是否有跨域配置把allowCredentials设成了true,同时没配合指定允许的源,在浏览器看来这是一个不安全的跨域模式,Cookie可能会被拦。6.2 数据库连接配置的坑用MySQL 8及以上版本时,驱动类名是com.mysql.cj.jdbc.Driver,不是老版本的com.mysql.jdbc.Driver,URL里还要带上时区参数,比如serverTimezone=Asia/Shanghai。这个坑极其常见,报错信息通常长这样:The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,看到这个错别慌,就是没配时区。6.3 文件上传大小限制问题Spring Boot默认上传文件大小限制是1MB。商品图片稍微大一点就会报MaxUploadSizeExceededException。解决方法是,同时设置两个配置项:spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB注意max-file-size是单文件限制,max-request-size是总请求大小限制。如果你只设置了一个,另一个还是默认值,照样会报错。6.4 端口占用问题本地开发时Tomcat的8080端口经常被其他程序占用。解决方式很简单:改端口号或者用netstat -ano | findstr 8080(Windows)把占用进程找出来结束掉。Linux服务器上则用lsof -i:8080排查。为了保险起见,我在实际部署时会把端口配置改成8000,避开默认端口冲突的概率。6.5 页面显示问题有些同学会碰到Thymeleaf页面改了但刷新没反应的情况。这多半是浏览器缓存了静态资源。开发阶段在application.yml里把缓存关掉:spring: thymeleaf: cache: false在浏览器里按Ctrl+F5强制刷新,通常就能解决。如果还是不行,检查一下Controller里的方法是否返回了正确视图名,Thymeleaf的视图解析器是默认在templates/目录下找html文件的。7. 项目亮点整合与答辩要点做完功能之后,你要能把自己的项目“讲”出来。我建议你重点准备这几个方向的亮点话术,它们能让评委或者面试官对你的项目评价上一个台阶:第一个亮点是库存防超卖设计。我的方案是“乐观锁 + 条件更新”实现原子扣减,避免高并发下库存超卖。这个设计在电商场景中很常见,但它体现了你对并发和数据一致性有认知。具体的实现就是那句UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity},你需要在答辩时用大白话把思路讲清楚。第二个亮点是订单状态机设计。我用整型状态码和前置状态校验控制订单流转,保证状态合法迁移,避免重复发货、重复支付等脏操作。你可以现场打个比方——状态机就像一个关卡流程,必须拿到上一关的通关凭证才能进入下一关,不能跳关。第三个亮点是订单商品快照。因为我在order_item表快照了商品名称、价格和图片,所以历史订单不会受商品信息变更影响。这是一个很多人想不到,但实际商业系统中非常必要的细节。第四个亮点是数据统计可视化。管理后台集成 ECharts 展示七日销售趋势,可以辅助商家做运营决策。这个功能难度不大,但是项目完整度和展示效果一下就上去了。这四个亮点不是一个花架子,它们是真实业务系统里会遇到的核心问题。如果你能把它们顺溜地讲明白,无论答辩还是面试,这部分都是实打实的加分项。8. 开发过程中我的个人体会每次做这种系统,我都会不自觉地拿它和真实的电商系统做对比。真实系统永远有更多边界条件,但对一个毕设或者练手项目来说,能把订单、库存、支付、用户这几条主线的逻辑做到严谨、闭环,已经是很不错的完成度了。我特别想说的是,很多人在做项目时会陷入“功能越多越好”的误区,这是我不太认同的。与其做十个功能但每个都是半吊子,不如把五个核心功能做透做扎实。比如购物车的幂等性、订单状态的合法性校验、库存扣减的原子性、支付回调的防重处理,每一个都值得你反复推敲。你把这些想通了,再去看更高阶的技术方案时,会发现自己理解得快得多。另外,写代码时一定要强迫自己养成“日志先行”的习惯。在订单提交、支付回调、库存扣减这些关键节点加上日志输出,出了问题能顺着日志快速排查。搜索引擎里搜不到的报错信息,日志里往往都有答案。做这类系统还有个附加的价值:它会让你真切经历从需求分析、数据库设计、编码实现、测试调试到部署上线的完整流程。这个流程本身比任何一门单独的课程都珍贵。我之前带过的一个同学,做完这个系统后说最大的体会是,原来一个看起来平平淡淡的商城,背后藏着这么多设计决策——我觉得这份体会,就是做这个项目最大的收获。如果你也在动手写这个系统,不用着急,先把这篇里提到的核心链路捋清楚,再一步步落地代码。遇到卡壳的地方,欢迎随时来交流。
每个做毕设或课设的同学,大概率都见过这类标题:“基于Spring Boot的XX商城系统”。我的待办事项列表里常年躺着类似的需求,其中“springboot167基于Java的水产品海鲜烧烤商城购物网站系统设计与实现”算是比较典型的电商类题目。你第一眼看到它,可能会觉得就是个普通商城,但仔细拆开看,水产品海鲜和烧烤食材这个垂直品类,其实给系统设计带来了一些很有趣的、和普通服装数码商城不太一样的挑战。这篇文章把我实际做这类系统时的完整思路、数据库设计、核心代码逻辑,还有那些文档里查不到的坑,一次性给你盘清楚。这篇内容适合谁看?如果你正在为毕业设计选这个题目,或者刚工作不久想拿一个完整的电商系统练手,又或者你想了解一个“带生鲜/食材属性”的电商系统和普通电商系统在设计上有啥区别,那这篇文章能给你省下不少琢磨的时间。我会尽量说人话,把关键模块的代码逻辑和设计理由讲透,你可以直接对照着去改写。1. 系统整体设计与思路拆解1.1 这个系统到底在解决什么问题很多人会把这类商城系统简单理解为“商品展示 + 购物车 + 订单”,做完发现功能是有了,但答辩时老师一问“为什么这么设计”就卡壳。如果只看表面,系统确实是在做买卖,但站在实际业务角度,海鲜烧烤商城比标品电商更麻烦的地方在于几个特殊属性。第一个是保鲜和时效性。买衣服的订单晚两天发货问题不大,但海鲜或者烧烤食材放不起,所以订单状态流转、配送时效提示、售后期这些逻辑需要专门考虑。第二个是商品形态复杂。海鲜类商品既有按份卖的(比如一份三文鱼刺身),也有按重量称重卖的(比如大闸蟹按只、冻虾按斤)。这种规格差异直接影响库存扣减方式和购物车的后端设计。第三个是冷链配送的边界约束。很多做普通商城的同学完全没考虑配送范围的问题,但在海鲜烧烤商城里,远距离地区能不能配送,直接关系到下单流程。如果系统里不做一个简单的配送区域判断,后面做订单履约成本会特别高。所以,在真正动手写代码之前,我建议你先画清楚这个系统的边界。我最终落地的方案是:前台用户端做到商品浏览、按分类筛选、搜索、购物车、下单、在线支付模拟、订单管理;后台管理端做到商品管理、分类管理、库存管理、订单处理(发货)、用户管理。营销活动、优惠券这类东西属于锦上添花,如果时间不够,砍掉了也不影响主线功能完整度。系统技术栈用的是Spring Boot作为后端框架,前端用Thymeleaf模板引擎加Bootstrap构成服务端渲染方案,数据存储用MySQL,ORM层使用MyBatis-Plus。1.2 技术选型的理由和取舍很多同学会纠结:为什么不用Vue做前后端分离?为什么不用Redis?为什么不用微服务?我的看法是,这些技术本身没有好坏,关键是适不适合你的场景和你要展示的能力。如果你的项目定位是课程设计或者毕业设计,用Spring Boot + Thymeleaf这种服务端渲染方案,好处极其明显:代码量少、调式简单、部署方便,一个jar包扔服务器上就能跑。你不需要维护两套工程,不需要处理跨域问题,不需要为了联调接口而头疼,这对于单人开发来说,省下的时间是非常客观的。但也要说清楚,如果选择了服务端渲染,你在简历上就不能硬吹“前后端分离”。我见过不少同学,代码明明是Thymeleaf写的,项目描述里非要写“基于Vue的SPA应用”,这是答辩翻车重灾区。你完全可以在设计说明里换一种说法:“本项目采用服务端渲染架构,使用Thymeleaf模板引擎实现动态页面生成,保证首屏加载速度,便于SEO优化”,这个说法真实、合理、禁得起追问。至于Redis和MQ,我的建议是:如果只是为了让项目看起来高级而引入,但你连Redis在项目里到底缓存了什么都说不清楚,那不如不引入。你完全可以把精力放在订单状态机的严谨性、库存扣减的并发控制这些真正能讲出东西的地方。1.3 目录结构与项目分层项目我采用经典的四层结构:src/main/java/com/example/seafoodmall/ ├── controller/ # 控制层:接收请求、返回视图或JSON ├── service/ # 业务层:核心业务逻辑、事务控制 ├── mapper/ # 持久层:MyBatis-Plus的Mapper接口 ├── entity/ # 实体类:与数据库表字段对应 ├── config/ # 配置类:拦截器、WebMvc配置等 ├── common/ # 公共类:统一返回结果、异常处理、常量定义 └── utils/ # 工具类这种分层结构很常规,但它是后面所有逻辑能够清晰展开的基础。每个层只负责自己的事情:Controller只处理参数接收和视图跳转,不写业务代码;Service扛起所有业务规则和事务边界;Mapper只负责数据库操作。这样做的好处是,一旦你后面要换成前后端分离模式,只需要把Controller返回值从ModelAndView改成JSON,业务逻辑完全不用动。2. 核心功能模块与数据库设计2.1 功能模块梳理我把整个系统拆成了七个核心模块,每个模块都有明确的职责边界:用户模块:注册、登录、个人信息维护、收货地址管理商品模块:商品列表展示、分类筛选、关键字搜索、商品详情购物车模块:加入购物车、修改数量、删除条目、批量结算订单模块:提交订单、订单状态流转、订单列表查询、订单详情支付模块:模拟支付流程、支付回调处理、订单支付状态更新库存模块:商品库存扣减、库存回滚、库存不足校验管理后台:商品管理、分类管理、订单管理、用户管理、数据统计这里面,商品模块和订单模块是核心,购物车和支付是辅助,但说到难点,支付回调的幂等处理、库存扣减的并发安全,是真正能区分“会写”和“懂设计”的地方。2.2 数据库表结构设计数据库设计是这类系统里最能看出水平的部分。我见过很多同学的库表设计,就是简单地把页面上的字段堆成一张表,商品、订单、用户全塞进去,结果后面写代码时懊恼不已。我给出一个相对标准的设计,共九张核心表:第一张是用户表user,字段包括id、username、password(加密存储)、nickname、phone、avatar、create_time。密码必须用加密方式存储,明文是绝对不可接受的,这个在答辩时也常被问到。第二张是商品分类表category,字段包括id、category_name、parent_id、sort_order。加一个parent_id是为了支持二级分类,比如“海鲜水产”下挂“虾类”“蟹类”“贝类”,如果不支持层级,后面数据一多你就知道有多痛苦了。第三张是商品表product,核心字段有id、category_id、product_name、description、main_image、price、stock、unit(比如“份”“500g”“只”)、sales、status(上架/下架)。这里unit字段很重要,它解决了我前面提到的“按份卖”和“按重量卖”的展示问题。第四张是购物车表cart_item,字段包括id、user_id、product_id、quantity、checked。checked用于记录用户在结算时选了哪几项,这个字段很多初学者会漏掉,导致只能整单结算。第五张是订单表orders,字段包括id、order_no(订单编号)、user_id、total_amount、status、receiver_name、receiver_phone、receiver_address、remark、create_time、pay_time、delivery_time、finish_time。订单状态我用了整数表示,0代表待付款,1代表待发货,2代表待收货,3代表已完成,4代表已取消。用整数比用字符串要省空间,也更方便状态机的逻辑判断。第六张是订单明细表order_item,字段包括id、order_id、product_id、product_name、product_image、price、quantity、subtotal。这里有个设计细节:商品名称和图片在下单那一刻就要快照到订单明细里,而不是通过外键去关联商品表。原因很简单——万一商家以后修改了商品名称或者删除了商品,你的历史订单不能跟着变,否则就乱套了。这个思路在很多面试题里也出现过。第七张是收货地址表address,字段包括id、user_id、receiver_name、receiver_phone、province、city、district、detail_address、is_default。第八张和第九张分别是管理员表和轮播图表,字段比较简单:管理员表有id、username、password,轮播图表有id、image_url、link_url、sort_order。2.3 表关系与核心查询逻辑表关系其实不复杂:用户对购物车是一对多,用户对订单是一对多,订单对订单明细是一对多,商品对订单明细是一对多(从商品视角看)。这些关系搞清楚之后,写查询就顺了。举个例子,你要展示订单列表,需要同时看到每个订单的多个商品信息,这就是典型的一对多查询。用MyBatis-Plus,我的实现方式是先查出订单主记录,再根据订单ID批量查询订单明细,最后在Service层组装成视图对象,而不是直接用传统的嵌套ResultMap去映射。这样代码看起来更直观,也方便处理分页。嵌套映射在简单场景没问题,但一旦订单状态或者条件查询变复杂,SQL会变得非常难维护。3. 关键功能实现与核心代码逻辑3.1 用户登录与拦截器设计登录这块用Session就够了,不要一上来就搞JWT。服务端渲染模式下,用Session天然合适,简单不折腾。我的实现逻辑是:用户提交用户名和密码,Service层校验通过后,把用户对象放进Session。然后注册一个拦截器,专门拦截需要登录才能访问的路径,比如购物车、订单结算、个人中心。拦截器配置是我要重点提醒的地方。很多同学写了拦截器,结果发现静态资源全被拦了,页面样式全丢了。这是因为路径拦截规则没写好。我实践下来的标准配置是:@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/cart/**", "/order/**", "/user/**", "/checkout/**") .excludePathPatterns("/css/**", "/js/**", "/images/**", "/fonts/**", "/login", "/register", "/product/**", "/category/**", "/"); }登录拦截器内部判断Session里有没有用户,没有就重定向到登录页。要注意,重定向时要保留用户原本想访问的地址,登录成功后可以再跳回去,这一步体验上的细节很加分。3.2 购物车的核心实现购物车放在Session里确实简单,但有个坑:用户换设备或者Session失效,购物车就没了。我在实际项目中把购物车状态放进了数据库,也就是前面设计的cart_item表。这么做的好处是,用户在手机上看好的东西,换电脑登录还能继续结算,体验是完整的。购物车的增删改查接口,需要关注两个细节。第一个是幂等性:同一个用户往购物车里加同一个商品,应该是数量加一,而不是插入两条新记录。所以代码逻辑要先查一下user_id + product_id是否存在,存在则update数量,不存在才insert。第二个是数量上限:库存只有10件,你购物车不能加100件,这个校验虽然数据库层也可以做,但展示层的友好提示更重要。3.3 订单提交与事务管理下单是整个系统里最核心、最容易出并发问题的地方。我用的方案是“一锁二判三更新”。下单逻辑的第一步是防止重复提交。前端按钮点击后置灰只是辅助手段,真正的防重要靠后端在生成订单号时保证唯一性。我的订单号生成规则是:时间戳 + 用户ID后四位 + 随机数。之所以不用数据库自增ID做订单号,是因为订单号要兼顾可读性、唯一性和一定程度的防猜测。第二步是校验库存。这里要注意,判断库存不能只靠查一次数据库就完事,因为两个用户同时下单时可能都查到库存有5件,然后都买入,最后超卖。解决超卖的办法是“乐观锁 + 条件更新”,SQL逻辑类似于:UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}这句SQL是关键。它把“库存是否足够”和“扣减库存”合并成了一个原子操作。如果影响行数为0,说明库存不足,整个下单流程就回滚。第三步是事务边界。下单涉及的操作包括:写订单主表、写订单明细表、扣库存、清购物车,这些必须在一个事务里,只要任何一个失败,所有操作都要回滚。在Spring Boot里就是给Service方法加@Transactional注解。但要注意,这个注解默认只在RuntimeException下触发回滚,如果你的代码里用try-catch把异常吃掉了,事务是不会回滚的。我看到过不少同学在这里踩坑,辛苦调了一晚上bug,最后发现是缓存异常导致库存没回滚。3.4 支付模块与回调处理实打实接入支付宝或者微信支付,需要商户号,个人开发一般不现实,而且答辩现场演示也容易出幺蛾子。我建议做成“模拟支付”,也就是用户点击支付按钮后,跳到支付确认页,显示订单号、应付金额和支付方式,再点确认支付,后端直接把这个订单的状态改为已支付。但是,支付状态更新这里要防一个很典型的“并发问题”——如果用户快速点了两次确认支付,会不会把订单状态从待支付改成已支付,然后再改成已支付,导致订单状态机错乱?我的方案是使用compareAndSet语义的更新语句:UPDATE orders SET status = 1, pay_time = NOW() WHERE id = #{orderId} AND status = 0也就是只把“待支付”状态的订单改为“已支付”,影响行数为0时说明订单已经被处理过了,避免重复支付。3.5 库存管理中的另一个细节商品上下架和库存预警,这些是后台管理的基础功能。上架下架就是个状态字段,很简单,但库存预警值得提一下。我建议在商品管理列表里加一个查询条件:库存低于阈值(比如10件)的商品高亮显示。这个功能说起来简单,但实现的时候其实就是一个stock <= threshold的条件筛选,不过它能体现你对业务的理解,在答辩时是很好的加分点。3.6 图片上传的处理后台商品管理需要上传图片。在Spring Boot里用MultipartFile接收文件,存到本机磁盘的某个目录,然后把文件路径存到数据库。这里有个特别容易出的问题:本地开发时你存的是D:\images\1.jpg,等部署到Linux服务器后,这个路径失效,图片全挂。我建议的做法是:在application.yml里配置一个上传路径的变量,同时在WebMvcConfig里配置资源映射。部署到新环境时,只需要改配置文件里的路径,不用动代码。这种做法虽然不是专业对象存储OSS方案,但它足够应对你的项目场景,迁移环境也不头疼。4. 管理后台模块的设计侧重后台管理系统一般用独立的前缀/admin,功能模块我拆成四个核心子模块:商品管理不只是简单的增删改查。你需要支持商品图上传、商品描述编辑(UEditor或类似富文本组件)、库存的即时调整、上下架切换。价格和库存字段尽量用decimal来存储,避免浮点精度问题。订单管理是后台操作最多的模块。订单列表要支持多条件筛选:订单号模糊搜索、用户手机号搜索、订单状态筛选、下单时间范围筛选。这些条件叠加起来,SQL使用MyBatis-Plus的LambdaQueryWrapper动态拼接就非常合适。订单处理的核心操作有“发货”和“完成”。发货需要填写物流单号(或者模拟),状态从“待发货”变为“待收货”。这个操作同样要做状态条件校验,只允许“待发货”状态下执行。用户管理比较简单,主要是查看用户列表、禁用/启用用户。要注意的是,禁用用户之后,该用户下次登录请求要能正常被拒。我的做法是在登录校验时多查一次用户状态,比单纯靠Session失效要可靠得多。数据统计模块,我认为是能拉开差距的地方。我不建议只做个SQL count就完事,至少要有:每日订单量、每日销售额统计,做成近七日的柱状图。Spring Boot后端提供统计数据接口,前端用ECharts画图。ECharts引入很简单,就是一个JavaScript组件库,重点是把后端返回的数据结构设计好,不要只返回一个数字列表,至少包含日期和数值两个数组。5. 系统测试与性能优化5.1 测试用例设计思路很多人对写测试文档特别敷衍,随便写几个“点击按钮,页面正常显示”应付了事。其实测试是展示你逻辑思维的好机会。我的建议是重点写两类测试用例。一类是正常业务流,也就是用户从注册登录到搜索商品、加入购物车、提交订单、支付、管理员发货、用户确认收货的完整流程,这个流程必须全通。另一类是异常场景测试,这部分才是亮点:库存不足时下单是否提示、未登录用户直接访问结算页是否被拦截、订单在待支付状态下超时未支付如何处理。这里提一个值得做的功能:订单超时取消。下单后超过30分钟未支付自动取消,同时把库存加回去。如果你不想用消息队列延迟消息,可以用定时任务扫描create_time + 30分钟 < NOW() AND status = 0的订单,执行取消操作。这个功能实现成本很低,但实用性非常高,也避免了用户故意锁库存不下单导致其他用户买不到货的情况。5.2 SQL优化与常见性能瓶颈如果商品列表页每次刷新都要全表查询,那随着数据量增长肯定会变慢。我建议做两件简单的事:第一,分页查询必须用上,MyBatis-Plus的分页插件配置很简单,关键是要记得加@Configuration类配置PaginationInterceptor;第二,商品列表的主查询加索引,category_id和status这两个字段是高频筛选条件,建立联合索引会让查询效率明显改善。另外,访问商品详情页时,一个页面上要展示商品基本信息、同类推荐。写代码时要注意别用循环查数据库的方式,每次循环都发一条SQL是最常见的N+1问题。正确做法是先把商品详情查出来,同类商品用一个批次查询拉回,再在Service层做聚合。5.3 部署方案与环境配置这是很多人忽略的部分。我强烈建议你至少学会用mvn clean package打成可执行jar包,然后部署到你自己的云服务器上。Spring Boot内置Tomcat,你不需要单独安装Tomcat,这个动作本身就是亮点:一条命令完成部署,明显比传统方式更简洁。配置方面,为了让项目可移植性强,我把数据库连接、上传路径这些环境相关配置全部放到application.yml里,分别维护本地开发和服务器生产两个配置文件。这样你换环境只需要切换profile,而不是在代码里写死。6. 常见问题与排查技巧实录6.1 跨域与会话问题服务端渲染一般没有跨域问题,但如果你调试的时候用了Chrome的某些插件,或者前端资源是从CDN加载的,可能会出现Session不一致的情况。排查思路是:用浏览器的隐身模式,或者在Network面板里看Cookie是否正常携带了JSESSIONID。如果发现Cookie没有设置,检查一下后端是否有跨域配置把allowCredentials设成了true,同时没配合指定允许的源,在浏览器看来这是一个不安全的跨域模式,Cookie可能会被拦。6.2 数据库连接配置的坑用MySQL 8及以上版本时,驱动类名是com.mysql.cj.jdbc.Driver,不是老版本的com.mysql.jdbc.Driver,URL里还要带上时区参数,比如serverTimezone=Asia/Shanghai。这个坑极其常见,报错信息通常长这样:The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,看到这个错别慌,就是没配时区。6.3 文件上传大小限制问题Spring Boot默认上传文件大小限制是1MB。商品图片稍微大一点就会报MaxUploadSizeExceededException。解决方法是,同时设置两个配置项:spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB注意max-file-size是单文件限制,max-request-size是总请求大小限制。如果你只设置了一个,另一个还是默认值,照样会报错。6.4 端口占用问题本地开发时Tomcat的8080端口经常被其他程序占用。解决方式很简单:改端口号或者用netstat -ano | findstr 8080(Windows)把占用进程找出来结束掉。Linux服务器上则用lsof -i:8080排查。为了保险起见,我在实际部署时会把端口配置改成8000,避开默认端口冲突的概率。6.5 页面显示问题有些同学会碰到Thymeleaf页面改了但刷新没反应的情况。这多半是浏览器缓存了静态资源。开发阶段在application.yml里把缓存关掉:spring: thymeleaf: cache: false在浏览器里按Ctrl+F5强制刷新,通常就能解决。如果还是不行,检查一下Controller里的方法是否返回了正确视图名,Thymeleaf的视图解析器是默认在templates/目录下找html文件的。7. 项目亮点整合与答辩要点做完功能之后,你要能把自己的项目“讲”出来。我建议你重点准备这几个方向的亮点话术,它们能让评委或者面试官对你的项目评价上一个台阶:第一个亮点是库存防超卖设计。我的方案是“乐观锁 + 条件更新”实现原子扣减,避免高并发下库存超卖。这个设计在电商场景中很常见,但它体现了你对并发和数据一致性有认知。具体的实现就是那句UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity},你需要在答辩时用大白话把思路讲清楚。第二个亮点是订单状态机设计。我用整型状态码和前置状态校验控制订单流转,保证状态合法迁移,避免重复发货、重复支付等脏操作。你可以现场打个比方——状态机就像一个关卡流程,必须拿到上一关的通关凭证才能进入下一关,不能跳关。第三个亮点是订单商品快照。因为我在order_item表快照了商品名称、价格和图片,所以历史订单不会受商品信息变更影响。这是一个很多人想不到,但实际商业系统中非常必要的细节。第四个亮点是数据统计可视化。管理后台集成 ECharts 展示七日销售趋势,可以辅助商家做运营决策。这个功能难度不大,但是项目完整度和展示效果一下就上去了。这四个亮点不是一个花架子,它们是真实业务系统里会遇到的核心问题。如果你能把它们顺溜地讲明白,无论答辩还是面试,这部分都是实打实的加分项。8. 开发过程中我的个人体会每次做这种系统,我都会不自觉地拿它和真实的电商系统做对比。真实系统永远有更多边界条件,但对一个毕设或者练手项目来说,能把订单、库存、支付、用户这几条主线的逻辑做到严谨、闭环,已经是很不错的完成度了。我特别想说的是,很多人在做项目时会陷入“功能越多越好”的误区,这是我不太认同的。与其做十个功能但每个都是半吊子,不如把五个核心功能做透做扎实。比如购物车的幂等性、订单状态的合法性校验、库存扣减的原子性、支付回调的防重处理,每一个都值得你反复推敲。你把这些想通了,再去看更高阶的技术方案时,会发现自己理解得快得多。另外,写代码时一定要强迫自己养成“日志先行”的习惯。在订单提交、支付回调、库存扣减这些关键节点加上日志输出,出了问题能顺着日志快速排查。搜索引擎里搜不到的报错信息,日志里往往都有答案。做这类系统还有个附加的价值:它会让你真切经历从需求分析、数据库设计、编码实现、测试调试到部署上线的完整流程。这个流程本身比任何一门单独的课程都珍贵。我之前带过的一个同学,做完这个系统后说最大的体会是,原来一个看起来平平淡淡的商城,背后藏着这么多设计决策——我觉得这份体会,就是做这个项目最大的收获。如果你也在动手写这个系统,不用着急,先把这篇里提到的核心链路捋清楚,再一步步落地代码。遇到卡壳的地方,欢迎随时来交流。