一.项目背景
本项目是基于 Spring Boot 3 独立开发的五子棋在线对战平台,面向多用户实时对弈场景。系统涵盖用户注册登录、JWT 身份认证、在线匹配、实时对局、认输结算及积分流水查询等核心功能。持久层采用 MyBatis-Plus 操作 MySQL,匹配队列与对局状态借助 Redis 实现高效缓存与排序;对局过程通过 WebSocket(STOMP) 完成落子、心跳等实时消息推送,并结合 RabbitMQ 支持异步消息处理。同时集成 Spring Security、Bean Validation、Lombok 与 Actuator,保障接口安全、参数校验与系统可观测性。
主要技术栈:Spring、Spring MVC、Spring Boot 3.2、Spring Security、Spring WebSocket、MySQL、MyBatis-Plus、Redis、RabbitMQ、JWT、Maven、HTML、CSS、JavaScript
技术亮点:
- 统一使用
Result返回体与全局错误码定义,保证前后端交互格式一致,提升联调与测试效率- 基于
@RestControllerAdvice+@ExceptionHandler实现全局异常处理,按业务模块划分异常类型,规范化错误返回,便于问题定位- 采用 Spring Security + JWT 过滤器完成登录鉴权与接口防护,未授权请求自动拦截,保障用户与对局数据安全
- 基于 Redis Sorted Set 按积分就近匹配,支持加入/取消匹配与手动触发,提升在线对战匹配效率与公平性
- 通过 WebSocket(STOMP)实现落子、心跳、状态同步等实时通信,降低对局延迟,提升对战体验
- 对局状态采用 Redis 缓存 + MySQL 持久化的双写策略,兼顾实时读写性能与数据可靠性
- 积分流水支持分页查询,对局结果驱动积分变更,形成完整的对战---结算---统计业务闭环
二.测试模块
1.用户模块
- 登录功能
- 注册功能
- 验证身份信息
- 查询当前用户
- 查询当前用户状态
- 查询用户信息
2.游戏模块
- 匹配功能
- 取消匹配功能
- 获取房间信息功能
3.积分系统模块
- 查询用户积分流水
三.测试用例设计

四.自动化测试
1.工具类Utils
createDriver():配置并创建谷歌驱动
ScreenShot():屏幕截图
login():使用默认账号从登录页进行登录
java
package common;
import io.github.bonigarcia.wdm.WebDriverManager;
import org.apache.commons.io.FileUtils;
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.Duration;
public class Utils {
public static WebDriver driver;
public Utils(String url) {
driver = createDriver();
driver.get(url);
}
/**
* 获取驱动
* 获取驱动对象
*
* @return
*/
public static WebDriver createDriver() {
if (driver == null) {
// WebDriverManager.chromedriver().setup();
ChromeOptions options = new ChromeOptions();
options.addArguments("--remote-allow-origins=*");
driver = new ChromeDriver(options);
//通用等待
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(3));
}
return driver;
}
/**
* 获取屏幕截图
*/
public void ScreenShot(String str) throws IOException {
// .src/test/java/images/2026-07-23/test01_14392010.png
SimpleDateFormat sim1 = new SimpleDateFormat("yyyy-MM-dd");
SimpleDateFormat sim2 = new SimpleDateFormat("HHmmssSS");
String dirTime = sim1.format(System.currentTimeMillis());
String fileTime = sim2.format(System.currentTimeMillis());
String fileName = ".src/test/java/images" + dirTime + "/" + str + "_" + fileTime + ".png";
File scrFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
FileUtils.copyFile(scrFile, new File(fileName));
}
public static void login() throws InterruptedException {
driver.findElement(By.cssSelector("#username")).sendKeys("xiaoliao");
driver.findElement(By.cssSelector("#password")).sendKeys("q12345678");
Thread.sleep(1000);
driver.findElement(By.cssSelector("#loginBtn")).click();
}
}
2.注册页面RegisterPage
CheckPageRight():检查页面元素是否正确加载
RegisterFail():测试注册失败的情况
RegisterSuccess():测试注册成功的情况
java
package tests;
import common.Utils;
import org.openqa.selenium.By;
import java.util.Random;
public class RegisterPage extends Utils {
public static String url = "http://1.13.23.131:8080/register.html";
public RegisterPage() {
super(url);
}
/**
* 检查页面元素是否存在
*/
public void CheckPageRight(){
driver.findElement(By.cssSelector("body > div > div > div.auth-header > div"));
driver.findElement(By.cssSelector("body > div > div > div.auth-header > h1"));
driver.findElement(By.cssSelector("#username"));
driver.findElement(By.cssSelector("#password"));
driver.findElement(By.cssSelector("#confirmPassword"));
driver.findElement(By.cssSelector("#registerBtn"));
driver.findElement(By.cssSelector("#registerForm > div.auth-footer > p > a"));
driver.findElement(By.cssSelector("body > div > div > div.auth-footer > p:nth-child(1)"));
}
/**
* 测试注册失败的场景
* --账号与密码为空
* --账号为空,密码不为空
* --账号已存在
* --密码为空
* --密码不符合要求
* --密码使用特殊字符
*/
public void RegisterFail(){
//账号与密码为空
driver.findElement(By.cssSelector("#registerBtn")).click();
//账号为空
driver.findElement(By.cssSelector("#password")).sendKeys("q12345678");
driver.findElement(By.cssSelector("#confirmPassword")).sendKeys("q12345678");
driver.findElement(By.cssSelector("#registerBtn")).click();
//账号已存在
driver.findElement(By.cssSelector("#username")).sendKeys("xiaoliao");
driver.findElement(By.cssSelector("#registerBtn")).click();
//密码为空
driver.findElement(By.cssSelector("#username")).clear();
driver.findElement(By.cssSelector("#username")).sendKeys("xiaoliao1");
driver.findElement(By.cssSelector("#password")).clear();
driver.findElement(By.cssSelector("#confirmPassword")).clear();
driver.findElement(By.cssSelector("#registerBtn")).click();
//密码不符合规则
driver.findElement(By.cssSelector("#password")).sendKeys("12345678");
driver.findElement(By.cssSelector("#confirmPassword")).sendKeys("12345678");
driver.findElement(By.cssSelector("#registerBtn")).click();
//密码使用特殊字符
driver.findElement(By.cssSelector("#password")).sendKeys("_@");
driver.findElement(By.cssSelector("#confirmPassword")).sendKeys("_@");
driver.findElement(By.cssSelector("#registerBtn")).click();
}
public void RegisterSuccess() throws InterruptedException {
driver.findElement(By.cssSelector("#username")).clear();
driver.findElement(By.cssSelector("#password")).clear();
driver.findElement(By.cssSelector("#confirmPassword")).clear();
driver.findElement(By.cssSelector("#username")).sendKeys("xiaoliao1");
driver.findElement(By.cssSelector("#password")).sendKeys("q12345678");
driver.findElement(By.cssSelector("#confirmPassword")).sendKeys("q12345678");
Thread.sleep(1000);
driver.findElement(By.cssSelector("#registerBtn")).click();
}
}
3.登录页面LoginPage
CheckPageRight():检查页面元素是否正确加载
LoginFail():测试登录失败的情况
LoginSuccess():测试登录成功的情况
java
package tests;
import common.Utils;
import org.openqa.selenium.By;
import java.time.Duration;
public class LoginPage extends Utils {
public static String url = "http://1.13.23.131:8080/login.html";
public LoginPage() {
super(url);
}
/**
* 检查页面元素是否加载
*/
public void checkPageRight(){
driver.findElement(By.cssSelector("body > div > div > div.auth-header > div"));
driver.findElement(By.cssSelector("body > div > div > div.auth-header > h1"));
driver.findElement(By.cssSelector("#username"));
driver.findElement(By.cssSelector("#password"));
driver.findElement(By.cssSelector("#loginBtn"));
driver.findElement(By.cssSelector("#loginForm > div.auth-footer > p > a"));
}
/**
* 测试登录失败的场景
*--账户与密码为空
*--账户为空
*--密码为空
*--密码错误
*/
public void loginFail(){
//账户与密码为空
driver.findElement(By.cssSelector("#loginBtn")).click();
//账户为空
driver.findElement(By.cssSelector("#password")).sendKeys("q12345678");
driver.findElement(By.cssSelector("#loginBtn")).click();
//密码为空
driver.findElement(By.cssSelector("#password")).clear();
driver.findElement(By.cssSelector("#username")).sendKeys("xiaoliao");
driver.findElement(By.cssSelector("#loginBtn")).click();
//密码错误
driver.findElement(By.cssSelector("#password")).sendKeys("q123456789");
driver.findElement(By.cssSelector("#loginBtn")).click();
}
/**
* 测试登录成功场景
*/
public void loginSuccess() throws InterruptedException {
driver.findElement(By.cssSelector("#password")).clear();
driver.findElement(By.cssSelector("#password")).sendKeys("q12345678");
driver.findElement(By.cssSelector("#loginBtn")).click();
}
}
4.大厅页面HallPage
checkPageRight():检查页面元素是否正确加载
Match():测试匹配功是否正常
java
package tests;
import common.Utils;
import org.openqa.selenium.By;
import java.io.IOException;
public class HallPage extends Utils {
public static String url = "http://1.13.23.131:8080/login.html";
public HallPage() {
super(url);
}
/**
* 检查元素是否存在
*/
public void checkPageRight() throws InterruptedException {
login();
driver.findElement(By.cssSelector("body > div.top-nav > div > div.nav-logo"));
driver.findElement(By.cssSelector("#navUsername"));
driver.findElement(By.cssSelector("body > div.top-nav > div > div.nav-user > button"));
driver.findElement(By.cssSelector("body > div.container > div > div.user-panel > h2"));
driver.findElement(By.cssSelector("body > div.container > div > div.user-panel"));
driver.findElement(By.cssSelector("#matchBtn"));
driver.findElement(By.cssSelector("body > div.container > div > div.history-panel > h2"));
driver.findElement(By.cssSelector("body > div.container > div > div.history-panel"));
}
/**
* 检查匹配功能能否使用
*/
public void Match() throws IOException, InterruptedException {
//已启动其他客户端进行匹配,检查能否匹配成功
driver.findElement(By.cssSelector("#matchBtn")).click();
//匹配成功后进行截图
Thread.sleep(5000);
ScreenShot(getClass().getName());
}
}
五.接口测试
1.用户接口


2.游戏接口


3.积分接口


六.性能测试
1.TPS监听器

2.汇总报告

3.响应超时时间(Response Times Over Time)

4.测试报告



七.Bug说明
Bug-01【严重】匹配接口高失败率(约 64%~66%)
- 现象:加入匹配大量失败,Apdex 最低(约 0.338)
- 影响:无法正常进入对局,核心业务不可用
- 可能原因:并发下 Redis 队列竞态、用户重复入队、鉴权 userId 不一致、匹配状态机异常、连接/锁竞争
Bug-02【严重】取消匹配与加入匹配同比例失败(约 64%~66%)
- 现象:取消失败样本数与匹配失败几乎一致
- 影响:用户可能卡在匹配中,存在体验问题与资源泄漏风险
- 可能原因:join 失败后仍调用 cancel、队列中无记录却取消、状态未同步
Bug-03【严重】积分流水查询高失败率(约 46%)
- 现象:读接口接近一半失败
- 影响:战绩 / 积分页不可用
- 可能原因:数据库压力、分页 SQL 问题、用户无数据时异常未兜底、连接池耗尽
Bug-04【高】获取房间信息失败率高 + 超长响应
- 现象:错误率约 34%~36%,Max 可达约 14s~30s
- 影响:进房 / 刷新房间失败,对局流程中断
- 可能原因:roomId 无效(匹配未成功)、Redis 无缓存回源慢、锁等待、超时配置过长
Bug-05【高】登录接口零失败但极端超时
- 现象:错误率 0%,Max ≈ 29s,99 分位可达约 7s;末段响应时间图垂直拉升
- 影响:高峰登录卡住,易被用户感知为"系统挂掉"
- 可能原因:DB / Redis 变慢、密码校验耗 CPU、线程池打满后排队
Bug-06【中】鉴权类读接口偶发失败(约 11%~16%)
- 涉及:当前用户、用户当前状态、查询用户信息
- 可能原因:Token 失效 / 并发覆盖、压测脚本 Token 复用不当、Security 过滤器异常、下游超时
Bug-07【中】整体稳定性不足(总失败约 28%)
- 现象:TPS 抖动、多接口同步下跌、末期性能塌陷
- 说明:不仅是单接口逻辑问题,还存在系统级瓶颈(连接池、线程、Redis / MySQL、锁)
八.总结
认证链路(登录、Token 验证)基本可用;匹配---房间---积分主链路在当前压力下不可接受。总失败率约 28%,匹配相关失败超过六成,末期出现近 30s 超时,性能与稳定性均不达标。
后续改进建议:
- 优先修复匹配 / 取消匹配的并发与状态一致性问题(Bug-01 / Bug-02)
- 排查积分流水与房间信息失败原因及慢查询(Bug-03 / Bug-04)
- 针对登录峰值优化耗时、连接池与超时配置(Bug-05)