|
|
@@ -0,0 +1,326 @@
|
|
|
+package com.fs.app.course;
|
|
|
+
|
|
|
+import com.fs.FsUserAppApplication;
|
|
|
+import com.fs.common.constant.FsConstants;
|
|
|
+import com.fs.common.core.redis.RedisCache;
|
|
|
+import com.fs.common.core.domain.R;
|
|
|
+import com.fs.course.mapper.FsCourseRedPacketLogMapper;
|
|
|
+import com.fs.course.param.FsCourseSendRewardUParam;
|
|
|
+import com.fs.course.service.IFsUserCourseVideoService;
|
|
|
+import org.junit.After;
|
|
|
+import org.junit.Before;
|
|
|
+import org.junit.Test;
|
|
|
+import org.junit.runner.RunWith;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.boot.test.context.SpringBootTest;
|
|
|
+import org.springframework.test.context.junit4.SpringRunner;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+
|
|
|
+import static org.junit.Assert.*;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 红包余额为0测试类
|
|
|
+ * 测试场景:公司红包余额为0时,不能发送红包
|
|
|
+ *
|
|
|
+ * @author test
|
|
|
+ * @date 2025-12-30
|
|
|
+ */
|
|
|
+@RunWith(SpringRunner.class)
|
|
|
+@SpringBootTest(classes = FsUserAppApplication.class)
|
|
|
+public class RedPacketBalanceZeroTest {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private IFsUserCourseVideoService courseVideoService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private RedisCache redisCache;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private FsCourseRedPacketLogMapper redPacketLogMapper;
|
|
|
+
|
|
|
+ // ==================== 测试数据配置 ====================
|
|
|
+ private static final Long TEST_COMPANY_ID = 300L; // 测试公司ID(请根据实际调整)
|
|
|
+ private static final Long TEST_COURSE_ID = 104L; // 测试课程ID
|
|
|
+ private static final Long TEST_VIDEO_ID = 3877L; // 测试视频ID
|
|
|
+ private static final Long TEST_USER_ID = 459809L; // 测试用户ID
|
|
|
+ private static final Long TEST_COMPANY_USER_ID = 9587L; // 测试公司用户ID
|
|
|
+ private static final Long TEST_PERIOD_ID = 705L; // 测试期数ID
|
|
|
+
|
|
|
+ private String companyMoneyKey;
|
|
|
+ private String originalBalance; // 保存原始余额,用于恢复
|
|
|
+
|
|
|
+ @Before
|
|
|
+ public void setUp() {
|
|
|
+ System.out.println("\n" + repeat("=", 60));
|
|
|
+ System.out.println(" 开始测试:红包余额为0不能发放红包");
|
|
|
+ System.out.println(repeat("=", 60));
|
|
|
+
|
|
|
+ // 初始化Redis Key
|
|
|
+ companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + TEST_COMPANY_ID;
|
|
|
+
|
|
|
+ // 保存原始余额
|
|
|
+ originalBalance = redisCache.getCacheObject(companyMoneyKey);
|
|
|
+ System.out.println("📌 测试准备:保存原始余额 = " + originalBalance);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 核心测试用例:余额为0时不能发送红包
|
|
|
+ * 验证点:
|
|
|
+ * 1. 设置余额为0后,发送红包应返回错误
|
|
|
+ * 2. 错误信息应包含"余额不足"
|
|
|
+ * 3. Redis余额应保持为0,未被扣减
|
|
|
+ * 4. 数据库不应生成成功的红包记录
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testSendRedPacketWhenBalanceIsZero() {
|
|
|
+ System.out.println("\n🧪 测试用例:余额为0时不能发送红包");
|
|
|
+ System.out.println(repeat("-", 60));
|
|
|
+
|
|
|
+ // ==================== 步骤1:设置Redis余额为0 ====================
|
|
|
+ System.out.println("\n📍 步骤1:设置Redis余额为0");
|
|
|
+ redisCache.setCacheObject(companyMoneyKey, "0");
|
|
|
+
|
|
|
+ String currentBalance = redisCache.getCacheObject(companyMoneyKey);
|
|
|
+ System.out.println(" Redis Key: " + companyMoneyKey);
|
|
|
+ System.out.println(" 当前余额: " + currentBalance);
|
|
|
+ assertEquals("初始余额应为0", "0", currentBalance);
|
|
|
+ System.out.println(" ✅ 余额设置成功");
|
|
|
+
|
|
|
+ // ==================== 步骤2:构造请求参数 ====================
|
|
|
+ System.out.println("\n📍 步骤2:构造发送红包请求参数");
|
|
|
+ FsCourseSendRewardUParam param = new FsCourseSendRewardUParam();
|
|
|
+ param.setCourseId(TEST_COURSE_ID);
|
|
|
+ param.setVideoId(TEST_VIDEO_ID);
|
|
|
+ param.setUserId(TEST_USER_ID);
|
|
|
+ param.setCompanyId(TEST_COMPANY_ID);
|
|
|
+ param.setCompanyUserId(TEST_COMPANY_USER_ID);
|
|
|
+ param.setPeriodId(TEST_PERIOD_ID);
|
|
|
+ param.setQwUserId("test_qw_user");
|
|
|
+ param.setAppId("test_app_id");
|
|
|
+ param.setQwExternalId(1L);
|
|
|
+
|
|
|
+ System.out.println(" 参数信息:");
|
|
|
+ System.out.println(" - 公司ID: " + param.getCompanyId());
|
|
|
+ System.out.println(" - 课程ID: " + param.getCourseId());
|
|
|
+ System.out.println(" - 视频ID: " + param.getVideoId());
|
|
|
+ System.out.println(" - 用户ID: " + param.getUserId());
|
|
|
+ System.out.println(" ✅ 参数构造完成");
|
|
|
+
|
|
|
+ // ==================== 步骤3:调用发送红包接口 ====================
|
|
|
+ System.out.println("\n📍 步骤3:调用发送红包接口");
|
|
|
+ R result = null;
|
|
|
+ try {
|
|
|
+ result = courseVideoService.sendReward(param);
|
|
|
+ } catch (Exception e) {
|
|
|
+ System.out.println(" ⚠️ 接口调用异常: " + e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 步骤4:验证返回结果 ====================
|
|
|
+ System.out.println("\n📍 步骤4:验证返回结果");
|
|
|
+
|
|
|
+ assertNotNull("返回结果不应为空", result);
|
|
|
+ System.out.println(" 返回码: " + result.get("code"));
|
|
|
+ System.out.println(" 返回消息: " + result.get("msg"));
|
|
|
+
|
|
|
+ // 验证返回错误
|
|
|
+ Integer code = (Integer) result.get("code");
|
|
|
+ assertTrue("应返回错误状态码 (500或400)",
|
|
|
+ code == 500 || code == 400);
|
|
|
+
|
|
|
+ // 验证错误消息包含"余额不足"
|
|
|
+ String msg = String.valueOf(result.get("msg"));
|
|
|
+ assertTrue("错误信息应包含'余额不足'",
|
|
|
+ msg != null && msg.contains("余额不足"));
|
|
|
+
|
|
|
+ System.out.println(" ✅ 返回错误提示正确");
|
|
|
+
|
|
|
+ // ==================== 步骤5:验证Redis余额未被扣减 ====================
|
|
|
+ System.out.println("\n📍 步骤5:验证Redis余额未被扣减");
|
|
|
+
|
|
|
+ String finalBalance = redisCache.getCacheObject(companyMoneyKey);
|
|
|
+ System.out.println(" 最终余额: " + finalBalance);
|
|
|
+
|
|
|
+ assertEquals("余额应保持为0,未被扣减", "0", finalBalance);
|
|
|
+ System.out.println(" ✅ 余额保持为0,未被扣减");
|
|
|
+
|
|
|
+ // ==================== 步骤6:验证数据库无成功记录 ====================
|
|
|
+ System.out.println("\n📍 步骤6:验证数据库日志");
|
|
|
+ System.out.println(" 提示:请手动查询数据库验证");
|
|
|
+ System.out.println(" SQL: SELECT * FROM fs_course_red_packet_log");
|
|
|
+ System.out.println(" WHERE company_id = " + TEST_COMPANY_ID);
|
|
|
+ System.out.println(" ORDER BY create_time DESC LIMIT 5;");
|
|
|
+
|
|
|
+ // ==================== 测试结论 ====================
|
|
|
+ System.out.println("\n" + repeat("=", 60));
|
|
|
+ System.out.println(" ✅ 测试通过:余额为0时成功阻止红包发放");
|
|
|
+ System.out.println(repeat("=", 60));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 测试用例2:余额不足时不能发送红包
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testSendRedPacketWhenBalanceNotEnough() {
|
|
|
+ System.out.println("\n🧪 测试用例:余额不足时不能发送红包");
|
|
|
+ System.out.println(repeat("-", 60));
|
|
|
+
|
|
|
+ // 设置余额为3元(假设红包金额为5元)
|
|
|
+ System.out.println("\n📍 步骤1:设置Redis余额为3元");
|
|
|
+ redisCache.setCacheObject(companyMoneyKey, "3");
|
|
|
+
|
|
|
+ String currentBalance = redisCache.getCacheObject(companyMoneyKey);
|
|
|
+ System.out.println(" 当前余额: " + currentBalance);
|
|
|
+
|
|
|
+ // 构造请求
|
|
|
+ System.out.println("\n📍 步骤2:调用发送红包接口");
|
|
|
+ FsCourseSendRewardUParam param = new FsCourseSendRewardUParam();
|
|
|
+ param.setCourseId(TEST_COURSE_ID);
|
|
|
+ param.setVideoId(TEST_VIDEO_ID);
|
|
|
+ param.setUserId(TEST_USER_ID);
|
|
|
+ param.setCompanyId(TEST_COMPANY_ID);
|
|
|
+ param.setCompanyUserId(TEST_COMPANY_USER_ID);
|
|
|
+ param.setPeriodId(TEST_PERIOD_ID);
|
|
|
+ param.setQwUserId("test_qw_user");
|
|
|
+ param.setAppId("test_app_id");
|
|
|
+ param.setQwExternalId(1L);
|
|
|
+
|
|
|
+ R result = courseVideoService.sendReward(param);
|
|
|
+
|
|
|
+ // 验证结果
|
|
|
+ System.out.println("\n📍 步骤3:验证返回结果");
|
|
|
+ System.out.println(" 返回码: " + result.get("code"));
|
|
|
+ System.out.println(" 返回消息: " + result.get("msg"));
|
|
|
+
|
|
|
+ Integer code = (Integer) result.get("code");
|
|
|
+ assertTrue("应返回错误状态码", code == 500 || code == 400);
|
|
|
+
|
|
|
+ // 验证余额未变化
|
|
|
+ String finalBalance = redisCache.getCacheObject(companyMoneyKey);
|
|
|
+ System.out.println(" 最终余额: " + finalBalance);
|
|
|
+
|
|
|
+ System.out.println("\n ✅ 余额不足时成功阻止红包发放");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 测试用例3:余额充足时能正常发送红包(可选,需要真实支付配置)
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testSendRedPacketWhenBalanceEnough() {
|
|
|
+ System.out.println("\n🧪 测试用例:余额充足时能正常发送红包");
|
|
|
+ System.out.println(repeat("-", 60));
|
|
|
+ System.out.println("⚠️ 注意:此测试需要真实的支付配置和红包接口");
|
|
|
+ System.out.println(" 建议在测试环境执行,避免产生真实交易");
|
|
|
+
|
|
|
+ // 设置充足余额
|
|
|
+ System.out.println("\n📍 步骤1:设置Redis余额为100元");
|
|
|
+ redisCache.setCacheObject(companyMoneyKey, "100");
|
|
|
+
|
|
|
+ String currentBalance = redisCache.getCacheObject(companyMoneyKey);
|
|
|
+ System.out.println(" 当前余额: " + currentBalance);
|
|
|
+
|
|
|
+ // 构造请求
|
|
|
+ FsCourseSendRewardUParam param = new FsCourseSendRewardUParam();
|
|
|
+ param.setCourseId(TEST_COURSE_ID);
|
|
|
+ param.setVideoId(TEST_VIDEO_ID);
|
|
|
+ param.setUserId(TEST_USER_ID);
|
|
|
+ param.setCompanyId(TEST_COMPANY_ID);
|
|
|
+ param.setCompanyUserId(TEST_COMPANY_USER_ID);
|
|
|
+ param.setPeriodId(TEST_PERIOD_ID);
|
|
|
+ param.setQwUserId("test_qw_user");
|
|
|
+ param.setAppId("test_app_id");
|
|
|
+ param.setQwExternalId(1L);
|
|
|
+
|
|
|
+ System.out.println("\n📍 步骤2:调用发送红包接口(跳过真实发送)");
|
|
|
+ System.out.println(" 提示:如需完整测试,请配置测试环境的支付接口");
|
|
|
+
|
|
|
+ // 注:此处可根据实际情况决定是否执行
|
|
|
+ // R result = courseVideoService.sendReward(param);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 测试用例4:并发场景测试(可选)
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testConcurrentSendRedPacketWithZeroBalance() {
|
|
|
+ System.out.println("\n🧪 测试用例:并发场景下余额为0的保护");
|
|
|
+ System.out.println(repeat("-", 60));
|
|
|
+
|
|
|
+ // 设置余额为0
|
|
|
+ redisCache.setCacheObject(companyMoneyKey, "0");
|
|
|
+ System.out.println(" 初始余额: 0");
|
|
|
+
|
|
|
+ // 模拟3个并发请求
|
|
|
+ int threadCount = 3;
|
|
|
+ System.out.println("\n📍 模拟" + threadCount + "个并发请求");
|
|
|
+
|
|
|
+ for (int i = 1; i <= threadCount; i++) {
|
|
|
+ System.out.println("\n 请求" + i + ":");
|
|
|
+
|
|
|
+ FsCourseSendRewardUParam param = new FsCourseSendRewardUParam();
|
|
|
+ param.setCourseId(TEST_COURSE_ID);
|
|
|
+ param.setVideoId(TEST_VIDEO_ID);
|
|
|
+ param.setUserId(TEST_USER_ID + i); // 不同用户
|
|
|
+ param.setCompanyId(TEST_COMPANY_ID);
|
|
|
+ param.setCompanyUserId(TEST_COMPANY_USER_ID + i);
|
|
|
+ param.setPeriodId(TEST_PERIOD_ID);
|
|
|
+ param.setQwUserId("test_qw_user_" + i);
|
|
|
+ param.setAppId("test_app_id");
|
|
|
+ param.setQwExternalId(1L);
|
|
|
+
|
|
|
+ R result = courseVideoService.sendReward(param);
|
|
|
+ System.out.println(" - 返回码: " + result.get("code"));
|
|
|
+ System.out.println(" - 消息: " + result.get("msg"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证余额仍为0
|
|
|
+ String finalBalance = redisCache.getCacheObject(companyMoneyKey);
|
|
|
+ System.out.println("\n 最终余额: " + finalBalance);
|
|
|
+ assertEquals("并发请求后余额应仍为0", "0", finalBalance);
|
|
|
+
|
|
|
+ System.out.println(" ✅ 并发场景下余额保护正常");
|
|
|
+ }
|
|
|
+
|
|
|
+ @After
|
|
|
+ public void tearDown() {
|
|
|
+ // 恢复原始余额
|
|
|
+ if (originalBalance != null) {
|
|
|
+ System.out.println("\n🔄 测试清理:恢复原始余额 = " + originalBalance);
|
|
|
+ redisCache.setCacheObject(companyMoneyKey, originalBalance);
|
|
|
+ } else {
|
|
|
+ System.out.println("\n🔄 测试清理:删除测试数据");
|
|
|
+ redisCache.deleteObject(companyMoneyKey);
|
|
|
+ }
|
|
|
+
|
|
|
+ System.out.println("✅ 测试清理完成\n");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 工具方法:查看当前Redis余额
|
|
|
+ */
|
|
|
+ private void printCurrentBalance() {
|
|
|
+ String balance = redisCache.getCacheObject(companyMoneyKey);
|
|
|
+ System.out.println("💰 当前Redis余额: " + balance);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 工具方法:验证余额
|
|
|
+ */
|
|
|
+ private void assertBalance(String expected, String message) {
|
|
|
+ String actual = redisCache.getCacheObject(companyMoneyKey);
|
|
|
+ System.out.println(" 预期余额: " + expected);
|
|
|
+ System.out.println(" 实际余额: " + actual);
|
|
|
+ assertEquals(message, expected, actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 工具方法:替代String.repeat()方法 (JDK 8兼容)
|
|
|
+ */
|
|
|
+ private String repeat(String str, int count) {
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ for (int i = 0; i < count; i++) {
|
|
|
+ sb.append(str);
|
|
|
+ }
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+}
|