diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/ImConversationController.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/ImConversationController.java index 6d08321..9f8c3a4 100755 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/ImConversationController.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/ImConversationController.java @@ -8,9 +8,6 @@ import cn.iocoder.yudao.module.im.controller.admin.conversation.vo.ImConversatio import cn.iocoder.yudao.module.im.controller.admin.conversation.vo.ImConversationUpdatePinnedReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ImConversationDO; import cn.iocoder.yudao.module.im.service.conversation.ImConversationService; -import cn.iocoder.yudao.module.im.service.message.ImMessageService; -import cn.iocoder.yudao.module.system.api.user.AdminUserApi; -import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; @@ -32,28 +29,12 @@ public class ImConversationController { @Resource private ImConversationService imConversationService; - @Resource - private ImMessageService imMessageService; - - @Resource - AdminUserApi adminUserApi; - + // TODO @芋艿:头像和昵称的聚合应交给前端完成 @GetMapping("/list") @Operation(summary = "获得用户的会话列表") public CommonResult> getConversationList() { List conversationList = imConversationService.getConversationList(getLoginUserId()); List imConversationRespVOList = BeanUtils.toBean(conversationList, ImConversationRespVO.class); - // TODO @dylan:这块,交给前端聚合哈。im 这块,我们重前端,后端更多解决消息的通信和存储。 - imConversationRespVOList.forEach(item -> { - // 处理个人图像和昵称 - Long receiverId = item.getTargetId(); - AdminUserRespDTO receiverUser = adminUserApi.getUser(receiverId); - if (receiverUser != null) { - item.setAvatar(receiverUser.getAvatar()); - item.setNickname(receiverUser.getNickname()); - } - - }); return success(imConversationRespVOList); } diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java index 830b4b5..6d2c763 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java @@ -2,46 +2,35 @@ package cn.iocoder.yudao.module.im.controller.admin.group.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; -import com.alibaba.excel.annotation.*; import java.time.LocalDateTime; -// TODO @AI:不需要导出的注解; @Schema(description = "管理后台 - 群 Response VO") @Data -@ExcelIgnoreUnannotated public class ImGroupRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1003") - @ExcelProperty("编号") private Long id; @Schema(description = "群名称", example = "芋艿") - @ExcelProperty("群名称") private String name; @Schema(description = "群主用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31460") - @ExcelProperty("群主用户编号") private Long ownerUserId; @Schema(description = "群头像") - @ExcelProperty("群头像") private String avatar; @Schema(description = "群公告") - @ExcelProperty("群公告") private String notice; @Schema(description = "是否封禁") - @ExcelProperty("是否封禁") private Boolean banned; @Schema(description = "是否解散") - @ExcelProperty("是否解散") private Boolean dissolved; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) - @ExcelProperty("创建时间") private LocalDateTime createTime; } \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/groupmember/ImGroupMemberController.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/groupmember/ImGroupMemberController.java index 6b96d37..05a3ecf 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/groupmember/ImGroupMemberController.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/groupmember/ImGroupMemberController.java @@ -1,28 +1,22 @@ package cn.iocoder.yudao.module.im.controller.admin.groupmember; import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; import cn.iocoder.yudao.module.im.controller.admin.groupmember.vo.ImGroupMemberPageReqVO; import cn.iocoder.yudao.module.im.controller.admin.groupmember.vo.ImGroupMemberRespVO; import cn.iocoder.yudao.module.im.controller.admin.groupmember.vo.ImGroupMemberSaveReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; -import cn.iocoder.yudao.module.im.service.groupmember.ImGroupMemberService; +import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; -import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import java.io.IOException; -import java.util.List; - import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; // TODO @芋艿:得看看 create、update、delete、get、page 这几个接口,要保留哪些 @@ -76,16 +70,4 @@ public class ImGroupMemberController { return success(BeanUtils.toBean(pageResult, ImGroupMemberRespVO.class)); } - @GetMapping("/export-excel") - @Operation(summary = "导出群成员 Excel") - @PreAuthorize("@ss.hasPermission('im:group-member:export')") - public void exportGroupMemberExcel(@Valid ImGroupMemberPageReqVO pageReqVO, - HttpServletResponse response) throws IOException { - pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); - List list = imGroupMemberService.getGroupMemberPage(pageReqVO).getList(); - // 导出 Excel - ExcelUtils.write(response, "群成员.xls", "数据", ImGroupMemberRespVO.class, - BeanUtils.toBean(list, ImGroupMemberRespVO.class)); - } - } \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/groupmember/vo/ImGroupMemberRespVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/groupmember/vo/ImGroupMemberRespVO.java index 2333de3..df52ad7 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/groupmember/vo/ImGroupMemberRespVO.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/groupmember/vo/ImGroupMemberRespVO.java @@ -1,56 +1,42 @@ package cn.iocoder.yudao.module.im.controller.admin.groupmember.vo; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; -// TODO @AI:去掉部分导出字段 @Schema(description = "管理后台 - 群成员 Response VO") @Data -@ExcelIgnoreUnannotated public class ImGroupMemberRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17071") - @ExcelProperty("编号") private Long id; @Schema(description = "群编号", example = "13279") - @ExcelProperty("群编号") private Long groupId; @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21730") - @ExcelProperty("用户编号") private Long userId; @Schema(description = "组内显示名", example = "芋艿") - @ExcelProperty("组内显示名") private String displayUserName; @Schema(description = "群显示备注", example = "核心群") - @ExcelProperty("群显示备注") private String displayGroupName; @Schema(description = "是否免打扰") - @ExcelProperty("是否免打扰") private Boolean muted; @Schema(description = "成员状态", example = "0") - @ExcelProperty("成员状态") private Integer status; @Schema(description = "入群时间") - @ExcelProperty("入群时间") private LocalDateTime joinTime; @Schema(description = "退群时间") - @ExcelProperty("退群时间") private LocalDateTime quitTime; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) - @ExcelProperty("创建时间") private LocalDateTime createTime; } \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImMessageController.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImMessageController.java deleted file mode 100755 index 053aad7..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImMessageController.java +++ /dev/null @@ -1,56 +0,0 @@ -package cn.iocoder.yudao.module.im.controller.admin.message; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.im.controller.admin.message.vo.*; -import cn.iocoder.yudao.module.im.dal.dataobject.message.ImMessageDO; -import cn.iocoder.yudao.module.im.service.message.ImMessageService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.validation.Valid; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; - -@Tag(name = "管理后台 - IM 消息") -@RestController -@RequestMapping("/im/message") -@Validated -public class ImMessageController { - - @Resource - private ImMessageService imMessageService; - - @PostMapping("/send") - @Operation(summary = "发送消息") - public CommonResult sendMessage(@Valid @RequestBody ImMessageSendReqVO imMessageSendReqVO) { - ImMessageDO message = imMessageService.sendMessage(getLoginUserId(), imMessageSendReqVO); - return success(BeanUtils.toBean(message, ImMessageSendRespVO.class)); - } - - @GetMapping("/pull") - @Operation(summary = "消息列表-拉取大于 sequence 的消息列表") - @Parameter(name = "sequence", description = "序号", required = true, example = "1") - @Parameter(name = "size", description = "条数", required = true, example = "10") - public CommonResult> pullMessageList(@RequestParam("sequence") Long sequence, - @RequestParam("size") Integer size) { - List messages = imMessageService.pullMessageList(getLoginUserId(), sequence, size); - return success(BeanUtils.toBean(messages, ImMessageRespVO.class)); - } - - @GetMapping("/list") - @Operation(summary = "消息列表-根据接收人和发送时间进行分页查询") - public CommonResult> getMessageList(@Valid ImMessageListReqVO listReqVO) { - - Long loginUserId = getLoginUserId(); - List messagePage = imMessageService.getMessageList(listReqVO, loginUserId); - return success(BeanUtils.toBean(messagePage, ImMessageRespVO.class)); - } - -} \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/ImMessageListByNoReqVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/ImMessageListByNoReqVO.java deleted file mode 100755 index 9b1bc57..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/ImMessageListByNoReqVO.java +++ /dev/null @@ -1,27 +0,0 @@ -package cn.iocoder.yudao.module.im.controller.admin.message.vo; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.Data; -import lombok.ToString; -import org.springframework.format.annotation.DateTimeFormat; - -import java.time.LocalDateTime; - -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; - -// TODO @dylan:看看,是不是融合到 ImMessageListReqVO 里 -@Schema(description = "管理后台 - 消息列表 Request VO") -@Data -@ToString(callSuper = true) -public class ImMessageListByNoReqVO { - - @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-03-27 12:00:00") - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) - private LocalDateTime sendTime; - - @Schema(description = "会话编号编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "32494") - @NotNull(message = "会话编号不能为空") - private String conversationNo ; - -} \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/ImMessageListReqVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/ImMessageListReqVO.java deleted file mode 100755 index c377875..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/ImMessageListReqVO.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.iocoder.yudao.module.im.controller.admin.message.vo; - -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.im.enums.conversation.ImConversationTypeEnum; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.Data; -import org.springframework.format.annotation.DateTimeFormat; - -import java.time.LocalDateTime; - -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; - -@Schema(description = "管理后台 - 消息列表 Request VO") -@Data -public class ImMessageListReqVO { - - @Schema(description = "接收人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "32494") - @NotNull(message = "接收人编号不能为空") - private Long receiverId; - - // TODO @dylan:这个是不是不用传递呀。因为 http 连接,有当前的 userid 呀 - @Schema(description = "会话所属人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "32494") - @NotNull(message = "会话所属人编号不能为空") - private Long userId; - - @Schema(description = "会话类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") - @InEnum(value = ImConversationTypeEnum.class,message = "会话类型必须是 {value}") - @NotNull(message = "会话类型不能为空") - private Integer conversationType; - - @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-03-27 12:00:00") - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) - private LocalDateTime sendTime; - -} \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/ImMessageRespVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/ImMessageRespVO.java deleted file mode 100755 index 10f0996..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/ImMessageRespVO.java +++ /dev/null @@ -1,47 +0,0 @@ -package cn.iocoder.yudao.module.im.controller.admin.message.vo; - -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.im.enums.conversation.ImConversationTypeEnum; -import cn.iocoder.yudao.module.im.enums.message.ImMessageContentTypeEnum; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.time.LocalDateTime; - -@Schema(description = "管理后台 - 消息 Response VO") -@Data -public class ImMessageRespVO { - - @Schema(description = "消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12454") - private Long id; - - @Schema(description = "会话类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @InEnum(value = ImConversationTypeEnum.class) - private Integer conversationType; - - @Schema(description = "发送人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - private Long senderId; - - @Schema(description = "发送人昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") - private String senderNickname; - - @Schema(description = "发送人头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://www.iocoder.cn/xxx.jpg") - private String senderAvatar; - - @Schema(description = "接收人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") - private Long receiverId; - - @Schema(description = "内容类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @InEnum(value = ImMessageContentTypeEnum.class) - private Integer contentType; - - @Schema(description = "消息内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好") - private String content; - - @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-03-27 12:00:00") - private LocalDateTime sendTime; - - @Schema(description = "序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - private Long sequence; - -} \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/ImMessageSendReqVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/ImMessageSendReqVO.java deleted file mode 100755 index 2f93fac..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/ImMessageSendReqVO.java +++ /dev/null @@ -1,41 +0,0 @@ -package cn.iocoder.yudao.module.im.controller.admin.message.vo; - -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.im.enums.conversation.ImConversationTypeEnum; -import cn.iocoder.yudao.module.im.enums.message.ImMessageContentTypeEnum; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -@Schema(description = "管理后台 - 发送消息 Request VO") -@Data -public class ImMessageSendReqVO { - - @Schema(description = "客户端消息编号 uuid,用于排重", requiredMode = Schema.RequiredMode.REQUIRED, example = "3331") - @NotNull(message = "客户端消息编号不能为空") - private String clientMessageId; - - @Schema(description = "接收人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "32494") - @NotNull(message = "接收人编号不能为空") - private Long receiverId; - - @Schema(description = "会话类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") - @InEnum(value = ImConversationTypeEnum.class) - @NotNull(message = "会话类型不能为空") - private Integer conversationType; - - // TODO @dylan:这个是不是不用传递呀。因为 http 连接,有当前的 userid 呀 - @Schema(description = "会话所属用户id", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") - @NotNull(message = "会话所属用户id") - private Long conversationUserId; - - @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @InEnum(ImMessageContentTypeEnum.class) - @NotNull(message = "消息类型不能为空") - private Integer contentType; - - @Schema(description = "消息内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好") - @NotNull(message = "消息内容不能为空") - private String content; - -} \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/ImMessageSendRespVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/ImMessageSendRespVO.java deleted file mode 100755 index 188cddc..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/ImMessageSendRespVO.java +++ /dev/null @@ -1,26 +0,0 @@ -package cn.iocoder.yudao.module.im.controller.admin.message.vo; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.format.annotation.DateTimeFormat; - -import java.time.LocalDateTime; - -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; - -@Schema(description = "管理后台 - 发送消息 Response VO") -@Data -@AllArgsConstructor -@NoArgsConstructor -public class ImMessageSendRespVO { - - @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12") - private Long id; - - @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-03-27 12:00:00") - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) - private LocalDateTime sendTime; - -} \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/inbox/ImInboxDO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/inbox/ImInboxDO.java deleted file mode 100755 index bea1f44..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/inbox/ImInboxDO.java +++ /dev/null @@ -1,49 +0,0 @@ -package cn.iocoder.yudao.module.im.dal.dataobject.inbox; - -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.module.im.dal.dataobject.message.ImMessageDO; -import com.baomidou.mybatisplus.annotation.KeySequence; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.*; - -// TODO 我们要不要改成 ImMessageQueue 队列?从理解上,概念上,可能都更清晰一点哈。每个用户一个消息队列; - -/** - * IM 收件箱 DO - * - * @author 芋道源码 - */ -@TableName("im_inbox") -@KeySequence("im_inbox_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 -@Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ImInboxDO extends BaseDO { - - /** - * 编号 - */ - @TableId - private Long id; - /** - * 用户编号 - *

- * 关联 {@link ImMessageDO#getSenderId()} 或者 {@link ImMessageDO#getReceiverId()} - */ - private Long userId; - /** - * 消息编号 - *

- * 关联 {@link ImMessageDO#getId()} - */ - private Long messageId; - /** - * 序号,按照 user 递增 - */ - private Long sequence; - -} \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/inbox/ImInboxMapper.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/inbox/ImInboxMapper.java deleted file mode 100755 index d9ff6c2..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/inbox/ImInboxMapper.java +++ /dev/null @@ -1,27 +0,0 @@ -package cn.iocoder.yudao.module.im.dal.mysql.inbox; - -import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; -import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; -import cn.iocoder.yudao.module.im.dal.dataobject.inbox.ImInboxDO; -import org.apache.ibatis.annotations.Mapper; - -import java.util.List; - -/** - * IM 收件箱 Mapper - * - * @author 芋道源码 - */ -@Mapper -public interface ImInboxMapper extends BaseMapperX { - - default List selectListByUserIdAndSequence(Long userId, Long sequence, Integer size) { - return selectList(new LambdaQueryWrapperX() - .eq(ImInboxDO::getUserId, userId) -// .gtIfPresent() // TODO @hao:可以用这个,简化下面的 .gt(sequence != null, ImInboxDO::getSequence, sequence) - .gt(sequence != null, ImInboxDO::getSequence, sequence) - .orderByDesc(ImInboxDO::getSequence) - .last("LIMIT " + size)); - } - -} \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImMessageMapper.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImMessageMapper.java deleted file mode 100755 index efd50f6..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImMessageMapper.java +++ /dev/null @@ -1,25 +0,0 @@ -package cn.iocoder.yudao.module.im.dal.mysql.message; - -import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; -import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; -import cn.iocoder.yudao.module.im.dal.dataobject.message.ImMessageDO; -import org.apache.ibatis.annotations.Mapper; - -import java.util.List; - -// TODO @hao:IM 前缀 -/** - * 消息 Mapper - * - * @author 芋道源码 - */ -@Mapper -public interface ImMessageMapper extends BaseMapperX { - - default List selectMessageList(ImMessageDO message) { - return selectList(new LambdaQueryWrapperX() - .eqIfPresent(ImMessageDO::getConversationNo, message.getConversationNo()) - .eqIfPresent(ImMessageDO::getSendTime, message.getSendTime()) - .orderByAsc(ImMessageDO::getSendTime)); - } -} \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/groupmember/ImGroupMemberService.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/groupmember/ImGroupMemberService.java deleted file mode 100644 index 6d75136..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/groupmember/ImGroupMemberService.java +++ /dev/null @@ -1,80 +0,0 @@ -package cn.iocoder.yudao.module.im.service.groupmember; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.im.controller.admin.groupmember.vo.ImGroupMemberPageReqVO; -import cn.iocoder.yudao.module.im.controller.admin.groupmember.vo.ImGroupMemberSaveReqVO; -import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; -import jakarta.validation.Valid; - -import java.util.List; - -/** - * 群成员 Service 接口 - * - * @author 芋道源码 - */ -public interface ImGroupMemberService { - - /** - * 创建群成员 - * - * @param createReqVO 创建信息 - * @return 编号 - */ - Long createGroupMember(@Valid ImGroupMemberSaveReqVO createReqVO); - - /** - * 更新群成员 - * - * @param updateReqVO 更新信息 - */ - void updateGroupMember(@Valid ImGroupMemberSaveReqVO updateReqVO); - - /** - * 删除群成员 - * - * @param id 编号 - */ - void deleteGroupMember(Long id); - - /** - * 获得群成员 - * - * @param id 编号 - * @return 群成员 - */ - ImGroupMemberDO getGroupMember(Long id); - - /** - * 获得群成员分页 - * - * @param pageReqVO 分页查询 - * @return 群成员分页 - */ - PageResult getGroupMemberPage(ImGroupMemberPageReqVO pageReqVO); - - /** - * 根据群组id查询群成员 - * - * @param groupId 群组id - * @return 群成员列表 - */ - List selectByGroupId(Long groupId); - - /** - * 根据群编号和用户编号查询群成员 - * - * @param groupId 群编号 - * @param userId 用户编号 - * @return 群成员 - */ - ImGroupMemberDO getGroupMember(Long groupId, Long userId); - - /** - * 查询用户所在的所有群的成员记录 - * - * @param userId 用户编号 - * @return 群成员记录列表 - */ - List getGroupMembersByUserId(Long userId); -} \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/groupmember/ImGroupMemberServiceImpl.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/groupmember/ImGroupMemberServiceImpl.java deleted file mode 100644 index 1c81819..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/groupmember/ImGroupMemberServiceImpl.java +++ /dev/null @@ -1,88 +0,0 @@ -package cn.iocoder.yudao.module.im.service.groupmember; - -import org.springframework.stereotype.Service; -import jakarta.annotation.Resource; -import org.springframework.validation.annotation.Validated; - -import cn.iocoder.yudao.module.im.controller.admin.groupmember.vo.*; -import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; - -import cn.iocoder.yudao.module.im.dal.mysql.groupmember.ImGroupMemberMapper; - -import java.util.List; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; - -/** - * 群成员 Service 实现类 - * - * @author 芋道源码 - */ -@Service -@Validated -public class ImGroupMemberServiceImpl implements ImGroupMemberService { - - @Resource - private ImGroupMemberMapper imGroupMemberMapper; - - @Override - public Long createGroupMember(ImGroupMemberSaveReqVO createReqVO) { - // 插入 - ImGroupMemberDO groupMember = BeanUtils.toBean(createReqVO, ImGroupMemberDO.class); - imGroupMemberMapper.insert(groupMember); - // 返回 - return groupMember.getId(); - } - - @Override - public void updateGroupMember(ImGroupMemberSaveReqVO updateReqVO) { - // 校验存在 - validateGroupMemberExists(updateReqVO.getId()); - // 更新 - ImGroupMemberDO updateObj = BeanUtils.toBean(updateReqVO, ImGroupMemberDO.class); - imGroupMemberMapper.updateById(updateObj); - } - - @Override - public void deleteGroupMember(Long id) { - // 校验存在 - validateGroupMemberExists(id); - // 删除 - imGroupMemberMapper.deleteById(id); - } - - private void validateGroupMemberExists(Long id) { - if (imGroupMemberMapper.selectById(id) == null) { - throw exception(GROUP_MEMBER_NOT_EXISTS); - } - } - - @Override - public ImGroupMemberDO getGroupMember(Long id) { - return imGroupMemberMapper.selectById(id); - } - - @Override - public PageResult getGroupMemberPage(ImGroupMemberPageReqVO pageReqVO) { - return imGroupMemberMapper.selectPage(pageReqVO); - } - - @Override - public List selectByGroupId(Long groupId) { - return imGroupMemberMapper.selectListByGroupId(groupId); - } - - @Override - public ImGroupMemberDO getGroupMember(Long groupId, Long userId) { - return imGroupMemberMapper.selectByGroupIdAndUserId(groupId, userId); - } - - @Override - public List getGroupMembersByUserId(Long userId) { - return imGroupMemberMapper.selectListByUserId(userId); - } - -} \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/inbox/ImInboxService.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/inbox/ImInboxService.java deleted file mode 100755 index 10cb04b..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/inbox/ImInboxService.java +++ /dev/null @@ -1,31 +0,0 @@ -package cn.iocoder.yudao.module.im.service.inbox; - -import cn.iocoder.yudao.module.im.dal.dataobject.message.ImMessageDO; - -import java.util.List; - -/** - * IM 收件箱 Service 接口 - * - * @author 芋道源码 - */ -public interface ImInboxService { - - /** - * 保存收件箱和发送消息 - * - * @param imMessageDO 收件箱保存消息 Request VO - */ - void saveInboxAndSendMessage(ImMessageDO imMessageDO); - - /** - * 获得大于 sequence 的消息 ids - * - * @param userId 用户编号 - * @param sequence 序列号 - * @param size 数量 - * @return 消息编号列表 - */ - List selectMessageIdsByUserIdAndSequence(Long userId, Long sequence, Integer size); - -} \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/inbox/ImInboxServiceImpl.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/inbox/ImInboxServiceImpl.java deleted file mode 100755 index 9f523d0..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/inbox/ImInboxServiceImpl.java +++ /dev/null @@ -1,93 +0,0 @@ -package cn.iocoder.yudao.module.im.service.inbox; - -import cn.hutool.core.date.DateUnit; -import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.im.controller.admin.message.vo.ImMessageRespVO; -import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; -import cn.iocoder.yudao.module.im.dal.dataobject.inbox.ImInboxDO; -import cn.iocoder.yudao.module.im.dal.dataobject.message.ImMessageDO; -import cn.iocoder.yudao.module.im.dal.mysql.inbox.ImInboxMapper; -import cn.iocoder.yudao.module.im.dal.redis.inbox.InboxLockRedisDAO; -import cn.iocoder.yudao.module.im.dal.redis.inbox.SequenceRedisDAO; -import cn.iocoder.yudao.module.im.enums.conversation.ImConversationTypeEnum; -import cn.iocoder.yudao.module.im.service.groupmember.ImGroupMemberService; -import cn.iocoder.yudao.module.infra.api.websocket.WebSocketSenderApi; -import jakarta.annotation.Resource; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; -import org.springframework.validation.annotation.Validated; - -import java.util.List; - -/** - * 收件箱 Service 实现类 - * - * @author 芋道源码 - */ -@Service -@Validated -public class ImInboxServiceImpl implements ImInboxService { - - private static final Long INBOX_LOCK_TIMEOUT = 120 * DateUnit.SECOND.getMillis(); - private static final String IM_MESSAGE_RECEIVE = "im-message-receive"; - - @Resource - private ImInboxMapper imInboxMapper; - - @Resource - private SequenceRedisDAO sequenceRedisDAO; - @Resource - private InboxLockRedisDAO inboxLockRedisDAO; - - @Resource - private WebSocketSenderApi webSocketSenderApi; - @Resource - private ImGroupMemberService imGroupMemberService; - - @Override - public void saveInboxAndSendMessage(ImMessageDO message) { - // 1. 保存收件箱 + 发送消息给发送人 - saveInboxAndSendMessageForUser(message.getSenderId(), message); - // 2. 保存收件箱 + 发送消息给接收人 - if (message.getConversationType().equals(ImConversationTypeEnum.SINGLE.getType())) { - // 2.1 如果是单聊,直接发送给接收人 - saveInboxAndSendMessageForUser(message.getReceiverId(), message); - } else if (message.getConversationType().equals(ImConversationTypeEnum.GROUP.getType())) { - // 2.2 如果是群聊,发送给群聊的所有人 - List groupMembers = imGroupMemberService.selectByGroupId(message.getReceiverId()); - groupMembers.forEach(groupMemberDO -> saveInboxAndSendMessageForUser(groupMemberDO.getUserId(), message)); - } - } - - @Override - public List selectMessageIdsByUserIdAndSequence(Long userId, Long sequence, Integer size) { - List imInboxDOS = imInboxMapper.selectListByUserIdAndSequence(userId, sequence, size); - return imInboxDOS.stream().map(ImInboxDO::getMessageId).toList(); - } - - - //TODO 多线程处理 - public void saveInboxAndSendMessageForUser(Long userId, ImMessageDO message) { - inboxLockRedisDAO.lock(userId, INBOX_LOCK_TIMEOUT, () -> { - // 1. 生成序列号 - Long userSequence = sequenceRedisDAO.generateSequence(userId); - // 2. 保存收件箱 - ImInboxDO inbox = new ImInboxDO() - .setUserId(userId) - .setMessageId(message.getId()) - .setSequence(userSequence); - imInboxMapper.insert(inbox); - // 3. 发送消息 - sendAsyncMessage(userId, message, userSequence); - }); - } - - @Async - public void sendAsyncMessage(Long userId, ImMessageDO message, Long userSequence) { - ImMessageRespVO messageRespVO = BeanUtils.toBean(message, ImMessageRespVO.class); - messageRespVO.setSequence(userSequence); - webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), userId, IM_MESSAGE_RECEIVE, messageRespVO); - } - -} \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImMessageService.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImMessageService.java deleted file mode 100755 index 26d35c5..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImMessageService.java +++ /dev/null @@ -1,51 +0,0 @@ -package cn.iocoder.yudao.module.im.service.message; - -import cn.iocoder.yudao.module.im.controller.admin.message.vo.ImMessageListByNoReqVO; -import cn.iocoder.yudao.module.im.controller.admin.message.vo.ImMessageListReqVO; -import cn.iocoder.yudao.module.im.controller.admin.message.vo.ImMessageSendReqVO; -import cn.iocoder.yudao.module.im.dal.dataobject.message.ImMessageDO; - -import java.util.List; - -/** - * 消息 Service 接口 - * - * @author 芋道源码 - */ -public interface ImMessageService { - - /** - * 获得历史消息 - * - * @param listReqVO 分页查询 - * @return 消息分页 - */ - List getMessageList(ImMessageListReqVO listReqVO, Long loginUserId); - - /** - * 获得历史消息 - * - * @param listReqVO 分页查询 - * @return 消息分页 - */ - List getMessageListByConversationNo(ImMessageListByNoReqVO listReqVO); - - /** - * 拉取消息-大于 seq 的消息 - * - * @param loginUserId 登录用户编号 - * @param sequence 序列号 - * @param size 数量 - * @return 消息列表 - */ - List pullMessageList(Long loginUserId, Long sequence, Integer size); - - /** - * 发送消息 - * - * @param loginUserId 登录用户编号 - * @param message 消息 - * @return 消息编号 - */ - ImMessageDO sendMessage(Long loginUserId, ImMessageSendReqVO message); -} \ No newline at end of file diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImMessageServiceImpl.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImMessageServiceImpl.java deleted file mode 100755 index 341d9f3..0000000 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImMessageServiceImpl.java +++ /dev/null @@ -1,130 +0,0 @@ -package cn.iocoder.yudao.module.im.service.message; - -import cn.hutool.core.bean.BeanUtil; -import cn.hutool.core.collection.CollUtil; -import cn.iocoder.yudao.module.im.controller.admin.message.vo.ImMessageListByNoReqVO; -import cn.iocoder.yudao.module.im.controller.admin.message.vo.ImMessageListReqVO; -import cn.iocoder.yudao.module.im.controller.admin.message.vo.ImMessageSendReqVO; -import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; -import cn.iocoder.yudao.module.im.dal.dataobject.message.ImMessageDO; -import cn.iocoder.yudao.module.im.dal.mysql.message.ImMessageMapper; -import cn.iocoder.yudao.module.im.enums.conversation.ImConversationTypeEnum; -import cn.iocoder.yudao.module.im.enums.message.ImMessageSourceEnum; -import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; -import cn.iocoder.yudao.module.im.service.groupmember.ImGroupMemberService; -import cn.iocoder.yudao.module.im.service.inbox.ImInboxService; -import cn.iocoder.yudao.module.system.api.user.AdminUserApi; -import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.validation.annotation.Validated; - -import java.time.LocalDateTime; -import java.util.Collections; -import java.util.List; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; -import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_RECEIVER_NOT_EXISTS; -import static cn.iocoder.yudao.module.im.enums.conversation.ImConversationTypeEnum.generateConversationNo; - -/** - * 消息 Service 实现类 - * - * @author 芋道源码 - */ -@Service -@Validated -@Slf4j -public class ImMessageServiceImpl implements ImMessageService { - - @Resource - private ImMessageMapper imMessageMapper; - @Resource - private AdminUserApi adminUserApi; - @Resource - private ImInboxService imInboxService; - @Resource - private ImGroupMemberService imGroupMemberService; - - @Override - public List getMessageList(ImMessageListReqVO listReqVO, Long loginUserId) { - - String no = generateConversationNo(loginUserId, listReqVO.getReceiverId(), listReqVO.getConversationType()); - - ImMessageDO message = new ImMessageDO() - .setSendTime(listReqVO.getSendTime()) - .setConversationNo(no); - - return imMessageMapper.selectMessageList(message); - } - - @Override - public List getMessageListByConversationNo(ImMessageListByNoReqVO listReqVO) { - - // 1. 查询历史消息 - ImMessageDO message = new ImMessageDO() - .setSendTime(listReqVO.getSendTime()) - .setConversationNo(listReqVO.getConversationNo()); - return imMessageMapper.selectMessageList(message); - } - - - @Override - public List pullMessageList(Long userId, Long sequence, Integer size) { - List messageIds = imInboxService.selectMessageIdsByUserIdAndSequence(userId, sequence, size); - if (CollUtil.isEmpty(messageIds)) { - return Collections.emptyList(); - } - return imMessageMapper.selectBatchIds(messageIds); - } - - @Override - public ImMessageDO sendMessage(Long fromUserId, ImMessageSendReqVO imMessageSendReqVO) { - // 1. 保存消息 - ImMessageDO message = saveMessage(fromUserId, imMessageSendReqVO); - // 2. 保存到收件箱,并发送消息 - imInboxService.saveInboxAndSendMessage(message); - return message; - } - - private ImMessageDO saveMessage(Long fromUserId, ImMessageSendReqVO message) { - // TODO 芋艿:消息格式的校验 - // 1. 校验接收人是否存在 - validateReceiverIdExists(message); - // 2. 查询发送人信息 - AdminUserRespDTO fromUser = adminUserApi.getUser(fromUserId); - - // 3. 生成conversationNo - String conversationNo = generateConversationNo(fromUserId, message.getReceiverId(), message.getConversationType()); - - // 4. 保存消息 - ImMessageDO imMessageDO = BeanUtil.copyProperties(message, ImMessageDO.class) - .setSenderNickname(fromUser.getNickname()).setSenderAvatar(fromUser.getAvatar()) - .setSenderId(fromUserId) - .setConversationNo(conversationNo) - .setSendFrom(ImMessageSourceEnum.USER_SEND.getSource()) - .setMessageStatus(ImMessageStatusEnum.SENDING.getStatus()) - .setSendTime(LocalDateTime.now()); - imMessageMapper.insert(imMessageDO); - return imMessageDO; - } - - private void validateReceiverIdExists(ImMessageSendReqVO message) { - if (message.getConversationType().equals(ImConversationTypeEnum.SINGLE.getType())) { - // 校验用户是否存在 - AdminUserRespDTO receiverUser = adminUserApi.getUser(message.getReceiverId()); - if (receiverUser == null) { - throw exception(MESSAGE_RECEIVER_NOT_EXISTS); - } - } else if (message.getConversationType().equals(ImConversationTypeEnum.GROUP.getType())) { - // 校验群聊是否存在 - List imGroupMemberDOS = imGroupMemberService.selectByGroupId(message.getReceiverId()); - if (imGroupMemberDOS.isEmpty()) { - throw exception(MESSAGE_RECEIVER_NOT_EXISTS); - } - } - } - -} \ No newline at end of file