From 69121bec6ea3361ac7e803e8d8b6434a89bce4c5 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 23 May 2026 18:23:49 +0800 Subject: [PATCH] fix(mall): 修复优惠券模板限领数量校验误报库存不足 --- yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java | 18 +++++++++++++++++- yudao-module-mall/yudao-module-promotion/src/test/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImplTest.java | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------- yudao-module-mall/yudao-module-promotion/src/test/resources/sql/create_tables.sql | 30 ++++++++++++++++++++++++++---- 3 files changed, 145 insertions(+), 19 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java index 8c25cb8..d4323c6 100755 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java @@ -52,6 +52,9 @@ public class CouponTemplateServiceImpl implements CouponTemplateService { @Override public Long createCouponTemplate(CouponTemplateCreateReqVO createReqVO) { + // 校验发放数量不能小于每人限领数量(仅在 CouponTakeTypeEnum.USER 用户领取时) + validateTotalCountNotLessThanTakeLimitCount(createReqVO.getTakeType(), createReqVO.getTotalCount(), + createReqVO.getTakeLimitCount()); // 校验商品范围 validateProductScope(createReqVO.getProductScope(), createReqVO.getProductScopeValues()); // 插入 @@ -66,8 +69,11 @@ public class CouponTemplateServiceImpl implements CouponTemplateService { public void updateCouponTemplate(CouponTemplateUpdateReqVO updateReqVO) { // 校验存在 CouponTemplateDO couponTemplate = validateCouponTemplateExists(updateReqVO.getId()); + // 校验发放数量不能小于每人限领数量(仅在 CouponTakeTypeEnum.USER 用户领取时) + validateTotalCountNotLessThanTakeLimitCount(updateReqVO.getTakeType(), updateReqVO.getTotalCount(), + updateReqVO.getTakeLimitCount()); // 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时) - if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType()) + if (CouponTakeTypeEnum.isUser(updateReqVO.getTakeType()) && !isTotalCountUnlimited(updateReqVO.getTotalCount()) // 非不限制总发放数量 && updateReqVO.getTotalCount() < couponTemplate.getTakeCount()) { throw exception(COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL, couponTemplate.getTakeCount()); @@ -104,6 +110,16 @@ public class CouponTemplateServiceImpl implements CouponTemplateService { return couponTemplate; } + private void validateTotalCountNotLessThanTakeLimitCount(Integer takeType, Integer totalCount, Integer takeLimitCount) { + // 修复 https://gitee.com/yudaocode/yudao-mall-uniapp/issues/IJLP6Q 反馈 + if (CouponTakeTypeEnum.isUser(takeType) + && !isTakeLimitCountUnlimited(takeLimitCount) + && !isTotalCountUnlimited(totalCount) + && takeLimitCount > totalCount) { + throw exception(COUPON_TEMPLATE_NOT_ENOUGH); + } + } + private void validateProductScope(Integer productScope, List productScopeValues) { if (Objects.equals(PromotionProductScopeEnum.SPU.getScope(), productScope)) { productSpuApi.validateSpuList(productScopeValues); diff --git a/yudao-module-mall/yudao-module-promotion/src/test/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImplTest.java b/yudao-module-mall/yudao-module-promotion/src/test/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImplTest.java index 2f38628..1845be2 100755 --- a/yudao-module-mall/yudao-module-promotion/src/test/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImplTest.java +++ b/yudao-module-mall/yudao-module-promotion/src/test/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImplTest.java @@ -2,7 +2,10 @@ package cn.iocoder.yudao.module.promotion.service.coupon; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.product.api.category.ProductCategoryApi; +import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi; import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.template.CouponTemplateCreateReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.template.CouponTemplatePageReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.template.CouponTemplateUpdateReqVO; @@ -10,21 +13,23 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO; import cn.iocoder.yudao.module.promotion.dal.mysql.coupon.CouponTemplateMapper; import cn.iocoder.yudao.module.promotion.enums.common.PromotionDiscountTypeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; +import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum; import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import jakarta.annotation.Resource; import java.time.LocalDateTime; +import java.util.function.Consumer; -import static cn.hutool.core.util.RandomUtil.randomEle; import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime; import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_NOT_ENOUGH; import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_NOT_EXISTS; import static org.junit.jupiter.api.Assertions.*; @@ -33,7 +38,6 @@ import static org.junit.jupiter.api.Assertions.*; * * @author 芋道源码 */ -@Disabled // TODO 芋艿:后续 fix 补充的单测 @Import(CouponTemplateServiceImpl.class) public class CouponTemplateServiceImplTest extends BaseDbUnitTest { @@ -43,13 +47,26 @@ public class CouponTemplateServiceImplTest extends BaseDbUnitTest { @Resource private CouponTemplateMapper couponTemplateMapper; + @MockitoBean + private ProductCategoryApi productCategoryApi; + @MockitoBean + private ProductSpuApi productSpuApi; + @Test public void testCreateCouponTemplate_success() { // 准备参数 - CouponTemplateCreateReqVO reqVO = randomPojo(CouponTemplateCreateReqVO.class, - o -> o.setProductScope(randomEle(PromotionProductScopeEnum.values()).getScope()) - .setValidityType(randomEle(CouponTemplateValidityTypeEnum.values()).getType()) - .setDiscountType(randomEle(PromotionDiscountTypeEnum.values()).getType())); + CouponTemplateCreateReqVO reqVO = randomPojo(CouponTemplateCreateReqVO.class, o -> { + o.setTotalCount(10); + o.setTakeLimitCount(2); + o.setTakeType(CouponTakeTypeEnum.USER.getType()); + o.setProductScope(PromotionProductScopeEnum.ALL.getScope()); + o.setProductScopeValues(null); + o.setValidityType(CouponTemplateValidityTypeEnum.TERM.getType()); + o.setFixedStartTerm(0); + o.setFixedEndTerm(7); + o.setDiscountType(PromotionDiscountTypeEnum.PRICE.getType()); + o.setDiscountPrice(100); + }); // 调用 Long couponTemplateId = couponTemplateService.createCouponTemplate(reqVO); @@ -63,15 +80,21 @@ public class CouponTemplateServiceImplTest extends BaseDbUnitTest { @Test public void testUpdateCouponTemplate_success() { // mock 数据 - CouponTemplateDO dbCouponTemplate = randomPojo(CouponTemplateDO.class); + CouponTemplateDO dbCouponTemplate = randomCouponTemplateDO(); couponTemplateMapper.insert(dbCouponTemplate);// @Sql: 先插入出一条存在的数据 // 准备参数 CouponTemplateUpdateReqVO reqVO = randomPojo(CouponTemplateUpdateReqVO.class, o -> { o.setId(dbCouponTemplate.getId()); // 设置更新的 ID - // 其它通用字段 - o.setProductScope(randomEle(PromotionProductScopeEnum.values()).getScope()) - .setValidityType(randomEle(CouponTemplateValidityTypeEnum.values()).getType()) - .setDiscountType(randomEle(PromotionDiscountTypeEnum.values()).getType()); + o.setTotalCount(10); + o.setTakeLimitCount(2); + o.setTakeType(CouponTakeTypeEnum.USER.getType()); + o.setProductScope(PromotionProductScopeEnum.ALL.getScope()); + o.setProductScopeValues(null); + o.setValidityType(CouponTemplateValidityTypeEnum.TERM.getType()); + o.setFixedStartTerm(0); + o.setFixedEndTerm(7); + o.setDiscountType(PromotionDiscountTypeEnum.PRICE.getType()); + o.setDiscountPrice(100); }); // 调用 @@ -84,16 +107,60 @@ public class CouponTemplateServiceImplTest extends BaseDbUnitTest { @Test public void testUpdateCouponTemplate_notExists() { // 准备参数 - CouponTemplateUpdateReqVO reqVO = randomPojo(CouponTemplateUpdateReqVO.class); + CouponTemplateUpdateReqVO reqVO = randomPojo(CouponTemplateUpdateReqVO.class, o -> o.setId(randomLongId())); // 调用, 并断言异常 assertServiceException(() -> couponTemplateService.updateCouponTemplate(reqVO), COUPON_TEMPLATE_NOT_EXISTS); } @Test + public void testCreateCouponTemplate_takeLimitCountGreaterThanTotalCount() { + // 准备参数 + CouponTemplateCreateReqVO reqVO = randomPojo(CouponTemplateCreateReqVO.class, o -> { + o.setTotalCount(1); + o.setTakeLimitCount(2); + o.setTakeType(CouponTakeTypeEnum.USER.getType()); + o.setProductScope(PromotionProductScopeEnum.ALL.getScope()); + o.setProductScopeValues(null); + o.setValidityType(CouponTemplateValidityTypeEnum.TERM.getType()); + o.setFixedStartTerm(0); + o.setFixedEndTerm(7); + o.setDiscountType(PromotionDiscountTypeEnum.PRICE.getType()); + o.setDiscountPrice(100); + }); + + // 调用,并断言 + assertServiceException(() -> couponTemplateService.createCouponTemplate(reqVO), COUPON_TEMPLATE_NOT_ENOUGH); + } + + @Test + public void testUpdateCouponTemplate_takeLimitCountGreaterThanTotalCount() { + // mock 数据 + CouponTemplateDO couponTemplate = randomCouponTemplateDO(); + couponTemplateMapper.insert(couponTemplate); + // 准备参数 + CouponTemplateUpdateReqVO reqVO = randomPojo(CouponTemplateUpdateReqVO.class, o -> { + o.setId(couponTemplate.getId()); // 设置更新的 ID + o.setTotalCount(1); + o.setTakeLimitCount(2); + o.setTakeType(CouponTakeTypeEnum.USER.getType()); + o.setProductScope(PromotionProductScopeEnum.ALL.getScope()); + o.setProductScopeValues(null); + o.setValidityType(CouponTemplateValidityTypeEnum.TERM.getType()); + o.setFixedStartTerm(0); + o.setFixedEndTerm(7); + o.setDiscountType(PromotionDiscountTypeEnum.PRICE.getType()); + o.setDiscountPrice(100); + }); + + // 调用,并断言 + assertServiceException(() -> couponTemplateService.updateCouponTemplate(reqVO), COUPON_TEMPLATE_NOT_ENOUGH); + } + + @Test public void testDeleteCouponTemplate_success() { // mock 数据 - CouponTemplateDO dbCouponTemplate = randomPojo(CouponTemplateDO.class); + CouponTemplateDO dbCouponTemplate = randomCouponTemplateDO(); couponTemplateMapper.insert(dbCouponTemplate);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbCouponTemplate.getId(); @@ -146,4 +213,25 @@ public class CouponTemplateServiceImplTest extends BaseDbUnitTest { assertPojoEquals(dbCouponTemplate, pageResult.getList().get(0)); } + @SafeVarargs + private static CouponTemplateDO randomCouponTemplateDO(Consumer... consumers) { + Consumer consumer = o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setTotalCount(10); + o.setTakeLimitCount(1); + o.setTakeType(CouponTakeTypeEnum.USER.getType()); + o.setUsePrice(0); + o.setProductScope(PromotionProductScopeEnum.ALL.getScope()); + o.setProductScopeValues(null); + o.setValidityType(CouponTemplateValidityTypeEnum.TERM.getType()); + o.setFixedStartTerm(0); + o.setFixedEndTerm(7); + o.setDiscountType(PromotionDiscountTypeEnum.PRICE.getType()); + o.setDiscountPrice(100); + o.setTakeCount(0); + o.setUseCount(0); + }; + return randomPojo(CouponTemplateDO.class, ArrayUtils.append(consumer, consumers)); + } + } diff --git a/yudao-module-mall/yudao-module-promotion/src/test/resources/sql/create_tables.sql b/yudao-module-mall/yudao-module-promotion/src/test/resources/sql/create_tables.sql index 00ac3f9..5b566ce 100644 --- a/yudao-module-mall/yudao-module-promotion/src/test/resources/sql/create_tables.sql +++ b/yudao-module-mall/yudao-module-promotion/src/test/resources/sql/create_tables.sql @@ -23,13 +23,14 @@ CREATE TABLE IF NOT EXISTS "promotion_coupon_template" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "name" varchar NOT NULL, + "description" varchar, "status" int NOT NULL, "total_count" int NOT NULL, "take_limit_count" int NOT NULL, "take_type" int NOT NULL, "use_price" int NOT NULL, "product_scope" int NOT NULL, - "product_spu_ids" varchar, + "product_scope_values" varchar, "validity_type" int NOT NULL, "valid_start_time" datetime, "valid_end_time" datetime, @@ -57,11 +58,11 @@ CREATE TABLE IF NOT EXISTS "promotion_coupon" "status" int NOT NULL, "user_id" bigint NOT NULL, "take_type" int NOT NULL, - "useprice" int NOT NULL, + "use_price" int NOT NULL, "valid_start_time" datetime NOT NULL, "valid_end_time" datetime NOT NULL, "product_scope" int NOT NULL, - "product_spu_ids" varchar, + "product_scope_values" varchar, "discount_type" int NOT NULL, "discount_percent" int, "discount_price" int, @@ -112,6 +113,27 @@ CREATE TABLE IF NOT EXISTS "promotion_discount_activity" PRIMARY KEY ("id") ) COMMENT '限时折扣活动'; +CREATE TABLE IF NOT EXISTS "promotion_discount_product" +( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "activity_id" bigint NOT NULL, + "spu_id" bigint NOT NULL, + "sku_id" bigint NOT NULL, + "discount_type" int NOT NULL, + "discount_percent" int, + "discount_price" int, + "activity_name" varchar NOT NULL, + "activity_status" int NOT NULL, + "activity_start_time" datetime NOT NULL, + "activity_end_time" datetime NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '限时折扣商品'; + CREATE TABLE IF NOT EXISTS "promotion_seckill_activity" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, @@ -253,4 +275,4 @@ CREATE TABLE IF NOT EXISTS "promotion_diy_page" "deleted" bit NOT NULL DEFAULT FALSE, "tenant_id" bigint NOT NULL, PRIMARY KEY ("id") -) COMMENT '装修页面'; \ No newline at end of file +) COMMENT '装修页面'; -- libgit2 0.21.4