diff --git a/db/im-platform.sql b/db/im-platform.sql index e1ca294..887006b 100644 --- a/db/im-platform.sql +++ b/db/im-platform.sql @@ -91,4 +91,18 @@ create table `im_sensitive_word`( `enabled` tinyint DEFAULT 0 COMMENT '是否启用 0:未启用 1:启用', `creator` bigint DEFAULT NULL COMMENT '创建者', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' -)ENGINE=InnoDB CHARSET=utf8mb4 comment '敏感词'; \ No newline at end of file +)ENGINE=InnoDB CHARSET=utf8mb4 comment '敏感词'; + +CREATE TABLE `im_file_info` ( + `id` BIGINT NOT NULL auto_increment PRIMARY key comment 'id', + `file_name` VARCHAR(255) NOT NULL comment '文件名', + `file_path` VARCHAR(255) NOT NULL comment '文件地址', + `file_size` INTEGER NOT NULL comment '文件大小', + `file_type` tinyint NOT NULL comment '0:普通文件 1:图片 2:视频', + `compressed_path` VARCHAR(255) DEFAULT NULL comment '压缩文件路径', + `cover_path` VARCHAR(255) DEFAULT NULL comment '封面文件路径,仅视频文件有效', + `upload_time` datetime DEFAULT CURRENT_TIMESTAMP comment '上传时间', + `is_permanent` tinyint DEFAULT 0 comment '是否永久文件', + `md5` VARCHAR(64) NOT NULL comment '文件md5', + UNIQUE KEY `idx_md5` (md5), +) ENGINE = InnoDB CHARSET = utf8mb4 comment '文件'; \ No newline at end of file diff --git a/im-platform/src/main/java/com/bx/implatform/annotation/RedisLock.java b/im-platform/src/main/java/com/bx/implatform/annotation/RedisLock.java index 0421931..fca9a98 100644 --- a/im-platform/src/main/java/com/bx/implatform/annotation/RedisLock.java +++ b/im-platform/src/main/java/com/bx/implatform/annotation/RedisLock.java @@ -21,7 +21,7 @@ public @interface RedisLock { /** * spel 表达式 */ - String key(); + String key() default ""; /** * 等待锁的时间,默认-1,不等待直接失败,redisson默认也是-1 diff --git a/im-platform/src/main/java/com/bx/implatform/aspect/RedisLockAspect.java b/im-platform/src/main/java/com/bx/implatform/aspect/RedisLockAspect.java index dc5c6b5..2c0d26a 100644 --- a/im-platform/src/main/java/com/bx/implatform/aspect/RedisLockAspect.java +++ b/im-platform/src/main/java/com/bx/implatform/aspect/RedisLockAspect.java @@ -4,6 +4,7 @@ import cn.hutool.core.util.StrUtil; import com.bx.implatform.annotation.RedisLock; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.logging.log4j.util.Strings; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -61,18 +62,22 @@ public class RedisLockAspect { private String parseKey(ProceedingJoinPoint joinPoint){ Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); RedisLock annotation = method.getAnnotation(RedisLock.class); + String key = annotation.key(); + if(StrUtil.isEmpty(key)){ + return Strings.EMPTY; + } // el解析需要的上下文对象 EvaluationContext context = new StandardEvaluationContext(); // 参数名 String[] params = parameterNameDiscoverer.getParameterNames(method); if(Objects.isNull(params)){ - return annotation.key(); + return key; } Object[] args = joinPoint.getArgs(); for (int i = 0; i < params.length; i++) { context.setVariable(params[i], args[i]);//所有参数都作为原材料扔进去 } - Expression expression = parser.parseExpression(annotation.key()); + Expression expression = parser.parseExpression(key); return expression.getValue(context, String.class); } diff --git a/im-platform/src/main/java/com/bx/implatform/config/props/MinioProperties.java b/im-platform/src/main/java/com/bx/implatform/config/props/MinioProperties.java index 2bfbf7f..5a64db3 100644 --- a/im-platform/src/main/java/com/bx/implatform/config/props/MinioProperties.java +++ b/im-platform/src/main/java/com/bx/implatform/config/props/MinioProperties.java @@ -29,4 +29,6 @@ public class MinioProperties { private String filePath; private String videoPath; + + private Integer expireIn; } diff --git a/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java b/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java index 3162ea4..7793598 100644 --- a/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java +++ b/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java @@ -53,4 +53,9 @@ public final class RedisKey { */ public static final String IM_REPEAT_SUBMIT = "im:repeat:submit"; + /** + * 分布式锁-清理过期文件 + */ + public static final String IM_LOCK_FILE_TASK = "im:lock:task:file"; + } diff --git a/im-platform/src/main/java/com/bx/implatform/controller/FileController.java b/im-platform/src/main/java/com/bx/implatform/controller/FileController.java index ddf9236..7f8295e 100644 --- a/im-platform/src/main/java/com/bx/implatform/controller/FileController.java +++ b/im-platform/src/main/java/com/bx/implatform/controller/FileController.java @@ -2,7 +2,7 @@ package com.bx.implatform.controller; import com.bx.implatform.result.Result; import com.bx.implatform.result.ResultUtils; -import com.bx.implatform.service.thirdparty.FileService; +import com.bx.implatform.service.FileService; import com.bx.implatform.vo.UploadImageVO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -24,15 +24,16 @@ public class FileController { @Operation(summary = "上传图片", description = "上传图片,上传后返回原图和缩略图的url") @PostMapping("/image/upload") - public Result uploadImage(@RequestParam("file") MultipartFile file) { - return ResultUtils.success(fileService.uploadImage(file)); + public Result uploadImage(@RequestParam("file") MultipartFile file, + @RequestParam(defaultValue = "true") Boolean isPermanent) { + return ResultUtils.success(fileService.uploadImage(file,isPermanent)); } @CrossOrigin @Operation(summary = "上传文件", description = "上传文件,上传后返回文件url") @PostMapping("/file/upload") public Result uploadFile(@RequestParam("file") MultipartFile file) { - return ResultUtils.success(fileService.uploadFile(file), ""); + return ResultUtils.success(fileService.uploadFile(file)); } } \ No newline at end of file diff --git a/im-platform/src/main/java/com/bx/implatform/controller/GroupController.java b/im-platform/src/main/java/com/bx/implatform/controller/GroupController.java index 98d3dc8..a3c84c5 100644 --- a/im-platform/src/main/java/com/bx/implatform/controller/GroupController.java +++ b/im-platform/src/main/java/com/bx/implatform/controller/GroupController.java @@ -1,10 +1,11 @@ package com.bx.implatform.controller; import com.bx.implatform.annotation.RepeatSubmit; +import com.bx.implatform.dto.GroupMemberRemoveDTO; import com.bx.implatform.result.Result; import com.bx.implatform.result.ResultUtils; import com.bx.implatform.service.GroupService; -import com.bx.implatform.vo.GroupInviteVO; +import com.bx.implatform.dto.GroupInviteDTO; import com.bx.implatform.vo.GroupMemberVO; import com.bx.implatform.vo.GroupVO; import io.swagger.v3.oas.annotations.Operation; @@ -62,8 +63,8 @@ public class GroupController { @RepeatSubmit @Operation(summary = "邀请进群", description = "邀请好友进群") @PostMapping("/invite") - public Result invite(@Valid @RequestBody GroupInviteVO vo) { - groupService.invite(vo); + public Result invite(@Valid @RequestBody GroupInviteDTO dto) { + groupService.invite(dto); return ResultUtils.success(); } @@ -74,6 +75,15 @@ public class GroupController { return ResultUtils.success(groupService.findGroupMembers(groupId)); } + @RepeatSubmit + @Operation(summary = "将成员移出群聊", description = "将成员移出群聊") + @DeleteMapping("/members/remove") + public Result removeMembers(@Valid @RequestBody GroupMemberRemoveDTO dto) { + groupService.removeGroupMembers(dto); + return ResultUtils.success(); + } + + @RepeatSubmit @Operation(summary = "退出群聊", description = "退出群聊") @DeleteMapping("/quit/{groupId}") @@ -83,7 +93,7 @@ public class GroupController { } @RepeatSubmit - @Operation(summary = "踢出群聊", description = "将用户踢出群聊") + @Operation(summary = "踢出群聊(已废弃)", description = "将用户踢出群聊") @DeleteMapping("/kick/{groupId}") public Result kickGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId, @NotNull(message = "用户id不能为空") @RequestParam Long userId) { diff --git a/im-platform/src/main/java/com/bx/implatform/vo/GroupInviteVO.java b/im-platform/src/main/java/com/bx/implatform/dto/GroupInviteDTO.java similarity index 82% rename from im-platform/src/main/java/com/bx/implatform/vo/GroupInviteVO.java rename to im-platform/src/main/java/com/bx/implatform/dto/GroupInviteDTO.java index b78f43f..efeb5fb 100644 --- a/im-platform/src/main/java/com/bx/implatform/vo/GroupInviteVO.java +++ b/im-platform/src/main/java/com/bx/implatform/dto/GroupInviteDTO.java @@ -1,4 +1,4 @@ -package com.bx.implatform.vo; +package com.bx.implatform.dto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; @@ -9,8 +9,8 @@ import lombok.Data; import java.util.List; @Data -@Schema(description = "邀请好友进群请求VO") -public class GroupInviteVO { +@Schema(description = "邀请好友进群请求DTO") +public class GroupInviteDTO { @NotNull(message = "群id不可为空") @Schema(description = "群id") diff --git a/im-platform/src/main/java/com/bx/implatform/dto/GroupMemberRemoveDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/GroupMemberRemoveDTO.java new file mode 100644 index 0000000..5d98e71 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/GroupMemberRemoveDTO.java @@ -0,0 +1,28 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +/** + * @author Blue + * @version 1.0 + * @date 2025-02-23 + */ +@Data +@Schema(description = "移除群聊成员") +public class GroupMemberRemoveDTO { + + @NotNull(message = "群id不可为空") + @Schema(description = "群组id") + private Long groupId; + + @Size(max = 50, message = "一次最多只能选择50位用户") + @NotEmpty(message = "成员用户id不可为空") + @Schema(description = "成员用户id") + private List userIds; +} diff --git a/im-platform/src/main/java/com/bx/implatform/entity/FileInfo.java b/im-platform/src/main/java/com/bx/implatform/entity/FileInfo.java new file mode 100644 index 0000000..9a847f5 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/entity/FileInfo.java @@ -0,0 +1,67 @@ +package com.bx.implatform.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.Date; + +/** + * @author Blue + * @version 1.0 + */ +@Data +@TableName("im_file_info") +public class FileInfo { + + /** + * 文件ID + */ + @TableId + private Long id; + + /** + * 文件名 + */ + private String fileName; + + /** + * 原始文件存储路径 + */ + private String filePath; + + /** + * 压缩文件存储路径 + */ + private String compressedPath; + + /** + * 封面文件路径 + */ + private String coverPath; + + /** + * 原始文件大小(字节) + */ + private Long fileSize; + + /** + * 上传时间 + */ + private Date uploadTime; + + /** + * 文件类型,枚举: FileType + */ + private Integer fileType; + + /** + * 是否永久存储 + */ + private Boolean isPermanent; + + /** + * 文件MD5哈希值 + */ + private String md5; +} diff --git a/im-platform/src/main/java/com/bx/implatform/enums/FileType.java b/im-platform/src/main/java/com/bx/implatform/enums/FileType.java index f7aea02..2237f48 100644 --- a/im-platform/src/main/java/com/bx/implatform/enums/FileType.java +++ b/im-platform/src/main/java/com/bx/implatform/enums/FileType.java @@ -16,11 +16,7 @@ public enum FileType { /** * 视频 */ - VIDEO(2, "视频"), - /** - * 声音 - */ - AUDIO(3, "声音"); + VIDEO(2, "视频"); private final Integer code; diff --git a/im-platform/src/main/java/com/bx/implatform/mapper/FileInfoMapper.java b/im-platform/src/main/java/com/bx/implatform/mapper/FileInfoMapper.java new file mode 100644 index 0000000..37ebf8c --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/mapper/FileInfoMapper.java @@ -0,0 +1,8 @@ +package com.bx.implatform.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bx.implatform.entity.FileInfo; + +public interface FileInfoMapper extends BaseMapper { + +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/FileService.java b/im-platform/src/main/java/com/bx/implatform/service/FileService.java new file mode 100644 index 0000000..bc32e19 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/FileService.java @@ -0,0 +1,15 @@ +package com.bx.implatform.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bx.implatform.entity.FileInfo; +import com.bx.implatform.vo.UploadImageVO; +import org.springframework.web.multipart.MultipartFile; + +public interface FileService extends IService { + + String uploadFile(MultipartFile file); + + UploadImageVO uploadImage(MultipartFile file,Boolean isPermanent); + + +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/FileServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/FileServiceImpl.java new file mode 100644 index 0000000..44b51e8 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/FileServiceImpl.java @@ -0,0 +1,191 @@ +package com.bx.implatform.service; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bx.implatform.config.props.MinioProperties; +import com.bx.implatform.contant.Constant; +import com.bx.implatform.entity.FileInfo; +import com.bx.implatform.enums.FileType; +import com.bx.implatform.enums.ResultCode; +import com.bx.implatform.exception.GlobalException; +import com.bx.implatform.mapper.FileInfoMapper; +import com.bx.implatform.session.SessionContext; +import com.bx.implatform.thirdparty.MinioService; +import com.bx.implatform.util.FileUtil; +import com.bx.implatform.util.ImageUtil; +import com.bx.implatform.vo.UploadImageVO; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.DigestUtils; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; +import java.util.Date; +import java.util.Objects; + +/** + * 文件上传服务 + * + * author: Blue date: 2024-09-28 version: 1.0 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class FileServiceImpl extends ServiceImpl implements FileService { + + private final MinioService minioSerivce; + + private final MinioProperties minioProps; + + @PostConstruct + public void init() { + if (!minioSerivce.bucketExists(minioProps.getBucketName())) { + // 创建bucket + minioSerivce.makeBucket(minioProps.getBucketName()); + // 公开bucket + minioSerivce.setBucketPublic(minioProps.getBucketName()); + } + } + + @Override + public String uploadFile(MultipartFile file) { + try { + Long userId = SessionContext.getSession().getUserId(); + // 大小校验 + if (file.getSize() > Constant.MAX_FILE_SIZE) { + throw new GlobalException(ResultCode.PROGRAM_ERROR, "文件大小不能超过20M"); + } + // 如果文件已存在,直接复用 + String md5 = DigestUtils.md5DigestAsHex(file.getInputStream()); + FileInfo fileInfo = findByMd5(md5); + if (!Objects.isNull(fileInfo)) { + // 更新上传时间 + fileInfo.setUploadTime(new Date()); + this.updateById(fileInfo); + // 返回 + return fileInfo.getFilePath(); + } + // 上传 + String fileName = minioSerivce.upload(minioProps.getBucketName(), minioProps.getFilePath(), file); + if (StringUtils.isEmpty(fileName)) { + throw new GlobalException(ResultCode.PROGRAM_ERROR, "文件上传失败"); + } + String url = generUrl(FileType.FILE, fileName); + // 保存文件 + saveFileInfo(file, md5, url); + log.info("文件文件成功,用户id:{},url:{}", userId, url); + return url; + } catch (IOException e) { + log.error("上传图片失败,{}", e.getMessage(), e); + throw new GlobalException(ResultCode.PROGRAM_ERROR, "上传图片失败"); + } + } + + @Transactional + @Override + public UploadImageVO uploadImage(MultipartFile file, Boolean isPermanent) { + try { + Long userId = SessionContext.getSession().getUserId(); + // 大小校验 + if (file.getSize() > Constant.MAX_IMAGE_SIZE) { + throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片大小不能超过20M"); + } + // 图片格式校验 + if (!FileUtil.isImage(file.getOriginalFilename())) { + throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片格式不合法"); + } + UploadImageVO vo = new UploadImageVO(); + // 如果文件已存在,直接复用 + String md5 = DigestUtils.md5DigestAsHex(file.getInputStream()); + FileInfo fileInfo = findByMd5(md5); + if (!Objects.isNull(fileInfo)) { + // 更新上传时间和持久化标记 + fileInfo.setIsPermanent(isPermanent || fileInfo.getIsPermanent()); + fileInfo.setUploadTime(new Date()); + this.updateById(fileInfo); + // 返回 + vo.setOriginUrl(fileInfo.getFilePath()); + vo.setThumbUrl(fileInfo.getCompressedPath()); + return vo; + } + // 上传原图 + String fileName = minioSerivce.upload(minioProps.getBucketName(), minioProps.getImagePath(), file); + if (StringUtils.isEmpty(fileName)) { + throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败"); + } + vo.setOriginUrl(generUrl(FileType.IMAGE, fileName)); + if (file.getSize() > 50 * 1024) { + // 大于50K的文件需上传缩略图 + byte[] imageByte = ImageUtil.compressForScale(file.getBytes(), 30); + String thumbFileName = minioSerivce.upload(minioProps.getBucketName(), minioProps.getImagePath(), + file.getOriginalFilename(), imageByte, file.getContentType()); + if (StringUtils.isEmpty(thumbFileName)) { + throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败"); + } + vo.setThumbUrl(generUrl(FileType.IMAGE, thumbFileName)); + // 保存文件信息 + saveImageFileInfo(file, md5, vo.getOriginUrl(), vo.getThumbUrl(), isPermanent); + }else{ + // 小于50k,用原图充当缩略图 + vo.setThumbUrl(generUrl(FileType.IMAGE, fileName)); + // 保存文件信息,由于缩略图不允许删除,此时原图也不允许删除 + saveImageFileInfo(file, md5, vo.getOriginUrl(), vo.getThumbUrl(), true); + } + log.info("文件图片成功,用户id:{},url:{}", userId, vo.getOriginUrl()); + return vo; + } catch (IOException e) { + log.error("上传图片失败,{}", e.getMessage(), e); + throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败"); + } + } + + private String generUrl(FileType fileType, String fileName) { + return StrUtil.join("/", minioProps.getDomain(), minioProps.getBucketName(), getBucketPath(fileType), fileName); + } + + private String getBucketPath(FileType fileType) { + return switch (fileType) { + case FILE -> minioProps.getFilePath(); + case IMAGE -> minioProps.getImagePath(); + case VIDEO -> minioProps.getVideoPath(); + }; + } + + private FileInfo findByMd5(String md5) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FileInfo::getMd5, md5); + return getOne(wrapper); + } + + private void saveImageFileInfo(MultipartFile file, String md5, String filePath, String compressedPath, + Boolean isPermanent) throws IOException { + FileInfo fileInfo = new FileInfo(); + fileInfo.setFileName(file.getOriginalFilename()); + fileInfo.setFileSize(file.getSize()); + fileInfo.setFileType(FileType.IMAGE.code()); + fileInfo.setFilePath(filePath); + fileInfo.setCompressedPath(compressedPath); + fileInfo.setMd5(md5); + fileInfo.setIsPermanent(isPermanent); + fileInfo.setUploadTime(new Date()); + this.save(fileInfo); + } + + private void saveFileInfo(MultipartFile file, String md5, String filePath) throws IOException { + FileInfo fileInfo = new FileInfo(); + fileInfo.setFileName(file.getOriginalFilename()); + fileInfo.setFileSize(file.getSize()); + fileInfo.setFileType(FileType.FILE.code()); + fileInfo.setFilePath(filePath); + fileInfo.setMd5(md5); + fileInfo.setIsPermanent(false); + fileInfo.setUploadTime(new Date()); + this.save(fileInfo); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/GroupMemberService.java b/im-platform/src/main/java/com/bx/implatform/service/GroupMemberService.java index 87ced17..dd0ae42 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/GroupMemberService.java +++ b/im-platform/src/main/java/com/bx/implatform/service/GroupMemberService.java @@ -16,6 +16,7 @@ public interface GroupMemberService extends IService { */ GroupMember findByGroupAndUserId(Long groupId, Long userId); + /** * 根据用户id查询群聊成员 * @@ -74,6 +75,14 @@ public interface GroupMemberService extends IService { */ void removeByGroupAndUserId(Long groupId, Long userId); + /** + * 根据群聊id和用户id移除成员 + * + * @param groupId 群聊id + * @param userIds 用户id + */ + void removeByGroupAndUserIds(Long groupId, List userIds); + /** * 用户用户是否在群中 * diff --git a/im-platform/src/main/java/com/bx/implatform/service/GroupService.java b/im-platform/src/main/java/com/bx/implatform/service/GroupService.java index fe2f6fa..3e0bead 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/GroupService.java +++ b/im-platform/src/main/java/com/bx/implatform/service/GroupService.java @@ -1,8 +1,9 @@ package com.bx.implatform.service; import com.baomidou.mybatisplus.extension.service.IService; +import com.bx.implatform.dto.GroupMemberRemoveDTO; import com.bx.implatform.entity.Group; -import com.bx.implatform.vo.GroupInviteVO; +import com.bx.implatform.dto.GroupInviteDTO; import com.bx.implatform.vo.GroupMemberVO; import com.bx.implatform.vo.GroupVO; @@ -48,6 +49,12 @@ public interface GroupService extends IService { */ void kickGroup(Long groupId, Long userId); + /** + * 将用户移出群聊 + * @param dto dto + */ + void removeGroupMembers(GroupMemberRemoveDTO dto); + /** * 查询当前用户的所有群聊 * @@ -58,9 +65,9 @@ public interface GroupService extends IService { /** * 邀请好友进群 * - * @param vo 群id、好友id列表 + * @param dto 群id、好友id列表 **/ - void invite(GroupInviteVO vo); + void invite(GroupInviteDTO dto); /** * 根据id查找群聊,并进行缓存 diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java index caf4b37..7f6d5f6 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java @@ -37,11 +37,13 @@ public class GroupMemberServiceImpl extends ServiceImpl wrapper = new QueryWrapper<>(); - wrapper.lambda().eq(GroupMember::getGroupId, groupId).eq(GroupMember::getUserId, userId); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(GroupMember::getGroupId, groupId); + wrapper.eq(GroupMember::getUserId, userId); return this.getOne(wrapper); } + @Override public List findByUserId(Long userId) { LambdaQueryWrapper memberWrapper = Wrappers.lambdaQuery(); @@ -88,19 +90,35 @@ public class GroupMemberServiceImpl extends ServiceImpl wrapper = Wrappers.lambdaUpdate(); - wrapper.eq(GroupMember::getGroupId, groupId).eq(GroupMember::getUserId, userId).set(GroupMember::getQuit, true) - .set(GroupMember::getQuitTime, new Date()); + wrapper.eq(GroupMember::getGroupId, groupId); + wrapper.eq(GroupMember::getUserId, userId); + wrapper.set(GroupMember::getQuit, true); + wrapper.set(GroupMember::getQuitTime, new Date()); this.update(wrapper); } + @CacheEvict(key = "#groupId") + @Override + public void removeByGroupAndUserIds(Long groupId, List userId) { + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(GroupMember::getGroupId, groupId); + wrapper.in(GroupMember::getUserId, userId); + wrapper.set(GroupMember::getQuit, true); + wrapper.set(GroupMember::getQuitTime, new Date()); + this.update(wrapper); + } + + @Override public Boolean isInGroup(Long groupId, List userIds) { if (CollectionUtils.isEmpty(userIds)) { return true; } LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); - wrapper.eq(GroupMember::getGroupId, groupId).eq(GroupMember::getQuit, false) - .in(GroupMember::getUserId, userIds); + wrapper.eq(GroupMember::getGroupId, groupId); + wrapper.eq(GroupMember::getQuit, false); + wrapper.in(GroupMember::getUserId, userIds); return userIds.size() == this.count(wrapper); } + } diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java index 809b9ac..fd243c2 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java @@ -12,6 +12,7 @@ import com.bx.imcommon.model.IMUserInfo; import com.bx.imcommon.util.CommaTextUtils; import com.bx.implatform.contant.Constant; import com.bx.implatform.contant.RedisKey; +import com.bx.implatform.dto.GroupMemberRemoveDTO; import com.bx.implatform.entity.*; import com.bx.implatform.enums.MessageStatus; import com.bx.implatform.enums.MessageType; @@ -25,7 +26,7 @@ import com.bx.implatform.service.UserService; import com.bx.implatform.session.SessionContext; import com.bx.implatform.session.UserSession; import com.bx.implatform.util.BeanUtils; -import com.bx.implatform.vo.GroupInviteVO; +import com.bx.implatform.dto.GroupInviteDTO; import com.bx.implatform.vo.GroupMemberVO; import com.bx.implatform.vo.GroupMessageVO; import com.bx.implatform.vo.GroupVO; @@ -126,7 +127,7 @@ public class GroupServiceImpl extends ServiceImpl implements String content = String.format("'%s'解散了群聊", session.getNickName()); this.sendTipMessage(groupId, userIds, content, true); // 推送同步消息 - this.sendDelGroupMessage(groupId, userIds, false); + this.sendDelGroupMessage(groupId, userIds); log.info("删除群聊,群聊id:{},群聊名称:{}", group.getId(), group.getName()); } @@ -145,7 +146,7 @@ public class GroupServiceImpl extends ServiceImpl implements // 推送退出群聊提示 this.sendTipMessage(groupId, List.of(userId), "您已退出群聊", false); // 推送同步消息 - this.sendDelGroupMessage(groupId, Lists.newArrayList(), true); + this.sendDelGroupMessage(groupId, List.of(userId)); log.info("退出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId); } @@ -167,10 +168,35 @@ public class GroupServiceImpl extends ServiceImpl implements // 推送踢出群聊提示 this.sendTipMessage(groupId, List.of(userId), "您已被移出群聊", false); // 推送同步消息 - this.sendDelGroupMessage(groupId, List.of(userId), false); + this.sendDelGroupMessage(groupId, List.of(userId)); log.info("踢出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId); } + @Override + public void removeGroupMembers(GroupMemberRemoveDTO dto) { + UserSession session = SessionContext.getSession(); + Group group = this.getAndCheckById(dto.getGroupId()); + if (!group.getOwnerId().equals(session.getUserId())) { + throw new GlobalException("您没有权限"); + } + if (dto.getUserIds().contains(group.getOwnerId())) { + throw new GlobalException("不允许移除群主"); + } + if (dto.getUserIds().contains(session.getUserId())) { + throw new GlobalException("不允许移除自己"); + } + // 删除群聊成员 + groupMemberService.removeByGroupAndUserIds(dto.getGroupId(), dto.getUserIds()); + // 清理已读缓存 + String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, dto.getGroupId()); + dto.getUserIds().forEach(id -> redisTemplate.opsForHash().delete(key, id.toString())); + // 推送踢出群聊提示 + this.sendTipMessage(dto.getGroupId(), dto.getUserIds(), "您已被移出群聊", false); + // 推送同步消息 + this.sendDelGroupMessage(dto.getGroupId(), dto.getUserIds()); + log.info("踢出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), dto.getUserIds()); + } + @Override public GroupVO findById(Long groupId) { UserSession session = SessionContext.getSession(); @@ -225,22 +251,22 @@ public class GroupServiceImpl extends ServiceImpl implements } @Override - public void invite(GroupInviteVO vo) { + public void invite(GroupInviteDTO dto) { UserSession session = SessionContext.getSession(); - Group group = this.getAndCheckById(vo.getGroupId()); - GroupMember member = groupMemberService.findByGroupAndUserId(vo.getGroupId(), session.getUserId()); + Group group = this.getAndCheckById(dto.getGroupId()); + GroupMember member = groupMemberService.findByGroupAndUserId(dto.getGroupId(), session.getUserId()); if (Objects.isNull(group) || member.getQuit()) { throw new GlobalException("您不在群聊中,邀请失败"); } // 群聊人数校验 - List members = groupMemberService.findByGroupId(vo.getGroupId()); + List members = groupMemberService.findByGroupId(dto.getGroupId()); long size = members.stream().filter(m -> !m.getQuit()).count(); - if (vo.getFriendIds().size() + size > Constant.MAX_LARGE_GROUP_MEMBER) { + if (dto.getFriendIds().size() + size > Constant.MAX_LARGE_GROUP_MEMBER) { throw new GlobalException("群聊人数不能大于" + Constant.MAX_LARGE_GROUP_MEMBER + "人"); } // 找出好友信息 - List friends = friendsService.findByFriendIds(vo.getFriendIds()); - if (vo.getFriendIds().size() != friends.size()) { + List friends = friendsService.findByFriendIds(dto.getFriendIds()); + if (dto.getFriendIds().size() != friends.size()) { throw new GlobalException("部分用户不是您的好友,邀请失败"); } // 批量保存成员数据 @@ -248,7 +274,7 @@ public class GroupServiceImpl extends ServiceImpl implements Optional optional = members.stream().filter(m -> m.getUserId().equals(f.getFriendId())).findFirst(); GroupMember groupMember = optional.orElseGet(GroupMember::new); - groupMember.setGroupId(vo.getGroupId()); + groupMember.setGroupId(dto.getGroupId()); groupMember.setUserId(f.getFriendId()); groupMember.setUserNickName(f.getFriendNickName()); groupMember.setHeadImage(f.getFriendHeadImage()); @@ -265,12 +291,12 @@ public class GroupServiceImpl extends ServiceImpl implements sendAddGroupMessage(groupVo, List.of(m.getUserId()), false); } // 推送进入群聊消息 - List userIds = groupMemberService.findUserIdsByGroupId(vo.getGroupId()); + List userIds = groupMemberService.findUserIdsByGroupId(dto.getGroupId()); String memberNames = groupMembers.stream().map(GroupMember::getShowNickName).collect(Collectors.joining(",")); String content = String.format("'%s'邀请'%s'加入了群聊", session.getNickName(), memberNames); - this.sendTipMessage(vo.getGroupId(), userIds, content, true); + this.sendTipMessage(dto.getGroupId(), userIds, content, true); log.info("邀请进入群聊,群聊id:{},群聊名称:{},被邀请用户id:{}", group.getId(), group.getName(), - vo.getFriendIds()); + dto.getFriendIds()); } @Override @@ -345,7 +371,7 @@ public class GroupServiceImpl extends ServiceImpl implements imClient.sendGroupMessage(sendMessage); } - private void sendDelGroupMessage(Long groupId, List recvIds, Boolean sendToSelf) { + private void sendDelGroupMessage(Long groupId, List recvIds) { UserSession session = SessionContext.getSession(); GroupMessageVO msgInfo = new GroupMessageVO(); msgInfo.setType(MessageType.GROUP_DEL.code()); @@ -357,7 +383,7 @@ public class GroupServiceImpl extends ServiceImpl implements sendMessage.setRecvIds(recvIds); sendMessage.setData(msgInfo); sendMessage.setSendResult(false); - sendMessage.setSendToSelf(sendToSelf); + sendMessage.setSendToSelf(false); imClient.sendGroupMessage(sendMessage); } } diff --git a/im-platform/src/main/java/com/bx/implatform/service/thirdparty/FileService.java b/im-platform/src/main/java/com/bx/implatform/service/thirdparty/FileService.java deleted file mode 100644 index 189bfbc..0000000 --- a/im-platform/src/main/java/com/bx/implatform/service/thirdparty/FileService.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.bx.implatform.service.thirdparty; - -import com.bx.implatform.config.props.MinioProperties; -import com.bx.implatform.contant.Constant; -import com.bx.implatform.enums.FileType; -import com.bx.implatform.enums.ResultCode; -import com.bx.implatform.exception.GlobalException; -import com.bx.implatform.session.SessionContext; -import com.bx.implatform.util.FileUtil; -import com.bx.implatform.util.ImageUtil; -import com.bx.implatform.util.MinioUtil; -import com.bx.implatform.vo.UploadImageVO; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.util.Objects; - -/** - * todo 通过校验文件MD5实现重复文件秒传 - * 文件上传服务 - * - * @author Blue - * @date 2022/10/28 - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class FileService { - private final MinioUtil minioUtil; - - private final MinioProperties minioProps; - - - @PostConstruct - public void init() { - if (!minioUtil.bucketExists(minioProps.getBucketName())) { - // 创建bucket - minioUtil.makeBucket(minioProps.getBucketName()); - // 公开bucket - minioUtil.setBucketPublic(minioProps.getBucketName()); - } - } - - - public String uploadFile(MultipartFile file) { - Long userId = SessionContext.getSession().getUserId(); - // 大小校验 - if (file.getSize() > Constant.MAX_FILE_SIZE) { - throw new GlobalException(ResultCode.PROGRAM_ERROR, "文件大小不能超过20M"); - } - // 上传 - String fileName = minioUtil.upload(minioProps.getBucketName(), minioProps.getFilePath(), file); - if (StringUtils.isEmpty(fileName)) { - throw new GlobalException(ResultCode.PROGRAM_ERROR, "文件上传失败"); - } - String url = generUrl(FileType.FILE, fileName); - log.info("文件文件成功,用户id:{},url:{}", userId, url); - return url; - } - - public UploadImageVO uploadImage(MultipartFile file) { - try { - Long userId = SessionContext.getSession().getUserId(); - // 大小校验 - if (file.getSize() > Constant.MAX_IMAGE_SIZE) { - throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片大小不能超过20M"); - } - // 图片格式校验 - if (!FileUtil.isImage(file.getOriginalFilename())) { - throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片格式不合法"); - } - // 上传原图 - UploadImageVO vo = new UploadImageVO(); - String fileName = minioUtil.upload(minioProps.getBucketName(), minioProps.getImagePath(), file); - if (StringUtils.isEmpty(fileName)) { - throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败"); - } - vo.setOriginUrl(generUrl(FileType.IMAGE, fileName)); - // 大于30K的文件需上传缩略图 - if (file.getSize() > 30 * 1024) { - byte[] imageByte = ImageUtil.compressForScale(file.getBytes(), 30); - fileName = minioUtil.upload(minioProps.getBucketName(), minioProps.getImagePath(), Objects.requireNonNull(file.getOriginalFilename()), imageByte, file.getContentType()); - if (StringUtils.isEmpty(fileName)) { - throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败"); - } - } - vo.setThumbUrl(generUrl(FileType.IMAGE, fileName)); - log.info("文件图片成功,用户id:{},url:{}", userId, vo.getOriginUrl()); - return vo; - } catch (IOException e) { - log.error("上传图片失败,{}", e.getMessage(), e); - throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败"); - } - } - - - public String generUrl(FileType fileTypeEnum, String fileName) { - String url = minioProps.getDomain() + "/" + minioProps.getBucketName(); - switch (fileTypeEnum) { - case FILE: - url += "/" + minioProps.getFilePath() + "/"; - break; - case IMAGE: - url += "/" + minioProps.getImagePath() + "/"; - break; - case VIDEO: - url += "/" + minioProps.getVideoPath() + "/"; - break; - default: - break; - } - url += fileName; - return url; - } - -} diff --git a/im-platform/src/main/java/com/bx/implatform/task/schedule/FileExpireTask.java b/im-platform/src/main/java/com/bx/implatform/task/schedule/FileExpireTask.java new file mode 100644 index 0000000..9dddcb0 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/task/schedule/FileExpireTask.java @@ -0,0 +1,78 @@ +package com.bx.implatform.task.schedule; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.bx.implatform.annotation.RedisLock; +import com.bx.implatform.config.props.MinioProperties; +import com.bx.implatform.contant.RedisKey; +import com.bx.implatform.entity.FileInfo; +import com.bx.implatform.service.FileService; +import com.bx.implatform.thirdparty.MinioService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.time.DateUtils; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.List; + +/** + * 过期文件清理任务 + * + * @author Blue + * @version 1.0 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class FileExpireTask { + + private final FileService fileService; + private final MinioService minioService; + private final MinioProperties minioProps; + + @RedisLock(prefixKey = RedisKey.IM_LOCK_FILE_TASK) + @Scheduled(cron = "0 * * * * ?") + public void run() { + log.info("【定时任务】过期文件处理..."); + int batchSize = 100; + List files = loadBatch(batchSize); + while (true) { + for (FileInfo fileInfo : files) { + String url = fileInfo.getFilePath(); + String relativePath = url.substring(fileInfo.getFilePath().indexOf(minioProps.getBucketName())); + String[] arr = relativePath.split("/"); + String bucket = minioProps.getBucketName(); + String path = arr[1]; + String fileNme = StrUtil.join("/", arr[2], arr[3]); + if (minioService.isExist(bucket, path, fileNme)) { + if (!minioService.remove(bucket, path, fileNme)) { + // 删除失败,不再往下执行 + log.error("删除过期文件异常, id:{},文件名:{}", fileInfo.getId(), fileInfo.getFileName()); + return; + } + // 删除文件信息 + fileService.removeById(fileInfo.getId()); + } + } + if (files.size() < batchSize) { + break; + } + // 下一批 + files = loadBatch(batchSize); + } + } + + List loadBatch(int size) { + Date minDate = DateUtils.addDays(new Date(), -minioProps.getExpireIn()); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FileInfo::getIsPermanent, false); + wrapper.le(FileInfo::getUploadTime, minDate); + wrapper.orderByAsc(FileInfo::getId); + wrapper.last("limit " + size); + return fileService.list(wrapper); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/util/MinioUtil.java b/im-platform/src/main/java/com/bx/implatform/thirdparty/MinioService.java similarity index 88% rename from im-platform/src/main/java/com/bx/implatform/util/MinioUtil.java rename to im-platform/src/main/java/com/bx/implatform/thirdparty/MinioService.java index bd0da21..254a8e2 100644 --- a/im-platform/src/main/java/com/bx/implatform/util/MinioUtil.java +++ b/im-platform/src/main/java/com/bx/implatform/thirdparty/MinioService.java @@ -1,5 +1,6 @@ -package com.bx.implatform.util; +package com.bx.implatform.thirdparty; +import com.bx.implatform.util.DateTimeUtils; import io.minio.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -14,7 +15,7 @@ import java.util.Date; @Slf4j @Component @RequiredArgsConstructor -public class MinioUtil { +public class MinioService { private final MinioClient minioClient; @@ -137,11 +138,29 @@ public class MinioUtil { */ public boolean remove(String bucketName, String path, String fileName) { try { - minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(path + fileName).build()); + minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(path + "/" + fileName).build()); } catch (Exception e) { log.error("删除文件失败,", e); return false; } return true; } + + /** + * 判断文件是否存在 + * + * @param bucketName bucket名称 + * @param path 路径 + * @param fileName 文件名 + * @return + */ + public Boolean isExist(String bucketName, String path, String fileName) { + try { + minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(path + "/" + fileName).build()); + } catch (Exception e) { + return false; + } + return true; + } + } diff --git a/im-platform/src/main/resources/application-dev.yml b/im-platform/src/main/resources/application-dev.yml index aff630b..a18048c 100644 --- a/im-platform/src/main/resources/application-dev.yml +++ b/im-platform/src/main/resources/application-dev.yml @@ -18,6 +18,7 @@ minio: imagePath: image filePath: file videoPath: video + expireIn: 180 # 文件过期时间,单位:天 webrtc: max-channel: 9 # 多人通话最大通道数量,最大不能超过16,建议值:4,9,16 diff --git a/im-platform/src/main/resources/application-prod.yml b/im-platform/src/main/resources/application-prod.yml index dc8e1b6..4b2047d 100644 --- a/im-platform/src/main/resources/application-prod.yml +++ b/im-platform/src/main/resources/application-prod.yml @@ -19,6 +19,7 @@ minio: imagePath: image filePath: file videoPath: video + expireIn: 180 # 文件过期时间,单位:天 webrtc: max-channel: 9 # 多人通话最大通道数量,最大不能超过16,建议值:4,9,16 diff --git a/im-platform/src/main/resources/application-test.yml b/im-platform/src/main/resources/application-test.yml index b9b852d..8be6393 100644 --- a/im-platform/src/main/resources/application-test.yml +++ b/im-platform/src/main/resources/application-test.yml @@ -19,6 +19,7 @@ minio: imagePath: image filePath: file videoPath: video + expireIn: 180 # 文件过期时间,单位:天 webrtc: max-channel: 9 # 多人通话最大通道数量,最大不能超过16,建议值:4,9,16 diff --git a/im-platform/src/main/resources/static/favicon.ico b/im-platform/src/main/resources/static/favicon.ico index 8cd39d4..d31f507 100644 Binary files a/im-platform/src/main/resources/static/favicon.ico and b/im-platform/src/main/resources/static/favicon.ico differ diff --git a/im-uniapp/App.vue b/im-uniapp/App.vue index c5dec44..9910879 100644 --- a/im-uniapp/App.vue +++ b/im-uniapp/App.vue @@ -407,7 +407,6 @@ export default { // #ifdef APP-PLUS // 关闭开机动画 setTimeout(() => { - console.log("plus.navigator.closeSplashscreen()") plus.navigator.closeSplashscreen() }, delay) // #endif diff --git a/im-uniapp/common/recorder-h5.js b/im-uniapp/common/recorder-h5.js index ddb4112..3855942 100644 --- a/im-uniapp/common/recorder-h5.js +++ b/im-uniapp/common/recorder-h5.js @@ -26,7 +26,6 @@ let checkIsEnable = () => { let start = () => { return navigator.mediaDevices.getUserMedia({ audio: true }).then(audioStream => { - console.log("start record") startTime = new Date().getTime(); chunks = []; stream = audioStream; @@ -36,7 +35,6 @@ let start = () => { } let close = () => { - console.log("stream:", stream) stream.getTracks().forEach((track) => { track.stop() }) @@ -47,9 +45,6 @@ let close = () => { let upload = () => { return new Promise((resolve, reject) => { rc.ondataavailable = (e) => { - console.log("ondataavailable:",e.data) - console.log("size:",e.data.size) - console.log("type:",e.data.type) chunks.push(e.data) } rc.onstop = () => { @@ -58,8 +53,6 @@ let upload = () => { return; } duration = (new Date().getTime() - startTime) / 1000; - console.log("时长:", duration) - console.log("上传,chunks:", chunks.length) const newbolb = new Blob(chunks, { 'type': 'audio/mpeg' }); const name = new Date().getDate() + '.mp3'; const file = new File([newbolb], name) diff --git a/im-uniapp/components/chat-item/chat-item.vue b/im-uniapp/components/chat-item/chat-item.vue index 288adef..d0ed604 100644 --- a/im-uniapp/components/chat-item/chat-item.vue +++ b/im-uniapp/components/chat-item/chat-item.vue @@ -9,7 +9,6 @@ {{ chat.showName }} - {{ $date.toTimeText(chat.lastSendTime, true) }} @@ -138,14 +137,6 @@ export default { overflow: hidden; display: flex; align-items: center; - - .uni-tag { - text-align: center; - margin-left: 5rpx; - border: 0; - padding: 1px 5px; - //opacity: 0.8; - } } .chat-time { diff --git a/im-uniapp/components/group-member-selector/group-member-selector.vue b/im-uniapp/components/group-member-selector/group-member-selector.vue index 273b3cc..018d4ee 100644 --- a/im-uniapp/components/group-member-selector/group-member-selector.vue +++ b/im-uniapp/components/group-member-selector/group-member-selector.vue @@ -42,12 +42,15 @@ export default { name: "chat-group-member-choose", props: { + group: { + type: Object + }, members: { type: Array }, maxSize: { type: Number, - default: -1 + default: 50 } }, data() { @@ -56,10 +59,11 @@ export default { }; }, methods: { - init(checkedIds, lockedIds) { + init(checkedIds, lockedIds, hideIds) { this.members.forEach((m) => { m.checked = checkedIds.indexOf(m.userId) >= 0; m.locked = lockedIds.indexOf(m.userId) >= 0; + m.hide = hideIds.indexOf(m.userId) >= 0; }); }, open() { @@ -80,7 +84,7 @@ export default { }, onClean() { this.members.forEach((m) => { - if (!m.locked) { + if (!m.locked && m.checked) { m.checked = false; } }) @@ -88,20 +92,17 @@ export default { onOk() { this.$refs.popup.close(); this.$emit("complete", this.checkedIds) - }, - isChecked(m) { - return this.checkedIds.indexOf(m.userId) >= 0; } }, computed: { checkedIds() { - return this.members.filter((m) => m.checked).map(m => m.userId) + return this.checkedMembers.map(m => m.userId) }, checkedMembers() { - return this.members.filter((m) => m.checked); + return this.members.filter((m) => !m.quit && !m.hide && m.checked); }, showMembers() { - return this.members.filter(m => !m.quit && m.showNickName.includes(this.searchText)) + return this.members.filter(m => !m.quit && !m.hide && m.showNickName.includes(this.searchText)) } } } @@ -115,7 +116,8 @@ export default { flex-direction: column; background-color: white; padding: 10rpx; - border-radius: 15rpx; + border-radius: 15rpx 15rpx 0 0; + overflow: hidden; .top-bar { display: flex; @@ -158,6 +160,8 @@ export default { white-space: nowrap; .member-name { + display: flex; + align-items: center; flex: 1; padding-left: 20rpx; font-size: 30rpx; @@ -165,11 +169,15 @@ export default { line-height: 60rpx; white-space: nowrap; overflow: hidden; + + .uni-tag { + margin-left: 5rpx; + } } } .scroll-bar { - height: 800rpx; + height: 65vh; } } } diff --git a/im-uniapp/components/image-upload/image-upload.vue b/im-uniapp/components/image-upload/image-upload.vue index 6cfe9e5..22ca4ef 100644 --- a/im-uniapp/components/image-upload/image-upload.vue +++ b/im-uniapp/components/image-upload/image-upload.vue @@ -29,6 +29,10 @@ export default { type: String, default: 'album' }, + isPermanent: { + type: Boolean, + default: false + }, onBefore: { type: Function, default: null @@ -50,7 +54,6 @@ export default { sizeType: ['original'], //original 原图,compressed 压缩图,默认二者都有 success: (res) => { res.tempFiles.forEach((file) => { - console.log("文件:", file) if (!this.onBefore || this.onBefore(file)) { // 调用上传图片的接口 this.uploadImage(file); @@ -61,7 +64,7 @@ export default { }, uploadImage(file) { uni.uploadFile({ - url: UNI_APP.BASE_URL + '/image/upload', + url: UNI_APP.BASE_URL + '/image/upload?isPermanent=' + this.isPermanent, header: { accessToken: uni.getStorageSync("loginInfo").accessToken }, diff --git a/im-uniapp/components/virtual-scroller/virtual-scroller.vue b/im-uniapp/components/virtual-scroller/virtual-scroller.vue index 049eb3f..ab99096 100644 --- a/im-uniapp/components/virtual-scroller/virtual-scroller.vue +++ b/im-uniapp/components/virtual-scroller/virtual-scroller.vue @@ -28,7 +28,6 @@ export default { }, methods: { onScrollToBottom(e) { - console.log("onScrollToBottom") if (this.showMaxIdx >= this.items.length) { this.showTip(); } else { diff --git a/im-uniapp/pages/chat/chat-box.vue b/im-uniapp/pages/chat/chat-box.vue index 3626bf7..2af4b66 100644 --- a/im-uniapp/pages/chat/chat-box.vue +++ b/im-uniapp/pages/chat/chat-box.vue @@ -733,7 +733,6 @@ export default { h -= 50; // 减去键盘高度 if (this.isShowKeyBoard || this.chatTabBox != 'none') { - console.log("减去键盘高度:", this.keyboardHeight) h -= this.keyboardHeight; this.scrollToBottom(); } @@ -742,7 +741,6 @@ export default { h -= uni.getSystemInfoSync().statusBarHeight; // #endif this.chatMainHeight = h; - console.log("窗口高度:", this.chatMainHeight) if (this.isShowKeyBoard || this.chatTabBox != 'none') { this.scrollToBottom(); } @@ -806,7 +804,6 @@ export default { this.reCalChatMainHeight(); }, resizeListener() { - console.log("resize") let keyboardHeight = this.initHeight - window.innerHeight; this.isShowKeyBoard = keyboardHeight > 150; if (this.isShowKeyBoard) { @@ -815,12 +812,10 @@ export default { this.reCalChatMainHeight(); }, focusInListener() { - console.log("focusInListener") this.isShowKeyBoard = true; this.reCalChatMainHeight(); }, focusOutListener() { - console.log("focusOutListener") this.isShowKeyBoard = false; this.reCalChatMainHeight(); }, diff --git a/im-uniapp/pages/chat/chat-group-video.vue b/im-uniapp/pages/chat/chat-group-video.vue index 7bb7314..434771f 100644 --- a/im-uniapp/pages/chat/chat-group-video.vue +++ b/im-uniapp/pages/chat/chat-group-video.vue @@ -76,7 +76,6 @@ export default { }, }, onBackPress() { - console.log("onBackPress") this.sendMessageToWebView("NAV_BACK", {}) }, onLoad(options) { diff --git a/im-uniapp/pages/group/group-edit.vue b/im-uniapp/pages/group/group-edit.vue index c1fd83f..eaae034 100644 --- a/im-uniapp/pages/group/group-edit.vue +++ b/im-uniapp/pages/group/group-edit.vue @@ -5,7 +5,7 @@ 群聊头像 - + - + @@ -12,8 +12,17 @@ - - + + + + + 邀请 + + + + + + 移除 {{ `查看全部群成员${groupMembers.length}人` }}> @@ -41,15 +50,15 @@ - 修改群聊资料 > - + @@ -69,6 +78,29 @@ export default { url: `/pages/group/group-invite?id=${this.groupId}` }) }, + onRemoveMember() { + // 群主不显示 + let hideIds = [this.group.ownerId]; + this.$refs.removeSelector.init([], [], hideIds); + this.$refs.removeSelector.open(); + }, + onRemoveComplete(userIds) { + let data = { + groupId: this.group.id, + userIds: userIds + } + this.$http({ + url: "/group/members/remove", + method: 'DELETE', + data: data + }).then(() => { + this.loadGroupMembers(); + uni.showToast({ + title: `您移除了${userIds.length}位成员`, + icon: 'none' + }) + }) + }, onShowMoreMmeber() { uni.navigateTo({ url: `/pages/group/group-member?id=${this.groupId}` @@ -160,7 +192,6 @@ export default { }); }, loadGroupMembers() { - console.log("loadGroupMembers") this.$http({ url: `/group/members/${this.groupId}`, method: "GET" @@ -176,6 +207,9 @@ export default { }, isOwner() { return this.group.ownerId == this.userStore.userInfo.id; + }, + showMaxIdx() { + return this.isOwner ? 8 : 9; } }, onLoad(options) { @@ -218,17 +252,21 @@ export default { padding-top: 8rpx; font-size: $im-font-size-smaller; } - } - .invite-btn { - display: flex; - justify-content: center; - align-items: center; - width: 86rpx; - height: 86rpx; - margin: 10rpx; - border: $im-border solid 2rpx; - border-radius: 10%; + .tools-btn { + display: flex; + justify-content: center; + align-items: center; + border: $im-border solid 1rpx; + border-radius: 10%; + width: 80rpx; + height: 80rpx; + + .icon { + font-size: 40rpx !important; + color: $im-text-color-lighter !important; + } + } } } diff --git a/im-uniapp/pages/group/group-member.vue b/im-uniapp/pages/group/group-member.vue index 860ab5a..434f085 100644 --- a/im-uniapp/pages/group/group-member.vue +++ b/im-uniapp/pages/group/group-member.vue @@ -13,14 +13,9 @@ {{ item.showNickName }} - - + - - - @@ -44,27 +39,6 @@ export default { url: "/pages/common/user-info?id=" + userId }) }, - onKickOut(member) { - uni.showModal({ - title: '确认移出?', - content: `确定将成员'${member.showNickName}'移出群聊吗?`, - success: (res) => { - if (res.cancel) - return; - this.$http({ - url: `/group/kick/${this.group.id}?userId=${member.userId}`, - method: 'DELETE' - }).then(() => { - uni.showToast({ - title: `已将${member.showNickName}移出群聊`, - icon: 'none' - }) - member.quit = true; - this.isModify = true; - }); - } - }) - }, loadGroupInfo(id) { this.$http({ url: `/group/find/${id}`, @@ -141,12 +115,6 @@ export default { .uni-tag { margin-left: 5rpx; - width: 40rpx; - border: 0; - height: 30rpx; - line-height: 30rpx; - font-size: 20rpx; - text-align: center; } } } diff --git a/im-uniapp/pages/mine/mine-edit.vue b/im-uniapp/pages/mine/mine-edit.vue index 164c92d..80ad592 100644 --- a/im-uniapp/pages/mine/mine-edit.vue +++ b/im-uniapp/pages/mine/mine-edit.vue @@ -4,7 +4,7 @@ 头像 - + diff --git a/im-uniapp/pages/mine/mine.vue b/im-uniapp/pages/mine/mine.vue index 266e73a..ecc8654 100644 --- a/im-uniapp/pages/mine/mine.vue +++ b/im-uniapp/pages/mine/mine.vue @@ -56,7 +56,6 @@ export default { title: '确认退出?', success: (res) => { if (res.confirm) { - console.log(getApp()) getApp().$vm.exit() } } diff --git a/im-uniapp/pages/register/register.vue b/im-uniapp/pages/register/register.vue index a2cbe23..553c340 100644 --- a/im-uniapp/pages/register/register.vue +++ b/im-uniapp/pages/register/register.vue @@ -59,7 +59,6 @@ export default { errorMessage: '请输入确认密码', }, { validateFunction: (rule, value, data, callback) => { - console.log("validateFunction") if (data.password != value) { callback('两次密码输入不一致') } diff --git a/im-uniapp/static/icon/iconfont.css b/im-uniapp/static/icon/iconfont.css index 71a64f4..8cda2a4 100644 --- a/im-uniapp/static/icon/iconfont.css +++ b/im-uniapp/static/icon/iconfont.css @@ -1,6 +1,6 @@ @font-face { font-family: "iconfont"; /* Project id 4272106 */ - src: url('iconfont.ttf?t=1739084401359') format('truetype'); + src: url('iconfont.ttf?t=1746119818070') format('truetype'); } .iconfont { @@ -11,6 +11,46 @@ -moz-osx-font-smoothing: grayscale; } +.icon-remove:before { + content: "\e603"; +} + +.icon-doc:before { + content: "\e61c"; +} + +.icon-image:before { + content: "\e7f7"; +} + +.icon-top-message:before { + content: "\e6ff"; +} + +.icon-setting:before { + content: "\e851"; +} + +.icon-phone:before { + content: "\e692"; +} + +.icon-email:before { + content: "\e611"; +} + +.icon-username:before { + content: "\e60f"; +} + +.icon-chat-muted:before { + content: "\e634"; +} + +.icon-chat-unmuted:before { + content: "\ec44"; +} + .icon-privacy-protocol:before { content: "\e70a"; } diff --git a/im-uniapp/static/icon/iconfont.ttf b/im-uniapp/static/icon/iconfont.ttf index 5d29d8e..c2fee58 100644 Binary files a/im-uniapp/static/icon/iconfont.ttf and b/im-uniapp/static/icon/iconfont.ttf differ diff --git a/im-uniapp/store/userStore.js b/im-uniapp/store/userStore.js index 168fc1b..7543bd0 100644 --- a/im-uniapp/store/userStore.js +++ b/im-uniapp/store/userStore.js @@ -20,7 +20,6 @@ export default defineStore('userStore', { url: '/user/self', method: 'GET' }).then((userInfo) => { - console.log(userInfo) this.setUserInfo(userInfo); resolve(); }).catch((res) => { diff --git a/im-web/src/api/camera.js b/im-web/src/api/camera.js index dc75a78..6801381 100644 --- a/im-web/src/api/camera.js +++ b/im-web/src/api/camera.js @@ -21,7 +21,6 @@ ImCamera.prototype.openVideo = function () { noiseSuppression: true // 开启降噪 } } - console.log("getUserMedia") navigator.mediaDevices.getUserMedia(constraints).then((stream) => { console.log("摄像头打开") this.stream = stream; diff --git a/im-web/src/assets/iconfont/iconfont.css b/im-web/src/assets/iconfont/iconfont.css index 8c5b819..f435d95 100644 --- a/im-web/src/assets/iconfont/iconfont.css +++ b/im-web/src/assets/iconfont/iconfont.css @@ -1,6 +1,6 @@ @font-face { font-family: "iconfont"; /* Project id 3791506 */ - src: url('iconfont.ttf?t=1718373714629') format('truetype'); + src: url('iconfont.ttf?t=1745933248800') format('truetype'); } .iconfont { @@ -11,6 +11,42 @@ -moz-osx-font-smoothing: grayscale; } +.icon-man:before { + content: "\e615"; +} + +.icon-girl:before { + content: "\e602"; +} + +.icon-no-data:before { + content: "\e61b"; +} + +.icon-phone:before { + content: "\e692"; +} + +.icon-email:before { + content: "\e610"; +} + +.icon-username:before { + content: "\e60f"; +} + +.icon-chat-unmuted:before { + content: "\ec44"; +} + +.icon-chat-muted:before { + content: "\e634"; +} + +.icon-modify:before { + content: "\e60d"; +} + .icon-invite-rtc:before { content: "\e65f"; } diff --git a/im-web/src/assets/iconfont/iconfont.ttf b/im-web/src/assets/iconfont/iconfont.ttf index 1697727..e6c7a35 100644 Binary files a/im-web/src/assets/iconfont/iconfont.ttf and b/im-web/src/assets/iconfont/iconfont.ttf differ diff --git a/im-web/src/components/chat/ChatAtBox.vue b/im-web/src/components/chat/ChatAtBox.vue index cbd0355..9229896 100644 --- a/im-web/src/components/chat/ChatAtBox.vue +++ b/im-web/src/components/chat/ChatAtBox.vue @@ -88,13 +88,13 @@ export default { this.close(); }, scrollToActive() { - if (this.activeIdx * 35 - this.$refs.scrollBox.wrap.clientHeight > this.$refs.scrollBox.wrap.scrollTop) { + if (this.activeIdx * 40 - this.$refs.scrollBox.wrap.clientHeight > this.$refs.scrollBox.wrap.scrollTop) { this.$refs.scrollBox.wrap.scrollTop += 140; if (this.$refs.scrollBox.wrap.scrollTop > this.$refs.scrollBox.wrap.scrollHeight) { this.$refs.scrollBox.wrap.scrollTop = this.$refs.scrollBox.wrap.scrollHeight } } - if (this.activeIdx * 35 < this.$refs.scrollBox.wrap.scrollTop) { + if (this.activeIdx * 40 < this.$refs.scrollBox.wrap.scrollTop) { this.$refs.scrollBox.wrap.scrollTop -= 140; if (this.$refs.scrollBox.wrap.scrollTop < 0) { this.$refs.scrollBox.wrap.scrollTop = 0; diff --git a/im-web/src/components/chat/ChatBox.vue b/im-web/src/components/chat/ChatBox.vue index dd3ea6f..f7195a4 100644 --- a/im-web/src/components/chat/ChatBox.vue +++ b/im-web/src/components/chat/ChatBox.vue @@ -480,7 +480,7 @@ export default { cancelButtonText: '取消', type: 'warning' }).then(() => { - this.$store.commit("deleteMessage", msgInfo); + this.$store.commit("deleteMessage", [msgInfo, this.chat]); }); }, recallMessage(msgInfo) { @@ -567,13 +567,15 @@ export default { }) }, showName(msgInfo) { - if (this.chat.type == 'GROUP') { + if (!msgInfo) { + return "" + } + if (this.isGroup) { let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId); - return member ? member.showNickName : ""; + return member ? member.showNickName : msgInfo.sendNickName || ""; } else { - return msgInfo.sendId == this.mine.id ? this.mine.nickName : this.chat.showName + return msgInfo.selfSend ? this.mine.nickName : this.chat.showName } - }, headImage(msgInfo) { if (this.chat.type == 'GROUP') { diff --git a/im-web/src/components/chat/ChatGroupMember.vue b/im-web/src/components/chat/ChatGroupMember.vue index beea61f..dff4da5 100644 --- a/im-web/src/components/chat/ChatGroupMember.vue +++ b/im-web/src/components/chat/ChatGroupMember.vue @@ -1,60 +1,64 @@ + \ No newline at end of file diff --git a/im-web/src/components/chat/ChatGroupSide.vue b/im-web/src/components/chat/ChatGroupSide.vue index 09a4594..0140128 100644 --- a/im-web/src/components/chat/ChatGroupSide.vue +++ b/im-web/src/components/chat/ChatGroupSide.vue @@ -5,66 +5,71 @@ -
- -
-
-
- -
-
邀请
- + +
+
+
+
-
- +
邀请
+ +
+
+
+
+
移除
+
- - - - - - - - - - - - - - - - - - -
- 保存 - 编辑 - 退出群聊 +
+
- -
- +
+ + + + + + + + + + + + + + + + + + +
+ 保存 + 编辑 + 退出群聊 +
+
diff --git a/im-web/src/components/group/GroupMember.vue b/im-web/src/components/group/GroupMember.vue index 5ce63f8..7d1dec9 100644 --- a/im-web/src/components/group/GroupMember.vue +++ b/im-web/src/components/group/GroupMember.vue @@ -2,7 +2,6 @@
-
{{ member.showNickName }}
@@ -20,16 +19,9 @@ export default { member: { type: Object, required: true - }, - showDel: { - type: Boolean, - default: false } }, methods: { - onDelete() { - this.$emit("del", this.member); - } } } diff --git a/im-web/src/components/group/GroupMemberSelector.vue b/im-web/src/components/group/GroupMemberSelector.vue index dc2f941..e3da675 100644 --- a/im-web/src/components/group/GroupMemberSelector.vue +++ b/im-web/src/components/group/GroupMemberSelector.vue @@ -1,5 +1,5 @@