From 6fbc28d6f29df450343db50c49cbf1ae858d660b Mon Sep 17 00:00:00 2001 From: irving Date: Fri, 2 Jan 2026 01:57:41 -0500 Subject: [PATCH] fix: harden apitest seeder and pk schedulers --- .../common/apitest/ApiTestDataSeeder.java | 20 +++- .../clerk/mapper/PlayClerkUserInfoMapper.java | 5 + .../admin/modules/pk/PkIntegrationTest.java | 103 ++++++++++++++++++ .../starry/admin/modules/pk/WxPkApiTest.java | 14 +++ 4 files changed, 141 insertions(+), 1 deletion(-) diff --git a/play-admin/src/main/java/com/starry/admin/common/apitest/ApiTestDataSeeder.java b/play-admin/src/main/java/com/starry/admin/common/apitest/ApiTestDataSeeder.java index 283e34d..8d51ed3 100644 --- a/play-admin/src/main/java/com/starry/admin/common/apitest/ApiTestDataSeeder.java +++ b/play-admin/src/main/java/com/starry/admin/common/apitest/ApiTestDataSeeder.java @@ -1,6 +1,7 @@ package com.starry.admin.common.apitest; import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.starry.admin.modules.clerk.mapper.PlayClerkUserInfoMapper; import com.starry.admin.modules.clerk.module.entity.PlayClerkCommodityEntity; import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity; import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity; @@ -23,6 +24,7 @@ import com.starry.admin.modules.shop.service.IPlayClerkGiftInfoService; import com.starry.admin.modules.shop.service.IPlayCommodityAndLevelInfoService; import com.starry.admin.modules.shop.service.IPlayCommodityInfoService; import com.starry.admin.modules.shop.service.IPlayGiftInfoService; +import com.starry.admin.modules.system.mapper.SysUserMapper; import com.starry.admin.modules.system.module.entity.SysTenantEntity; import com.starry.admin.modules.system.module.entity.SysTenantPackageEntity; import com.starry.admin.modules.system.module.entity.SysUserEntity; @@ -77,6 +79,7 @@ public class ApiTestDataSeeder implements CommandLineRunner { private final ISysTenantPackageService tenantPackageService; private final ISysTenantService tenantService; private final SysUserService sysUserService; + private final SysUserMapper sysUserMapper; private final IPlayPersonnelGroupInfoService personnelGroupInfoService; private final IPlayClerkLevelInfoService clerkLevelInfoService; private final IPlayClerkUserInfoService clerkUserInfoService; @@ -84,6 +87,7 @@ public class ApiTestDataSeeder implements CommandLineRunner { private final IPlayCommodityAndLevelInfoService commodityAndLevelInfoService; private final IPlayGiftInfoService giftInfoService; private final IPlayClerkCommodityService clerkCommodityService; + private final PlayClerkUserInfoMapper clerkUserInfoMapper; private final IPlayClerkGiftInfoService playClerkGiftInfoService; private final IPlayCustomUserInfoService customUserInfoService; private final IPlayCustomGiftInfoService playCustomGiftInfoService; @@ -96,6 +100,7 @@ public class ApiTestDataSeeder implements CommandLineRunner { ISysTenantPackageService tenantPackageService, ISysTenantService tenantService, SysUserService sysUserService, + SysUserMapper sysUserMapper, IPlayPersonnelGroupInfoService personnelGroupInfoService, IPlayClerkLevelInfoService clerkLevelInfoService, IPlayClerkUserInfoService clerkUserInfoService, @@ -103,6 +108,7 @@ public class ApiTestDataSeeder implements CommandLineRunner { IPlayCommodityAndLevelInfoService commodityAndLevelInfoService, IPlayGiftInfoService giftInfoService, IPlayClerkCommodityService clerkCommodityService, + PlayClerkUserInfoMapper clerkUserInfoMapper, IPlayClerkGiftInfoService playClerkGiftInfoService, IPlayCustomUserInfoService customUserInfoService, IPlayCustomGiftInfoService playCustomGiftInfoService, @@ -113,6 +119,7 @@ public class ApiTestDataSeeder implements CommandLineRunner { this.tenantPackageService = tenantPackageService; this.tenantService = tenantService; this.sysUserService = sysUserService; + this.sysUserMapper = sysUserMapper; this.personnelGroupInfoService = personnelGroupInfoService; this.clerkLevelInfoService = clerkLevelInfoService; this.clerkUserInfoService = clerkUserInfoService; @@ -120,6 +127,7 @@ public class ApiTestDataSeeder implements CommandLineRunner { this.commodityAndLevelInfoService = commodityAndLevelInfoService; this.giftInfoService = giftInfoService; this.clerkCommodityService = clerkCommodityService; + this.clerkUserInfoMapper = clerkUserInfoMapper; this.playClerkGiftInfoService = playClerkGiftInfoService; this.customUserInfoService = customUserInfoService; this.playCustomGiftInfoService = playCustomGiftInfoService; @@ -200,7 +208,8 @@ public class ApiTestDataSeeder implements CommandLineRunner { } private void seedAdminUser() { - SysUserEntity existing = sysUserService.getById(DEFAULT_ADMIN_USER_ID); + SysUserEntity existing = sysUserMapper.selectUserByUserNameAndTenantId( + DEFAULT_ADMIN_USERNAME, DEFAULT_TENANT_ID); if (existing != null) { log.info("API test admin user {} already exists", DEFAULT_ADMIN_USER_ID); return; @@ -374,6 +383,15 @@ public class ApiTestDataSeeder implements CommandLineRunner { log.info("API test clerk {} already exists", DEFAULT_CLERK_ID); return; } + PlayClerkUserInfoEntity existing = clerkUserInfoMapper.selectByIdIncludingDeleted(DEFAULT_CLERK_ID); + if (existing != null) { + clerkUserInfoService.update(Wrappers.lambdaUpdate() + .eq(PlayClerkUserInfoEntity::getId, DEFAULT_CLERK_ID) + .set(PlayClerkUserInfoEntity::getDeleted, Boolean.FALSE) + .set(PlayClerkUserInfoEntity::getToken, clerkToken)); + log.info("API test clerk {} restored from deleted state", DEFAULT_CLERK_ID); + return; + } PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity(); entity.setId(DEFAULT_CLERK_ID); diff --git a/play-admin/src/main/java/com/starry/admin/modules/clerk/mapper/PlayClerkUserInfoMapper.java b/play-admin/src/main/java/com/starry/admin/modules/clerk/mapper/PlayClerkUserInfoMapper.java index ec891f8..2e8e3e1 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/clerk/mapper/PlayClerkUserInfoMapper.java +++ b/play-admin/src/main/java/com/starry/admin/modules/clerk/mapper/PlayClerkUserInfoMapper.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.InterceptorIgnore; import com.github.yulichang.base.MPJBaseMapper; import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity; import java.util.List; +import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; /** @@ -17,4 +18,8 @@ public interface PlayClerkUserInfoMapper extends MPJBaseMapper selectAllWithAlbumIgnoringTenant(); + + @InterceptorIgnore(tenantLine = "true") + @Select("SELECT id, tenant_id, deleted FROM play_clerk_user_info WHERE id = #{id} LIMIT 1") + PlayClerkUserInfoEntity selectByIdIncludingDeleted(@Param("id") String id); } diff --git a/play-admin/src/test/java/com/starry/admin/modules/pk/PkIntegrationTest.java b/play-admin/src/test/java/com/starry/admin/modules/pk/PkIntegrationTest.java index f6695f3..b54433c 100644 --- a/play-admin/src/test/java/com/starry/admin/modules/pk/PkIntegrationTest.java +++ b/play-admin/src/test/java/com/starry/admin/modules/pk/PkIntegrationTest.java @@ -144,6 +144,7 @@ class PkIntegrationTest extends AbstractApiTest { if (stringRedisTemplate.getConnectionFactory() == null) { return; } + SecurityUtils.setTenantId(DEFAULT_TENANT); stringRedisTemplate.getConnectionFactory().getConnection().flushDb(); } @@ -792,6 +793,56 @@ class PkIntegrationTest extends AbstractApiTest { assertThat(SecurityUtils.getTenantId()).isEqualTo(TENANT_ORIGIN); } + @Test + @DisplayName("Start Scheduler 应处理多个租户的PK") + void startSchedulerShouldProcessMultipleTenants() { + LocalDateTime now = LocalDateTime.now(); + String pkIdA = IdUtils.getUuid(); + String pkIdB = IdUtils.getUuid(); + String originalTenant = SecurityUtils.getTenantId(); + + PlayClerkPkEntity pkA = buildPk(pkIdA, newClerkId(), newClerkId(), + now.minusMinutes(MINUTES_BEFORE_START), + now.plusMinutes(MINUTES_AFTER_START), + ClerkPkEnum.TO_BE_STARTED.name(), + SETTLED_FALSE); + pkA.setTenantId(TENANT_A); + PlayClerkPkEntity pkB = buildPk(pkIdB, newClerkId(), newClerkId(), + now.minusMinutes(MINUTES_BEFORE_START), + now.plusMinutes(MINUTES_AFTER_START), + ClerkPkEnum.TO_BE_STARTED.name(), + SETTLED_FALSE); + pkB.setTenantId(TENANT_B); + + SecurityUtils.setTenantId(TENANT_A); + clerkPkService.save(pkA); + SecurityUtils.setTenantId(TENANT_B); + clerkPkService.save(pkB); + + long beginEpochSeconds = pkA.getPkBeginTime().toInstant().getEpochSecond(); + stringRedisTemplate.opsForZSet().add(PkRedisKeyConstants.startScheduleKey(TENANT_A), pkIdA, beginEpochSeconds); + stringRedisTemplate.opsForZSet().add(PkRedisKeyConstants.startScheduleKey(TENANT_B), pkIdB, beginEpochSeconds); + + SysTenantEntity tenantA = buildTenant(TENANT_A); + SysTenantEntity tenantB = buildTenant(TENANT_B); + try { + Mockito.doReturn(Arrays.asList(tenantA, tenantB)) + .when(sysTenantServiceSpy).listAll(); + startSchedulerJob.scanStartSchedule(); + } finally { + Mockito.reset(sysTenantServiceSpy); + SecurityUtils.setTenantId(originalTenant); + } + + SecurityUtils.setTenantId(TENANT_A); + PlayClerkPkEntity persistedA = clerkPkService.selectPlayClerkPkById(pkIdA); + assertThat(persistedA.getStatus()).isEqualTo(ClerkPkEnum.IN_PROGRESS.name()); + + SecurityUtils.setTenantId(TENANT_B); + PlayClerkPkEntity persistedB = clerkPkService.selectPlayClerkPkById(pkIdB); + assertThat(persistedB.getStatus()).isEqualTo(ClerkPkEnum.IN_PROGRESS.name()); + } + @Test @DisplayName("Start Scheduler 不应启动未到时间的PK") void startSchedulerShouldSkipFuturePk() { @@ -872,6 +923,58 @@ class PkIntegrationTest extends AbstractApiTest { assertThat(stringRedisTemplate.opsForZSet().score(finishKey, pkId)).isNull(); } + @Test + @DisplayName("Finish Scheduler 应处理多个租户的PK") + void finishSchedulerShouldProcessMultipleTenants() { + LocalDateTime now = LocalDateTime.now(); + String pkIdA = IdUtils.getUuid(); + String pkIdB = IdUtils.getUuid(); + String originalTenant = SecurityUtils.getTenantId(); + + PlayClerkPkEntity pkA = buildPk(pkIdA, newClerkId(), newClerkId(), + now.minusMinutes(MINUTES_BEFORE_START), + now.minusMinutes(MINUTES_AFTER_START), + ClerkPkEnum.IN_PROGRESS.name(), + SETTLED_FALSE); + pkA.setTenantId(TENANT_A); + PlayClerkPkEntity pkB = buildPk(pkIdB, newClerkId(), newClerkId(), + now.minusMinutes(MINUTES_BEFORE_START), + now.minusMinutes(MINUTES_AFTER_START), + ClerkPkEnum.IN_PROGRESS.name(), + SETTLED_FALSE); + pkB.setTenantId(TENANT_B); + + SecurityUtils.setTenantId(TENANT_A); + clerkPkService.save(pkA); + SecurityUtils.setTenantId(TENANT_B); + clerkPkService.save(pkB); + + long endEpochSeconds = pkA.getPkEndTime().toInstant().getEpochSecond(); + stringRedisTemplate.opsForZSet().add(PkRedisKeyConstants.finishScheduleKey(TENANT_A), pkIdA, endEpochSeconds); + stringRedisTemplate.opsForZSet().add(PkRedisKeyConstants.finishScheduleKey(TENANT_B), pkIdB, endEpochSeconds); + + SysTenantEntity tenantA = buildTenant(TENANT_A); + SysTenantEntity tenantB = buildTenant(TENANT_B); + try { + Mockito.doReturn(Arrays.asList(tenantA, tenantB)) + .when(sysTenantServiceSpy).listAll(); + finishSchedulerJob.scanFinishSchedule(); + } finally { + Mockito.reset(sysTenantServiceSpy); + SecurityUtils.setTenantId(originalTenant); + } + + SecurityUtils.setTenantId(TENANT_A); + PlayClerkPkEntity persistedA = clerkPkService.selectPlayClerkPkById(pkIdA); + assertThat(persistedA.getStatus()).isEqualTo(ClerkPkEnum.FINISHED.name()); + assertThat(persistedA.getSettled()).isEqualTo(SETTLED_TRUE); + + SecurityUtils.setTenantId(TENANT_B); + PlayClerkPkEntity persistedB = clerkPkService.selectPlayClerkPkById(pkIdB); + assertThat(persistedB.getStatus()).isEqualTo(ClerkPkEnum.FINISHED.name()); + assertThat(persistedB.getSettled()).isEqualTo(SETTLED_TRUE); + } + @Test @DisplayName("Finish Scheduler 应恢复租户上下文") void finishSchedulerShouldRestoreTenantContext() { diff --git a/play-admin/src/test/java/com/starry/admin/modules/pk/WxPkApiTest.java b/play-admin/src/test/java/com/starry/admin/modules/pk/WxPkApiTest.java index 1af1acb..19ebea8 100644 --- a/play-admin/src/test/java/com/starry/admin/modules/pk/WxPkApiTest.java +++ b/play-admin/src/test/java/com/starry/admin/modules/pk/WxPkApiTest.java @@ -67,6 +67,7 @@ class WxPkApiTest extends AbstractApiTest { private static final int HISTORY_PAGE_NUM = 1; private static final int HISTORY_PAGE_SIZE = 10; private static final long REMAINING_SECONDS_MIN = 1L; + private static final long TIME_SYNC_TOLERANCE_SECONDS = 2L; private static final int SETTLED_FALSE = 0; private static final int SETTLED_TRUE = 1; private static final double ZSET_SCORE_PAST = 1D; @@ -146,6 +147,7 @@ class WxPkApiTest extends AbstractApiTest { assertThat(data.get("remainingSeconds").asLong()).isGreaterThanOrEqualTo(REMAINING_SECONDS_MIN); assertThat(data.get("serverEpochSeconds").asLong()).isGreaterThanOrEqualTo(REMAINING_SECONDS_MIN); assertThat(data.get("pkEndEpochSeconds").asLong()).isGreaterThanOrEqualTo(REMAINING_SECONDS_MIN); + assertTimeSync(data); } @Test @@ -639,4 +641,16 @@ class WxPkApiTest extends AbstractApiTest { JsonNode root = OBJECT_MAPPER.readTree(result.getResponse().getContentAsString()); return root.get("data"); } + + private static void assertTimeSync(JsonNode data) { + assertThat(data.get("remainingSeconds")).isNotNull(); + assertThat(data.get("serverEpochSeconds")).isNotNull(); + assertThat(data.get("pkEndEpochSeconds")).isNotNull(); + long remainingSeconds = data.get("remainingSeconds").asLong(); + long serverEpochSeconds = data.get("serverEpochSeconds").asLong(); + long pkEndEpochSeconds = data.get("pkEndEpochSeconds").asLong(); + long deltaSeconds = pkEndEpochSeconds - serverEpochSeconds; + long drift = Math.abs(deltaSeconds - remainingSeconds); + assertThat(drift).isLessThanOrEqualTo(TIME_SYNC_TOLERANCE_SECONDS); + } }