fix: allow editing blind box pools referencing inactive gifts
Some checks failed
Build and Push Backend / docker (push) Failing after 7s
Some checks failed
Build and Push Backend / docker (push) Failing after 7s
This commit is contained in:
@@ -162,7 +162,7 @@ public class BlindBoxPoolAdminService {
|
|||||||
if (!tenantId.equals(config.getTenantId())) {
|
if (!tenantId.equals(config.getTenantId())) {
|
||||||
throw new CustomException("盲盒不存在或已被移除");
|
throw new CustomException("盲盒不存在或已被移除");
|
||||||
}
|
}
|
||||||
PlayGiftInfoEntity rewardGift = requireRewardGift(tenantId, request.getRewardGiftId());
|
PlayGiftInfoEntity rewardGift = requireRewardGiftForUpdate(tenantId, request.getRewardGiftId());
|
||||||
validateTimeRange(request.getValidFrom(), request.getValidTo());
|
validateTimeRange(request.getValidFrom(), request.getValidTo());
|
||||||
Integer weight = requirePositiveWeight(request.getWeight(), rewardGift.getName());
|
Integer weight = requirePositiveWeight(request.getWeight(), rewardGift.getName());
|
||||||
Integer remainingStock = normalizeRemainingStock(request.getRemainingStock(), rewardGift.getName());
|
Integer remainingStock = normalizeRemainingStock(request.getRemainingStock(), rewardGift.getName());
|
||||||
@@ -326,18 +326,30 @@ public class BlindBoxPoolAdminService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private PlayGiftInfoEntity requireRewardGift(String tenantId, String rewardGiftId) {
|
private PlayGiftInfoEntity requireRewardGift(String tenantId, String rewardGiftId) {
|
||||||
|
return requireRewardGift(tenantId, rewardGiftId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayGiftInfoEntity requireRewardGiftForUpdate(String tenantId, String rewardGiftId) {
|
||||||
|
return requireRewardGift(tenantId, rewardGiftId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayGiftInfoEntity requireRewardGift(String tenantId, String rewardGiftId, boolean strictAvailability) {
|
||||||
if (StrUtil.isBlank(rewardGiftId)) {
|
if (StrUtil.isBlank(rewardGiftId)) {
|
||||||
throw new CustomException("请选择中奖礼物");
|
throw new CustomException("请选择中奖礼物");
|
||||||
}
|
}
|
||||||
PlayGiftInfoEntity gift = playGiftInfoMapper.selectById(rewardGiftId);
|
PlayGiftInfoEntity gift = playGiftInfoMapper.selectById(rewardGiftId);
|
||||||
if (gift == null
|
if (gift == null
|
||||||
|| !tenantId.equals(gift.getTenantId())
|
|| !tenantId.equals(gift.getTenantId())
|
||||||
|| !GiftHistory.CURRENT.getCode().equals(gift.getHistory())
|
|
||||||
|| !GiftState.ACTIVE.getCode().equals(gift.getState())
|
|
||||||
|| !GiftType.NORMAL.getCode().equals(gift.getType())
|
|| !GiftType.NORMAL.getCode().equals(gift.getType())
|
||||||
|| Boolean.TRUE.equals(gift.getDeleted())) {
|
|| Boolean.TRUE.equals(gift.getDeleted())) {
|
||||||
throw new CustomException("中奖礼物不存在或已下架");
|
throw new CustomException("中奖礼物不存在或已下架");
|
||||||
}
|
}
|
||||||
|
if (strictAvailability) {
|
||||||
|
if (!GiftHistory.CURRENT.getCode().equals(gift.getHistory())
|
||||||
|
|| !GiftState.ACTIVE.getCode().equals(gift.getState())) {
|
||||||
|
throw new CustomException("中奖礼物不存在或已下架");
|
||||||
|
}
|
||||||
|
}
|
||||||
return gift;
|
return gift;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.hasItem;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper;
|
||||||
|
import com.starry.admin.modules.blindbox.module.constant.BlindBoxConfigStatus;
|
||||||
|
import com.starry.admin.modules.blindbox.module.constant.BlindBoxPoolStatus;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxPoolEntity;
|
||||||
|
import com.starry.admin.modules.blindbox.service.BlindBoxConfigService;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.GiftHistory;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.GiftState;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.GiftType;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayGiftInfoService;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
|
||||||
|
class BlindBoxPoolControllerApiTest extends AbstractApiTest {
|
||||||
|
|
||||||
|
private static final String TEST_BLIND_BOX_ID = "blindbox-admin-api";
|
||||||
|
private static final DateTimeFormatter DATE_TIME_FORMATTER =
|
||||||
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BlindBoxConfigService blindBoxConfigService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayGiftInfoService giftInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BlindBoxPoolMapper blindBoxPoolMapper;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
private final List<Long> poolIdsToCleanup = new ArrayList<>();
|
||||||
|
private final List<String> giftIdsToCleanup = new ArrayList<>();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
ensureTenantContext();
|
||||||
|
ensureBlindBoxConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
ensureTenantContext();
|
||||||
|
if (!poolIdsToCleanup.isEmpty()) {
|
||||||
|
blindBoxPoolMapper.deleteBatchIds(poolIdsToCleanup);
|
||||||
|
poolIdsToCleanup.clear();
|
||||||
|
}
|
||||||
|
if (!giftIdsToCleanup.isEmpty()) {
|
||||||
|
giftInfoService.removeByIds(giftIdsToCleanup);
|
||||||
|
giftIdsToCleanup.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:校验奖池管理 API 的新增、更新(禁用/修改权重时间库存)、删除以及礼物选项查询功能,
|
||||||
|
// 确保后台盲盒奖池的所有按钮都能正常调用并持久化。
|
||||||
|
void adminCanCreateUpdateToggleAndDeletePoolEntries() throws Exception {
|
||||||
|
PlayGiftInfoEntity freshlyAddedGift = seedGift("API盲盒新礼物");
|
||||||
|
giftIdsToCleanup.add(freshlyAddedGift.getId());
|
||||||
|
|
||||||
|
mockMvc.perform(get("/play/blind-box/pool/gifts")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.param("keyword", "盲盒新礼物"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data[*].id", hasItem(freshlyAddedGift.getId())));
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now().withNano(0);
|
||||||
|
long configurableEntryId = createPoolEntryViaApi(
|
||||||
|
ApiTestDataSeeder.DEFAULT_GIFT_ID,
|
||||||
|
new BigDecimal("25.88"),
|
||||||
|
40,
|
||||||
|
8,
|
||||||
|
BlindBoxPoolStatus.ENABLED.getCode(),
|
||||||
|
now.minusDays(1),
|
||||||
|
now.plusDays(5));
|
||||||
|
|
||||||
|
LocalDateTime updatedFrom = now.minusHours(2);
|
||||||
|
LocalDateTime updatedTo = now.plusDays(10);
|
||||||
|
ObjectNode updatePayload = objectMapper.createObjectNode();
|
||||||
|
updatePayload.put("blindBoxId", TEST_BLIND_BOX_ID);
|
||||||
|
updatePayload.put("rewardGiftId", ApiTestDataSeeder.DEFAULT_GIFT_ID);
|
||||||
|
updatePayload.put("rewardPrice", "35.66");
|
||||||
|
updatePayload.put("weight", 75);
|
||||||
|
updatePayload.put("remainingStock", 3);
|
||||||
|
updatePayload.put("status", BlindBoxPoolStatus.DISABLED.getCode());
|
||||||
|
updatePayload.put("validFrom", updatedFrom.format(DATE_TIME_FORMATTER));
|
||||||
|
updatePayload.put("validTo", updatedTo.format(DATE_TIME_FORMATTER));
|
||||||
|
|
||||||
|
mockMvc.perform(put("/play/blind-box/pool/" + configurableEntryId)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(updatePayload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.status").value(BlindBoxPoolStatus.DISABLED.getCode()))
|
||||||
|
.andExpect(jsonPath("$.data.weight").value(75))
|
||||||
|
.andExpect(jsonPath("$.data.remainingStock").value(3));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
BlindBoxPoolEntity updated = blindBoxPoolMapper.selectById(configurableEntryId);
|
||||||
|
assertThat(updated).isNotNull();
|
||||||
|
assertThat(updated.getStatus()).isEqualTo(BlindBoxPoolStatus.DISABLED.getCode());
|
||||||
|
assertThat(updated.getWeight()).isEqualTo(75);
|
||||||
|
assertThat(updated.getRemainingStock()).isEqualTo(3);
|
||||||
|
assertThat(updated.getValidFrom()).isEqualTo(updatedFrom);
|
||||||
|
assertThat(updated.getValidTo()).isEqualTo(updatedTo);
|
||||||
|
|
||||||
|
long reusableEntryId = createPoolEntryViaApi(
|
||||||
|
freshlyAddedGift.getId(),
|
||||||
|
new BigDecimal("18.50"),
|
||||||
|
10,
|
||||||
|
1,
|
||||||
|
BlindBoxPoolStatus.ENABLED.getCode(),
|
||||||
|
now.minusDays(2),
|
||||||
|
now.plusDays(3));
|
||||||
|
|
||||||
|
updateGiftState(freshlyAddedGift.getId(), GiftState.OFF_SHELF);
|
||||||
|
|
||||||
|
ObjectNode inactiveUpdate = objectMapper.createObjectNode();
|
||||||
|
inactiveUpdate.put("blindBoxId", TEST_BLIND_BOX_ID);
|
||||||
|
inactiveUpdate.put("rewardGiftId", freshlyAddedGift.getId());
|
||||||
|
inactiveUpdate.put("weight", 15);
|
||||||
|
inactiveUpdate.put("status", BlindBoxPoolStatus.ENABLED.getCode());
|
||||||
|
inactiveUpdate.putNull("remainingStock");
|
||||||
|
|
||||||
|
mockMvc.perform(put("/play/blind-box/pool/" + reusableEntryId)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(inactiveUpdate.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.weight").value(15));
|
||||||
|
|
||||||
|
mockMvc.perform(delete("/play/blind-box/pool/" + reusableEntryId)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
BlindBoxPoolEntity deleted = blindBoxPoolMapper.selectById(reusableEntryId);
|
||||||
|
assertThat(deleted).isNull();
|
||||||
|
poolIdsToCleanup.remove(reusableEntryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long createPoolEntryViaApi(
|
||||||
|
String giftId,
|
||||||
|
BigDecimal rewardPrice,
|
||||||
|
int weight,
|
||||||
|
Integer remainingStock,
|
||||||
|
int status,
|
||||||
|
LocalDateTime validFrom,
|
||||||
|
LocalDateTime validTo) throws Exception {
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("blindBoxId", TEST_BLIND_BOX_ID);
|
||||||
|
payload.put("rewardGiftId", giftId);
|
||||||
|
payload.put("rewardPrice", rewardPrice.setScale(2, RoundingMode.HALF_UP).toPlainString());
|
||||||
|
payload.put("weight", weight);
|
||||||
|
if (remainingStock != null) {
|
||||||
|
payload.put("remainingStock", remainingStock);
|
||||||
|
} else {
|
||||||
|
payload.putNull("remainingStock");
|
||||||
|
}
|
||||||
|
payload.put("status", status);
|
||||||
|
payload.put("validFrom", validFrom.format(DATE_TIME_FORMATTER));
|
||||||
|
payload.put("validTo", validTo.format(DATE_TIME_FORMATTER));
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(post("/play/blind-box/pool")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.id").isNumber())
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode response = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||||
|
long id = response.path("data").path("id").asLong();
|
||||||
|
poolIdsToCleanup.add(id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureBlindBoxConfig() {
|
||||||
|
BlindBoxConfigEntity existing = blindBoxConfigService.getById(TEST_BLIND_BOX_ID);
|
||||||
|
if (existing != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BlindBoxConfigEntity entity = new BlindBoxConfigEntity();
|
||||||
|
entity.setId(TEST_BLIND_BOX_ID);
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setName("API盲盒(Admin)");
|
||||||
|
entity.setPrice(new BigDecimal("19.90"));
|
||||||
|
entity.setStatus(BlindBoxConfigStatus.ENABLED.getCode());
|
||||||
|
blindBoxConfigService.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayGiftInfoEntity seedGift(String name) {
|
||||||
|
PlayGiftInfoEntity gift = new PlayGiftInfoEntity();
|
||||||
|
gift.setId("gift-admin-" + IdUtils.getUuid().substring(0, 12));
|
||||||
|
gift.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
gift.setHistory(GiftHistory.CURRENT.getCode());
|
||||||
|
gift.setName(name);
|
||||||
|
gift.setType(GiftType.NORMAL.getCode());
|
||||||
|
gift.setUrl("https://example.com/assets/" + gift.getId() + ".png");
|
||||||
|
gift.setPrice(new BigDecimal("58.80"));
|
||||||
|
gift.setUnit("CNY");
|
||||||
|
gift.setState(GiftState.ACTIVE.getCode());
|
||||||
|
gift.setListingTime(LocalDateTime.now().minusDays(1));
|
||||||
|
gift.setRemark("Seeded for blind box pool admin test");
|
||||||
|
giftInfoService.save(gift);
|
||||||
|
return gift;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateGiftState(String giftId, GiftState state) {
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayGiftInfoEntity gift = giftInfoService.getById(giftId);
|
||||||
|
if (gift != null) {
|
||||||
|
gift.setState(state.getCode());
|
||||||
|
giftInfoService.updateById(gift);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ensureTenantContext() {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper;
|
|||||||
import com.starry.admin.modules.blindbox.mapper.BlindBoxRewardMapper;
|
import com.starry.admin.modules.blindbox.mapper.BlindBoxRewardMapper;
|
||||||
import com.starry.admin.modules.blindbox.module.constant.BlindBoxConfigStatus;
|
import com.starry.admin.modules.blindbox.module.constant.BlindBoxConfigStatus;
|
||||||
import com.starry.admin.modules.blindbox.module.constant.BlindBoxPoolStatus;
|
import com.starry.admin.modules.blindbox.module.constant.BlindBoxPoolStatus;
|
||||||
|
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.BlindBoxConfigEntity;
|
||||||
import com.starry.admin.modules.blindbox.module.entity.BlindBoxPoolEntity;
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxPoolEntity;
|
||||||
import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity;
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity;
|
||||||
@@ -34,6 +35,11 @@ class BlindBoxServiceWeightTest extends WxCustomOrderApiTestSupport {
|
|||||||
private static final String TEST_BLIND_BOX_ID = "blindbox-apitest";
|
private static final String TEST_BLIND_BOX_ID = "blindbox-apitest";
|
||||||
private static final String PRIMARY_GIFT_ID = ApiTestDataSeeder.DEFAULT_GIFT_ID;
|
private static final String PRIMARY_GIFT_ID = ApiTestDataSeeder.DEFAULT_GIFT_ID;
|
||||||
private static final String SECONDARY_GIFT_ID = "gift-blindbox-secondary";
|
private static final String SECONDARY_GIFT_ID = "gift-blindbox-secondary";
|
||||||
|
private static final String MIXED_GIFT_A_ID = "gift-blindbox-mixed-a";
|
||||||
|
private static final String MIXED_GIFT_B_ID = "gift-blindbox-mixed-b";
|
||||||
|
private static final String MIXED_GIFT_C_ID = "gift-blindbox-mixed-c";
|
||||||
|
private static final String MIXED_GIFT_D_ID = "gift-blindbox-mixed-d";
|
||||||
|
private static final String MIXED_GIFT_E_ID = "gift-blindbox-mixed-e";
|
||||||
private static final int DRAW_ATTEMPT_COUNT = 1_000;
|
private static final int DRAW_ATTEMPT_COUNT = 1_000;
|
||||||
private static final int PRIMARY_WEIGHT = 80;
|
private static final int PRIMARY_WEIGHT = 80;
|
||||||
private static final int SECONDARY_WEIGHT = 20;
|
private static final int SECONDARY_WEIGHT = 20;
|
||||||
@@ -41,6 +47,11 @@ class BlindBoxServiceWeightTest extends WxCustomOrderApiTestSupport {
|
|||||||
private static final double PRIMARY_RATIO_MAX = 0.88;
|
private static final double PRIMARY_RATIO_MAX = 0.88;
|
||||||
private static final double SECONDARY_RATIO_MIN = 0.12;
|
private static final double SECONDARY_RATIO_MIN = 0.12;
|
||||||
private static final double SECONDARY_RATIO_MAX = 0.32;
|
private static final double SECONDARY_RATIO_MAX = 0.32;
|
||||||
|
private static final int MIXED_TOTAL_DRAWS = 400;
|
||||||
|
private static final int MIXED_GIFT_A_STOCK = 10;
|
||||||
|
private static final int MIXED_GIFT_B_STOCK = 5;
|
||||||
|
private static final int MIXED_GIFT_C_STOCK = 0;
|
||||||
|
private static final int MIXED_GIFT_E_STOCK = 2;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private BlindBoxService blindBoxService;
|
private BlindBoxService blindBoxService;
|
||||||
@@ -112,6 +123,66 @@ class BlindBoxServiceWeightTest extends WxCustomOrderApiTestSupport {
|
|||||||
purgePool();
|
purgePool();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:混合 5 种礼物(部分限量、部分不限量),验证限量礼物被抽满后不再出现,
|
||||||
|
// 不限量礼物可继续抽取,且库存为 0 的礼物永远不会返回。
|
||||||
|
void blindBoxDrawHandlesMixedInventory() {
|
||||||
|
ensureTenantContext();
|
||||||
|
ensureBlindBoxConfig();
|
||||||
|
ensureMixedGifts();
|
||||||
|
resetCustomerBalance();
|
||||||
|
|
||||||
|
purgeRewards();
|
||||||
|
purgePool();
|
||||||
|
insertPoolEntry(MIXED_GIFT_A_ID, 40, MIXED_GIFT_A_STOCK);
|
||||||
|
insertPoolEntry(MIXED_GIFT_B_ID, 25, MIXED_GIFT_B_STOCK);
|
||||||
|
insertPoolEntry(MIXED_GIFT_C_ID, 15, MIXED_GIFT_C_STOCK);
|
||||||
|
insertPoolEntry(MIXED_GIFT_D_ID, 10, null);
|
||||||
|
insertPoolEntry(MIXED_GIFT_E_ID, 10, MIXED_GIFT_E_STOCK);
|
||||||
|
|
||||||
|
Map<String, Integer> frequency = new HashMap<>();
|
||||||
|
for (int i = 0; i < MIXED_TOTAL_DRAWS; i++) {
|
||||||
|
BlindBoxRewardEntity reward = blindBoxService.drawReward(
|
||||||
|
ApiTestDataSeeder.DEFAULT_TENANT_ID,
|
||||||
|
"mixed-order-" + i,
|
||||||
|
ApiTestDataSeeder.DEFAULT_CUSTOMER_ID,
|
||||||
|
TEST_BLIND_BOX_ID,
|
||||||
|
"mixed-seed-" + i);
|
||||||
|
frequency.merge(reward.getRewardGiftId(), 1, Integer::sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assertions.assertThat(frequency.getOrDefault(MIXED_GIFT_C_ID, 0))
|
||||||
|
.as("库存为 0 的礼物永远不应被抽中")
|
||||||
|
.isEqualTo(0);
|
||||||
|
Assertions.assertThat(frequency.getOrDefault(MIXED_GIFT_A_ID, 0))
|
||||||
|
.as("限量礼物 A 应被精确抽完")
|
||||||
|
.isEqualTo(MIXED_GIFT_A_STOCK);
|
||||||
|
Assertions.assertThat(frequency.getOrDefault(MIXED_GIFT_B_ID, 0))
|
||||||
|
.as("限量礼物 B 应被精确抽完")
|
||||||
|
.isEqualTo(MIXED_GIFT_B_STOCK);
|
||||||
|
Assertions.assertThat(frequency.getOrDefault(MIXED_GIFT_E_ID, 0))
|
||||||
|
.as("限量礼物 E 应被精确抽完")
|
||||||
|
.isEqualTo(MIXED_GIFT_E_STOCK);
|
||||||
|
|
||||||
|
int unlimitedCount = frequency.getOrDefault(MIXED_GIFT_D_ID, 0);
|
||||||
|
int finiteTotal = MIXED_GIFT_A_STOCK + MIXED_GIFT_B_STOCK + MIXED_GIFT_E_STOCK;
|
||||||
|
Assertions.assertThat(unlimitedCount)
|
||||||
|
.as("不限量礼物承担剩余抽奖次数")
|
||||||
|
.isEqualTo(MIXED_TOTAL_DRAWS - finiteTotal)
|
||||||
|
.isGreaterThan(0);
|
||||||
|
|
||||||
|
// 抽完后,奖池中仅剩不限量礼物
|
||||||
|
java.util.List<BlindBoxCandidate> remaining = blindBoxPoolMapper.listActiveEntries(
|
||||||
|
ApiTestDataSeeder.DEFAULT_TENANT_ID, TEST_BLIND_BOX_ID, LocalDateTime.now());
|
||||||
|
Assertions.assertThat(remaining)
|
||||||
|
.as("只剩不限量礼物可用")
|
||||||
|
.hasSize(1);
|
||||||
|
Assertions.assertThat(remaining.get(0).getRewardGiftId()).isEqualTo(MIXED_GIFT_D_ID);
|
||||||
|
|
||||||
|
purgeRewards();
|
||||||
|
purgePool();
|
||||||
|
}
|
||||||
|
|
||||||
private void ensureBlindBoxConfig() {
|
private void ensureBlindBoxConfig() {
|
||||||
BlindBoxConfigEntity config = blindBoxConfigService.getById(TEST_BLIND_BOX_ID);
|
BlindBoxConfigEntity config = blindBoxConfigService.getById(TEST_BLIND_BOX_ID);
|
||||||
if (config != null) {
|
if (config != null) {
|
||||||
@@ -129,46 +200,81 @@ class BlindBoxServiceWeightTest extends WxCustomOrderApiTestSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void ensureSecondaryGift() {
|
private void ensureSecondaryGift() {
|
||||||
PlayGiftInfoEntity existing = findGift(SECONDARY_GIFT_ID);
|
ensureGift(
|
||||||
if (existing != null) {
|
SECONDARY_GIFT_ID,
|
||||||
return;
|
"API盲盒奖励",
|
||||||
}
|
new BigDecimal("9.99"),
|
||||||
PlayGiftInfoEntity entity = new PlayGiftInfoEntity();
|
"https://example.com/apitest/blindbox.png",
|
||||||
entity.setId(SECONDARY_GIFT_ID);
|
"Seeded secondary gift for blind box tests");
|
||||||
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
|
||||||
entity.setHistory(GiftHistory.CURRENT.getCode());
|
|
||||||
entity.setName("API盲盒奖励");
|
|
||||||
entity.setType(GiftType.NORMAL.getCode());
|
|
||||||
entity.setUrl("https://example.com/apitest/blindbox.png");
|
|
||||||
entity.setPrice(new BigDecimal("9.99"));
|
|
||||||
entity.setUnit("CNY");
|
|
||||||
entity.setState(GiftState.ACTIVE.getCode());
|
|
||||||
entity.setListingTime(LocalDateTime.now());
|
|
||||||
entity.setRemark("Seeded secondary gift for blind box tests");
|
|
||||||
giftInfoService.save(entity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensurePrimaryGift() {
|
private void ensurePrimaryGift() {
|
||||||
PlayGiftInfoEntity existing = findGift(PRIMARY_GIFT_ID);
|
ensureGift(
|
||||||
|
PRIMARY_GIFT_ID,
|
||||||
|
ApiTestDataSeeder.DEFAULT_GIFT_NAME,
|
||||||
|
new BigDecimal("15.00"),
|
||||||
|
"https://example.com/apitest/gift-basic.png",
|
||||||
|
"Seeded default gift for blind box tests");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureMixedGifts() {
|
||||||
|
ensureGift(
|
||||||
|
MIXED_GIFT_A_ID,
|
||||||
|
"API盲盒混合A",
|
||||||
|
new BigDecimal("11.11"),
|
||||||
|
"https://example.com/apitest/mixed-a.png",
|
||||||
|
"Mixed blind box gift A");
|
||||||
|
ensureGift(
|
||||||
|
MIXED_GIFT_B_ID,
|
||||||
|
"API盲盒混合B",
|
||||||
|
new BigDecimal("22.22"),
|
||||||
|
"https://example.com/apitest/mixed-b.png",
|
||||||
|
"Mixed blind box gift B");
|
||||||
|
ensureGift(
|
||||||
|
MIXED_GIFT_C_ID,
|
||||||
|
"API盲盒混合C",
|
||||||
|
new BigDecimal("33.33"),
|
||||||
|
"https://example.com/apitest/mixed-c.png",
|
||||||
|
"Mixed blind box gift C");
|
||||||
|
ensureGift(
|
||||||
|
MIXED_GIFT_D_ID,
|
||||||
|
"API盲盒混合D",
|
||||||
|
new BigDecimal("44.44"),
|
||||||
|
"https://example.com/apitest/mixed-d.png",
|
||||||
|
"Mixed blind box gift D");
|
||||||
|
ensureGift(
|
||||||
|
MIXED_GIFT_E_ID,
|
||||||
|
"API盲盒混合E",
|
||||||
|
new BigDecimal("55.55"),
|
||||||
|
"https://example.com/apitest/mixed-e.png",
|
||||||
|
"Mixed blind box gift E");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureGift(String giftId, String name, BigDecimal price, String imageUrl, String remark) {
|
||||||
|
PlayGiftInfoEntity existing = findGift(giftId);
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PlayGiftInfoEntity entity = new PlayGiftInfoEntity();
|
PlayGiftInfoEntity entity = new PlayGiftInfoEntity();
|
||||||
entity.setId(PRIMARY_GIFT_ID);
|
entity.setId(giftId);
|
||||||
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
entity.setHistory(GiftHistory.CURRENT.getCode());
|
entity.setHistory(GiftHistory.CURRENT.getCode());
|
||||||
entity.setName(ApiTestDataSeeder.DEFAULT_GIFT_NAME);
|
entity.setName(name);
|
||||||
entity.setType(GiftType.NORMAL.getCode());
|
entity.setType(GiftType.NORMAL.getCode());
|
||||||
entity.setUrl("https://example.com/apitest/gift-basic.png");
|
entity.setUrl(imageUrl);
|
||||||
entity.setPrice(new BigDecimal("15.00"));
|
entity.setPrice(price);
|
||||||
entity.setUnit("CNY");
|
entity.setUnit("CNY");
|
||||||
entity.setState(GiftState.ACTIVE.getCode());
|
entity.setState(GiftState.ACTIVE.getCode());
|
||||||
entity.setListingTime(LocalDateTime.now());
|
entity.setListingTime(LocalDateTime.now());
|
||||||
entity.setRemark("Seeded default gift for blind box tests");
|
entity.setRemark(remark);
|
||||||
giftInfoService.save(entity);
|
giftInfoService.save(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void insertPoolEntry(String giftId, int weight) {
|
private void insertPoolEntry(String giftId, int weight) {
|
||||||
|
insertPoolEntry(giftId, weight, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertPoolEntry(String giftId, int weight, Integer remainingStock) {
|
||||||
PlayGiftInfoEntity gift = findGift(giftId);
|
PlayGiftInfoEntity gift = findGift(giftId);
|
||||||
if (gift == null) {
|
if (gift == null) {
|
||||||
throw new IllegalStateException("Expected gift to be seeded: " + giftId);
|
throw new IllegalStateException("Expected gift to be seeded: " + giftId);
|
||||||
@@ -179,7 +285,7 @@ class BlindBoxServiceWeightTest extends WxCustomOrderApiTestSupport {
|
|||||||
entry.setRewardGiftId(giftId);
|
entry.setRewardGiftId(giftId);
|
||||||
entry.setRewardPrice(gift.getPrice());
|
entry.setRewardPrice(gift.getPrice());
|
||||||
entry.setWeight(weight);
|
entry.setWeight(weight);
|
||||||
entry.setRemainingStock(null);
|
entry.setRemainingStock(remainingStock);
|
||||||
entry.setValidFrom(null);
|
entry.setValidFrom(null);
|
||||||
entry.setValidTo(null);
|
entry.setValidTo(null);
|
||||||
entry.setStatus(BlindBoxPoolStatus.ENABLED.getCode());
|
entry.setStatus(BlindBoxPoolStatus.ENABLED.getCode());
|
||||||
|
|||||||
@@ -4,17 +4,32 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
|
|||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper;
|
||||||
|
import com.starry.admin.modules.blindbox.mapper.BlindBoxRewardMapper;
|
||||||
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxPoolEntity;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity;
|
||||||
import com.starry.admin.modules.blindbox.service.BlindBoxConfigService;
|
import com.starry.admin.modules.blindbox.service.BlindBoxConfigService;
|
||||||
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.GiftHistory;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.GiftState;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.GiftType;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayClerkGiftInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayClerkGiftInfoService;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayGiftInfoService;
|
||||||
import com.starry.admin.utils.SecurityUtils;
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
import com.starry.common.constant.Constants;
|
import com.starry.common.constant.Constants;
|
||||||
import com.starry.common.context.CustomSecurityContextHolder;
|
import com.starry.common.context.CustomSecurityContextHolder;
|
||||||
import com.starry.common.utils.IdUtils;
|
import com.starry.common.utils.IdUtils;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Objects;
|
||||||
import org.assertj.core.api.Assertions;
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.assertj.core.api.SoftAssertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@@ -23,6 +38,14 @@ class WxBlindBoxOrderApiTest extends WxCustomOrderApiTestSupport {
|
|||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private BlindBoxConfigService blindBoxConfigService;
|
private BlindBoxConfigService blindBoxConfigService;
|
||||||
|
@Autowired
|
||||||
|
private BlindBoxPoolMapper blindBoxPoolMapper;
|
||||||
|
@Autowired
|
||||||
|
private IPlayGiftInfoService giftInfoService;
|
||||||
|
@Autowired
|
||||||
|
private BlindBoxRewardMapper blindBoxRewardMapper;
|
||||||
|
@Autowired
|
||||||
|
private IPlayClerkGiftInfoService clerkGiftInfoService;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void blindBoxPurchaseFailsWhenBalanceInsufficient() throws Exception {
|
void blindBoxPurchaseFailsWhenBalanceInsufficient() throws Exception {
|
||||||
@@ -73,4 +96,116 @@ class WxBlindBoxOrderApiTest extends WxCustomOrderApiTestSupport {
|
|||||||
CustomSecurityContextHolder.remove();
|
CustomSecurityContextHolder.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void blindBoxPurchaseAndDispatchSucceedWhenGiftInactive() throws Exception {
|
||||||
|
String configId = "blind-inactive-" + IdUtils.getUuid().substring(0, 6);
|
||||||
|
String giftId = "gift-inactive-" + IdUtils.getUuid().substring(0, 6);
|
||||||
|
Long poolId = null;
|
||||||
|
String rewardId = null;
|
||||||
|
try {
|
||||||
|
ensureTenantContext();
|
||||||
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
|
config.setId(configId);
|
||||||
|
config.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
config.setName("下架礼物测试盲盒");
|
||||||
|
config.setPrice(new BigDecimal("19.90"));
|
||||||
|
config.setStatus(1);
|
||||||
|
blindBoxConfigService.save(config);
|
||||||
|
|
||||||
|
PlayGiftInfoEntity gift = new PlayGiftInfoEntity();
|
||||||
|
gift.setId(giftId);
|
||||||
|
gift.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
gift.setName("盲盒下架礼物");
|
||||||
|
gift.setHistory(GiftHistory.CURRENT.getCode());
|
||||||
|
gift.setState(GiftState.ACTIVE.getCode());
|
||||||
|
gift.setType(GiftType.NORMAL.getCode());
|
||||||
|
gift.setUrl("https://example.com/apitest/blindbox-off.png");
|
||||||
|
gift.setPrice(new BigDecimal("9.99"));
|
||||||
|
giftInfoService.save(gift);
|
||||||
|
|
||||||
|
BlindBoxPoolEntity entry = new BlindBoxPoolEntity();
|
||||||
|
entry.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entry.setBlindBoxId(configId);
|
||||||
|
entry.setRewardGiftId(giftId);
|
||||||
|
entry.setRewardPrice(gift.getPrice());
|
||||||
|
entry.setWeight(100);
|
||||||
|
entry.setRemainingStock(1);
|
||||||
|
entry.setStatus(1);
|
||||||
|
entry.setValidFrom(LocalDateTime.now().minusDays(1));
|
||||||
|
entry.setValidTo(LocalDateTime.now().plusDays(1));
|
||||||
|
blindBoxPoolMapper.insert(entry);
|
||||||
|
poolId = entry.getId();
|
||||||
|
|
||||||
|
gift.setState(GiftState.OFF_SHELF.getCode());
|
||||||
|
giftInfoService.updateById(gift);
|
||||||
|
|
||||||
|
resetCustomerBalance();
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
|
||||||
|
String payload = objectMapper.createObjectNode()
|
||||||
|
.put("blindBoxId", configId)
|
||||||
|
.put("clerkId", ApiTestDataSeeder.DEFAULT_CLERK_ID)
|
||||||
|
.put("weiChatCode", "apitest-customer-wx")
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
MvcResult purchaseResult = mockMvc.perform(post("/wx/blind-box/order/purchase")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.reward.rewardId").isString())
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode rewardNode = objectMapper.readTree(purchaseResult.getResponse().getContentAsString())
|
||||||
|
.path("data").path("reward");
|
||||||
|
rewardId = rewardNode.path("rewardId").asText();
|
||||||
|
Assertions.assertThat(rewardId).isNotBlank();
|
||||||
|
|
||||||
|
SoftAssertions softly = new SoftAssertions();
|
||||||
|
softly.assertThat(rewardNode.path("rewardGiftId").asText()).isEqualTo(giftId);
|
||||||
|
softly.assertThat(rewardNode.path("status").asText()).isEqualTo("UNUSED");
|
||||||
|
softly.assertAll();
|
||||||
|
|
||||||
|
PlayClerkGiftInfoEntity before = clerkGiftInfoService.selectByGiftIdAndClerkId(giftId, ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
long beforeCount = before != null && before.getGiffNumber() != null ? before.getGiffNumber() : 0L;
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/blind-box/reward/" + rewardId + "/dispatch")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.createObjectNode()
|
||||||
|
.put("clerkId", ApiTestDataSeeder.DEFAULT_CLERK_ID)
|
||||||
|
.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.status").value("USED"));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
BlindBoxRewardEntity storedReward = blindBoxRewardMapper.selectById(rewardId);
|
||||||
|
Assertions.assertThat(storedReward).isNotNull();
|
||||||
|
Assertions.assertThat(storedReward.getStatus()).isEqualTo("USED");
|
||||||
|
Assertions.assertThat(storedReward.getUsedClerkId()).isEqualTo(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
|
||||||
|
PlayClerkGiftInfoEntity after = clerkGiftInfoService.selectByGiftIdAndClerkId(giftId, ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
long afterCount = after != null && after.getGiffNumber() != null ? after.getGiffNumber() : 0L;
|
||||||
|
Assertions.assertThat(afterCount).isEqualTo(beforeCount + 1);
|
||||||
|
} finally {
|
||||||
|
ensureTenantContext();
|
||||||
|
if (Objects.nonNull(poolId)) {
|
||||||
|
blindBoxPoolMapper.deleteById(poolId);
|
||||||
|
}
|
||||||
|
blindBoxConfigService.removeById(configId);
|
||||||
|
giftInfoService.removeById(giftId);
|
||||||
|
if (rewardId != null) {
|
||||||
|
blindBoxRewardMapper.deleteById(rewardId);
|
||||||
|
}
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package com.starry.admin.modules.blindbox.service;
|
package com.starry.admin.modules.blindbox.service;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@@ -20,6 +23,7 @@ import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
|
|||||||
import com.starry.admin.utils.SecurityUtils;
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -185,6 +189,84 @@ class BlindBoxPoolAdminServiceTest {
|
|||||||
assertEquals("超值娃娃", options.get(0).getName());
|
assertEquals("超值娃娃", options.get(0).getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldOverwriteRemainingStockOnReimport() {
|
||||||
|
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);
|
||||||
|
|
||||||
|
List<BlindBoxPoolEntity> inserted = new ArrayList<>();
|
||||||
|
when(blindBoxPoolMapper.insert(any(BlindBoxPoolEntity.class))).thenAnswer(invocation -> {
|
||||||
|
BlindBoxPoolEntity entity = invocation.getArgument(0);
|
||||||
|
BlindBoxPoolEntity snapshot = new BlindBoxPoolEntity();
|
||||||
|
snapshot.setBlindBoxId(entity.getBlindBoxId());
|
||||||
|
snapshot.setRewardGiftId(entity.getRewardGiftId());
|
||||||
|
snapshot.setRemainingStock(entity.getRemainingStock());
|
||||||
|
inserted.add(snapshot);
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
BlindBoxPoolImportRow first = new BlindBoxPoolImportRow();
|
||||||
|
first.setRewardGiftName("超值娃娃");
|
||||||
|
first.setWeight(50);
|
||||||
|
first.setRemainingStock(5);
|
||||||
|
first.setStatus(1);
|
||||||
|
|
||||||
|
BlindBoxPoolImportRow second = new BlindBoxPoolImportRow();
|
||||||
|
second.setRewardGiftName("超值娃娃");
|
||||||
|
second.setWeight(60);
|
||||||
|
second.setRemainingStock(1);
|
||||||
|
second.setStatus(1);
|
||||||
|
|
||||||
|
blindBoxPoolAdminService.replacePool("blind-1", Collections.singletonList(first));
|
||||||
|
blindBoxPoolAdminService.replacePool("blind-1", Collections.singletonList(second));
|
||||||
|
|
||||||
|
assertEquals(2, inserted.size());
|
||||||
|
assertEquals(Integer.valueOf(5), inserted.get(0).getRemainingStock());
|
||||||
|
assertEquals(Integer.valueOf(1), inserted.get(1).getRemainingStock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldKeepUnlimitedStockWhenRemainingStockBlank() {
|
||||||
|
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);
|
||||||
|
|
||||||
|
ArgumentCaptor<BlindBoxPoolEntity> captor = ArgumentCaptor.forClass(BlindBoxPoolEntity.class);
|
||||||
|
when(blindBoxPoolMapper.insert(any(BlindBoxPoolEntity.class))).thenReturn(1);
|
||||||
|
|
||||||
|
BlindBoxPoolImportRow importRow = new BlindBoxPoolImportRow();
|
||||||
|
importRow.setRewardGiftName("超值娃娃");
|
||||||
|
importRow.setWeight(30);
|
||||||
|
importRow.setStatus(1);
|
||||||
|
|
||||||
|
blindBoxPoolAdminService.replacePool("blind-1", Collections.singletonList(importRow));
|
||||||
|
|
||||||
|
verify(blindBoxPoolMapper).insert(captor.capture());
|
||||||
|
BlindBoxPoolEntity saved = captor.getValue();
|
||||||
|
assertNull(saved.getRemainingStock());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldCreatePoolEntry() {
|
void shouldCreatePoolEntry() {
|
||||||
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
@@ -271,4 +353,68 @@ class BlindBoxPoolAdminServiceTest {
|
|||||||
assertEquals("超级公仔", view.getRewardGiftName());
|
assertEquals("超级公仔", view.getRewardGiftName());
|
||||||
verify(blindBoxPoolMapper).updateById(existing);
|
verify(blindBoxPoolMapper).updateById(existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAllowUpdateWhenGiftInactiveOrHistorical() {
|
||||||
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
|
config.setId("blind-1");
|
||||||
|
config.setTenantId("tenant-1");
|
||||||
|
when(blindBoxConfigService.requireById("blind-1")).thenReturn(config);
|
||||||
|
|
||||||
|
PlayGiftInfoEntity inactiveGift = new PlayGiftInfoEntity();
|
||||||
|
inactiveGift.setId("gift-offline");
|
||||||
|
inactiveGift.setTenantId("tenant-1");
|
||||||
|
inactiveGift.setHistory("1");
|
||||||
|
inactiveGift.setState("1");
|
||||||
|
inactiveGift.setType("1");
|
||||||
|
inactiveGift.setPrice(BigDecimal.valueOf(66.6));
|
||||||
|
inactiveGift.setName("下架礼物");
|
||||||
|
when(playGiftInfoMapper.selectById("gift-offline")).thenReturn(inactiveGift);
|
||||||
|
|
||||||
|
BlindBoxPoolEntity existing = new BlindBoxPoolEntity();
|
||||||
|
existing.setId(500L);
|
||||||
|
existing.setTenantId("tenant-1");
|
||||||
|
existing.setBlindBoxId("blind-1");
|
||||||
|
existing.setRewardGiftId("gift-on");
|
||||||
|
existing.setStatus(1);
|
||||||
|
when(blindBoxPoolMapper.selectById(500L)).thenReturn(existing);
|
||||||
|
when(blindBoxPoolMapper.updateById(existing)).thenReturn(1);
|
||||||
|
|
||||||
|
BlindBoxPoolUpsertRequest request = new BlindBoxPoolUpsertRequest();
|
||||||
|
request.setBlindBoxId("blind-1");
|
||||||
|
request.setRewardGiftId("gift-offline");
|
||||||
|
request.setWeight(10);
|
||||||
|
request.setStatus(1);
|
||||||
|
|
||||||
|
blindBoxPoolAdminService.update(500L, request);
|
||||||
|
|
||||||
|
verify(blindBoxPoolMapper).updateById(existing);
|
||||||
|
assertEquals("gift-offline", existing.getRewardGiftId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectCreateWhenGiftInactive() {
|
||||||
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
|
config.setId("blind-1");
|
||||||
|
config.setTenantId("tenant-1");
|
||||||
|
when(blindBoxConfigService.requireById("blind-1")).thenReturn(config);
|
||||||
|
|
||||||
|
PlayGiftInfoEntity inactiveGift = new PlayGiftInfoEntity();
|
||||||
|
inactiveGift.setId("gift-off");
|
||||||
|
inactiveGift.setTenantId("tenant-1");
|
||||||
|
inactiveGift.setHistory("1");
|
||||||
|
inactiveGift.setState("1");
|
||||||
|
inactiveGift.setType("1");
|
||||||
|
when(playGiftInfoMapper.selectById("gift-off")).thenReturn(inactiveGift);
|
||||||
|
|
||||||
|
BlindBoxPoolUpsertRequest request = new BlindBoxPoolUpsertRequest();
|
||||||
|
request.setBlindBoxId("blind-1");
|
||||||
|
request.setRewardGiftId("gift-off");
|
||||||
|
request.setWeight(10);
|
||||||
|
|
||||||
|
CustomException ex = assertThrows(CustomException.class,
|
||||||
|
() -> blindBoxPoolAdminService.create("blind-1", request));
|
||||||
|
assertTrue(ex.getMessage().contains("中奖礼物不存在或已下架"));
|
||||||
|
verify(blindBoxPoolMapper, times(0)).insert(any());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,6 +171,21 @@ class BlindBoxServiceTest {
|
|||||||
verify(rewardMapper, times(0)).markUsed(any(), any(), any(), any());
|
verify(rewardMapper, times(0)).markUsed(any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectWhenNoEligibleRewardExists() {
|
||||||
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
|
config.setId("blind-1");
|
||||||
|
config.setTenantId("tenant-1");
|
||||||
|
when(configService.requireById("blind-1")).thenReturn(config);
|
||||||
|
when(poolMapper.listActiveEntries(eq("tenant-1"), eq("blind-1"), any(LocalDateTime.class)))
|
||||||
|
.thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
CustomException ex = assertThrows(CustomException.class,
|
||||||
|
() -> blindBoxService.drawReward("tenant-1", "order-404", "customer-9", "blind-1", "seed-out"));
|
||||||
|
assertTrue(ex.getMessage().contains("奖池暂无可用奖励"));
|
||||||
|
verify(inventoryService, times(0)).reserveRewardStock(any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
private BlindBoxRewardEntity buildRewardEntity() {
|
private BlindBoxRewardEntity buildRewardEntity() {
|
||||||
BlindBoxRewardEntity reward = new BlindBoxRewardEntity();
|
BlindBoxRewardEntity reward = new BlindBoxRewardEntity();
|
||||||
reward.setId("reward-1");
|
reward.setId("reward-1");
|
||||||
|
|||||||
Reference in New Issue
Block a user