feat: 实现盲盒功能模块
新增功能: - 盲盒配置管理:支持盲盒的创建、编辑、上下架 - 盲盒奖池管理:支持奖池配置、Excel导入、权重抽奖、库存管理 - 盲盒购买流程:客户购买盲盒并抽取奖励 - 奖励兑现流程:客户可将盲盒奖励兑现为实际礼物订单 - 店员提成:奖励兑现时自动增加店员礼物提成 核心实现: - BlindBoxService: 抽奖核心逻辑,支持权重算法和库存扣减 - BlindBoxDispatchService: 奖励兑现订单创建 - BlindBoxInventoryService: 奖池库存管理 - BlindBoxPoolAdminService: 奖池配置管理,支持批量导入 API接口: - /play/blind-box/config: 盲盒配置CRUD - /play/blind-box/pool: 奖池配置管理和导入 - /wx/blind-box: 客户端盲盒购买和奖励查询 数据库变更: - blind_box_config: 盲盒配置表 - blind_box_pool: 盲盒奖池表 - blind_box_reward: 盲盒奖励记录表 - play_order_info: 新增 payment_source 和 source_reward_id 字段 其他改进: - 订单模块支持盲盒支付来源,区分余额扣款和奖励抵扣 - 优惠券校验:盲盒相关订单不支持使用优惠券 - 完善单元测试覆盖
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package com.starry.admin.modules.blindbox.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.starry.admin.common.exception.CustomException;
|
||||
import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class BlindBoxInventoryServiceTest {
|
||||
|
||||
@Mock
|
||||
private BlindBoxPoolMapper blindBoxPoolMapper;
|
||||
|
||||
@InjectMocks
|
||||
private BlindBoxInventoryService blindBoxInventoryService;
|
||||
|
||||
@Test
|
||||
void shouldReserveStockWhenDraw() {
|
||||
when(blindBoxPoolMapper.consumeRewardStock(eq("tenant-1"), eq(5L), eq("gift-1"))).thenReturn(1);
|
||||
|
||||
blindBoxInventoryService.reserveRewardStock("tenant-1", 5L, "gift-1");
|
||||
|
||||
verify(blindBoxPoolMapper).consumeRewardStock("tenant-1", 5L, "gift-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowWhenStockInsufficient() {
|
||||
when(blindBoxPoolMapper.consumeRewardStock(eq("tenant-1"), eq(6L), eq("gift-1"))).thenReturn(0);
|
||||
|
||||
CustomException ex = assertThrows(CustomException.class,
|
||||
() -> blindBoxInventoryService.reserveRewardStock("tenant-1", 6L, "gift-1"));
|
||||
assertTrue(ex.getMessage().contains("库存不足"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package com.starry.admin.modules.blindbox.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.starry.admin.common.exception.CustomException;
|
||||
import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper;
|
||||
import com.starry.admin.modules.blindbox.module.dto.BlindBoxGiftOption;
|
||||
import com.starry.admin.modules.blindbox.module.dto.BlindBoxPoolImportRow;
|
||||
import com.starry.admin.modules.blindbox.module.dto.BlindBoxPoolUpsertRequest;
|
||||
import com.starry.admin.modules.blindbox.module.dto.BlindBoxPoolView;
|
||||
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
|
||||
import com.starry.admin.modules.blindbox.module.entity.BlindBoxPoolEntity;
|
||||
import com.starry.admin.modules.shop.mapper.PlayGiftInfoMapper;
|
||||
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
|
||||
import com.starry.admin.utils.SecurityUtils;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class BlindBoxPoolAdminServiceTest {
|
||||
|
||||
@Mock
|
||||
private BlindBoxPoolMapper blindBoxPoolMapper;
|
||||
|
||||
@Mock
|
||||
private PlayGiftInfoMapper playGiftInfoMapper;
|
||||
|
||||
@Mock
|
||||
private BlindBoxConfigService blindBoxConfigService;
|
||||
|
||||
@InjectMocks
|
||||
private BlindBoxPoolAdminService blindBoxPoolAdminService;
|
||||
|
||||
@BeforeEach
|
||||
void setupContext() {
|
||||
SecurityUtils.setTenantId("tenant-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldListPoolsWithGiftNames() {
|
||||
BlindBoxPoolEntity entity = new BlindBoxPoolEntity();
|
||||
entity.setId(10L);
|
||||
entity.setTenantId("tenant-1");
|
||||
entity.setBlindBoxId("blind-1");
|
||||
entity.setRewardGiftId("gift-2");
|
||||
entity.setRewardPrice(BigDecimal.valueOf(9.9));
|
||||
entity.setWeight(50);
|
||||
entity.setRemainingStock(100);
|
||||
entity.setValidFrom(LocalDateTime.of(2024, 8, 1, 0, 0));
|
||||
entity.setStatus(1);
|
||||
|
||||
when(blindBoxPoolMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Collections.singletonList(entity));
|
||||
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||
config.setId("blind-1");
|
||||
config.setTenantId("tenant-1");
|
||||
config.setName("幸运盲盒");
|
||||
when(blindBoxConfigService.requireById("blind-1")).thenReturn(config);
|
||||
PlayGiftInfoEntity reward = new PlayGiftInfoEntity();
|
||||
reward.setId("gift-2");
|
||||
reward.setName("超值娃娃");
|
||||
when(playGiftInfoMapper.selectBatchIds(any(Collection.class)))
|
||||
.thenAnswer(invocation -> Arrays.asList(reward));
|
||||
|
||||
List<BlindBoxPoolView> views = blindBoxPoolAdminService.list("blind-1");
|
||||
|
||||
assertEquals(1, views.size());
|
||||
BlindBoxPoolView view = views.get(0);
|
||||
assertEquals("幸运盲盒", view.getBlindBoxName());
|
||||
assertEquals("超值娃娃", view.getRewardGiftName());
|
||||
assertEquals(50, view.getWeight());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReplacePoolAndInsertRows() {
|
||||
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||
config.setId("blind-1");
|
||||
config.setTenantId("tenant-1");
|
||||
when(blindBoxConfigService.requireById("blind-1")).thenReturn(config);
|
||||
|
||||
PlayGiftInfoEntity reward = new PlayGiftInfoEntity();
|
||||
reward.setId("gift-2");
|
||||
reward.setName("超值娃娃");
|
||||
reward.setType("1");
|
||||
reward.setPrice(BigDecimal.valueOf(9.9));
|
||||
when(playGiftInfoMapper.selectList(any(LambdaQueryWrapper.class)))
|
||||
.thenReturn(Collections.singletonList(reward));
|
||||
|
||||
when(blindBoxPoolMapper.delete(any(LambdaQueryWrapper.class))).thenReturn(1);
|
||||
AtomicInteger insertCount = new AtomicInteger();
|
||||
when(blindBoxPoolMapper.insert(any(BlindBoxPoolEntity.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
insertCount.incrementAndGet();
|
||||
return 1;
|
||||
});
|
||||
|
||||
BlindBoxPoolImportRow row = new BlindBoxPoolImportRow();
|
||||
row.setRewardGiftName("超值娃娃");
|
||||
row.setWeight(80);
|
||||
row.setRemainingStock(10);
|
||||
row.setStatus(1);
|
||||
|
||||
blindBoxPoolAdminService.replacePool("blind-1", Collections.singletonList(row));
|
||||
|
||||
assertEquals(1, insertCount.get());
|
||||
ArgumentCaptor<BlindBoxPoolEntity> captor = ArgumentCaptor.forClass(BlindBoxPoolEntity.class);
|
||||
verify(blindBoxPoolMapper).insert(captor.capture());
|
||||
BlindBoxPoolEntity saved = captor.getValue();
|
||||
assertEquals("gift-2", saved.getRewardGiftId());
|
||||
assertEquals(Integer.valueOf(80), saved.getWeight());
|
||||
assertEquals(BigDecimal.valueOf(9.9), saved.getRewardPrice());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowWhenRewardGiftMissing() {
|
||||
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||
config.setId("blind-1");
|
||||
config.setTenantId("tenant-1");
|
||||
when(blindBoxConfigService.requireById("blind-1")).thenReturn(config);
|
||||
when(playGiftInfoMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Collections.emptyList());
|
||||
|
||||
BlindBoxPoolImportRow row = new BlindBoxPoolImportRow();
|
||||
row.setRewardGiftName("未知礼物");
|
||||
row.setWeight(10);
|
||||
|
||||
assertThrows(CustomException.class,
|
||||
() -> blindBoxPoolAdminService.replacePool("blind-1", Collections.singletonList(row)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowForInvalidWeight() {
|
||||
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||
config.setId("blind-1");
|
||||
config.setTenantId("tenant-1");
|
||||
when(blindBoxConfigService.requireById("blind-1")).thenReturn(config);
|
||||
PlayGiftInfoEntity reward = new PlayGiftInfoEntity();
|
||||
reward.setId("gift-2");
|
||||
reward.setName("超值娃娃");
|
||||
reward.setType("1");
|
||||
reward.setPrice(BigDecimal.ONE);
|
||||
when(playGiftInfoMapper.selectList(any(LambdaQueryWrapper.class)))
|
||||
.thenReturn(Collections.singletonList(reward));
|
||||
|
||||
BlindBoxPoolImportRow row = new BlindBoxPoolImportRow();
|
||||
row.setRewardGiftName("超值娃娃");
|
||||
row.setWeight(0);
|
||||
|
||||
assertThrows(CustomException.class,
|
||||
() -> blindBoxPoolAdminService.replacePool("blind-1", Collections.singletonList(row)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldListGiftOptions() {
|
||||
PlayGiftInfoEntity gift = new PlayGiftInfoEntity();
|
||||
gift.setId("gift-1");
|
||||
gift.setTenantId("tenant-1");
|
||||
gift.setHistory("0");
|
||||
gift.setState("0");
|
||||
gift.setType("1");
|
||||
gift.setPrice(BigDecimal.valueOf(19.9));
|
||||
gift.setName("超值娃娃");
|
||||
gift.setUrl("https://image");
|
||||
when(playGiftInfoMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Collections.singletonList(gift));
|
||||
|
||||
List<BlindBoxGiftOption> options = blindBoxPoolAdminService.listGiftOptions("娃");
|
||||
|
||||
assertEquals(1, options.size());
|
||||
assertEquals("gift-1", options.get(0).getId());
|
||||
assertEquals("超值娃娃", options.get(0).getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreatePoolEntry() {
|
||||
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||
config.setId("blind-1");
|
||||
config.setTenantId("tenant-1");
|
||||
config.setName("幸运盲盒");
|
||||
when(blindBoxConfigService.requireById("blind-1")).thenReturn(config);
|
||||
|
||||
PlayGiftInfoEntity gift = new PlayGiftInfoEntity();
|
||||
gift.setId("gift-2");
|
||||
gift.setTenantId("tenant-1");
|
||||
gift.setHistory("0");
|
||||
gift.setState("0");
|
||||
gift.setType("1");
|
||||
gift.setPrice(BigDecimal.valueOf(9.9));
|
||||
gift.setName("超值娃娃");
|
||||
when(playGiftInfoMapper.selectById("gift-2")).thenReturn(gift);
|
||||
|
||||
when(blindBoxPoolMapper.insert(any(BlindBoxPoolEntity.class))).thenAnswer(invocation -> {
|
||||
BlindBoxPoolEntity entity = invocation.getArgument(0);
|
||||
entity.setId(100L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
BlindBoxPoolUpsertRequest request = new BlindBoxPoolUpsertRequest();
|
||||
request.setBlindBoxId("blind-1");
|
||||
request.setRewardGiftId("gift-2");
|
||||
request.setRewardPrice(BigDecimal.valueOf(12.5));
|
||||
request.setWeight(80);
|
||||
request.setRemainingStock(5);
|
||||
request.setStatus(1);
|
||||
request.setValidFrom(LocalDateTime.of(2024, 8, 1, 0, 0));
|
||||
request.setValidTo(LocalDateTime.of(2024, 8, 31, 23, 59, 59));
|
||||
|
||||
BlindBoxPoolView view = blindBoxPoolAdminService.create("blind-1", request);
|
||||
|
||||
assertEquals("gift-2", view.getRewardGiftId());
|
||||
assertEquals(Integer.valueOf(80), view.getWeight());
|
||||
verify(blindBoxPoolMapper).insert(any(BlindBoxPoolEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUpdatePoolEntry() {
|
||||
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||
config.setId("blind-1");
|
||||
config.setTenantId("tenant-1");
|
||||
config.setName("幸运盲盒");
|
||||
when(blindBoxConfigService.requireById("blind-1")).thenReturn(config);
|
||||
|
||||
PlayGiftInfoEntity gift = new PlayGiftInfoEntity();
|
||||
gift.setId("gift-3");
|
||||
gift.setTenantId("tenant-1");
|
||||
gift.setHistory("0");
|
||||
gift.setState("0");
|
||||
gift.setType("1");
|
||||
gift.setPrice(BigDecimal.valueOf(19.9));
|
||||
gift.setName("超级公仔");
|
||||
when(playGiftInfoMapper.selectById("gift-3")).thenReturn(gift);
|
||||
|
||||
BlindBoxPoolEntity existing = new BlindBoxPoolEntity();
|
||||
existing.setId(200L);
|
||||
existing.setTenantId("tenant-1");
|
||||
existing.setBlindBoxId("blind-1");
|
||||
existing.setRewardGiftId("gift-1");
|
||||
existing.setStatus(1);
|
||||
when(blindBoxPoolMapper.selectById(200L)).thenReturn(existing);
|
||||
when(blindBoxPoolMapper.updateById(any(BlindBoxPoolEntity.class))).thenReturn(1);
|
||||
|
||||
BlindBoxPoolUpsertRequest request = new BlindBoxPoolUpsertRequest();
|
||||
request.setBlindBoxId("blind-1");
|
||||
request.setRewardGiftId("gift-3");
|
||||
request.setWeight(60);
|
||||
request.setRemainingStock(null);
|
||||
request.setStatus(0);
|
||||
request.setRewardPrice(null);
|
||||
request.setValidFrom(LocalDateTime.of(2024, 9, 1, 0, 0));
|
||||
request.setValidTo(LocalDateTime.of(2024, 9, 30, 23, 59, 59));
|
||||
|
||||
BlindBoxPoolView view = blindBoxPoolAdminService.update(200L, request);
|
||||
|
||||
assertEquals("gift-3", existing.getRewardGiftId());
|
||||
assertEquals(Integer.valueOf(60), existing.getWeight());
|
||||
assertEquals(Integer.valueOf(0), existing.getStatus());
|
||||
assertEquals("超级公仔", view.getRewardGiftName());
|
||||
verify(blindBoxPoolMapper).updateById(existing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package com.starry.admin.modules.blindbox.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.starry.admin.common.exception.CustomException;
|
||||
import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper;
|
||||
import com.starry.admin.modules.blindbox.mapper.BlindBoxRewardMapper;
|
||||
import com.starry.admin.modules.blindbox.module.constant.BlindBoxRewardStatus;
|
||||
import com.starry.admin.modules.blindbox.module.dto.BlindBoxCandidate;
|
||||
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
|
||||
import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity;
|
||||
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class BlindBoxServiceTest {
|
||||
|
||||
@Mock
|
||||
private BlindBoxPoolMapper poolMapper;
|
||||
|
||||
@Mock
|
||||
private BlindBoxRewardMapper rewardMapper;
|
||||
|
||||
@Mock
|
||||
private BlindBoxInventoryService inventoryService;
|
||||
|
||||
@Mock
|
||||
private BlindBoxDispatchService dispatchService;
|
||||
|
||||
@Mock
|
||||
private BlindBoxConfigService configService;
|
||||
|
||||
private Clock clock;
|
||||
private TestRandomAdapter randomAdapter;
|
||||
private BlindBoxService blindBoxService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
clock = Clock.fixed(Instant.parse("2024-08-01T10:15:30Z"), ZoneOffset.UTC);
|
||||
randomAdapter = new TestRandomAdapter();
|
||||
blindBoxService = new BlindBoxService(poolMapper, rewardMapper, inventoryService, dispatchService, configService,
|
||||
clock, Duration.ofDays(7), randomAdapter);
|
||||
|
||||
lenient().when(rewardMapper.insert(any())).thenAnswer(invocation -> {
|
||||
BlindBoxRewardEntity entity = invocation.getArgument(0);
|
||||
if (entity.getId() == null) {
|
||||
entity.setId(UUID.randomUUID().toString());
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateRewardRecordWhenOrderCompleted() {
|
||||
randomAdapter.nextDoubleToReturn = 0.95;
|
||||
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||
config.setId("blind-1");
|
||||
config.setTenantId("tenant-1");
|
||||
config.setPrice(BigDecimal.valueOf(49));
|
||||
when(configService.requireById("blind-1")).thenReturn(config);
|
||||
List<BlindBoxCandidate> candidates = Arrays.asList(
|
||||
BlindBoxCandidate.of(1L, "tenant-1", "blind-1", "gift-low", BigDecimal.valueOf(10), 10, 5),
|
||||
BlindBoxCandidate.of(2L, "tenant-1", "blind-1", "gift-high", BigDecimal.valueOf(99), 90, 3)
|
||||
);
|
||||
when(poolMapper.listActiveEntries(eq("tenant-1"), eq("blind-1"), any())).thenReturn(candidates);
|
||||
|
||||
BlindBoxRewardEntity entity = blindBoxService.drawReward("tenant-1", "order-1", "customer-1", "blind-1",
|
||||
"seed-123");
|
||||
|
||||
ArgumentCaptor<BlindBoxRewardEntity> captor = ArgumentCaptor.forClass(BlindBoxRewardEntity.class);
|
||||
verify(rewardMapper).insert(captor.capture());
|
||||
BlindBoxRewardEntity persisted = captor.getValue();
|
||||
|
||||
assertEquals("gift-high", persisted.getRewardGiftId());
|
||||
assertEquals(BigDecimal.valueOf(99).setScale(2), persisted.getRewardPrice());
|
||||
assertEquals(BigDecimal.valueOf(49).setScale(2), persisted.getBoxPrice());
|
||||
assertEquals(BlindBoxRewardStatus.UNUSED.getCode(), persisted.getStatus());
|
||||
assertEquals(LocalDateTime.ofInstant(clock.instant(), clock.getZone()).plusDays(7), persisted.getExpiresAt());
|
||||
verify(inventoryService).reserveRewardStock("tenant-1", 2L, "gift-high");
|
||||
assertEquals(entity.getRewardGiftId(), persisted.getRewardGiftId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleUnlimitedStock() {
|
||||
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||
config.setId("blind-1");
|
||||
config.setTenantId("tenant-1");
|
||||
config.setPrice(BigDecimal.valueOf(29));
|
||||
when(configService.requireById("blind-1")).thenReturn(config);
|
||||
List<BlindBoxCandidate> candidates = Collections.singletonList(
|
||||
BlindBoxCandidate.of(7L, "tenant-1", "blind-1", "gift-unlimited", BigDecimal.valueOf(59), 100, null)
|
||||
);
|
||||
when(poolMapper.listActiveEntries(eq("tenant-1"), eq("blind-1"), any())).thenReturn(candidates);
|
||||
|
||||
blindBoxService.drawReward("tenant-1", "order-2", "customer-9", "blind-1", "seed-unlimited");
|
||||
|
||||
ArgumentCaptor<BlindBoxRewardEntity> captor = ArgumentCaptor.forClass(BlindBoxRewardEntity.class);
|
||||
verify(rewardMapper).insert(captor.capture());
|
||||
BlindBoxRewardEntity persisted = captor.getValue();
|
||||
assertEquals("gift-unlimited", persisted.getRewardGiftId());
|
||||
assertEquals(BigDecimal.valueOf(29).setScale(2), persisted.getBoxPrice());
|
||||
assertEquals(BigDecimal.valueOf(59).setScale(2), persisted.getRewardPrice());
|
||||
assertEquals(BlindBoxRewardStatus.UNUSED.getCode(), persisted.getStatus());
|
||||
assertNull(persisted.getRewardStockSnapshot());
|
||||
verify(inventoryService).reserveRewardStock("tenant-1", 7L, "gift-unlimited");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreventDoubleDispatch() {
|
||||
BlindBoxRewardEntity reward = buildRewardEntity();
|
||||
when(rewardMapper.lockByIdForUpdate("reward-1")).thenReturn(reward);
|
||||
when(dispatchService.dispatchRewardOrder(eq(reward), eq("clerk-1"))).thenReturn(mock(OrderPlacementResult.class));
|
||||
when(rewardMapper.markUsed(eq("reward-1"), eq("clerk-1"), any(), any())).thenReturn(1);
|
||||
|
||||
blindBoxService.dispatchReward("reward-1", "clerk-1");
|
||||
|
||||
reward.setStatus(BlindBoxRewardStatus.USED.getCode());
|
||||
CustomException ex = assertThrows(CustomException.class, () ->
|
||||
blindBoxService.dispatchReward("reward-1", "clerk-1"));
|
||||
assertTrue(ex.getMessage().contains("已使用"));
|
||||
|
||||
verify(rewardMapper, times(1)).markUsed(eq("reward-1"), eq("clerk-1"), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectExpiredReward() {
|
||||
BlindBoxRewardEntity reward = buildRewardEntity();
|
||||
reward.setExpiresAt(LocalDateTime.ofInstant(clock.instant(), clock.getZone()).minusHours(1));
|
||||
when(rewardMapper.lockByIdForUpdate("reward-1")).thenReturn(reward);
|
||||
|
||||
CustomException ex = assertThrows(CustomException.class, () ->
|
||||
blindBoxService.dispatchReward("reward-1", "clerk-1"));
|
||||
assertTrue(ex.getMessage().contains("已过期"));
|
||||
verify(dispatchService, times(0)).dispatchRewardOrder(any(), any());
|
||||
}
|
||||
|
||||
private BlindBoxRewardEntity buildRewardEntity() {
|
||||
BlindBoxRewardEntity reward = new BlindBoxRewardEntity();
|
||||
reward.setId("reward-1");
|
||||
reward.setTenantId("tenant-1");
|
||||
reward.setCustomerId("customer-1");
|
||||
reward.setBlindBoxId("blind-1");
|
||||
reward.setRewardGiftId("gift-high");
|
||||
reward.setRewardPrice(BigDecimal.valueOf(99));
|
||||
reward.setBoxPrice(BigDecimal.valueOf(49));
|
||||
reward.setRewardStockSnapshot(3);
|
||||
reward.setSeed("seed-123");
|
||||
reward.setStatus(BlindBoxRewardStatus.UNUSED.getCode());
|
||||
reward.setCreatedByOrder("order-1");
|
||||
reward.setCreatedTime(java.sql.Timestamp.from(clock.instant()));
|
||||
reward.setUpdatedTime(java.sql.Timestamp.from(clock.instant()));
|
||||
reward.setExpiresAt(LocalDateTime.ofInstant(clock.instant(), clock.getZone()).plusHours(1));
|
||||
reward.setVersion(0);
|
||||
return reward;
|
||||
}
|
||||
|
||||
private static class TestRandomAdapter implements BlindBoxService.RandomAdapter {
|
||||
|
||||
double nextDoubleToReturn = 0.5;
|
||||
|
||||
@Override
|
||||
public double nextDouble() {
|
||||
return nextDoubleToReturn;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user