好久没系统梳理一个全栈项目了,这次刚好把一个企业级网上超市管理系统从头到尾走了一遍,技术栈就是标题里那套:SpringBoot + Vue + MyBatis + MySQL。这套组合放到现在依然能打,尤其适合做毕业设计、课程设计,或者你想找一个能写进简历里的完整业务项目来练手。网上超市听起来简单,但真正做起来,从用户注册登录、商品浏览、加入购物车、下单扣库存,到后台的商品维护和订单处理,电商系统该有的核心链路基本都覆盖全了。这篇文章我把整个项目的设计思路、数据库建模、后端实现、前端交互、部署运行都拆开讲讲,还会把我实际做的时候踩过的坑一并说清楚,希望能给你省点时间。
1. 项目定位与技术选型思路
1.1 企业级网上超市的系统边界
很多人一听"网上超市"就觉得是仿淘宝、仿京东的小demo,其实不是。企业级这三个字,决定了系统的复杂度并不是单纯"能买能卖"那么简单。
真正的企业级网上超市,至少要覆盖三个端:用户端负责商品浏览、搜索、购物车、订单、支付模拟;管理端负责商品上架下架、分类管理、库存调整、订单处理、用户管理;再加上一套支撑这些业务的后端服务,处理鉴权、业务逻辑、数据持久化。这还只是业务功能层面,工程层面还要考虑统一的返回格式、异常处理、数据库事务、部署配置等。把这些串起来,就是一个可以面向真实场景演示和二次开发的项目。
我当时做这个系统的时候,第一步没有急着写代码,而是先把业务边界画清楚。用户角色分两种,普通用户和管理员。普通用户在前端商城页面操作,管理员进后台管理界面。两个端的数据是同一套,只是接口权限不同。这个角色划分决定了后面鉴权模块的设计方式,也直接影响数据库表里要不要单独建角色表。
1.2 技术选型:为什么是SpringBoot+Vue+MyBatis+MySQL
这套技术栈不是随便拼的,每一层都有它存在的理由。
先看后端。SpringBoot目前已经是Java后端开发的事实标准,它解决了传统SSM项目中大量繁琐的XML配置问题,内嵌Tomcat,打包就是一个可运行的Jar包,部署非常省事。尤其对于学生项目或者小团队项目,SpringBoot能让你把精力集中在业务代码上,而不是在配置环境里挣扎。
MyBatis和Spring Data JPA相比,更贴近SQL本身。电商这类业务SQL复杂多变,尤其涉及多表联查、动态条件查询、库存扣减这种需要精确控制SQL语义的操作,MyBatis的半自动映射反而更灵活。而且MyBatis的XML文件里写SQL,后期调优、修改查询逻辑非常直观。
前端选Vue则是因为它对中小型项目太友好了。Vue 2的Options API上手快,组件化思路清晰,配Element UI做后台管理界面,半天就能把骨架搭出来。商城用户端用Vue同样合适,数据绑定和路由切换都比传统的JSP加jQuery方案体验好太多。
数据库这块,MySQL是开源关系型数据库里最普及的选择,无论是学习成本、资料丰富度还是部署便利性都是最优解。企业级网上超市的数据量级以中小型规模为主,MySQL配合InnoDB引擎的事务支持和行级锁,完全够用。
顺手给一个对比表,方便你看明白选型逻辑:
| 对比维度 | 这套方案 | 替代方案 | 为什么这么选 |
|---|---|---|---|
| 后端框架 | SpringBoot | SSM手动配置 | 省去大量XML配置,内置容器,开发效率高 |
| 持久层 | MyBatis | JPA/Hibernate | 电商SQL复杂,MyBatis对SQL控制更直接 |
| 前端 | Vue | JSP+JQuery | 前后端分离,组件化开发,维护成本低 |
| 数据库 | MySQL | Oracle/SQLServer | 开源免费、资料多、部署简单,满足项目量级 |
1.3 功能模块与角色权限的总体划分
系统的功能模块大致可以分成前台和后台两块,再加上贯穿始终的用户体系。
前台商城端包含:用户注册与登录、首页商品展示、商品分类检索、关键词搜索、商品详情、购物车管理、确认订单、模拟支付成功后的订单状态流转。
后台管理端包含:商品分类管理、商品信息维护、库存管理、订单列表查看与发货操作、用户列表管理、首页轮播图配置。
这些模块落到代码上,对应到后端就是一个个Controller、Service、Mapper的层次关系。明白了功能全景,再看后面的数据库设计,就知道哪些表是必须的、字段为什么要这么设计。
2. 数据库设计与核心业务模型
数据库是电商系统的地基,表结构设计不好,后续写业务代码会非常痛苦。我一直认为看一个项目源码的功力,先看表结构设计——字段命名规不规范、主外键关系清不清楚、有没有冗余字段、金额字段用的什么类型,这些都直接反映项目质量。
2.1 核心表结构的设计思路
这个网上超市项目大概需要九张核心表,按业务域可以分成四类。
第一类是用户域:用户表(user)、角色表(role)、用户角色关联表(user_role)。有人觉得角色表多余,毕竟就两种角色,加个字段就行。但做成完整项目后你会发现,单独建角色表扩展性更好,以后要加一个"运营人员"或者"仓库管理员"角色,不用改表结构,只加数据就行。
第二类是商品域:商品分类表(category)、商品表(product)。商品表是核心中的核心。关键字段包括商品名称、主图、轮播图、价格、原价、库存量、销量、上下架状态、描述内容。商品和分类是多对一关系,外键指向分类表。这里有一点要注意,商品价格和原价我建议都用decimal(10,2),绝不用float或double,浮点类型在金额计算时会出现精度丢失,这在电商系统里是不可接受的。
第三类是交易域:购物车表(cart)、订单表(orders)、订单明细表(order_item)。购物车表记录用户加入购物车的商品和数量,下单成功后清空对应记录。订单表是交易域的主表,存订单号、用户id、订单总金额、订单状态、收货人信息、创建时间。订单明细表则把每个订单对应的商品快照存下来,包括商品名、单价、数量。为什么存快照而不是直接关联商品表?因为商品信息后续可能改动,但用户下单那一刻看到的价格和商品名称必须永久保留在订单里,这是电商设计的常识。
第四类是运营域:轮播图表(banner),用来配置商城首页的滚动图片。虽然不起眼,但把这类运营配置做成表,比硬编码在代码里优雅得多,管理员可以在后台可视化维护。
再多说一句用户表的设计。密码字段存的必须是加密后的密文,我用的BCrypt加密算法,后面详解。另外一定要加status字段做逻辑删除和禁用,别直接物理删除用户记录,订单、购物车都关联着用户的id,物理删除会破坏数据完整性。
2.2 容易栽跟头的字段与类型设计细节
这些细节很少有人专门讲,但恰恰决定了系统能不能跑到上线级别。
金额全部用decimal。这是我先强调的,订单总金额、商品单价、原价,都用decimal(10,2)或者decimal(12,2)。Java实体类里对应BigDecimal类型。MySQL中float和double存在精度丢失问题,尤其是在聚合计算和比较运算时会有莫名其妙的小数位,这是行业老前辈用事故换来的教训。
时间字段统一用datetime。创建时间、更新时间、支付时间,都用datetime类型,不要用timestamp。datetime的存储范围更大,没有时区转换的坑。实体类里用LocalDateTime接收,映射关系MyBatis处理得很干净。每个业务表建议都加create_time和update_time两个通用字段,where条件做数据筛选的时候非常方便。
逻辑删除与唯一约束。逻辑删除字段delete_flag(0存在,1已删除)基本是标配。但加了逻辑删除后要注意唯一约束的冲突问题。比如分类表里分类名称如果要加唯一索引,一条被逻辑删除的记录会占住这个名称,导致无法新增同名分类。解决方法是唯一索引带上delete_flag字段,或者在删除时把名称改成一个带时间戳的副本。我在做这个项目时就遇到了分类重名的问题,后面在代码里做了删除时自动追加后缀的处理。
订单号生成规则。订单号不能用数据库自增id直接暴露给用户,会泄露业务量,而且多表交互时需要字符串类型主键更灵活。我用的规则是:时间戳(yyyyMMddHHmmss)+ 用户id后四位 + 四位随机数,简单且够用。高并发场景可以引入雪花算法,但这套项目用时间戳加随机数的方案足以支撑演示和中小规模业务。
2.3 建表SQL脚本的执行要点
完整的项目源码里通常包含database.sql初始化脚本。拿到脚本后,我的建议是别急着直接执行,先看一遍表结构和初始化数据。这个项目初始化时会自动创建数据库(如果不存在),然后切到该库执行建表语句,并且插入默认管理员账号和测试商品数据。这样做对使用者非常友好,省去了手工建库的步骤。
执行的时候有一点要留意,MySQL 8.0以上版本对utf8mb4的支持已经很完善,建表时我统一把字符集设置成utf8mb4,排序规则用utf8mb4_general_ci。这样能完整支持emoji字符和大部分生僻字。在MySQL 5.7上,utf8mb4同样可用,只是需要注意索引字节数的限制,VARCHAR字段长度别太大,我在商品描述字段上用的text类型就没这个问题。
3. 后端SpringBoot+MyBatis的核心实现
后端是这套系统的控制中枢。SpringBoot负责提供REST接口,MyBatis负责跟MySQL打交道,业务逻辑全部封装在Service层。
3.1 分层工程结构与统一返回格式
拿到源码先看包名结构,这个项目的组织方式很标准:controller层接收前端请求、service层处理业务逻辑、mapper层做数据持久化、entity(或者model/pojo)承载实体类,此外还有config、common、utils等辅助包。
Controller层只做参数接收和结果返回,不写任何业务SQL。Service层是业务逻辑的集中地,事务注解加在Service层方法上。Mapper层只声明接口和SQL映射,MyBatis通过代理机制自动实现。这样的好处是职责单一,出了问题能快速定位。
前后端分离项目一个很重要的设计是统一返回格式。我定义了一个Result类,包含code(状态码)、msg(提示信息)、data(业务数据)三个字段。所有接口成功时返回code=200,业务异常时返回code=500或自定义错误码,前端axios拦截器统一处理这些返回。如果每个接口都返回裸数据,前端就要针对各种情况做判断,代码会非常散乱。
public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 200; result.msg = "操作成功"; result.data = data; return result; } public static <T> Result<T> error(String msg) { Result<T> result = new Result<>(); result.code = 500; result.msg = msg; return result; } }统一异常处理也不能少。我加了@RestControllerAdvice全局异常捕获,业务代码里抛出异常后,统一转换成Result.error返回给前端,前端弹出一个错误提示就完事,不会出现一大段Tomcat默认错误页面的情况。
3.2 登录鉴权:JWT与拦截器的配合
系统分普通用户和管理员两种角色,不可能让未登录的人乱调接口。我用的方案是JWT配合Spring拦截器,这也是目前企业级项目里广泛使用的做法。
登录流程是这样的:用户输入用户名和密码,后端根据用户名查出用户记录,用BCrypt验证密码是否匹配,匹配通过后生成一个JWT令牌返回给前端。JWT令牌里塞入了用户id、用户名、角色这些关键信息,并且设置了过期时间。前端拿到令牌后存在localStorage里,之后每次请求都在请求头里带上Authorization字段。
后端拦截器的逻辑是:放行登录、注册、首页商品查询等公开接口,其余接口全部校验令牌。校验逻辑写在HandlerInterceptor的preHandle方法里,先取出请求头里的token,解析失败或过期就直接返回401状态码。根据角色再做二次过滤,后台管理接口要求必须有管理员角色才能访问。
用JWT最大的好处是服务端无状态,不需要把会话存在MySQL或Redis里,系统水平扩展时不用考虑session同步的问题。有个坑值得说:JWT令牌如果泄露,在过期之前都能被使用。所以我把过期时间设置成2小时,并且在密码修改这个操作上做了让老令牌失效的处理,实现方式比较简单,就是给用户表加一个密码修改时间戳,JWT里也放这个字段,校验时对比一下。
3.3 下单事务与并发扣库存的实现
整个项目里技术含量最集中的就是下单接口。一个下单操作涵盖了:校验用户是否登录、校验商品是否存在且上架、校验库存是否充足、计算订单总金额、创建订单主记录、创建订单明细记录、扣减库存、清空购物车中对应商品。这么多步骤,任何一步失败都不能留下脏数据,所以必须用事务包裹。
@Transactional(rollbackFor = Exception.class) public OrderVO createOrder(OrderCreateDTO dto) { User user = userMapper.selectById(dto.getUserId()); if (user == null) { throw new BizException("用户不存在"); } List<CartItem> cartItems = cartMapper.selectCheckedItems(dto.getUserId()); if (cartItems.isEmpty()) { throw new BizException("购物车为空"); } BigDecimal totalAmount = BigDecimal.ZERO; String orderNo = generateOrderNo(user.getId()); Order order = new Order(); order.setOrderNo(orderNo); order.setUserId(user.getId()); order.setStatus(0); // 待支付 for (CartItem item : cartItems) { Product product = productMapper.selectById(item.getProductId()); if (product == null || product.getStatus() != 1) { throw new BizException("商品已下架或不存在"); } if (product.getStock() < item.getQuantity()) { throw new BizException("商品【" + product.getName() + "】库存不足"); } BigDecimal itemAmount = product.getPrice().multiply(new BigDecimal(item.getQuantity())); totalAmount = totalAmount.add(itemAmount); OrderItem orderItem = new OrderItem(); orderItem.setOrderNo(orderNo); orderItem.setProductId(product.getId()); orderItem.setProductName(product.getName()); orderItem.setProductPrice(product.getPrice()); orderItem.setQuantity(item.getQuantity()); orderMapper.insertOrderItem(orderItem); } order.setTotalAmount(totalAmount); orderMapper.insertOrder(order); cartMapper.deleteCheckedItems(user.getId()); return new OrderVO(order); }这里有个并发安全的关键点。多个用户同时购买同一件库存只剩一件的商品,如果不做任何控制,两个用户都能通过库存校验,最终导致超卖。解决办法是在扣减库存的SQL上做条件更新:
UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}这条SQL执行后,受影响行数为0就说明库存已经不足,业务上直接抛异常触发事务回滚。利用InnoDB的行锁确保同一时刻只有一个事务能成功扣减库存,这个方案比先查后改更安全,也是在实际项目中验证过的高效做法。
3.4 后台管理与文件上传的实现细节
后台管理端的核心是商品CRUD。新增或编辑商品时,涉及图片上传,我用的方案是本地存储加虚拟路径映射。前端用Element UI的Upload组件把图片传到后端的/upload接口,后端把文件保存到服务器的一个upload目录下,然后把可访问的图片URL返回给前端。为了让前端能访问到图片,需要配置一个静态资源映射:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + uploadPath + "/"); } }这个配置自己在本地写很容易忘,在开发时总是图片传上去了但页面显示不出来,其实就是静态资源路径没映射对。
商品列表查询用了MyBatis的分页插件PageHelper,传pageNum和pageSize就能拿到分页数据和总条数,前端表格组件做分页显示非常流畅。搜索功能支持按商品名称模糊查询、按分类筛选、按下架状态筛选,这些动态SQL都写在Mapper的XML文件里,用 标签处理条件拼接,避免空参数导致SQL语法错误。
4. 前端Vue实现的交互与联调
后端把接口提供好之后,前端的工作就是把这些接口转化成用户可操作的界面。这个项目的Vue部分同样分成用户端和管理端两大块。
4.1 前端目录结构与组件拆分
我习惯的Vue项目目录是这样组织的:
views目录放页面级组件,比如首页Home.vue、商品列表ProductList.vue、商品详情ProductDetail.vue、购物车Cart.vue、确认订单OrderConfirm.vue、个人中心UserCenter.vue,管理端的页面则放在views/admin下面。components目录放可复用的公共组件,比如轮播图组件、商品卡片组件、分页组件。router目录维护路由表,api目录统一存放接口调用代码,store目录是Vuex状态管理,主要存放用户登录信息和购物车数量这类全局状态。
用户端和管理端用路由模块分隔开,管理端的路由组件通过beforeEach守卫做管理员权限校验。这样设计的好处是:页面职责单一,组件复用率高,权限控制集中在一个地方。
有一点容易被忽视:每个页面的资产、接口、组件如果要全局注册,一定要在main.js里按需引入。Element UI虽然好用,但如果全部组件全局引入,打包体积会大很多。我实际做的时候用了按需引入的方式,配合babel-plugin-component,打包出来的体积小了不少。
4.2 商品搜索、购物车与订单确认的交互细节
商品搜索这块有个很容易被忽略的体验问题:用户在搜索框里连续打字,如果每敲一个字符就发一次请求,后端会很累。我在搜索框的监听器里做了防抖处理:用户停止输入300毫秒后才会发起接口请求,这样既保证了搜索的实时性,又不会有大量无效请求。
购物车交互要处理好三个操作:勾选商品、修改数量、删除商品。勾选状态对订单确认页有直接影响,我在购物车页面把勾选状态放在Vuex里统一管理,这样跳转到订单确认页时,可以直接拿到所有勾选的商品,不需要再用各种事件传递。修改数量时,前端做简单的数量范围校验,比如不能小于1,最大99,然后调用后端接口更新数量和价格小计。
订单确认页的重点是收货地址和金额核对。项目里做了简单的地址管理,用户在下单时填写收货人姓名、手机号、详细地址,确认订单后把这些信息随订单一起保存。金额部分由后端在创建订单时统一计算,前端展示的金额只做展示,以后端返回的接口数据为准。这一点要特别强调:前端传金额给后端是不可信的,价格应该全部以后端查到的商品价格为准来计算。
4.3 axios封装、路由守卫与开发环境代理
前端与后端的通信通过axios完成。我在项目里对axios做了二次封装:封装一个request.js文件,设置baseURL,配置请求拦截器把token加到请求头里,配置响应拦截器统一处理业务码。后端返回的code为401时,说明登录过期,前端自动清除本地用户信息和token,跳转回登录页。
开发环境跨域问题看似麻烦,实际只需要配置Vue CLI的proxy代理。我在vue.config.js中设置了devServer.proxy,把所有/api开头的请求代理到localhost:8080,后端Controller的RequestMapping统一加上/api前缀。这个方案的好处是前端代码里不需要写完整的后端地址,部署时可以灵活切换。
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }生产环境下,nginx反向代理同样把/api请求转发给后端服务,前端打包后的dist目录直接由nginx托管。整个联调流程非常顺畅,前后端各改各的,只要维护好接口文档,几乎不会出现互相阻塞的情况。
5. 部署运行与踩坑排查实录
这个项目拿到手之后,从本地跑起来到部署上线,大概分几步。每一步都有几个常见的坑,我把它们记在这里,你遇到问题的时候可以直接对照排查。
5.1 本地启动:环境准备与运行步骤
本地运行需要环境:JDK 1.8及以上、Maven 3.6、MySQL 5.7或8.x、Node.js 14及以上。
第一步是导入数据库。直接用数据库客户端工具执行项目里的sql脚本,脚本会自动建库建表并插入初始化数据。执行成功后检查一下user表里是否已经有了默认管理员账号。
第二步是改后端配置。打开application.yml,把数据源相关的url、username、password改成你的本地配置。配置里的serverTimezone要特别注意,用Asia/Shanghai避免中国时区的时间偏移8小时的问题。然后启动SpringBoot的启动类,看到控制台输出Tomcat started on port(s): 8080就说明后端起来了。
第三步是跑前端。进入vue目录执行npm install安装依赖,npm run serve启动开发服务器,默认端口一般是8081。浏览器访问localhost:8081,出现商城首页说明前后端已经打通。
注意:如果你本地的8080端口被占用,改后端端口时记得同时改前端vue.config.js里的代理目标端口,以及前端request.js的baseURL,三处保持一致才不会出现连不上的问题。
5.2 前后端部署:Jar包加Nginx的经典组合
部署阶段我选择的方式是后端打Jar包,前端打静态文件,用Nginx统一入口。这样做的好处是前后端可以独立部署、独立升级,Nginx配置也很简单。
后端在项目根目录执行mvn clean package -DskipTests,生成target目录下的jar包。生产服务器上执行nohup java -jar xxx.jar > logs/console.log 2>&1 &即可启动。
前端在vue目录执行npm run build,生成dist目录。把dist目录下的文件上传到服务器指定目录,然后在nginx配置里设置:
server { listen 80; server_name yourDomain; location / { root /opt/shop/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }注意try_files那行配置。Vue Router如果用的是history模式,刷新页面时如果nginx没配置try_files,就会出现404。很多人在部署Vue项目时遇到"刷新就404"的问题,罪魁祸首就在这里。如果不想处理这个问题,前端改用hash模式也能绕过去,但URL里会有个#号,不美观。
5.3 实际开发中踩过的坑与排查方法
这部分价值很高,我挨个记录一下踩过的真实问题。
第一个是MySQL驱动和时区问题。SpringBoot 2.x默认引入的是com.mysql.cj.jdbc.Driver,如果用低版本习惯了com.mysql.jdbc.Driver,启动时就会报ClassNotFound异常。解决方法是检查pom中的mysql-connector-java版本和驱动类名,我统一用的SpringBoot父依赖管理的版本。时区问题则表现为JVM时间与数据库存储时间相差8小时,原因就是连接URL没加serverTimezone=Asia/Shanghai。
第二个是MyBatis的Mapper扫描问题。SpringBoot启动类或配置类上必须加@MapperScan注解,或者在每个Mapper接口上加@Mapper注解,二选一。漏了的话启动时就会报mapper bean找不到。我习惯用@MapperScan("com.shop.mapper")把整个mapper包一次扫描,省事。
第三个是前端跨域问题。开发环境用proxy代理解决,但有时候改了代理配置不生效,需要重启npm run serve,Vue CLI对proxy配置的修改不会热更新。这个情况确实很容易困扰新手,改了配置没重启,来回折腾半天。
第四个是文件上传大小限制。默认情况下SpringBoot的Tomcat对上传文件有1MB的限制,商城商品图片动辄几百KB,多张图片可能就超了。在application.yml里设置:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB第五个是事务不生效的问题。我见过不少同学把@Transactional加在Controller层的方法上,结果是表面运行正常,但真正发生异常时数据没有回滚。原因是Spring的声明式事务是通过AOP代理实现的,只有通过代理对象调用Service层的public方法时事务才会生效。Controller调用Service属于外部调用,没问题;同一个Service内部方法之间互相调用,事务就不生效了。事务注解的正确位置是Service层的public方法上,并且要保证方法被外部调用。
还有一个MyBatis特有的坑:查询结果映射到实体类时,如果数据库字段是user_id这种下划线风格,实体类是userId的驼峰风格,需要在application.yml里开启下划线转驼峰:
mybatis: configuration: map-underscore-to-camel-case: true不开这个配置,查询出来的userId会一直为null,前端拿不到用户相关数据,排查起来还挺隐蔽的。
5.4 常见问题速查表
整理一个表格,把最典型的几个问题列出来,方便你遇到时直接定位。
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 后端启动报ClassNotFound: com.mysql.cj.jdbc.Driver | 数据库驱动版本过旧 | 用SpringBoot版本管理的mysql-connector-java,或显式升级驱动 |
| 前端请求接口报404 | 代理目标端口与后端不一致 | 检查vue.config.js中target和application.yml端口 |
| 刷新页面404 | Nginx未配置try_files | 增加try_files $uri $uri/ /index.html; |
| 图片上传后无法访问 | 静态资源映射未配置 | 添加ResourceHandler映射到upload目录 |
| 查询返回的用户ID为空 | 驼峰映射未开启 | 设置mybatis.configuration.map-underscore-to-camel-case为true |
| 下单后库存没有更新 | 事务未生效 | 确认@Transactional在Service层public方法上,且被外部调用 |
| 登录后一段时间自动失效 | token过期时间太短 | 调整JWT过期时间,但要注意安全性 |
| 中文乱码 | 字符集配置不一致 | 数据库连接URL加characterEncoding=utf8 |
写在最后的经验
这套系统前后端加起来大概二十多个接口、十多个页面,看起来功能不少,但真正考验人的地方全在细节里。我做完之后最大的体会是,一个项目不是把功能跑通就算完,事务控不控得住、库存扣得准不准、接口返回格式统不统一、部署配置规不规范,这些才是拉开差距的地方。如果你正在拿这套源码做课设或者面试作品,建议你从下单流程读起,把事务和库存扣减的SQL理解透,再去看前端购物车的状态管理。这两个地方读明白了,面试官问你项目亮点时你就有的说了。最后提醒一句,复制代码不是目的,把每行SQL、每个注解背后的为什么搞清楚,这个项目才算真正变成了你的东西。