diff --git a/sql/mysql/member_level.sql b/sql/mysql/member_level.sql index abaedb288..1687b842f 100644 --- a/sql/mysql/member_level.sql +++ b/sql/mysql/member_level.sql @@ -29,8 +29,8 @@ create table member_level_log level_id bigint default 0 not null comment '等级编号', level int default 0 not null comment '会员等级', discount int(4) default 100 not null comment '享受折扣', - experience int(4) default 100 not null comment '升级经验', - user_experience int(4) default 100 not null comment '会员此时的经验', + experience int(4) default 0 not null comment '升级经验', + user_experience int(4) default 0 not null comment '会员此时的经验', remark varchar(255) default '' not null comment '备注', description varchar(255) default '' not null comment '描述', creator varchar(64) default '' null comment '创建者', diff --git a/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/ErrorCodeConstants.java b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/ErrorCodeConstants.java index 4e36e53fd..c2b20cea7 100644 --- a/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/ErrorCodeConstants.java +++ b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/ErrorCodeConstants.java @@ -44,6 +44,11 @@ public interface ErrorCodeConstants { //========== 会员等级 1004007000 ========== ErrorCode LEVEL_NOT_EXISTS = new ErrorCode(1004007000, "会员等级不存在"); + ErrorCode LEVEL_NAME_EXISTS = new ErrorCode(1004007001, "会员等级名称[{}]已被使用"); + ErrorCode LEVEL_VALUE_EXISTS = new ErrorCode(1004007002, "会员等级值[{}]已被[{}]使用"); + ErrorCode LEVEL_EXPERIENCE_MIN = new ErrorCode(1004007003, "升级经验必须大于上一个等级[{}]设置的升级经验[{}]"); + ErrorCode LEVEL_EXPERIENCE_MAX = new ErrorCode(1004007004, "升级经验必须小于下一个等级[{}]设置的升级经验[{}]"); + ErrorCode LEVEL_LOG_NOT_EXISTS = new ErrorCode(1004007100, "会员等级记录不存在"); ErrorCode EXPERIENCE_LOG_NOT_EXISTS = new ErrorCode(1004007200, "会员经验记录不存在"); } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/MemberLevelBaseVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/MemberLevelBaseVO.java index 2339f9322..e58d2551e 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/MemberLevelBaseVO.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/MemberLevelBaseVO.java @@ -29,6 +29,7 @@ public class MemberLevelBaseVO { @Schema(description = "等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "等级不能为空") + @Positive(message = "等级必须大于0") private Integer level; @Schema(description = "享受折扣", requiredMode = Schema.RequiredMode.REQUIRED, example = "98") diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberLevelServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberLevelServiceImpl.java index 22383fedd..97b5369f1 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberLevelServiceImpl.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberLevelServiceImpl.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.member.service.level; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.member.controller.admin.level.vo.MemberLevelCreateReqVO; import cn.iocoder.yudao.module.member.controller.admin.level.vo.MemberLevelPageReqVO; @@ -7,6 +9,7 @@ import cn.iocoder.yudao.module.member.controller.admin.level.vo.MemberLevelUpdat import cn.iocoder.yudao.module.member.convert.level.MemberLevelConvert; import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberLevelDO; import cn.iocoder.yudao.module.member.dal.mysql.level.MemberLevelMapper; +import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -16,7 +19,7 @@ import java.util.Collection; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.LEVEL_NOT_EXISTS; +import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.*; /** * 会员等级 Service 实现类 @@ -33,6 +36,9 @@ public class MemberLevelServiceImpl implements MemberLevelService { @Override public Long createLevel(MemberLevelCreateReqVO createReqVO) { + // 校验配置是否有效 + validateConfigValid(null, createReqVO.getName(), createReqVO.getLevel(), createReqVO.getExperience()); + // 插入 MemberLevelDO level = MemberLevelConvert.INSTANCE.convert(createReqVO); levelMapper.insert(level); @@ -44,6 +50,9 @@ public class MemberLevelServiceImpl implements MemberLevelService { public void updateLevel(MemberLevelUpdateReqVO updateReqVO) { // 校验存在 validateLevelExists(updateReqVO.getId()); + // 校验配置是否有效 + validateConfigValid(updateReqVO.getId(), updateReqVO.getName(), updateReqVO.getLevel(), updateReqVO.getExperience()); + // 更新 MemberLevelDO updateObj = MemberLevelConvert.INSTANCE.convert(updateReqVO); levelMapper.updateById(updateObj); @@ -57,10 +66,72 @@ public class MemberLevelServiceImpl implements MemberLevelService { levelMapper.deleteById(id); } - private void validateLevelExists(Long id) { - if (levelMapper.selectById(id) == null) { + @VisibleForTesting + MemberLevelDO validateLevelExists(Long id) { + MemberLevelDO levelDO = levelMapper.selectById(id); + if (levelDO == null) { throw exception(LEVEL_NOT_EXISTS); } + return levelDO; + } + + @VisibleForTesting + void validateNameUnique(List list, Long id, String name) { + for (MemberLevelDO levelDO : list) { + if (ObjUtil.notEqual(levelDO.getName(), name)) { + continue; + } + + if (id == null || !id.equals(levelDO.getId())) { + throw exception(LEVEL_NAME_EXISTS, levelDO.getName()); + } + } + } + + @VisibleForTesting + void validateLevelUnique(List list, Long id, Integer level) { + for (MemberLevelDO levelDO : list) { + if (ObjUtil.notEqual(levelDO.getLevel(), level)) { + continue; + } + + if (id == null || !id.equals(levelDO.getId())) { + throw exception(LEVEL_VALUE_EXISTS, levelDO.getLevel(), levelDO.getName()); + } + } + } + + @VisibleForTesting + void validateExperienceOutRange(List list, Long id, Integer level, Integer experience) { + for (MemberLevelDO levelDO : list) { + if (levelDO.getId().equals(id)) { + continue; + } + + if (levelDO.getLevel() < level) { + // 经验大于前一个等级 + if (experience <= levelDO.getExperience()) { + throw exception(LEVEL_EXPERIENCE_MIN, levelDO.getName(), levelDO.getExperience()); + } + } else if (levelDO.getLevel() > level) { + //小于下一个级别 + if (experience >= levelDO.getExperience()) { + throw exception(LEVEL_EXPERIENCE_MAX, levelDO.getName(), levelDO.getExperience()); + } + } + } + } + + @VisibleForTesting + void validateConfigValid(Long id, String name, Integer level, Integer experience) { + List list = levelMapper.selectList(); + + // 校验名称唯一 + validateNameUnique(list, id, name); + // 校验等级唯一 + validateLevelUnique(list, id, level); + // 校验升级所需经验是否有效: 大于前一个等级,小于下一个级别 + validateExperienceOutRange(list, id, level, experience); } @Override diff --git a/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/level/MemberLevelServiceImplTest.java b/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/level/MemberLevelServiceImplTest.java index 94349da45..c85a62164 100644 --- a/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/level/MemberLevelServiceImplTest.java +++ b/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/level/MemberLevelServiceImplTest.java @@ -1,6 +1,8 @@ package cn.iocoder.yudao.module.member.service.level; +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.member.controller.admin.level.vo.MemberLevelCreateReqVO; import cn.iocoder.yudao.module.member.controller.admin.level.vo.MemberLevelPageReqVO; @@ -8,16 +10,19 @@ import cn.iocoder.yudao.module.member.controller.admin.level.vo.MemberLevelUpdat import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberLevelDO; import cn.iocoder.yudao.module.member.dal.mysql.level.MemberLevelMapper; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import javax.annotation.Resource; +import java.util.List; +import java.util.function.Consumer; +import static cn.hutool.core.util.RandomUtil.randomInt; 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.member.enums.ErrorCodeConstants.LEVEL_NOT_EXISTS; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.*; /** @@ -34,10 +39,19 @@ public class MemberLevelServiceImplTest extends BaseDbUnitTest { @Resource private MemberLevelMapper levelMapper; + @MockBean + private MemberLevelLogService memberLevelLogService; + @MockBean + private MemberExperienceLogService memberExperienceLogService; + @Test public void testCreateLevel_success() { // 准备参数 - MemberLevelCreateReqVO reqVO = randomPojo(MemberLevelCreateReqVO.class); + MemberLevelCreateReqVO reqVO = randomPojo(MemberLevelCreateReqVO.class, o -> { + o.setDiscount(randomInt()); + o.setIcon(randomURL()); + o.setBackgroundUrl(randomURL()); + }); // 调用 Long levelId = levelService.createLevel(reqVO); @@ -56,6 +70,14 @@ public class MemberLevelServiceImplTest extends BaseDbUnitTest { // 准备参数 MemberLevelUpdateReqVO reqVO = randomPojo(MemberLevelUpdateReqVO.class, o -> { o.setId(dbLevel.getId()); // 设置更新的 ID + //以下要保持一致 + o.setName(dbLevel.getName()); + o.setLevel(dbLevel.getLevel()); + o.setExperience(dbLevel.getExperience()); + //以下是要修改的字段 + o.setDiscount(randomInt()); + o.setIcon(randomURL()); + o.setBackgroundUrl(randomURL()); }); // 调用 @@ -106,9 +128,9 @@ public class MemberLevelServiceImplTest extends BaseDbUnitTest { }); levelMapper.insert(dbLevel); // 测试 name 不匹配 - levelMapper.insert(cloneIgnoreId(dbLevel, o -> o.setName(null))); + levelMapper.insert(cloneIgnoreId(dbLevel, o -> o.setName(""))); // 测试 status 不匹配 - levelMapper.insert(cloneIgnoreId(dbLevel, o -> o.setStatus(null))); + levelMapper.insert(cloneIgnoreId(dbLevel, o -> o.setStatus(0))); // 准备参数 MemberLevelPageReqVO reqVO = new MemberLevelPageReqVO(); reqVO.setName("黄金会员"); @@ -122,4 +144,121 @@ public class MemberLevelServiceImplTest extends BaseDbUnitTest { assertPojoEquals(dbLevel, pageResult.getList().get(0)); } + @Test + public void testCreateLevel_nameUnique() { + // 准备参数 + String name = randomString(); + + // mock 数据 + levelMapper.insert(randomLevelDO(o -> o.setName(name))); + + // 调用,校验异常 + List list = levelMapper.selectList(); + assertServiceException(() -> levelService.validateNameUnique(list, null, name), LEVEL_NAME_EXISTS, name); + } + + @Test + public void testUpdateLevel_nameUnique() { + // 准备参数 + Long id = randomLongId(); + String name = randomString(); + + // mock 数据 + levelMapper.insert(randomLevelDO(o -> o.setName(name))); + + // 调用,校验异常 + List list = levelMapper.selectList(); + assertServiceException(() -> levelService.validateNameUnique(list, id, name), LEVEL_NAME_EXISTS, name); + } + + @Test + public void testCreateLevel_levelUnique() { + // 准备参数 + Integer level = randomInteger(); + String name = randomString(); + + // mock 数据 + levelMapper.insert(randomLevelDO(o -> { + o.setLevel(level); + o.setName(name); + })); + + // 调用,校验异常 + List list = levelMapper.selectList(); + assertServiceException(() -> levelService.validateLevelUnique(list, null, level), LEVEL_VALUE_EXISTS, level, name); + } + + @Test + public void testUpdateLevel_levelUnique() { + // 准备参数 + Long id = randomLongId(); + Integer level = randomInteger(); + String name = randomString(); + + // mock 数据 + levelMapper.insert(randomLevelDO(o -> { + o.setLevel(level); + o.setName(name); + })); + + // 调用,校验异常 + List list = levelMapper.selectList(); + assertServiceException(() -> levelService.validateLevelUnique(list, id, level), LEVEL_VALUE_EXISTS, level, name); + } + + @Test + public void testCreateLevel_experienceOutRange() { + // 准备参数 + int level = 10; + int experience = 10; + String name = randomString(); + + // mock 数据 + levelMapper.insert(randomLevelDO(o -> { + o.setLevel(level); + o.setExperience(experience); + o.setName(name); + })); + List list = levelMapper.selectList(); + + // 调用,校验异常 + assertServiceException(() -> levelService.validateExperienceOutRange(list, null, level + 1, experience - 1), LEVEL_EXPERIENCE_MIN, name, level); + // 调用,校验异常 + assertServiceException(() -> levelService.validateExperienceOutRange(list, null, level - 1, experience + 1), LEVEL_EXPERIENCE_MAX, name, level); + } + + @Test + public void testUpdateLevel_experienceOutRange() { + // 准备参数 + int level = 10; + int experience = 10; + Long id = randomLongId(); + String name = randomString(); + + // mock 数据 + levelMapper.insert(randomLevelDO(o -> { + o.setLevel(level); + o.setExperience(experience); + o.setName(name); + })); + List list = levelMapper.selectList(); + + // 调用,校验异常 + assertServiceException(() -> levelService.validateExperienceOutRange(list, id, level + 1, experience - 1), LEVEL_EXPERIENCE_MIN, name, level); + // 调用,校验异常 + assertServiceException(() -> levelService.validateExperienceOutRange(list, id, level - 1, experience + 1), LEVEL_EXPERIENCE_MAX, name, level); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static MemberLevelDO randomLevelDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setDiscount(randomInt(0, 100)); + o.setIcon(randomURL()); + o.setBackgroundUrl(randomURL()); + }; + return randomPojo(MemberLevelDO.class, ArrayUtils.append(consumer, consumers)); + } } diff --git a/yudao-module-member/yudao-module-member-biz/src/test/resources/sql/create_tables.sql b/yudao-module-member/yudao-module-member-biz/src/test/resources/sql/create_tables.sql index 7769478ba..a497a2471 100644 --- a/yudao-module-member/yudao-module-member-biz/src/test/resources/sql/create_tables.sql +++ b/yudao-module-member/yudao-module-member-biz/src/test/resources/sql/create_tables.sql @@ -48,19 +48,19 @@ CREATE TABLE IF NOT EXISTS "member_tag" CREATE TABLE IF NOT EXISTS "member_level" ( - "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, - "name" varchar NOT NULL, - "experience" int NOT NULL, - "value" int NOT NULL, - "discount" int NOT NULL, - "icon" varchar NOT NULL, - "bg_url" varchar 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, - "tenant_id" bigint not null default '0', - "status" int NOT NULL, + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "experience" int NOT NULL, + "level" int NOT NULL, + "discount" int NOT NULL, + "icon" varchar NOT NULL, + "background_url" varchar 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, + "tenant_id" bigint not null default '0', + "status" int NOT NULL, PRIMARY KEY ("id") ) COMMENT '会员等级'; \ No newline at end of file