实时通讯

This commit is contained in:
高保安
2025-12-15 11:46:00 +08:00
parent 7fb4bf94c4
commit 01ea7e15fa
53 changed files with 1755 additions and 135 deletions

11
pom.xml
View File

@@ -21,11 +21,6 @@
<version>4.9.0</version> <version>4.9.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency> <dependency>
<groupId>commons-fileupload</groupId> <groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId> <artifactId>commons-fileupload</artifactId>
@@ -63,11 +58,7 @@
<version>3.0.2</version> <version>3.0.2</version>
</dependency> </dependency>
<!-- 音频处理(格式转换) --> <!-- 音频处理(格式转换) -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.9</version>
</dependency>
<dependency> <dependency>
<groupId>mysql</groupId> <groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId> <artifactId>mysql-connector-java</artifactId>

View File

@@ -1,90 +1,90 @@
package com.realtime.config; //package com.realtime.config;
//
import com.realtime.service.DoubaoVoiceService; //import com.realtime.service.DoubaoVoiceService;
import lombok.extern.slf4j.Slf4j; //import lombok.extern.slf4j.Slf4j;
import org.bytedeco.javacv.FFmpegFrameGrabber; //import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder; //import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame; //import org.bytedeco.javacv.Frame;
import org.springframework.stereotype.Component; //import org.springframework.stereotype.Component;
import org.springframework.web.socket.BinaryMessage; //import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus; //import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketSession; //import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.BinaryWebSocketHandler; //import org.springframework.web.socket.handler.BinaryWebSocketHandler;
//
import java.io.ByteArrayInputStream; //import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; //import java.io.ByteArrayOutputStream;
import java.io.EOFException; //import java.io.EOFException;
@Slf4j //@Slf4j
@Component //@Component
public class ClientVoiceHandler extends BinaryWebSocketHandler { //public class ClientVoiceHandler extends BinaryWebSocketHandler {
private final DoubaoVoiceService doubaoVoiceService; // private final DoubaoVoiceService doubaoVoiceService;
//
public ClientVoiceHandler(DoubaoVoiceService doubaoVoiceService) { // public ClientVoiceHandler(DoubaoVoiceService doubaoVoiceService) {
this.doubaoVoiceService = doubaoVoiceService; // this.doubaoVoiceService = doubaoVoiceService;
} // }
//
@Override // @Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception { // public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 客户端会话 // // 客户端会话
// 建立与豆包 API 的连接 // // 建立与豆包 API 的连接
//
System.out.println(session); // System.out.println(session);
doubaoVoiceService.connect(session); // doubaoVoiceService.connect(session);
} // }
//
@Override // @Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception { // protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
System.out.println("2"); // System.out.println("2");
// 接收客户端的音频数据(如 PCM 片段) // // 接收客户端的音频数据(如 PCM 片段)
byte[] audioData = message.getPayload().array(); // byte[] audioData = message.getPayload().array();
// 预处理:确保格式符合豆包 API 要求(如采样率、位深) // // 预处理:确保格式符合豆包 API 要求(如采样率、位深)
byte[] processedData = preprocessAudio(audioData); // byte[] processedData = preprocessAudio(audioData);
// 发送给豆包 API // // 发送给豆包 API
doubaoVoiceService.sendAudio(processedData); // doubaoVoiceService.sendAudio(processedData);
} // }
//
@Override // @Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { // public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// 关闭与豆包 API 的连接 // // 关闭与豆包 API 的连接
doubaoVoiceService.close(); // doubaoVoiceService.close();
} // }
//
private byte[] preprocessAudio(byte[] audioData) { // private byte[] preprocessAudio(byte[] audioData) {
if (audioData == null || audioData.length == 0) { // if (audioData == null || audioData.length == 0) {
log.warn("接收到空的音频数据,跳过处理"); // log.warn("接收到空的音频数据,跳过处理");
return new byte[0]; // 返回空数组,避免后续错误 // return new byte[0]; // 返回空数组,避免后续错误
} // }
//
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); // ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
//
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(new ByteArrayInputStream(audioData)); // try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(new ByteArrayInputStream(audioData));
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputStream, 1)) { // FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputStream, 1)) {
//
grabber.start(); // grabber.start();
// 打印输入音频信息(调试用) // // 打印输入音频信息(调试用)
log.debug("输入音频格式:{},采样率:{},声道:{}", // log.debug("输入音频格式:{},采样率:{},声道:{}",
grabber.getFormat(), grabber.getSampleRate(), grabber.getAudioChannels()); // grabber.getFormat(), grabber.getSampleRate(), grabber.getAudioChannels());
//
recorder.setFormat("s16le"); // recorder.setFormat("s16le");
recorder.setSampleRate(16000); // recorder.setSampleRate(16000);
recorder.setAudioChannels(1); // recorder.setAudioChannels(1);
recorder.start(); // recorder.start();
//
Frame frame; // Frame frame;
int frameCount = 0; // int frameCount = 0;
while ((frame = grabber.grab()) != null) { // while ((frame = grabber.grab()) != null) {
recorder.record(frame); // recorder.record(frame);
frameCount++; // frameCount++;
} // }
log.debug("成功处理 {} 帧音频", frameCount); // log.debug("成功处理 {} 帧音频", frameCount);
//
recorder.stop(); // recorder.stop();
grabber.stop(); // grabber.stop();
return outputStream.toByteArray(); // return outputStream.toByteArray();
//
} catch (Exception e) { // } catch (Exception e) {
log.error("音频预处理失败", e); // log.error("音频预处理失败", e);
return new byte[0]; // return new byte[0];
} // }
} // }
} //}

View File

@@ -0,0 +1,18 @@
package com.realtime.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 云端API配置
*/
@Data
@Configuration
public class CloudApiConfig {
/**
* 云端API基础URL
*/
private String baseUrl = "http://www.yuxindazhineng.com:3001/cloud_api";
}

View File

@@ -0,0 +1,21 @@
package com.realtime.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
* RestTemplate配置
*/
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(10000); // 连接超时10秒
factory.setReadTimeout(30000); // 读取超时30秒
return new RestTemplate(factory);
}
}

View File

@@ -0,0 +1,83 @@
package com.realtime.controller;
import com.realtime.model.query.*;
import com.realtime.service.CloudFileService;
import com.realtime.sysconst.Result;
import com.realtime.vo.CloudFileDownloadVo;
import com.realtime.vo.CloudFileListVo;
import com.realtime.vo.CloudFileOperationVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* 云端文件管理控制器
*/
@Slf4j
@RestController
@RequestMapping("/cloud_api/file")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class CloudFileController {
private final CloudFileService cloudFileService;
/**
* 批量上传云端文件
*/
@PostMapping("/upload")
public Result<CloudFileOperationVo> batchUpload(
@RequestPart("files") List<MultipartFile> files,
@RequestParam("path") String path,
@RequestParam("type") String type,
@RequestParam(value = "team_id", required = false) String teamId) {
CloudFileUploadReq request = new CloudFileUploadReq();
request.setPath(path);
request.setType(type);
request.setTeamId(teamId);
// TODO: 从Session或Token获取当前用户ID
String userId = "current_user_id";
return Result.success(cloudFileService.batchUpload(files, request, userId));
}
/**
* 通过URL批量上传云端文件
*/
@PostMapping("/upload/url")
public Result<CloudFileOperationVo> batchUploadByUrl(@RequestBody CloudFileUploadByUrlReq request) {
// TODO: 从Session或Token获取当前用户ID
String userId = "current_user_id";
return Result.success(cloudFileService.batchUploadByUrl(request, userId));
}
/**
* 下载文件
*/
@PostMapping("/download")
public Result<CloudFileDownloadVo> downloadFile(@RequestBody CloudFileDownloadReq request) {
return Result.success(cloudFileService.downloadFile(request));
}
/**
* 查看文件列表
*/
@PostMapping("/list")
public Result<CloudFileListVo> listFiles(@RequestBody CloudFileListReq request) {
return Result.success(cloudFileService.listFiles(request));
}
/**
* 删除文件
*/
@PostMapping("/delete")
public Result<CloudFileOperationVo> deleteFile(@RequestBody CloudFileDeleteReq request) {
return Result.success(cloudFileService.deleteFile(request));
}
}

View File

@@ -72,8 +72,8 @@ public class GroupController {
} }
@PostMapping("/saveInvent") @PostMapping("/saveInvent")
Result<String> saveInvent(@RequestBody List<GroupMember> groupMembers,@RequestParam("launchContactId")String launchContactId) { Result<String> saveInvent(@RequestBody List<GroupMember> groupMembers, @RequestParam("launchContactId") String launchContactId) {
return groupMemberService.saveInvent(groupMembers,launchContactId); return groupMemberService.saveInvent(groupMembers, launchContactId);
} }
@PostMapping("/disband") @PostMapping("/disband")
@@ -85,4 +85,6 @@ public class GroupController {
Result<String> quit(@RequestBody RemoveGroupReq removeGroupReq) { Result<String> quit(@RequestBody RemoveGroupReq removeGroupReq) {
return groupMemberService.quit(removeGroupReq); return groupMemberService.quit(removeGroupReq);
} }
} }

View File

@@ -0,0 +1,112 @@
package com.realtime.controller;
import com.realtime.model.query.*;
import com.realtime.service.TeamService;
import com.realtime.sysconst.Result;
import com.realtime.vo.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 团队管理控制器
*/
@Slf4j
@RestController
@RequestMapping("/cloud_api/team")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class TeamController {
private final TeamService teamService;
/**
* 获取用户可访问的团队列表
* @param request 团队列表查询请求
* @return 团队列表
*/
@PostMapping("/list")
public Result<TeamListVo> getTeamList(@RequestBody TeamListReq request) {
return Result.success(teamService.getTeamList(request));
}
/**
* 创建团队
* @param request 创建团队请求
* @return 创建结果
*/
@PostMapping("/create")
public Result<TeamOperationVo> createTeam(@RequestBody TeamCreateReq request) {
return Result.success(teamService.createTeam(request));
}
/**
* 删除团队
* @param request 删除团队请求
* @return 删除结果
*/
@PostMapping("/delete")
public Result<TeamOperationVo> deleteTeam(@RequestBody TeamDeleteReq request) {
return Result.success(teamService.deleteTeam(request));
}
/**
* 修改团队信息
* @param request 修改团队请求
* @return 修改结果
*/
@PostMapping("/update")
public Result<TeamOperationVo> updateTeam(@RequestBody TeamUpdateReq request) {
return Result.success(teamService.updateTeam(request));
}
/**
* 查看全部成员
* @param request 查看成员请求
* @return 成员列表
*/
@PostMapping("/get_all_member")
public Result<TeamMemberListVo> getAllMembers(@RequestBody TeamGetAllMemberReq request) {
return Result.success(teamService.getAllMembers(request));
}
/**
* 批量添加团队成员
* @param request 添加成员请求
* @return 添加结果
*/
@PostMapping("/add_member")
public Result<TeamOperationVo> addMembers(@RequestBody TeamAddMemberReq request) {
return Result.success(teamService.addMembers(request));
}
/**
* 批量删除团队成员
* @param request 删除成员请求
* @return 删除结果
*/
@PostMapping("/remove_member")
public Result<TeamOperationVo> removeMembers(@RequestBody TeamRemoveMemberReq request) {
return Result.success(teamService.removeMembers(request));
}
/**
* 调整团队成员身份
* @param request 调整身份请求
* @return 调整结果
*/
@PostMapping("/adjust_member_role")
public Result<TeamOperationVo> adjustMemberRole(@RequestBody TeamAdjustMemberRoleReq request) {
return Result.success(teamService.adjustMemberRole(request));
}
/**
* 获取部门结构
* @param request 获取部门结构请求
* @return 部门结构
*/
@PostMapping("/get_department")
public Result<TeamDepartmentVo> getDepartment(@RequestBody TeamGetDepartmentReq request) {
return Result.success(teamService.getDepartment(request));
}
}

View File

@@ -10,4 +10,7 @@ import lombok.EqualsAndHashCode;
public class BaseQueryModel<T> extends Page<T> { public class BaseQueryModel<T> extends Page<T> {
private Long id; private Long id;
private String contactId; private String contactId;
private String accessToken;
private String teamType;
private String path;
} }

View File

@@ -37,6 +37,11 @@ public class GroupList implements Serializable {
*/ */
private Integer type; private Integer type;
/**
* 团队ID
*/
private String teamId;
/** /**
* 是否需要邀请才能加入 1 = 是 0 = 否 * 是否需要邀请才能加入 1 = 是 0 = 否
*/ */

View File

@@ -0,0 +1,19 @@
package com.realtime.model.query;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 团队请求基类
*/
@Data
public class BaseTeamReq implements Serializable {
/**
* 访问令牌
*/
@JsonProperty("access_token")
private String accessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiNjhlNzY0ZjBmOTU0ODg3MWMyMmY5M2I4IiwibGFzdExvZ2luIjoiMTc2NTQzNDMyNy41MjA5ODYifQ.Vj0ovpCsziFzeR2qroYnN-3KevR78krx-rDFQ9BKlgU";
}

View File

@@ -0,0 +1,18 @@
package com.realtime.model.query;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 云端文件删除请求
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class CloudFileDeleteReq extends BaseTeamReq implements Serializable {
@JsonProperty("file_id")
private String fileId;
}

View File

@@ -0,0 +1,18 @@
package com.realtime.model.query;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 云端文件下载请求
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class CloudFileDownloadReq extends BaseTeamReq implements Serializable {
@JsonProperty("file_id")
private String fileId;
}

View File

@@ -0,0 +1,31 @@
package com.realtime.model.query;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 云端文件列表查询请求
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class CloudFileListReq extends BaseTeamReq implements Serializable {
/**
* 查询路径,默认根目录 /
*/
private String path = "/";
/**
* 文件类型user/team
*/
private String type;
/**
* 团队IDtype为team时必填
*/
@JsonProperty("team_id")
private String teamId;
}

View File

@@ -0,0 +1,47 @@
package com.realtime.model.query;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.List;
/**
* 通过URL批量上传云端文件请求
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class CloudFileUploadByUrlReq extends BaseTeamReq implements Serializable {
/**
* 上传路径
*/
private String path;
/**
* 上传类型user/team
*/
private String type;
/**
* 团队IDtype为team时必填
*/
@JsonProperty("team_id")
private String teamId;
/**
* 文件URL列表
*/
private List<String> urls;
/**
* 文件名列表
*/
private List<String> filename;
/**
* 文件大小列表
*/
private List<Long> size;
}

View File

@@ -0,0 +1,29 @@
package com.realtime.model.query;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 云端文件上传请求
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class CloudFileUploadReq extends BaseTeamReq implements Serializable {
/**
* 上传路径
*/
private String path;
/**
* 上传类型user/team
*/
private String type;
/**
* 团队IDtype为team时必填
*/
private String teamId;
}

View File

@@ -9,4 +9,5 @@ import lombok.EqualsAndHashCode;
@Data @Data
public class GroupDetailQueryReq extends BaseQueryModel<GroupDetailQueryReq> { public class GroupDetailQueryReq extends BaseQueryModel<GroupDetailQueryReq> {
private Long groupId; private Long groupId;
} }

View File

@@ -0,0 +1,34 @@
package com.realtime.model.query;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.List;
/**
* 批量添加团队成员请求
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TeamAddMemberReq extends BaseTeamReq {
/**
* 团队ID
*/
@JsonProperty("team_id")
private String teamId;
/**
* 用户ID列表
*/
@JsonProperty("user_ids")
private List<String> userIds;
/**
* 角色admin、member
*/
@JsonProperty("role")
private String role;
}

View File

@@ -0,0 +1,33 @@
package com.realtime.model.query;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 调整团队成员身份请求
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TeamAdjustMemberRoleReq extends BaseTeamReq {
/**
* 团队ID
*/
@JsonProperty("team_id")
private String teamId;
/**
* 用户ID
*/
@JsonProperty("user_id")
private String userId;
/**
* 角色admin、member
*/
@JsonProperty("role")
private String role;
}

View File

@@ -0,0 +1,46 @@
package com.realtime.model.query;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 创建团队请求
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TeamCreateReq extends BaseTeamReq {
/**
* 团队名称
*/
@JsonProperty("team_name")
private String teamName;
/**
* 团队描述
*/
@JsonProperty("team_description")
private String teamDescription;
/**
* 团队类型
*/
@JsonProperty("team_type")
private String teamType;
/**
* 父团队ID可选
*/
@JsonProperty("parent_id")
private String parentId;
/**
* 群ID
*/
@JsonProperty("group")
private String group;
}

View File

@@ -0,0 +1,21 @@
package com.realtime.model.query;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 删除团队请求
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TeamDeleteReq extends BaseTeamReq {
/**
* 团队ID
*/
@JsonProperty("team_id")
private String teamId;
}

View File

@@ -0,0 +1,21 @@
package com.realtime.model.query;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 查看全部成员请求
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TeamGetAllMemberReq extends BaseTeamReq {
/**
* 团队ID
*/
@JsonProperty("team_id")
private String teamId;
}

View File

@@ -0,0 +1,15 @@
package com.realtime.model.query;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 获取部门结构请求
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TeamGetDepartmentReq extends BaseTeamReq {
// 空请求体
}

View File

@@ -0,0 +1,21 @@
package com.realtime.model.query;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 团队列表查询请求
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TeamListReq extends BaseTeamReq {
/**
* 团队名称(可选,用于搜索)
*/
@JsonProperty("team_name")
private String teamName;
}

View File

@@ -0,0 +1,28 @@
package com.realtime.model.query;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.List;
/**
* 批量删除团队成员请求
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TeamRemoveMemberReq extends BaseTeamReq {
/**
* 团队ID
*/
@JsonProperty("team_id")
private String teamId;
/**
* 用户ID列表
*/
@JsonProperty("user_ids")
private List<String> userIds;
}

View File

@@ -0,0 +1,45 @@
package com.realtime.model.query;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 修改团队信息请求
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TeamUpdateReq extends BaseTeamReq {
/**
* 团队ID
*/
@JsonProperty("team_id")
private String teamId;
/**
* 团队名称
*/
@JsonProperty("team_name")
private String teamName;
/**
* 团队描述
*/
@JsonProperty("team_description")
private String teamDescription;
/**
* 团队类型
*/
@JsonProperty("team_type")
private String teamType;
/**
* 父团队ID可选
*/
@JsonProperty("parent_id")
private String parentId;
}

View File

@@ -11,11 +11,7 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Data @Data
public class AutoSoftwarePacket extends BasePackets { public class AutoSoftwarePacket extends BasePackets {
private String url; private String merMessage;
private String body;
private String method;
private String commandLine;
@Override @Override
public Byte getCommand() { public Byte getCommand() {
return Command.SOFT_CALL_MSG; return Command.SOFT_CALL_MSG;

View File

@@ -1,6 +1,7 @@
package com.realtime.packets; package com.realtime.packets;
import com.realtime.model.query.TeamCreateReq;
import com.realtime.packets.basePackets.BasePackets; import com.realtime.packets.basePackets.BasePackets;
import com.realtime.packets.command.Command; import com.realtime.packets.command.Command;
import lombok.Data; import lombok.Data;
@@ -14,6 +15,7 @@ public class GroupPacket extends BasePackets {
private List<String> userIds; private List<String> userIds;
private Integer type = 2; private Integer type = 2;
private String groupName; private String groupName;
private TeamCreateReq teamCreateReq;
@Override @Override
public Byte getCommand() { public Byte getCommand() {

View File

@@ -1,5 +1,6 @@
package com.realtime.packets.server.handler; package com.realtime.packets.server.handler;
import com.realtime.utils.SessionUtils;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandler.Sharable;
@@ -7,6 +8,7 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@@ -22,12 +24,29 @@ public class ExceptionHandler extends ChannelDuplexHandler {
@Override @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 处理连接重置异常(客户端断开连接)
if (cause instanceof IOException) {
log.warn("连接异常: {}, 通道: {}", cause.getMessage(), ctx.channel().id().asShortText());
// 清理会话信息
SessionUtils.unbind(ctx.channel());
// 关闭连接
ctx.channel().close();
return;
}
// 处理运行时异常
if (cause instanceof RuntimeException) { if (cause instanceof RuntimeException) {
log.error("运行时异常: ", cause);
ByteBuf byteBuf = ctx.alloc().buffer(); ByteBuf byteBuf = ctx.alloc().buffer();
map.put("errorCode",-10000); map.put("errorCode",-10000);
map.put("errorMessage",cause.getMessage()); map.put("errorMessage",cause.getMessage());
byteBuf.writeBytes(map.toString().getBytes(StandardCharsets.UTF_8)); byteBuf.writeBytes(map.toString().getBytes(StandardCharsets.UTF_8));
ctx.channel().writeAndFlush(new TextWebSocketFrame(byteBuf)); ctx.channel().writeAndFlush(new TextWebSocketFrame(byteBuf));
} else {
// 其他异常
log.error("未处理的异常: ", cause);
SessionUtils.unbind(ctx.channel());
ctx.channel().close();
} }
} }
} }

View File

@@ -4,6 +4,7 @@ package com.realtime.packets.server.handler;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.realtime.exception.BusinessException;
import com.realtime.model.pojo.GroupMessage; import com.realtime.model.pojo.GroupMessage;
import com.realtime.packets.GroupSendPacket; import com.realtime.packets.GroupSendPacket;
import com.realtime.service.GroupMessageService; import com.realtime.service.GroupMessageService;
@@ -33,8 +34,11 @@ public class GroupMessageHandler extends SimpleChannelInboundHandler<GroupSendPa
@Override @Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, GroupSendPacket groupSendPacket) throws Exception { protected void channelRead0(ChannelHandlerContext channelHandlerContext, GroupSendPacket groupSendPacket) throws Exception {
ChannelGroup group = SessionUtils.getChannelGroup(groupSendPacket.getGroupId()); ChannelGroup group = SessionUtils.getChannelGroup(groupSendPacket.getGroupId());
if (group == null) {
log.warn("群组不存在或未初始化: groupId={}", groupSendPacket.getGroupId());
throw new BusinessException("群组通道未初始化");
}
ByteBuf buff = getBuff(channelHandlerContext, groupSendPacket); ByteBuf buff = getBuff(channelHandlerContext, groupSendPacket);
assert group != null;
group.writeAndFlush(new TextWebSocketFrame(buff)); group.writeAndFlush(new TextWebSocketFrame(buff));
} }

View File

@@ -4,9 +4,12 @@ package com.realtime.packets.server.handler;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.realtime.model.query.TeamCreateReq;
import com.realtime.packets.GroupPacket; import com.realtime.packets.GroupPacket;
import com.realtime.service.GroupListService; import com.realtime.service.GroupListService;
import com.realtime.service.TeamService;
import com.realtime.utils.SessionUtils; import com.realtime.utils.SessionUtils;
import com.realtime.vo.TeamOperationVo;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel; import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandler;
@@ -30,6 +33,8 @@ public class JoinGroupHandler extends SimpleChannelInboundHandler<GroupPacket> {
private final GroupListService groupListService; private final GroupListService groupListService;
private final TeamService teamService;
@Override @Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, GroupPacket groupPacket) throws Exception { protected void channelRead0(ChannelHandlerContext channelHandlerContext, GroupPacket groupPacket) throws Exception {
ChannelGroup channels = new DefaultChannelGroup(channelHandlerContext.executor()); ChannelGroup channels = new DefaultChannelGroup(channelHandlerContext.executor());
@@ -52,7 +57,14 @@ public class JoinGroupHandler extends SimpleChannelInboundHandler<GroupPacket> {
groupPacket.setGroupId(groupId); groupPacket.setGroupId(groupId);
byte[] bytes = JSONObject.toJSONString(groupPacket).getBytes(StandardCharsets.UTF_8); byte[] bytes = JSONObject.toJSONString(groupPacket).getBytes(StandardCharsets.UTF_8);
byteBuf.writeBytes(bytes); byteBuf.writeBytes(bytes);
groupListService.saveGroupList(groupPacket, groupId); TeamCreateReq teamCreateReq = new TeamCreateReq();
teamCreateReq.setTeamName(groupPacket.getGroupName()+System.currentTimeMillis()+"_的团队");
teamCreateReq.setTeamDescription("默认介绍");
teamCreateReq.setTeamType("company");
teamCreateReq.setParentId("");
teamCreateReq.setGroup(groupId.toString());
TeamOperationVo team = teamService.createTeam(teamCreateReq);
groupListService.saveGroupList(groupPacket, groupId,team);
return byteBuf; return byteBuf;
} }
} }

View File

@@ -6,6 +6,7 @@ import com.alibaba.fastjson2.JSONObject;
import com.realtime.config.NettyConfig; import com.realtime.config.NettyConfig;
import com.realtime.packets.basePackets.BasePackets; import com.realtime.packets.basePackets.BasePackets;
import com.realtime.packets.strategy.PacketService; import com.realtime.packets.strategy.PacketService;
import com.realtime.utils.SessionUtils;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
@@ -16,6 +17,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@Slf4j @Slf4j
@@ -62,6 +64,10 @@ public class MessageSocketHandler extends SimpleChannelInboundHandler<TextWebSoc
@Override @Override
public void channelInactive(ChannelHandlerContext ctx) { public void channelInactive(ChannelHandlerContext ctx) {
log.info("连接断开: {}", ctx.channel().id().asShortText());
// 清理会话信息
SessionUtils.unbind(ctx.channel());
// 从通道组移除
NettyConfig.group.remove(ctx.channel()); NettyConfig.group.remove(ctx.channel());
} }
@@ -72,7 +78,15 @@ public class MessageSocketHandler extends SimpleChannelInboundHandler<TextWebSoc
@Override @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace(); // 处理连接重置异常
// ctx.close(); if (cause instanceof IOException) {
log.warn("连接IO异常: {}, 通道: {}", cause.getMessage(), ctx.channel().id().asShortText());
} else {
log.error("处理消息异常: ", cause);
}
// 清理会话信息
SessionUtils.unbind(ctx.channel());
// 关闭连接
ctx.close();
} }
} }

View File

@@ -0,0 +1,40 @@
package com.realtime.service;
import com.realtime.model.query.*;
import com.realtime.vo.CloudFileDownloadVo;
import com.realtime.vo.CloudFileListVo;
import com.realtime.vo.CloudFileOperationVo;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* 云端文件服务接口
*/
public interface CloudFileService {
/**
* 批量上传云端文件
*/
CloudFileOperationVo batchUpload(List<MultipartFile> files, CloudFileUploadReq request, String userId);
/**
* 通过URL批量上传云端文件
*/
CloudFileOperationVo batchUploadByUrl(CloudFileUploadByUrlReq request, String userId);
/**
* 下载文件
*/
CloudFileDownloadVo downloadFile(CloudFileDownloadReq request);
/**
* 查看文件列表
*/
CloudFileListVo listFiles(CloudFileListReq request);
/**
* 删除文件
*/
CloudFileOperationVo deleteFile(CloudFileDeleteReq request);
}

View File

@@ -13,13 +13,14 @@ import com.realtime.packets.GroupPacket;
import com.realtime.sysconst.Result; import com.realtime.sysconst.Result;
import com.realtime.vo.GroupDetailVo; import com.realtime.vo.GroupDetailVo;
import com.realtime.vo.GroupListVo; import com.realtime.vo.GroupListVo;
import com.realtime.vo.TeamOperationVo;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
public interface GroupListService extends IService<GroupList> { public interface GroupListService extends IService<GroupList> {
void saveGroupList(GroupPacket groupPacket, Long groupId); void saveGroupList(GroupPacket groupPacket, Long groupId, TeamOperationVo teamOperationVo);
Result<IPage<GroupListVo>> getGroup(GroupListQueryReq groupDetailQueryReq); Result<IPage<GroupListVo>> getGroup(GroupListQueryReq groupDetailQueryReq);

View File

@@ -0,0 +1,73 @@
package com.realtime.service;
import com.realtime.model.query.*;
import com.realtime.vo.*;
/**
* 团队管理服务接口
*/
public interface TeamService {
/**
* 获取用户可访问的团队列表
* @param request 团队列表查询请求
* @return 团队列表
*/
TeamListVo getTeamList(TeamListReq request);
/**
* 创建团队
* @param request 创建团队请求
* @return 创建结果
*/
TeamOperationVo createTeam(TeamCreateReq request);
/**
* 删除团队
* @param request 删除团队请求
* @return 删除结果
*/
TeamOperationVo deleteTeam(TeamDeleteReq request);
/**
* 修改团队信息
* @param request 修改团队请求
* @return 修改结果
*/
TeamOperationVo updateTeam(TeamUpdateReq request);
/**
* 查看全部成员
* @param request 查看成员请求
* @return 成员列表
*/
TeamMemberListVo getAllMembers(TeamGetAllMemberReq request);
/**
* 批量添加团队成员
* @param request 添加成员请求
* @return 添加结果
*/
TeamOperationVo addMembers(TeamAddMemberReq request);
/**
* 批量删除团队成员
* @param request 删除成员请求
* @return 删除结果
*/
TeamOperationVo removeMembers(TeamRemoveMemberReq request);
/**
* 调整团队成员身份
* @param request 调整身份请求
* @return 调整结果
*/
TeamOperationVo adjustMemberRole(TeamAdjustMemberRoleReq request);
/**
* 获取部门结构
* @param request 获取部门结构请求
* @return 部门结构
*/
TeamDepartmentVo getDepartment(TeamGetDepartmentReq request);
}

View File

@@ -0,0 +1,167 @@
package com.realtime.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.realtime.config.CloudApiConfig;
import com.realtime.exception.BusinessException;
import com.realtime.model.query.*;
import com.realtime.service.CloudFileService;
import com.realtime.vo.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
* 云端文件服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class CloudFileServiceImpl implements CloudFileService {
private final CloudApiConfig cloudApiConfig;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
@Override
public CloudFileOperationVo batchUpload(List<MultipartFile> files, CloudFileUploadReq request, String userId) {
if (files == null || files.isEmpty()) {
throw new BusinessException("文件列表不能为空");
}
try {
String url = cloudApiConfig.getBaseUrl() + "/file/upload";
// 构建multipart请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
// 添加文件
for (MultipartFile file : files) {
body.add("files", file.getResource());
}
// 添加其他参数
body.add("path", request.getPath());
body.add("type", request.getType());
if (request.getTeamId() != null) {
body.add("team_id", request.getTeamId());
}
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
// 调用外部API
ResponseEntity<CloudFileOperationVo> response = restTemplate.postForEntity(
url, requestEntity, CloudFileOperationVo.class);
return response.getBody();
} catch (Exception e) {
log.error("批量上传文件失败", e);
throw new BusinessException("批量上传文件失败: " + e.getMessage());
}
}
@Override
public CloudFileOperationVo batchUploadByUrl(CloudFileUploadByUrlReq request, String userId) {
if (request.getUrls() == null || request.getUrls().isEmpty()) {
throw new BusinessException("URL列表不能为空");
}
try {
String url = cloudApiConfig.getBaseUrl() + "/file/upload/url";
// 构建请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<CloudFileUploadByUrlReq> requestEntity = new HttpEntity<>(request, headers);
// 调用外部API
ResponseEntity<CloudFileOperationVo> response = restTemplate.postForEntity(
url, requestEntity, CloudFileOperationVo.class);
return response.getBody();
} catch (Exception e) {
log.error("批量URL上传文件失败", e);
throw new BusinessException("批量URL上传文件失败: " + e.getMessage());
}
}
@Override
public CloudFileDownloadVo downloadFile(CloudFileDownloadReq request) {
try {
String url = cloudApiConfig.getBaseUrl() + "/file/download";
// 构建请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<CloudFileDownloadReq> requestEntity = new HttpEntity<>(request, headers);
// 调用外部API
ResponseEntity<CloudFileDownloadVo> response = restTemplate.postForEntity(
url, requestEntity, CloudFileDownloadVo.class);
return response.getBody();
} catch (Exception e) {
log.error("生成下载链接失败", e);
throw new BusinessException("生成下载链接失败: " + e.getMessage());
}
}
@Override
public CloudFileListVo listFiles(CloudFileListReq request) {
try {
String url = cloudApiConfig.getBaseUrl() + "/file/list";
// 构建请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<CloudFileListReq> requestEntity = new HttpEntity<>(request, headers);
// 调用外部API
ResponseEntity<CloudFileListVo> response = restTemplate.postForEntity(
url, requestEntity, CloudFileListVo.class);
return response.getBody();
} catch (Exception e) {
log.error("查询文件列表失败", e);
throw new BusinessException("查询文件列表失败: " + e.getMessage());
}
}
@Override
public CloudFileOperationVo deleteFile(CloudFileDeleteReq request) {
try {
String url = cloudApiConfig.getBaseUrl() + "/file/delete";
// 构建请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<CloudFileDeleteReq> requestEntity = new HttpEntity<>(request, headers);
// 调用外部API
ResponseEntity<CloudFileOperationVo> response = restTemplate.postForEntity(
url, requestEntity, CloudFileOperationVo.class);
return response.getBody();
} catch (Exception e) {
log.error("删除文件失败", e);
throw new BusinessException("删除文件失败: " + e.getMessage());
}
}
}

View File

@@ -25,6 +25,7 @@ import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@@ -51,39 +52,38 @@ public class FileServiceImpl implements FileService {
throw new BusinessException(ResultEnum.FILE_FORMAT_ERROR); throw new BusinessException(ResultEnum.FILE_FORMAT_ERROR);
} }
UploadVo vo = generateFileName(file,fileSuffix); UploadVo vo = generateFileName(file, fileSuffix);
InputStream inputStream = file.getInputStream(); InputStream inputStream = file.getInputStream();
minioClient.putObject(PutObjectArgs.builder() minioClient.putObject(PutObjectArgs.builder()
.bucket(minioConfig.getBucketName()) .bucket(minioConfig.getBucketName())
.object(vo.getNewFileName()) .object(vo.getNewFileName())
.stream(inputStream, file.getSize(), -1) .stream(inputStream, file.getSize(), -1)
.contentType(file.getContentType()) .contentType(file.getContentType())
.build() .build()
); );
return Result.success(vo); return Result.success(vo);
} }
private UploadVo generateFileName(MultipartFile file,String fileSuffix) { private UploadVo generateFileName(MultipartFile file, String fileSuffix) {
String originalFilename = file.getOriginalFilename(); String originalFilename = file.getOriginalFilename();
assert originalFilename != null; assert originalFilename != null;
UploadVo uploadVo = new UploadVo(); UploadVo uploadVo = new UploadVo();
String storeFileName = UUID.randomUUID() + "_" + originalFilename; String storeFileName = originalFilename + Instant.now().getEpochSecond();
String url = generateFileUrl(storeFileName); String url = generateFileUrl(storeFileName);
uploadVo.setName(originalFilename); uploadVo.setName(originalFilename);
uploadVo.setUrl(url); uploadVo.setUrl(url);
uploadVo.setExtendName(fileSuffix); uploadVo.setExtendName(fileSuffix);
uploadVo.setNewFileName(storeFileName); uploadVo.setNewFileName(storeFileName);
uploadVo.setFileSize(BigDecimal.valueOf(file.getSize()).divide(BigDecimal.valueOf(1048576)).setScale(3, RoundingMode.HALF_UP)); // MB uploadVo.setFileSize(BigDecimal.valueOf(file.getSize()).divide(BigDecimal.valueOf(1048576), 3, RoundingMode.HALF_UP)); // MB
uploadVo.setFileNameCode(Base64.encode(originalFilename)); uploadVo.setFileNameCode(Base64.encode(originalFilename));
return uploadVo; return uploadVo;
} }
private String generateFileUrl(String fileName) {
return String generateFileUrl(String fileName) {
minioConfig.getEndpoint()+"/real/" + fileName; return new StringBuilder().append(minioConfig.getEndpoint()).append("/").append(minioConfig.getBucketName()).append("/").append(fileName).toString();
} }
} }

View File

@@ -1,6 +1,8 @@
package com.realtime.service.impl; package com.realtime.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.zxing.BarcodeFormat; import com.google.zxing.BarcodeFormat;
@@ -12,17 +14,22 @@ import com.realtime.exception.BusinessException;
import com.realtime.mappers.GroupListMapper; import com.realtime.mappers.GroupListMapper;
import com.realtime.model.pojo.GroupList; import com.realtime.model.pojo.GroupList;
import com.realtime.model.pojo.GroupMember; import com.realtime.model.pojo.GroupMember;
import com.realtime.model.query.CloudFileListReq;
import com.realtime.model.query.GroupDetailQueryReq; import com.realtime.model.query.GroupDetailQueryReq;
import com.realtime.model.query.GroupListQueryReq; import com.realtime.model.query.GroupListQueryReq;
import com.realtime.model.remove.DisbandGroupReq; import com.realtime.model.remove.DisbandGroupReq;
import com.realtime.model.update.GroupInventUpdateReq; import com.realtime.model.update.GroupInventUpdateReq;
import com.realtime.packets.GroupPacket; import com.realtime.packets.GroupPacket;
import com.realtime.service.CloudFileService;
import com.realtime.service.GroupListService; import com.realtime.service.GroupListService;
import com.realtime.service.GroupMemberService; import com.realtime.service.GroupMemberService;
import com.realtime.service.TeamService;
import com.realtime.sysconst.Result; import com.realtime.sysconst.Result;
import com.realtime.utils.SessionUtils; import com.realtime.utils.SessionUtils;
import com.realtime.vo.CloudFileListVo;
import com.realtime.vo.GroupDetailVo; import com.realtime.vo.GroupDetailVo;
import com.realtime.vo.GroupListVo; import com.realtime.vo.GroupListVo;
import com.realtime.vo.TeamOperationVo;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -33,10 +40,7 @@ import javax.imageio.ImageIO;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.*;
import java.util.Base64;
import java.util.List;
import java.util.Objects;
@Slf4j @Slf4j
@Service @Service
@@ -46,13 +50,21 @@ public class GroupListServiceImpl extends ServiceImpl<GroupListMapper, GroupList
private final GroupMemberService groupMemberService; private final GroupMemberService groupMemberService;
private final TeamService teamService;
private final CloudFileService cloudFileService;
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void saveGroupList(GroupPacket groupPacket, Long groupId) { public void saveGroupList(GroupPacket groupPacket, Long groupId, TeamOperationVo teamOperationVo) {
GroupList groupList = new GroupList(); GroupList groupList = new GroupList();
groupList.setId(groupId); groupList.setId(groupId);
groupList.setCreator(groupPacket.getSender()); groupList.setCreator(groupPacket.getSender());
groupList.setOwner(1); groupList.setOwner(1);
Map<String,Object> teamData = (Map<String, Object>) teamOperationVo.getTeam().get("team");
groupList.setTeamId(teamData.get("id").toString());
groupList.setName(groupPacket.getGroupName()); groupList.setName(groupPacket.getGroupName());
save(groupList); save(groupList);
@@ -78,7 +90,14 @@ public class GroupListServiceImpl extends ServiceImpl<GroupListMapper, GroupList
byte[] bytes = baos.toByteArray(); byte[] bytes = baos.toByteArray();
String qrcode = "data:image/png;base64,"+ Base64.getEncoder().encodeToString(bytes); String qrcode = "data:image/png;base64,"+ Base64.getEncoder().encodeToString(bytes);
GroupDetailVo groupDetail = baseMapper.getGroupDetail(queryReq); GroupDetailVo groupDetail = baseMapper.getGroupDetail(queryReq);
CloudFileListReq cloudFileListReq = new CloudFileListReq();
cloudFileListReq.setAccessToken(queryReq.getAccessToken());
cloudFileListReq.setTeamId(groupDetail.getTeamId());
cloudFileListReq.setType("user");
cloudFileListReq.setPath("/");
CloudFileListVo cloudFileListVo = cloudFileService.listFiles(cloudFileListReq);
groupDetail.setQrCode(qrcode); groupDetail.setQrCode(qrcode);
groupDetail.setTeamFileData(cloudFileListVo);
return Result.success(groupDetail); return Result.success(groupDetail);
} }

View File

@@ -0,0 +1,278 @@
package com.realtime.service.impl;
import com.realtime.config.CloudApiConfig;
import com.realtime.exception.BusinessException;
import com.realtime.model.query.*;
import com.realtime.service.TeamService;
import com.realtime.vo.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
/**
* 团队管理服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class TeamServiceImpl implements TeamService {
private final CloudApiConfig cloudApiConfig;
private final RestTemplate restTemplate;
/**
* 获取用户可访问的团队列表
*
* @param request 团队列表查询请求
* @return 团队列表
*/
@Override
public TeamListVo getTeamList(TeamListReq request) {
try {
String url = cloudApiConfig.getBaseUrl() + "/team/list";
// 构建请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<TeamListReq> requestEntity = new HttpEntity<>(request, headers);
// 调用外部API
ResponseEntity<TeamListVo> response = restTemplate.postForEntity(
url, requestEntity, TeamListVo.class);
return response.getBody();
} catch (Exception e) {
log.error("获取团队列表失败", e);
throw new BusinessException("获取团队列表失败: " + e.getMessage());
}
}
/**
* 创建团队
*
* @param request 创建团队请求
* @return 创建结果
*/
@Override
public TeamOperationVo createTeam(TeamCreateReq request) {
try {
String url = cloudApiConfig.getBaseUrl() + "/team/create";
// 构建请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<TeamCreateReq> requestEntity = new HttpEntity<>(request, headers);
// 调用外部API
ResponseEntity<TeamOperationVo> response = restTemplate.postForEntity(
url, requestEntity, TeamOperationVo.class);
return response.getBody();
} catch (Exception e) {
log.error("创建团队失败", e);
throw new BusinessException("创建团队失败: " + e.getMessage());
}
}
/**
* 删除团队
*
* @param request 删除团队请求
* @return 删除结果
*/
@Override
public TeamOperationVo deleteTeam(TeamDeleteReq request) {
try {
String url = cloudApiConfig.getBaseUrl() + "/team/delete";
// 构建请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<TeamDeleteReq> requestEntity = new HttpEntity<>(request, headers);
// 调用外部API
ResponseEntity<TeamOperationVo> response = restTemplate.postForEntity(
url, requestEntity, TeamOperationVo.class);
return response.getBody();
} catch (Exception e) {
log.error("删除团队失败", e);
throw new BusinessException("删除团队失败: " + e.getMessage());
}
}
/**
* 修改团队信息
*
* @param request 修改团队请求
* @return 修改结果
*/
@Override
public TeamOperationVo updateTeam(TeamUpdateReq request) {
try {
String url = cloudApiConfig.getBaseUrl() + "/team/update";
// 构建请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<TeamUpdateReq> requestEntity = new HttpEntity<>(request, headers);
// 调用外部API
ResponseEntity<TeamOperationVo> response = restTemplate.postForEntity(
url, requestEntity, TeamOperationVo.class);
return response.getBody();
} catch (Exception e) {
log.error("修改团队信息失败", e);
throw new BusinessException("修改团队信息失败: " + e.getMessage());
}
}
/**
* 查看全部成员
*
* @param request 查看成员请求
* @return 成员列表
*/
@Override
public TeamMemberListVo getAllMembers(TeamGetAllMemberReq request) {
try {
String url = cloudApiConfig.getBaseUrl() + "/team/get_all_member";
// 构建请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<TeamGetAllMemberReq> requestEntity = new HttpEntity<>(request, headers);
// 调用外部API
ResponseEntity<TeamMemberListVo> response = restTemplate.postForEntity(
url, requestEntity, TeamMemberListVo.class);
return response.getBody();
} catch (Exception e) {
log.error("查看团队成员失败", e);
throw new BusinessException("查看团队成员失败: " + e.getMessage());
}
}
/**
* 批量添加团队成员
*
* @param request 添加成员请求
* @return 添加结果
*/
@Override
public TeamOperationVo addMembers(TeamAddMemberReq request) {
try {
String url = cloudApiConfig.getBaseUrl() + "/team/add_member";
// 构建请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<TeamAddMemberReq> requestEntity = new HttpEntity<>(request, headers);
// 调用外部API
ResponseEntity<TeamOperationVo> response = restTemplate.postForEntity(
url, requestEntity, TeamOperationVo.class);
return response.getBody();
} catch (Exception e) {
log.error("添加团队成员失败", e);
throw new BusinessException("添加团队成员失败: " + e.getMessage());
}
}
/**
* 批量删除团队成员
*
* @param request 删除成员请求
* @return 删除结果
*/
@Override
public TeamOperationVo removeMembers(TeamRemoveMemberReq request) {
try {
String url = cloudApiConfig.getBaseUrl() + "/team/remove_member";
// 构建请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<TeamRemoveMemberReq> requestEntity = new HttpEntity<>(request, headers);
// 调用外部API
ResponseEntity<TeamOperationVo> response = restTemplate.postForEntity(
url, requestEntity, TeamOperationVo.class);
return response.getBody();
} catch (Exception e) {
log.error("删除团队成员失败", e);
throw new BusinessException("删除团队成员失败: " + e.getMessage());
}
}
/**
* 调整团队成员身份
*
* @param request 调整身份请求
* @return 调整结果
*/
@Override
public TeamOperationVo adjustMemberRole(TeamAdjustMemberRoleReq request) {
try {
String url = cloudApiConfig.getBaseUrl() + "/team/adjust_member_role";
// 构建请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<TeamAdjustMemberRoleReq> requestEntity = new HttpEntity<>(request, headers);
// 调用外部API
ResponseEntity<TeamOperationVo> response = restTemplate.postForEntity(
url, requestEntity, TeamOperationVo.class);
return response.getBody();
} catch (Exception e) {
log.error("调整团队成员身份失败", e);
throw new BusinessException("调整团队成员身份失败: " + e.getMessage());
}
}
/**
* 获取部门结构
*
* @param request 获取部门结构请求
* @return 部门结构
*/
@Override
public TeamDepartmentVo getDepartment(TeamGetDepartmentReq request) {
try {
String url = cloudApiConfig.getBaseUrl() + "/team/get_department";
// 构建请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<TeamGetDepartmentReq> requestEntity = new HttpEntity<>(request, headers);
// 调用外部API
ResponseEntity<TeamDepartmentVo> response = restTemplate.postForEntity(
url, requestEntity, TeamDepartmentVo.class);
return response.getBody();
} catch (Exception e) {
log.error("获取部门结构失败", e);
throw new BusinessException("获取部门结构失败: " + e.getMessage());
}
}
}

View File

@@ -3,11 +3,13 @@ package com.realtime.utils;
import com.realtime.packets.basePackets.BasePackets; import com.realtime.packets.basePackets.BasePackets;
import io.netty.channel.Channel; import io.netty.channel.Channel;
import io.netty.channel.group.ChannelGroup; import io.netty.channel.group.ChannelGroup;
import lombok.extern.slf4j.Slf4j;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@Slf4j
public class SessionUtils { public class SessionUtils {
public static final Map<String, Channel> userIdChannelMap = new ConcurrentHashMap<>(); public static final Map<String, Channel> userIdChannelMap = new ConcurrentHashMap<>();
@@ -27,7 +29,22 @@ public class SessionUtils {
public static void unbind(Channel channel) { public static void unbind(Channel channel) {
if (hasLogin(channel)) { if (hasLogin(channel)) {
userIdChannelMap.remove(getUser(channel).getSender()); BasePackets user = getUser(channel);
if (user != null && user.getSender() != null) {
log.info("清理用户会话: userId={}, channel={}", user.getSender(), channel.id().asShortText());
// 从用户-通道映射中移除
userIdChannelMap.remove(user.getSender());
// 从会话用户映射中移除
sessionUser.remove(user.getSender());
// 从所有群组中移除该通道
groupIdChannelGroupMap.values().forEach(channelGroup -> {
if (channelGroup != null && channelGroup.contains(channel)) {
channelGroup.remove(channel);
log.debug("从群组中移除通道: {}", channel.id().asShortText());
}
});
}
// 清除通道属性
channel.attr(Attributes.SESSION).set(null); channel.attr(Attributes.SESSION).set(null);
} }
} }

View File

@@ -0,0 +1,29 @@
package com.realtime.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 云端文件下载响应VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CloudFileDownloadVo implements Serializable {
private Boolean success;
private DownloadData data;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class DownloadData implements Serializable {
@JsonProperty("download_url")
private String downloadUrl;
}
}

View File

@@ -0,0 +1,24 @@
package com.realtime.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* 云端文件列表响应VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CloudFileListVo implements Serializable {
private Boolean success;
private List<CloudFileVo> data;
private Integer count;
}

View File

@@ -0,0 +1,22 @@
package com.realtime.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 云端文件操作响应VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CloudFileOperationVo implements Serializable {
private Boolean success;
private String message;
private String error;
}

View File

@@ -0,0 +1,49 @@
package com.realtime.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* 云端文件VO
*/
@Data
public class CloudFileVo implements Serializable {
private String id;
private String name;
private String path;
@JsonProperty("full_path")
private String fullPath;
private Long size;
private String type;
@JsonProperty("owner_id")
private String ownerId;
@JsonProperty("owner_type")
private String ownerType;
@JsonProperty("updated_id")
private String updatedId;
@JsonProperty("created_at")
private LocalDateTime createdAt;
@JsonProperty("updated_at")
private LocalDateTime updatedAt;
/**
* 子文件/文件夹列表
*/
private List<CloudFileVo> children = new ArrayList<>();
}

View File

@@ -2,6 +2,8 @@ package com.realtime.vo;
import lombok.Data; import lombok.Data;
import java.util.Map;
@Data @Data
public class GroupDetailVo { public class GroupDetailVo {
private Long id; private Long id;
@@ -10,4 +12,7 @@ public class GroupDetailVo {
private Integer isInvent; private Integer isInvent;
private String creator; private String creator;
private String qrCode; private String qrCode;
private String teamId;
private CloudFileListVo teamFileData;
private String teamInfo;
} }

View File

@@ -0,0 +1,20 @@
package com.realtime.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.Map;
/**
* 部门结构响应VO
*/
@Data
public class TeamDepartmentVo implements Serializable {
private Boolean success;
/**
* 部门结构数据
*/
private Map<String, Object> data;
}

View File

@@ -0,0 +1,21 @@
package com.realtime.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 团队列表响应VO
*/
@Data
public class TeamListVo implements Serializable {
private Boolean success;
private List<TeamVo> teams;
@JsonProperty("total_count")
private Integer totalCount;
}

View File

@@ -0,0 +1,21 @@
package com.realtime.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
/**
* 团队成员列表响应VO
*/
@Data
public class TeamMemberListVo implements Serializable {
private Boolean success;
/**
* 嵌套的团队成员数据
*/
private Map<String, Object> team;
}

View File

@@ -0,0 +1,34 @@
package com.realtime.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 团队成员用户信息VO
*/
@Data
public class TeamMemberUserVo implements Serializable {
@JsonProperty("_id")
private String id;
private String username;
private String email;
@JsonProperty("is_beta")
private Boolean isBeta;
@JsonProperty("trial_end_date")
private String trialEndDate;
@JsonProperty("trial_start_date")
private String trialStartDate;
@JsonProperty("lastLogin")
private String lastLogin;
private String role;
}

View File

@@ -0,0 +1,38 @@
package com.realtime.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 团队成员VO
*/
@Data
public class TeamMemberVo implements Serializable {
@JsonProperty("_id")
private String id;
@JsonProperty("team_id")
private String teamId;
@JsonProperty("user_id")
private String userId;
/**
* 角色admin、member
*/
private String role;
@JsonProperty("created_at")
private String createdAt;
@JsonProperty("updated_at")
private String updatedAt;
/**
* 用户信息
*/
private TeamMemberUserVo user;
}

View File

@@ -0,0 +1,22 @@
package com.realtime.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.Map;
/**
* 团队操作响应VO
*/
@Data
public class TeamOperationVo implements Serializable {
private Boolean success;
private String message;
/**
* 嵌套的团队数据
*/
private Map<String, Object> team;
}

View File

@@ -0,0 +1,31 @@
package com.realtime.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 团队VO
*/
@Data
public class TeamVo implements Serializable {
@JsonProperty("_id")
private String id;
private String name;
private String description;
@JsonProperty("created_at")
private String createdAt;
@JsonProperty("updated_at")
private String updatedAt;
/**
* 用户在该团队的角色admin、member
*/
private String role;
}

View File

@@ -6,7 +6,7 @@ minio:
endpoint: https://database.yuxindazhineng.com endpoint: https://database.yuxindazhineng.com
access-key: yuxinda_admin access-key: yuxinda_admin
secret-key: yuxinda_admin01 secret-key: yuxinda_admin01
bucket-name: real bucket-name: team-bucket
file: file:
picMaxSize: 104857600 # 100MB picMaxSize: 104857600 # 100MB
picMaxCount: 1 picMaxCount: 1
@@ -22,7 +22,7 @@ spring:
url: jdbc:mysql://8.134.75.237:3309/real_time?serverTimezone=Asia/Shanghai url: jdbc:mysql://8.134.75.237:3309/real_time?serverTimezone=Asia/Shanghai
username: root username: root
password: zhengfei_2024 password: zhengfei_2024
type: com.alibaba.druid.pool.DruidDataSource type: com.zaxxer.hikari.HikariDataSource
mybatis-plus: mybatis-plus:
configuration: configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

View File

@@ -9,7 +9,7 @@
id id
,name,creator, ,name,creator,
created_time,picture,type, created_time,picture,type,
is_invent is_invent,team_id
</sql> </sql>
<select id="getGroup" resultType="groupListVo"> <select id="getGroup" resultType="groupListVo">