Commit 611880a3c40266d402702b7489d76b61fa712ccb
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,6 +80,14 @@ public interface ErrorCodeConstants { | ||
| 80 | ErrorCode FACE_USER_ITEM_NOT_OWN = new ErrorCode(1_040_800_011, "不能操作他人的表情"); | 80 | ErrorCode FACE_USER_ITEM_NOT_OWN = new ErrorCode(1_040_800_011, "不能操作他人的表情"); |
| 81 | ErrorCode FACE_USER_ITEM_DUPLICATED = new ErrorCode(1_040_800_013, "该表情已添加到个人表情"); | 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 | // ========== 实时通话 (1-040-900-000) ========== | 91 | // ========== 实时通话 (1-040-900-000) ========== |
| 84 | ErrorCode RTC_NOT_ENABLED = new ErrorCode(1_040_900_000, "通话功能未开启"); | 92 | ErrorCode RTC_NOT_ENABLED = new ErrorCode(1_040_900_000, "通话功能未开启"); |
| 85 | ErrorCode RTC_SESSION_NOT_EXISTS = new ErrorCode(1_040_900_001, "通话已结束"); | 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,7 +17,8 @@ import java.util.Objects; | ||
| 17 | public enum ImConversationTypeEnum implements ArrayValuable<Integer> { | 17 | public enum ImConversationTypeEnum implements ArrayValuable<Integer> { |
| 18 | 18 | ||
| 19 | PRIVATE(1, "私聊"), // 私聊 | 19 | PRIVATE(1, "私聊"), // 私聊 |
| 20 | - GROUP(2, "群聊"); // 群聊 | 20 | + GROUP(2, "群聊"), // 群聊 |
| 21 | + CHANNEL(3, "频道"); // 频道 / 公众号 | ||
| 21 | 22 | ||
| 22 | public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImConversationTypeEnum::getType).toArray(Integer[]::new); | 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<Integer> { | @@ -43,4 +44,8 @@ public enum ImConversationTypeEnum implements ArrayValuable<Integer> { | ||
| 43 | return Objects.equals(GROUP.type, type); | 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<Integer> { | @@ -61,6 +61,15 @@ public enum ImMessageTypeEnum implements ArrayValuable<Integer> { | ||
| 61 | */ | 61 | */ |
| 62 | FACE(115, "表情", true, true), | 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 | // ========== 信号类(2101 / 2200 直接复用 OpenIM 段位编号;2201 自有扩展) ========== | 73 | // ========== 信号类(2101 / 2200 直接复用 OpenIM 段位编号;2201 自有扩展) ========== |
| 65 | /** | 74 | /** |
| 66 | * 对应 OpenIM:RevokeNotification 2101 | 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 | package cn.iocoder.yudao.module.im.service.websocket; | 1 | package cn.iocoder.yudao.module.im.service.websocket; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.im.service.websocket.dto.ImChannelMessageDTO; | ||
| 3 | import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; | 4 | import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; |
| 4 | import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; | 5 | import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; |
| 5 | 6 | ||
| @@ -53,4 +54,22 @@ public interface ImWebSocketService { | @@ -53,4 +54,22 @@ public interface ImWebSocketService { | ||
| 53 | */ | 54 | */ |
| 54 | void sendGroupMessageAsync(Collection<Long> userIds, ImGroupMessageDTO dto); | 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,6 +3,7 @@ package cn.iocoder.yudao.module.im.service.websocket; | ||
| 3 | import cn.hutool.extra.spring.SpringUtil; | 3 | import cn.hutool.extra.spring.SpringUtil; |
| 4 | import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | 4 | import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; |
| 5 | import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; | 5 | import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; |
| 6 | +import cn.iocoder.yudao.module.im.service.websocket.dto.ImChannelMessageDTO; | ||
| 6 | import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; | 7 | import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; |
| 7 | import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; | 8 | import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; |
| 8 | import jakarta.annotation.Resource; | 9 | import jakarta.annotation.Resource; |
| @@ -43,6 +44,11 @@ public class ImWebSocketServiceImpl implements ImWebSocketService { | @@ -43,6 +44,11 @@ public class ImWebSocketServiceImpl implements ImWebSocketService { | ||
| 43 | executeAfterTransaction(() -> getSelf().doSendGroupMessage(userIds, dto)); | 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 | * 异步发送私聊 WebSocket 消息;多收件人共享同一 dto,避免按收件人重复注册 afterCommit 回调 | 53 | * 异步发送私聊 WebSocket 消息;多收件人共享同一 dto,避免按收件人重复注册 afterCommit 回调 |
| 48 | */ | 54 | */ |
| @@ -74,6 +80,21 @@ public class ImWebSocketServiceImpl implements ImWebSocketService { | @@ -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 | * - 有事务:注册 afterCommit 回调,事务提交后再执行,防止客户端拿到消息去查库时数据还没落盘 | 99 | * - 有事务:注册 afterCommit 回调,事务提交后再执行,防止客户端拿到消息去查库时数据还没落盘 |
| 79 | * - 无事务:直接执行(如非 @Transactional 方法中的调用) | 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 "im_face_user_item" ( | @@ -236,6 +236,57 @@ CREATE TABLE IF NOT EXISTS "im_face_user_item" ( | ||
| 236 | PRIMARY KEY ("id") | 236 | PRIMARY KEY ("id") |
| 237 | ) COMMENT 'IM 用户私有表情表'; | 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 | CREATE TABLE IF NOT EXISTS "system_users" ( | 290 | CREATE TABLE IF NOT EXISTS "system_users" ( |
| 240 | "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, | 291 | "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, |
| 241 | "username" varchar(30) NOT NULL DEFAULT '', | 292 | "username" varchar(30) NOT NULL DEFAULT '', |