Commit 2347bffeb199f6cd400a2f27aa66838d64f00ffc

Authored by 芋道源码
Committed by Gitee
2 parents 3025bff6 127a98a9

!980 MALL-KEHU: 新增客服相关

Merge pull request !980 from puhui999/develop
Showing 36 changed files with 1250 additions and 35 deletions
sql/mysql/mall-promotion-kefu.sql 0 → 100644
  1 +DROP TABLE IF EXISTS `promotion_kefu_conversation`;
  2 +CREATE TABLE `promotion_kefu_conversation` (
  3 + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
  4 + `user_id` BIGINT NOT NULL COMMENT '会话所属用户',
  5 + `last_message_time` DATETIME NOT NULL COMMENT '最后聊天时间',
  6 + `last_message_content` VARCHAR(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '最后聊天内容',
  7 + `last_message_content_type` INT NOT NULL COMMENT '最后发送的消息类型',
  8 + `admin_pinned` BIT(1) NOT NULL DEFAULT b'0' COMMENT '管理端置顶',
  9 + `user_deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '用户是否可见',
  10 + `admin_deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '管理员是否可见',
  11 + `admin_unread_message_count` INT NOT NULL COMMENT '管理员未读消息数',
  12 + `creator` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
  13 + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  14 + `updater` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
  15 + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  16 + `deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
  17 + PRIMARY KEY (`id`) USING BTREE
  18 +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '客服会话' ROW_FORMAT = Dynamic;
  19 +
  20 +DROP TABLE IF EXISTS `promotion_kefu_message`;
  21 +CREATE TABLE `promotion_kefu_message` (
  22 + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
  23 + `conversation_id` BIGINT NOT NULL COMMENT '会话编号',
  24 + `sender_id` BIGINT NOT NULL COMMENT '发送人编号',
  25 + `sender_type` INT NOT NULL COMMENT '发送人类型',
  26 + `receiver_id` BIGINT NOT NULL COMMENT '接收人编号',
  27 + `receiver_type` INT NOT NULL COMMENT '接收人类型',
  28 + `content_type` INT NOT NULL COMMENT '消息类型',
  29 + `content` VARCHAR(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '消息',
  30 + `read_status` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否已读',
  31 + `creator` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
  32 + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  33 + `updater` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
  34 + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  35 + `deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
  36 + PRIMARY KEY (`id`) USING BTREE
  37 +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '客服消息' ROW_FORMAT = Dynamic;
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java
@@ -125,4 +125,10 @@ public interface ErrorCodeConstants { @@ -125,4 +125,10 @@ public interface ErrorCodeConstants {
125 ErrorCode DIY_PAGE_NOT_EXISTS = new ErrorCode(1_013_018_000, "装修页面不存在"); 125 ErrorCode DIY_PAGE_NOT_EXISTS = new ErrorCode(1_013_018_000, "装修页面不存在");
126 ErrorCode DIY_PAGE_NAME_USED = new ErrorCode(1_013_018_001, "装修页面名称({})已经被使用"); 126 ErrorCode DIY_PAGE_NAME_USED = new ErrorCode(1_013_018_001, "装修页面名称({})已经被使用");
127 127
  128 + // ========== 客服会话 1-013-019-000 ==========
  129 + ErrorCode KEFU_CONVERSATION_NOT_EXISTS = new ErrorCode(1_013_019_000, "客服会话不存在");
  130 +
  131 + // ========== 客服消息 1-013-020-000 ==========
  132 + ErrorCode KEFU_MESSAGE_NOT_EXISTS = new ErrorCode(1_013_020_000, "客服消息不存在");
  133 +
128 } 134 }
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/kehu/KeFuMessageContentTypeEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.enums.kehu;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Getter;
  6 +
  7 +import java.util.Arrays;
  8 +
  9 +/**
  10 + * 消息类型枚举
  11 + *
  12 + * @author HUIHUI
  13 + */
  14 +@AllArgsConstructor
  15 +@Getter
  16 +public enum KeFuMessageContentTypeEnum implements IntArrayValuable {
  17 +
  18 + TEXT(1, "文本消息"),
  19 + IMAGE(2, "图片消息"),
  20 + VOICE(3, "语音消息"),
  21 + VIDEO(4, "视频消息"),
  22 + // 和正常消息隔离下
  23 + PRODUCT(10, "商品消息"),
  24 + ORDER(11, "订单消息");
  25 +
  26 + private static final int[] ARRAYS = Arrays.stream(values()).mapToInt(KeFuMessageContentTypeEnum::getType).toArray();
  27 +
  28 + /**
  29 + * 类型
  30 + */
  31 + private final Integer type;
  32 +
  33 + /**
  34 + * 名称
  35 + */
  36 + private final String name;
  37 +
  38 + @Override
  39 + public int[] array() {
  40 + return ARRAYS;
  41 + }
  42 +
  43 +}
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/KeFuConversationController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.controller.admin.kefu;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  5 +import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.conversation.KeFuConversationRespVO;
  6 +import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.conversation.KeFuConversationUpdatePinnedReqVO;
  7 +import cn.iocoder.yudao.module.promotion.service.kefu.KeFuConversationService;
  8 +import io.swagger.v3.oas.annotations.Operation;
  9 +import io.swagger.v3.oas.annotations.Parameter;
  10 +import io.swagger.v3.oas.annotations.tags.Tag;
  11 +import jakarta.annotation.Resource;
  12 +import jakarta.validation.Valid;
  13 +import org.springframework.security.access.prepost.PreAuthorize;
  14 +import org.springframework.validation.annotation.Validated;
  15 +import org.springframework.web.bind.annotation.*;
  16 +
  17 +import java.util.List;
  18 +
  19 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  20 +
  21 +@Tag(name = "管理后台 - 客服会话")
  22 +@RestController
  23 +@RequestMapping("/promotion/kefu-conversation")
  24 +@Validated
  25 +public class KeFuConversationController {
  26 +
  27 + @Resource
  28 + private KeFuConversationService conversationService;
  29 +
  30 + @PostMapping("/update-pinned")
  31 + @Operation(summary = "置顶客服会话")
  32 + @PreAuthorize("@ss.hasPermission('promotion:kefu-conversation:update')")
  33 + public CommonResult<Boolean> updatePinned(@Valid @RequestBody KeFuConversationUpdatePinnedReqVO updateReqVO) {
  34 + conversationService.updatePinned(updateReqVO);
  35 + return success(true);
  36 + }
  37 +
  38 + @DeleteMapping("/delete")
  39 + @Operation(summary = "删除客服会话")
  40 + @Parameter(name = "id", description = "编号", required = true)
  41 + @PreAuthorize("@ss.hasPermission('promotion:kefu-conversation:delete')")
  42 + public CommonResult<Boolean> deleteKefuConversation(@RequestParam("id") Long id) {
  43 + conversationService.deleteKefuConversation(id);
  44 + return success(true);
  45 + }
  46 +
  47 + @GetMapping("/list")
  48 + @Operation(summary = "获得客服会话列表")
  49 + @PreAuthorize("@ss.hasPermission('promotion:kefu-conversation:query')")
  50 + public CommonResult<List<KeFuConversationRespVO>> getKefuConversationPage() {
  51 + return success(BeanUtils.toBean(conversationService.getKefuConversationList(), KeFuConversationRespVO.class));
  52 + }
  53 +
  54 +}
0 \ No newline at end of file 55 \ No newline at end of file
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/KeFuMessageController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.controller.admin.kefu;
  2 +
  3 +import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessagePageReqVO;
  4 +import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageRespVO;
  5 +import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageSendReqVO;
  6 +import org.springframework.web.bind.annotation.*;
  7 +import jakarta.annotation.Resource;
  8 +import org.springframework.validation.annotation.Validated;
  9 +import org.springframework.security.access.prepost.PreAuthorize;
  10 +import io.swagger.v3.oas.annotations.tags.Tag;
  11 +import io.swagger.v3.oas.annotations.Parameter;
  12 +import io.swagger.v3.oas.annotations.Operation;
  13 +
  14 +import jakarta.validation.*;
  15 +
  16 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  17 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  18 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  19 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  20 +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
  21 +
  22 +import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuMessageDO;
  23 +import cn.iocoder.yudao.module.promotion.service.kefu.KeFuMessageService;
  24 +
  25 +@Tag(name = "管理后台 - 客服消息")
  26 +@RestController
  27 +@RequestMapping("/promotion/kefu-message")
  28 +@Validated
  29 +public class KeFuMessageController {
  30 +
  31 + @Resource
  32 + private KeFuMessageService messageService;
  33 +
  34 + @PostMapping("/send")
  35 + @Operation(summary = "发送客服消息")
  36 + @PreAuthorize("@ss.hasPermission('promotion:kefu-message:send')")
  37 + public CommonResult<Long> createKefuMessage(@Valid @RequestBody KeFuMessageSendReqVO sendReqVO) {
  38 + return success(messageService.sendKefuMessage(sendReqVO));
  39 + }
  40 +
  41 + @PutMapping("/update-read-status")
  42 + @Operation(summary = "更新客服消息已读状态")
  43 + @Parameter(name = "conversationId", description = "会话编号", required = true)
  44 + @PreAuthorize("@ss.hasPermission('promotion:kefu-message:update')")
  45 + public CommonResult<Boolean> updateKefuMessageReadStatus(@RequestParam("conversationId") Long conversationId) {
  46 + messageService.updateKefuMessageReadStatus(conversationId, getLoginUserId());
  47 + return success(true);
  48 + }
  49 +
  50 + @GetMapping("/page")
  51 + @Operation(summary = "获得客服消息分页")
  52 + @PreAuthorize("@ss.hasPermission('promotion:kefu-message:query')")
  53 + public CommonResult<PageResult<KeFuMessageRespVO>> getKefuMessagePage(@Valid KeFuMessagePageReqVO pageReqVO) {
  54 + PageResult<KeFuMessageDO> pageResult = messageService.getKefuMessagePage(pageReqVO);
  55 + return success(BeanUtils.toBean(pageResult, KeFuMessageRespVO.class));
  56 + }
  57 +
  58 +}
0 \ No newline at end of file 59 \ No newline at end of file
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/conversation/KeFuConversationRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.conversation;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.*;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - 客服会话 Response VO")
  9 +@Data
  10 +public class KeFuConversationRespVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24988")
  13 + private Long id;
  14 +
  15 + @Schema(description = "会话所属用户", requiredMode = Schema.RequiredMode.REQUIRED, example = "8300")
  16 + private Long userId;
  17 +
  18 + @Schema(description = "最后聊天时间", requiredMode = Schema.RequiredMode.REQUIRED,example = "2024-01-01 00:00:00")
  19 + private LocalDateTime lastMessageTime;
  20 +
  21 + @Schema(description = "最后聊天内容", requiredMode = Schema.RequiredMode.REQUIRED,example = "嗨,您好啊")
  22 + private String lastMessageContent;
  23 +
  24 + @Schema(description = "最后发送的消息类型", requiredMode = Schema.RequiredMode.REQUIRED,example = "1")
  25 + private Integer lastMessageContentType;
  26 +
  27 + @Schema(description = "管理端置顶", requiredMode = Schema.RequiredMode.REQUIRED,example = "false")
  28 + private Boolean adminPinned;
  29 +
  30 + @Schema(description = "用户是否可见", requiredMode = Schema.RequiredMode.REQUIRED,example = "true")
  31 + private Boolean userDeleted;
  32 +
  33 + @Schema(description = "管理员是否可见", requiredMode = Schema.RequiredMode.REQUIRED,example = "true")
  34 + private Boolean adminDeleted;
  35 +
  36 + @Schema(description = "管理员未读消息数", requiredMode = Schema.RequiredMode.REQUIRED,example = "6")
  37 + private Integer adminUnreadMessageCount;
  38 +
  39 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED,example = "2024-01-01 00:00:00")
  40 + private LocalDateTime createTime;
  41 +
  42 +}
0 \ No newline at end of file 43 \ No newline at end of file
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/conversation/KeFuConversationUpdatePinnedReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.conversation;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +@Schema(description = "管理后台 - 客服会话置顶 Request VO")
  8 +@Data
  9 +public class KeFuConversationUpdatePinnedReqVO {
  10 +
  11 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23202")
  12 + @NotNull(message = "会话编号,不能为空")
  13 + private Long id;
  14 +
  15 + @Schema(description = "管理端置顶", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
  16 + @NotNull(message = "管理端置顶,不能为空")
  17 + private Boolean adminPinned;
  18 +
  19 +}
0 \ No newline at end of file 20 \ No newline at end of file
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessagePageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message;
  2 +
  3 +import lombok.*;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  6 +
  7 +@Schema(description = "管理后台 - 客服消息分页 Request VO")
  8 +@Data
  9 +@EqualsAndHashCode(callSuper = true)
  10 +@ToString(callSuper = true)
  11 +public class KeFuMessagePageReqVO extends PageParam {
  12 +
  13 + @Schema(description = "会话编号", example = "12580")
  14 + private Long conversationId;
  15 +
  16 +}
0 \ No newline at end of file 17 \ No newline at end of file
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessageRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.*;
  5 +
  6 +import java.time.LocalDateTime;
  7 +import com.alibaba.excel.annotation.*;
  8 +
  9 +@Schema(description = "管理后台 - 客服消息 Response VO")
  10 +@Data
  11 +public class KeFuMessageRespVO {
  12 +
  13 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23202")
  14 + private Long id;
  15 +
  16 + @Schema(description = "会话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12580")
  17 + private Long conversationId;
  18 +
  19 + @Schema(description = "发送人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24571")
  20 + private Long senderId;
  21 +
  22 + @Schema(description = "发送人类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  23 + private Integer senderType;
  24 +
  25 + @Schema(description = "接收人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29124")
  26 + private Long receiverId;
  27 +
  28 + @Schema(description = "接收人类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
  29 + private Integer receiverType;
  30 +
  31 + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  32 + private Integer contentType;
  33 +
  34 + @Schema(description = "消息", requiredMode = Schema.RequiredMode.REQUIRED)
  35 + private String content;
  36 +
  37 + @Schema(description = "是否已读", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  38 + private Boolean readStatus;
  39 +
  40 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  41 + private LocalDateTime createTime;
  42 +
  43 +}
0 \ No newline at end of file 44 \ No newline at end of file
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessageSendReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotEmpty;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +
  8 +@Schema(description = "管理后台 - 发送客服消息 Request VO")
  9 +@Data
  10 +public class KeFuMessageSendReqVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23202")
  13 + private Long id;
  14 +
  15 + @Schema(description = "会话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12580")
  16 + @NotNull(message = "会话编号不能为空")
  17 + private Long conversationId;
  18 +
  19 + @Schema(description = "发送人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24571")
  20 + @NotNull(message = "发送人编号不能为空")
  21 + private Long senderId;
  22 +
  23 + @Schema(description = "发送人类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  24 + @NotNull(message = "发送人类型不能为空")
  25 + private Integer senderType;
  26 +
  27 + @Schema(description = "接收人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29124")
  28 + @NotNull(message = "接收人编号不能为空")
  29 + private Long receiverId;
  30 +
  31 + @Schema(description = "接收人类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
  32 + @NotNull(message = "接收人类型不能为空")
  33 + private Integer receiverType;
  34 +
  35 + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  36 + @NotNull(message = "消息类型不能为空")
  37 + private Integer contentType;
  38 +
  39 + @Schema(description = "消息", requiredMode = Schema.RequiredMode.REQUIRED)
  40 + @NotEmpty(message = "消息不能为空")
  41 + private String content;
  42 +
  43 +}
0 \ No newline at end of file 44 \ No newline at end of file
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/kefu/AppKeFuConversationController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.controller.app.kefu;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  5 +import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
  6 +import cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.conversation.AppKeFuConversationRespVO;
  7 +import cn.iocoder.yudao.module.promotion.service.kefu.KeFuConversationService;
  8 +import io.swagger.v3.oas.annotations.Operation;
  9 +import io.swagger.v3.oas.annotations.tags.Tag;
  10 +import jakarta.annotation.Resource;
  11 +import org.springframework.validation.annotation.Validated;
  12 +import org.springframework.web.bind.annotation.GetMapping;
  13 +import org.springframework.web.bind.annotation.RequestMapping;
  14 +import org.springframework.web.bind.annotation.RestController;
  15 +
  16 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  17 +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
  18 +
  19 +@Tag(name = "用户 APP - 客户会话")
  20 +@RestController
  21 +@RequestMapping("/promotion/kefu-conversation")
  22 +@Validated
  23 +public class AppKeFuConversationController {
  24 +
  25 + @Resource
  26 + private KeFuConversationService conversationService;
  27 +
  28 + @GetMapping("/get")
  29 + @Operation(summary = "获得客服会话")
  30 + @PreAuthenticated
  31 + public CommonResult<AppKeFuConversationRespVO> getDiyPage() {
  32 + return success(BeanUtils.toBean(conversationService.getOrCreateConversation(getLoginUserId()), AppKeFuConversationRespVO.class));
  33 + }
  34 +
  35 +}
0 \ No newline at end of file 36 \ No newline at end of file
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/kefu/AppKeFuMessageController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.controller.app.kefu;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
  7 +import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessagePageReqVO;
  8 +import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageRespVO;
  9 +import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageSendReqVO;
  10 +import cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message.AppKeFuMessageSendReqVO;
  11 +import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuMessageDO;
  12 +import cn.iocoder.yudao.module.promotion.service.kefu.KeFuMessageService;
  13 +import io.swagger.v3.oas.annotations.Operation;
  14 +import io.swagger.v3.oas.annotations.Parameter;
  15 +import io.swagger.v3.oas.annotations.tags.Tag;
  16 +import jakarta.annotation.Resource;
  17 +import jakarta.validation.Valid;
  18 +import org.springframework.security.access.prepost.PreAuthorize;
  19 +import org.springframework.validation.annotation.Validated;
  20 +import org.springframework.web.bind.annotation.*;
  21 +
  22 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  23 +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
  24 +
  25 +@Tag(name = "管理后台 - 客服消息")
  26 +@RestController
  27 +@RequestMapping("/promotion/kefu-message")
  28 +@Validated
  29 +public class AppKeFuMessageController {
  30 +
  31 + @Resource
  32 + private KeFuMessageService kefuMessageService;
  33 +
  34 + @PostMapping("/send")
  35 + @Operation(summary = "发送客服消息")
  36 + @PreAuthenticated
  37 + public CommonResult<Long> createKefuMessage(@Valid @RequestBody AppKeFuMessageSendReqVO sendReqVO) {
  38 + return success(kefuMessageService.sendKefuMessage(BeanUtils.toBean(sendReqVO, KeFuMessageSendReqVO.class)));
  39 + }
  40 +
  41 + @PutMapping("/update-read-status")
  42 + @Operation(summary = "更新客服消息已读状态")
  43 + @Parameter(name = "conversationId", description = "会话编号", required = true)
  44 + @PreAuthenticated
  45 + public CommonResult<Boolean> updateKefuMessageReadStatus(@RequestParam("conversationId") Long conversationId) {
  46 + kefuMessageService.updateKefuMessageReadStatus(conversationId, getLoginUserId());
  47 + return success(true);
  48 + }
  49 +
  50 + @GetMapping("/page")
  51 + @Operation(summary = "获得客服消息分页")
  52 + @PreAuthenticated
  53 + public CommonResult<PageResult<KeFuMessageRespVO>> getKefuMessagePage(@Valid KeFuMessagePageReqVO pageReqVO) {
  54 + PageResult<KeFuMessageDO> pageResult = kefuMessageService.getKefuMessagePage(pageReqVO);
  55 + return success(BeanUtils.toBean(pageResult, KeFuMessageRespVO.class));
  56 + }
  57 +
  58 +}
0 \ No newline at end of file 59 \ No newline at end of file
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/kefu/vo/conversation/AppKeFuConversationRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.conversation;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.*;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "用户 App - 客服会话 Response VO")
  9 +@Data
  10 +public class AppKeFuConversationRespVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24988")
  13 + private Long id;
  14 +
  15 + @Schema(description = "会话所属用户", requiredMode = Schema.RequiredMode.REQUIRED, example = "8300")
  16 + private Long userId;
  17 +
  18 + @Schema(description = "最后聊天时间", requiredMode = Schema.RequiredMode.REQUIRED,example = "2024-01-01 00:00:00")
  19 + private LocalDateTime lastMessageTime;
  20 +
  21 + @Schema(description = "最后聊天内容", requiredMode = Schema.RequiredMode.REQUIRED,example = "嗨,您好啊")
  22 + private String lastMessageContent;
  23 +
  24 + @Schema(description = "最后发送的消息类型", requiredMode = Schema.RequiredMode.REQUIRED,example = "1")
  25 + private Integer lastMessageContentType;
  26 +
  27 + @Schema(description = "管理端置顶", requiredMode = Schema.RequiredMode.REQUIRED,example = "false")
  28 + private Boolean adminPinned;
  29 +
  30 + @Schema(description = "用户是否可见", requiredMode = Schema.RequiredMode.REQUIRED,example = "true")
  31 + private Boolean userDeleted;
  32 +
  33 + @Schema(description = "管理员是否可见", requiredMode = Schema.RequiredMode.REQUIRED,example = "true")
  34 + private Boolean adminDeleted;
  35 +
  36 + @Schema(description = "管理员未读消息数", requiredMode = Schema.RequiredMode.REQUIRED,example = "6")
  37 + private Integer adminUnreadMessageCount;
  38 +
  39 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED,example = "2024-01-01 00:00:00")
  40 + private LocalDateTime createTime;
  41 +
  42 +}
0 \ No newline at end of file 43 \ No newline at end of file
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/kefu/vo/message/AppKeFuMessagePageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import lombok.ToString;
  8 +
  9 +@Schema(description = "用户 App - 客服消息分页 Request VO")
  10 +@Data
  11 +@EqualsAndHashCode(callSuper = true)
  12 +@ToString(callSuper = true)
  13 +public class AppKeFuMessagePageReqVO extends PageParam {
  14 +
  15 + @Schema(description = "会话编号", example = "12580")
  16 + private Long conversationId;
  17 +
  18 +}
0 \ No newline at end of file 19 \ No newline at end of file
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/kefu/vo/message/AppKeFuMessageRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "用户 App - 客服消息 Response VO")
  9 +@Data
  10 +public class AppKeFuMessageRespVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23202")
  13 + private Long id;
  14 +
  15 + @Schema(description = "会话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12580")
  16 + private Long conversationId;
  17 +
  18 + @Schema(description = "发送人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24571")
  19 + private Long senderId;
  20 +
  21 + @Schema(description = "发送人类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  22 + private Integer senderType;
  23 +
  24 + @Schema(description = "接收人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29124")
  25 + private Long receiverId;
  26 +
  27 + @Schema(description = "接收人类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
  28 + private Integer receiverType;
  29 +
  30 + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  31 + private Integer contentType;
  32 +
  33 + @Schema(description = "消息", requiredMode = Schema.RequiredMode.REQUIRED)
  34 + private String content;
  35 +
  36 + @Schema(description = "是否已读", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  37 + private Boolean readStatus;
  38 +
  39 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  40 + private LocalDateTime createTime;
  41 +
  42 +}
0 \ No newline at end of file 43 \ No newline at end of file
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/kefu/vo/message/AppKeFuMessageSendReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotEmpty;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +
  8 +@Schema(description = "用户 App - 发送客服消息 Request VO")
  9 +@Data
  10 +public class AppKeFuMessageSendReqVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23202")
  13 + private Long id;
  14 +
  15 + @Schema(description = "会话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12580")
  16 + @NotNull(message = "会话编号不能为空")
  17 + private Long conversationId;
  18 +
  19 + @Schema(description = "发送人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24571")
  20 + @NotNull(message = "发送人编号不能为空")
  21 + private Long senderId;
  22 +
  23 + @Schema(description = "发送人类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  24 + @NotNull(message = "发送人类型不能为空")
  25 + private Integer senderType;
  26 +
  27 + @Schema(description = "接收人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29124")
  28 + @NotNull(message = "接收人编号不能为空")
  29 + private Long receiverId;
  30 +
  31 + @Schema(description = "接收人类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
  32 + @NotNull(message = "接收人类型不能为空")
  33 + private Integer receiverType;
  34 +
  35 + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  36 + @NotNull(message = "消息类型不能为空")
  37 + private Integer contentType;
  38 +
  39 + @Schema(description = "消息", requiredMode = Schema.RequiredMode.REQUIRED)
  40 + @NotEmpty(message = "消息不能为空")
  41 + private String content;
  42 +
  43 +}
0 \ No newline at end of file 44 \ No newline at end of file
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/kefu/KeFuConversationDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.dal.dataobject.kefu;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  4 +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
  5 +import cn.iocoder.yudao.module.promotion.enums.kehu.KeFuMessageContentTypeEnum;
  6 +import com.baomidou.mybatisplus.annotation.KeySequence;
  7 +import com.baomidou.mybatisplus.annotation.TableId;
  8 +import com.baomidou.mybatisplus.annotation.TableName;
  9 +import lombok.*;
  10 +
  11 +import java.time.LocalDateTime;
  12 +
  13 +/**
  14 + * 客服会话 DO
  15 + *
  16 + * @author HUIHUI
  17 + */
  18 +@TableName("promotion_kefu_conversation")
  19 +@KeySequence("promotion_kefu_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  20 +@Data
  21 +@EqualsAndHashCode(callSuper = true)
  22 +@ToString(callSuper = true)
  23 +@Builder
  24 +@NoArgsConstructor
  25 +@AllArgsConstructor
  26 +public class KeFuConversationDO extends BaseDO {
  27 +
  28 + /**
  29 + * 编号
  30 + */
  31 + @TableId
  32 + private Long id;
  33 + /**
  34 + * 会话所属用户
  35 + *
  36 + * 关联 {@link MemberUserRespDTO#getId()}
  37 + */
  38 + private Long userId;
  39 +
  40 + /**
  41 + * 最后聊天时间
  42 + */
  43 + private LocalDateTime lastMessageTime;
  44 + /**
  45 + * 最后聊天内容
  46 + */
  47 + private String lastMessageContent;
  48 + /**
  49 + * 最后发送的消息类型
  50 + *
  51 + * 枚举 {@link KeFuMessageContentTypeEnum}
  52 + */
  53 + private Integer lastMessageContentType;
  54 +
  55 + //======================= 会话操作相关 =======================
  56 +
  57 + /**
  58 + * 管理端置顶
  59 + */
  60 + private Boolean adminPinned;
  61 + /**
  62 + * 用户是否可见
  63 + *
  64 + * false - 可见,默认值
  65 + * true - 不可见,用户删除时设置为 true
  66 + */
  67 + private Boolean userDeleted;
  68 + /**
  69 + * 管理员是否可见
  70 + *
  71 + * false - 可见,默认值
  72 + * true - 不可见,管理员删除时设置为 true
  73 + */
  74 + private Boolean adminDeleted;
  75 +
  76 + /**
  77 + * 管理员未读消息数
  78 + *
  79 + * 用户发送消息时增加,管理员查看后扣减
  80 + */
  81 + private Integer adminUnreadMessageCount;
  82 +
  83 +}
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/kefu/KeFuMessageDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.dal.dataobject.kefu;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
  4 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  5 +import cn.iocoder.yudao.module.promotion.enums.kehu.KeFuMessageContentTypeEnum;
  6 +import com.baomidou.mybatisplus.annotation.KeySequence;
  7 +import com.baomidou.mybatisplus.annotation.TableId;
  8 +import com.baomidou.mybatisplus.annotation.TableName;
  9 +import lombok.*;
  10 +
  11 +/**
  12 + * 客服消息 DO
  13 + *
  14 + * @author HUIHUI
  15 + */
  16 +@TableName("promotion_kefu_message")
  17 +@KeySequence("promotion_kefu_message_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  18 +@Data
  19 +@EqualsAndHashCode(callSuper = true)
  20 +@ToString(callSuper = true)
  21 +@Builder
  22 +@NoArgsConstructor
  23 +@AllArgsConstructor
  24 +public class KeFuMessageDO extends BaseDO {
  25 +
  26 + /**
  27 + * 编号
  28 + */
  29 + @TableId
  30 + private Long id;
  31 + /**
  32 + * 会话编号
  33 + *
  34 + * 关联 {@link KeFuConversationDO#getId()}
  35 + */
  36 + private Long conversationId;
  37 +
  38 + /**
  39 + * 发送人编号
  40 + *
  41 + * 存储的是用户编号
  42 + */
  43 + private Long senderId;
  44 + /**
  45 + * 发送人类型
  46 + *
  47 + * 枚举,{@link UserTypeEnum}
  48 + */
  49 + private Integer senderType;
  50 + /**
  51 + * 接收人编号
  52 + *
  53 + * 存储的是用户编号
  54 + */
  55 + private Long receiverId;
  56 + /**
  57 + * 接收人类型
  58 + *
  59 + * 枚举,{@link UserTypeEnum}
  60 + */
  61 + private Integer receiverType;
  62 +
  63 + /**
  64 + * 消息类型
  65 + *
  66 + * 枚举 {@link KeFuMessageContentTypeEnum}
  67 + */
  68 + private Integer contentType;
  69 + /**
  70 + * 消息
  71 + */
  72 + private String content;
  73 +
  74 + //======================= 消息相关状态 =======================
  75 +
  76 + /**
  77 + * 是/否已读
  78 + */
  79 + private Boolean readStatus;
  80 +
  81 +}
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/kefu/KeFuConversationMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.dal.mysql.kefu;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  4 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  5 +import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuConversationDO;
  6 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  7 +import org.apache.ibatis.annotations.Mapper;
  8 +
  9 +import java.util.List;
  10 +
  11 +/**
  12 + * 客服会话 Mapper
  13 + *
  14 + * @author HUIHUI
  15 + */
  16 +@Mapper
  17 +public interface KeFuConversationMapper extends BaseMapperX<KeFuConversationDO> {
  18 +
  19 + default List<KeFuConversationDO> selectListWithSort() {
  20 + return selectList(new LambdaQueryWrapperX<KeFuConversationDO>()
  21 + .eq(KeFuConversationDO::getAdminDeleted, Boolean.FALSE)
  22 + .orderByDesc(KeFuConversationDO::getAdminPinned) // 置顶优先
  23 + .orderByDesc(KeFuConversationDO::getCreateTime));
  24 + }
  25 +
  26 + default void updateAdminUnreadMessageCountByConversationId(Long id, Integer count) {
  27 + LambdaUpdateWrapper<KeFuConversationDO> updateWrapper = new LambdaUpdateWrapper<>();
  28 + updateWrapper.eq(KeFuConversationDO::getId, id);
  29 + if (count != null && count > 0) { // 情况一:会员发送消息时增加管理员的未读消息数
  30 + updateWrapper.setSql("admin_unread_message_count = admin_unread_message_count + 1");
  31 + } else { // 情况二:管理员已读后重置
  32 + updateWrapper.set(KeFuConversationDO::getAdminUnreadMessageCount, 0);
  33 + }
  34 +
  35 + update(updateWrapper);
  36 + }
  37 +
  38 +}
0 \ No newline at end of file 39 \ No newline at end of file
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/kefu/KeFuMessageMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.dal.mysql.kefu;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessagePageReqVO;
  7 +import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuMessageDO;
  8 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  9 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  10 +import org.apache.ibatis.annotations.Mapper;
  11 +
  12 +import java.util.Collection;
  13 +import java.util.List;
  14 +
  15 +/**
  16 + * 客服消息 Mapper
  17 + *
  18 + * @author HUIHUI
  19 + */
  20 +@Mapper
  21 +public interface KeFuMessageMapper extends BaseMapperX<KeFuMessageDO> {
  22 +
  23 + default PageResult<KeFuMessageDO> selectPage(KeFuMessagePageReqVO reqVO) {
  24 + return selectPage(reqVO, new LambdaQueryWrapperX<KeFuMessageDO>()
  25 + .eqIfPresent(KeFuMessageDO::getConversationId, reqVO.getConversationId())
  26 + .orderByDesc(KeFuMessageDO::getId));
  27 + }
  28 +
  29 + default List<KeFuMessageDO> selectListByConversationIdAndReceiverIdAndReadStatus(Long conversationId, Long receiverId, Boolean readStatus) {
  30 + return selectList(new LambdaQueryWrapper<KeFuMessageDO>()
  31 + .eq(KeFuMessageDO::getConversationId, conversationId)
  32 + .eq(KeFuMessageDO::getReceiverId, receiverId)
  33 + .eq(KeFuMessageDO::getReadStatus, readStatus));
  34 + }
  35 +
  36 + default void updateReadStstusBatchByIds(Collection<Long> ids, Boolean readStatus) {
  37 + update(new LambdaUpdateWrapper<KeFuMessageDO>()
  38 + .in(KeFuMessageDO::getId, ids)
  39 + .set(KeFuMessageDO::getReadStatus, readStatus));
  40 + }
  41 +
  42 +}
0 \ No newline at end of file 43 \ No newline at end of file
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/kefu/KeFuConversationService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.service.kefu;
  2 +
  3 +import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.conversation.KeFuConversationUpdatePinnedReqVO;
  4 +import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuConversationDO;
  5 +
  6 +import java.time.LocalDateTime;
  7 +import java.util.List;
  8 +
  9 +/**
  10 + * 客服会话 Service 接口
  11 + *
  12 + * @author HUIHUI
  13 + */
  14 +public interface KeFuConversationService {
  15 +
  16 + /**
  17 + * 删除客服会话
  18 + *
  19 + * @param id 编号
  20 + */
  21 + void deleteKefuConversation(Long id);
  22 +
  23 + /**
  24 + * 客服会话置顶
  25 + *
  26 + * @param updateReqVO 请求
  27 + */
  28 + void updatePinned(KeFuConversationUpdatePinnedReqVO updateReqVO);
  29 +
  30 + /**
  31 + * 更新会话客服消息冗余信息
  32 + *
  33 + * @param id 编号
  34 + * @param lastMessageTime 最后聊天时间
  35 + * @param lastMessageContent 最后聊天内容
  36 + * @param lastMessageContentType 最后聊天内容类型
  37 + */
  38 + void updateConversationMessage(Long id, LocalDateTime lastMessageTime, String lastMessageContent, Integer lastMessageContentType);
  39 +
  40 + /**
  41 + * 更新管理员未读消息数
  42 + *
  43 + * @param id 编号
  44 + * @param count 数量:0 则重置 1 则消息数加一
  45 + */
  46 + void updateAdminUnreadMessageCountByConversationId(Long id, Integer count);
  47 +
  48 + /**
  49 + * 更新会话对于管理员是否可见
  50 + *
  51 + * @param adminDeleted 管理员是否可见
  52 + */
  53 + void updateConversationAdminDeleted(Long id, Boolean adminDeleted);
  54 +
  55 + /**
  56 + * 获得客服会话列表
  57 + *
  58 + * @return 会话列表
  59 + */
  60 + List<KeFuConversationDO> getKefuConversationList();
  61 +
  62 + /**
  63 + * 获得或创建会话
  64 + *
  65 + * @param userId 用户编号
  66 + * @return 客服会话
  67 + */
  68 + KeFuConversationDO getOrCreateConversation(Long userId);
  69 +
  70 + /**
  71 + * 校验客服会话是否存在
  72 + *
  73 + * @param id 编号
  74 + * @return 客服会话
  75 + */
  76 + KeFuConversationDO validateKefuConversationExists(Long id);
  77 +
  78 +}
0 \ No newline at end of file 79 \ No newline at end of file
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/kefu/KeFuConversationServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.service.kefu;
  2 +
  3 +import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.conversation.KeFuConversationUpdatePinnedReqVO;
  4 +import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuConversationDO;
  5 +import cn.iocoder.yudao.module.promotion.dal.mysql.kefu.KeFuConversationMapper;
  6 +import cn.iocoder.yudao.module.promotion.enums.kehu.KeFuMessageContentTypeEnum;
  7 +import jakarta.annotation.Resource;
  8 +import org.springframework.stereotype.Service;
  9 +import org.springframework.validation.annotation.Validated;
  10 +
  11 +import java.time.LocalDateTime;
  12 +import java.util.List;
  13 +
  14 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  15 +import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.KEFU_CONVERSATION_NOT_EXISTS;
  16 +
  17 +/**
  18 + * 客服会话 Service 实现类
  19 + *
  20 + * @author HUIHUI
  21 + */
  22 +@Service
  23 +@Validated
  24 +public class KeFuConversationServiceImpl implements KeFuConversationService {
  25 +
  26 + @Resource
  27 + private KeFuConversationMapper conversationMapper;
  28 +
  29 + @Override
  30 + public void deleteKefuConversation(Long id) {
  31 + // 校验存在
  32 + validateKefuConversationExists(id);
  33 +
  34 + // 只有管理员端可以删除会话,也不真的删,只是管理员端看不到啦
  35 + conversationMapper.updateById(new KeFuConversationDO().setId(id).setAdminDeleted(Boolean.TRUE));
  36 + }
  37 +
  38 + @Override
  39 + public void updatePinned(KeFuConversationUpdatePinnedReqVO updateReqVO) {
  40 + // 只有管理员端可以置顶会话
  41 + conversationMapper.updateById(new KeFuConversationDO().setId(updateReqVO.getId()).setAdminPinned(updateReqVO.getAdminPinned()));
  42 + }
  43 +
  44 + @Override
  45 + public void updateConversationMessage(Long id, LocalDateTime lastMessageTime, String lastMessageContent, Integer lastMessageContentType) {
  46 + conversationMapper.updateById(new KeFuConversationDO().setId(id).setLastMessageTime(lastMessageTime)
  47 + .setLastMessageContent(lastMessageContent).setLastMessageContentType(lastMessageContentType));
  48 + }
  49 +
  50 + @Override
  51 + public void updateAdminUnreadMessageCountByConversationId(Long id, Integer count) {
  52 + conversationMapper.updateAdminUnreadMessageCountByConversationId(id, count);
  53 + }
  54 +
  55 + @Override
  56 + public void updateConversationAdminDeleted(Long id, Boolean adminDeleted) {
  57 + conversationMapper.updateById(new KeFuConversationDO().setId(id).setAdminDeleted(adminDeleted));
  58 + }
  59 +
  60 + @Override
  61 + public List<KeFuConversationDO> getKefuConversationList() {
  62 + return conversationMapper.selectListWithSort();
  63 + }
  64 +
  65 + @Override
  66 + public KeFuConversationDO getOrCreateConversation(Long userId) {
  67 + KeFuConversationDO conversation = conversationMapper.selectOne(KeFuConversationDO::getUserId, userId);
  68 + if (conversation == null) { // 没有历史会话则初始化一个新会话
  69 + conversation = new KeFuConversationDO().setUserId(userId).setLastMessageTime(LocalDateTime.now())
  70 + .setLastMessageContent("").setLastMessageContentType(KeFuMessageContentTypeEnum.TEXT.getType())
  71 + .setAdminPinned(Boolean.FALSE).setUserDeleted(Boolean.FALSE).setAdminDeleted(Boolean.FALSE)
  72 + .setAdminUnreadMessageCount(0);
  73 + conversationMapper.insert(conversation);
  74 + }
  75 + return conversation;
  76 + }
  77 +
  78 + @Override
  79 + public KeFuConversationDO validateKefuConversationExists(Long id) {
  80 + KeFuConversationDO conversationDO = conversationMapper.selectById(id);
  81 + if (conversationDO == null) {
  82 + throw exception(KEFU_CONVERSATION_NOT_EXISTS);
  83 + }
  84 +
  85 + return conversationDO;
  86 + }
  87 +
  88 +}
0 \ No newline at end of file 89 \ No newline at end of file
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/kefu/KeFuMessageService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.service.kefu;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessagePageReqVO;
  5 +import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageSendReqVO;
  6 +import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuMessageDO;
  7 +import jakarta.validation.Valid;
  8 +
  9 +/**
  10 + * 客服消息 Service 接口
  11 + *
  12 + * @author HUIHUI
  13 + */
  14 +public interface KeFuMessageService {
  15 +
  16 + /**
  17 + * 发送客服消息
  18 + *
  19 + * @param sendReqVO 信息
  20 + * @return 编号
  21 + */
  22 + Long sendKefuMessage(@Valid KeFuMessageSendReqVO sendReqVO);
  23 +
  24 + /**
  25 + * 更新消息已读状态
  26 + *
  27 + * @param conversationId 会话编号
  28 + * @param receiverId 用户编号
  29 + */
  30 + void updateKefuMessageReadStatus(Long conversationId, Long receiverId);
  31 +
  32 + /**
  33 + * 获得客服消息分页
  34 + *
  35 + * @param pageReqVO 分页查询
  36 + * @return 客服消息分页
  37 + */
  38 + PageResult<KeFuMessageDO> getKefuMessagePage(KeFuMessagePageReqVO pageReqVO);
  39 +
  40 +}
0 \ No newline at end of file 41 \ No newline at end of file
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/kefu/KeFuMessageServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.service.kefu;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.extra.spring.SpringUtil;
  5 +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  7 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  8 +import cn.iocoder.yudao.module.infra.api.websocket.WebSocketSenderApi;
  9 +import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
  10 +import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessagePageReqVO;
  11 +import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageSendReqVO;
  12 +import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuConversationDO;
  13 +import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuMessageDO;
  14 +import cn.iocoder.yudao.module.promotion.dal.mysql.kefu.KeFuMessageMapper;
  15 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  16 +import jakarta.annotation.Resource;
  17 +import org.springframework.scheduling.annotation.Async;
  18 +import org.springframework.stereotype.Service;
  19 +import org.springframework.transaction.annotation.Transactional;
  20 +import org.springframework.validation.annotation.Validated;
  21 +
  22 +import java.time.LocalDateTime;
  23 +import java.util.List;
  24 +
  25 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  26 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getFirst;
  27 +
  28 +/**
  29 + * 客服消息 Service 实现类
  30 + *
  31 + * @author HUIHUI
  32 + */
  33 +@Service
  34 +@Validated
  35 +public class KeFuMessageServiceImpl implements KeFuMessageService {
  36 +
  37 + private static final String KEFU_MESSAGE_TYPE = "kefu_message_type"; // 客服消息类型
  38 +
  39 + @Resource
  40 + private KeFuMessageMapper messageMapper;
  41 + @Resource
  42 + private KeFuConversationService conversationService;
  43 + @Resource
  44 + private AdminUserApi adminUserApi;
  45 + @Resource
  46 + private MemberUserApi memberUserApi;
  47 + @Resource
  48 + private WebSocketSenderApi webSocketSenderApi;
  49 +
  50 + @Override
  51 + @Transactional(rollbackFor = Exception.class)
  52 + public Long sendKefuMessage(KeFuMessageSendReqVO sendReqVO) {
  53 + // 1.1 校验会话是否存在
  54 + KeFuConversationDO conversation = conversationService.validateKefuConversationExists(sendReqVO.getConversationId());
  55 + // 1.2 校验接收人是否存在
  56 + validateReceiverExist(sendReqVO.getReceiverId(), sendReqVO.getReceiverType());
  57 +
  58 + // 2.1 保存消息
  59 + KeFuMessageDO kefuMessage = BeanUtils.toBean(sendReqVO, KeFuMessageDO.class);
  60 + messageMapper.insert(kefuMessage);
  61 + // 2.2 更新会话消息冗余
  62 + conversationService.updateConversationMessage(kefuMessage.getConversationId(), LocalDateTime.now(),
  63 + kefuMessage.getContent(), kefuMessage.getContentType());
  64 + // 2.3 更新管理员未读消息数
  65 + if (UserTypeEnum.ADMIN.getValue().equals(kefuMessage.getReceiverType())) {
  66 + conversationService.updateAdminUnreadMessageCountByConversationId(kefuMessage.getConversationId(), 1);
  67 + }
  68 + // 2.4 会员用户发送消息时,如果管理员删除过会话则进行恢复
  69 + if (UserTypeEnum.MEMBER.getValue().equals(kefuMessage.getSenderType()) && Boolean.TRUE.equals(conversation.getAdminDeleted())) {
  70 + conversationService.updateConversationAdminDeleted(kefuMessage.getConversationId(), Boolean.FALSE);
  71 + }
  72 +
  73 + // 3. 发送消息
  74 + getSelf().sendAsyncMessage(sendReqVO.getReceiverType(), sendReqVO.getReceiverId(), kefuMessage);
  75 +
  76 + // 返回
  77 + return kefuMessage.getId();
  78 + }
  79 +
  80 + @Override
  81 + @Transactional(rollbackFor = Exception.class)
  82 + public void updateKefuMessageReadStatus(Long conversationId, Long receiverId) {
  83 + // 1.1 校验会话是否存在
  84 + conversationService.validateKefuConversationExists(conversationId);
  85 + // 1.2 查询接收人所有的未读消息
  86 + List<KeFuMessageDO> messageList = messageMapper.selectListByConversationIdAndReceiverIdAndReadStatus(
  87 + conversationId, receiverId, Boolean.FALSE);
  88 + // 1.3 情况一:没有未读消息
  89 + if (CollUtil.isEmpty(messageList)) {
  90 + return;
  91 + }
  92 +
  93 + // 2.1 情况二:更新未读消息状态为已读
  94 + messageMapper.updateReadStstusBatchByIds(convertSet(messageList, KeFuMessageDO::getId), Boolean.TRUE);
  95 + // 2.2 更新管理员未读消息数
  96 + KeFuMessageDO message = getFirst(messageList);
  97 + assert message != null;
  98 + if (UserTypeEnum.ADMIN.getValue().equals(message.getReceiverType())) {
  99 + conversationService.updateAdminUnreadMessageCountByConversationId(conversationId, 0);
  100 + }
  101 + // 2.3 发送消息通知发送者,接收者已读 -> 发送者更新发送的消息状态
  102 + getSelf().sendAsyncMessage(message.getSenderType(), message.getSenderId(), "keFuMessageReadStatusChange");
  103 + }
  104 +
  105 + private void validateReceiverExist(Long receiverId, Integer receiverType) {
  106 + if (UserTypeEnum.ADMIN.getValue().equals(receiverType)) {
  107 + adminUserApi.validateUser(receiverId);
  108 + }
  109 + if (UserTypeEnum.MEMBER.getValue().equals(receiverType)) {
  110 + memberUserApi.validateUser(receiverId);
  111 + }
  112 + }
  113 +
  114 + @Async
  115 + public void sendAsyncMessage(Integer userType, Long userId, Object content) {
  116 + webSocketSenderApi.sendObject(userType, userId, KEFU_MESSAGE_TYPE, content);
  117 + }
  118 +
  119 + @Override
  120 + public PageResult<KeFuMessageDO> getKefuMessagePage(KeFuMessagePageReqVO pageReqVO) {
  121 + return messageMapper.selectPage(pageReqVO);
  122 + }
  123 +
  124 + private KeFuMessageServiceImpl getSelf() {
  125 + return SpringUtil.getBean(getClass());
  126 + }
  127 +
  128 +}
0 \ No newline at end of file 129 \ No newline at end of file
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/AppBrokerageUserController.java
1 package cn.iocoder.yudao.module.trade.controller.app.brokerage; 1 package cn.iocoder.yudao.module.trade.controller.app.brokerage;
2 2
3 import cn.hutool.core.date.LocalDateTimeUtil; 3 import cn.hutool.core.date.LocalDateTimeUtil;
4 -import cn.hutool.core.util.ObjUtil;  
5 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 4 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
6 import cn.iocoder.yudao.framework.common.pojo.PageResult; 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
7 import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated; 6 import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
@@ -11,8 +10,6 @@ import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.user.*; @@ -11,8 +10,6 @@ import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.user.*;
11 import cn.iocoder.yudao.module.trade.convert.brokerage.BrokerageRecordConvert; 10 import cn.iocoder.yudao.module.trade.convert.brokerage.BrokerageRecordConvert;
12 import cn.iocoder.yudao.module.trade.convert.brokerage.BrokerageUserConvert; 11 import cn.iocoder.yudao.module.trade.convert.brokerage.BrokerageUserConvert;
13 import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO; 12 import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO;
14 -import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO;  
15 -import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageEnabledConditionEnum;  
16 import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum; 13 import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum;
17 import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordStatusEnum; 14 import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordStatusEnum;
18 import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawStatusEnum; 15 import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawStatusEnum;
@@ -20,7 +17,6 @@ import cn.iocoder.yudao.module.trade.service.brokerage.BrokerageRecordService; @@ -20,7 +17,6 @@ import cn.iocoder.yudao.module.trade.service.brokerage.BrokerageRecordService;
20 import cn.iocoder.yudao.module.trade.service.brokerage.BrokerageUserService; 17 import cn.iocoder.yudao.module.trade.service.brokerage.BrokerageUserService;
21 import cn.iocoder.yudao.module.trade.service.brokerage.BrokerageWithdrawService; 18 import cn.iocoder.yudao.module.trade.service.brokerage.BrokerageWithdrawService;
22 import cn.iocoder.yudao.module.trade.service.brokerage.bo.BrokerageWithdrawSummaryRespBO; 19 import cn.iocoder.yudao.module.trade.service.brokerage.bo.BrokerageWithdrawSummaryRespBO;
23 -import cn.iocoder.yudao.module.trade.service.config.TradeConfigService;  
24 import io.swagger.v3.oas.annotations.Operation; 20 import io.swagger.v3.oas.annotations.Operation;
25 import io.swagger.v3.oas.annotations.Parameter; 21 import io.swagger.v3.oas.annotations.Parameter;
26 import io.swagger.v3.oas.annotations.tags.Tag; 22 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -55,22 +51,16 @@ public class AppBrokerageUserController { @@ -55,22 +51,16 @@ public class AppBrokerageUserController {
55 @Resource 51 @Resource
56 private BrokerageWithdrawService brokerageWithdrawService; 52 private BrokerageWithdrawService brokerageWithdrawService;
57 @Resource 53 @Resource
58 - private TradeConfigService tradeConfigService;  
59 - @Resource  
60 private MemberUserApi memberUserApi; 54 private MemberUserApi memberUserApi;
61 55
62 @GetMapping("/get") 56 @GetMapping("/get")
63 @Operation(summary = "获得个人分销信息") 57 @Operation(summary = "获得个人分销信息")
64 @PreAuthenticated 58 @PreAuthenticated
65 public CommonResult<AppBrokerageUserRespVO> getBrokerageUser() { 59 public CommonResult<AppBrokerageUserRespVO> getBrokerageUser() {
66 - Optional<BrokerageUserDO> user = Optional.ofNullable(brokerageUserService.getBrokerageUser(getLoginUserId()));  
67 - // 获得交易中心配置  
68 - TradeConfigDO tradeConfig = tradeConfigService.getTradeConfig();  
69 - // 如果是人人分佣 BrokerageUserDO 为 null 时,也有分销资格  
70 - boolean brokerageEnabled = ObjUtil.equal(BrokerageEnabledConditionEnum.ALL.getCondition(), tradeConfig.getBrokerageEnabledCondition()); 60 + Optional<BrokerageUserDO> user = Optional.ofNullable(brokerageUserService.getOrCreateBrokerageUser(getLoginUserId()));
71 // 返回数据 61 // 返回数据
72 AppBrokerageUserRespVO respVO = new AppBrokerageUserRespVO() 62 AppBrokerageUserRespVO respVO = new AppBrokerageUserRespVO()
73 - .setBrokerageEnabled(user.map(BrokerageUserDO::getBrokerageEnabled).orElse(brokerageEnabled)) 63 + .setBrokerageEnabled(user.map(BrokerageUserDO::getBrokerageEnabled).orElse(false))
74 .setBrokeragePrice(user.map(BrokerageUserDO::getBrokeragePrice).orElse(0)) 64 .setBrokeragePrice(user.map(BrokerageUserDO::getBrokeragePrice).orElse(0))
75 .setFrozenPrice(user.map(BrokerageUserDO::getFrozenPrice).orElse(0)); 65 .setFrozenPrice(user.map(BrokerageUserDO::getFrozenPrice).orElse(0));
76 return success(respVO); 66 return success(respVO);
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/vo/user/AppBrokerageUserBindReqVO.java
@@ -8,7 +8,7 @@ import jakarta.validation.constraints.NotNull; @@ -8,7 +8,7 @@ import jakarta.validation.constraints.NotNull;
8 8
9 @Schema(description = "应用 App - 绑定推广员 Request VO") 9 @Schema(description = "应用 App - 绑定推广员 Request VO")
10 @Data 10 @Data
11 -public class AppBrokerageUserBindReqVO extends PageParam { 11 +public class AppBrokerageUserBindReqVO {
12 12
13 @Schema(description = "推广员编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") 13 @Schema(description = "推广员编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
14 @NotNull(message = "推广员编号不能为空") 14 @NotNull(message = "推广员编号不能为空")
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserService.java
@@ -7,8 +7,8 @@ import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.user.AppBrokera @@ -7,8 +7,8 @@ import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.user.AppBrokera
7 import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankByUserCountRespVO; 7 import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankByUserCountRespVO;
8 import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankPageReqVO; 8 import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankPageReqVO;
9 import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO; 9 import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO;
10 -  
11 import jakarta.validation.constraints.NotNull; 10 import jakarta.validation.constraints.NotNull;
  11 +
12 import java.util.Collection; 12 import java.util.Collection;
13 import java.util.List; 13 import java.util.List;
14 14
@@ -68,6 +68,14 @@ public interface BrokerageUserService { @@ -68,6 +68,14 @@ public interface BrokerageUserService {
68 BrokerageUserDO getBindBrokerageUser(Long id); 68 BrokerageUserDO getBindBrokerageUser(Long id);
69 69
70 /** 70 /**
  71 + * 获得或创建分销用户
  72 + *
  73 + * @param id 用户编号
  74 + * @return 分销用户
  75 + */
  76 + BrokerageUserDO getOrCreateBrokerageUser(Long id);
  77 +
  78 + /**
71 * 更新用户佣金 79 * 更新用户佣金
72 * 80 *
73 * @param id 用户编号 81 * @param id 用户编号
@@ -104,8 +112,8 @@ public interface BrokerageUserService { @@ -104,8 +112,8 @@ public interface BrokerageUserService {
104 /** 112 /**
105 * 【会员】绑定推广员 113 * 【会员】绑定推广员
106 * 114 *
107 - * @param userId 用户编号  
108 - * @param bindUserId 推广员编号 115 + * @param userId 用户编号
  116 + * @param bindUserId 推广员编号
109 * @return 是否绑定 117 * @return 是否绑定
110 */ 118 */
111 boolean bindBrokerageUser(@NotNull Long userId, @NotNull Long bindUserId); 119 boolean bindBrokerageUser(@NotNull Long userId, @NotNull Long bindUserId);
@@ -134,4 +142,5 @@ public interface BrokerageUserService { @@ -134,4 +142,5 @@ public interface BrokerageUserService {
134 * @return 下级分销统计分页 142 * @return 下级分销统计分页
135 */ 143 */
136 PageResult<AppBrokerageUserChildSummaryRespVO> getBrokerageUserChildSummaryPage(AppBrokerageUserChildSummaryPageReqVO pageReqVO, Long userId); 144 PageResult<AppBrokerageUserChildSummaryRespVO> getBrokerageUserChildSummaryPage(AppBrokerageUserChildSummaryPageReqVO pageReqVO, Long userId);
  145 +
137 } 146 }
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java
@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil; @@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.lang.Assert; 4 import cn.hutool.core.lang.Assert;
5 import cn.hutool.core.util.ArrayUtil; 5 import cn.hutool.core.util.ArrayUtil;
6 import cn.hutool.core.util.BooleanUtil; 6 import cn.hutool.core.util.BooleanUtil;
  7 +import cn.hutool.core.util.ObjUtil;
7 import cn.hutool.core.util.StrUtil; 8 import cn.hutool.core.util.StrUtil;
8 import cn.iocoder.yudao.framework.common.pojo.PageResult; 9 import cn.iocoder.yudao.framework.common.pojo.PageResult;
9 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; 10 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
@@ -25,10 +26,10 @@ import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum; @@ -25,10 +26,10 @@ import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum;
25 import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordStatusEnum; 26 import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordStatusEnum;
26 import cn.iocoder.yudao.module.trade.service.config.TradeConfigService; 27 import cn.iocoder.yudao.module.trade.service.config.TradeConfigService;
27 import com.baomidou.mybatisplus.core.metadata.IPage; 28 import com.baomidou.mybatisplus.core.metadata.IPage;
  29 +import jakarta.annotation.Resource;
28 import org.springframework.stereotype.Service; 30 import org.springframework.stereotype.Service;
29 import org.springframework.validation.annotation.Validated; 31 import org.springframework.validation.annotation.Validated;
30 32
31 -import jakarta.annotation.Resource;  
32 import java.time.LocalDateTime; 33 import java.time.LocalDateTime;
33 import java.util.*; 34 import java.util.*;
34 35
@@ -128,6 +129,18 @@ public class BrokerageUserServiceImpl implements BrokerageUserService { @@ -128,6 +129,18 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
128 } 129 }
129 130
130 @Override 131 @Override
  132 + public BrokerageUserDO getOrCreateBrokerageUser(Long id) {
  133 + BrokerageUserDO brokerageUser = brokerageUserMapper.selectById(id);
  134 + if (brokerageUser == null && ObjUtil.equal(BrokerageEnabledConditionEnum.ALL.getCondition(),
  135 + tradeConfigService.getTradeConfig().getBrokerageEnabledCondition())) { // 人人分销的情况下,如果分销人为空则创建分销人
  136 + brokerageUser = new BrokerageUserDO().setId(id).setBrokerageEnabled(true).setBrokeragePrice(0)
  137 + .setBrokerageTime(LocalDateTime.now()).setFrozenPrice(0);
  138 + brokerageUserMapper.insert(brokerageUser);
  139 + }
  140 + return brokerageUser;
  141 + }
  142 +
  143 + @Override
131 public boolean updateUserPrice(Long id, Integer price) { 144 public boolean updateUserPrice(Long id, Integer price) {
132 if (price > 0) { 145 if (price > 0) {
133 brokerageUserMapper.updatePriceIncr(id, price); 146 brokerageUserMapper.updatePriceIncr(id, price);
@@ -184,7 +197,6 @@ public class BrokerageUserServiceImpl implements BrokerageUserService { @@ -184,7 +197,6 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
184 if (BrokerageEnabledConditionEnum.ALL.getCondition().equals(enabledCondition)) { // 人人分销:用户默认就有分销资格 197 if (BrokerageEnabledConditionEnum.ALL.getCondition().equals(enabledCondition)) { // 人人分销:用户默认就有分销资格
185 brokerageUser.setBrokerageEnabled(true).setBrokerageTime(LocalDateTime.now()); 198 brokerageUser.setBrokerageEnabled(true).setBrokerageTime(LocalDateTime.now());
186 } 199 }
187 - brokerageUser.setBindUserId(bindUserId).setBindUserTime(LocalDateTime.now());  
188 brokerageUserMapper.insert(fillBindUserData(bindUserId, brokerageUser)); 200 brokerageUserMapper.insert(fillBindUserData(bindUserId, brokerageUser));
189 } else { 201 } else {
190 brokerageUserMapper.updateById(fillBindUserData(bindUserId, new BrokerageUserDO().setId(userId))); 202 brokerageUserMapper.updateById(fillBindUserData(bindUserId, new BrokerageUserDO().setId(userId)));
@@ -294,18 +306,23 @@ public class BrokerageUserServiceImpl implements BrokerageUserService { @@ -294,18 +306,23 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
294 } 306 }
295 307
296 private void validateCanBindUser(BrokerageUserDO user, Long bindUserId) { 308 private void validateCanBindUser(BrokerageUserDO user, Long bindUserId) {
297 - // 校验要绑定的用户有无推广资格  
298 - BrokerageUserDO bindUser = brokerageUserMapper.selectById(bindUserId); 309 + // 1.1 校验推广人是否存在
  310 + MemberUserRespDTO bindUserInfo = memberUserApi.getUser(bindUserId);
  311 + if (bindUserInfo == null) {
  312 + throw exception(BROKERAGE_USER_NOT_EXISTS);
  313 + }
  314 + // 1.2 校验要绑定的用户有无推广资格
  315 + BrokerageUserDO bindUser = getOrCreateBrokerageUser(bindUserId);
299 if (bindUser == null || BooleanUtil.isFalse(bindUser.getBrokerageEnabled())) { 316 if (bindUser == null || BooleanUtil.isFalse(bindUser.getBrokerageEnabled())) {
300 throw exception(BROKERAGE_BIND_USER_NOT_ENABLED); 317 throw exception(BROKERAGE_BIND_USER_NOT_ENABLED);
301 } 318 }
302 319
303 - // 校验绑定自己 320 + // 2. 校验绑定自己
304 if (Objects.equals(user.getId(), bindUserId)) { 321 if (Objects.equals(user.getId(), bindUserId)) {
305 throw exception(BROKERAGE_BIND_SELF); 322 throw exception(BROKERAGE_BIND_SELF);
306 } 323 }
307 324
308 - // 下级不能绑定自己的上级 325 + // 3. 下级不能绑定自己的上级
309 for (int i = 0; i <= Short.MAX_VALUE; i++) { 326 for (int i = 0; i <= Short.MAX_VALUE; i++) {
310 if (Objects.equals(bindUser.getBindUserId(), user.getId())) { 327 if (Objects.equals(bindUser.getBindUserId(), user.getId())) {
311 throw exception(BROKERAGE_BIND_LOOP); 328 throw exception(BROKERAGE_BIND_LOOP);
yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/api/user/MemberUserApi.java
@@ -57,4 +57,12 @@ public interface MemberUserApi { @@ -57,4 +57,12 @@ public interface MemberUserApi {
57 * @return 用户信息 57 * @return 用户信息
58 */ 58 */
59 MemberUserRespDTO getUserByMobile(String mobile); 59 MemberUserRespDTO getUserByMobile(String mobile);
  60 +
  61 + /**
  62 + * 校验用户是否存在
  63 + *
  64 + * @param id 用户编号
  65 + */
  66 + void validateUser(Long id);
  67 +
60 } 68 }
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/api/user/MemberUserApiImpl.java
@@ -11,6 +11,9 @@ import jakarta.annotation.Resource; @@ -11,6 +11,9 @@ import jakarta.annotation.Resource;
11 import java.util.Collection; 11 import java.util.Collection;
12 import java.util.List; 12 import java.util.List;
13 13
  14 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  15 +import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.USER_MOBILE_NOT_EXISTS;
  16 +
14 /** 17 /**
15 * 会员用户的 API 实现类 18 * 会员用户的 API 实现类
16 * 19 *
@@ -44,4 +47,12 @@ public class MemberUserApiImpl implements MemberUserApi { @@ -44,4 +47,12 @@ public class MemberUserApiImpl implements MemberUserApi {
44 return MemberUserConvert.INSTANCE.convert2(userService.getUserByMobile(mobile)); 47 return MemberUserConvert.INSTANCE.convert2(userService.getUserByMobile(mobile));
45 } 48 }
46 49
  50 + @Override
  51 + public void validateUser(Long id) {
  52 + MemberUserDO user = userService.getUser(id);
  53 + if (user == null) {
  54 + throw exception(USER_MOBILE_NOT_EXISTS);
  55 + }
  56 + }
  57 +
47 } 58 }
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/social/AppSocialUserController.java
@@ -67,8 +67,8 @@ public class AppSocialUserController { @@ -67,8 +67,8 @@ public class AppSocialUserController {
67 67
68 @PostMapping("/wxa-qrcode") 68 @PostMapping("/wxa-qrcode")
69 @Operation(summary = "获得微信小程序码(base64 image)") 69 @Operation(summary = "获得微信小程序码(base64 image)")
70 - public CommonResult<String> getWxQrcode(@RequestBody @Valid AppSocialWxQrcodeReqVO reqVO) {  
71 - byte[] wxQrcode = socialClientApi.getWxaQrcode(new SocialWxQrcodeReqDTO().setPath(reqVO.getPath())); 70 + public CommonResult<String> getWxaQrcode(@RequestBody @Valid AppSocialWxQrcodeReqVO reqVO) {
  71 + byte[] wxQrcode = socialClientApi.getWxaQrcode(BeanUtils.toBean(reqVO, SocialWxQrcodeReqDTO.class));
72 return success(Base64.encode(wxQrcode)); 72 return success(Base64.encode(wxQrcode));
73 } 73 }
74 74
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxQrcodeReqDTO.java
@@ -13,12 +13,6 @@ import lombok.Data; @@ -13,12 +13,6 @@ import lombok.Data;
13 public class SocialWxQrcodeReqDTO { 13 public class SocialWxQrcodeReqDTO {
14 14
15 /** 15 /**
16 - * 小程序版本  
17 - *  
18 - * 正式版为 "release";体验版为 "trial";开发版为 "develop"  
19 - */  
20 - public static final String ENV_VERSION = "release";  
21 - /**  
22 * 页面路径不能携带参数(参数请放在scene字段里) 16 * 页面路径不能携带参数(参数请放在scene字段里)
23 */ 17 */
24 public static final String SCENE = ""; 18 public static final String SCENE = "";
@@ -52,10 +46,6 @@ public class SocialWxQrcodeReqDTO { @@ -52,10 +46,6 @@ public class SocialWxQrcodeReqDTO {
52 @NotEmpty(message = "页面路径不能为空") 46 @NotEmpty(message = "页面路径不能为空")
53 private String path; 47 private String path;
54 /** 48 /**
55 - * 要打开的小程序版本  
56 - */  
57 - private String envVersion;  
58 - /**  
59 * 二维码宽度 49 * 二维码宽度
60 */ 50 */
61 private Integer width; 51 private Integer width;
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java
@@ -41,6 +41,7 @@ import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps; @@ -41,6 +41,7 @@ import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
41 import me.chanjar.weixin.mp.api.WxMpService; 41 import me.chanjar.weixin.mp.api.WxMpService;
42 import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl; 42 import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
43 import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl; 43 import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
  44 +import org.springframework.beans.factory.annotation.Value;
44 import org.springframework.data.redis.core.StringRedisTemplate; 45 import org.springframework.data.redis.core.StringRedisTemplate;
45 import org.springframework.stereotype.Service; 46 import org.springframework.stereotype.Service;
46 47
@@ -60,6 +61,12 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; @@ -60,6 +61,12 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
60 @Slf4j 61 @Slf4j
61 public class SocialClientServiceImpl implements SocialClientService { 62 public class SocialClientServiceImpl implements SocialClientService {
62 63
  64 + /**
  65 + * 小程序版本
  66 + */
  67 + @Value("${yudao.wxa-code.env-version}")
  68 + public String envVersion;
  69 +
63 @Resource 70 @Resource
64 private AuthRequestFactory authRequestFactory; 71 private AuthRequestFactory authRequestFactory;
65 72
@@ -237,7 +244,7 @@ public class SocialClientServiceImpl implements SocialClientService { @@ -237,7 +244,7 @@ public class SocialClientServiceImpl implements SocialClientService {
237 ObjUtil.defaultIfEmpty(reqVO.getScene(), SocialWxQrcodeReqDTO.SCENE), 244 ObjUtil.defaultIfEmpty(reqVO.getScene(), SocialWxQrcodeReqDTO.SCENE),
238 reqVO.getPath(), 245 reqVO.getPath(),
239 ObjUtil.defaultIfNull(reqVO.getCheckPath(), SocialWxQrcodeReqDTO.CHECK_PATH), 246 ObjUtil.defaultIfNull(reqVO.getCheckPath(), SocialWxQrcodeReqDTO.CHECK_PATH),
240 - ObjUtil.defaultIfBlank(reqVO.getEnvVersion(), SocialWxQrcodeReqDTO.ENV_VERSION), 247 + envVersion,
241 ObjUtil.defaultIfNull(reqVO.getWidth(), SocialWxQrcodeReqDTO.WIDTH), 248 ObjUtil.defaultIfNull(reqVO.getWidth(), SocialWxQrcodeReqDTO.WIDTH),
242 ObjUtil.defaultIfNull(reqVO.getAutoColor(), SocialWxQrcodeReqDTO.AUTO_COLOR), 249 ObjUtil.defaultIfNull(reqVO.getAutoColor(), SocialWxQrcodeReqDTO.AUTO_COLOR),
243 null, 250 null,
yudao-server/src/main/resources/application-dev.yaml
@@ -171,6 +171,8 @@ yudao: @@ -171,6 +171,8 @@ yudao:
171 order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址 171 order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
172 refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址 172 refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
173 demo: true # 开启演示模式 173 demo: true # 开启演示模式
  174 + wxa-code:
  175 + env-version: release # 小程序版本: 正式版为 "release";体验版为 "trial";开发版为 "develop"
174 tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc 176 tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
175 177
176 justauth: 178 justauth:
yudao-server/src/main/resources/application-local.yaml
@@ -220,6 +220,8 @@ yudao: @@ -220,6 +220,8 @@ yudao:
220 access-log: # 访问日志的配置项 220 access-log: # 访问日志的配置项
221 enable: false 221 enable: false
222 demo: false # 关闭演示模式 222 demo: false # 关闭演示模式
  223 + wxa-code:
  224 + env-version: develop # 小程序版本: 正式版为 "release";体验版为 "trial";开发版为 "develop"
223 tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc 225 tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
224 226
225 justauth: 227 justauth:
yudao-server/src/main/resources/application.yaml
@@ -177,6 +177,8 @@ yudao: @@ -177,6 +177,8 @@ yudao:
177 license-url: https://gitee.com/zhijiantianya/ruoyi-vue-pro/blob/master/LICENSE 177 license-url: https://gitee.com/zhijiantianya/ruoyi-vue-pro/blob/master/LICENSE
178 captcha: 178 captcha:
179 enable: true # 验证码的开关,默认为 true 179 enable: true # 验证码的开关,默认为 true
  180 + wxa-code:
  181 + env-version: release # 小程序版本: 正式版为 "release";体验版为 "trial";开发版为 "develop"。默认为 release
180 codegen: 182 codegen:
181 base-package: ${yudao.info.base-package} 183 base-package: ${yudao.info.base-package}
182 db-schemas: ${spring.datasource.dynamic.datasource.master.name} 184 db-schemas: ${spring.datasource.dynamic.datasource.master.name}