Explorar o código

新增站内信模板和站内信模块

车车 hai 1 ano
pai
achega
b8a0e1ecb0
Modificáronse 25 ficheiros con 1698 adicións e 0 borrados
  1. 97 0
      ktg-admin/src/main/java/com/ktg/web/controller/system/NotifyMessageController.java
  2. 91 0
      ktg-admin/src/main/java/com/ktg/web/controller/system/NotifyTemplateController.java
  3. 97 0
      ktg-system/src/main/java/com/ktg/system/domain/NotifyMessageDO.java
  4. 66 0
      ktg-system/src/main/java/com/ktg/system/domain/NotifyTemplateDO.java
  5. 26 0
      ktg-system/src/main/java/com/ktg/system/domain/message/NotifyMessageMyPageReqVO.java
  6. 35 0
      ktg-system/src/main/java/com/ktg/system/domain/message/NotifyMessagePageReqVO.java
  7. 49 0
      ktg-system/src/main/java/com/ktg/system/domain/message/NotifyMessageRespVO.java
  8. 32 0
      ktg-system/src/main/java/com/ktg/system/domain/template/NotifyTemplatePageReqVO.java
  9. 43 0
      ktg-system/src/main/java/com/ktg/system/domain/template/NotifyTemplateRespVO.java
  10. 43 0
      ktg-system/src/main/java/com/ktg/system/domain/template/NotifyTemplateSaveReqVO.java
  11. 29 0
      ktg-system/src/main/java/com/ktg/system/domain/template/NotifyTemplateSendReqVO.java
  12. 76 0
      ktg-system/src/main/java/com/ktg/system/mapper/NotifyMessageMapper.java
  13. 26 0
      ktg-system/src/main/java/com/ktg/system/mapper/NotifyTemplateMapper.java
  14. 98 0
      ktg-system/src/main/java/com/ktg/system/service/NotifyMessageService.java
  15. 55 0
      ktg-system/src/main/java/com/ktg/system/service/NotifySendService.java
  16. 74 0
      ktg-system/src/main/java/com/ktg/system/service/NotifyTemplateService.java
  17. 82 0
      ktg-system/src/main/java/com/ktg/system/service/impl/NotifyMessageServiceImpl.java
  18. 91 0
      ktg-system/src/main/java/com/ktg/system/service/impl/NotifySendServiceImpl.java
  19. 140 0
      ktg-system/src/main/java/com/ktg/system/service/impl/NotifyTemplateServiceImpl.java
  20. 44 0
      ktg-system/src/main/java/com/ktg/system/strategy/CommonStatusEnum.java
  21. 166 0
      ktg-system/src/main/java/com/ktg/system/strategy/ErrorCodeConstants.java
  22. 14 0
      ktg-system/src/main/java/com/ktg/system/strategy/IntArrayValuable.java
  23. 109 0
      ktg-system/src/main/java/com/ktg/system/strategy/RedisKeyConstants.java
  24. 77 0
      ktg-system/src/main/java/com/ktg/system/strategy/ServiceExceptionUtil.java
  25. 38 0
      ktg-system/src/main/java/com/ktg/system/strategy/UserTypeEnum.java

+ 97 - 0
ktg-admin/src/main/java/com/ktg/web/controller/system/NotifyMessageController.java

@@ -0,0 +1,97 @@
+package com.ktg.web.controller.system;
+
+import com.ktg.common.pojo.CommonResult;
+import com.ktg.common.pojo.PageResult;
+import com.ktg.common.utils.SecurityUtils;
+import com.ktg.common.utils.bean.BeanUtils;
+import com.ktg.system.domain.NotifyMessageDO;
+import com.ktg.system.domain.message.NotifyMessageMyPageReqVO;
+import com.ktg.system.domain.message.NotifyMessagePageReqVO;
+import com.ktg.system.domain.message.NotifyMessageRespVO;
+import com.ktg.system.service.NotifyMessageService;
+import com.ktg.system.strategy.UserTypeEnum;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.v3.oas.annotations.Parameter;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+import static com.ktg.common.pojo.CommonResult.success;
+
+
+@Api(tags = "管理后台 - 我的站内信")
+@RestController
+@RequestMapping("/system/notify-message")
+@Validated
+public class NotifyMessageController {
+
+    @Resource
+    private NotifyMessageService notifyMessageService;
+
+    // ========== 管理所有的站内信 ==========
+
+    @GetMapping("/get")
+    @ApiOperation("获得站内信")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    //@PreAuthorize("@ss.hasPermission('system:notify-message:query')")
+    public CommonResult<NotifyMessageRespVO> getNotifyMessage(@RequestParam("id") Long id) {
+        NotifyMessageDO message = notifyMessageService.getNotifyMessage(id);
+        return success(BeanUtils.toBean(message, NotifyMessageRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @ApiOperation("获得站内信分页")
+    // @PreAuthorize("@ss.hasPermission('system:notify-message:query')")
+    public CommonResult<PageResult<NotifyMessageRespVO>> getNotifyMessagePage(@Valid NotifyMessagePageReqVO pageVO) {
+        PageResult<NotifyMessageDO> pageResult = notifyMessageService.getNotifyMessagePage(pageVO);
+        return success(BeanUtils.toBean(pageResult, NotifyMessageRespVO.class));
+    }
+
+    // ========== 查看自己的站内信 ==========
+
+    @GetMapping("/my-page")
+    @ApiOperation("获得我的站内信分页")
+    public CommonResult<PageResult<NotifyMessageRespVO>> getMyMyNotifyMessagePage(@Valid NotifyMessageMyPageReqVO pageVO) {
+        PageResult<NotifyMessageDO> pageResult = notifyMessageService.getMyMyNotifyMessagePage(pageVO,
+                SecurityUtils.getLoginUser().getUserId(), UserTypeEnum.ADMIN.getValue());
+        return success(BeanUtils.toBean(pageResult, NotifyMessageRespVO.class));
+    }
+
+    @PutMapping("/update-read")
+    @ApiOperation("标记站内信为已读")
+    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
+    public CommonResult<Boolean> updateNotifyMessageRead(@RequestParam("ids") List<Long> ids) {
+        notifyMessageService.updateNotifyMessageRead(ids, SecurityUtils.getLoginUser().getUserId(), UserTypeEnum.ADMIN.getValue());
+        return success(Boolean.TRUE);
+    }
+
+    @PutMapping("/update-all-read")
+    @ApiOperation("标记所有站内信为已读")
+    public CommonResult<Boolean> updateAllNotifyMessageRead() {
+        notifyMessageService.updateAllNotifyMessageRead(SecurityUtils.getLoginUser().getUserId(), UserTypeEnum.ADMIN.getValue());
+        return success(Boolean.TRUE);
+    }
+
+    @GetMapping("/get-unread-list")
+    @ApiOperation("获取当前用户的最新站内信列表,默认 10 条")
+    @Parameter(name = "size", description = "10")
+    public CommonResult<List<NotifyMessageRespVO>> getUnreadNotifyMessageList(
+            @RequestParam(name = "size", defaultValue = "10") Integer size) {
+        List<NotifyMessageDO> list = notifyMessageService.getUnreadNotifyMessageList(
+                SecurityUtils.getLoginUser().getUserId(), UserTypeEnum.ADMIN.getValue(), size);
+        return success(BeanUtils.toBean(list, NotifyMessageRespVO.class));
+    }
+
+    @GetMapping("/get-unread-count")
+    @ApiOperation("获得当前用户的未读站内信数量")
+    // @ApiAccessLog(enable = false) // 由于前端会不断轮询该接口,记录日志没有意义
+    public CommonResult<Long> getUnreadNotifyMessageCount() {
+        return success(notifyMessageService.getUnreadNotifyMessageCount(
+                SecurityUtils.getLoginUser().getUserId(), UserTypeEnum.ADMIN.getValue()));
+    }
+
+}

+ 91 - 0
ktg-admin/src/main/java/com/ktg/web/controller/system/NotifyTemplateController.java

@@ -0,0 +1,91 @@
+package com.ktg.web.controller.system;
+
+import com.ktg.common.pojo.CommonResult;
+import com.ktg.common.pojo.PageResult;
+import com.ktg.common.utils.bean.BeanUtils;
+import com.ktg.system.domain.NotifyTemplateDO;
+import com.ktg.system.domain.template.NotifyTemplatePageReqVO;
+import com.ktg.system.domain.template.NotifyTemplateRespVO;
+import com.ktg.system.domain.template.NotifyTemplateSaveReqVO;
+import com.ktg.system.domain.template.NotifyTemplateSendReqVO;
+import com.ktg.system.service.NotifySendService;
+import com.ktg.system.service.NotifyTemplateService;
+import com.ktg.system.strategy.UserTypeEnum;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.v3.oas.annotations.Parameter;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+
+import static com.ktg.common.pojo.CommonResult.success;
+
+
+@Api(tags = "管理后台 - 站内信模版")
+@RestController
+@RequestMapping("/system/notify-template")
+@Validated
+public class NotifyTemplateController {
+
+    @Resource
+    private NotifyTemplateService notifyTemplateService;
+
+    @Resource
+    private NotifySendService notifySendService;
+
+    @PostMapping("/create")
+    @ApiOperation("创建站内信模版")
+    // @PreAuthorize("@ss.hasPermission('system:notify-template:create')")
+    public CommonResult<Long> createNotifyTemplate(@Valid @RequestBody NotifyTemplateSaveReqVO createReqVO) {
+        return success(notifyTemplateService.createNotifyTemplate(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @ApiOperation("更新站内信模版")
+    // @PreAuthorize("@ss.hasPermission('system:notify-template:update')")
+    public CommonResult<Boolean> updateNotifyTemplate(@Valid @RequestBody NotifyTemplateSaveReqVO updateReqVO) {
+        notifyTemplateService.updateNotifyTemplate(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @ApiOperation("删除站内信模版")
+    @Parameter(name = "id", description = "编号", required = true)
+    // @PreAuthorize("@ss.hasPermission('system:notify-template:delete')")
+    public CommonResult<Boolean> deleteNotifyTemplate(@RequestParam("id") Long id) {
+        notifyTemplateService.deleteNotifyTemplate(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @ApiOperation("获得站内信模版")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    // @PreAuthorize("@ss.hasPermission('system:notify-template:query')")
+    public CommonResult<NotifyTemplateRespVO> getNotifyTemplate(@RequestParam("id") Long id) {
+        NotifyTemplateDO template = notifyTemplateService.getNotifyTemplate(id);
+        return success(BeanUtils.toBean(template, NotifyTemplateRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @ApiOperation("获得站内信模版分页")
+    // @PreAuthorize("@ss.hasPermission('system:notify-template:query')")
+    public CommonResult<PageResult<NotifyTemplateRespVO>> getNotifyTemplatePage(@Valid NotifyTemplatePageReqVO pageVO) {
+        PageResult<NotifyTemplateDO> pageResult = notifyTemplateService.getNotifyTemplatePage(pageVO);
+        return success(BeanUtils.toBean(pageResult, NotifyTemplateRespVO.class));
+    }
+
+    @PostMapping("/send-notify")
+    @ApiOperation("发送站内信")
+    // @PreAuthorize("@ss.hasPermission('system:notify-template:send-notify')")
+    public CommonResult<Long> sendNotify(@Valid @RequestBody NotifyTemplateSendReqVO sendReqVO) {
+        if (UserTypeEnum.MEMBER.getValue().equals(sendReqVO.getUserType())) {
+            return success(notifySendService.sendSingleNotifyToMember(sendReqVO.getUserId(),
+                    sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams()));
+        } else {
+            return success(notifySendService.sendSingleNotifyToAdmin(sendReqVO.getUserId(),
+                    sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams()));
+        }
+    }
+}

+ 97 - 0
ktg-system/src/main/java/com/ktg/system/domain/NotifyMessageDO.java

@@ -0,0 +1,97 @@
+package com.ktg.system.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
+import com.ktg.common.core.domain.model.BaseBean;
+import lombok.*;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+/**
+ * 站内信 DO
+ *
+ * @author xrcoder
+ */
+@TableName(value = "system_notify_message", autoResultMap = true)
+@KeySequence("system_notify_message_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class NotifyMessageDO extends BaseBean {
+
+    /**
+     * 站内信编号,自增
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    /**
+     * 用户编号
+     *
+     * 关联 MemberUserDO 的 id 字段、或者 AdminUserDO 的 id 字段
+     */
+    private Long userId;
+    /**
+     * 用户类型
+     *
+     */
+    private Integer userType;
+
+    // ========= 模板相关字段 =========
+
+    /**
+     * 模版编号
+     *
+     * 关联 {@link NotifyTemplateDO#getId()}
+     */
+    private Long templateId;
+    /**
+     * 模版编码
+     *
+     * 关联 {@link NotifyTemplateDO#getCode()}
+     */
+    private String templateCode;
+    /**
+     * 模版类型
+     *
+     * 冗余 {@link NotifyTemplateDO#getType()}
+     */
+    private Integer templateType;
+    /**
+     * 模版发送人名称
+     *
+     * 冗余 {@link NotifyTemplateDO#getNickname()}
+     */
+    private String templateNickname;
+    /**
+     * 模版内容
+     *
+     * 基于 {@link NotifyTemplateDO#getContent()} 格式化后的内容
+     */
+    private String templateContent;
+    /**
+     * 模版参数
+     *
+     * 基于 {@link NotifyTemplateDO#getParams()} 输入后的参数
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private Map<String, Object> templateParams;
+
+    // ========= 读取相关字段 =========
+
+    /**
+     * 是否已读
+     */
+    private Boolean readStatus;
+    /**
+     * 阅读时间
+     */
+    private LocalDateTime readTime;
+
+    // @ApiModelProperty(value = "删除标志(0代表存在 2代表删除)")
+    private String delFlag;
+
+}

+ 66 - 0
ktg-system/src/main/java/com/ktg/system/domain/NotifyTemplateDO.java

@@ -0,0 +1,66 @@
+package com.ktg.system.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
+import com.ktg.common.core.domain.model.BaseBean;
+import lombok.*;
+
+import java.util.List;
+
+/**
+ * 站内信模版 DO
+ *
+ * @author xrcoder
+ */
+@TableName(value = "system_notify_template", autoResultMap = true)
+@KeySequence("system_notify_template_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class NotifyTemplateDO extends BaseBean {
+
+    /**
+     * ID
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    /**
+     * 模版名称
+     */
+    private String name;
+    /**
+     * 模版编码
+     */
+    private String code;
+    /**
+     * 模版类型
+     *
+     * 对应 system_notify_template_type 字典
+     */
+    private Integer type;
+    /**
+     * 发送人名称
+     */
+    private String nickname;
+    /**
+     * 模版内容
+     */
+    private String content;
+    /**
+     * 参数数组
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private List<String> params;
+    /**
+     * 状态
+     *
+     */
+    private Integer status;
+
+   //  @ApiModelProperty(value = "删除标志(0代表存在 2代表删除)")
+    private String delFlag;
+
+}

+ 26 - 0
ktg-system/src/main/java/com/ktg/system/domain/message/NotifyMessageMyPageReqVO.java

@@ -0,0 +1,26 @@
+package com.ktg.system.domain.message;
+
+import com.ktg.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;
+
+
+@Schema(description = "管理后台 - 站内信分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class NotifyMessageMyPageReqVO extends PageParam {
+
+    @Schema(description = "是否已读", example = "true")
+    private Boolean readStatus;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime[] createTime;
+
+}

+ 35 - 0
ktg-system/src/main/java/com/ktg/system/domain/message/NotifyMessagePageReqVO.java

@@ -0,0 +1,35 @@
+package com.ktg.system.domain.message;
+
+import com.ktg.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;
+
+
+@Schema(description = "管理后台 - 站内信分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class NotifyMessagePageReqVO extends PageParam {
+
+    @Schema(description = "用户编号", example = "25025")
+    private Long userId;
+
+    @Schema(description = "用户类型", example = "1")
+    private Integer userType;
+
+    @Schema(description = "模板编码", example = "test_01")
+    private String templateCode;
+
+    @Schema(description = "模版类型", example = "2")
+    private Integer templateType;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime[] createTime;
+
+}

+ 49 - 0
ktg-system/src/main/java/com/ktg/system/domain/message/NotifyMessageRespVO.java

@@ -0,0 +1,49 @@
+package com.ktg.system.domain.message;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+@Schema(description = "管理后台 - 站内信 Response VO")
+@Data
+public class NotifyMessageRespVO {
+
+    @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25025")
+    private Long userId;
+
+    @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Byte userType;
+
+    @Schema(description = "模版编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13013")
+    private Long templateId;
+
+    @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01")
+    private String templateCode;
+
+    @Schema(description = "模版发送人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
+    private String templateNickname;
+
+    @Schema(description = "模版内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试内容")
+    private String templateContent;
+
+    @Schema(description = "模版类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer templateType;
+
+    @Schema(description = "模版参数", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Map<String, Object> templateParams;
+
+    @Schema(description = "是否已读", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    private Boolean readStatus;
+
+    @Schema(description = "阅读时间")
+    private LocalDateTime readTime;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

+ 32 - 0
ktg-system/src/main/java/com/ktg/system/domain/template/NotifyTemplatePageReqVO.java

@@ -0,0 +1,32 @@
+package com.ktg.system.domain.template;
+
+import com.ktg.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;
+
+
+@Schema(description = "管理后台 - 站内信模版分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class NotifyTemplatePageReqVO extends PageParam {
+
+    @Schema(description = "模版编码", example = "test_01")
+    private String code;
+
+    @Schema(description = "模版名称", example = "我是名称")
+    private String name;
+
+    @Schema(description = "状态,参见 CommonStatusEnum 枚举类", example = "1")
+    private Integer status;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime[] createTime;
+
+}

+ 43 - 0
ktg-system/src/main/java/com/ktg/system/domain/template/NotifyTemplateRespVO.java

@@ -0,0 +1,43 @@
+package com.ktg.system.domain.template;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - 站内信模版 Response VO")
+@Data
+public class NotifyTemplateRespVO {
+
+    @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "模版名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试模版")
+    private String name;
+
+    @Schema(description = "模版编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "SEND_TEST")
+    private String code;
+
+    @Schema(description = "模版类型,对应 system_notify_template_type 字典", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer type;
+
+    @Schema(description = "发送人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆")
+    private String nickname;
+
+    @Schema(description = "模版内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是模版内容")
+    private String content;
+
+    @Schema(description = "参数数组", example = "name,code")
+    private List<String> params;
+
+    @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer status;
+
+    @Schema(description = "备注", example = "我是备注")
+    private String remark;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

+ 43 - 0
ktg-system/src/main/java/com/ktg/system/domain/template/NotifyTemplateSaveReqVO.java

@@ -0,0 +1,43 @@
+package com.ktg.system.domain.template;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 站内信模版创建/修改 Request VO")
+@Data
+public class NotifyTemplateSaveReqVO {
+
+    @Schema(description = "ID", example = "1024")
+    private Long id;
+
+    @Schema(description = "模版名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试模版")
+    @NotEmpty(message = "模版名称不能为空")
+    private String name;
+
+    @Schema(description = "模版编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "SEND_TEST")
+    @NotNull(message = "模版编码不能为空")
+    private String code;
+
+    @Schema(description = "模版类型,对应 system_notify_template_type 字典", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "模版类型不能为空")
+    private Integer type;
+
+    @Schema(description = "发送人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆")
+    @NotEmpty(message = "发送人名称不能为空")
+    private String nickname;
+
+    @Schema(description = "模版内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是模版内容")
+    @NotEmpty(message = "模版内容不能为空")
+    private String content;
+
+    @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "状态不能为空")
+    private Integer status;
+
+    @Schema(description = "备注", example = "我是备注")
+    private String remark;
+
+}

+ 29 - 0
ktg-system/src/main/java/com/ktg/system/domain/template/NotifyTemplateSendReqVO.java

@@ -0,0 +1,29 @@
+package com.ktg.system.domain.template;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.Map;
+
+@Schema(description = "管理后台 - 站内信模板的发送 Request VO")
+@Data
+public class NotifyTemplateSendReqVO {
+
+    @Schema(description = "用户id", requiredMode = Schema.RequiredMode.REQUIRED, example = "01")
+    @NotNull(message = "用户id不能为空")
+    private Long userId;
+
+    @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "用户类型不能为空")
+    private Integer userType;
+
+    @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "01")
+    @NotEmpty(message = "模板编码不能为空")
+    private String templateCode;
+
+    @Schema(description = "模板参数")
+    private Map<String, Object> templateParams;
+
+}

+ 76 - 0
ktg-system/src/main/java/com/ktg/system/mapper/NotifyMessageMapper.java

@@ -0,0 +1,76 @@
+package com.ktg.system.mapper;
+
+import com.ktg.common.mapper.BaseMapperX;
+import com.ktg.common.pojo.PageResult;
+import com.ktg.common.query.LambdaQueryWrapperX;
+import com.ktg.common.query.QueryWrapperX;
+import com.ktg.system.domain.NotifyMessageDO;
+import com.ktg.system.domain.message.NotifyMessageMyPageReqVO;
+import com.ktg.system.domain.message.NotifyMessagePageReqVO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.List;
+
+@Mapper
+public interface NotifyMessageMapper extends BaseMapperX<NotifyMessageDO> {
+
+    default PageResult<NotifyMessageDO> selectPage(NotifyMessagePageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<NotifyMessageDO>()
+                .eqIfPresent(NotifyMessageDO::getUserId, reqVO.getUserId())
+                .eqIfPresent(NotifyMessageDO::getUserType, reqVO.getUserType())
+                .likeIfPresent(NotifyMessageDO::getTemplateCode, reqVO.getTemplateCode())
+                .eqIfPresent(NotifyMessageDO::getTemplateType, reqVO.getTemplateType())
+                .betweenIfPresent(NotifyMessageDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(NotifyMessageDO::getId));
+    }
+
+    default PageResult<NotifyMessageDO> selectPage(NotifyMessageMyPageReqVO reqVO, Long userId, Integer userType) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<NotifyMessageDO>()
+                .eqIfPresent(NotifyMessageDO::getReadStatus, reqVO.getReadStatus())
+                .betweenIfPresent(NotifyMessageDO::getCreateTime, reqVO.getCreateTime())
+                .eq(NotifyMessageDO::getUserId, userId)
+                .eq(NotifyMessageDO::getUserType, userType)
+                .orderByDesc(NotifyMessageDO::getId));
+    }
+
+    default int updateListRead(Collection<Long> ids, Long userId, Integer userType) {
+        NotifyMessageDO notifyMessageDO = new NotifyMessageDO();
+        notifyMessageDO.setReadStatus(true);
+        notifyMessageDO.setReadTime(LocalDateTime.now());
+        return update(notifyMessageDO,
+                new LambdaQueryWrapperX<NotifyMessageDO>()
+                        .in(NotifyMessageDO::getId, ids)
+                        .eq(NotifyMessageDO::getUserId, userId)
+                        .eq(NotifyMessageDO::getUserType, userType)
+                        .eq(NotifyMessageDO::getReadStatus, false));
+    }
+
+    default int updateListRead(Long userId, Integer userType) {
+        NotifyMessageDO notifyMessageDO = new NotifyMessageDO();
+        notifyMessageDO.setReadStatus(true);
+        notifyMessageDO.setReadTime(LocalDateTime.now());
+        return update(notifyMessageDO,
+                new LambdaQueryWrapperX<NotifyMessageDO>()
+                        .eq(NotifyMessageDO::getUserId, userId)
+                        .eq(NotifyMessageDO::getUserType, userType)
+                        .eq(NotifyMessageDO::getReadStatus, false));
+    }
+
+    default List<NotifyMessageDO> selectUnreadListByUserIdAndUserType(Long userId, Integer userType, Integer size) {
+        return selectList(new QueryWrapperX<NotifyMessageDO>() // 由于要使用 limitN 语句,所以只能用 QueryWrapperX
+                .eq("user_id", userId)
+                .eq("user_type", userType)
+                .eq("read_status", false)
+                .orderByDesc("id").last("limit " + size));
+    }
+
+    default Long selectUnreadCountByUserIdAndUserType(Long userId, Integer userType) {
+        return selectCount(new LambdaQueryWrapperX<NotifyMessageDO>()
+                .eq(NotifyMessageDO::getReadStatus, false)
+                .eq(NotifyMessageDO::getUserId, userId)
+                .eq(NotifyMessageDO::getUserType, userType));
+    }
+
+}

+ 26 - 0
ktg-system/src/main/java/com/ktg/system/mapper/NotifyTemplateMapper.java

@@ -0,0 +1,26 @@
+package com.ktg.system.mapper;
+
+import com.ktg.common.mapper.BaseMapperX;
+import com.ktg.common.pojo.PageResult;
+import com.ktg.common.query.LambdaQueryWrapperX;
+import com.ktg.system.domain.NotifyTemplateDO;
+import com.ktg.system.domain.template.NotifyTemplatePageReqVO;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface NotifyTemplateMapper extends BaseMapperX<NotifyTemplateDO> {
+
+    default NotifyTemplateDO selectByCode(String code) {
+        return selectOne(NotifyTemplateDO::getCode, code);
+    }
+
+    default PageResult<NotifyTemplateDO> selectPage(NotifyTemplatePageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<NotifyTemplateDO>()
+                .likeIfPresent(NotifyTemplateDO::getCode, reqVO.getCode())
+                .likeIfPresent(NotifyTemplateDO::getName, reqVO.getName())
+                .eqIfPresent(NotifyTemplateDO::getStatus, reqVO.getStatus())
+                .betweenIfPresent(NotifyTemplateDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(NotifyTemplateDO::getId));
+    }
+
+}

+ 98 - 0
ktg-system/src/main/java/com/ktg/system/service/NotifyMessageService.java

@@ -0,0 +1,98 @@
+package com.ktg.system.service;
+
+
+import com.ktg.common.pojo.PageResult;
+import com.ktg.system.domain.NotifyMessageDO;
+import com.ktg.system.domain.NotifyTemplateDO;
+import com.ktg.system.domain.message.NotifyMessageMyPageReqVO;
+import com.ktg.system.domain.message.NotifyMessagePageReqVO;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 站内信 Service 接口
+ *
+ * @author xrcoder
+ */
+public interface NotifyMessageService {
+
+    /**
+     * 创建站内信
+     *
+     * @param userId 用户编号
+     * @param userType 用户类型
+     * @param template 模版信息
+     * @param templateContent 模版内容
+     * @param templateParams 模版参数
+     * @return 站内信编号
+     */
+    Long createNotifyMessage(Long userId, Integer userType,
+                             NotifyTemplateDO template, String templateContent, Map<String, Object> templateParams);
+
+    /**
+     * 获得站内信分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 站内信分页
+     */
+    PageResult<NotifyMessageDO> getNotifyMessagePage(NotifyMessagePageReqVO pageReqVO);
+
+    /**
+     * 获得【我的】站内信分页
+     *
+     * @param pageReqVO 分页查询
+     * @param userId 用户编号
+     * @param userType 用户类型
+     * @return 站内信分页
+     */
+    PageResult<NotifyMessageDO> getMyMyNotifyMessagePage(NotifyMessageMyPageReqVO pageReqVO, Long userId, Integer userType);
+
+    /**
+     * 获得站内信
+     *
+     * @param id 编号
+     * @return 站内信
+     */
+    NotifyMessageDO getNotifyMessage(Long id);
+
+    /**
+     * 获得【我的】未读站内信列表
+     *
+     * @param userId   用户编号
+     * @param userType 用户类型
+     * @param size     数量
+     * @return 站内信列表
+     */
+    List<NotifyMessageDO> getUnreadNotifyMessageList(Long userId, Integer userType, Integer size);
+
+    /**
+     * 统计用户未读站内信条数
+     *
+     * @param userId   用户编号
+     * @param userType 用户类型
+     * @return 返回未读站内信条数
+     */
+    Long getUnreadNotifyMessageCount(Long userId, Integer userType);
+
+    /**
+     * 标记站内信为已读
+     *
+     * @param ids    站内信编号集合
+     * @param userId 用户编号
+     * @param userType 用户类型
+     * @return 更新到的条数
+     */
+    int updateNotifyMessageRead(Collection<Long> ids, Long userId, Integer userType);
+
+    /**
+     * 标记所有站内信为已读
+     *
+     * @param userId   用户编号
+     * @param userType 用户类型
+     * @return 更新到的条数
+     */
+    int updateAllNotifyMessageRead(Long userId, Integer userType);
+
+}

+ 55 - 0
ktg-system/src/main/java/com/ktg/system/service/NotifySendService.java

@@ -0,0 +1,55 @@
+package com.ktg.system.service;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 站内信发送 Service 接口
+ *
+ * @author xrcoder
+ */
+public interface NotifySendService {
+
+    /**
+     * 发送单条站内信给管理后台的用户
+     *
+     * 在 mobile 为空时,使用 userId 加载对应管理员的手机号
+     *
+     * @param userId 用户编号
+     * @param templateCode 短信模板编号
+     * @param templateParams 短信模板参数
+     * @return 发送日志编号
+     */
+    Long sendSingleNotifyToAdmin(Long userId,
+                                 String templateCode, Map<String, Object> templateParams);
+    /**
+     * 发送单条站内信给用户 APP 的用户
+     *
+     * 在 mobile 为空时,使用 userId 加载对应会员的手机号
+     *
+     * @param userId 用户编号
+     * @param templateCode 站内信模板编号
+     * @param templateParams 站内信模板参数
+     * @return 发送日志编号
+     */
+    Long sendSingleNotifyToMember(Long userId,
+                                  String templateCode, Map<String, Object> templateParams);
+
+    /**
+     * 发送单条站内信给用户
+     *
+     * @param userId 用户编号
+     * @param userType 用户类型
+     * @param templateCode 站内信模板编号
+     * @param templateParams 站内信模板参数
+     * @return 发送日志编号
+     */
+    Long sendSingleNotify( Long userId, Integer userType,
+                           String templateCode, Map<String, Object> templateParams);
+
+    default void sendBatchNotify(List<String> mobiles, List<Long> userIds, Integer userType,
+                                 String templateCode, Map<String, Object> templateParams) {
+        throw new UnsupportedOperationException("暂时不支持该操作,感兴趣可以实现该功能哟!");
+    }
+
+}

+ 74 - 0
ktg-system/src/main/java/com/ktg/system/service/NotifyTemplateService.java

@@ -0,0 +1,74 @@
+package com.ktg.system.service;
+
+
+import com.ktg.common.pojo.PageResult;
+import com.ktg.system.domain.NotifyTemplateDO;
+import com.ktg.system.domain.template.NotifyTemplatePageReqVO;
+import com.ktg.system.domain.template.NotifyTemplateSaveReqVO;
+
+import javax.validation.Valid;
+import java.util.Map;
+
+/**
+ * 站内信模版 Service 接口
+ *
+ * @author xrcoder
+ */
+public interface NotifyTemplateService {
+
+    /**
+     * 创建站内信模版
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createNotifyTemplate(@Valid NotifyTemplateSaveReqVO createReqVO);
+
+    /**
+     * 更新站内信模版
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateNotifyTemplate(@Valid NotifyTemplateSaveReqVO updateReqVO);
+
+    /**
+     * 删除站内信模版
+     *
+     * @param id 编号
+     */
+    void deleteNotifyTemplate(Long id);
+
+    /**
+     * 获得站内信模版
+     *
+     * @param id 编号
+     * @return 站内信模版
+     */
+    NotifyTemplateDO getNotifyTemplate(Long id);
+
+    /**
+     * 获得站内信模板,从缓存中
+     *
+     * @param code 模板编码
+     * @return 站内信模板
+     */
+    NotifyTemplateDO getNotifyTemplateByCodeFromCache(String code);
+
+    /**
+     * 获得站内信模版分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 站内信模版分页
+     */
+    PageResult<NotifyTemplateDO> getNotifyTemplatePage(NotifyTemplatePageReqVO pageReqVO);
+
+    /**
+     * 格式化站内信内容
+     *
+     * @param content 站内信模板的内容
+     * @param params 站内信内容的参数
+     * @return 格式化后的内容
+     */
+    String formatNotifyTemplateContent(String content, Map<String, Object> params);
+
+}

+ 82 - 0
ktg-system/src/main/java/com/ktg/system/service/impl/NotifyMessageServiceImpl.java

@@ -0,0 +1,82 @@
+package com.ktg.system.service.impl;
+
+import com.ktg.common.pojo.PageResult;
+import com.ktg.system.domain.NotifyMessageDO;
+import com.ktg.system.domain.NotifyTemplateDO;
+import com.ktg.system.domain.message.NotifyMessageMyPageReqVO;
+import com.ktg.system.domain.message.NotifyMessagePageReqVO;
+import com.ktg.system.mapper.NotifyMessageMapper;
+import com.ktg.system.service.NotifyMessageService;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 站内信 Service 实现类
+ *
+ * @author xrcoder
+ */
+@Service
+@Validated
+public class NotifyMessageServiceImpl implements NotifyMessageService {
+
+    @Resource
+    private NotifyMessageMapper notifyMessageMapper;
+
+    @Override
+    public Long createNotifyMessage(Long userId, Integer userType,
+                                    NotifyTemplateDO template, String templateContent, Map<String, Object> templateParams) {
+        NotifyMessageDO message = new NotifyMessageDO();
+        message.setUserId(userId);
+        message.setUserType(userType);
+        message.setTemplateId(template.getId());
+        message.setTemplateCode(template.getCode());
+        message.setTemplateType(template.getType());
+        message.setTemplateNickname(template.getNickname());
+        message.setTemplateContent(templateContent);
+        message.setTemplateParams(templateParams);
+        message.setReadStatus(false);
+        notifyMessageMapper.insert(message);
+        return message.getId();
+    }
+
+    @Override
+    public PageResult<NotifyMessageDO> getNotifyMessagePage(NotifyMessagePageReqVO pageReqVO) {
+        return notifyMessageMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public PageResult<NotifyMessageDO> getMyMyNotifyMessagePage(NotifyMessageMyPageReqVO pageReqVO, Long userId, Integer userType) {
+        return notifyMessageMapper.selectPage(pageReqVO, userId, userType);
+    }
+
+    @Override
+    public NotifyMessageDO getNotifyMessage(Long id) {
+        return notifyMessageMapper.selectById(id);
+    }
+
+    @Override
+    public List<NotifyMessageDO> getUnreadNotifyMessageList(Long userId, Integer userType, Integer size) {
+        return notifyMessageMapper.selectUnreadListByUserIdAndUserType(userId, userType, size);
+    }
+
+    @Override
+    public Long getUnreadNotifyMessageCount(Long userId, Integer userType) {
+        return notifyMessageMapper.selectUnreadCountByUserIdAndUserType(userId, userType);
+    }
+
+    @Override
+    public int updateNotifyMessageRead(Collection<Long> ids, Long userId, Integer userType) {
+        return notifyMessageMapper.updateListRead(ids, userId, userType);
+    }
+
+    @Override
+    public int updateAllNotifyMessageRead(Long userId, Integer userType) {
+        return notifyMessageMapper.updateListRead(userId, userType);
+    }
+
+}

+ 91 - 0
ktg-system/src/main/java/com/ktg/system/service/impl/NotifySendServiceImpl.java

@@ -0,0 +1,91 @@
+package com.ktg.system.service.impl;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.ktg.system.domain.NotifyTemplateDO;
+import com.ktg.system.service.NotifyMessageService;
+import com.ktg.system.service.NotifySendService;
+import com.ktg.system.service.NotifyTemplateService;
+import com.ktg.system.strategy.CommonStatusEnum;
+import com.ktg.system.strategy.UserTypeEnum;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Map;
+import java.util.Objects;
+
+import static com.ktg.system.strategy.ErrorCodeConstants.NOTICE_NOT_FOUND;
+import static com.ktg.system.strategy.ErrorCodeConstants.NOTIFY_SEND_TEMPLATE_PARAM_MISS;
+import static com.ktg.system.strategy.ServiceExceptionUtil.exception;
+
+
+/**
+ * 站内信发送 Service 实现类
+ *
+ * @author xrcoder
+ */
+@Service
+@Validated
+@Slf4j
+public class NotifySendServiceImpl implements NotifySendService {
+
+    @Resource
+    private NotifyTemplateService notifyTemplateService;
+
+    @Resource
+    private NotifyMessageService notifyMessageService;
+
+    @Override
+    public Long sendSingleNotifyToAdmin(Long userId, String templateCode, Map<String, Object> templateParams) {
+        return sendSingleNotify(userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams);
+    }
+
+    @Override
+    public Long sendSingleNotifyToMember(Long userId, String templateCode, Map<String, Object> templateParams) {
+        return sendSingleNotify(userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams);
+    }
+
+    @Override
+    public Long sendSingleNotify(Long userId, Integer userType, String templateCode, Map<String, Object> templateParams) {
+        // 校验模版
+        NotifyTemplateDO template = validateNotifyTemplate(templateCode);
+        if (Objects.equals(template.getStatus(), CommonStatusEnum.DISABLE.getStatus())) {
+            log.info("[sendSingleNotify][模版({})已经关闭,无法给用户({}/{})发送]", templateCode, userId, userType);
+            return null;
+        }
+        // 校验参数
+        validateTemplateParams(template, templateParams);
+
+        // 发送站内信
+        String content = notifyTemplateService.formatNotifyTemplateContent(template.getContent(), templateParams);
+        return notifyMessageService.createNotifyMessage(userId, userType, template, content, templateParams);
+    }
+
+    @VisibleForTesting
+    public NotifyTemplateDO validateNotifyTemplate(String templateCode) {
+        // 获得站内信模板。考虑到效率,从缓存中获取
+        NotifyTemplateDO template = notifyTemplateService.getNotifyTemplateByCodeFromCache(templateCode);
+        // 站内信模板不存在
+        if (template == null) {
+            throw exception(NOTICE_NOT_FOUND);
+        }
+        return template;
+    }
+
+    /**
+     * 校验站内信模版参数是否确实
+     *
+     * @param template 邮箱模板
+     * @param templateParams 参数列表
+     */
+    @VisibleForTesting
+    public void validateTemplateParams(NotifyTemplateDO template, Map<String, Object> templateParams) {
+        template.getParams().forEach(key -> {
+            Object value = templateParams.get(key);
+            if (value == null) {
+                throw exception(NOTIFY_SEND_TEMPLATE_PARAM_MISS, key);
+            }
+        });
+    }
+}

+ 140 - 0
ktg-system/src/main/java/com/ktg/system/service/impl/NotifyTemplateServiceImpl.java

@@ -0,0 +1,140 @@
+package com.ktg.system.service.impl;
+
+import cn.hutool.core.util.ReUtil;
+import cn.hutool.core.util.StrUtil;
+import com.google.common.annotations.VisibleForTesting;
+import com.ktg.common.pojo.PageResult;
+import com.ktg.common.utils.bean.BeanUtils;
+import com.ktg.system.domain.NotifyTemplateDO;
+import com.ktg.system.domain.template.NotifyTemplatePageReqVO;
+import com.ktg.system.domain.template.NotifyTemplateSaveReqVO;
+import com.ktg.system.mapper.NotifyTemplateMapper;
+import com.ktg.system.service.NotifyTemplateService;
+import com.ktg.system.strategy.RedisKeyConstants;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import static com.ktg.system.strategy.ErrorCodeConstants.NOTIFY_TEMPLATE_CODE_DUPLICATE;
+import static com.ktg.system.strategy.ErrorCodeConstants.NOTIFY_TEMPLATE_NOT_EXISTS;
+import static com.ktg.system.strategy.ServiceExceptionUtil.exception;
+
+
+/**
+ * 站内信模版 Service 实现类
+ *
+ * @author xrcoder
+ */
+@Service
+@Validated
+@Slf4j
+public class NotifyTemplateServiceImpl implements NotifyTemplateService {
+
+    /**
+     * 正则表达式,匹配 {} 中的变量
+     */
+    private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{(.*?)}");
+
+    @Resource
+    private NotifyTemplateMapper notifyTemplateMapper;
+
+    @Override
+    public Long createNotifyTemplate(NotifyTemplateSaveReqVO createReqVO) {
+        // 校验站内信编码是否重复
+        validateNotifyTemplateCodeDuplicate(null, createReqVO.getCode());
+
+        // 插入
+        NotifyTemplateDO notifyTemplate = BeanUtils.toBean(createReqVO, NotifyTemplateDO.class);
+        notifyTemplate.setParams(parseTemplateContentParams(notifyTemplate.getContent()));
+        notifyTemplateMapper.insert(notifyTemplate);
+        return notifyTemplate.getId();
+    }
+
+    @Override
+    @CacheEvict(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE,
+            allEntries = true) // allEntries 清空所有缓存,因为可能修改到 code 字段,不好清理
+    public void updateNotifyTemplate(NotifyTemplateSaveReqVO updateReqVO) {
+        // 校验存在
+        validateNotifyTemplateExists(updateReqVO.getId());
+        // 校验站内信编码是否重复
+        validateNotifyTemplateCodeDuplicate(updateReqVO.getId(), updateReqVO.getCode());
+
+        // 更新
+        NotifyTemplateDO updateObj = BeanUtils.toBean(updateReqVO, NotifyTemplateDO.class);
+        updateObj.setParams(parseTemplateContentParams(updateObj.getContent()));
+        notifyTemplateMapper.updateById(updateObj);
+    }
+
+    @VisibleForTesting
+    public List<String> parseTemplateContentParams(String content) {
+        return ReUtil.findAllGroup1(PATTERN_PARAMS, content);
+    }
+
+    @Override
+    @CacheEvict(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE,
+            allEntries = true) // allEntries 清空所有缓存,因为 id 不是直接的缓存 code,不好清理
+    public void deleteNotifyTemplate(Long id) {
+        // 校验存在
+        validateNotifyTemplateExists(id);
+        // 删除
+        notifyTemplateMapper.deleteById(id);
+    }
+
+    private void validateNotifyTemplateExists(Long id) {
+        if (notifyTemplateMapper.selectById(id) == null) {
+            throw exception(NOTIFY_TEMPLATE_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public NotifyTemplateDO getNotifyTemplate(Long id) {
+        return notifyTemplateMapper.selectById(id);
+    }
+
+    @Override
+    @Cacheable(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE, key = "#code",
+            unless = "#result == null")
+    public NotifyTemplateDO getNotifyTemplateByCodeFromCache(String code) {
+        return notifyTemplateMapper.selectByCode(code);
+    }
+
+    @Override
+    public PageResult<NotifyTemplateDO> getNotifyTemplatePage(NotifyTemplatePageReqVO pageReqVO) {
+        return notifyTemplateMapper.selectPage(pageReqVO);
+    }
+
+    @VisibleForTesting
+    void validateNotifyTemplateCodeDuplicate(Long id, String code) {
+        NotifyTemplateDO template = notifyTemplateMapper.selectByCode(code);
+        if (template == null) {
+            return;
+        }
+        // 如果 id 为空,说明不用比较是否为相同 id 的字典类型
+        if (id == null) {
+            throw exception(NOTIFY_TEMPLATE_CODE_DUPLICATE, code);
+        }
+        if (!template.getId().equals(id)) {
+            throw exception(NOTIFY_TEMPLATE_CODE_DUPLICATE, code);
+        }
+    }
+
+    /**
+     * 格式化站内信内容
+     *
+     * @param content 站内信模板的内容
+     * @param params  站内信内容的参数
+     * @return 格式化后的内容
+     */
+    @Override
+    public String formatNotifyTemplateContent(String content, Map<String, Object> params) {
+        return StrUtil.format(content, params);
+    }
+
+}

+ 44 - 0
ktg-system/src/main/java/com/ktg/system/strategy/CommonStatusEnum.java

@@ -0,0 +1,44 @@
+package com.ktg.system.strategy;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 通用状态枚举
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum CommonStatusEnum implements IntArrayValuable {
+
+    ENABLE(0, "开启"),
+    DISABLE(1, "关闭");
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CommonStatusEnum::getStatus).toArray();
+
+    /**
+     * 状态值
+     */
+    private final Integer status;
+    /**
+     * 状态名
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+    public static boolean isEnable(Integer status) {
+        return ENABLE.status.equals(status);
+    }
+
+    public static boolean isDisable(Integer status) {
+        return DISABLE.status.equals(status);
+    }
+
+}

+ 166 - 0
ktg-system/src/main/java/com/ktg/system/strategy/ErrorCodeConstants.java

@@ -0,0 +1,166 @@
+package com.ktg.system.strategy;
+
+
+import com.ktg.common.exception.ErrorCode;
+
+/**
+ * System 错误码枚举类
+ *
+ * system 系统,使用 1-002-000-000 段
+ */
+public interface ErrorCodeConstants {
+
+    // ========== AUTH 模块 1-002-000-000 ==========
+    ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1_002_000_000, "登录失败,账号密码不正确");
+    ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1_002_000_001, "登录失败,账号被禁用");
+    ErrorCode AUTH_LOGIN_CAPTCHA_CODE_ERROR = new ErrorCode(1_002_000_004, "验证码不正确,原因:{}");
+    ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1_002_000_005, "未绑定账号,需要进行绑定");
+    ErrorCode AUTH_MOBILE_NOT_EXISTS = new ErrorCode(1_002_000_007, "手机号不存在");
+
+    // ========== 菜单模块 1-002-001-000 ==========
+    ErrorCode MENU_NAME_DUPLICATE = new ErrorCode(1_002_001_000, "已经存在该名字的菜单");
+    ErrorCode MENU_PARENT_NOT_EXISTS = new ErrorCode(1_002_001_001, "父菜单不存在");
+    ErrorCode MENU_PARENT_ERROR = new ErrorCode(1_002_001_002, "不能设置自己为父菜单");
+    ErrorCode MENU_NOT_EXISTS = new ErrorCode(1_002_001_003, "菜单不存在");
+    ErrorCode MENU_EXISTS_CHILDREN = new ErrorCode(1_002_001_004, "存在子菜单,无法删除");
+    ErrorCode MENU_PARENT_NOT_DIR_OR_MENU = new ErrorCode(1_002_001_005, "父菜单的类型必须是目录或者菜单");
+
+    // ========== 角色模块 1-002-002-000 ==========
+    ErrorCode ROLE_NOT_EXISTS = new ErrorCode(1_002_002_000, "角色不存在");
+    ErrorCode ROLE_NAME_DUPLICATE = new ErrorCode(1_002_002_001, "已经存在名为【{}】的角色");
+    ErrorCode ROLE_CODE_DUPLICATE = new ErrorCode(1_002_002_002, "已经存在标识为【{}】的角色");
+    ErrorCode ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE = new ErrorCode(1_002_002_003, "不能操作类型为系统内置的角色");
+    ErrorCode ROLE_IS_DISABLE = new ErrorCode(1_002_002_004, "名字为【{}】的角色已被禁用");
+    ErrorCode ROLE_ADMIN_CODE_ERROR = new ErrorCode(1_002_002_005, "标识【{}】不能使用");
+
+    // ========== 用户模块 1-002-003-000 ==========
+    ErrorCode USER_USERNAME_EXISTS = new ErrorCode(1_002_003_000, "用户账号已经存在");
+    ErrorCode USER_MOBILE_EXISTS = new ErrorCode(1_002_003_001, "手机号已经存在");
+    ErrorCode USER_EMAIL_EXISTS = new ErrorCode(1_002_003_002, "邮箱已经存在");
+    ErrorCode USER_NOT_EXISTS = new ErrorCode(1_002_003_003, "用户不存在");
+    ErrorCode USER_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_002_003_004, "导入用户数据不能为空!");
+    ErrorCode USER_PASSWORD_FAILED = new ErrorCode(1_002_003_005, "用户密码校验失败");
+    ErrorCode USER_IS_DISABLE = new ErrorCode(1_002_003_006, "名字为【{}】的用户已被禁用");
+    ErrorCode USER_COUNT_MAX = new ErrorCode(1_002_003_008, "创建用户失败,原因:超过租户最大租户配额({})!");
+    ErrorCode USER_IMPORT_INIT_PASSWORD = new ErrorCode(1_002_003_009, "初始密码不能为空");
+
+    // ========== 部门模块 1-002-004-000 ==========
+    ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1_002_004_000, "已经存在该名字的部门");
+    ErrorCode DEPT_PARENT_NOT_EXITS = new ErrorCode(1_002_004_001,"父级部门不存在");
+    ErrorCode DEPT_NOT_FOUND = new ErrorCode(1_002_004_002, "当前部门不存在");
+    ErrorCode DEPT_EXITS_CHILDREN = new ErrorCode(1_002_004_003, "存在子部门,无法删除");
+    ErrorCode DEPT_PARENT_ERROR = new ErrorCode(1_002_004_004, "不能设置自己为父部门");
+    ErrorCode DEPT_NOT_ENABLE = new ErrorCode(1_002_004_006, "部门({})不处于开启状态,不允许选择");
+    ErrorCode DEPT_PARENT_IS_CHILD = new ErrorCode(1_002_004_007, "不能设置自己的子部门为父部门");
+
+    // ========== 岗位模块 1-002-005-000 ==========
+    ErrorCode POST_NOT_FOUND = new ErrorCode(1_002_005_000, "当前岗位不存在");
+    ErrorCode POST_NOT_ENABLE = new ErrorCode(1_002_005_001, "岗位({}) 不处于开启状态,不允许选择");
+    ErrorCode POST_NAME_DUPLICATE = new ErrorCode(1_002_005_002, "已经存在该名字的岗位");
+    ErrorCode POST_CODE_DUPLICATE = new ErrorCode(1_002_005_003, "已经存在该标识的岗位");
+
+    // ========== 字典类型 1-002-006-000 ==========
+    ErrorCode DICT_TYPE_NOT_EXISTS = new ErrorCode(1_002_006_001, "当前字典类型不存在");
+    ErrorCode DICT_TYPE_NOT_ENABLE = new ErrorCode(1_002_006_002, "字典类型不处于开启状态,不允许选择");
+    ErrorCode DICT_TYPE_NAME_DUPLICATE = new ErrorCode(1_002_006_003, "已经存在该名字的字典类型");
+    ErrorCode DICT_TYPE_TYPE_DUPLICATE = new ErrorCode(1_002_006_004, "已经存在该类型的字典类型");
+    ErrorCode DICT_TYPE_HAS_CHILDREN = new ErrorCode(1_002_006_005, "无法删除,该字典类型还有字典数据");
+
+    // ========== 字典数据 1-002-007-000 ==========
+    ErrorCode DICT_DATA_NOT_EXISTS = new ErrorCode(1_002_007_001, "当前字典数据不存在");
+    ErrorCode DICT_DATA_NOT_ENABLE = new ErrorCode(1_002_007_002, "字典数据({})不处于开启状态,不允许选择");
+    ErrorCode DICT_DATA_VALUE_DUPLICATE = new ErrorCode(1_002_007_003, "已经存在该值的字典数据");
+
+    // ========== 通知公告 1-002-008-000 ==========
+    ErrorCode NOTICE_NOT_FOUND = new ErrorCode(1_002_008_001, "当前通知公告不存在");
+
+    // ========== 短信渠道 1-002-011-000 ==========
+    ErrorCode SMS_CHANNEL_NOT_EXISTS = new ErrorCode(1_002_011_000, "短信渠道不存在");
+    ErrorCode SMS_CHANNEL_DISABLE = new ErrorCode(1_002_011_001, "短信渠道不处于开启状态,不允许选择");
+    ErrorCode SMS_CHANNEL_HAS_CHILDREN = new ErrorCode(1_002_011_002, "无法删除,该短信渠道还有短信模板");
+
+    // ========== 短信模板 1-002-012-000 ==========
+    ErrorCode SMS_TEMPLATE_NOT_EXISTS = new ErrorCode(1_002_012_000, "短信模板不存在");
+    ErrorCode SMS_TEMPLATE_CODE_DUPLICATE = new ErrorCode(1_002_012_001, "已经存在编码为【{}】的短信模板");
+    ErrorCode SMS_TEMPLATE_API_ERROR = new ErrorCode(1_002_012_002, "短信 API 模板调用失败,原因是:{}");
+    ErrorCode SMS_TEMPLATE_API_AUDIT_CHECKING = new ErrorCode(1_002_012_003, "短信 API 模版无法使用,原因:审批中");
+    ErrorCode SMS_TEMPLATE_API_AUDIT_FAIL = new ErrorCode(1_002_012_004, "短信 API 模版无法使用,原因:审批不通过,{}");
+    ErrorCode SMS_TEMPLATE_API_NOT_FOUND = new ErrorCode(1_002_012_005, "短信 API 模版无法使用,原因:模版不存在");
+
+    // ========== 短信发送 1-002-013-000 ==========
+    ErrorCode SMS_SEND_MOBILE_NOT_EXISTS = new ErrorCode(1_002_013_000, "手机号不存在");
+    ErrorCode SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS = new ErrorCode(1_002_013_001, "模板参数({})缺失");
+    ErrorCode SMS_SEND_TEMPLATE_NOT_EXISTS = new ErrorCode(1_002_013_002, "短信模板不存在");
+
+    // ========== 短信验证码 1-002-014-000 ==========
+    ErrorCode SMS_CODE_NOT_FOUND = new ErrorCode(1_002_014_000, "验证码不存在");
+    ErrorCode SMS_CODE_EXPIRED = new ErrorCode(1_002_014_001, "验证码已过期");
+    ErrorCode SMS_CODE_USED = new ErrorCode(1_002_014_002, "验证码已使用");
+    ErrorCode SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY = new ErrorCode(1_002_014_004, "超过每日短信发送数量");
+    ErrorCode SMS_CODE_SEND_TOO_FAST = new ErrorCode(1_002_014_005, "短信发送过于频繁");
+
+    // ========== 租户信息 1-002-015-000 ==========
+    ErrorCode TENANT_NOT_EXISTS = new ErrorCode(1_002_015_000, "租户不存在");
+    ErrorCode TENANT_DISABLE = new ErrorCode(1_002_015_001, "名字为【{}】的租户已被禁用");
+    ErrorCode TENANT_EXPIRE = new ErrorCode(1_002_015_002, "名字为【{}】的租户已过期");
+    ErrorCode TENANT_CAN_NOT_UPDATE_SYSTEM = new ErrorCode(1_002_015_003, "系统租户不能进行修改、删除等操作!");
+    ErrorCode TENANT_NAME_DUPLICATE = new ErrorCode(1_002_015_004, "名字为【{}】的租户已存在");
+    ErrorCode TENANT_WEBSITE_DUPLICATE = new ErrorCode(1_002_015_005, "域名为【{}】的租户已存在");
+
+    // ========== 租户套餐 1-002-016-000 ==========
+    ErrorCode TENANT_PACKAGE_NOT_EXISTS = new ErrorCode(1_002_016_000, "租户套餐不存在");
+    ErrorCode TENANT_PACKAGE_USED = new ErrorCode(1_002_016_001, "租户正在使用该套餐,请给租户重新设置套餐后再尝试删除");
+    ErrorCode TENANT_PACKAGE_DISABLE = new ErrorCode(1_002_016_002, "名字为【{}】的租户套餐已被禁用");
+
+    // ========== 社交用户 1-002-018-000 ==========
+    ErrorCode SOCIAL_USER_AUTH_FAILURE = new ErrorCode(1_002_018_000, "社交授权失败,原因是:{}");
+    ErrorCode SOCIAL_USER_NOT_FOUND = new ErrorCode(1_002_018_001, "社交授权失败,找不到对应的用户");
+
+    ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_PHONE_CODE_ERROR = new ErrorCode(1_002_018_200, "获得手机号失败");
+    ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_QRCODE_ERROR = new ErrorCode(1_002_018_201, "获得小程序码失败");
+    ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_SUBSCRIBE_TEMPLATE_ERROR = new ErrorCode(1_002_018_202, "获得小程序订阅消息模版失败");
+    ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_SUBSCRIBE_MESSAGE_ERROR = new ErrorCode(1_002_018_203, "发送小程序订阅消息失败");
+    ErrorCode SOCIAL_CLIENT_NOT_EXISTS = new ErrorCode(1_002_018_210, "社交客户端不存在");
+    ErrorCode SOCIAL_CLIENT_UNIQUE = new ErrorCode(1_002_018_211, "社交客户端已存在配置");
+
+
+    // ========== OAuth2 客户端 1-002-020-000 =========
+    ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1_002_020_000, "OAuth2 客户端不存在");
+    ErrorCode OAUTH2_CLIENT_EXISTS = new ErrorCode(1_002_020_001, "OAuth2 客户端编号已存在");
+    ErrorCode OAUTH2_CLIENT_DISABLE = new ErrorCode(1_002_020_002, "OAuth2 客户端已禁用");
+    ErrorCode OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS = new ErrorCode(1_002_020_003, "不支持该授权类型");
+    ErrorCode OAUTH2_CLIENT_SCOPE_OVER = new ErrorCode(1_002_020_004, "授权范围过大");
+    ErrorCode OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH = new ErrorCode(1_002_020_005, "无效 redirect_uri: {}");
+    ErrorCode OAUTH2_CLIENT_CLIENT_SECRET_ERROR = new ErrorCode(1_002_020_006, "无效 client_secret: {}");
+
+    // ========== OAuth2 授权 1-002-021-000 =========
+    ErrorCode OAUTH2_GRANT_CLIENT_ID_MISMATCH = new ErrorCode(1_002_021_000, "client_id 不匹配");
+    ErrorCode OAUTH2_GRANT_REDIRECT_URI_MISMATCH = new ErrorCode(1_002_021_001, "redirect_uri 不匹配");
+    ErrorCode OAUTH2_GRANT_STATE_MISMATCH = new ErrorCode(1_002_021_002, "state 不匹配");
+
+    // ========== OAuth2 授权 1-002-022-000 =========
+    ErrorCode OAUTH2_CODE_NOT_EXISTS = new ErrorCode(1_002_022_000, "code 不存在");
+    ErrorCode OAUTH2_CODE_EXPIRE = new ErrorCode(1_002_022_001, "code 已过期");
+
+    // ========== 邮箱账号 1-002-023-000 ==========
+    ErrorCode MAIL_ACCOUNT_NOT_EXISTS = new ErrorCode(1_002_023_000, "邮箱账号不存在");
+    ErrorCode MAIL_ACCOUNT_RELATE_TEMPLATE_EXISTS = new ErrorCode(1_002_023_001, "无法删除,该邮箱账号还有邮件模板");
+
+    // ========== 邮件模版 1-002-024-000 ==========
+    ErrorCode MAIL_TEMPLATE_NOT_EXISTS = new ErrorCode(1_002_024_000, "邮件模版不存在");
+    ErrorCode MAIL_TEMPLATE_CODE_EXISTS = new ErrorCode(1_002_024_001, "邮件模版 code({}) 已存在");
+
+    // ========== 邮件发送 1-002-025-000 ==========
+    ErrorCode MAIL_SEND_TEMPLATE_PARAM_MISS = new ErrorCode(1_002_025_000, "模板参数({})缺失");
+    ErrorCode MAIL_SEND_MAIL_NOT_EXISTS = new ErrorCode(1_002_025_001, "邮箱不存在");
+
+    // ========== 站内信模版 1-002-026-000 ==========
+    ErrorCode NOTIFY_TEMPLATE_NOT_EXISTS = new ErrorCode(1_002_026_000, "站内信模版不存在");
+    ErrorCode NOTIFY_TEMPLATE_CODE_DUPLICATE = new ErrorCode(1_002_026_001, "已经存在编码为【{}】的站内信模板");
+
+    // ========== 站内信模版 1-002-027-000 ==========
+
+    // ========== 站内信发送 1-002-028-000 ==========
+    ErrorCode NOTIFY_SEND_TEMPLATE_PARAM_MISS = new ErrorCode(1_002_028_000, "模板参数({})缺失");
+
+}

+ 14 - 0
ktg-system/src/main/java/com/ktg/system/strategy/IntArrayValuable.java

@@ -0,0 +1,14 @@
+package com.ktg.system.strategy;
+
+/**
+ * 可生成 Int 数组的接口
+ *
+ */
+public interface IntArrayValuable {
+
+    /**
+     * @return int 数组
+     */
+    int[] array();
+
+}

+ 109 - 0
ktg-system/src/main/java/com/ktg/system/strategy/RedisKeyConstants.java

@@ -0,0 +1,109 @@
+package com.ktg.system.strategy;
+
+
+/**
+ * System Redis Key 枚举类
+ *
+ * @author 芋道源码
+ */
+public interface RedisKeyConstants {
+
+    /**
+     * 指定部门的所有子部门编号数组的缓存
+     * <p>
+     * KEY 格式:dept_children_ids:{id}
+     * VALUE 数据类型:String 子部门编号集合
+     */
+    String DEPT_CHILDREN_ID_LIST = "dept_children_ids";
+
+    /**
+     * 角色的缓存
+     * <p>
+     * KEY 格式:role:{id}
+     * VALUE 数据类型:String 角色信息
+     */
+    String ROLE = "role";
+
+    /**
+     * 用户拥有的角色编号的缓存
+     * <p>
+     * KEY 格式:user_role_ids:{userId}
+     * VALUE 数据类型:String 角色编号集合
+     */
+    String USER_ROLE_ID_LIST = "user_role_ids";
+
+    /**
+     * 拥有指定菜单的角色编号的缓存
+     * <p>
+     * KEY 格式:menu_role_ids:{menuId}
+     * VALUE 数据类型:String 角色编号集合
+     */
+    String MENU_ROLE_ID_LIST = "menu_role_ids";
+
+    /**
+     * 拥有权限对应的菜单编号数组的缓存
+     * <p>
+     * KEY 格式:permission_menu_ids:{permission}
+     * VALUE 数据类型:String 菜单编号数组
+     */
+    String PERMISSION_MENU_ID_LIST = "permission_menu_ids";
+
+    /**
+     * OAuth2 客户端的缓存
+     * <p>
+     * KEY 格式:oauth_client:{id}
+     * VALUE 数据类型:String 客户端信息
+     */
+    String OAUTH_CLIENT = "oauth_client";
+
+    /**
+     * 访问令牌的缓存
+     * <p>
+     * KEY 格式:oauth2_access_token:{token}
+     * VALUE 数据类型:String 访问令牌信息 {@link OAuth2AccessTokenDO}
+     * <p>
+     * 由于动态过期时间,使用 RedisTemplate 操作
+     */
+    String OAUTH2_ACCESS_TOKEN = "oauth2_access_token:%s";
+
+    /**
+     * 站内信模版的缓存
+     * <p>
+     * KEY 格式:notify_template:{code}
+     * VALUE 数据格式:String 模版信息
+     */
+    String NOTIFY_TEMPLATE = "notify_template";
+
+    /**
+     * 邮件账号的缓存
+     * <p>
+     * KEY 格式:mail_account:{id}
+     * VALUE 数据格式:String 账号信息
+     */
+    String MAIL_ACCOUNT = "mail_account";
+
+    /**
+     * 邮件模版的缓存
+     * <p>
+     * KEY 格式:mail_template:{code}
+     * VALUE 数据格式:String 模版信息
+     */
+    String MAIL_TEMPLATE = "mail_template";
+
+    /**
+     * 短信模版的缓存
+     * <p>
+     * KEY 格式:sms_template:{id}
+     * VALUE 数据格式:String 模版信息
+     */
+    String SMS_TEMPLATE = "sms_template";
+
+    /**
+     * 小程序订阅模版的缓存
+     *
+     * KEY 格式:wxa_subscribe_template:{userType}
+     * VALUE 数据格式 String, 模版信息
+     */
+    String WXA_SUBSCRIBE_TEMPLATE = "wxa_subscribe_template";
+
+}

+ 77 - 0
ktg-system/src/main/java/com/ktg/system/strategy/ServiceExceptionUtil.java

@@ -0,0 +1,77 @@
+package com.ktg.system.strategy;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.ktg.common.exception.ErrorCode;
+import com.ktg.common.exception.ServiceException;
+import com.ktg.common.exception.enums.GlobalErrorCodeConstants;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * {@link ServiceException} 工具类
+ *
+ * 目的在于,格式化异常信息提示。
+ * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化
+ *
+ */
+@Slf4j
+public class ServiceExceptionUtil {
+
+    // ========== 和 ServiceException 的集成 ==========
+
+    public static ServiceException exception(ErrorCode errorCode) {
+        return exception0(errorCode.getCode(), errorCode.getMsg());
+    }
+
+    public static ServiceException exception(ErrorCode errorCode, Object... params) {
+        return exception0(errorCode.getCode(), errorCode.getMsg(), params);
+    }
+
+    public static ServiceException exception0(Integer code, String messagePattern, Object... params) {
+        String message = doFormat(code, messagePattern, params);
+        return new ServiceException(code, message);
+    }
+
+    public static ServiceException invalidParamException(String messagePattern, Object... params) {
+        return exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), messagePattern, params);
+    }
+
+    // ========== 格式化方法 ==========
+
+    /**
+     * 将错误编号对应的消息使用 params 进行格式化。
+     *
+     * @param code           错误编号
+     * @param messagePattern 消息模版
+     * @param params         参数
+     * @return 格式化后的提示
+     */
+    @VisibleForTesting
+    public static String doFormat(int code, String messagePattern, Object... params) {
+        StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);
+        int i = 0;
+        int j;
+        int l;
+        for (l = 0; l < params.length; l++) {
+            j = messagePattern.indexOf("{}", i);
+            if (j == -1) {
+                log.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params);
+                if (i == 0) {
+                    return messagePattern;
+                } else {
+                    sbuf.append(messagePattern.substring(i));
+                    return sbuf.toString();
+                }
+            } else {
+                sbuf.append(messagePattern, i, j);
+                sbuf.append(params[l]);
+                i = j + 2;
+            }
+        }
+        if (messagePattern.indexOf("{}", i) != -1) {
+            log.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params);
+        }
+        sbuf.append(messagePattern.substring(i));
+        return sbuf.toString();
+    }
+
+}

+ 38 - 0
ktg-system/src/main/java/com/ktg/system/strategy/UserTypeEnum.java

@@ -0,0 +1,38 @@
+package com.ktg.system.strategy;
+
+import cn.hutool.core.util.ArrayUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 全局用户类型枚举
+ */
+@AllArgsConstructor
+@Getter
+public enum UserTypeEnum implements IntArrayValuable {
+
+    MEMBER(1, "会员"), // 面向 c 端,普通用户
+    ADMIN(2, "管理员"); // 面向 b 端,管理后台
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(UserTypeEnum::getValue).toArray();
+
+    /**
+     * 类型
+     */
+    private final Integer value;
+    /**
+     * 类型名
+     */
+    private final String name;
+
+    public static UserTypeEnum valueOf(Integer value) {
+        return ArrayUtil.firstMatch(userType -> userType.getValue().equals(value), UserTypeEnum.values());
+    }
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+}