package cn.iocoder.yudao.module.promotion.service.combination; import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.module.member.api.user.MemberUserApi; import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi; import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi; import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO; import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO; import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationProductDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationRecordDO; import cn.iocoder.yudao.module.promotion.dal.mysql.combination.CombinationRecordMapper; import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum; import cn.iocoder.yudao.module.system.api.social.SocialClientApi; import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi; import cn.iocoder.yudao.module.trade.enums.order.TradeOrderCancelTypeEnum; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; import org.springframework.test.context.bean.override.mockito.MockitoBean; import java.time.LocalDateTime; import java.util.List; import java.util.function.Consumer; import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * {@link CombinationRecordServiceImpl} 的单元测试类 * * @author 芋道源码 */ @Import(CombinationRecordServiceImpl.class) public class CombinationRecordServiceImplTest extends BaseDbUnitTest { private static final Long USER_ID = 100L; private static final Long ACTIVITY_ID = 200L; private static final Long SPU_ID = 300L; private static final Long SKU_ID = 400L; private static final Long HEAD_ID = 500L; private static final Long ORDER_ID = 600L; @Resource private CombinationRecordServiceImpl combinationRecordService; @Resource private CombinationRecordMapper combinationRecordMapper; @MockitoBean private CombinationActivityService combinationActivityService; @MockitoBean private MemberUserApi memberUserApi; @MockitoBean private ProductSpuApi productSpuApi; @MockitoBean private ProductSkuApi productSkuApi; @MockitoBean private TradeOrderApi tradeOrderApi; @MockitoBean private SocialClientApi socialClientApi; @Test public void testValidateCombinationRecord_headIdNullAsNewGroup() { // mock 数据 CombinationActivityDO activity = mockValidateContext(10); // 调用 KeyValue result = combinationRecordService .validateCombinationRecord(USER_ID, ACTIVITY_ID, null, SKU_ID, 1); // 断言 assertSame(activity, result.getKey()); assertEquals(SKU_ID, result.getValue().getSkuId()); } @Test public void testValidateCombinationRecord_headIdGroupAsNewGroup() { // mock 数据:headId 为 0 时,也代表新开团 CombinationActivityDO activity = mockValidateContext(10); // 调用 KeyValue result = combinationRecordService .validateCombinationRecord(USER_ID, ACTIVITY_ID, CombinationRecordDO.HEAD_ID_GROUP, SKU_ID, 1); // 断言 assertSame(activity, result.getKey()); assertEquals(SKU_ID, result.getValue().getSkuId()); } @Test public void testValidateCombinationRecord_realHeadIdAsJoinGroup() { // mock 数据:真实 headId 时,按参团校验父拼团 CombinationActivityDO activity = mockValidateContext(10); combinationRecordMapper.insert(randomRecord(o -> { o.setId(HEAD_ID); o.setHeadId(CombinationRecordDO.HEAD_ID_GROUP); o.setUserId(101L); o.setOrderId(601L); })); // 调用 KeyValue result = combinationRecordService .validateCombinationRecord(USER_ID, ACTIVITY_ID, HEAD_ID, SKU_ID, 1); // 断言 assertSame(activity, result.getKey()); assertEquals(SKU_ID, result.getValue().getSkuId()); } @Test public void testValidateCombinationRecord_realHeadIdNotExists() { // mock 数据:拼团活动正常,但父拼团不存在 mockActivity(); // 调用,并断言 assertServiceException(() -> combinationRecordService.validateCombinationRecord( USER_ID, ACTIVITY_ID, HEAD_ID, SKU_ID, 1), COMBINATION_RECORD_HEAD_NOT_EXISTS); } @Test public void testValidateCombinationRecord_countEqualsStock() { // mock 数据:库存刚好等于购买数量 mockValidateContext(10); // 调用 KeyValue result = combinationRecordService .validateCombinationRecord(USER_ID, ACTIVITY_ID, null, SKU_ID, 10); // 断言 assertNotNull(result); } @Test public void testValidateCombinationRecord_countGreaterThanStock() { // mock 数据:库存不足 mockValidateContext(10); // 调用,并断言 assertServiceException(() -> combinationRecordService.validateCombinationRecord( USER_ID, ACTIVITY_ID, null, SKU_ID, 11), COMBINATION_ACTIVITY_UPDATE_STOCK_FAIL); } @Test public void testValidateCombinationRecord_haveJoined() { // mock 数据:用户存在进行中的拼团记录 mockValidateContext(10); combinationRecordMapper.insert(randomRecord(o -> { o.setStatus(CombinationRecordStatusEnum.IN_PROGRESS.getStatus()); o.setCount(1); })); // 调用,并断言 assertServiceException(() -> combinationRecordService.validateCombinationRecord( USER_ID, ACTIVITY_ID, null, SKU_ID, 1), COMBINATION_RECORD_FAILED_HAVE_JOINED); } @Test public void testValidateCombinationRecord_totalLimitCountExceed() { // mock 数据:用户历史购买数量加本次数量超过总限购 CombinationActivityDO activity = mockValidateContext(10); activity.setTotalLimitCount(5); combinationRecordMapper.insert(randomRecord(o -> { o.setStatus(CombinationRecordStatusEnum.SUCCESS.getStatus()); o.setCount(3); })); // 调用,并断言 assertServiceException(() -> combinationRecordService.validateCombinationRecord( USER_ID, ACTIVITY_ID, null, SKU_ID, 3), COMBINATION_RECORD_FAILED_TOTAL_LIMIT_COUNT_EXCEED); } @Test public void testCreateCombinationRecord_headIdGroupAsNewGroup() { // mock 数据:开团参数中 headId 为 0 mockValidateContext(10); mockCreateContext(); CombinationRecordCreateReqDTO reqDTO = randomCreateReqDTO(CombinationRecordDO.HEAD_ID_GROUP); // 调用 CombinationRecordDO record = combinationRecordService.createCombinationRecord(reqDTO); // 断言:仍然按开团处理,不更新其它团记录 assertNotNull(record.getId()); assertEquals(CombinationRecordDO.HEAD_ID_GROUP, record.getHeadId()); assertEquals(CombinationRecordStatusEnum.IN_PROGRESS.getStatus(), record.getStatus()); assertEquals(1, record.getUserCount()); assertFalse(record.getVirtualGroup()); assertNotNull(record.getStartTime()); assertNotNull(record.getExpireTime()); assertEquals(1L, combinationRecordMapper.selectCount()); assertEquals(CombinationRecordDO.HEAD_ID_GROUP, combinationRecordMapper.selectById(record.getId()).getHeadId()); } @Test public void testHandleExpireRecord_cancelOrders() { // mock 数据:过期团长和一个团员 CombinationRecordDO headRecord = randomRecord(o -> { o.setId(HEAD_ID); o.setHeadId(CombinationRecordDO.HEAD_ID_GROUP); o.setUserId(USER_ID); o.setOrderId(ORDER_ID); }); CombinationRecordDO memberRecord = randomRecord(o -> { o.setId(501L); o.setHeadId(HEAD_ID); o.setUserId(101L); o.setOrderId(601L); }); combinationRecordMapper.insert(headRecord); combinationRecordMapper.insert(memberRecord); // 调用 combinationRecordService.handleExpireRecord(headRecord); // 断言:整团记录标记为失败,并取消已支付订单 List records = combinationRecordMapper.selectList(); assertEquals(2, records.size()); assertTrue(records.stream().allMatch(item -> CombinationRecordStatusEnum.FAILED.getStatus().equals(item.getStatus()))); assertTrue(records.stream().allMatch(item -> item.getEndTime() != null)); verify(tradeOrderApi).cancelPaidOrder(USER_ID, ORDER_ID, TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType()); verify(tradeOrderApi).cancelPaidOrder(101L, 601L, TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType()); } private CombinationActivityDO mockValidateContext(Integer stock) { CombinationActivityDO activity = mockActivity(); CombinationProductDO product = randomPojo(CombinationProductDO.class, o -> { o.setActivityId(ACTIVITY_ID); o.setSpuId(SPU_ID); o.setSkuId(SKU_ID); o.setCombinationPrice(100); }); ProductSkuRespDTO sku = randomPojo(ProductSkuRespDTO.class, o -> { o.setId(SKU_ID); o.setSpuId(SPU_ID); o.setStock(stock); o.setPicUrl("https://www.iocoder.cn/sku.png"); }); when(combinationActivityService.selectByActivityIdAndSkuId(ACTIVITY_ID, SKU_ID)).thenReturn(product); when(productSkuApi.getSku(SKU_ID)).thenReturn(sku); return activity; } private CombinationActivityDO mockActivity() { CombinationActivityDO activity = randomPojo(CombinationActivityDO.class, o -> { o.setId(ACTIVITY_ID); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setStartTime(LocalDateTime.now().minusHours(1)); o.setEndTime(LocalDateTime.now().plusHours(1)); o.setSingleLimitCount(100); o.setTotalLimitCount(100); o.setUserSize(3); o.setVirtualGroup(false); o.setLimitDuration(24); }); when(combinationActivityService.validateCombinationActivityExists(ACTIVITY_ID)).thenReturn(activity); return activity; } private void mockCreateContext() { MemberUserRespDTO user = randomPojo(MemberUserRespDTO.class, o -> { o.setId(USER_ID); o.setNickname("芋道源码"); o.setAvatar("https://www.iocoder.cn/avatar.png"); }); when(memberUserApi.getUser(USER_ID)).thenReturn(user); ProductSpuRespDTO spu = randomPojo(ProductSpuRespDTO.class, o -> { o.setId(SPU_ID); o.setName("测试商品"); o.setPicUrl("https://www.iocoder.cn/spu.png"); }); when(productSpuApi.getSpu(SPU_ID)).thenReturn(spu); } private static CombinationRecordCreateReqDTO randomCreateReqDTO(Long headId) { return randomPojo(CombinationRecordCreateReqDTO.class, o -> { o.setActivityId(ACTIVITY_ID); o.setSpuId(SPU_ID); o.setSkuId(SKU_ID); o.setCount(1); o.setOrderId(ORDER_ID); o.setUserId(USER_ID); o.setHeadId(headId); o.setCombinationPrice(100); }); } @SafeVarargs private static CombinationRecordDO randomRecord(Consumer... consumers) { return randomPojo(CombinationRecordDO.class, o -> { o.setActivityId(ACTIVITY_ID); o.setCombinationPrice(100); o.setSpuId(SPU_ID); o.setSpuName("测试商品"); o.setPicUrl("https://www.iocoder.cn/spu.png"); o.setSkuId(SKU_ID); o.setCount(1); o.setUserId(USER_ID); o.setNickname("芋道源码"); o.setAvatar("https://www.iocoder.cn/avatar.png"); o.setHeadId(CombinationRecordDO.HEAD_ID_GROUP); o.setStatus(CombinationRecordStatusEnum.IN_PROGRESS.getStatus()); o.setOrderId(ORDER_ID); o.setUserSize(3); o.setUserCount(1); o.setVirtualGroup(false); o.setStartTime(LocalDateTime.now().minusMinutes(10)); o.setExpireTime(LocalDateTime.now().plusHours(1)); o.setEndTime(null); for (Consumer consumer : consumers) { consumer.accept(o); } }); } }