Commit 611880a3c40266d402702b7489d76b61fa712ccb

Authored by YunaiV
1 parent 6b1a0cfc

✨ feat(im): 新增频道消息的后端实现

Showing 32 changed files with 1690 additions and 1 deletions
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/ImChannelManagerController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.channel;
  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.module.im.controller.admin.manager.channel.vo.channel.ImChannelPageReqVO;
  7 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel.ImChannelRespVO;
  8 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel.ImChannelSaveReqVO;
  9 +import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelDO;
  10 +import cn.iocoder.yudao.module.im.service.channel.ImChannelService;
  11 +import io.swagger.v3.oas.annotations.Operation;
  12 +import io.swagger.v3.oas.annotations.Parameter;
  13 +import io.swagger.v3.oas.annotations.tags.Tag;
  14 +import jakarta.annotation.Resource;
  15 +import jakarta.validation.Valid;
  16 +import org.springframework.security.access.prepost.PreAuthorize;
  17 +import org.springframework.validation.annotation.Validated;
  18 +import org.springframework.web.bind.annotation.*;
  19 +
  20 +import java.util.List;
  21 +
  22 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  23 +
  24 +@Tag(name = "管理后台 - IM 频道")
  25 +@RestController
  26 +@RequestMapping("/im/manager/channel")
  27 +@Validated
  28 +public class ImChannelManagerController {
  29 +
  30 + @Resource
  31 + private ImChannelService channelService;
  32 +
  33 + @PostMapping("/create")
  34 + @Operation(summary = "新增频道")
  35 + @PreAuthorize("@ss.hasPermission('im:manager:channel:create')")
  36 + public CommonResult<Long> createChannel(@Valid @RequestBody ImChannelSaveReqVO reqVO) {
  37 + return success(channelService.createChannel(reqVO));
  38 + }
  39 +
  40 + @PutMapping("/update")
  41 + @Operation(summary = "修改频道")
  42 + @PreAuthorize("@ss.hasPermission('im:manager:channel:update')")
  43 + public CommonResult<Boolean> updateChannel(@Valid @RequestBody ImChannelSaveReqVO reqVO) {
  44 + channelService.updateChannel(reqVO);
  45 + return success(true);
  46 + }
  47 +
  48 + @DeleteMapping("/delete")
  49 + @Operation(summary = "删除频道")
  50 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  51 + @PreAuthorize("@ss.hasPermission('im:manager:channel:delete')")
  52 + public CommonResult<Boolean> deleteChannel(@RequestParam("id") Long id) {
  53 + channelService.deleteChannel(id);
  54 + return success(true);
  55 + }
  56 +
  57 + @GetMapping("/page")
  58 + @Operation(summary = "获得频道分页")
  59 + @PreAuthorize("@ss.hasPermission('im:manager:channel:query')")
  60 + public CommonResult<PageResult<ImChannelRespVO>> getChannelPage(@Valid ImChannelPageReqVO pageReqVO) {
  61 + PageResult<ImChannelDO> pageResult = channelService.getChannelPage(pageReqVO);
  62 + return success(BeanUtils.toBean(pageResult, ImChannelRespVO.class));
  63 + }
  64 +
  65 + @GetMapping("/get")
  66 + @Operation(summary = "获得频道详情")
  67 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  68 + @PreAuthorize("@ss.hasPermission('im:manager:channel:query')")
  69 + public CommonResult<ImChannelRespVO> getChannel(@RequestParam("id") Long id) {
  70 + ImChannelDO channel = channelService.getChannel(id);
  71 + return success(BeanUtils.toBean(channel, ImChannelRespVO.class));
  72 + }
  73 +
  74 + @GetMapping("/simple-list")
  75 + @Operation(summary = "获得启用的频道精简列表;前端表单选择频道时调用")
  76 + public CommonResult<List<ImChannelRespVO>> getEnabledChannelList() {
  77 + // TODO @AI:getChannelListByXXX;更通用点;
  78 + List<ImChannelDO> list = channelService.getEnabledChannelList();
  79 + return success(BeanUtils.toBean(list, ImChannelRespVO.class));
  80 + }
  81 +
  82 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/ImChannelMaterialManagerController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.channel;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  7 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  8 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material.ImChannelMaterialPageReqVO;
  9 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material.ImChannelMaterialRespVO;
  10 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material.ImChannelMaterialSaveReqVO;
  11 +import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelDO;
  12 +import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMaterialDO;
  13 +import cn.iocoder.yudao.module.im.service.channel.ImChannelMaterialService;
  14 +import cn.iocoder.yudao.module.im.service.channel.ImChannelService;
  15 +import io.swagger.v3.oas.annotations.Operation;
  16 +import io.swagger.v3.oas.annotations.Parameter;
  17 +import io.swagger.v3.oas.annotations.tags.Tag;
  18 +import jakarta.annotation.Resource;
  19 +import jakarta.validation.Valid;
  20 +import org.springframework.security.access.prepost.PreAuthorize;
  21 +import org.springframework.validation.annotation.Validated;
  22 +import org.springframework.web.bind.annotation.*;
  23 +
  24 +import java.util.List;
  25 +import java.util.Map;
  26 +
  27 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  28 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
  29 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  30 +
  31 +@Tag(name = "管理后台 - IM 频道素材")
  32 +@RestController
  33 +@RequestMapping("/im/manager/channel-material")
  34 +@Validated
  35 +public class ImChannelMaterialManagerController {
  36 +
  37 + @Resource
  38 + private ImChannelMaterialService channelMaterialService;
  39 + @Resource
  40 + private ImChannelService channelService;
  41 +
  42 + @PostMapping("/create")
  43 + @Operation(summary = "新增素材")
  44 + @PreAuthorize("@ss.hasPermission('im:manager:channel-material:create')")
  45 + public CommonResult<Long> createMaterial(@Valid @RequestBody ImChannelMaterialSaveReqVO reqVO) {
  46 + return success(channelMaterialService.createMaterial(reqVO));
  47 + }
  48 +
  49 + @PutMapping("/update")
  50 + @Operation(summary = "修改素材")
  51 + @PreAuthorize("@ss.hasPermission('im:manager:channel-material:update')")
  52 + public CommonResult<Boolean> updateMaterial(@Valid @RequestBody ImChannelMaterialSaveReqVO reqVO) {
  53 + channelMaterialService.updateMaterial(reqVO);
  54 + return success(true);
  55 + }
  56 +
  57 + @DeleteMapping("/delete")
  58 + @Operation(summary = "删除素材")
  59 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  60 + @PreAuthorize("@ss.hasPermission('im:manager:channel-material:delete')")
  61 + public CommonResult<Boolean> deleteMaterial(@RequestParam("id") Long id) {
  62 + channelMaterialService.deleteMaterial(id);
  63 + return success(true);
  64 + }
  65 +
  66 + @GetMapping("/page")
  67 + @Operation(summary = "获得素材分页;含频道名回填")
  68 + @PreAuthorize("@ss.hasPermission('im:manager:channel-material:query')")
  69 + public CommonResult<PageResult<ImChannelMaterialRespVO>> getMaterialPage(@Valid ImChannelMaterialPageReqVO pageReqVO) {
  70 + PageResult<ImChannelMaterialDO> pageResult = channelMaterialService.getMaterialPage(pageReqVO);
  71 + if (CollUtil.isEmpty(pageResult.getList())) {
  72 + return success(PageResult.empty(pageResult.getTotal()));
  73 + }
  74 + // 回填频道名
  75 + List<ImChannelDO> channels = channelService.getChannelList(
  76 + convertSet(pageResult.getList(), ImChannelMaterialDO::getChannelId));
  77 + Map<Long, ImChannelDO> channelMap = convertMap(channels, ImChannelDO::getId);
  78 + return success(BeanUtils.toBean(pageResult, ImChannelMaterialRespVO.class, vo ->
  79 + MapUtils.findAndThen(channelMap, vo.getChannelId(), c -> vo.setChannelName(c.getName()))));
  80 + }
  81 +
  82 + @GetMapping("/get")
  83 + @Operation(summary = "获得素材详情(含富文本正文)")
  84 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  85 + @PreAuthorize("@ss.hasPermission('im:manager:channel-material:query')")
  86 + public CommonResult<ImChannelMaterialRespVO> getMaterial(@RequestParam("id") Long id) {
  87 + ImChannelMaterialDO material = channelMaterialService.getMaterial(id);
  88 + return success(BeanUtils.toBean(material, ImChannelMaterialRespVO.class));
  89 + }
  90 +
  91 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/ImChannelMessageManagerController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.channel;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  7 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  8 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.message.ImChannelMessagePageReqVO;
  9 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.message.ImChannelMessageRespVO;
  10 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.message.ImChannelMessageSendReqVO;
  11 +import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelDO;
  12 +import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMaterialDO;
  13 +import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMessageDO;
  14 +import cn.iocoder.yudao.module.im.service.channel.ImChannelMaterialService;
  15 +import cn.iocoder.yudao.module.im.service.channel.ImChannelMessageService;
  16 +import cn.iocoder.yudao.module.im.service.channel.ImChannelService;
  17 +import io.swagger.v3.oas.annotations.Operation;
  18 +import io.swagger.v3.oas.annotations.Parameter;
  19 +import io.swagger.v3.oas.annotations.tags.Tag;
  20 +import jakarta.annotation.Resource;
  21 +import jakarta.validation.Valid;
  22 +import org.springframework.security.access.prepost.PreAuthorize;
  23 +import org.springframework.validation.annotation.Validated;
  24 +import org.springframework.web.bind.annotation.*;
  25 +
  26 +import java.util.Map;
  27 +
  28 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  29 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
  30 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  31 +
  32 +@Tag(name = "管理后台 - IM 频道消息")
  33 +@RestController
  34 +@RequestMapping("/im/manager/channel-message")
  35 +@Validated
  36 +public class ImChannelMessageManagerController {
  37 +
  38 + @Resource
  39 + private ImChannelMessageService channelMessageService;
  40 + @Resource
  41 + private ImChannelService channelService;
  42 + @Resource
  43 + private ImChannelMaterialService channelMaterialService;
  44 +
  45 + @PostMapping("/send")
  46 + @Operation(summary = "立即推送频道消息")
  47 + @PreAuthorize("@ss.hasPermission('im:manager:channel-message:send')")
  48 + public CommonResult<Long> sendMessage(@Valid @RequestBody ImChannelMessageSendReqVO reqVO) {
  49 + return success(channelMessageService.sendMessage(reqVO));
  50 + }
  51 +
  52 + @DeleteMapping("/delete")
  53 + @Operation(summary = "删除频道消息")
  54 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  55 + @PreAuthorize("@ss.hasPermission('im:manager:channel-message:delete')")
  56 + public CommonResult<Boolean> deleteMessage(@RequestParam("id") Long id) {
  57 + channelMessageService.deleteMessage(id);
  58 + return success(true);
  59 + }
  60 +
  61 + @GetMapping("/page")
  62 + @Operation(summary = "获得频道消息分页;回填频道名 / 素材标题")
  63 + @PreAuthorize("@ss.hasPermission('im:manager:channel-message:query')")
  64 + public CommonResult<PageResult<ImChannelMessageRespVO>> getMessagePage(@Valid ImChannelMessagePageReqVO pageReqVO) {
  65 + PageResult<ImChannelMessageDO> pageResult = channelMessageService.getMessagePage(pageReqVO);
  66 + if (CollUtil.isEmpty(pageResult.getList())) {
  67 + return success(PageResult.empty(pageResult.getTotal()));
  68 + }
  69 + // TODO @AI:最好是 channelService 也包出一个 getChannelMap;类似别的模块
  70 + Map<Long, ImChannelDO> channelMap = convertMap(
  71 + channelService.getChannelList(convertSet(pageResult.getList(), ImChannelMessageDO::getChannelId)),
  72 + ImChannelDO::getId);
  73 + // TODO @AI:最好是 channelService 也包出一个 getChannelMap;类似别的模块
  74 + Map<Long, ImChannelMaterialDO> materialMap = convertMap(
  75 + channelMaterialService.getMaterialList(convertSet(pageResult.getList(), ImChannelMessageDO::getMaterialId)),
  76 + ImChannelMaterialDO::getId);
  77 + return success(BeanUtils.toBean(pageResult, ImChannelMessageRespVO.class, vo -> {
  78 + MapUtils.findAndThen(channelMap, vo.getChannelId(), c -> vo.setChannelName(c.getName()));
  79 + MapUtils.findAndThen(materialMap, vo.getMaterialId(), m -> vo.setMaterialTitle(m.getTitle()));
  80 + }));
  81 + }
  82 +
  83 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/channel/ImChannelPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel;
  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 +
  8 +@Schema(description = "管理后台 - IM 频道分页 Request VO")
  9 +@Data
  10 +@EqualsAndHashCode(callSuper = true)
  11 +public class ImChannelPageReqVO extends PageParam {
  12 +
  13 + @Schema(description = "频道业务码", example = "system_notice")
  14 + private String code;
  15 +
  16 + @Schema(description = "频道名称", example = "系统")
  17 + private String name;
  18 +
  19 + @Schema(description = "状态", example = "0")
  20 + private Integer status; // 参见 CommonStatusEnum 枚举类
  21 +
  22 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/channel/ImChannelRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - IM 频道 Response VO")
  9 +@Data
  10 +public class ImChannelRespVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "频道业务码", requiredMode = Schema.RequiredMode.REQUIRED, example = "system_notice")
  16 + private String code;
  17 +
  18 + @Schema(description = "频道名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "系统公告")
  19 + private String name;
  20 +
  21 + @Schema(description = "频道头像")
  22 + private String avatar;
  23 +
  24 + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  25 + private Integer sort;
  26 +
  27 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  28 + private Integer status; // 参见 CommonStatusEnum 枚举类
  29 +
  30 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  31 + private LocalDateTime createTime;
  32 +
  33 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/channel/ImChannelSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotBlank;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import jakarta.validation.constraints.Pattern;
  7 +import jakarta.validation.constraints.Size;
  8 +import lombok.Data;
  9 +
  10 +@Schema(description = "管理后台 - IM 频道新增 / 修改 Request VO")
  11 +@Data
  12 +public class ImChannelSaveReqVO {
  13 +
  14 + @Schema(description = "编号(修改时必填)", example = "1024")
  15 + private Long id;
  16 +
  17 + @Schema(description = "频道业务码;唯一", requiredMode = Schema.RequiredMode.REQUIRED, example = "system_notice")
  18 + @NotBlank(message = "频道编码不能为空")
  19 + @Size(max = 64, message = "频道编码长度不能超过 64")
  20 + @Pattern(regexp = "^[a-z][a-z0-9_]*$", message = "频道编码只能由小写字母 / 数字 / 下划线组成,且必须以字母开头")
  21 + private String code;
  22 +
  23 + @Schema(description = "频道名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "系统公告")
  24 + @NotBlank(message = "频道名称不能为空")
  25 + @Size(max = 64, message = "频道名称长度不能超过 64")
  26 + private String name;
  27 +
  28 + @Schema(description = "频道头像", example = "https://cdn.example.com/channel/system_notice.png")
  29 + @Size(max = 512, message = "头像长度不能超过 512")
  30 + private String avatar;
  31 +
  32 + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  33 + @NotNull(message = "排序不能为空")
  34 + private Integer sort;
  35 +
  36 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  37 + @NotNull(message = "状态不能为空")
  38 + private Integer status; // 参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用)
  39 +
  40 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/material/ImChannelMaterialPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material;
  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 org.springframework.format.annotation.DateTimeFormat;
  8 +
  9 +import java.time.LocalDateTime;
  10 +
  11 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  12 +
  13 +@Schema(description = "管理后台 - IM 频道素材分页 Request VO")
  14 +@Data
  15 +@EqualsAndHashCode(callSuper = true)
  16 +public class ImChannelMaterialPageReqVO extends PageParam {
  17 +
  18 + @Schema(description = "频道编号", example = "1")
  19 + private Long channelId;
  20 +
  21 + @Schema(description = "素材类型", example = "125")
  22 + private Integer type;
  23 +
  24 + @Schema(description = "标题", example = "活动")
  25 + private String title;
  26 +
  27 + @Schema(description = "创建时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
  28 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  29 + private LocalDateTime[] createTime;
  30 +
  31 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/material/ImChannelMaterialRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - IM 频道素材 Response VO")
  9 +@Data
  10 +public class ImChannelMaterialRespVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "频道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  16 + private Long channelId;
  17 +
  18 + @Schema(description = "频道名称(关联查询填充)")
  19 + private String channelName;
  20 +
  21 + @Schema(description = "素材类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "125")
  22 + private Integer type; // 参见 ImMessageTypeEnum 枚举类
  23 +
  24 + @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED)
  25 + private String title;
  26 +
  27 + @Schema(description = "封面图")
  28 + private String coverUrl;
  29 +
  30 + @Schema(description = "摘要")
  31 + private String summary;
  32 +
  33 + @Schema(description = "正文;富文本 HTML")
  34 + private String content;
  35 +
  36 + @Schema(description = "跳转链接")
  37 + private String url;
  38 +
  39 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  40 + private LocalDateTime createTime;
  41 +
  42 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/material/ImChannelMaterialSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotBlank;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import jakarta.validation.constraints.Size;
  7 +import lombok.Data;
  8 +
  9 +@Schema(description = "管理后台 - IM 频道素材新增 / 修改 Request VO")
  10 +@Data
  11 +public class ImChannelMaterialSaveReqVO {
  12 +
  13 + @Schema(description = "编号(修改时必填)", example = "1024")
  14 + private Long id;
  15 +
  16 + @Schema(description = "频道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  17 + @NotNull(message = "频道编号不能为空")
  18 + private Long channelId;
  19 +
  20 + @Schema(description = "素材类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "125")
  21 + @NotNull(message = "素材类型不能为空")
  22 + private Integer type; // 参见 ImMessageTypeEnum 枚举类
  23 +
  24 + @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "双十一活动来啦")
  25 + @NotBlank(message = "标题不能为空")
  26 + @Size(max = 128, message = "标题长度不能超过 128")
  27 + private String title;
  28 +
  29 + @Schema(description = "封面图", example = "https://cdn.example.com/cover.png")
  30 + @Size(max = 512, message = "封面图长度不能超过 512")
  31 + private String coverUrl;
  32 +
  33 + @Schema(description = "摘要", example = "全场五折,戳详情看玩法")
  34 + @Size(max = 255, message = "摘要长度不能超过 255")
  35 + private String summary;
  36 +
  37 + @Schema(description = "正文;富文本 HTML")
  38 + private String content;
  39 +
  40 + @Schema(description = "跳转链接;为空表示走客户端内置详情页", example = "https://example.com/activity/123")
  41 + @Size(max = 512, message = "跳转链接长度不能超过 512")
  42 + private String url;
  43 +
  44 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/message/ImChannelMessagePageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.channel.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 org.springframework.format.annotation.DateTimeFormat;
  8 +
  9 +import java.time.LocalDateTime;
  10 +
  11 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  12 +
  13 +@Schema(description = "管理后台 - IM 频道消息分页 Request VO")
  14 +@Data
  15 +@EqualsAndHashCode(callSuper = true)
  16 +public class ImChannelMessagePageReqVO extends PageParam {
  17 +
  18 + @Schema(description = "频道编号", example = "1")
  19 + private Long channelId;
  20 +
  21 + @Schema(description = "素材编号", example = "1024")
  22 + private Long materialId;
  23 +
  24 + @Schema(description = "发送时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
  25 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  26 + private LocalDateTime[] sendTime;
  27 +
  28 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/message/ImChannelMessageRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.message;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +import java.util.List;
  8 +
  9 +@Schema(description = "管理后台 - IM 频道消息 Response VO")
  10 +@Data
  11 +public class ImChannelMessageRespVO {
  12 +
  13 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  14 + private Long id;
  15 +
  16 + @Schema(description = "频道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  17 + private Long channelId;
  18 +
  19 + @Schema(description = "频道名称(关联查询填充)")
  20 + private String channelName;
  21 +
  22 + @Schema(description = "素材编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  23 + private Long materialId;
  24 +
  25 + @Schema(description = "素材标题(关联查询填充)")
  26 + private String materialTitle;
  27 +
  28 + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "125")
  29 + private Integer type; // 参见 ImMessageTypeEnum 枚举类
  30 +
  31 + @Schema(description = "消息内容;payload JSON 快照")
  32 + private String content;
  33 +
  34 + @Schema(description = "接收人编号列表;为空表示全员")
  35 + private List<Long> receiverUserIds;
  36 +
  37 + @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED)
  38 + private LocalDateTime sendTime;
  39 +
  40 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/vo/message/ImChannelMessageSendReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.message;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +import java.util.List;
  8 +
  9 +@Schema(description = "管理后台 - IM 频道消息推送 Request VO")
  10 +@Data
  11 +public class ImChannelMessageSendReqVO {
  12 +
  13 + @Schema(description = "素材编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  14 + @NotNull(message = "素材编号不能为空")
  15 + private Long materialId;
  16 +
  17 + @Schema(description = "接收人编号列表;为空表示全员", example = "[1024, 2048]")
  18 + private List<Long> receiverUserIds;
  19 +
  20 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/channel/ImChannelDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.dal.dataobject.channel;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  5 +import com.baomidou.mybatisplus.annotation.KeySequence;
  6 +import com.baomidou.mybatisplus.annotation.TableId;
  7 +import com.baomidou.mybatisplus.annotation.TableName;
  8 +import lombok.*;
  9 +
  10 +/**
  11 + * IM 频道 DO
  12 + * <p>
  13 + * 业务语义:
  14 + * - 频道是运营单向推送的主体;C 端用户不能向频道发消息
  15 + * - {@link #code} 是业务码(API / 字典外露),id 是数字主键给前端会话 targetId 用
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +@TableName("im_channel")
  20 +@KeySequence("im_channel_seq")
  21 +@Data
  22 +@EqualsAndHashCode(callSuper = true)
  23 +@ToString(callSuper = true)
  24 +@Builder
  25 +@NoArgsConstructor
  26 +@AllArgsConstructor
  27 +public class ImChannelDO extends BaseDO {
  28 +
  29 + /**
  30 + * 编号
  31 + */
  32 + @TableId
  33 + private Long id;
  34 + /**
  35 + * 频道业务码
  36 + */
  37 + private String code;
  38 + /**
  39 + * 频道名称
  40 + */
  41 + private String name;
  42 + /**
  43 + * 频道头像
  44 + */
  45 + private String avatar;
  46 + /**
  47 + * 排序
  48 + */
  49 + private Integer sort;
  50 + /**
  51 + * 状态
  52 + * <p>
  53 + * 枚举 {@link CommonStatusEnum}
  54 + */
  55 + private Integer status;
  56 +
  57 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/channel/ImChannelMaterialDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.dal.dataobject.channel;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  4 +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  5 +import com.baomidou.mybatisplus.annotation.KeySequence;
  6 +import com.baomidou.mybatisplus.annotation.TableId;
  7 +import com.baomidou.mybatisplus.annotation.TableName;
  8 +import lombok.*;
  9 +
  10 +/**
  11 + * IM 频道素材 DO
  12 + * <p>
  13 + * 业务语义:
  14 + * - 运营素材库,可被反复推送
  15 + * - 一条素材 1:N 关联多条 {@link ImChannelMessageDO}
  16 + * - {@link #content} 富文本仅在素材详情接口按需返回,推送 payload 不带,避免压爆 WebSocket 通道
  17 + *
  18 + * @author 芋道源码
  19 + */
  20 +@TableName("im_channel_material")
  21 +@KeySequence("im_channel_material_seq")
  22 +@Data
  23 +@EqualsAndHashCode(callSuper = true)
  24 +@ToString(callSuper = true)
  25 +@Builder
  26 +@NoArgsConstructor
  27 +@AllArgsConstructor
  28 +public class ImChannelMaterialDO extends BaseDO {
  29 +
  30 + /**
  31 + * 编号
  32 + */
  33 + @TableId
  34 + private Long id;
  35 + /**
  36 + * 频道编号
  37 + * <p>
  38 + * 关联 {@link ImChannelDO#getId()}
  39 + */
  40 + private Long channelId;
  41 + /**
  42 + * 素材类型
  43 + * <p>
  44 + * 枚举 {@link ImMessageTypeEnum};当前仅 MATERIAL(125)
  45 + */
  46 + private Integer type;
  47 + /**
  48 + * 标题
  49 + */
  50 + private String title;
  51 + /**
  52 + * 封面图
  53 + */
  54 + private String coverUrl;
  55 + /**
  56 + * 摘要
  57 + */
  58 + private String summary;
  59 + // TODO @AI:素材类型,是不是有个 type;这样一个 content、url 更好区分;然后这个 type ,是不是在 MATERIAL Message 类上,也带上;
  60 + /**
  61 + * 正文;富文本 HTML
  62 + */
  63 + private String content;
  64 + /**
  65 + * 跳转链接
  66 + * <p>
  67 + * 为空时点击在客户端内置详情页拉 {@link #content} 渲染;非空则跳 url
  68 + */
  69 + private String url;
  70 +
  71 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/channel/ImChannelMessageDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.dal.dataobject.channel;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  4 +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
  5 +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  6 +import com.baomidou.mybatisplus.annotation.KeySequence;
  7 +import com.baomidou.mybatisplus.annotation.TableField;
  8 +import com.baomidou.mybatisplus.annotation.TableId;
  9 +import com.baomidou.mybatisplus.annotation.TableName;
  10 +import lombok.*;
  11 +
  12 +import java.time.LocalDateTime;
  13 +import java.util.List;
  14 +
  15 +/**
  16 + * IM 频道消息 DO
  17 + * <p>
  18 + * 业务语义:
  19 + * - 一次推送 1 行;{@link #receiverUserIds} 为空表示全员
  20 + * - {@link #channelId} 冗余 {@link ImChannelMaterialDO#getChannelId()} 便于按频道检索
  21 + * - {@link #content} 存推送时 payload 的 JSON 快照(title / coverUrl / summary / url);不含富文本正文
  22 + *
  23 + * @author 芋道源码
  24 + */
  25 +@TableName(value = "im_channel_message", autoResultMap = true)
  26 +@KeySequence("im_channel_message_seq")
  27 +@Data
  28 +@EqualsAndHashCode(callSuper = true)
  29 +@ToString(callSuper = true)
  30 +@Builder
  31 +@NoArgsConstructor
  32 +@AllArgsConstructor
  33 +public class ImChannelMessageDO extends BaseDO {
  34 +
  35 + /**
  36 + * 编号
  37 + */
  38 + @TableId
  39 + private Long id;
  40 + /**
  41 + * 频道编号
  42 + * <p>
  43 + * 关联 {@link ImChannelDO#getId()};冗余便于按频道检索
  44 + */
  45 + private Long channelId;
  46 + /**
  47 + * 关联素材编号
  48 + * <p>
  49 + * 关联 {@link ImChannelMaterialDO#getId()}
  50 + */
  51 + private Long materialId;
  52 + /**
  53 + * 消息类型
  54 + * <p>
  55 + * 枚举 {@link ImMessageTypeEnum}
  56 + */
  57 + private Integer type;
  58 + /**
  59 + * 消息内容;推送时 payload 的 JSON 快照
  60 + */
  61 + private String content;
  62 + /**
  63 + * 接收人编号列表;为空表示全员
  64 + */
  65 + @TableField(typeHandler = LongListTypeHandler.class)
  66 + private List<Long> receiverUserIds;
  67 + /**
  68 + * 发送时间
  69 + */
  70 + private LocalDateTime sendTime;
  71 +
  72 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/channel/ImChannelMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.dal.mysql.channel;
  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.im.controller.admin.manager.channel.vo.channel.ImChannelPageReqVO;
  7 +import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelDO;
  8 +import org.apache.ibatis.annotations.Mapper;
  9 +
  10 +import java.util.List;
  11 +
  12 +/**
  13 + * IM 频道 Mapper
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Mapper
  18 +public interface ImChannelMapper extends BaseMapperX<ImChannelDO> {
  19 +
  20 + default ImChannelDO selectByCode(String code) {
  21 + return selectOne(ImChannelDO::getCode, code);
  22 + }
  23 +
  24 + default List<ImChannelDO> selectListByStatusOrderBySort(Integer status) {
  25 + return selectList(new LambdaQueryWrapperX<ImChannelDO>()
  26 + .eq(ImChannelDO::getStatus, status)
  27 + .orderByAsc(ImChannelDO::getSort));
  28 + }
  29 +
  30 + default PageResult<ImChannelDO> selectPage(ImChannelPageReqVO reqVO) {
  31 + return selectPage(reqVO, new LambdaQueryWrapperX<ImChannelDO>()
  32 + .likeIfPresent(ImChannelDO::getCode, reqVO.getCode())
  33 + .likeIfPresent(ImChannelDO::getName, reqVO.getName())
  34 + .eqIfPresent(ImChannelDO::getStatus, reqVO.getStatus())
  35 + .orderByAsc(ImChannelDO::getSort)
  36 + .orderByDesc(ImChannelDO::getId));
  37 + }
  38 +
  39 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/channel/ImChannelMaterialMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.dal.mysql.channel;
  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.im.controller.admin.manager.channel.vo.material.ImChannelMaterialPageReqVO;
  7 +import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMaterialDO;
  8 +import org.apache.ibatis.annotations.Mapper;
  9 +
  10 +/**
  11 + * IM 频道素材 Mapper
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@Mapper
  16 +public interface ImChannelMaterialMapper extends BaseMapperX<ImChannelMaterialDO> {
  17 +
  18 + default Long selectCountByChannelId(Long channelId) {
  19 + return selectCount(ImChannelMaterialDO::getChannelId, channelId);
  20 + }
  21 +
  22 + default PageResult<ImChannelMaterialDO> selectPage(ImChannelMaterialPageReqVO reqVO) {
  23 + LambdaQueryWrapperX<ImChannelMaterialDO> wrapper = new LambdaQueryWrapperX<ImChannelMaterialDO>()
  24 + .eqIfPresent(ImChannelMaterialDO::getChannelId, reqVO.getChannelId())
  25 + .eqIfPresent(ImChannelMaterialDO::getType, reqVO.getType())
  26 + .likeIfPresent(ImChannelMaterialDO::getTitle, reqVO.getTitle())
  27 + .betweenIfPresent(ImChannelMaterialDO::getCreateTime, reqVO.getCreateTime())
  28 + .orderByDesc(ImChannelMaterialDO::getId);
  29 + // TODO @AI:不用排除;列表问题不大的;
  30 + // 列表查询排除富文本 content 字段,避免 mediumtext 拖垮分页响应
  31 + wrapper.select(ImChannelMaterialDO::getId, ImChannelMaterialDO::getChannelId,
  32 + ImChannelMaterialDO::getType, ImChannelMaterialDO::getTitle,
  33 + ImChannelMaterialDO::getCoverUrl, ImChannelMaterialDO::getSummary,
  34 + ImChannelMaterialDO::getUrl, ImChannelMaterialDO::getCreator,
  35 + ImChannelMaterialDO::getCreateTime, ImChannelMaterialDO::getUpdater,
  36 + ImChannelMaterialDO::getUpdateTime, ImChannelMaterialDO::getDeleted);
  37 + return selectPage(reqVO, wrapper);
  38 + }
  39 +
  40 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/channel/ImChannelMessageMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.dal.mysql.channel;
  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.im.controller.admin.manager.channel.vo.message.ImChannelMessagePageReqVO;
  7 +import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMessageDO;
  8 +import org.apache.ibatis.annotations.Mapper;
  9 +import org.apache.ibatis.annotations.Param;
  10 +import org.apache.ibatis.annotations.Select;
  11 +
  12 +import java.util.List;
  13 +
  14 +/**
  15 + * IM 频道消息 Mapper
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +@Mapper
  20 +public interface ImChannelMessageMapper extends BaseMapperX<ImChannelMessageDO> {
  21 +
  22 + // TODO @AI:代码风格,和别的 message 一致;
  23 + /**
  24 + * 拉取指定用户应收的频道消息
  25 + * <p>
  26 + * 命中条件:id 大于游标 + (receiver_user_ids 为空表示全员 OR 逗号分隔列表里包含当前 userId)
  27 + *
  28 + * @param userId 当前用户编号
  29 + * @param minId 游标;返回大于此值的消息
  30 + * @param size 返回条数
  31 + * @return 频道消息列表;按 id 升序
  32 + */
  33 + @Select("SELECT * FROM im_channel_message "
  34 + + "WHERE deleted = 0 "
  35 + + "AND id > #{minId} "
  36 + + "AND (receiver_user_ids IS NULL "
  37 + + " OR receiver_user_ids = '' "
  38 + + " OR FIND_IN_SET(#{userId}, receiver_user_ids)) "
  39 + + "ORDER BY id ASC "
  40 + + "LIMIT #{size}")
  41 + List<ImChannelMessageDO> selectListByUserAndMinId(@Param("userId") Long userId,
  42 + @Param("minId") Long minId,
  43 + @Param("size") Integer size);
  44 +
  45 + // TODO @AI:代码风格,和别的 message 一致;
  46 + /**
  47 + * 校验用户是否曾经收到过指定素材(receiver_user_ids 为空表示全员);用于详情接口归属校验
  48 + */
  49 + @Select("SELECT EXISTS(SELECT 1 FROM im_channel_message "
  50 + + "WHERE deleted = 0 AND material_id = #{materialId} "
  51 + + "AND (receiver_user_ids IS NULL "
  52 + + " OR receiver_user_ids = '' "
  53 + + " OR FIND_IN_SET(#{userId}, receiver_user_ids)) "
  54 + + "LIMIT 1)")
  55 + boolean existsByUserAndMaterial(@Param("userId") Long userId,
  56 + @Param("materialId") Long materialId);
  57 +
  58 + default Long selectCountByMaterialId(Long materialId) {
  59 + return selectCount(ImChannelMessageDO::getMaterialId, materialId);
  60 + }
  61 +
  62 + default PageResult<ImChannelMessageDO> selectPage(ImChannelMessagePageReqVO reqVO) {
  63 + return selectPage(reqVO, new LambdaQueryWrapperX<ImChannelMessageDO>()
  64 + .eqIfPresent(ImChannelMessageDO::getChannelId, reqVO.getChannelId())
  65 + .eqIfPresent(ImChannelMessageDO::getMaterialId, reqVO.getMaterialId())
  66 + .betweenIfPresent(ImChannelMessageDO::getSendTime, reqVO.getSendTime())
  67 + .orderByDesc(ImChannelMessageDO::getId));
  68 + }
  69 +
  70 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java
... ... @@ -80,6 +80,14 @@ public interface ErrorCodeConstants {
80 80 ErrorCode FACE_USER_ITEM_NOT_OWN = new ErrorCode(1_040_800_011, "不能操作他人的表情");
81 81 ErrorCode FACE_USER_ITEM_DUPLICATED = new ErrorCode(1_040_800_013, "该表情已添加到个人表情");
82 82  
  83 + // ========== 频道 (1-040-810-000) ==========
  84 + ErrorCode IM_CHANNEL_NOT_EXISTS = new ErrorCode(1_040_810_000, "频道不存在");
  85 + ErrorCode IM_CHANNEL_CODE_DUPLICATED = new ErrorCode(1_040_810_001, "频道编码 '{}' 已存在");
  86 + ErrorCode IM_CHANNEL_HAS_MATERIAL = new ErrorCode(1_040_810_002, "频道下还有素材,无法删除");
  87 + ErrorCode IM_CHANNEL_MATERIAL_NOT_EXISTS = new ErrorCode(1_040_810_010, "素材不存在");
  88 + ErrorCode IM_CHANNEL_MATERIAL_USED = new ErrorCode(1_040_810_011, "素材已被推送过,无法删除");
  89 + ErrorCode IM_CHANNEL_MESSAGE_NOT_EXISTS = new ErrorCode(1_040_810_020, "频道消息不存在");
  90 +
83 91 // ========== 实时通话 (1-040-900-000) ==========
84 92 ErrorCode RTC_NOT_ENABLED = new ErrorCode(1_040_900_000, "通话功能未开启");
85 93 ErrorCode RTC_SESSION_NOT_EXISTS = new ErrorCode(1_040_900_001, "通话已结束");
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImConversationTypeEnum.java
... ... @@ -17,7 +17,8 @@ import java.util.Objects;
17 17 public enum ImConversationTypeEnum implements ArrayValuable<Integer> {
18 18  
19 19 PRIVATE(1, "私聊"), // 私聊
20   - GROUP(2, "群聊"); // 群聊
  20 + GROUP(2, "群聊"), // 群聊
  21 + CHANNEL(3, "频道"); // 频道 / 公众号
21 22  
22 23 public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImConversationTypeEnum::getType).toArray(Integer[]::new);
23 24  
... ... @@ -43,4 +44,8 @@ public enum ImConversationTypeEnum implements ArrayValuable&lt;Integer&gt; {
43 44 return Objects.equals(GROUP.type, type);
44 45 }
45 46  
  47 + public static boolean isChannel(Integer type) {
  48 + return Objects.equals(CHANNEL.type, type);
  49 + }
  50 +
46 51 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java
... ... @@ -61,6 +61,15 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; {
61 61 */
62 62 FACE(115, "表情", true, true),
63 63  
  64 + // ========== 频道消息扩展段(125+;OpenIM 122 之后未占用,本系统在 125 起步给频道 / 公众号类消息扩展) ==========
  65 + /**
  66 + * 对应 OpenIM:无(125 段位 OpenIM 未占用,作为频道消息扩展起始位)
  67 + * 对应自己的类:MaterialMessage
  68 + * 场景:频道运营推送的素材消息;当前形态为图文卡片(title + coverUrl + summary + url)
  69 + * 详情:url 非空跳 url;url 为空时客户端按 materialId 拉 /get-content 渲染富文本正文
  70 + */
  71 + MATERIAL(125, "素材", true, true),
  72 +
64 73 // ========== 信号类(2101 / 2200 直接复用 OpenIM 段位编号;2201 自有扩展) ==========
65 74 /**
66 75 * 对应 OpenIM:RevokeNotification 2101
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/channel/ImChannelMaterialService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.channel;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material.ImChannelMaterialPageReqVO;
  5 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material.ImChannelMaterialSaveReqVO;
  6 +import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMaterialDO;
  7 +import jakarta.validation.Valid;
  8 +
  9 +import java.util.Collection;
  10 +import java.util.List;
  11 +
  12 +/**
  13 + * IM 频道素材 Service 接口
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +public interface ImChannelMaterialService {
  18 +
  19 + // ==================== 用户端 ====================
  20 +
  21 + /**
  22 + * 获取素材正文;用于客户端点击图文卡片时拉富文本
  23 + *
  24 + * @param id 素材编号
  25 + * @return 富文本 HTML
  26 + */
  27 + String getMaterialContent(Long id);
  28 +
  29 + /**
  30 + * 校验素材存在
  31 + *
  32 + * @param id 素材编号
  33 + * @return 素材 DO
  34 + */
  35 + ImChannelMaterialDO validateMaterialExists(Long id);
  36 +
  37 + /**
  38 + * 按编号批量查询素材
  39 + *
  40 + * @param ids 素材编号列表
  41 + * @return 素材列表
  42 + */
  43 + List<ImChannelMaterialDO> getMaterialList(Collection<Long> ids);
  44 +
  45 + /**
  46 + * 统计指定频道下的素材数量
  47 + *
  48 + * @param channelId 频道编号
  49 + * @return 数量
  50 + */
  51 + Long getMaterialCountByChannelId(Long channelId);
  52 +
  53 + // ==================== 管理后台 ====================
  54 +
  55 + /**
  56 + * 分页查询素材
  57 + *
  58 + * @param reqVO 分页查询条件
  59 + * @return 素材分页
  60 + */
  61 + PageResult<ImChannelMaterialDO> getMaterialPage(ImChannelMaterialPageReqVO reqVO);
  62 +
  63 + /**
  64 + * 获取素材详情(含 content 富文本)
  65 + *
  66 + * @param id 素材编号
  67 + * @return 素材 DO
  68 + */
  69 + ImChannelMaterialDO getMaterial(Long id);
  70 +
  71 + /**
  72 + * 新增素材
  73 + *
  74 + * @param reqVO 新增请求
  75 + * @return 新增素材编号
  76 + */
  77 + Long createMaterial(@Valid ImChannelMaterialSaveReqVO reqVO);
  78 +
  79 + /**
  80 + * 修改素材
  81 + *
  82 + * @param reqVO 修改请求
  83 + */
  84 + void updateMaterial(@Valid ImChannelMaterialSaveReqVO reqVO);
  85 +
  86 + /**
  87 + * 删除素材;素材已被推送过时拒绝,避免历史消息无法回查
  88 + *
  89 + * @param id 素材编号
  90 + */
  91 + void deleteMaterial(Long id);
  92 +
  93 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/channel/ImChannelMaterialServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.channel;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material.ImChannelMaterialPageReqVO;
  7 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material.ImChannelMaterialSaveReqVO;
  8 +import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMaterialDO;
  9 +import cn.iocoder.yudao.module.im.dal.mysql.channel.ImChannelMaterialMapper;
  10 +import cn.iocoder.yudao.module.im.dal.mysql.channel.ImChannelMessageMapper;
  11 +import jakarta.annotation.Resource;
  12 +import org.springframework.stereotype.Service;
  13 +import org.springframework.validation.annotation.Validated;
  14 +
  15 +import java.util.Collection;
  16 +import java.util.Collections;
  17 +import java.util.List;
  18 +
  19 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  20 +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.IM_CHANNEL_MATERIAL_NOT_EXISTS;
  21 +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.IM_CHANNEL_MATERIAL_USED;
  22 +
  23 +/**
  24 + * IM 频道素材 Service 实现类
  25 + *
  26 + * @author 芋道源码
  27 + */
  28 +@Service
  29 +@Validated
  30 +public class ImChannelMaterialServiceImpl implements ImChannelMaterialService {
  31 +
  32 + @Resource
  33 + private ImChannelMaterialMapper channelMaterialMapper;
  34 + @Resource
  35 + private ImChannelService channelService;
  36 + @Resource
  37 + private ImChannelMessageMapper channelMessageMapper;
  38 +
  39 + // ==================== 用户端 ====================
  40 +
  41 + @Override
  42 + public String getMaterialContent(Long id) {
  43 + ImChannelMaterialDO material = validateMaterialExists(id);
  44 + return material.getContent();
  45 + }
  46 +
  47 + @Override
  48 + public ImChannelMaterialDO validateMaterialExists(Long id) {
  49 + ImChannelMaterialDO material = channelMaterialMapper.selectById(id);
  50 + if (material == null) {
  51 + throw exception(IM_CHANNEL_MATERIAL_NOT_EXISTS);
  52 + }
  53 + return material;
  54 + }
  55 +
  56 + @Override
  57 + public List<ImChannelMaterialDO> getMaterialList(Collection<Long> ids) {
  58 + if (CollUtil.isEmpty(ids)) {
  59 + return Collections.emptyList();
  60 + }
  61 + return channelMaterialMapper.selectByIds(ids);
  62 + }
  63 +
  64 + @Override
  65 + public Long getMaterialCountByChannelId(Long channelId) {
  66 + return channelMaterialMapper.selectCountByChannelId(channelId);
  67 + }
  68 +
  69 + // ==================== 管理后台 ====================
  70 +
  71 + @Override
  72 + public PageResult<ImChannelMaterialDO> getMaterialPage(ImChannelMaterialPageReqVO reqVO) {
  73 + return channelMaterialMapper.selectPage(reqVO);
  74 + }
  75 +
  76 + @Override
  77 + public ImChannelMaterialDO getMaterial(Long id) {
  78 + return channelMaterialMapper.selectById(id);
  79 + }
  80 +
  81 + // TODO @AI:类似别的模块,写全注释
  82 + @Override
  83 + public Long createMaterial(ImChannelMaterialSaveReqVO reqVO) {
  84 + channelService.validateChannelExists(reqVO.getChannelId());
  85 + ImChannelMaterialDO material = BeanUtils.toBean(reqVO, ImChannelMaterialDO.class);
  86 + channelMaterialMapper.insert(material);
  87 + return material.getId();
  88 + }
  89 +
  90 + // TODO @AI:类似别的模块,写全注释
  91 + @Override
  92 + public void updateMaterial(ImChannelMaterialSaveReqVO reqVO) {
  93 + validateMaterialExists(reqVO.getId());
  94 + channelService.validateChannelExists(reqVO.getChannelId());
  95 + ImChannelMaterialDO updateObj = BeanUtils.toBean(reqVO, ImChannelMaterialDO.class);
  96 + channelMaterialMapper.updateById(updateObj);
  97 + }
  98 +
  99 + @Override
  100 + public void deleteMaterial(Long id) {
  101 + validateMaterialExists(id);
  102 + // 防止删除素材导致历史 channel_message 反查不到内容
  103 + if (channelMessageMapper.selectCountByMaterialId(id) > 0) {
  104 + throw exception(IM_CHANNEL_MATERIAL_USED);
  105 + }
  106 + channelMaterialMapper.deleteById(id);
  107 + }
  108 +
  109 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/channel/ImChannelMessageService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.channel;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.message.ImChannelMessagePageReqVO;
  5 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.message.ImChannelMessageSendReqVO;
  6 +import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMessageDO;
  7 +import jakarta.validation.Valid;
  8 +
  9 +import java.util.List;
  10 +
  11 +// TODO @AI:这块消息,我想挪到 message 里;后续,可能整合成一个表存储的;
  12 +/**
  13 + * IM 频道消息 Service 接口
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +public interface ImChannelMessageService {
  18 +
  19 + // ==================== 用户端 ====================
  20 +
  21 + /**
  22 + * 拉取当前用户应收的频道消息(离线增量)
  23 + *
  24 + * @param userId 当前用户编号
  25 + * @param minId 游标;返回大于此值的消息
  26 + * @param size 返回条数
  27 + * @return 频道消息列表;按 id 升序
  28 + */
  29 + List<ImChannelMessageDO> getMessageListForPull(Long userId, Long minId, Integer size);
  30 +
  31 + /**
  32 + * 校验用户是否能看到指定素材;用作详情接口的归属校验
  33 + *
  34 + * @param userId 当前用户编号
  35 + * @param materialId 素材编号
  36 + * @return 是否能看到(曾经收到过 / 全员消息)
  37 + */
  38 + boolean isUserReceivedMaterial(Long userId, Long materialId);
  39 +
  40 + // ==================== 管理后台 ====================
  41 +
  42 + /**
  43 + * 立即推送频道消息
  44 + * <p>
  45 + * 流程:读 material → 组装 payload → INSERT 1 行 im_channel_message → WebSocket 扇出
  46 + *
  47 + * @param reqVO 推送请求
  48 + * @return 消息编号
  49 + */
  50 + Long sendMessage(@Valid ImChannelMessageSendReqVO reqVO);
  51 +
  52 + /**
  53 + * 分页查询消息
  54 + *
  55 + * @param reqVO 分页查询条件
  56 + * @return 消息分页
  57 + */
  58 + PageResult<ImChannelMessageDO> getMessagePage(ImChannelMessagePageReqVO reqVO);
  59 +
  60 + /**
  61 + * 删除消息
  62 + *
  63 + * @param id 消息编号
  64 + */
  65 + void deleteMessage(Long id);
  66 +
  67 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/channel/ImChannelMessageServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.channel;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  6 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.message.ImChannelMessagePageReqVO;
  7 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.message.ImChannelMessageSendReqVO;
  8 +import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMaterialDO;
  9 +import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMessageDO;
  10 +import cn.iocoder.yudao.module.im.dal.mysql.channel.ImChannelMessageMapper;
  11 +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  12 +import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
  13 +import cn.iocoder.yudao.module.im.service.websocket.dto.ImChannelMessageDTO;
  14 +import cn.iocoder.yudao.module.im.service.websocket.dto.message.MaterialMessage;
  15 +import jakarta.annotation.Resource;
  16 +import org.springframework.stereotype.Service;
  17 +import org.springframework.transaction.annotation.Transactional;
  18 +import org.springframework.validation.annotation.Validated;
  19 +
  20 +import java.time.LocalDateTime;
  21 +import java.util.List;
  22 +
  23 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  24 +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.IM_CHANNEL_MESSAGE_NOT_EXISTS;
  25 +
  26 +/**
  27 + * IM 频道消息 Service 实现类
  28 + *
  29 + * @author 芋道源码
  30 + */
  31 +@Service
  32 +@Validated
  33 +public class ImChannelMessageServiceImpl implements ImChannelMessageService {
  34 +
  35 + @Resource
  36 + private ImChannelMessageMapper channelMessageMapper;
  37 + @Resource
  38 + private ImChannelMaterialService channelMaterialService;
  39 + @Resource
  40 + private ImWebSocketService webSocketService;
  41 +
  42 + // ==================== 用户端 ====================
  43 +
  44 + @Override
  45 + public List<ImChannelMessageDO> getMessageListForPull(Long userId, Long minId, Integer size) {
  46 + return channelMessageMapper.selectListByUserAndMinId(userId, minId, size);
  47 + }
  48 +
  49 + @Override
  50 + public boolean isUserReceivedMaterial(Long userId, Long materialId) {
  51 + return channelMessageMapper.existsByUserAndMaterial(userId, materialId);
  52 + }
  53 +
  54 + // ==================== 管理后台 ====================
  55 +
  56 + @Override
  57 + @Transactional(rollbackFor = Exception.class)
  58 + public Long sendMessage(ImChannelMessageSendReqVO reqVO) {
  59 + // 1. 校验素材存在
  60 + ImChannelMaterialDO material = channelMaterialService.validateMaterialExists(reqVO.getMaterialId());
  61 +
  62 + // 2. 组装 payload(不带富文本正文)
  63 + // TODO @AI:链式调用;想通在一行
  64 + MaterialMessage payload = new MaterialMessage()
  65 + .setTitle(material.getTitle())
  66 + .setCoverUrl(material.getCoverUrl())
  67 + .setSummary(material.getSummary())
  68 + .setUrl(material.getUrl());
  69 + String payloadJson = JsonUtils.toJsonString(payload);
  70 +
  71 + // 3. 落库 1 行 message
  72 + // TODO @AI:链式调用;想通在一行
  73 + ImChannelMessageDO message = new ImChannelMessageDO()
  74 + .setChannelId(material.getChannelId())
  75 + .setMaterialId(material.getId())
  76 + .setType(ImMessageTypeEnum.MATERIAL.getType())
  77 + .setContent(payloadJson)
  78 + .setReceiverUserIds(reqVO.getReceiverUserIds())
  79 + .setSendTime(LocalDateTime.now());
  80 + channelMessageMapper.insert(message);
  81 +
  82 + // 4. 异步推 WebSocket;全员场景(receiverUserIds 为空)暂不实时广播,依靠客户端上线后 pull 兜底
  83 + // TODO @AI:全院也要发;
  84 + if (CollUtil.isNotEmpty(reqVO.getReceiverUserIds())) {
  85 + webSocketService.sendChannelMessageAsync(reqVO.getReceiverUserIds(),
  86 + ImChannelMessageDTO.ofSend(message));
  87 + }
  88 + return message.getId();
  89 + }
  90 +
  91 + @Override
  92 + public PageResult<ImChannelMessageDO> getMessagePage(ImChannelMessagePageReqVO reqVO) {
  93 + return channelMessageMapper.selectPage(reqVO);
  94 + }
  95 +
  96 + @Override
  97 + public void deleteMessage(Long id) {
  98 + if (channelMessageMapper.selectById(id) == null) {
  99 + throw exception(IM_CHANNEL_MESSAGE_NOT_EXISTS);
  100 + }
  101 + channelMessageMapper.deleteById(id);
  102 + }
  103 +
  104 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/channel/ImChannelService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.channel;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel.ImChannelPageReqVO;
  5 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel.ImChannelSaveReqVO;
  6 +import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelDO;
  7 +import jakarta.validation.Valid;
  8 +
  9 +import java.util.Collection;
  10 +import java.util.List;
  11 +
  12 +/**
  13 + * IM 频道 Service 接口
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +public interface ImChannelService {
  18 +
  19 + // ==================== 用户端 ====================
  20 +
  21 + // TODO @AI:getChannelListByStatus 这种形式;
  22 + /**
  23 + * 获取所有启用的频道,按 sort 升序
  24 + *
  25 + * @return 启用的频道列表
  26 + */
  27 + List<ImChannelDO> getEnabledChannelList();
  28 +
  29 + /**
  30 + * 按编号批量查询频道
  31 + *
  32 + * @param ids 频道编号列表
  33 + * @return 频道列表
  34 + */
  35 + List<ImChannelDO> getChannelList(Collection<Long> ids);
  36 +
  37 + /**
  38 + * 校验频道存在
  39 + *
  40 + * @param id 频道编号
  41 + * @return 频道 DO
  42 + */
  43 + @SuppressWarnings("UnusedReturnValue")
  44 + ImChannelDO validateChannelExists(Long id);
  45 +
  46 + // ==================== 管理后台 ====================
  47 +
  48 + /**
  49 + * 分页查询频道
  50 + *
  51 + * @param reqVO 分页查询条件
  52 + * @return 频道分页
  53 + */
  54 + PageResult<ImChannelDO> getChannelPage(ImChannelPageReqVO reqVO);
  55 +
  56 + /**
  57 + * 获取频道详情
  58 + *
  59 + * @param id 频道编号
  60 + * @return 频道 DO
  61 + */
  62 + ImChannelDO getChannel(Long id);
  63 +
  64 + /**
  65 + * 新增频道
  66 + *
  67 + * @param reqVO 新增请求
  68 + * @return 新增频道编号
  69 + */
  70 + Long createChannel(@Valid ImChannelSaveReqVO reqVO);
  71 +
  72 + /**
  73 + * 修改频道
  74 + *
  75 + * @param reqVO 修改请求
  76 + */
  77 + void updateChannel(@Valid ImChannelSaveReqVO reqVO);
  78 +
  79 + /**
  80 + * 删除频道;频道下有素材或消息时拒绝
  81 + *
  82 + * @param id 频道编号
  83 + */
  84 + void deleteChannel(Long id);
  85 +
  86 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/channel/ImChannelServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.channel;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  7 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel.ImChannelPageReqVO;
  8 +import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel.ImChannelSaveReqVO;
  9 +import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelDO;
  10 +import cn.iocoder.yudao.module.im.dal.mysql.channel.ImChannelMapper;
  11 +import jakarta.annotation.Resource;
  12 +import org.springframework.stereotype.Service;
  13 +import org.springframework.validation.annotation.Validated;
  14 +
  15 +import java.util.Collection;
  16 +import java.util.Collections;
  17 +import java.util.List;
  18 +import java.util.Objects;
  19 +
  20 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  21 +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.IM_CHANNEL_CODE_DUPLICATED;
  22 +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.IM_CHANNEL_HAS_MATERIAL;
  23 +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.IM_CHANNEL_NOT_EXISTS;
  24 +
  25 +/**
  26 + * IM 频道 Service 实现类
  27 + *
  28 + * @author 芋道源码
  29 + */
  30 +@Service
  31 +@Validated
  32 +public class ImChannelServiceImpl implements ImChannelService {
  33 +
  34 + @Resource
  35 + private ImChannelMapper channelMapper;
  36 + @Resource
  37 + private ImChannelMaterialService channelMaterialService;
  38 +
  39 + // ==================== 用户端 ====================
  40 +
  41 + @Override
  42 + public List<ImChannelDO> getEnabledChannelList() {
  43 + return channelMapper.selectListByStatusOrderBySort(CommonStatusEnum.ENABLE.getStatus());
  44 + }
  45 +
  46 + @Override
  47 + public List<ImChannelDO> getChannelList(Collection<Long> ids) {
  48 + if (CollUtil.isEmpty(ids)) {
  49 + return Collections.emptyList();
  50 + }
  51 + return channelMapper.selectByIds(ids);
  52 + }
  53 +
  54 + @Override
  55 + public ImChannelDO validateChannelExists(Long id) {
  56 + ImChannelDO channel = channelMapper.selectById(id);
  57 + if (channel == null) {
  58 + throw exception(IM_CHANNEL_NOT_EXISTS);
  59 + }
  60 + return channel;
  61 + }
  62 +
  63 + // ==================== 管理后台 ====================
  64 +
  65 + @Override
  66 + public PageResult<ImChannelDO> getChannelPage(ImChannelPageReqVO reqVO) {
  67 + return channelMapper.selectPage(reqVO);
  68 + }
  69 +
  70 + @Override
  71 + public ImChannelDO getChannel(Long id) {
  72 + return channelMapper.selectById(id);
  73 + }
  74 +
  75 + @Override
  76 + public Long createChannel(ImChannelSaveReqVO reqVO) {
  77 + // 校验 code 唯一
  78 + validateCodeUnique(null, reqVO.getCode());
  79 +
  80 + // 插入
  81 + ImChannelDO channel = BeanUtils.toBean(reqVO, ImChannelDO.class);
  82 + channelMapper.insert(channel);
  83 + return channel.getId();
  84 + }
  85 +
  86 + @Override
  87 + public void updateChannel(ImChannelSaveReqVO reqVO) {
  88 + // 1.1 校验存在
  89 + validateChannelExists(reqVO.getId());
  90 + // 1.2 校验 code 唯一
  91 + validateCodeUnique(reqVO.getId(), reqVO.getCode());
  92 +
  93 + // 2. 更新
  94 + ImChannelDO updateObj = BeanUtils.toBean(reqVO, ImChannelDO.class);
  95 + channelMapper.updateById(updateObj);
  96 + }
  97 +
  98 + @Override
  99 + public void deleteChannel(Long id) {
  100 + // 1.1 校验存在
  101 + validateChannelExists(id);
  102 + // 1.2 防止误删频道导致历史素材 / 消息回查不到归属
  103 + if (channelMaterialService.getMaterialCountByChannelId(id) > 0) {
  104 + throw exception(IM_CHANNEL_HAS_MATERIAL);
  105 + }
  106 +
  107 + // 2. 删除频道
  108 + channelMapper.deleteById(id);
  109 + }
  110 +
  111 + private void validateCodeUnique(Long id, String code) {
  112 + ImChannelDO exist = channelMapper.selectByCode(code);
  113 + if (exist == null) {
  114 + return;
  115 + }
  116 + // TODO @AI:notEquals objutils;
  117 + if (id == null || !Objects.equals(exist.getId(), id)) {
  118 + throw exception(IM_CHANNEL_CODE_DUPLICATED, code);
  119 + }
  120 + }
  121 +
  122 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketService.java
1 1 package cn.iocoder.yudao.module.im.service.websocket;
2 2  
  3 +import cn.iocoder.yudao.module.im.service.websocket.dto.ImChannelMessageDTO;
3 4 import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
4 5 import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
5 6  
... ... @@ -53,4 +54,22 @@ public interface ImWebSocketService {
53 54 */
54 55 void sendGroupMessageAsync(Collection<Long> userIds, ImGroupMessageDTO dto);
55 56  
  57 + /**
  58 + * 异步推送频道消息给指定用户
  59 + *
  60 + * @param userId 目标用户编号
  61 + * @param dto 频道消息 DTO
  62 + */
  63 + default void sendChannelMessageAsync(Long userId, ImChannelMessageDTO dto) {
  64 + sendChannelMessageAsync(Collections.singleton(userId), dto);
  65 + }
  66 +
  67 + /**
  68 + * 异步批量推送频道消息给多个用户
  69 + *
  70 + * @param userIds 目标用户编号列表
  71 + * @param dto 频道消息 DTO
  72 + */
  73 + void sendChannelMessageAsync(Collection<Long> userIds, ImChannelMessageDTO dto);
  74 +
56 75 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImpl.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.im.service.websocket;
3 3 import cn.hutool.extra.spring.SpringUtil;
4 4 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
5 5 import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
  6 +import cn.iocoder.yudao.module.im.service.websocket.dto.ImChannelMessageDTO;
6 7 import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
7 8 import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
8 9 import jakarta.annotation.Resource;
... ... @@ -43,6 +44,11 @@ public class ImWebSocketServiceImpl implements ImWebSocketService {
43 44 executeAfterTransaction(() -> getSelf().doSendGroupMessage(userIds, dto));
44 45 }
45 46  
  47 + @Override
  48 + public void sendChannelMessageAsync(Collection<Long> userIds, ImChannelMessageDTO dto) {
  49 + executeAfterTransaction(() -> getSelf().doSendChannelMessage(userIds, dto));
  50 + }
  51 +
46 52 /**
47 53 * 异步发送私聊 WebSocket 消息;多收件人共享同一 dto,避免按收件人重复注册 afterCommit 回调
48 54 */
... ... @@ -74,6 +80,21 @@ public class ImWebSocketServiceImpl implements ImWebSocketService {
74 80 }
75 81  
76 82 /**
  83 + * 异步发送频道 WebSocket 消息
  84 + */
  85 + @Async
  86 + public void doSendChannelMessage(Collection<Long> userIds, ImChannelMessageDTO dto) {
  87 + for (Long userId : userIds) {
  88 + try {
  89 + webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId,
  90 + ImChannelMessageDTO.TYPE, dto);
  91 + } catch (Exception e) {
  92 + log.error("[doSendChannelMessage][userId({}) dto({}) 发送失败]", userId, dto, e);
  93 + }
  94 + }
  95 + }
  96 +
  97 + /**
77 98 * 事务感知的任务调度:
78 99 * - 有事务:注册 afterCommit 回调,事务提交后再执行,防止客户端拿到消息去查库时数据还没落盘
79 100 * - 无事务:直接执行(如非 @Transactional 方法中的调用)
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImChannelMessageDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto;
  2 +
  3 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  4 +import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMessageDO;
  5 +import lombok.Data;
  6 +import lombok.experimental.Accessors;
  7 +
  8 +import java.time.LocalDateTime;
  9 +
  10 +/**
  11 + * IM 频道消息 WebSocket 推送 DTO
  12 + * <p>
  13 + * 单向:服务端运营推送 → C 端用户;C 端不能向频道发消息。
  14 + * 字段分层:顶层是消息元数据 + 检索维度,content 是 MaterialMessage payload 的 JSON 串。
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@Data
  19 +@Accessors(chain = true)
  20 +public class ImChannelMessageDTO {
  21 +
  22 + public static final String TYPE = "im-channel-message";
  23 +
  24 + /**
  25 + * 消息编号
  26 + */
  27 + private Long id;
  28 + /**
  29 + * 频道编号
  30 + */
  31 + private Long channelId;
  32 + /**
  33 + * 关联素材编号
  34 + */
  35 + private Long materialId;
  36 + /**
  37 + * 消息类型
  38 + */
  39 + private Integer type;
  40 + /**
  41 + * 消息内容;payload JSON 串
  42 + */
  43 + private String content;
  44 + /**
  45 + * 发送时间
  46 + */
  47 + private LocalDateTime sendTime;
  48 +
  49 + /**
  50 + * 由频道消息 DO 构建推送 DTO
  51 + */
  52 + public static ImChannelMessageDTO ofSend(ImChannelMessageDO message) {
  53 + return BeanUtils.toBean(message, ImChannelMessageDTO.class);
  54 + }
  55 +
  56 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/MaterialMessage.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.message;
  2 +
  3 +import lombok.Data;
  4 +import lombok.experimental.Accessors;
  5 +
  6 +/**
  7 + * 频道素材消息 payload
  8 + * <p>
  9 + * 对应 {@link cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum#MATERIAL}(type=125)。
  10 + * 客户端按本字段集渲染图文卡片;
  11 + * 富文本正文不在本 payload 中传递,点击详情时另调 /im/channel/material/get-content?id= 按需拉取。
  12 + */
  13 +@Data
  14 +@Accessors(chain = true)
  15 +public class MaterialMessage {
  16 +
  17 + /**
  18 + * 标题
  19 + */
  20 + private String title;
  21 + /**
  22 + * 封面图
  23 + */
  24 + private String coverUrl;
  25 + /**
  26 + * 摘要
  27 + */
  28 + private String summary;
  29 + /**
  30 + * 跳转链接;为空时点击在客户端内置详情页按 materialId 拉 content 渲染;非空则跳 url
  31 + */
  32 + private String url;
  33 +
  34 +}
... ...
yudao-module-im/src/test/resources/sql/create_tables.sql
... ... @@ -236,6 +236,57 @@ CREATE TABLE IF NOT EXISTS &quot;im_face_user_item&quot; (
236 236 PRIMARY KEY ("id")
237 237 ) COMMENT 'IM 用户私有表情表';
238 238  
  239 +CREATE TABLE IF NOT EXISTS "im_channel" (
  240 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号',
  241 + "code" varchar(64) NOT NULL COMMENT '频道业务码;唯一',
  242 + "name" varchar(64) NOT NULL COMMENT '频道名称',
  243 + "avatar" varchar(512) DEFAULT NULL COMMENT '频道头像',
  244 + "sort" int NOT NULL DEFAULT 0 COMMENT '排序',
  245 + "status" tinyint NOT NULL DEFAULT 0 COMMENT '状态;0 启用 1 停用',
  246 + "creator" varchar(64) DEFAULT '',
  247 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  248 + "updater" varchar(64) DEFAULT '',
  249 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  250 + "deleted" bit NOT NULL DEFAULT FALSE,
  251 + "tenant_id" bigint NOT NULL DEFAULT 0,
  252 + PRIMARY KEY ("id")
  253 +) COMMENT 'IM 频道表';
  254 +
  255 +CREATE TABLE IF NOT EXISTS "im_channel_material" (
  256 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号',
  257 + "channel_id" bigint NOT NULL COMMENT '频道编号',
  258 + "type" smallint NOT NULL COMMENT '素材类型;对应 ImMessageTypeEnum',
  259 + "title" varchar(128) NOT NULL COMMENT '标题',
  260 + "cover_url" varchar(512) DEFAULT NULL COMMENT '封面图',
  261 + "summary" varchar(255) DEFAULT NULL COMMENT '摘要',
  262 + "content" clob DEFAULT NULL COMMENT '正文;富文本 HTML',
  263 + "url" varchar(512) DEFAULT NULL COMMENT '跳转链接;为空时点击在客户端内置详情页拉 content;非空则跳 url',
  264 + "creator" varchar(64) DEFAULT '',
  265 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  266 + "updater" varchar(64) DEFAULT '',
  267 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  268 + "deleted" bit NOT NULL DEFAULT FALSE,
  269 + "tenant_id" bigint NOT NULL DEFAULT 0,
  270 + PRIMARY KEY ("id")
  271 +) COMMENT 'IM 频道素材表';
  272 +
  273 +CREATE TABLE IF NOT EXISTS "im_channel_message" (
  274 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号',
  275 + "channel_id" bigint NOT NULL COMMENT '频道编号;冗余 im_channel_material.channel_id 便于检索',
  276 + "material_id" bigint NOT NULL COMMENT '关联素材编号',
  277 + "type" smallint NOT NULL COMMENT '消息类型',
  278 + "content" varchar(8192) DEFAULT NULL COMMENT '消息内容;推送时 payload JSON 快照;不含富文本正文',
  279 + "receiver_user_ids" varchar(1024) DEFAULT NULL COMMENT '接收人编号列表;逗号分隔;为空表示全员',
  280 + "send_time" timestamp NOT NULL COMMENT '发送时间',
  281 + "creator" varchar(64) DEFAULT '',
  282 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  283 + "updater" varchar(64) DEFAULT '',
  284 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  285 + "deleted" bit NOT NULL DEFAULT FALSE,
  286 + "tenant_id" bigint NOT NULL DEFAULT 0,
  287 + PRIMARY KEY ("id")
  288 +) COMMENT 'IM 频道消息表';
  289 +
239 290 CREATE TABLE IF NOT EXISTS "system_users" (
240 291 "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
241 292 "username" varchar(30) NOT NULL DEFAULT '',
... ...