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:
@@ -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.module.constant.BlindBoxConfigStatus;
|
||||
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.BlindBoxPoolEntity;
|
||||
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 PRIMARY_GIFT_ID = ApiTestDataSeeder.DEFAULT_GIFT_ID;
|
||||
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 PRIMARY_WEIGHT = 80;
|
||||
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 SECONDARY_RATIO_MIN = 0.12;
|
||||
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
|
||||
private BlindBoxService blindBoxService;
|
||||
@@ -112,6 +123,66 @@ class BlindBoxServiceWeightTest extends WxCustomOrderApiTestSupport {
|
||||
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() {
|
||||
BlindBoxConfigEntity config = blindBoxConfigService.getById(TEST_BLIND_BOX_ID);
|
||||
if (config != null) {
|
||||
@@ -129,46 +200,81 @@ class BlindBoxServiceWeightTest extends WxCustomOrderApiTestSupport {
|
||||
}
|
||||
|
||||
private void ensureSecondaryGift() {
|
||||
PlayGiftInfoEntity existing = findGift(SECONDARY_GIFT_ID);
|
||||
if (existing != null) {
|
||||
return;
|
||||
}
|
||||
PlayGiftInfoEntity entity = new PlayGiftInfoEntity();
|
||||
entity.setId(SECONDARY_GIFT_ID);
|
||||
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);
|
||||
ensureGift(
|
||||
SECONDARY_GIFT_ID,
|
||||
"API盲盒奖励",
|
||||
new BigDecimal("9.99"),
|
||||
"https://example.com/apitest/blindbox.png",
|
||||
"Seeded secondary gift for blind box tests");
|
||||
}
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
PlayGiftInfoEntity entity = new PlayGiftInfoEntity();
|
||||
entity.setId(PRIMARY_GIFT_ID);
|
||||
entity.setId(giftId);
|
||||
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||
entity.setHistory(GiftHistory.CURRENT.getCode());
|
||||
entity.setName(ApiTestDataSeeder.DEFAULT_GIFT_NAME);
|
||||
entity.setName(name);
|
||||
entity.setType(GiftType.NORMAL.getCode());
|
||||
entity.setUrl("https://example.com/apitest/gift-basic.png");
|
||||
entity.setPrice(new BigDecimal("15.00"));
|
||||
entity.setUrl(imageUrl);
|
||||
entity.setPrice(price);
|
||||
entity.setUnit("CNY");
|
||||
entity.setState(GiftState.ACTIVE.getCode());
|
||||
entity.setListingTime(LocalDateTime.now());
|
||||
entity.setRemark("Seeded default gift for blind box tests");
|
||||
entity.setRemark(remark);
|
||||
giftInfoService.save(entity);
|
||||
}
|
||||
|
||||
private void insertPoolEntry(String giftId, int weight) {
|
||||
insertPoolEntry(giftId, weight, null);
|
||||
}
|
||||
|
||||
private void insertPoolEntry(String giftId, int weight, Integer remainingStock) {
|
||||
PlayGiftInfoEntity gift = findGift(giftId);
|
||||
if (gift == null) {
|
||||
throw new IllegalStateException("Expected gift to be seeded: " + giftId);
|
||||
@@ -179,7 +285,7 @@ class BlindBoxServiceWeightTest extends WxCustomOrderApiTestSupport {
|
||||
entry.setRewardGiftId(giftId);
|
||||
entry.setRewardPrice(gift.getPrice());
|
||||
entry.setWeight(weight);
|
||||
entry.setRemainingStock(null);
|
||||
entry.setRemainingStock(remainingStock);
|
||||
entry.setValidFrom(null);
|
||||
entry.setValidTo(null);
|
||||
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.status;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
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.BlindBoxPoolEntity;
|
||||
import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity;
|
||||
import com.starry.admin.modules.blindbox.service.BlindBoxConfigService;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||
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.common.constant.Constants;
|
||||
import com.starry.common.context.CustomSecurityContextHolder;
|
||||
import com.starry.common.utils.IdUtils;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -23,6 +38,14 @@ class WxBlindBoxOrderApiTest extends WxCustomOrderApiTestSupport {
|
||||
|
||||
@Autowired
|
||||
private BlindBoxConfigService blindBoxConfigService;
|
||||
@Autowired
|
||||
private BlindBoxPoolMapper blindBoxPoolMapper;
|
||||
@Autowired
|
||||
private IPlayGiftInfoService giftInfoService;
|
||||
@Autowired
|
||||
private BlindBoxRewardMapper blindBoxRewardMapper;
|
||||
@Autowired
|
||||
private IPlayClerkGiftInfoService clerkGiftInfoService;
|
||||
|
||||
@Test
|
||||
void blindBoxPurchaseFailsWhenBalanceInsufficient() throws Exception {
|
||||
@@ -73,4 +96,116 @@ class WxBlindBoxOrderApiTest extends WxCustomOrderApiTestSupport {
|
||||
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;
|
||||
|
||||
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.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
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 java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -185,6 +189,84 @@ class BlindBoxPoolAdminServiceTest {
|
||||
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
|
||||
void shouldCreatePoolEntry() {
|
||||
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||
@@ -271,4 +353,68 @@ class BlindBoxPoolAdminServiceTest {
|
||||
assertEquals("超级公仔", view.getRewardGiftName());
|
||||
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());
|
||||
}
|
||||
|
||||
@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() {
|
||||
BlindBoxRewardEntity reward = new BlindBoxRewardEntity();
|
||||
reward.setId("reward-1");
|
||||
|
||||
Reference in New Issue
Block a user