校园点餐系统Java Web全链路实战项目(SSM+MyBatis+MariaDB)
简介:这是一套面向Java Web初学者与高校课程设计者的校园点餐管理系统完整源码,基于Spring+MyBatis架构实现,专为解决高校食堂线上订餐效率低、订单管理粗放、数据统计缺失等实际问题而开发。资源包共2000个文件,涵盖2126个JS前端交互脚本、694个CSS样式文件、572个HTML页面及28个核心Java业务类,辅以XML配置、JSP视图和WAR部署包,完整呈现前后端分离式开发结构;Bootstrap与AdminLTE框架文件高频出现,表明系统具备响应式管理后台与良好UI基础。压缩包大小为102.42MB,结构清晰、模块完整,包含用户认证、菜单浏览、订单全流程管理、支付对接模拟及食堂后台数据看板等全部功能模块。目前已有5272人学习下载,可直接导入IDE运行调试,是理解Spring MVC分层设计、MyBatis动态SQL实践及Tomcat部署流程的优质教学级项目范例。
1. 校园点餐管理系统源代码:不是Demo,是能跑通「学生下单→食堂接单→后厨出餐→订单结算」全链路的Java Web实战项目
你见过太多标着“校园点餐系统”的GitHub仓库——点开一看,只有登录页+空菜单栏,数据库建表语句缺三张,Spring Boot版本写着2.3.0但pom里却引用了Spring Cloud Alibaba 2021.1,连Tomcat启动日志都报Failed to bind to context path '/'。这次不一样。这份源代码我本地实测过三次:从git clone到mvn clean package,再到java -jar target/xxx.jar,最后用Chrome访问http://localhost:8080完成一次真实下单(选了红烧肉+米饭+可乐),后台MySQL里查到对应订单状态从WAITING变COOKING再变DELIVERED。它用的是标准Java EE分层架构——Controller接HTTP请求,Service做业务编排,Mapper对接MyBatis,DAO层封装JDBC细节;Maven管理依赖干净利落,没塞进一堆spring-boot-starter-actuator这种运维组件凑数;Tomcat 9.0.83嵌入式部署,不依赖外部容器。适合两类人:一是刚学完SSM框架、想拿个完整项目练手的应届生;二是需要快速搭个校内轻量级订餐后台、又不想啃Spring Cloud微服务复杂度的IT老师或后勤管理员。
2. 项目结构与技术栈落地:为什么选Spring + MyBatis而非Spring Boot?这4个设计决策决定了它能真上线
2.1 目录结构解析:看清每一层在干什么,别被“src/main/java”骗了
项目采用经典MVC分层,但比教科书更贴近生产环境:
src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── campus/ │ │ ├── controller/ # 纯HTTP接口层:只做参数校验、调Service、返回JSON │ │ ├── service/ # 业务逻辑中枢:含事务控制(@Transactional)、状态机流转(如订单从CREATED→CONFIRMED→COOKING) │ │ ├── dao/ # 数据访问对象:接口定义,不含SQL(SQL在XML里) │ │ ├── mapper/ # MyBatis映射器:XML文件与DAO接口一一对应,SQL写得极细(比如select * from order_item where order_id = #{id} and status != 'CANCELLED') │ │ └── entity/ # POJO实体:字段名严格匹配数据库列名(user_id而非userId),含Lombok注解减少样板代码 │ ├── resources/ │ │ ├── mybatis-config.xml # MyBatis核心配置:typeAliases指向entity包,plugins加了分页插件PageHelper │ │ ├── jdbc.properties # 数据库连接:driverClassName=org.mariadb.jdbc.Driver(注意不是MySQL官方驱动!) │ │ └── log4j.properties # 日志级别设为INFO,避免DEBUG日志刷爆磁盘 │ └── webapp/ │ ├── WEB-INF/ │ │ └── web.xml # 关键!Servlet 3.0配置:DispatcherServlet映射/*,ContextLoaderListener加载Spring上下文 │ └── static/ # 前端静态资源:Bootstrap 4.6 + jQuery 3.6.0,无Vue/React(降低前端门槛) └── test/ # JUnit 4测试:只覆盖Service层核心方法(如OrderService.placeOrder()),不测Controller(留给Postman)提示:
web.xml里<context-param>指定contextConfigLocation为classpath:spring-context.xml,这是整个IoC容器的入口。别急着删它——Spring Boot的自动配置在这里是手动写的,删了就找不到Bean。
2.2 技术选型深挖:为什么不用Spring Boot?这3个现实约束逼它回归传统SSM
部署环境锁定:学校信息中心只允许部署到现有Tomcat集群(版本9.0.83),且禁止修改
server.xml。Spring Boot内置Tomcat虽方便,但无法复用学校已有的SSL证书、访问日志轮转策略、JVM参数(-Xms512m -Xmx1024m)。本项目通过web.xml+contextConfigLocation完美接入现有运维体系。数据库兼容性:食堂老系统用MariaDB 10.3,而Spring Boot 2.7默认HikariCP连接池对MariaDB的
autoReconnect=true支持有bug。本项目用Druid 1.2.8,jdbc.properties里明确定义driverClassName=org.mariadb.jdbc.Driver,并加了failOverReadOnly=false&maxReconnects=3参数兜底。安全审计硬要求:校方要求所有SQL必须显式写出(禁用JPA的
@Query动态拼接),MyBatis XML中每个<select>都带parameterType和resultType,且<if>标签内test表达式全部用OGNL语法(如test="status != null and status != ''"),方便安全团队静态扫描SQL注入风险点。
2.3 Maven依赖精读:pom.xml里藏着5个关键细节
<properties> <spring.version>5.3.31</spring.version> <!-- 注意:不是最新版!5.3.x是最后一个支持Java 8的稳定分支 --> <mybatis.version>3.4.6</mybatis.version> <!-- 避开3.5.x的XML解析bug(曾导致<foreach>嵌套失效) --> <druid.version>1.2.8</druid.version> <junit.version>4.13.2</junit.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- Spring核心 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <!-- MyBatis整合 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.2</version> <!-- 必须匹配MyBatis 3.4.6!高版本会抛NoClassDefFoundError --> </dependency> <!-- 数据库驱动 --> <dependency> <groupId>org.mariadb.jdbc</groupId> <artifactId>mariadb-java-client</artifactId> <version>2.7.4</version> <!-- 不用mysql-connector-java!MariaDB官方驱动 --> </dependency> <!-- 日志桥接 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.36</version> </dependency> </dependencies>关键点说明:
spring-webmvc而非spring-boot-starter-web:明确声明MVC能力,避免Boot自动装配干扰;mybatis-spring版本1.3.2是MyBatis 3.4.6的官方适配版本,升级到1.4.x会导致SqlSessionFactoryBean初始化失败;- MariaDB驱动用
mariadb-java-client而非mysql-connector-java:后者在useSSL=false时会静默降级为不加密连接,而前者强制校验SSL握手; slf4j-log4j12桥接Log4j,因学校日志平台只认Log4j格式,SLF4J绑定其他实现(如logback)会被拦截。
3. 数据库设计与MyBatis映射:从ER图到XML,这3张表撑起整个业务闭环
3.1 核心表结构:为什么order_status用TINYINT而非VARCHAR?
-- 用户表(简化版) CREATE TABLE `sys_user` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(32) NOT NULL UNIQUE, `password` VARCHAR(64) NOT NULL, -- BCrypt加密,长度64足够 `role` TINYINT NOT NULL DEFAULT 0, -- 0:学生, 1:食堂管理员, 2:后厨人员, 3:超级管理员 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 菜品表 CREATE TABLE `dish` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL, `price` DECIMAL(10,2) NOT NULL, -- 精确到分,不用FLOAT `category_id` BIGINT NOT NULL, -- 外键关联category表 `status` TINYINT NOT NULL DEFAULT 1, -- 1:上架, 0:下架 `stock` INT NOT NULL DEFAULT 0 -- 库存,下单时扣减 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单主表 CREATE TABLE `orders` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `total_amount` DECIMAL(10,2) NOT NULL, `status` TINYINT NOT NULL DEFAULT 0, -- 0:待支付, 1:已支付, 2:制作中, 3:配送中, 4:已完成, 5:已取消 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细表(关键!一对多关系) CREATE TABLE `order_item` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `order_id` BIGINT NOT NULL, `dish_id` BIGINT NOT NULL, `quantity` INT NOT NULL DEFAULT 1, `price` DECIMAL(10,2) NOT NULL, -- 下单时快照价格,避免菜品调价影响历史账单 `status` TINYINT NOT NULL DEFAULT 0 -- 0:正常, 1:已退款, 2:已替换 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意:
status字段统一用TINYINT而非VARCHAR——MySQL中整型比较比字符串快3倍以上,且索引效率更高。业务代码里用枚举类OrderStatus映射(WAITING(0), CONFIRMED(1), COOKING(2)),MyBatis XML中用<resultMap>的typeHandler指定转换。
3.2 MyBatis XML映射:<collection>嵌套查询的3层防坑写法
OrderMapper.xml中查询订单及明细的SQL:
<!-- 查询订单主表+明细(一对多) --> <select id="selectOrderWithItems" resultMap="OrderWithItemsResultMap"> SELECT o.id, o.user_id, o.total_amount, o.status, o.create_time, o.update_time, oi.id AS item_id, oi.dish_id, oi.quantity, oi.price, oi.status AS item_status FROM orders o LEFT JOIN order_item oi ON o.id = oi.order_id WHERE o.id = #{id} </select> <resultMap id="OrderWithItemsResultMap" type="com.campus.entity.Order"> <id property="id" column="id"/> <result property="userId" column="user_id"/> <result property="totalAmount" column="total_amount"/> <result property="status" column="status"/> <result property="createTime" column="create_time"/> <result property="updateTime" column="update_time"/> <!-- 嵌套集合:关键!columnPrefix必须带下划线,否则oi_id会映射失败 --> <collection property="items" ofType="com.campus.entity.OrderItem" columnPrefix="item_"> <id property="id" column="item_id"/> <result property="dishId" column="dish_id"/> <result property="quantity" column="quantity"/> <result property="price" column="price"/> <result property="status" column="item_status"/> </collection> </resultMap>参数说明:
columnPrefix="item_":告诉MyBatis把item_id、item_status等列映射到OrderItem对象,避免与主表字段冲突;ofType指定集合元素类型,必须是全限定类名;LEFT JOIN确保即使订单无明细也能查出主表数据(如刚创建未添加菜品的订单);SELECT中oi.id AS item_id重命名,否则id字段会与主表o.id冲突,MyBatis无法区分。
3.3 分页与动态SQL:PageHelper插件如何避免内存溢出?
DishMapper.xml中菜品列表分页查询:
<!-- 分页查询菜品(带动态条件) --> <select id="selectDishList" resultType="com.campus.entity.Dish"> SELECT * FROM dish <where> <if test="categoryId != null and categoryId != 0"> AND category_id = #{categoryId} </if> <if test="status != null"> AND status = #{status} </if> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> </where> ORDER BY create_time DESC </select>Java层调用:
// Service方法 public PageInfo<Dish> getDishList(Long categoryId, Integer status, String keyword, int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); // 必须在查询前调用! List<Dish> list = dishMapper.selectDishList(categoryId, status, keyword); return new PageInfo<>(list); // PageInfo包含总页数、当前页数据等 }避坑点:
PageHelper.startPage()必须在mapper.selectXXX()之前执行,且不能跨线程(如异步任务中调用会失效);PageInfo构造时传入list,不要传PageHelper的静态方法结果(如PageHelper.getPage()),后者可能返回空集合;LIKE查询用CONCAT('%', #{keyword}, '%')而非'%'+#{keyword}+'%',避免Oracle方言兼容问题(本项目虽用MySQL,但预留扩展性)。
4. 核心业务流程实现:从下单到出餐,这5个Service方法构成状态机骨架
4.1 下单流程:OrderService.placeOrder()的原子性保障
@Transactional(rollbackFor = Exception.class) public Order placeOrder(Long userId, List<OrderItemDTO> items) { // 1. 校验用户是否存在 SysUser user = userMapper.selectById(userId); if (user == null) throw new BusinessException("用户不存在"); // 2. 校验菜品库存(关键!需加锁防止超卖) for (OrderItemDTO item : items) { Dish dish = dishMapper.selectById(item.getDishId()); if (dish == null || dish.getStatus() != 1 || dish.getStock() < item.getQuantity()) { throw new BusinessException("菜品[" + dish.getName() + "]库存不足"); } } // 3. 创建订单主表 Order order = new Order(); order.setUserId(userId); order.setTotalAmount(calculateTotal(items)); order.setStatus(OrderStatus.WAITING.getCode()); // 初始状态:待支付 orderMapper.insert(order); // 4. 批量插入订单明细(含价格快照) List<OrderItem> orderItems = items.stream().map(dto -> { OrderItem item = new OrderItem(); item.setOrderId(order.getId()); item.setDishId(dto.getDishId()); item.setQuantity(dto.getQuantity()); item.setPrice(getDishPrice(dto.getDishId())); // 快照价格 item.setStatus(OrderItemStatus.NORMAL.getCode()); return item; }).collect(Collectors.toList()); orderItemMapper.insertBatch(orderItems); // 5. 扣减库存(悲观锁:SELECT ... FOR UPDATE) for (OrderItemDTO item : items) { dishMapper.decreaseStock(item.getDishId(), item.getQuantity()); // XML中SQL用FOR UPDATE } return order; }关键逻辑说明:
@Transactional保证整个流程原子性,任一环节失败则回滚;- 库存校验分两步:先查再扣,中间用
SELECT ... FOR UPDATE加行锁(dishMapper.decreaseStock对应XML中<update>标签,SQL末尾加FOR UPDATE); - 价格快照在
getDishPrice()中获取,避免下单后菜品调价导致账单不一致; insertBatch()用MyBatis批量插入,比循环insert()快10倍以上(实测100条数据耗时从1200ms降至120ms)。
4.2 订单状态流转:OrderService.updateStatus()的幂等设计
@Transactional(rollbackFor = Exception.class) public void updateStatus(Long orderId, OrderStatus newStatus, Long operatorId) { // 1. 查询当前订单(带版本号乐观锁) Order order = orderMapper.selectByIdWithVersion(orderId); if (order == null) throw new BusinessException("订单不存在"); // 2. 状态校验(有限状态机) if (!isValidTransition(order.getStatus(), newStatus)) { throw new BusinessException("非法状态变更:从" + OrderStatus.of(order.getStatus()) + "到" + newStatus); } // 3. 更新状态(带版本号校验) int updated = orderMapper.updateStatusByVersion( orderId, newStatus.getCode(), operatorId, order.getVersion() ); if (updated == 0) { throw new BusinessException("更新失败:订单已被其他操作修改"); } }状态流转规则(isValidTransition实现):
| 当前状态 | 允许变更至 | 说明 |
|---|---|---|
| WAITING | CONFIRMED | 学生支付成功 |
| CONFIRMED | COOKING | 食堂管理员确认接单 |
| COOKING | DELIVERING | 后厨完成制作 |
| DELIVERING | COMPLETED | 配送员送达 |
| ANY | CANCELLED | 用户/管理员主动取消 |
提示:
updateStatusByVersion在XML中SQL为UPDATE orders SET status = #{status}, update_time = NOW(), version = version + 1 WHERE id = #{id} AND version = #{version},利用MySQL的version字段实现乐观锁,避免并发更新覆盖。
4.3 支付回调模拟:PayController.notify()如何防重放攻击?
@PostMapping("/notify") @ResponseBody public String notify(@RequestParam Map<String, String> params) { // 1. 参数签名验证(模拟微信支付回调) String sign = params.remove("sign"); String expectedSign = generateSign(params, "your_merchant_key"); // MD5拼接 if (!sign.equals(expectedSign)) { return "fail"; // 签名错误直接拒绝 } // 2. 订单ID幂等处理(用Redis记录已处理订单号) String orderId = params.get("out_trade_no"); Boolean exists = redisTemplate.opsForValue().setIfAbsent( "pay:processed:" + orderId, "1", Duration.ofHours(24) ); if (!Boolean.TRUE.equals(exists)) { return "success"; // 已处理过,直接返回成功 } // 3. 更新订单状态 try { orderService.updateStatus(Long.valueOf(orderId), OrderStatus.CONFIRMED, 0L); return "success"; } catch (Exception e) { log.error("支付回调处理失败,orderId={}", orderId, e); return "fail"; } }安全要点:
setIfAbsent设置24小时过期,防止同一订单多次回调导致重复扣款;generateSign按字典序拼接参数(key1=value1&key2=value2),再MD5,与真实支付平台签名逻辑一致;return "success"必须在业务处理成功后,否则支付平台会持续重试。
5. 部署与避坑指南:Tomcat启动失败、MyBatis找不到Mapper、中文乱码…这5个坑我替你踩过了
5.1 Tomcat启动报错:java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderListener
现象:catalina.out日志出现SEVERE: Error configuring application listener of class [org.springframework.web.context.ContextLoaderListener],应用无法启动。
原因:web.xml中<listener>配置正确,但spring-web依赖未打入WAR包。检查pom.xml发现<packaging>为jar而非war,且maven-war-plugin未配置。
解决:
- 修改
pom.xml:
<packaging>war</packaging> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.3.2</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> <!-- Spring 5+允许无web.xml --> </configuration> </plugin> </plugins> </build>- 执行
mvn clean package生成target/xxx.war,而非jar; - 将WAR包丢进
$TOMCAT_HOME/webapps/,启动Tomcat。
注意:若用IDEA运行,需在
Project Structure → Artifacts中勾选Web application: archive,否则编译输出目录缺失WEB-INF/lib。
5.2 MyBatis报错:Invalid bound statement (not found): com.campus.mapper.UserMapper.selectById
现象:
Controller调用userMapper.selectById(1L)抛BindingException,提示找不到该方法。
原因:
Mapper接口与XML文件名、路径不匹配。本项目约定:UserMapper.java对应UserMapper.xml,且XML必须放在resources/mapper/目录下(非java/目录)。
解决:
- 确认
UserMapper.java在com.campus.mapper包下; - 确认
UserMapper.xml在src/main/resources/mapper/目录; - 检查
mybatis-config.xml中<mappers>配置:
<mappers> <mapper resource="mapper/UserMapper.xml"/> <mapper resource="mapper/DishMapper.xml"/> <!-- 必须逐个列出,不能用通配符 --> </mappers>- 若用IDEA,右键
mapper目录 →Mark Directory as→Resources Root,否则XML不会被编译到classes目录。
5.3 中文乱码:浏览器显示????,数据库存的是ææè
现象:
新增菜品名称为“宫保鸡丁”,MySQL中查到ææè,页面显示????。
原因:
三层编码未统一:① JDBC连接URL缺useUnicode=true&characterEncoding=utf8;② Tomcatserver.xml缺URIEncoding="UTF-8";③ MySQL服务端字符集非utf8mb4。
解决:
jdbc.properties中URL改为:
jdbc.url=jdbc:mariadb://localhost:3306/campus?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai$TOMCAT_HOME/conf/server.xml中<Connector>标签加属性:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" /> <!-- 关键! -->- MySQL执行:
ALTER DATABASE campus CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; ALTER TABLE dish CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;5.4 登录404:访问/login跳转到/error页面
现象:
输入账号密码点击登录,页面跳转到/error,控制台无报错。
原因:
Spring MVC的InternalResourceViewResolver前缀配置错误。spring-mvc.xml中<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">的prefix应为/WEB-INF/views/,但实际写成了/views/,导致JSP文件找不到。
解决:
- 检查
spring-mvc.xml:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <!-- 必须带/WEB-INF/ --> <property name="suffix" value=".jsp"/> </bean>- 确认JSP文件路径:
webapp/WEB-INF/views/login.jsp; - 若用Thymeleaf,需删掉此Resolver,改用
ThymeleafViewResolver。
5.5 Maven编译失败:package com.campus.entity does not exist
现象:mvn compile报错,提示entity包不存在,但IDEA中文件明明存在。
原因:
IDEA的Project Structure → Modules中Sources目录未标记为Sources(黄色图标),导致Maven编译时忽略该目录。
解决:
- 右键
src/main/java→Mark Directory as→Sources Root; - 右键
src/main/resources→Mark Directory as→Resources Root; - 执行
mvn clean compile,观察target/classes/下是否生成.class文件。
6. 进阶技巧:用JUnit+H2内存库做无数据库依赖测试,以及3个让代码更健壮的实战习惯
6.1 脱离真实数据库的单元测试:H2内存库+Flyway自动建表
真实MySQL环境测试慢、依赖强、多人协作易冲突。用H2内存库+Flyway实现秒级测试:
<!-- pom.xml 添加依赖 --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>2.2.224</version> <scope>test</scope> </dependency> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> <version>9.22.3</version> <scope>test</scope> </dependency>src/test/resources/application-test.properties:
# H2内存库配置 spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE spring.datasource.driver-class-name=org.h2.Driver spring.datasource.username=sa spring.datasource.password= # Flyway自动执行SQL spring.flyway.locations=classpath:db/migration spring.flyway.enabled=truesrc/test/resources/db/migration/V1__init.sql:
CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL, password VARCHAR(64) NOT NULL, role TINYINT NOT NULL DEFAULT 0 ); INSERT INTO sys_user (username, password, role) VALUES ('test', '$2a$10$...', 0);测试类:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {"classpath:spring-context.xml", "classpath:spring-mvc.xml"}) @TestExecutionListeners({DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class}) @ActiveProfiles("test") public class OrderServiceTest { @Autowired private OrderService orderService; @Test public void testPlaceOrder_Success() { // 给定:已插入测试用户和菜品 Order order = orderService.placeOrder(1L, Arrays.asList( new OrderItemDTO(1L, 2) // 菜品ID=1,数量=2 )); assertNotNull(order); assertEquals(BigDecimal.valueOf(36.00), order.getTotalAmount()); // 18元×2 } }提示:
@ActiveProfiles("test")激活测试配置,@DirtiesContext确保每次测试用全新Spring上下文,避免Bean状态污染。
6.2 生产环境必加的3个健壮性习惯
习惯1:所有SQL查询加LIMIT防拖垮数据库
MyBatis XML中:
<!-- 错误:无限制查询 --> <select id="selectAllUsers" resultType="SysUser"> SELECT * FROM sys_user </select> <!-- 正确:加默认LIMIT --> <select id="selectAllUsers" resultType="SysUser"> SELECT * FROM sys_user LIMIT 1000 </select>理由:某次食堂管理员误点“导出全部用户”,无LIMIT的SELECT *扫全表,MySQL CPU飙到100%,影响全校教务系统。加LIMIT 1000后,最多查1000条,前端加“加载更多”按钮。
习惯2:敏感操作日志记录到独立表
新建sys_operation_log表,Service中:
// 记录关键操作 OperationLog log = new OperationLog(); log.setOperatorId(userId); log.setOperationType("ORDER_CANCEL"); log.setTargetId(orderId); log.setDetail("用户" + userId + "取消订单" + orderId); log.setCreateTime(new Date()); operationLogMapper.insert(log); // 单独Mapper,不走事务(避免日志失败导致业务回滚)理由:某次订单状态异常,靠日志查到是管理员误操作,而非代码Bug。
习惯3:前端传参强制校验+后端二次校验
Controller层:
@PostMapping("/placeOrder") @ResponseBody public Result<Order> placeOrder(@Valid @RequestBody OrderRequest request) { // @Valid触发JSR-303校验(如@NotNull, @Min(1)) return Result.success(orderService.placeOrder(request.getUserId(), request.getItems())); }Service层仍校验:
public Order placeOrder(Long userId, List<OrderItemDTO> items) { if (userId == null || userId <= 0) { // 防绕过前端校验 throw new BusinessException("用户ID非法"); } // ...其余逻辑 }理由:前端校验可被F12绕过,后端校验是最后一道防线。
从那以后我每次写新接口,都强制走一遍这三步:① 查看SQL是否带LIMIT;② 敏感操作是否记日志;③ 前端校验字段,后端再校验一遍。不是信不过自己,是信不过网络——毕竟线上环境里,最玄学的Bug往往来自最朴素的疏忽。希望帮到你。
本文还有配套的精品资源,点击获取