Ver Fonte

!577 会员:修改会员标签名称唯一校验逻辑
Merge pull request !577 from 疯狂的世界/member_dev

芋道源码 há 1 ano atrás
pai
commit
4cd5b5712b
56 ficheiros alterados com 2534 adições e 16 exclusões
  1. 94 0
      sql/mysql/member_level.sql
  2. 21 0
      yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/api/level/MemberLevelApi.java
  3. 15 0
      yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/DictTypeConstants.java
  4. 11 0
      yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/ErrorCodeConstants.java
  5. 28 0
      yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberExperienceBizTypeEnum.java
  6. 25 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/api/level/MemberLevelApiImpl.java
  7. 90 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/MemberExperienceLogController.java
  8. 94 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/MemberLevelController.java
  9. 90 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/MemberLevelLogController.java
  10. 52 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/MemberLevelBaseVO.java
  11. 17 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/MemberLevelCreateReqVO.java
  12. 24 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/MemberLevelPageReqVO.java
  13. 25 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/MemberLevelRespVO.java
  14. 26 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/MemberLevelSimpleRespVO.java
  15. 23 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/MemberLevelUpdateReqVO.java
  16. 45 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/experience/MemberExperienceLogBaseVO.java
  17. 48 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/experience/MemberExperienceLogExcelVO.java
  18. 34 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/experience/MemberExperienceLogExportReqVO.java
  19. 39 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/experience/MemberExperienceLogPageReqVO.java
  20. 25 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/experience/MemberExperienceLogRespVO.java
  21. 49 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/log/MemberLevelLogBaseVO.java
  22. 46 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/log/MemberLevelLogExcelVO.java
  23. 28 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/log/MemberLevelLogExportReqVO.java
  24. 33 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/log/MemberLevelLogPageReqVO.java
  25. 25 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/log/MemberLevelLogRespVO.java
  26. 9 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/tag/MemberTagController.java
  27. 9 1
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java
  28. 3 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserBaseVO.java
  29. 4 1
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserPageReqVO.java
  30. 3 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserRespVO.java
  31. 3 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserUpdateReqVO.java
  32. 30 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/convert/level/MemberExperienceLogConvert.java
  33. 35 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/convert/level/MemberLevelConvert.java
  34. 30 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/convert/level/MemberLevelLogConvert.java
  35. 11 1
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/convert/user/MemberUserConvert.java
  36. 61 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/level/MemberExperienceLogDO.java
  37. 61 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/level/MemberLevelDO.java
  38. 62 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/level/MemberLevelLogDO.java
  39. 10 1
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/user/MemberUserDO.java
  40. 41 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/level/MemberExperienceLogMapper.java
  41. 37 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/level/MemberLevelLogMapper.java
  42. 33 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/level/MemberLevelMapper.java
  43. 2 4
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/tag/MemberTagMapper.java
  44. 14 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapper.java
  45. 77 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberExperienceLogService.java
  46. 86 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberExperienceLogServiceImpl.java
  47. 84 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberLevelLogService.java
  48. 115 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberLevelLogServiceImpl.java
  49. 107 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberLevelService.java
  50. 279 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberLevelServiceImpl.java
  51. 6 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/tag/MemberTagService.java
  52. 19 4
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/tag/MemberTagServiceImpl.java
  53. 10 2
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java
  54. 264 0
      yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/level/MemberLevelServiceImplTest.java
  55. 2 1
      yudao-module-member/yudao-module-member-biz/src/test/resources/sql/clean.sql
  56. 20 1
      yudao-module-member/yudao-module-member-biz/src/test/resources/sql/create_tables.sql

+ 94 - 0
sql/mysql/member_level.sql

@@ -0,0 +1,94 @@
+-- 会员表增加字段
+alter table member_user add column experience int not null default 0 comment '经验';
+alter table member_user add column level_id bigint comment '等级编号';
+
+-- 增加3张表
+create table member_level
+(
+    id             bigint auto_increment comment '编号' primary key,
+    name           varchar(30)  default ''                not null comment '等级名称',
+    experience     int          default 0                 not null comment '升级经验',
+    level          int          default 0                 not null comment '等级',
+    discount       tinyint      default 100               not null comment '享受折扣',
+    icon           varchar(255) default ''                not null comment '等级图标',
+    background_url varchar(255) default ''                not null comment '等级背景图',
+    status         tinyint      default 0                 not null comment '状态',
+    creator        varchar(64)  default ''                null comment '创建者',
+    create_time    datetime     default CURRENT_TIMESTAMP not null comment '创建时间',
+    updater        varchar(64)  default ''                null comment '更新者',
+    update_time    datetime     default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
+    deleted        bit          default b'0'              not null comment '是否删除',
+    tenant_id      bigint       default 0                 not null comment '租户编号'
+)
+    comment '会员等级';
+
+create table member_level_log
+(
+    id              bigint auto_increment comment '编号' primary key,
+    user_id         bigint       default 0                 not null comment '用户编号',
+    level_id        bigint       default 0                 not null comment '等级编号',
+    level           int          default 0                 not null comment '会员等级',
+    discount        tinyint      default 100               not null comment '享受折扣',
+    experience      int          default 0                 not null comment '升级经验',
+    user_experience int          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 '创建者',
+    create_time     datetime     default CURRENT_TIMESTAMP not null comment '创建时间',
+    updater         varchar(64)  default ''                null comment '更新者',
+    update_time     datetime     default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
+    deleted         bit          default b'0'              not null comment '是否删除',
+    tenant_id       bigint       default 0                 not null comment '租户编号'
+)
+    comment '会员等级记录';
+
+create index idx_user_id on member_level_log (user_id) comment '会员等级记录-用户编号';
+
+create table member_experience_log
+(
+    id               bigint auto_increment comment '编号' primary key,
+    user_id          bigint       default 0                 not null comment '用户编号',
+    biz_id           varchar(64)  default ''                not null comment '业务编号',
+    biz_type         tinyint      default 0                 not null comment '业务类型',
+    title            varchar(30)  default ''                not null comment '标题',
+    experience       int          default 0                 not null comment '经验',
+    total_experience int          default 0                 not null comment '变更后的经验',
+    description      varchar(512) default ''                not null comment '描述',
+    creator          varchar(64)  default ''                null comment '创建者',
+    create_time      datetime     default CURRENT_TIMESTAMP not null comment '创建时间',
+    updater          varchar(64)  default ''                null comment '更新者',
+    update_time      datetime     default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
+    deleted          bit          default b'0'              not null comment '是否删除',
+    tenant_id        bigint       default 0                 not null comment '租户编号'
+)
+    comment '会员经验记录';
+
+create index idx_user_id on member_experience_log (user_id) comment '会员经验记录-用户编号';
+create index idx_user_biz_type on member_experience_log (user_id, biz_type) comment '会员经验记录-用户业务类型';
+
+-- 增加字典
+insert system_dict_type(name, type) values ('会员经验业务类型', 'member_experience_biz_type');
+insert system_dict_data(dict_type, label, value, sort) values ('member_experience_biz_type', '管理员调整', '0', 0);
+insert system_dict_data(dict_type, label, value, sort) values ('member_experience_biz_type', '邀新奖励', '1', 1);
+insert system_dict_data(dict_type, label, value, sort) values ('member_experience_biz_type', '下单奖励', '2', 2);
+insert system_dict_data(dict_type, label, value, sort) values ('member_experience_biz_type', '退单扣除', '3', 3);
+insert system_dict_data(dict_type, label, value, sort) values ('member_experience_biz_type', '签到奖励', '4', 4);
+insert system_dict_data(dict_type, label, value, sort) values ('member_experience_biz_type', '抽奖奖励', '5', 5);
+
+-- 菜单 SQL
+INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status, component_name)
+VALUES ('会员等级', '', 2, 3, 2262, 'level', '', 'member/level/index', 0, 'MemberLevel');
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
+VALUES ('会员等级查询', 'member:level:query', 3, 1, @parentId, '', '', '', 0);
+INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
+VALUES ('会员等级创建', 'member:level:create', 3, 2, @parentId, '', '', '', 0);
+INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
+VALUES ('会员等级更新', 'member:level:update', 3, 3, @parentId, '', '', '', 0);
+INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
+VALUES ('会员等级删除', 'member:level:delete', 3, 4, @parentId, '', '', '', 0);

+ 21 - 0
yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/api/level/MemberLevelApi.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.member.api.level;
+
+import cn.iocoder.yudao.module.member.enums.MemberExperienceBizTypeEnum;
+
+/**
+ * 会员等级 API接口
+ *
+ * @author owen
+ */
+public interface MemberLevelApi {
+
+    /**
+     * 增加会员经验
+     *
+     * @param userId     会员ID
+     * @param experience 经验
+     * @param bizType    业务类型
+     * @param bizId      业务编号
+     */
+    void plusExperience(Long userId, Integer experience, MemberExperienceBizTypeEnum bizType, String bizId);
+}

+ 15 - 0
yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/DictTypeConstants.java

@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.module.member.enums;
+
+/**
+ * Member 字典类型的枚举类
+ *
+ * @author owen
+ */
+public interface DictTypeConstants {
+
+    /**
+     * 会员经验记录 - 业务类型
+     */
+    String MEMBER_EXPERIENCE_BIZ_TYPE = "member_experience_biz_type";
+
+}

+ 11 - 0
yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/ErrorCodeConstants.java

@@ -38,4 +38,15 @@ public interface ErrorCodeConstants {
 
     //========== 签到配置 1004010000 ==========
 
+
+    //========== 会员等级 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, "会员经验记录不存在");
+    ErrorCode LEVEL_REASON_NOT_EXISTS = new ErrorCode(1004007300, "会员等级调整原因不能为空");
 }

+ 28 - 0
yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberExperienceBizTypeEnum.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.member.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 会员经验 - 业务类型
+ *
+ * @author owen
+ */
+@Getter
+@AllArgsConstructor
+public enum MemberExperienceBizTypeEnum {
+    /**
+     * 管理员调整、邀请新用户、下单、退单、签到、抽奖
+     */
+    ADMIN(0, "管理员调整","管理员调整获得{}经验"),
+    INVITE_REGISTER(1, "邀新奖励","邀请好友获得{}经验"),
+    ORDER(2, "下单奖励", "下单获得{}经验"),
+    REFUND(3, "退单扣除","退单获得{}经验"),
+    SIGN_IN(4, "签到奖励","签到获得{}经验"),
+    LOTTERY(5, "抽奖奖励","抽奖获得{}经验"),
+    ;
+
+    private final int value;
+    private final String title;
+    private final String desc;
+}

+ 25 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/api/level/MemberLevelApiImpl.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.member.api.level;
+
+import cn.iocoder.yudao.module.member.enums.MemberExperienceBizTypeEnum;
+import cn.iocoder.yudao.module.member.service.level.MemberLevelService;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+
+/**
+ * 会员等级 API 实现类
+ *
+ * @author owen
+ */
+@Service
+@Validated
+public class MemberLevelApiImpl implements MemberLevelApi {
+
+    @Resource
+    private MemberLevelService memberLevelService;
+
+    public void plusExperience(Long userId, Integer experience, MemberExperienceBizTypeEnum bizType, String bizId) {
+        memberLevelService.plusExperience(userId, experience, bizType, bizId);
+    }
+}

+ 90 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/MemberExperienceLogController.java

@@ -0,0 +1,90 @@
+package cn.iocoder.yudao.module.member.controller.admin.level;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.experience.MemberExperienceLogExcelVO;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.experience.MemberExperienceLogExportReqVO;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.experience.MemberExperienceLogPageReqVO;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.experience.MemberExperienceLogRespVO;
+import cn.iocoder.yudao.module.member.convert.level.MemberExperienceLogConvert;
+import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberExperienceLogDO;
+import cn.iocoder.yudao.module.member.service.level.MemberExperienceLogService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+/**
+ * @author owen
+ */
+@Tag(name = "管理后台 - 会员经验记录")
+@RestController
+@RequestMapping("/member/experience-log")
+@Validated
+public class MemberExperienceLogController {
+
+    @Resource
+    private MemberExperienceLogService experienceLogService;
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除会员经验记录")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('member:experience-log:delete')")
+    public CommonResult<Boolean> deleteExperienceLog(@RequestParam("id") Long id) {
+        experienceLogService.deleteExperienceLog(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得会员经验记录")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('member:experience-log:query')")
+    public CommonResult<MemberExperienceLogRespVO> getExperienceLog(@RequestParam("id") Long id) {
+        MemberExperienceLogDO experienceLog = experienceLogService.getExperienceLog(id);
+        return success(MemberExperienceLogConvert.INSTANCE.convert(experienceLog));
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "获得会员经验记录列表")
+    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
+    @PreAuthorize("@ss.hasPermission('member:experience-log:query')")
+    public CommonResult<List<MemberExperienceLogRespVO>> getExperienceLogList(@RequestParam("ids") Collection<Long> ids) {
+        List<MemberExperienceLogDO> list = experienceLogService.getExperienceLogList(ids);
+        return success(MemberExperienceLogConvert.INSTANCE.convertList(list));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得会员经验记录分页")
+    @PreAuthorize("@ss.hasPermission('member:experience-log:query')")
+    public CommonResult<PageResult<MemberExperienceLogRespVO>> getExperienceLogPage(@Valid MemberExperienceLogPageReqVO pageVO) {
+        PageResult<MemberExperienceLogDO> pageResult = experienceLogService.getExperienceLogPage(pageVO);
+        return success(MemberExperienceLogConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出会员经验记录 Excel")
+    @PreAuthorize("@ss.hasPermission('member:experience-log:export')")
+    @OperateLog(type = EXPORT)
+    public void exportExperienceLogExcel(@Valid MemberExperienceLogExportReqVO exportReqVO,
+                                         HttpServletResponse response) throws IOException {
+        List<MemberExperienceLogDO> list = experienceLogService.getExperienceLogList(exportReqVO);
+        // 导出 Excel
+        List<MemberExperienceLogExcelVO> datas = MemberExperienceLogConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "会员经验记录.xls", "数据", MemberExperienceLogExcelVO.class, datas);
+    }
+
+}

+ 94 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/MemberLevelController.java

@@ -0,0 +1,94 @@
+package cn.iocoder.yudao.module.member.controller.admin.level;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.*;
+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.service.level.MemberLevelService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+/**
+ * @author owen
+ */
+@Tag(name = "管理后台 - 会员等级")
+@RestController
+@RequestMapping("/member/level")
+@Validated
+public class MemberLevelController {
+
+    @Resource
+    private MemberLevelService levelService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建会员等级")
+    @PreAuthorize("@ss.hasPermission('member:level:create')")
+    public CommonResult<Long> createLevel(@Valid @RequestBody MemberLevelCreateReqVO createReqVO) {
+        return success(levelService.createLevel(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新会员等级")
+    @PreAuthorize("@ss.hasPermission('member:level:update')")
+    public CommonResult<Boolean> updateLevel(@Valid @RequestBody MemberLevelUpdateReqVO updateReqVO) {
+        levelService.updateLevel(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除会员等级")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('member:level:delete')")
+    public CommonResult<Boolean> deleteLevel(@RequestParam("id") Long id) {
+        levelService.deleteLevel(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得会员等级")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('member:level:query')")
+    public CommonResult<MemberLevelRespVO> getLevel(@RequestParam("id") Long id) {
+        MemberLevelDO level = levelService.getLevel(id);
+        return success(MemberLevelConvert.INSTANCE.convert(level));
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "获得会员等级列表")
+    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
+    @PreAuthorize("@ss.hasPermission('member:level:query')")
+    public CommonResult<List<MemberLevelRespVO>> getLevelList(@RequestParam("ids") Collection<Long> ids) {
+        List<MemberLevelDO> list = levelService.getLevelList(ids);
+        return success(MemberLevelConvert.INSTANCE.convertList(list));
+    }
+
+    @GetMapping("/list-all-simple")
+    @Operation(summary = "获取会员等级精简信息列表", description = "只包含被开启的会员等级,主要用于前端的下拉选项")
+    public CommonResult<List<MemberLevelSimpleRespVO>> getSimpleLevelList() {
+        // 获用户列表,只要开启状态的
+        List<MemberLevelDO> list = levelService.getEnableLevelList();
+        // 排序后,返回给前端
+        return success(MemberLevelConvert.INSTANCE.convertSimpleList(list));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得会员等级分页")
+    @PreAuthorize("@ss.hasPermission('member:level:query')")
+    public CommonResult<PageResult<MemberLevelRespVO>> getLevelPage(@Valid MemberLevelPageReqVO pageVO) {
+        PageResult<MemberLevelDO> pageResult = levelService.getLevelPage(pageVO);
+        return success(MemberLevelConvert.INSTANCE.convertPage(pageResult));
+    }
+
+}

+ 90 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/MemberLevelLogController.java

@@ -0,0 +1,90 @@
+package cn.iocoder.yudao.module.member.controller.admin.level;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.log.MemberLevelLogExcelVO;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.log.MemberLevelLogExportReqVO;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.log.MemberLevelLogPageReqVO;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.log.MemberLevelLogRespVO;
+import cn.iocoder.yudao.module.member.convert.level.MemberLevelLogConvert;
+import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberLevelLogDO;
+import cn.iocoder.yudao.module.member.service.level.MemberLevelLogService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+/**
+ * @author owen
+ */
+@Tag(name = "管理后台 - 会员等级记录")
+@RestController
+@RequestMapping("/member/level-log")
+@Validated
+public class MemberLevelLogController {
+
+    @Resource
+    private MemberLevelLogService levelLogService;
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除会员等级记录")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('member:level-log:delete')")
+    public CommonResult<Boolean> deleteLevelLog(@RequestParam("id") Long id) {
+        levelLogService.deleteLevelLog(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得会员等级记录")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('member:level-log:query')")
+    public CommonResult<MemberLevelLogRespVO> getLevelLog(@RequestParam("id") Long id) {
+        MemberLevelLogDO levelLog = levelLogService.getLevelLog(id);
+        return success(MemberLevelLogConvert.INSTANCE.convert(levelLog));
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "获得会员等级记录列表")
+    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
+    @PreAuthorize("@ss.hasPermission('member:level-log:query')")
+    public CommonResult<List<MemberLevelLogRespVO>> getLevelLogList(@RequestParam("ids") Collection<Long> ids) {
+        List<MemberLevelLogDO> list = levelLogService.getLevelLogList(ids);
+        return success(MemberLevelLogConvert.INSTANCE.convertList(list));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得会员等级记录分页")
+    @PreAuthorize("@ss.hasPermission('member:level-log:query')")
+    public CommonResult<PageResult<MemberLevelLogRespVO>> getLevelLogPage(@Valid MemberLevelLogPageReqVO pageVO) {
+        PageResult<MemberLevelLogDO> pageResult = levelLogService.getLevelLogPage(pageVO);
+        return success(MemberLevelLogConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出会员等级记录 Excel")
+    @PreAuthorize("@ss.hasPermission('member:level-log:export')")
+    @OperateLog(type = EXPORT)
+    public void exportLevelLogExcel(@Valid MemberLevelLogExportReqVO exportReqVO,
+                                    HttpServletResponse response) throws IOException {
+        List<MemberLevelLogDO> list = levelLogService.getLevelLogList(exportReqVO);
+        // 导出 Excel
+        List<MemberLevelLogExcelVO> datas = MemberLevelLogConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "会员等级记录.xls", "数据", MemberLevelLogExcelVO.class, datas);
+    }
+
+}

+ 52 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/MemberLevelBaseVO.java

@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.member.controller.admin.level.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.hibernate.validator.constraints.Range;
+import org.hibernate.validator.constraints.URL;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Positive;
+
+/**
+ * 会员等级 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ *
+ * @author owen
+ */
+@Data
+public class MemberLevelBaseVO {
+
+    @Schema(description = "等级名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
+    @NotBlank(message = "等级名称不能为空")
+    private String name;
+
+    @Schema(description = "升级经验", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
+    @NotNull(message = "升级经验不能为空")
+    @Positive(message = "升级经验必须大于0")
+    private Integer experience;
+
+    @Schema(description = "等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "等级不能为空")
+    @Positive(message = "等级必须大于0")
+    private Integer level;
+
+    @Schema(description = "享受折扣", requiredMode = Schema.RequiredMode.REQUIRED, example = "98")
+    @NotNull(message = "享受折扣不能为空")
+    @Range(min = 0, max = 100, message = "享受折扣的范围为0-100")
+    private Integer discount;
+
+    @Schema(description = "等级图标", example = "https://www.iocoder.cn/yudao.jpg")
+    @URL(message = "等级图标 必须是 URL 格式")
+    private String icon;
+
+    @Schema(description = "等级背景图", example = "https://www.iocoder.cn/yudao.jpg")
+    @URL(message = "等级背景图 必须是 URL 格式")
+    private String backgroundUrl;
+
+    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "状态不能为空")
+    private Integer status;
+
+}

+ 17 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/MemberLevelCreateReqVO.java

@@ -0,0 +1,17 @@
+package cn.iocoder.yudao.module.member.controller.admin.level.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+/**
+ * @author owen
+ */
+@Schema(description = "管理后台 - 会员等级创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MemberLevelCreateReqVO extends MemberLevelBaseVO {
+
+}

+ 24 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/MemberLevelPageReqVO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.member.controller.admin.level.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+/**
+ * @author owen
+ */
+@Schema(description = "管理后台 - 会员等级分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MemberLevelPageReqVO extends PageParam {
+
+    @Schema(description = "等级名称", example = "芋艿")
+    private String name;
+
+    @Schema(description = "状态", example = "1")
+    private Integer status;
+
+}

+ 25 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/MemberLevelRespVO.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.member.controller.admin.level.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author owen
+ */
+@Schema(description = "管理后台 - 会员等级 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MemberLevelRespVO extends MemberLevelBaseVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6103")
+    private Long id;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

+ 26 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/MemberLevelSimpleRespVO.java

@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.member.controller.admin.level.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+/**
+ * @author owen
+ */
+@Schema(description = "管理后台 - 会员等级 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MemberLevelSimpleRespVO extends MemberLevelBaseVO {
+
+    @Schema(description = "编号", example = "6103")
+    private Long id;
+
+    @Schema(description = "等级名称", example = "芋艿")
+    private String name;
+
+    @Schema(description = "等级图标", example = "https://www.iocoder.cn/yudao.jpg")
+    private String icon;
+
+}

+ 23 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/MemberLevelUpdateReqVO.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.member.controller.admin.level.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * @author owen
+ */
+@Schema(description = "管理后台 - 会员等级更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MemberLevelUpdateReqVO extends MemberLevelBaseVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6103")
+    @NotNull(message = "编号不能为空")
+    private Long id;
+
+}

+ 45 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/experience/MemberExperienceLogBaseVO.java

@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.member.controller.admin.level.vo.experience;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 会员经验记录 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ *
+ * @author owen
+ */
+@Data
+public class MemberExperienceLogBaseVO {
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3638")
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+
+    @Schema(description = "业务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12164")
+    @NotNull(message = "业务编号不能为空")
+    private String bizId;
+
+    @Schema(description = "业务类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "业务类型不能为空")
+    private Integer bizType;
+
+    @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "增加经验")
+    @NotNull(message = "标题不能为空")
+    private String title;
+
+    @Schema(description = "经验", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
+    @NotNull(message = "经验不能为空")
+    private Integer experience;
+
+    @Schema(description = "变更后的经验", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
+    @NotNull(message = "变更后的经验不能为空")
+    private Integer totalExperience;
+
+    @Schema(description = "描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "下单增加100经验")
+    @NotNull(message = "描述不能为空")
+    private String description;
+
+}

+ 48 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/experience/MemberExperienceLogExcelVO.java

@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.module.member.controller.admin.level.vo.experience;
+
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+import cn.iocoder.yudao.module.member.enums.DictTypeConstants;
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+
+/**
+ * 会员经验记录 Excel VO
+ *
+ * @author owen
+ */
+@Data
+public class MemberExperienceLogExcelVO {
+
+    @ExcelProperty("编号")
+    private Long id;
+
+    @ExcelProperty("用户编号")
+    private Long userId;
+
+    @ExcelProperty(value = "业务类型", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.MEMBER_EXPERIENCE_BIZ_TYPE)
+    private Integer bizType;
+
+    @ExcelProperty("业务编号")
+    private String bizId;
+
+    @ExcelProperty("标题")
+    private String title;
+
+    @ExcelProperty("经验")
+    private Integer experience;
+
+    @ExcelProperty("变更后的经验")
+    private Integer totalExperience;
+
+    @ExcelProperty("描述")
+    private String description;
+
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 34 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/experience/MemberExperienceLogExportReqVO.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.member.controller.admin.level.vo.experience;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * @author owen
+ */
+@Schema(description = "管理后台 - 会员经验记录 Excel 导出 Request VO,参数和 MemberExperienceLogPageReqVO 是一致的")
+@Data
+public class MemberExperienceLogExportReqVO {
+
+    @Schema(description = "用户编号", example = "3638")
+    private Long userId;
+
+    @Schema(description = "业务类型", example = "1")
+    private Integer bizType;
+
+    @Schema(description = "业务编号", example = "12164")
+    private String bizId;
+
+    @Schema(description = "标题", example = "增加经验")
+    private String title;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 39 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/experience/MemberExperienceLogPageReqVO.java

@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.module.member.controller.admin.level.vo.experience;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * @author owen
+ */
+@Schema(description = "管理后台 - 会员经验记录分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MemberExperienceLogPageReqVO extends PageParam {
+
+    @Schema(description = "用户编号", example = "3638")
+    private Long userId;
+
+    @Schema(description = "业务编号", example = "12164")
+    private String bizId;
+
+    @Schema(description = "业务类型", example = "1")
+    private Integer bizType;
+
+    @Schema(description = "标题", example = "增加经验")
+    private String title;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 25 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/experience/MemberExperienceLogRespVO.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.member.controller.admin.level.vo.experience;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author owen
+ */
+@Schema(description = "管理后台 - 会员经验记录 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MemberExperienceLogRespVO extends MemberExperienceLogBaseVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "19610")
+    private Long id;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

+ 49 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/log/MemberLevelLogBaseVO.java

@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.member.controller.admin.level.vo.log;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 会员等级记录 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ *
+ * @author owen
+ */
+@Data
+public class MemberLevelLogBaseVO {
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25923")
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+
+    @Schema(description = "等级编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25985")
+    @NotNull(message = "等级编号不能为空")
+    private Long levelId;
+
+    @Schema(description = "会员等级", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "会员等级不能为空")
+    private Integer level;
+
+    @Schema(description = "享受折扣", requiredMode = Schema.RequiredMode.REQUIRED, example = "13319")
+    @NotNull(message = "享受折扣不能为空")
+    private Integer discount;
+
+    @Schema(description = "升级经验", requiredMode = Schema.RequiredMode.REQUIRED, example = "13319")
+    @NotNull(message = "升级经验不能为空")
+    private Integer experience;
+
+    @Schema(description = "会员此时的经验", requiredMode = Schema.RequiredMode.REQUIRED, example = "13319")
+    @NotNull(message = "会员此时的经验不能为空")
+    private Integer userExperience;
+
+    @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "推广需要")
+    @NotNull(message = "备注不能为空")
+    private String remark;
+
+    @Schema(description = "描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "升级为金牌会员")
+    @NotNull(message = "描述不能为空")
+    private String description;
+
+}

+ 46 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/log/MemberLevelLogExcelVO.java

@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.member.controller.admin.level.vo.log;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 会员等级记录 Excel VO
+ *
+ * @author owen
+ */
+@Data
+public class MemberLevelLogExcelVO {
+
+    @ExcelProperty("编号")
+    private Long id;
+
+    @ExcelProperty("用户编号")
+    private Long userId;
+
+    @ExcelProperty("等级编号")
+    private Long levelId;
+
+    @ExcelProperty("会员等级")
+    private Integer level;
+
+    @ExcelProperty("享受折扣")
+    private Integer discount;
+
+    @ExcelProperty("升级经验")
+    private Integer experience;
+
+    @ExcelProperty("会员此时的经验")
+    private Integer userExperience;
+
+    @ExcelProperty("备注")
+    private String remark;
+
+    @ExcelProperty("描述")
+    private String description;
+
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 28 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/log/MemberLevelLogExportReqVO.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.member.controller.admin.level.vo.log;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * @author owen
+ */
+@Schema(description = "管理后台 - 会员等级记录 Excel 导出 Request VO,参数和 MemberLevelLogPageReqVO 是一致的")
+@Data
+public class MemberLevelLogExportReqVO {
+
+    @Schema(description = "用户编号", example = "25923")
+    private Long userId;
+
+    @Schema(description = "等级编号", example = "25985")
+    private Long levelId;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 33 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/log/MemberLevelLogPageReqVO.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.member.controller.admin.level.vo.log;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * @author owen
+ */
+@Schema(description = "管理后台 - 会员等级记录分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MemberLevelLogPageReqVO extends PageParam {
+
+    @Schema(description = "用户编号", example = "25923")
+    private Long userId;
+
+    @Schema(description = "等级编号", example = "25985")
+    private Long levelId;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 25 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/level/vo/log/MemberLevelLogRespVO.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.member.controller.admin.level.vo.log;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author owen
+ */
+@Schema(description = "管理后台 - 会员等级记录 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MemberLevelLogRespVO extends MemberLevelLogBaseVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8741")
+    private Long id;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

+ 9 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/tag/MemberTagController.java

@@ -65,6 +65,15 @@ public class MemberTagController {
         return success(MemberTagConvert.INSTANCE.convert(tag));
     }
 
+    @GetMapping("/list-all-simple")
+    @Operation(summary = "获取会员标签精简信息列表", description = "只包含被开启的会员标签,主要用于前端的下拉选项")
+    public CommonResult<List<MemberTagRespVO>> getSimpleTagList() {
+        // 获用户列表,只要开启状态的
+        List<MemberTagDO> list = tagService.getList();
+        // 排序后,返回给前端
+        return success(MemberTagConvert.INSTANCE.convertList(list));
+    }
+
     @GetMapping("/list")
     @Operation(summary = "获得会员标签列表")
     @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")

+ 9 - 1
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java

@@ -7,8 +7,10 @@ import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserPageReq
 import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserRespVO;
 import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserUpdateReqVO;
 import cn.iocoder.yudao.module.member.convert.user.MemberUserConvert;
+import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberLevelDO;
 import cn.iocoder.yudao.module.member.dal.dataobject.tag.MemberTagDO;
 import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
+import cn.iocoder.yudao.module.member.service.level.MemberLevelService;
 import cn.iocoder.yudao.module.member.service.tag.MemberTagService;
 import cn.iocoder.yudao.module.member.service.user.MemberUserService;
 import io.swagger.v3.oas.annotations.Operation;
@@ -38,6 +40,8 @@ public class MemberUserController {
     private MemberUserService memberUserService;
     @Resource
     private MemberTagService memberTagService;
+    @Resource
+    private MemberLevelService memberLevelService;
 
     @PutMapping("/update")
     @Operation(summary = "更新会员用户")
@@ -72,7 +76,11 @@ public class MemberUserController {
                 .flatMap(Collection::stream)
                 .collect(Collectors.toSet());
         List<MemberTagDO> tags = memberTagService.getTagList(tagIds);
-        return success(MemberUserConvert.INSTANCE.convertPage(pageResult, tags));
+
+        // 处理会员级别返显
+        List<MemberLevelDO> levels = memberLevelService.getEnableLevelList();
+
+        return success(MemberUserConvert.INSTANCE.convertPage(pageResult, tags, levels));
     }
 
 }

+ 3 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserBaseVO.java

@@ -53,4 +53,7 @@ public class MemberUserBaseVO {
     @Schema(description = "会员标签", example = "[1, 2]")
     private List<Long> tagIds;
 
+    @Schema(description = "会员等级编号", example = "1")
+    private Long levelId;
+
 }

+ 4 - 1
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserPageReqVO.java

@@ -32,7 +32,10 @@ public class MemberUserPageReqVO extends PageParam {
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] createTime;
 
-    @Schema(description = "会员标签", example = "[1, 2]")
+    @Schema(description = "会员标签编号列表", example = "[1, 2]")
     private List<Long> tagIds;
 
+    @Schema(description = "会员等级标号", example = "1")
+    private Long levelId;
+
 }

+ 3 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserRespVO.java

@@ -35,4 +35,7 @@ public class MemberUserRespVO extends MemberUserBaseVO {
     @Schema(description = "会员标签", example = "[红色, 快乐]")
     private List<String> tagNames;
 
+    @Schema(description = "会员等级", example = "黄金会员")
+    private String levelName;
+
 }

+ 3 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserUpdateReqVO.java

@@ -17,4 +17,7 @@ public class MemberUserUpdateReqVO extends MemberUserBaseVO {
     @NotNull(message = "编号不能为空")
     private Long id;
 
+    @Schema(description = "会员级别修改原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "推广需要")
+    private String levelReason;
+
 }

+ 30 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/convert/level/MemberExperienceLogConvert.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.member.convert.level;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.experience.MemberExperienceLogExcelVO;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.experience.MemberExperienceLogRespVO;
+import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberExperienceLogDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+/**
+ * 会员经验记录 Convert
+ *
+ * @author owen
+ */
+@Mapper
+public interface MemberExperienceLogConvert {
+
+    MemberExperienceLogConvert INSTANCE = Mappers.getMapper(MemberExperienceLogConvert.class);
+
+    MemberExperienceLogRespVO convert(MemberExperienceLogDO bean);
+
+    List<MemberExperienceLogRespVO> convertList(List<MemberExperienceLogDO> list);
+
+    PageResult<MemberExperienceLogRespVO> convertPage(PageResult<MemberExperienceLogDO> page);
+
+    List<MemberExperienceLogExcelVO> convertList02(List<MemberExperienceLogDO> list);
+
+}

+ 35 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/convert/level/MemberLevelConvert.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.member.convert.level;
+
+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.MemberLevelRespVO;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.MemberLevelSimpleRespVO;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.MemberLevelUpdateReqVO;
+import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberLevelDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+/**
+ * 会员等级 Convert
+ *
+ * @author owen
+ */
+@Mapper
+public interface MemberLevelConvert {
+
+    MemberLevelConvert INSTANCE = Mappers.getMapper(MemberLevelConvert.class);
+
+    MemberLevelDO convert(MemberLevelCreateReqVO bean);
+
+    MemberLevelDO convert(MemberLevelUpdateReqVO bean);
+
+    MemberLevelRespVO convert(MemberLevelDO bean);
+
+    List<MemberLevelRespVO> convertList(List<MemberLevelDO> list);
+
+    PageResult<MemberLevelRespVO> convertPage(PageResult<MemberLevelDO> page);
+
+    List<MemberLevelSimpleRespVO> convertSimpleList(List<MemberLevelDO> list);
+}

+ 30 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/convert/level/MemberLevelLogConvert.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.member.convert.level;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.log.MemberLevelLogExcelVO;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.log.MemberLevelLogRespVO;
+import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberLevelLogDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+/**
+ * 会员等级记录 Convert
+ *
+ * @author owen
+ */
+@Mapper
+public interface MemberLevelLogConvert {
+
+    MemberLevelLogConvert INSTANCE = Mappers.getMapper(MemberLevelLogConvert.class);
+
+    MemberLevelLogRespVO convert(MemberLevelLogDO bean);
+
+    List<MemberLevelLogRespVO> convertList(List<MemberLevelLogDO> list);
+
+    PageResult<MemberLevelLogRespVO> convertPage(PageResult<MemberLevelLogDO> page);
+
+    List<MemberLevelLogExcelVO> convertList02(List<MemberLevelLogDO> list);
+
+}

+ 11 - 1
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/convert/user/MemberUserConvert.java

@@ -1,10 +1,13 @@
 package cn.iocoder.yudao.module.member.convert.user;
 
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
 import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserRespVO;
 import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserUpdateReqVO;
 import cn.iocoder.yudao.module.member.controller.app.user.vo.AppMemberUserInfoRespVO;
+import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberLevelDO;
 import cn.iocoder.yudao.module.member.dal.dataobject.tag.MemberTagDO;
 import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
 import org.mapstruct.Mapper;
@@ -34,11 +37,18 @@ public interface MemberUserConvert {
     MemberUserRespVO convert03(MemberUserDO bean);
 
     default PageResult<MemberUserRespVO> convertPage(PageResult<MemberUserDO> pageResult,
-                                                     List<MemberTagDO> tags) {
+                                                     List<MemberTagDO> tags,
+                                                     List<MemberLevelDO> levels) {
         PageResult<MemberUserRespVO> result = convertPage(pageResult);
+
+        // 处理关联数据
         Map<Long, String> tagMap = convertMap(tags, MemberTagDO::getId, MemberTagDO::getName);
+        Map<Long, String> levelMap = convertMap(levels, MemberLevelDO::getId, MemberLevelDO::getName);
+
+        // 填充关联数据
         for (MemberUserRespVO vo : result.getList()) {
             vo.setTagNames(convertList(vo.getTagIds(), tagMap::get));
+            vo.setLevelName(MapUtil.getStr(levelMap, vo.getLevelId(), StrUtil.EMPTY));
         }
         return result;
     }

+ 61 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/level/MemberExperienceLogDO.java

@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.member.dal.dataobject.level;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.member.enums.MemberExperienceBizTypeEnum;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 会员经验记录 DO
+ *
+ * @author owen
+ */
+@TableName("member_experience_log")
+@KeySequence("member_experience_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MemberExperienceLogDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 用户编号
+     */
+    private Long userId;
+    /**
+     * 业务类型
+     * <p>
+     * 枚举 {@link MemberExperienceBizTypeEnum}
+     */
+    private Integer bizType;
+    /**
+     * 业务编号
+     */
+    private String bizId;
+    /**
+     * 标题
+     */
+    private String title;
+    /**
+     * 经验
+     */
+    private Integer experience;
+    /**
+     * 变更后的经验
+     */
+    private Integer totalExperience;
+    /**
+     * 描述
+     */
+    private String description;
+
+}

+ 61 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/level/MemberLevelDO.java

@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.member.dal.dataobject.level;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 会员等级 DO
+ *
+ * @author owen
+ */
+@TableName("member_level")
+@KeySequence("member_level_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MemberLevelDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 等级名称
+     */
+    private String name;
+    /**
+     * 升级经验
+     */
+    private Integer experience;
+    /**
+     * 等级
+     */
+    private Integer level;
+    /**
+     * 享受折扣
+     */
+    private Integer discount;
+    /**
+     * 等级图标
+     */
+    private String icon;
+    /**
+     * 等级背景图
+     */
+    private String backgroundUrl;
+    /**
+     * 状态
+     * <p>
+     * 枚举 {@link CommonStatusEnum}
+     */
+    private Integer status;
+
+}

+ 62 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/level/MemberLevelLogDO.java

@@ -0,0 +1,62 @@
+package cn.iocoder.yudao.module.member.dal.dataobject.level;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 会员等级记录 DO
+ *
+ * @author owen
+ */
+@TableName("member_level_log")
+@KeySequence("member_level_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MemberLevelLogDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 用户编号
+     */
+    private Long userId;
+    /**
+     * 等级编号
+     */
+    private Long levelId;
+    /**
+     * 会员等级
+     */
+    private Integer level;
+    /**
+     * 享受折扣
+     */
+    private Integer discount;
+    /**
+     * 升级经验
+     */
+    private Integer experience;
+    /**
+     * 会员此时的经验
+     */
+    private Integer userExperience;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 描述
+     */
+    private String description;
+
+}

+ 10 - 1
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/user/MemberUserDO.java

@@ -117,5 +117,14 @@ public class MemberUserDO extends TenantBaseDO {
     @TableField(typeHandler = LongListTypeHandler.class)
     private List<Long> tagIds;
 
-    // TODO 成长值、会员等级等等
+    /**
+     * 会员级别编号
+     */
+    private Long levelId;
+    /**
+     * 会员经验
+     */
+    private Integer experience;
+
+    // TODO 积分等等
 }

+ 41 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/level/MemberExperienceLogMapper.java

@@ -0,0 +1,41 @@
+package cn.iocoder.yudao.module.member.dal.mysql.level;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.experience.MemberExperienceLogExportReqVO;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.experience.MemberExperienceLogPageReqVO;
+import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberExperienceLogDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 会员经验记录 Mapper
+ *
+ * @author owen
+ */
+@Mapper
+public interface MemberExperienceLogMapper extends BaseMapperX<MemberExperienceLogDO> {
+
+    default PageResult<MemberExperienceLogDO> selectPage(MemberExperienceLogPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<MemberExperienceLogDO>()
+                .eqIfPresent(MemberExperienceLogDO::getUserId, reqVO.getUserId())
+                .eqIfPresent(MemberExperienceLogDO::getBizId, reqVO.getBizId())
+                .eqIfPresent(MemberExperienceLogDO::getBizType, reqVO.getBizType())
+                .eqIfPresent(MemberExperienceLogDO::getTitle, reqVO.getTitle())
+                .betweenIfPresent(MemberExperienceLogDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(MemberExperienceLogDO::getId));
+    }
+
+    default List<MemberExperienceLogDO> selectList(MemberExperienceLogExportReqVO reqVO) {
+        return selectList(new LambdaQueryWrapperX<MemberExperienceLogDO>()
+                .eqIfPresent(MemberExperienceLogDO::getUserId, reqVO.getUserId())
+                .eqIfPresent(MemberExperienceLogDO::getBizId, reqVO.getBizId())
+                .eqIfPresent(MemberExperienceLogDO::getBizType, reqVO.getBizType())
+                .eqIfPresent(MemberExperienceLogDO::getTitle, reqVO.getTitle())
+                .betweenIfPresent(MemberExperienceLogDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(MemberExperienceLogDO::getId));
+    }
+
+}

+ 37 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/level/MemberLevelLogMapper.java

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.member.dal.mysql.level;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.log.MemberLevelLogExportReqVO;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.log.MemberLevelLogPageReqVO;
+import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberLevelLogDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 会员等级记录 Mapper
+ *
+ * @author owen
+ */
+@Mapper
+public interface MemberLevelLogMapper extends BaseMapperX<MemberLevelLogDO> {
+
+    default PageResult<MemberLevelLogDO> selectPage(MemberLevelLogPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<MemberLevelLogDO>()
+                .eqIfPresent(MemberLevelLogDO::getUserId, reqVO.getUserId())
+                .eqIfPresent(MemberLevelLogDO::getLevelId, reqVO.getLevelId())
+                .betweenIfPresent(MemberLevelLogDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(MemberLevelLogDO::getId));
+    }
+
+    default List<MemberLevelLogDO> selectList(MemberLevelLogExportReqVO reqVO) {
+        return selectList(new LambdaQueryWrapperX<MemberLevelLogDO>()
+                .eqIfPresent(MemberLevelLogDO::getUserId, reqVO.getUserId())
+                .eqIfPresent(MemberLevelLogDO::getLevelId, reqVO.getLevelId())
+                .betweenIfPresent(MemberLevelLogDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(MemberLevelLogDO::getId));
+    }
+
+}

+ 33 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/level/MemberLevelMapper.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.member.dal.mysql.level;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.MemberLevelPageReqVO;
+import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberLevelDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 会员等级 Mapper
+ *
+ * @author owen
+ */
+@Mapper
+public interface MemberLevelMapper extends BaseMapperX<MemberLevelDO> {
+
+    default PageResult<MemberLevelDO> selectPage(MemberLevelPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<MemberLevelDO>()
+                .likeIfPresent(MemberLevelDO::getName, reqVO.getName())
+                .eqIfPresent(MemberLevelDO::getStatus, reqVO.getStatus())
+                .orderByAsc(MemberLevelDO::getLevel));
+    }
+
+
+    default List<MemberLevelDO> selectListByStatus(Integer status) {
+        return selectList(new LambdaQueryWrapperX<MemberLevelDO>()
+                .eq(MemberLevelDO::getStatus, status)
+                .orderByAsc(MemberLevelDO::getLevel));
+    }
+}

+ 2 - 4
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/tag/MemberTagMapper.java

@@ -22,9 +22,7 @@ public interface MemberTagMapper extends BaseMapperX<MemberTagDO> {
                 .orderByDesc(MemberTagDO::getId));
     }
 
-    default boolean exists(Long id, String name) {
-        return exists(new LambdaQueryWrapperX<MemberTagDO>()
-                .neIfPresent(MemberTagDO::getId, id)
-                .eq(MemberTagDO::getName, name));
+    default MemberTagDO selelctByName(String name) {
+        return selectOne(MemberTagDO::getName, name);
     }
 }

+ 14 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapper.java

@@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserPageReqVO;
 import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
 import java.util.List;
@@ -43,8 +44,21 @@ public interface MemberUserMapper extends BaseMapperX<MemberUserDO> {
                 .betweenIfPresent(MemberUserDO::getLoginDate, reqVO.getLoginDate())
                 .likeIfPresent(MemberUserDO::getNickname, reqVO.getNickname())
                 .betweenIfPresent(MemberUserDO::getCreateTime, reqVO.getCreateTime())
+                .eqIfPresent(MemberUserDO::getLevelId, reqVO.getLevelId())
                 .apply(StrUtil.isNotEmpty(tagIdSql), tagIdSql)
                 .orderByDesc(MemberUserDO::getId));
     }
 
+    /**
+     * 取消会员的等级
+     *
+     * @param userId 会员编号
+     * @return 受影响的行数
+     */
+    default int cancelUserLevel(Long userId) {
+        return update(null, new LambdaUpdateWrapper<MemberUserDO>()
+                .eq(MemberUserDO::getId, userId)
+                .set(MemberUserDO::getExperience, 0)
+                .set(MemberUserDO::getLevelId, null));
+    }
 }

+ 77 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberExperienceLogService.java

@@ -0,0 +1,77 @@
+package cn.iocoder.yudao.module.member.service.level;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.experience.MemberExperienceLogExportReqVO;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.experience.MemberExperienceLogPageReqVO;
+import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberExperienceLogDO;
+import cn.iocoder.yudao.module.member.enums.MemberExperienceBizTypeEnum;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 会员经验记录 Service 接口
+ *
+ * @author owen
+ */
+public interface MemberExperienceLogService {
+
+    /**
+     * 删除会员经验记录
+     *
+     * @param id 编号
+     */
+    void deleteExperienceLog(Long id);
+
+    /**
+     * 获得会员经验记录
+     *
+     * @param id 编号
+     * @return 会员经验记录
+     */
+    MemberExperienceLogDO getExperienceLog(Long id);
+
+    /**
+     * 获得会员经验记录列表
+     *
+     * @param ids 编号
+     * @return 会员经验记录列表
+     */
+    List<MemberExperienceLogDO> getExperienceLogList(Collection<Long> ids);
+
+    /**
+     * 获得会员经验记录分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 会员经验记录分页
+     */
+    PageResult<MemberExperienceLogDO> getExperienceLogPage(MemberExperienceLogPageReqVO pageReqVO);
+
+    /**
+     * 获得会员经验记录列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 会员经验记录列表
+     */
+    List<MemberExperienceLogDO> getExperienceLogList(MemberExperienceLogExportReqVO exportReqVO);
+
+    /**
+     * 创建 手动调整 经验变动记录
+     *
+     * @param userId          会员编号
+     * @param experience      变动经验值
+     * @param totalExperience 会员当前的经验
+     */
+    void createAdjustLog(Long userId, int experience, int totalExperience);
+
+    /**
+     * 根据业务类型, 创建 经验变动记录
+     *
+     * @param userId          会员编号
+     * @param experience      变动经验值
+     * @param totalExperience 会员当前的经验
+     * @param bizType         业务类型
+     * @param bizId           业务ID
+     */
+    void createBizLog(Long userId, int experience, int totalExperience, MemberExperienceBizTypeEnum bizType, String bizId);
+}

+ 86 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberExperienceLogServiceImpl.java

@@ -0,0 +1,86 @@
+package cn.iocoder.yudao.module.member.service.level;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.experience.MemberExperienceLogExportReqVO;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.experience.MemberExperienceLogPageReqVO;
+import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberExperienceLogDO;
+import cn.iocoder.yudao.module.member.dal.mysql.level.MemberExperienceLogMapper;
+import cn.iocoder.yudao.module.member.enums.MemberExperienceBizTypeEnum;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+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.EXPERIENCE_LOG_NOT_EXISTS;
+
+/**
+ * 会员经验记录 Service 实现类
+ *
+ * @author owen
+ */
+@Service
+@Validated
+public class MemberExperienceLogServiceImpl implements MemberExperienceLogService {
+
+    @Resource
+    private MemberExperienceLogMapper experienceLogMapper;
+
+    @Override
+    public void deleteExperienceLog(Long id) {
+        // 校验存在
+        validateExperienceLogExists(id);
+        // 删除
+        experienceLogMapper.deleteById(id);
+    }
+
+    private void validateExperienceLogExists(Long id) {
+        if (experienceLogMapper.selectById(id) == null) {
+            throw exception(EXPERIENCE_LOG_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public MemberExperienceLogDO getExperienceLog(Long id) {
+        return experienceLogMapper.selectById(id);
+    }
+
+    @Override
+    public List<MemberExperienceLogDO> getExperienceLogList(Collection<Long> ids) {
+        return experienceLogMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<MemberExperienceLogDO> getExperienceLogPage(MemberExperienceLogPageReqVO pageReqVO) {
+        return experienceLogMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<MemberExperienceLogDO> getExperienceLogList(MemberExperienceLogExportReqVO exportReqVO) {
+        return experienceLogMapper.selectList(exportReqVO);
+    }
+
+    @Override
+    public void createAdjustLog(Long userId, int experience, int totalExperience) {
+        // 管理员调整时, 没有业务编号, 记录对应的枚举值
+        String bizId = MemberExperienceBizTypeEnum.ADMIN.getValue() + "";
+        this.createBizLog(userId, experience, totalExperience, MemberExperienceBizTypeEnum.ADMIN, bizId);
+    }
+
+    @Override
+    public void createBizLog(Long userId, int experience, int totalExperience, MemberExperienceBizTypeEnum bizType, String bizId) {
+        MemberExperienceLogDO experienceLogDO = new MemberExperienceLogDO();
+        experienceLogDO.setUserId(userId);
+        experienceLogDO.setExperience(experience);
+        experienceLogDO.setTotalExperience(totalExperience);
+        experienceLogDO.setBizId(bizId);
+        experienceLogDO.setBizType(bizType.getValue());
+        experienceLogDO.setTitle(bizType.getTitle());
+        experienceLogDO.setDescription(StrUtil.format(bizType.getDesc(), experience));
+        experienceLogMapper.insert(experienceLogDO);
+    }
+
+}

+ 84 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberLevelLogService.java

@@ -0,0 +1,84 @@
+package cn.iocoder.yudao.module.member.service.level;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.log.MemberLevelLogExportReqVO;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.log.MemberLevelLogPageReqVO;
+import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberLevelDO;
+import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberLevelLogDO;
+import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 会员等级记录 Service 接口
+ *
+ * @author owen
+ */
+public interface MemberLevelLogService {
+
+    /**
+     * 删除会员等级记录
+     *
+     * @param id 编号
+     */
+    void deleteLevelLog(Long id);
+
+    /**
+     * 获得会员等级记录
+     *
+     * @param id 编号
+     * @return 会员等级记录
+     */
+    MemberLevelLogDO getLevelLog(Long id);
+
+    /**
+     * 获得会员等级记录列表
+     *
+     * @param ids 编号
+     * @return 会员等级记录列表
+     */
+    List<MemberLevelLogDO> getLevelLogList(Collection<Long> ids);
+
+    /**
+     * 获得会员等级记录分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 会员等级记录分页
+     */
+    PageResult<MemberLevelLogDO> getLevelLogPage(MemberLevelLogPageReqVO pageReqVO);
+
+    /**
+     * 获得会员等级记录列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 会员等级记录列表
+     */
+    List<MemberLevelLogDO> getLevelLogList(MemberLevelLogExportReqVO exportReqVO);
+
+    /**
+     * 创建记录: 取消等级
+     *
+     * @param userId 会员编号
+     * @param reason 调整原因
+     */
+    void createCancelLog(Long userId, String reason);
+
+    /**
+     * 创建记录: 手动调整
+     *
+     * @param user       会员
+     * @param level      等级
+     * @param experience 变动经验值
+     * @param reason     调整原因
+     */
+    void createAdjustLog(MemberUserDO user, MemberLevelDO level, int experience, String reason);
+
+    /**
+     * 创建记录: 自动升级
+     *
+     * @param user  会员
+     * @param level 等级
+     */
+    void createAutoUpgradeLog(MemberUserDO user, MemberLevelDO level);
+}

+ 115 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberLevelLogServiceImpl.java

@@ -0,0 +1,115 @@
+package cn.iocoder.yudao.module.member.service.level;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.log.MemberLevelLogExportReqVO;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.log.MemberLevelLogPageReqVO;
+import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberLevelDO;
+import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberLevelLogDO;
+import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
+import cn.iocoder.yudao.module.member.dal.mysql.level.MemberLevelLogMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+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_LOG_NOT_EXISTS;
+
+/**
+ * 会员等级记录 Service 实现类
+ *
+ * @author owen
+ */
+@Service
+@Validated
+public class MemberLevelLogServiceImpl implements MemberLevelLogService {
+
+    @Resource
+    private MemberLevelLogMapper levelLogMapper;
+
+    @Override
+    public void deleteLevelLog(Long id) {
+        // 校验存在
+        validateLevelLogExists(id);
+        // 删除
+        levelLogMapper.deleteById(id);
+    }
+
+    private void validateLevelLogExists(Long id) {
+        if (levelLogMapper.selectById(id) == null) {
+            throw exception(LEVEL_LOG_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public MemberLevelLogDO getLevelLog(Long id) {
+        return levelLogMapper.selectById(id);
+    }
+
+    @Override
+    public List<MemberLevelLogDO> getLevelLogList(Collection<Long> ids) {
+        return levelLogMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<MemberLevelLogDO> getLevelLogPage(MemberLevelLogPageReqVO pageReqVO) {
+        return levelLogMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<MemberLevelLogDO> getLevelLogList(MemberLevelLogExportReqVO exportReqVO) {
+        return levelLogMapper.selectList(exportReqVO);
+    }
+
+    @Override
+    public void createCancelLog(Long userId, String reason) {
+        MemberLevelLogDO levelLogDO = new MemberLevelLogDO();
+        levelLogDO.setUserId(userId);
+        levelLogDO.setRemark(reason);
+        levelLogDO.setDescription("管理员取消");
+        levelLogMapper.insert(levelLogDO);
+
+        // 给会员发送等级变动消息
+        notifyMember(userId, levelLogDO);
+    }
+
+    @Override
+    public void createAdjustLog(MemberUserDO user, MemberLevelDO level, int experience, String reason) {
+        MemberLevelLogDO levelLogDO = new MemberLevelLogDO();
+        levelLogDO.setUserId(user.getId());
+        levelLogDO.setLevelId(level.getId());
+        levelLogDO.setLevel(level.getLevel());
+        levelLogDO.setDiscount(level.getDiscount());
+        levelLogDO.setUserExperience(level.getExperience());
+        levelLogDO.setExperience(experience);
+        levelLogDO.setRemark(reason);
+        levelLogDO.setDescription("管理员调整为:" + level.getName());
+        levelLogMapper.insert(levelLogDO);
+
+        // 给会员发送等级变动消息
+        notifyMember(user.getId(), levelLogDO);
+    }
+
+    @Override
+    public void createAutoUpgradeLog(MemberUserDO user, MemberLevelDO level) {
+        MemberLevelLogDO levelLogDO = new MemberLevelLogDO();
+        levelLogDO.setUserId(user.getId());
+        levelLogDO.setLevelId(level.getId());
+        levelLogDO.setLevel(level.getLevel());
+        levelLogDO.setDiscount(level.getDiscount());
+        levelLogDO.setExperience(level.getExperience());
+        levelLogDO.setUserExperience(user.getExperience());
+        levelLogDO.setDescription("成为:" + level.getName());
+        levelLogMapper.insert(levelLogDO);
+
+        // 给会员发送等级变动消息
+        notifyMember(user.getId(), levelLogDO);
+    }
+
+    private void notifyMember(Long userId, MemberLevelLogDO level) {
+        //todo: 给会员发消息
+    }
+
+}

+ 107 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberLevelService.java

@@ -0,0 +1,107 @@
+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.module.member.controller.admin.level.vo.MemberLevelCreateReqVO;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.MemberLevelPageReqVO;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.MemberLevelUpdateReqVO;
+import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberLevelDO;
+import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
+import cn.iocoder.yudao.module.member.enums.MemberExperienceBizTypeEnum;
+
+import javax.annotation.Nullable;
+import javax.validation.Valid;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 会员等级 Service 接口
+ *
+ * @author owen
+ */
+public interface MemberLevelService {
+
+    /**
+     * 创建会员等级
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createLevel(@Valid MemberLevelCreateReqVO createReqVO);
+
+    /**
+     * 更新会员等级
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateLevel(@Valid MemberLevelUpdateReqVO updateReqVO);
+
+    /**
+     * 删除会员等级
+     *
+     * @param id 编号
+     */
+    void deleteLevel(Long id);
+
+    /**
+     * 获得会员等级
+     *
+     * @param id 编号
+     * @return 会员等级
+     */
+    MemberLevelDO getLevel(Long id);
+
+    /**
+     * 获得会员等级列表
+     *
+     * @param ids 编号
+     * @return 会员等级列表
+     */
+    List<MemberLevelDO> getLevelList(Collection<Long> ids);
+
+    /**
+     * 获得会员等级分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 会员等级分页
+     */
+    PageResult<MemberLevelDO> getLevelPage(MemberLevelPageReqVO pageReqVO);
+
+
+    /**
+     * 获得指定状态的会员等级列表
+     *
+     * @param status 状态
+     * @return 会员等级列表
+     */
+    List<MemberLevelDO> getLevelListByStatus(Integer status);
+
+
+    /**
+     * 获得开启状态的会员等级列表
+     *
+     * @return 会员等级列表
+     */
+    default List<MemberLevelDO> getEnableLevelList() {
+        return getLevelListByStatus(CommonStatusEnum.ENABLE.getStatus());
+    }
+
+    /**
+     * 修改会员的等级
+     *
+     * @param user        会员
+     * @param levelId     要修改的等级编号,编号为空时,代表取消会员的等级
+     * @param levelReason 修改原因
+     */
+    void updateUserLevel(MemberUserDO user, @Nullable Long levelId, String levelReason);
+
+    /**
+     * 增加会员经验
+     *
+     * @param userId     会员ID
+     * @param experience 经验
+     * @param bizType    业务类型
+     * @param bizId      业务编号
+     */
+    void plusExperience(Long userId, Integer experience, MemberExperienceBizTypeEnum bizType, String bizId);
+}

+ 279 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberLevelServiceImpl.java

@@ -0,0 +1,279 @@
+package cn.iocoder.yudao.module.member.service.level;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.NumberUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.ObjectUtil;
+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;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.MemberLevelUpdateReqVO;
+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.dataobject.user.MemberUserDO;
+import cn.iocoder.yudao.module.member.dal.mysql.level.MemberLevelMapper;
+import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper;
+import cn.iocoder.yudao.module.member.enums.MemberExperienceBizTypeEnum;
+import com.google.common.annotations.VisibleForTesting;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.*;
+
+/**
+ * 会员等级 Service 实现类
+ *
+ * @author owen
+ */
+@Slf4j
+@Service
+@Validated
+public class MemberLevelServiceImpl implements MemberLevelService {
+
+    @Resource
+    private MemberLevelMapper levelMapper;
+    @Resource
+    private MemberLevelLogService memberLevelLogService;
+    @Resource
+    private MemberExperienceLogService memberExperienceLogService;
+    @Resource
+    private MemberUserMapper memberUserMapper;
+
+    @Override
+    public Long createLevel(MemberLevelCreateReqVO createReqVO) {
+        // 校验配置是否有效
+        validateConfigValid(null, createReqVO.getName(), createReqVO.getLevel(), createReqVO.getExperience());
+
+        // 插入
+        MemberLevelDO level = MemberLevelConvert.INSTANCE.convert(createReqVO);
+        levelMapper.insert(level);
+        // 返回
+        return level.getId();
+    }
+
+    @Override
+    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);
+    }
+
+    @Override
+    public void deleteLevel(Long id) {
+        // 校验存在
+        validateLevelExists(id);
+        // 删除
+        levelMapper.deleteById(id);
+    }
+
+    @VisibleForTesting
+    MemberLevelDO validateLevelExists(Long id) {
+        MemberLevelDO levelDO = levelMapper.selectById(id);
+        if (levelDO == null) {
+            throw exception(LEVEL_NOT_EXISTS);
+        }
+        return levelDO;
+    }
+
+    @VisibleForTesting
+    void validateNameUnique(List<MemberLevelDO> 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<MemberLevelDO> 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<MemberLevelDO> 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<MemberLevelDO> list = levelMapper.selectList();
+
+        // 校验名称唯一
+        validateNameUnique(list, id, name);
+        // 校验等级唯一
+        validateLevelUnique(list, id, level);
+        // 校验升级所需经验是否有效: 大于前一个等级,小于下一个级别
+        validateExperienceOutRange(list, id, level, experience);
+    }
+
+    @Override
+    public MemberLevelDO getLevel(Long id) {
+        return levelMapper.selectById(id);
+    }
+
+    @Override
+    public List<MemberLevelDO> getLevelList(Collection<Long> ids) {
+        return levelMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<MemberLevelDO> getLevelPage(MemberLevelPageReqVO pageReqVO) {
+        return levelMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<MemberLevelDO> getLevelListByStatus(Integer status) {
+        return levelMapper.selectListByStatus(status);
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void updateUserLevel(MemberUserDO user, Long levelId, String reason) {
+        // 未调整的情况1
+        if (user.getLevelId() == null && levelId == null) {
+            return;
+        }
+        // 未调整的情况2
+        if (ObjUtil.equal(user.getLevelId(), levelId)) {
+            return;
+        }
+
+        // 需要后台用户填写为什么调整会员的等级
+        if (StrUtil.isBlank(reason)) {
+            throw exception(LEVEL_REASON_NOT_EXISTS);
+        }
+
+        int experience;
+        int totalExperience = 0;
+        // 记录等级变动
+        if (levelId == null) {
+            experience = -user.getExperience();
+
+            // 取消了会员的等级
+            memberLevelLogService.createCancelLog(user.getId(), reason);
+            memberUserMapper.cancelUserLevel(user.getId());
+        } else {
+            MemberLevelDO level = validateLevelExists(levelId);
+            // 变动经验值 = 等级的升级经验 - 会员当前的经验;正数为增加经验,负数为扣减经验
+            experience = level.getExperience() - user.getExperience();
+            // 会员当前的经验 = 等级的升级经验
+            totalExperience = level.getExperience();
+
+            memberLevelLogService.createAdjustLog(user, level, experience, reason);
+
+            // 更新会员表上的等级编号、经验值
+            updateUserLevelIdAndExperience(user.getId(), levelId, totalExperience);
+        }
+
+
+        // 记录会员经验变动
+        memberExperienceLogService.createAdjustLog(user.getId(), experience, totalExperience);
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void plusExperience(Long userId, Integer experience, MemberExperienceBizTypeEnum bizType, String bizId) {
+        if (experience == 0) {
+            return;
+        }
+
+        MemberUserDO user = memberUserMapper.selectById(userId);
+        if (user.getExperience() == null) {
+            user.setExperience(0);
+        }
+
+        // 防止扣出负数
+        int userExperience = NumberUtil.max(user.getExperience() + experience, 0);
+
+        // 创建经验记录
+        memberExperienceLogService.createBizLog(userId, experience, userExperience, bizType, bizId);
+
+        // 计算会员等级
+        Long levelId = calcLevel(user, userExperience);
+
+        // 更新会员表上的等级编号、经验值
+        updateUserLevelIdAndExperience(user.getId(), levelId, userExperience);
+    }
+
+    private void updateUserLevelIdAndExperience(Long userId, Long levelId, Integer experience) {
+        memberUserMapper.updateById(new MemberUserDO()
+                .setId(userId)
+                .setLevelId(levelId).setExperience(experience)
+        );
+    }
+
+    /**
+     * 计算会员等级
+     *
+     * @param user           会员
+     * @param userExperience 会员当前的经验值
+     * @return 会员等级编号,null表示无变化
+     */
+    private Long calcLevel(MemberUserDO user, int userExperience) {
+        List<MemberLevelDO> list = getEnableLevelList();
+        if (CollUtil.isEmpty(list)) {
+            log.warn("计算会员等级失败:会员等级配置不存在");
+            return null;
+        }
+
+        MemberLevelDO matchLevel = list.stream()
+                .filter(level -> userExperience >= level.getExperience())
+                .max(Comparator.nullsFirst(Comparator.comparing(MemberLevelDO::getLevel)))
+                .orElse(null);
+        if (matchLevel == null) {
+            log.warn("计算会员等级失败:未找到会员{}经验{}对应的等级配置", user.getId(), userExperience);
+            return null;
+        }
+
+        // 等级没有变化
+        if (ObjectUtil.equal(matchLevel.getId(), user.getLevelId())) {
+            return null;
+        }
+
+        // 保存等级变更记录
+        memberLevelLogService.createAutoUpgradeLog(user, matchLevel);
+        return matchLevel.getId();
+    }
+}

+ 6 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/tag/MemberTagService.java

@@ -63,4 +63,10 @@ public interface MemberTagService {
      */
     PageResult<MemberTagDO> getTagPage(MemberTagPageReqVO pageReqVO);
 
+    /**
+     * 获取标签列表
+     *
+     * @return 标签列表
+     */
+    List<MemberTagDO> getList();
 }

+ 19 - 4
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/tag/MemberTagServiceImpl.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.member.service.tag;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.member.controller.admin.tag.vo.MemberTagCreateReqVO;
 import cn.iocoder.yudao.module.member.controller.admin.tag.vo.MemberTagPageReqVO;
@@ -62,8 +63,6 @@ public class MemberTagServiceImpl implements MemberTagService {
         tagMapper.deleteById(id);
     }
 
-    // TODO @疯狂:校验 tag name 不重复,参考 validateMobileUnique 方法,Mapper 尽量逻辑通用,处理交给 Service
-
     private void validateTagExists(Long id) {
         if (tagMapper.selectById(id) == null) {
             throw exception(TAG_NOT_EXISTS);
@@ -71,8 +70,19 @@ public class MemberTagServiceImpl implements MemberTagService {
     }
 
     private void validateTagNameUnique(Long id, String name) {
-        boolean exists = tagMapper.exists(id, name);
-        if (exists) {
+        if (StrUtil.isBlank(name)) {
+            return;
+        }
+        MemberTagDO tag = tagMapper.selelctByName(name);
+        if (tag == null) {
+            return;
+        }
+
+        // 如果 id 为空,说明不用比较是否为相同 id 的标签
+        if (id == null) {
+            throw exception(TAG_NAME_EXISTS);
+        }
+        if (!tag.getId().equals(id)) {
             throw exception(TAG_NAME_EXISTS);
         }
     }
@@ -95,4 +105,9 @@ public class MemberTagServiceImpl implements MemberTagService {
         return tagMapper.selectPage(pageReqVO);
     }
 
+    @Override
+    public List<MemberTagDO> getList() {
+        return tagMapper.selectList();
+    }
+
 }

+ 10 - 2
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java

@@ -8,13 +8,14 @@ import cn.iocoder.yudao.module.infra.api.file.FileApi;
 import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserPageReqVO;
 import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserUpdateReqVO;
 import cn.iocoder.yudao.module.member.controller.app.user.vo.AppMemberUserResetPasswordReqVO;
+import cn.iocoder.yudao.module.member.controller.app.user.vo.AppMemberUserUpdateMobileReqVO;
 import cn.iocoder.yudao.module.member.controller.app.user.vo.AppMemberUserUpdatePasswordReqVO;
 import cn.iocoder.yudao.module.member.controller.app.user.vo.AppMemberUserUpdateReqVO;
-import cn.iocoder.yudao.module.member.controller.app.user.vo.AppMemberUserUpdateMobileReqVO;
 import cn.iocoder.yudao.module.member.convert.auth.AuthConvert;
 import cn.iocoder.yudao.module.member.convert.user.MemberUserConvert;
 import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
 import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper;
+import cn.iocoder.yudao.module.member.service.level.MemberLevelService;
 import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
 import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
 import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
@@ -55,6 +56,9 @@ public class MemberUserServiceImpl implements MemberUserService {
     @Resource
     private PasswordEncoder passwordEncoder;
 
+    @Resource
+    private MemberLevelService memberLevelService;
+
     @Override
     public MemberUserDO getUserByMobile(String mobile) {
         return memberUserMapper.selectByMobile(mobile);
@@ -180,16 +184,20 @@ public class MemberUserServiceImpl implements MemberUserService {
         return passwordEncoder.encode(password);
     }
 
+    @Transactional(rollbackFor = Exception.class)
     @Override
     public void updateUser(MemberUserUpdateReqVO updateReqVO) {
         // 校验存在
-        validateUserExists(updateReqVO.getId());
+        MemberUserDO user = validateUserExists(updateReqVO.getId());
         // 校验手机唯一
         validateMobileUnique(updateReqVO.getId(), updateReqVO.getMobile());
 
         // 更新
         MemberUserDO updateObj = MemberUserConvert.INSTANCE.convert(updateReqVO);
         memberUserMapper.updateById(updateObj);
+
+        // 会员级别修改
+        memberLevelService.updateUserLevel(user, updateReqVO.getLevelId(), updateReqVO.getLevelReason());
     }
 
     @VisibleForTesting

+ 264 - 0
yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/level/MemberLevelServiceImplTest.java

@@ -0,0 +1,264 @@
+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;
+import cn.iocoder.yudao.module.member.controller.admin.level.vo.MemberLevelUpdateReqVO;
+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.*;
+import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link MemberLevelServiceImpl} 的单元测试类
+ *
+ * @author owen
+ */
+@Import(MemberLevelServiceImpl.class)
+public class MemberLevelServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private MemberLevelServiceImpl levelService;
+
+    @Resource
+    private MemberLevelMapper levelMapper;
+
+    @MockBean
+    private MemberLevelLogService memberLevelLogService;
+    @MockBean
+    private MemberExperienceLogService memberExperienceLogService;
+
+    @Test
+    public void testCreateLevel_success() {
+        // 准备参数
+        MemberLevelCreateReqVO reqVO = randomPojo(MemberLevelCreateReqVO.class, o -> {
+            o.setDiscount(randomInt());
+            o.setIcon(randomURL());
+            o.setBackgroundUrl(randomURL());
+        });
+
+        // 调用
+        Long levelId = levelService.createLevel(reqVO);
+        // 断言
+        assertNotNull(levelId);
+        // 校验记录的属性是否正确
+        MemberLevelDO level = levelMapper.selectById(levelId);
+        assertPojoEquals(reqVO, level);
+    }
+
+    @Test
+    public void testUpdateLevel_success() {
+        // mock 数据
+        MemberLevelDO dbLevel = randomPojo(MemberLevelDO.class);
+        levelMapper.insert(dbLevel);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        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());
+        });
+
+        // 调用
+        levelService.updateLevel(reqVO);
+        // 校验是否更新正确
+        MemberLevelDO level = levelMapper.selectById(reqVO.getId()); // 获取最新的
+        assertPojoEquals(reqVO, level);
+    }
+
+    @Test
+    public void testUpdateLevel_notExists() {
+        // 准备参数
+        MemberLevelUpdateReqVO reqVO = randomPojo(MemberLevelUpdateReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> levelService.updateLevel(reqVO), LEVEL_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteLevel_success() {
+        // mock 数据
+        MemberLevelDO dbLevel = randomPojo(MemberLevelDO.class);
+        levelMapper.insert(dbLevel);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbLevel.getId();
+
+        // 调用
+        levelService.deleteLevel(id);
+        // 校验数据不存在了
+        assertNull(levelMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteLevel_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> levelService.deleteLevel(id), LEVEL_NOT_EXISTS);
+    }
+
+    @Test
+    public void testGetLevelPage() {
+        // mock 数据
+        MemberLevelDO dbLevel = randomPojo(MemberLevelDO.class, o -> { // 等会查询到
+            o.setName("黄金会员");
+            o.setStatus(1);
+        });
+        levelMapper.insert(dbLevel);
+        // 测试 name 不匹配
+        levelMapper.insert(cloneIgnoreId(dbLevel, o -> o.setName("")));
+        // 测试 status 不匹配
+        levelMapper.insert(cloneIgnoreId(dbLevel, o -> o.setStatus(0)));
+        // 准备参数
+        MemberLevelPageReqVO reqVO = new MemberLevelPageReqVO();
+        reqVO.setName("黄金会员");
+        reqVO.setStatus(1);
+
+        // 调用
+        PageResult<MemberLevelDO> pageResult = levelService.getLevelPage(reqVO);
+        // 断言
+        assertEquals(1, pageResult.getTotal());
+        assertEquals(1, pageResult.getList().size());
+        assertPojoEquals(dbLevel, pageResult.getList().get(0));
+    }
+
+    @Test
+    public void testCreateLevel_nameUnique() {
+        // 准备参数
+        String name = randomString();
+
+        // mock 数据
+        levelMapper.insert(randomLevelDO(o -> o.setName(name)));
+
+        // 调用,校验异常
+        List<MemberLevelDO> 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<MemberLevelDO> 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<MemberLevelDO> 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<MemberLevelDO> 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<MemberLevelDO> 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<MemberLevelDO> 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<MemberLevelDO>... consumers) {
+        Consumer<MemberLevelDO> 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));
+    }
+}

+ 2 - 1
yudao-module-member/yudao-module-member-biz/src/test/resources/sql/clean.sql

@@ -1,3 +1,4 @@
 DELETE FROM "member_user";
 DELETE FROM "member_address";
-DELETE FROM "member_tag";
+DELETE FROM "member_tag";
+DELETE FROM "member_level";

+ 20 - 1
yudao-module-member/yudao-module-member-biz/src/test/resources/sql/create_tables.sql

@@ -44,4 +44,23 @@ CREATE TABLE IF NOT EXISTS "member_tag"
     "deleted"     bit      NOT NULL DEFAULT FALSE,
     "tenant_id"   bigint   NOT NULL default '0',
     PRIMARY KEY ("id")
-) COMMENT '会员标签';
+) COMMENT '会员标签';
+
+CREATE TABLE IF NOT EXISTS "member_level"
+(
+    "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 '会员等级';