简介:这是一份基于前后端分离架构的黑马商城小项目,适合正在学习Java Web开发的初学者,也适合需要完成课程设计或毕业设计的学生。项目完整实现了电商平台核心业务流程,包括商品列表展示、商品详情、购物车增删改、订单提交与支付对接。压缩包内共425个文件,大小44.22MB,以Java源码、Servlet编译类、JSP页面、HTML页面、JavaScript脚本、样式表以及大量商品图片为主,同时包含项目依赖的JAR包和数据库配置文件。通过阅读源码可以清晰看到浏览器通过异步请求对象向服务器发送请求,服务器端Servlet处理业务后返回轻量级的JSON数据,前端再动态更新页面的全过程。资源附带完整的工程结构、数据库脚本和部署说明,便于直接导入开发工具运行调试。已有2806人学习下载,适合对照代码逐模块理解电商常用功能的设计与实现。
1. 黑马商城是什么项目:一套能两周吃透的 JavaWeb 电商骨架
很多自学 Java 的人都有这个阶段:Servlet、JSP、Spring 都背得出来,合在一起就不知道代码该往哪儿放。黑马商城小项目解决的就是这个卡点。它是很多培训班和自学者电脑里都会躺着的教学型电商工程,解压后是一个完整的 SSM(Spring + SpringMVC + MyBatis)项目,带有商品列表、登录注册、购物车、下单、模拟支付这几条电商主线。它能验证你学过的所有 JavaWeb 组件能不能真正串起来,也能直接当课设或简历项目的底子。本文按最常见版本拆开讲:Maven 聚合、JSP 前端、MySQL 存储,并会说明 SpringBoot 版改起来差在哪。
2. 动手之前先做三件事:解压、环境核对、数据库脚本导入
2.1 解压后的目录怎么读:先看结构再写代码
拿到 zip 之后别急着双击 IDEA 打开,先在命令行里看一眼目录结构。常见做法是用tree命令,Windows 下可以用tree /F,macOS/Linux 用tree -L 3。这个压缩包解压后通常是标准的 Maven 工程,顶层目录大概长这样:
heima-shop/ ├── pom.xml # 父 POM,统一管理依赖版本和模块 ├── heima-shop-dao/ # MyBatis Mapper 与实体类 ├── heima-shop-service/ # 业务逻辑层 ├── heima-shop-web/ # SpringMVC Controller + JSP 页面 └── sql/ └── heima_shop.sql # 建库、建表、初始化数据脚本先看sql目录在不在,再看pom.xml里的<modules>。如果modules里列了三个子模块,说明这是多模块工程,导入时要把父 POM 作为 Maven 工程打开,而不是单独打开子模块。很多翻车案例都是第一次直接打开了 heima-shop-web,结果 Maven 找不到 dao 和 service 的依赖,整个项目标红。
我一般会顺手检查pom.xml里 Spring、MyBatis 的版本号。常见的组合是 Spring 5.x + MyBatis 3.5 + MyBatis-Spring 2.x,这个组合在 JDK 8 和 Tomcat 8.5 上兼容性最好。如果看到mybatis-spring版本是 1.x,就要注意和 Spring 5 存在包名冲突,启动时容易报NoSuchBeanDefinitionException。
2.2 环境版本照这个配:JDK、Maven、Tomcat、MySQL
版本不匹配是这个项目第一天的头号杀手,我踩过一次 JDK 8 编译的项目放到 JDK 11 上运行,反射相关的代码直接抛异常。建议按下面这套来,不折腾:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 项目编译目标基本都是 8,换更高版本要改 pom 编译插件 |
| Maven | 3.6.x | 3.8+ 对中央仓库 HTTPS 更严格,老项目的镜像配置可能失效 |
| Tomcat | 8.5.x | 支持 Servlet 3.1,和 Spring 5 匹配 |
| MySQL | 5.7 或 8.0 | 5.7 零配置最稳;8.0 需要处理驱动类名和时区问题 |
先说为什么固定 JDK 8。老教学项目的pom.xml里经常写着<source>1.8</source><target>1.8</target>,你本地装的是 JDK 17 也能编译,但运行期像 CGLIB 代理、JSP 编译这些环节在更高 JDK 上会出现兼容性告警,对新手来说排错成本很高。装双 JDK 是常见做法,IDEA 里每个工程单独指定 SDK 即可。
MySQL 是另一个变量。5.7 不用改任何参数就能连上;8.0 必须在 JDBC 连接串里带serverTimezone=Asia/Shanghai和allowPublicKeyRetrieval=true,否则启动就报时区错误或公钥检索错误。如果你和我一样主力 MySQL 是 8.0,建议建一个专用库而不是复用生产库。
2.3 导入数据库脚本:手工建库再导入最省事
把 SQL 脚本交给 Navicat 直接运行也没问题,但我不太建议这样做,因为脚本里如果没有CREATE DATABASE语句,你在某个默认库里建出一堆表,后面改连接串时容易对不上。手动建库再导入,每一步都看得见:
mysql -uroot -p > CREATE DATABASE heima_shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; > USE heima_shop; > SOURCE /your/path/to/heima-shop/sql/heima_shop.sql; > SHOW TABLES;这段命令的逻辑:先建库并指定字符集,utf8mb4是为了让商品名称、用户昵称里如果有表情符号也不会乱码;SOURCE是 MySQL 命令行执行 SQL 文件的指令,路径不要带中文,否则偶尔会碰到编码解析问题;SHOW TABLES用来确认表建出来了,正常应该能看到tb_user、tb_category、tb_product、tb_order、tb_order_item这五类核心表。
导入完成后,顺手查三张表的数据量,判断初始化数据是否完整:
SELECT COUNT(*) FROM tb_product; SELECT COUNT(*) FROM tb_user; SELECT COUNT(*) FROM tb_order;如果tb_product只有几条记录,说明脚本可能被改过或导入时中断了,后面首页商品展示会非常空,排查时容易误判是前端的问题。这一步相当于给项目做一次“体检”,确保起点数据是对的。
2.4 工程导入 IDEA:改对数据源连接串再启动
导入步骤不多,但每步都有坑。先在 IDEA 里File -> New -> Project from Existing Sources,选中解压目录里的pom.xml,Maven 会自动识别多模块结构。然后等依赖下载完,检查右侧 Maven 面板里是否出现heima-shop-dao、heima-shop-service、heima-shop-web三个模块。
启动前必须改的是数据库连接配置。老项目常见命名是jdbc.properties或db.properties,位置在heima-shop-web/src/main/resources/下:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/heima_shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456参数说明:driver在 MySQL 8.0 下要换成com.mysql.cj.jdbc.Driver,5.7 用com.mysql.jdbc.Driver就行;serverTimezone只针对 MySQL 8.0,不加的话连接层直接报The server time zone value错误;useSSL=false是必须的,因为教学项目的证书配置基本不存在,开着 TLS 握手反而失败;characterEncoding=utf8保证中文写入不乱码。
改完连接串,启动 Tomcat 前还要确认 IDEA 的 Deployment 里Application context是/而不是/heima-shop-web_war,否则浏览器里所有请求路径都要带项目名,页面里写死的/product/list这种绝对路径会全部 404。这一步是新手最容易忽略的:配置里改对了数据库,却在访问路径上卡半小时。
3. 拆解商城内部:从登录到下单的完整调用链路
3.1 分层架构怎么分工:Controller、Service、Mapper 各自管到哪一层
Spring 系的 Web 项目最核心的就是分层的边界感。黑马商城这样一个教学项目,它的包结构本身就是答案:controller只管收参数和返回视图,service管业务规则和事务,dao层只负责和数据库打交道。理解这个边界,后面改造任何功能都知道代码该写在哪。
| 层 | 包名示例 | 职责 | 反面案例 |
|---|---|---|---|
| Controller | com.heima.shop.controller | 参数校验、调用 Service、跳转页面 | 在 Controller 里写 SQL |
| Service | com.heima.shop.service | 业务编排、事务控制、库存判断 | 一个方法里写 200 行 SQL |
| Mapper | com.heima.shop.dao | 单表 CRUD、复杂查询 SQL | 把分页逻辑写在 Service 里 |
| Domain | com.heima.shop.pojo | 实体映射、VO 对象 | 实体类里写业务方法 |
这个项目的 Controller 返回值通常是String,配合 SpringMVC 的视图解析器跳到对应 JSP。如果你看代码时发现 Controller 里返回 JSON,那多半是项目里某个接口单独加了@ResponseBody或@RestController。同一套项目里混用两种返回方式很常见,别觉得奇怪,前端页面用视图跳转,异步接口用 JSON 返回。
我建议你从“商品列表”这个最简单的功能入手读代码,理由有三:一是链路短,从请求到 SQL 只有三个类;二是覆盖了参数绑定、Service 调用、MyBatis 查询、数据回显这四大基本功;三是它不涉及事务和状态流转,读起来没有认知负担。
3.2 商品列表的请求链路:一个 URL 从浏览器到 SQL 的完整过程
以/product/list?cid=2这个地址为例,一次请求经历了这些步骤:Tomcat 把请求交给 DispatcherServlet,它通过@RequestMapping匹配到ProductController的list方法;Controller 调用ProductService.listByCategory(cid);Service 调用ProductDao.selectByCategoryId(cid);Mapper XML 里执行真实 SQL 返回商品集合;Controller 把集合放进Model,视图解析器渲染product_list.jsp。下面两个关键代码片段是这套链路的前后两段:
@Controller @RequestMapping("/product") public class ProductController { @Autowired private ProductService productService; @RequestMapping("/list") public String list(Integer cid, Model model) { // 按分类查商品列表,cid 为空时默认查全部 List<Product> products = productService.listByCategory(cid); model.addAttribute("products", products); return "product_list"; } }代码逻辑:Integer cid是参数绑定,SpringMVC 自动把 URL 里的cid转成 Integer,传空时又能拿 null 做兜底;Model是 SpringMVC 的传参容器,页面里用${products}就能遍历输出。这个类是最标准的 SSM 写法,你看懂这一页,后面订单、用户模块的 Controller 都是同一个套路。
再往下看 Mapper 层,这个项目用的是 XML 映射文件,而不是注解 SQL。XML 的好处是 SQL 可以写复杂动态条件,不用在 Java 代码里拼字符串:
<select id="selectByCategoryId" parameterType="int" resultType="com.heima.shop.pojo.Product"> SELECT id, name, price, image, stock, cid FROM tb_product <where> <if test="cid != null"> AND cid = #{cid} </if> </where> ORDER BY id DESC </select><where>标签是 MyBatis 的动态 SQL,cid为 null 时自动去掉AND,避免 SQL 语法错误;#{cid}是预编译占位符,可以防 SQL 注入。Order By id DESC让新商品排在前面,教学项目没有做商品上架时间字段,用主键倒序是简化处理。你以后做真实项目,这里基本会改成create_time DESC。
3.3 登录状态和购物车数据放哪:Session 方案与它的替代品
这个项目的登录态用的是 Session,购物车也是 Session 存的。逻辑很直接:用户登录成功后session.setAttribute("user", user),每次请求从 Session 拿用户;购物车用一个Map<Integer, CartItem>装进 Session,加购就是往 Map 里 put。这是教学项目最常见的方案,简单、好理解、代码量少。
但它有两个天然的痛点:一是用户关掉浏览器再打开,购物车就没了——Session 的生命周期是会话级别的;二是集群部署下 Session 不共享,用户在服务器 A 登录,请求被分发到服务器 B 就变成未登录。解决思路不复杂:把购物车持久化到数据库表tb_cart,登录状态换成 Token 或 Redis 共享 Session。后续章节我会给到改造方向,这里先把它是什么看清楚。
购物车 Service 的典型逻辑是这样一段:
public void addToCart(HttpSession session, Integer productId, Integer num) { // 从 Session 拿购物车,没有就新建一个 Map<Integer, CartItem> cart = (Map<Integer, CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<>(); } CartItem item = cart.get(productId); if (item == null) { Item itemInfo = productService.findById(productId); cart.put(productId, new CartItem(itemInfo, num)); } else { // 已存在则数量累加,避免重复商品出现两行 item.setNum(item.getNum() + num); } session.setAttribute("cart", cart); }代码逻辑:外层用 Map 以商品 ID 为 key,天然去重;item == null判断该商品是否已在购物车,不存在就新建,存在就累计数量。这里有个隐藏问题:num没有做正整数校验,如果前端传入-1,购物车数量会变成负数,结算金额也可能为负——真实项目里入参校验不能只靠前端。你可以在 Controller 层加一层@Min(1)校验,或者参考新版里的assert判断。
3.4 下单事务:为什么超卖问题会出现在这个教学项目里
下单是商城项目里业务最重的一个方法,它的完整事务链是:查商品 → 扣库存 → 生成订单 → 生成订单明细 → 清空购物车。大多数教学项目把这五步放在同一个 Service 方法里,方法上标注@Transactional,这个注解可以保证任何一步抛异常,前面已经执行的 SQL 全部回滚。
下面是这个项目里常见的下单核心代码骨架:
@Transactional(rollbackFor = Exception.class) public Order createOrder(Integer userId, Integer[] productIds, Integer[] nums) { // 1. 生成订单主表记录 Order order = new Order(userId, totalPrice, OrderState.UNPAID); orderDao.insert(order); // 2. 循环扣减库存,同时生成明细 for (int i = 0; i < productIds.length; i++) { Product product = productDao.selectByIdForUpdate(productIds[i]); if (product.getStock() < nums[i]) { throw new BusinessException("库存不足"); } productDao.decreaseStock(productIds[i], nums[i]); orderItemDao.insert(new OrderItem(order.getId(), productIds[i], nums[i])); } // 3. 清空购物车 cartDao.deleteByUserId(userId); return order; }代码逻辑:selectByIdForUpdate里的FOR UPDATE是行级锁,目的是防止两个用户同时下单时把库存扣成负数——这是教学项目里处理超卖最关键的一行SQL;rollbackFor = Exception.class表示捕获所有异常都回滚,如果只写@Transactional其实默认只回滚 RuntimeException,业务异常BusinessException不会触发回滚,这是个很阴间的细节。
但这套写法在并发下并不是绝对安全的,原因在于FOR UPDATE锁是数据库层的,如果项目换了数据库或者锁被共享读覆盖,超卖还是会发生。教学项目通常不会去压测并发,但你在简历上写“解决超卖问题”时,至少要知道两个进阶方案:一个是用乐观锁版本号,在UPDATE语句里加WHERE stock >= num条件;另一个是预减库存,下单先扣 Redis 里的库存,支付成功后再真正落库。前者更简单,后者更接近大型系统的做法。
4. 让商城真正能用的关键配置:连接池、静态资源、分页和订单状态
4.1 数据库连接池的 4 个必调参数
项目里数据源一般用 DBCP 或 C3P0,教学项目里 DBCP 见到最多。配置都在jdbc.properties里,你改数据库账号密码的同时,建议顺手把连接池参数也改到位。默认配置经常是没写这些参数的,跑通没问题,接口一多就频繁卡顿。
| 参数 | 建议值 | 作用 |
|---|---|---|
| initialSize | 5 | 启动时预创建连接,避免第一个请求现连数据库 |
| maxTotal | 20 | 最大连接数,超出后请求排队等待 |
| maxIdle | 10 | 空闲连接上限,超过会被回收 |
| testWhileIdle | true | 空闲时检测连接有效性,防止拿到失效连接报 Connection closed |
DBCP 的配置写在applicationContext.xml的数据源 bean 里,参数会通过${jdbc.initialSize}这类占位符引用 properties 文件。如果你发现启动时控制台打出一大段告警,说连接池初始化失败,多数是 maxTotal 与 MySQL 自带的 max_connections 冲突,或者在连接串里没带useSSL=false。
4.2 静态资源放行与 JSP 路径解析:页面 404 的一半原因在这
SpringMVC 默认拦截所有请求,包括.js、.css、.jpg。如果springmvc.xml里没有配置静态资源放行,浏览器请求 CSS 会返回 404,页面看起来就是裸的 HTML。很多新手以为是前端代码的问题,其实后端根本没把资源放出去。配置是这样写的:
<mvc:resources location="/static/" mapping="/static/**" /> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean>第一行把/static/**映射到/static/目录,这样浏览器访问/static/css/style.css就能拿到真实文件;第二行是视图解析器的标准配置,Controller 里返回product_list时,它会拼接成/WEB-INF/jsp/product_list.jsp。注意一个细节:JSP 放在WEB-INF下面有个好处,用户不能通过地址栏直接访问 JSP 源码,必须经过 Controller,这是项目里页面跳转都走后端的原因。
很多人的 404 是这么来的:Controller 返回值写错了大小写,比如返回Product_List,而文件名是product_list.jsp;或者前缀配到了/WEB-INF/views/但 JSP 实际放在/WEB-INF/jsp/。排查时直接在浏览器访问转出的完整路径,看 Tomcat 的日志里有没有Resource not found字样。
4.3 分页查询:PageHelper 的两个必填参数
商品列表页一般不会把所有商品一次查出来,教学项目里最常见的是 PageHelper 插件,几行代码就能把分页逻辑接好。它是在 MyBatis 执行层通过拦截器改写的 SQL,自动在尾部拼接LIMIT。配置如下:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="plugins"> <array> <bean class="com.github.pagehelper.PageInterceptor"> <property name="properties"> <props> <prop key="helperDialect">mysql</prop> <prop key="reasonable">true</prop> </props> </property> </bean> </array> </property> </bean>helperDialect告诉插件数据库方言,mysql 会生成LIMIT ?语句;reasonable的作用是防止越界请求,当pageNum小于 1 时自动置为 1,大于最大页数时自动置为最后一页。这个参数在你做毕设演示时特别有用,别人故意输入?pageNum=999页面不会白屏。
代码层面,Service 里只在查询前调用一次PageHelper.startPage(pageNum, pageSize),后面紧跟的第一次查询就会被分页拦截:
public PageInfo<Product> listByPage(Integer cid, Integer pageNum, Integer pageSize) { PageHelper.startPage(pageNum, pageSize); List<Product> list = productDao.selectByCategoryId(cid); return new PageInfo<>(list); }一定要记住startPage只对紧接着执行的第一条查询生效。如果你在它和查询之间插入了别的数据库操作,分页会被那个查询抢走,结果商品列表永远查不到第二页数据。这也是这个插件最常见的误用姿势。
4.4 订单状态怎么设计:从待支付到已完成的状态机
这个项目的订单表核心字段是status,状态值一般是数字,映射关系大概是:0 待支付、1 已支付待发货、2 已发货、3 已完成、4 已取消。整个状态流转是单向的,不允许从已支付跳回待支付。
| status | 含义 | 触发方式 |
|---|---|---|
| 0 | 待支付 | 下单成功创建订单时写入 |
| 1 | 已支付 | 模拟支付接口回调后更新 |
| 3 | 已完成 | 用户点击确认收货 |
| 4 | 已取消 | 待支付状态下用户取消 |
教学项目里的支付一般是模拟实现,常见做法是点击“去支付”直接跳到一个mockPay接口,接口内把订单状态从 0 改成 1,不做真实资金流转。你在改造成简历项目时可以保留这个模拟接口,并补一个说明文档,写清楚真实支付替换方案是接第三方沙箱支付或本地支付网关。
订单超时取消通常不在教学项目里实现,但这是一个很好的加分功能。思路是下单时记录create_time,用户查询订单时如果状态为 0 且创建时间超过 30 分钟,自动置为 4 并释放库存。这个逻辑可以用 Spring 定时任务实现,也可以只在查询时按条件更新,后者对新手更友好,不需要引入消息队列。
5. 黑马商城跑不起来?这 5 个坑我踩了不止一次
5.1 数据库驱动报错:Public Key Retrieval is not allowed 与时区异常
现象:项目启动或第一次查商品列表时,控制台报Public Key Retrieval is not allowed或The server time zone value '�й���ʱ��' is unrecognized,连接串在正常数据库客户端里又能连上。原因:这两条都是 MySQL 8.0 的连接层校验问题,前者是因为 8.0 默认用caching_sha2_password认证插件,安全传输公钥需要客户端显式允许;后者是驱动读到了 MySQL 服务器的系统时区,但本地没有做映射。解决:在jdbc.url后面加上allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai,并把驱动改成com.mysql.cj.jdbc.Driver。如果仍然报错,检查你本地装的是不是 mysql-connector 5.1.x,换成 8.0.x 再试。
5.2 Tomcat 端口被占用,启动后页面自动跳回 IDEA 首页
现象:启动 Tomcat 时日志显示Port 8080 was already in use,或者浏览器访问 localhost:8080 打开的是 IDEA 的欢迎页。原因:8080 被别的进程占了,最常见的是之前残留的 Tomcat 实例或者另一个开发工具使用了默认端口。解决:两个选择——关掉占用进程,或者在 IDEA 的Edit Configuration里把 Tomcat 的 HTTP port 改成 8081,并同步检查Application context是否还是/。改端口后如果页面样式全丢了,十有八九是页面里的静态资源路径写死了 8080,检查前端模板或全局配置里的端口相关常量。
5.3 Maven 依赖全是红、下载慢、找不到本地仓库
现象:导入工程后 IDEA 里spring-webmvc、mybatis等坐标全部报红,Maven 面板刷新后卡在下载不结束。原因:国内直连中央仓库速度不稳定,或者老项目的仓库地址是 http 协议,新版 Maven 默认禁止非 HTTPS 仓库。解决:在settings.xml的<mirrors>节点配置一个可用的镜像源,并确认仓库地址是 https。改完镜像后记得在 IDEA 的 Maven 配置项里指定本地仓库地址和settings.xml路径,然后执行一次clean install把依赖真正拉下来。这一步不需要改项目代码,但没做对的话后面所有操作都动不了。
5.4 页面能打开但图片全裂、CSS 完全裸奔
现象:商品列表页面能显示文字,但所有图片都是裂图,页面没有任何样式,控制台一堆 404。原因:多半是springmvc.xml里缺静态资源映射,或者项目实际目录结构里静态文件根本不在/static/下。教学项目的 JSP 里常常用${pageContext.request.contextPath}/static/...拼路径,如果项目部署名不是/,这个路径会带上应用名,后端放行配置又只匹配了/static/**的绝对路径,就会匹配失败。解决:先打开浏览器控制台看具体 404 的 URL,再对照springmvc.xml的<mvc:resources>看是否匹配。还有一种隐蔽情况:图片文件在WEB-INF下,这属于安全目录,容器不会对外暴露,必须把图片挪到webapp/static或webapp/upload下。
5.5 下单后库存没扣,或扣了库存订单没生成
现象:提交订单提示成功,但商品详情页库存数量没变;或者库存变了,订单列表里查不到记录。原因:这两种情况本质是事务没有生效,最常见是@Transactional写在了私有方法上,或是在同一个类里调用自己的方法,导致事务代理没有介入。另一个常见原因是用try-catch包住了异常,事务捕获不到抛出点,默认就不会回滚。解决:把事务注解放到 public 方法上,并且通过注入的 Service 对象跨类调用;不要在事务方法内部把异常吞掉,要么向上抛出,要么在 catch 后手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。判断事务是否生效,可以在运行时看日志里有没有Creating new transaction字样。
6. 从跑通到变成你自己的项目:三个改造方向与一个调试习惯
如果你已经能正常跑起来并在浏览器完成一次下单,接下来这几步值得做,它们能让这个教学项目在简历上变得有辨识度。
方向一是把 Session 购物车改成 Redis 存储。核心是把购物车的存取从 HttpSession 换成 Redis 的 Hash 结构,用 userId 作为 key、商品 id 作为 field。改动点集中在购物车的 Service 和拦截器,前端代码不用动。这一步要搞懂 Redis 的序列化和过期策略,可以在项目里加一套 RedisConfig 来指定 key 的过期时间。
方向二是把页面从 JSP 迁到前后端分离。这个工作量稍大,但价值也最大。先把 Controller 返回值改成@RestController返回 JSON,再用 Vue 或原生 JS 重写页面。迁移过程中会踩到跨域问题,可以直接在 SpringMVC 里配置 CORS 过滤器解决。这个改造做完,项目就从“课设”变成了“接口项目”,面试时聊起来也更贴近团队实际协作方式。
方向三是在模拟支付接口里增加一个简单的回调校验。真实支付最重要的不是页面跳转,而是后端回调时验签并更新订单状态。教学项目没有这一环,你可以模拟一个回调地址,在控制器里实现“校验参数 → 改变订单状态 → 返回 success”的完整闭环,顺便把订单状态机的代码捋一遍。
养成一个验证习惯会省掉后面很多调试时间:每次改完订单相关逻辑,跑一遍“加购→下单→支付→查库存”的闭环,并在每个关键方法里加一条日志。我见过太多人以为页面跳转到了支付页就代表下单成功,结果库存和订单对不上。给下单方法加一个 AOP 日志切面,打印耗时和订单号,能快速定位是哪个环节慢。这个习惯来自我自己的一次教训,当时调了半天“订单丢失”,最后发现是事务方法内部调用了自己,代理没生效。如果你也遇到类似问题,先别怀疑业务代码,查声明式事务的边界。
先跑通,再拆开,最后改成自己的样子,这条路对每个学 JavaWeb 的人都是最快的成长路径。希望帮到你。
本文还有配套的精品资源,点击获取