提交之GitHub

This commit is contained in:
2026-02-08 23:58:00 +08:00
commit b4f25e99b1
43 changed files with 7926 additions and 0 deletions
+119
View File
@@ -0,0 +1,119 @@
package client;
import com.formdev.flatlaf.FlatLightLaf;
import client.service.*;
import client.view.LoginPage;
import global.global;
import server.serveice.Wrapper;
import javax.swing.*;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class Client {
private static boolean isConnected = false; // 是否连接成功
// 接收信息线程
private ChatReceiver chatReceiver;
// 发送信息线程
private ChatSender chatSender;
// 信息处理队列
private BlockingQueue<Wrapper> messageQueue;
// 用于分配动态端口的计数器
private static int dynamicPortCounter = 10000;
private static final int MAX_DYNAMIC_PORT = 20000;
// 程序入口
public static void main(String[] args) {
// 设置 FlatLaf 主题,使界面更现代化,符合当前审美
try {
FlatLightLaf.setup();
} catch (Exception e) {
System.err.println("Failed to initialize FlatLaf");
}
Client client = new Client();
client.startClient();
System.out.println("客户端启动成功,本地端口:" +
(dynamicPortCounter > 10000 ? dynamicPortCounter - 1 : "未分配"));
SwingUtilities.invokeLater(() -> {
LoginPage.get().setVisible(true);
});
}
// 启动客户端,连接服务端,初始化相关内容
public void startClient() {
messageQueue = new ArrayBlockingQueue<>(40);
// 连接服务器 创建 clientSocket
try {
int localPort = findAvailablePort(getNextDynamicPort());
Socket socket;
if (localPort > 0) {
socket = new Socket(
global.LOCAL_HOST,
global.SERVER_PORT,
null,
localPort);
} else {
socket = new Socket(
global.LOCAL_HOST,
global.SERVER_PORT);
}
chatSender = new ChatSender(socket, messageQueue);
chatReceiver = new ChatReceiver(socket, messageQueue);
chatSender.start();
Thread.sleep(200);
chatReceiver.start();
isConnected = true;
} catch (IOException e) {
isConnected = false;
e.printStackTrace();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 获取动态分配的本地端口
private synchronized int getNextDynamicPort() {
int port = dynamicPortCounter;
dynamicPortCounter++;
// 如果超出范围,重置到起始端口
if (dynamicPortCounter >= MAX_DYNAMIC_PORT) {
dynamicPortCounter = 10000;
}
return port;
}
// 寻找可用端口
private int findAvailablePort(int startPort) {
int port = startPort;
while (port < MAX_DYNAMIC_PORT) {
try (ServerSocket serverSocket = new ServerSocket(port)) {
// 如果能成功创建ServerSocket,说明端口可用
serverSocket.close();
return port;
} catch (IOException e) {
// 端口被占用,尝试下一个
port++;
}
}
// 如果没找到可用端口,使用0让系统自动分配
return 0;
}
public static boolean isConnected() {
return isConnected;
}
}
+480
View File
@@ -0,0 +1,480 @@
package client.service;
import client.view.LoginPage;
import client.view.MainPage;
import client.view.main.*;
import global.global;
import server.data.GroupData;
import server.data.UserData;
import server.serveice.Wrapper;
import util.MsgUtil;
import javax.swing.*;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
/**
* 客户端消息接收服务线程
* <p>
* 该类负责维护与服务端的长连接,持续监听并接收服务端推送的消息包(Wrapper)。
* 作为客户端的“下行数据通道”,它将接收到的原始数据根据操作码(Operation Code)进行解析和分发,
* 驱动本地数据更新(LocalData)及 UI 界面刷新(UIUpdate)。
* <p>
* 核心功能包括:
* 1. 登录/注册响应处理
* 2. 实时聊天消息接收与存储
* 3. 群组创建、邀请及变更通知
* 4. 异常捕获与日志记录
*/
public class ChatReceiver extends Thread {
// 客户端本地Socket,用于与服务端通信
private final Socket localSocket;
ObjectInputStream ois;
private static boolean isRunning;
/**
* 构造函数
* <p>
* 初始化接收线程,绑定到已连接的客户端Socket。
* 创建对象输入流,用于从Socket读取服务端消息。
*
* @param localSocket 已连接的客户端Socket
* @param messageQueue 消息处理队列
* @throws IOException 如果获取输入流失败
*/
public ChatReceiver(Socket localSocket, BlockingQueue<Wrapper> messageQueue) {
this.localSocket = localSocket;
isRunning = true;
}
/**
* 线程主循环:持续阻塞读取服务端消息并分发处理
*/
@Override
public void run() {
try {
ois = new ObjectInputStream(localSocket.getInputStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
while (isRunning) {
try {
// 阻塞式读取服务端发送的 Wrapper 对象
Wrapper message = (Wrapper) ois.readObject();
System.out.println("收到消息:" + message.getOperation());
handleMessage(message);
}
// 捕获 IO 异常(如连接中断)
catch (IOException e) {
e.printStackTrace();
// 连接中断,退出循环
break;
}
// 捕获反序列化异常
catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
public static void stopRunning() {
ChatReceiver.isRunning = false;
}
/**
* 消息处理核心方法:根据操作码分发业务逻辑
* 1. 登录/注册响应处理
* 2. 实时聊天消息接收与存储
* 3. 群组创建、邀请及变更通知
* 4. 异常捕获与日志记录
* 5. 服务器关闭通知
* 6. 好友添加响应处理
* 7. 好友删除响应处理
* 8. 群组解散响应处理
* 9. 好友添加请求处理
* 10. 好友删除请求处理
* 11. 群组解散请求处理
* 12. 好友添加响应处理
* 13. 好友删除响应处理
* 14. 群组解散响应处理
*
* @param msg 服务端下发的消息包
*/
@SuppressWarnings("unchecked")
private void handleMessage(Wrapper msg) {
int opt = msg.getOperation();
switch (opt) {
case global.OPT_QUEST_WRONG:
// 通用异常响应:直接展示服务端返回的错误信息
System.out.println("出现未知异常");
break;
case global.OPT_REGISTER_FAILED_ACC:
LoginPage.get().showMsgDialog("账号已存在");
break;
case global.OPT_REGISTER_SUCCESS:
case global.OPT_LOGIN_SUCCESS:
System.out.println("登录/注册成功");
LoginPage.get().openMainPage();
ChatSender.addMsg(Wrapper.initRequest(LocalData.get().getId(), global.OPT_INIT_USER));
// 新增:请求初始化所有用户详细信息
ChatSender.addMsg(Wrapper.simpleRequest(LocalData.get().getId(), null, global.OPT_INIT_USER_DETAIL));
ChatSender.addMsg(Wrapper.initRequest(LocalData.get().getId(), global.OPT_INIT_GROUP));
ChatSender.addMsg(Wrapper.initRequest(LocalData.get().getId(), global.OPT_INIT_CHAT));
break;
case global.OPT_LOGIN_FAILED_ACC:
LoginPage.get().showMsgDialog("账号不存在");
break;
case global.OPT_LOGIN_FAILED_PWD:
LoginPage.get().showMsgDialog("密码错误");
break;
case global.OPT_LOGIN_FAILED_REPEATED:
LoginPage.get().showMsgDialog("账户已登录,请勿重复登录");
break;
case global.OPT_ERROR_NOT_LOGIN:
System.out.println("未知的登录问题?");
break;
case global.OPT_LOGOUT:
MainPage.get().openLogInPage();
break;
case global.OPT_DELETE_ACCOUNT:
MainPage.get().showMsgDialog("账号已被删除");
MainPage.get().openLogInPage();
break;
case global.OPT_UPDATE_NICKNAME:
LocalData.get().setUserName(msg.getSenderId(), (String) msg.getData());
break;
case global.OPT_UPDATE_PASSWORD:
MainPage.get().showMsgDialog("密码已经更新");
break;
case global.OPT_USER_UPDATE_NAME_FAILED:
MainPage.get().showMsgDialog("昵称修改失败");
break;
case global.OPT_USER_UPDATE_PASSWORD_FAILED:
MainPage.get().showMsgDialog("密码修改失败");
break;
case global.OPT_GROUP_CREATE_SUCCESS:
MainPage.get().showMsgDialog("群组创建成功");
break;
case global.OPT_GROUP_INVITE:
// 群组变更(新建/被拉入):更新本地群组列表并刷新左侧导航栏
MainPage.get().showGroupInviteRequestDialog(
msg.getSenderId(),
LocalData.get().getUserName(msg.getSenderId()),
(String) msg.getData(),
msg.getGroupId());
break;
case global.OPT_GROUP_INVITE_REFUSE:
MainPage.get().showMsgDialog("对方拒绝了你的邀请");
break;
case global.OPT_GROUP_INVITE_OFFLINE:
MainPage.get().showMsgDialog("邀请的用户并不在线");
break;
case global.OPT_GROUP_QUIT:
handleGroupQuit(msg);
break;
case global.OPT_GROUP_DISBAND:
MainPage.get().showMsgDialog("群聊已被解散");
break;
case global.OPT_CHAT:
handleChatRequest(msg);
break;
case global.OPT_PRIVATE_CHAT:
handlePrivateChatRequest(msg);
break;
case global.OPT_GROUP_UPDATE_NAME:
// 群信息变更:更新群名显示
LocalData.get().setGroupName(msg.getGroupId(), (String) msg.getData());
break;
case global.OPT_GROUP_UPDATE_OWNER:
LocalData.get().getGroupData(msg.getGroupId()).setGroupOwner((String) msg.getData());
break;
case global.OPT_USER_UPDATE_NAME_FAILED_WRONG_FORMAT:
MainPage.get().showMsgDialog("用户名格式错误,请重新输入");
break;
case global.OPT_GROUP_JOIN_FAILED:
MainPage.get().showMsgDialog("加入群聊失败:群组不存在");
break;
case global.OPT_FRIEND_ADD_SUCCESS:
handleFriendAddSuccess(msg);
break;
case global.OPT_FRIEND_ADD_FAILED:
MainPage.get().showMsgDialog("添加好友失败:用户不存在");
break;
case global.OPT_INIT_USER_DETAIL:
// 初始化所有用户详细信息
Map<String, UserData> userDetails = (Map<String, UserData>) msg.getData();
LocalData.get().setUserDetails(userDetails);
break;
case global.OPT_UPDATE_USER_DETAIL:
// 更新单个用户详细信息
UserData updatedUser = (UserData) msg.getData();
LocalData.get().updateUserDetails(updatedUser);
// 刷新界面(如果当前正显示该用户的资料)
// 由于目前没有全局事件总线,这里简单起见,可以刷新整个 MainView 或者依赖用户重新点击
// 实际上,如果正在查看好友资料,可能需要实时刷新。
// 简单实现:不主动刷新UI,下次进入界面时读取最新数据即可。
// 如果在“设置-个人信息”界面,且更新的是自己,界面通常已经由用户操作更新了,或者可以刷新一下。
break;
case global.OPT_EXIT:
// 服务器关闭通知
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(MainPage.get(), "服务器已关闭,程序将退出。", "系统通知", JOptionPane.WARNING_MESSAGE);
System.exit(0);
});
break;
case global.OPT_FRIEND_ADD:
handleFriendAddRequest(msg);
break;
case global.OPT_FRIEND_ADD_REFUSE:
MainPage.get().showMsgDialog("对方拒绝了你的好友请求");
break;
case global.OPT_INIT_CHAT:
handleChatInit(msg);
break;
case global.OPT_INIT_USER:
handleUserInit(msg);
break;
case global.OPT_INIT_GROUP:
handleGroupInit(msg);
break;
case global.SERVER_MESSAGE:
MainPage.get().showMsgDialog((String) msg.getData());
}
}
/**
* 处理收到的好友请求
* 弹出对话框询问用户是否同意
*/
private void handleFriendAddRequest(Wrapper msg) {
String senderId = msg.getSenderId();
String senderName = (String) msg.getData();
SwingUtilities.invokeLater(() -> {
int option = JOptionPane.showConfirmDialog(
MainPage.get(),
"收到来自 " + senderName + " (" + senderId + ") 的好友请求,是否同意?",
"好友请求",
JOptionPane.YES_NO_OPTION);
if (option == JOptionPane.YES_OPTION) {
ChatSender.addMsg(Wrapper.friendAddAgree(LocalData.get().getId(), senderId));
} else {
ChatSender.addMsg(Wrapper.friendAddRefuse(LocalData.get().getId(), senderId));
}
});
}
/**
* 处理好友添加成功消息
*
* @param msg 好友添加成功消息
*/
@SuppressWarnings("unchecked")
private void handleFriendAddSuccess(Wrapper msg) {
Map<String, String> friendInfo = (Map<String, String>) msg.getData();
if (friendInfo != null) {
for (Map.Entry<String, String> entry : friendInfo.entrySet()) {
LocalData.get().addFriend(entry.getKey(), entry.getValue());
}
// 刷新好友列表 UI
SwingUtilities.invokeLater(() -> {
SecondaryOptionView.get().exchangeToFriendList();
MainPage.get().showMsgDialog("添加好友成功");
});
}
}
/**
* 处理群聊退出消息
*
* @param msg 群聊退出消息
*/
private void handleGroupQuit(Wrapper msg) {
LocalData.get().removeGroupChatMsg(msg.getGroupId());
LocalData.get().setCurrentChatId(null);
SwingUtilities.invokeLater(() -> {
SecondaryOptionView.get().removeGroupListItem(msg.getGroupId());
MainPage.get().exchangeToBlankContent();
MainPage.get().revalidate();
MainPage.get().repaint();
});
}
/**
* 处理群聊初始化消息
* 初始化群聊消息记录
*
* @param msg 群聊初始化消息
*/
@SuppressWarnings("unchecked")
private void handleChatInit(Wrapper msg) {
List<String> chatHistory = (List<String>) msg.getData();
// 初始化群聊消息记录
LocalData.get().addChatMsg(msg.getGroupId(), chatHistory);
// 如果当前正好停留在该群聊界面,刷新消息
if (msg.getGroupId().equals(LocalData.get().getCurrentChatId())) {
SwingUtilities.invokeLater(() -> {
ChatInfoView.get().setChatInfo(msg.getGroupId());
});
}
}
/**
* 处理用户初始化消息
*
* @param msg 用户初始化消息
*/
@SuppressWarnings("unchecked")
private void handleUserInit(Wrapper msg) {
Map<String, String> groupMap = (Map<String, String>) msg.getData();
for (Map.Entry<String, String> entry : groupMap.entrySet()) {
LocalData.get().addUserId_name(entry.getKey(), entry.getValue());
}
}
/**
* 处理群聊初始化消息
*
* @param msg 群聊初始化消息
*/
private void handleGroupInit(Wrapper msg) {
GroupData newGroupData = (GroupData) msg.getData();
// 打印群聊初始化信息
System.out.println(
"handleGroupInit: " + newGroupData.getGroupId() + ", members: " + newGroupData.getMembers().size());
// 添加群聊数据到本地存储
LocalData.get().addGroup(newGroupData.getGroupId(), newGroupData);
// 更新群聊列表 UI
SecondaryOptionView.get().updateGroupList(
newGroupData.getGroupId(),
newGroupData.getGroupName(),
0);
// 如果当前处于群聊模式,刷新群聊列表以显示新加入的群组
SecondaryOptionView.get().refreshIfInGroupMode();
// 更新当前群组信息(如果当前正打开该群)
String currentChatId = LocalData.get().getCurrentChatId();
// SecondaryOptionView.get().updateGroupList(newGroupData.getGroupId(),
// newGroupData.getGroupName(), 0);
// 检查是否需要更新当前显示的群组信息(如果当前正打开该群)
if (newGroupData.getGroupId().equals(currentChatId)) {
System.out.println("Updating GroupInfoView...");
SwingUtilities.invokeLater(() -> {
GroupInfoView.get().updateInfo();
});
}
}
/**
* 处理群聊请求消息
*
* @param msg 群聊请求消息
*/
private void handleChatRequest(Wrapper msg) {
String content = (String) msg.getData();
String[] split = MsgUtil.splitMsg(content);
LocalData.get().addChatMsg(msg.getGroupId(), content);
// 如果就在当前这个面板,立即更新面板
if (LocalData.get().getCurrentChatId().equals(msg.getGroupId())) {
SwingUtilities.invokeLater(() -> {
ChatInfoView.get().addOtherUserMessage(split[1], split[2]);
});
} else {
SecondaryOptionView.get().updateGroupList(
msg.getGroupId(),
LocalData.get().getGroupName(msg.getGroupId()),
1);
}
}
/**
* 处理私聊请求消息
*/
private void handlePrivateChatRequest(Wrapper msg) {
String senderId = msg.getSenderId();
String text = (String) msg.getData();
// 获取发送者名称
String senderName = LocalData.get().getFriends().get(senderId);
if (senderName == null) {
senderName = LocalData.get().getUserName(senderId);
if (senderName == null)
senderName = senderId;
}
// 组合成标准消息格式: id|name|text
String combinedMsg = MsgUtil.combineMsg(senderId, senderName, text);
// 存储消息,使用senderId作为chatId
LocalData.get().addChatMsg(senderId, combinedMsg);
// 如果就在当前这个面板,立即更新面板
if (senderId.equals(LocalData.get().getCurrentChatId())) {
String finalSenderName = senderName;
SwingUtilities.invokeLater(() -> {
ChatInfoView.get().addOtherUserMessage(finalSenderName, text);
});
} else {
// 更新消息列表
SecondaryOptionView.get().updateMessageList(
senderId,
senderName,
text,
1);
}
}
}
+80
View File
@@ -0,0 +1,80 @@
package client.service;
import client.view.MainPage;
import server.serveice.Wrapper;
import javax.swing.*;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.util.concurrent.BlockingQueue;
public class ChatSender extends Thread {
private static BlockingQueue<Wrapper> messageQueue;
public static void addMsg(Wrapper msg) {
messageQueue.add(msg);
}
private ObjectOutputStream oos;
private final Socket clientSocket;
private static boolean isRunning;
public static void stopRunning() {
isRunning = false;
}
public ChatSender(Socket clientSocket, BlockingQueue<Wrapper> messageQueue) {
ChatSender.messageQueue = messageQueue;
this.clientSocket = clientSocket;
isRunning = true;
}
@Override
public void run() {
try {
oos = new ObjectOutputStream(clientSocket.getOutputStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
Wrapper msg = null;
while (isRunning) {
try {
msg = messageQueue.take();
oos.writeObject(msg);
oos.flush();
System.out.println("信息已发出:" + msg.getOperation());
} catch (InterruptedException e) {
System.out.println(LocalData.get().getId() + ": 消息队列被中断");
break;
} catch (IOException e) {
if (isConnectionClosed(e)) {
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(MainPage.get(), "服务器连接已断开,请重新登录。", "连接断开",
JOptionPane.ERROR_MESSAGE);
System.exit(0);
});
System.out.println("服务器连接已断开");
break;
} else {
System.out.println("发送消息时发生IO异常: " + e.getMessage());
e.printStackTrace();
}
}
}
}
private boolean isConnectionClosed(IOException e) {
// 根据异常类型判断连接是否断开
return e instanceof SocketException ||
e.getMessage() != null && (e.getMessage().contains("Connection reset") ||
e.getMessage().contains("Broken pipe") ||
e.getMessage().contains("Connection refused") ||
e.getMessage().contains("Software caused connection abort"));
}
}
+155
View File
@@ -0,0 +1,155 @@
package client.service;
import server.data.GroupData;
import server.data.UserData;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
// 这个类用于存储本地的数据,便于之后的UI更新操作。
public class LocalData {
private static final LocalData INSTANCE = new LocalData();
public static LocalData get() {
return INSTANCE;
}
private String id;
// 当前的所在的群聊id,辅助UI更新
private String currentChatId;
// 使用 ConcurrentHashMap 替代 HashMap
private ConcurrentMap<String, String> userIdNameMap;
private ConcurrentMap<String, GroupData> groupDataMap;
private ConcurrentMap<String, List<String>> groupChatMap;
// 好友列表
private ConcurrentMap<String, String> friendMap;
// 存储所有用户的详细信息
private ConcurrentMap<String, UserData> userDetailsMap;
private LocalData() {
userIdNameMap = new ConcurrentHashMap<>();
groupDataMap = new ConcurrentHashMap<>();
groupChatMap = new ConcurrentHashMap<>();
friendMap = new ConcurrentHashMap<>();
userDetailsMap = new ConcurrentHashMap<>();
id = "";
currentChatId = "";
}
public Map<String, String> getFriends() {
return friendMap;
}
public void addFriend(String id, String name) {
friendMap.put(id, name);
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getCurrentChatName() {
return groupDataMap.get(currentChatId).getGroupName();
}
public synchronized void setCurrentChatId(String id) {
this.currentChatId = id;
}
public synchronized String getCurrentChatId() {
return currentChatId;
}
public void addUserId_name(String id, String name) {
userIdNameMap.put(id, name);
}
public String getGroupName(String groupId) {
return groupDataMap.get(groupId).getGroupName();
}
public void addChatMsg(String groupId, List<String> messages) {
// 覆盖模式:用于初始化加载历史消息,避免重复添加
groupChatMap.put(groupId, new ArrayList<>(messages));
}
public void addChatMsg(String groupId, String message) {
if (groupChatMap.containsKey(groupId)) {
groupChatMap.get(groupId).add(message);
} else {
groupChatMap.put(groupId, new ArrayList<>());
groupChatMap.get(groupId).add(message);
}
}
public List<String> getChatMsg(String groupId) {
if (groupChatMap.containsKey(groupId)) {
return groupChatMap.get(groupId);
} else {
return new ArrayList<>();
}
}
public void setUserDetails(Map<String, UserData> userDetails) {
userDetailsMap.clear();
userDetailsMap.putAll(userDetails);
// 同时更新 userIdNameMap,确保一致性
userDetails.forEach((id, userData) -> {
userIdNameMap.put(id, userData.getNickname());
});
}
public void updateUserDetails(UserData userData) {
userDetailsMap.put(userData.getUserId(), userData);
userIdNameMap.put(userData.getUserId(), userData.getNickname());
}
public UserData getUserDetail(String userId) {
return userDetailsMap.get(userId);
}
public void removeGroupChatMsg(String groupId) {
if (groupChatMap.containsKey(groupId)) {
groupChatMap.remove(groupId);
}
}
public String getUserName(String userId) {
if (userIdNameMap.containsKey(userId))
return userIdNameMap.get(userId);
else
return userId;
}
public void setUserName(String suerId, String name) {
userIdNameMap.put(suerId, name);
}
public synchronized void addGroup(String groupId, GroupData groupData) {
groupDataMap.put(groupId, groupData); // HashMap的put方法会自动替换已有的键值对
}
public void setGroupName(String groupId, String name) {
groupDataMap.get(groupId).setGroupName(name);
}
public GroupData getGroupData(String groupId) {
return groupDataMap.get(groupId);
}
public List<GroupData> getAllGroups() {
return new ArrayList<>(groupDataMap.values());
}
}
+115
View File
@@ -0,0 +1,115 @@
package client.view;
import client.Client;
import client.service.ChatSender;
import client.service.LocalData;
import client.view.login.*;
import client.view.util.DesignToken;
import server.serveice.Wrapper;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
/**
* 登录页面,用于完成用户账户登录的功能,作为一个独立的页面,登录完成之后将自动关闭,并开启主界面
*/
public class LoginPage extends JFrame {
private static volatile LoginPage INSTANCE;
// 获取登录页面实例,使用单例模式确保全局唯一性
public static LoginPage get() {
if (INSTANCE == null) {
synchronized (LoginPage.class) {
if (INSTANCE == null) {
INSTANCE = new LoginPage();
}
}
}
return INSTANCE;
}
// 交替展示两个窗口,分别用于进行注册和登录操作。
private SignInView signInView;
private SignUpView signUpView;
private LoginPage() {
setTitle("欢迎来到本地网聊天室!");
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
setSize(DesignToken.LOGIN_WIDTH, DesignToken.LOGIN_HEIGHT);
setLocationRelativeTo(null);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
// 如果链接上了,发出退出信息
if (Client.isConnected()) {
ChatSender.addMsg(Wrapper.logoutRequest(LocalData.get().getId()));
}
super.windowClosing(e);
// 如果未登录,则直接退出。
if (LocalData.get().getId().length() == 0) {
System.exit(0);
}
}
});
signInView = new SignInView(this);
signUpView = new SignUpView(this);
// 默认为登录
exchangeToSignInView();
}
// 更换到注册界面
public void exchangeToSignUpView() {
this.remove(signInView);
this.add(signUpView, BorderLayout.CENTER);
this.validate();
this.repaint();
}
// 更换到登录界面
public void exchangeToSignInView() {
this.remove(signUpView);
this.add(signInView, BorderLayout.CENTER);
this.validate();
this.repaint();
}
// 打开主界面,并关闭本界面
public void openMainPage() {
// 创建新窗口
MainPage.get().setVisible(true);
// 关闭当前窗口
this.dispose();
}
public void showMsgDialog(String text) {
JDialog inviteDialog = new JDialog(this, "信息", true);
inviteDialog.setSize(300, 200);
inviteDialog.setLocationRelativeTo(this);
// 设置对话框内容
String htmlText = "<html><body style='width: 210px; padding: 10px;'>" + text + "</body></html>";
JLabel label = new JLabel(htmlText, SwingConstants.CENTER);
JButton closeBtn = new JButton("确定");
closeBtn.addActionListener(e -> inviteDialog.dispose());
JPanel panel = new JPanel(new BorderLayout());
JPanel centerPanel = new JPanel(new FlowLayout());
centerPanel.add(label);
panel.add(centerPanel, BorderLayout.CENTER);
panel.add(closeBtn, BorderLayout.SOUTH);
inviteDialog.add(panel);
// 显示对话框(会阻塞主窗口交互)
inviteDialog.setVisible(true);
}
}
+503
View File
@@ -0,0 +1,503 @@
package client.view;
import client.Client;
import client.service.ChatSender;
import client.service.LocalData;
import client.view.main.*;
import global.global;
import server.serveice.Wrapper;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import static client.view.util.DesignToken.*;
/**
* 主界面。
*/
public class MainPage extends JFrame {
private volatile static MainPage instance;
// 获取主界面
public static MainPage get() {
if (instance == null) {
synchronized (LoginPage.class) {
if (instance == null) {
instance = new MainPage();
}
}
}
return instance;
}
// 可以左右的移动大小的分割界面
private final JSplitPane splitPane;
private final SideOptionView sideOptionView;
private final SecondaryOptionView secondaryOptionView;
private final ContentView contentView;
/**
* 构造函数
* 初始化主界面组件,包括侧边栏、二级选项栏、详细内容区域等。
* 设置主界面的标题、关闭操作、大小、位置等属性。
* 同时添加窗口关闭监听器,在窗口关闭时发送登出请求。
*/
private MainPage() {
Dimension size = new Dimension(WINDOW_ORI_WIDTH, WINDOW_ORI_HEIGHT);
setTitle("本地网络聊天室");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(size);
setLocationRelativeTo(null);
// 侧边栏
sideOptionView = new SideOptionView();
sideOptionView.setMinimumSize(new Dimension(SIDE_PANEL_WIDTH, WINDOW_ORI_HEIGHT));
sideOptionView.setMaximumSize(new Dimension(SIDE_PANEL_WIDTH, Integer.MAX_VALUE));
sideOptionView.setPreferredSize(new Dimension(SIDE_PANEL_WIDTH, WINDOW_ORI_HEIGHT));
// 二级选项栏
secondaryOptionView = SecondaryOptionView.get();
// 详细内容
contentView = new ContentView();
splitPane = new JSplitPane(
JSplitPane.HORIZONTAL_SPLIT,
secondaryOptionView,
contentView);
splitPane.setDividerSize(2);
splitPane.setOneTouchExpandable(true);
splitPane.setResizeWeight(0.5);
splitPane.setContinuousLayout(true);
splitPane.setMinimumSize(new Dimension(SECONDARY_PANEL_WIDTH_MIN + CONTENT_PANEL_WIDTH_MIN, size.height));
splitPane.setPreferredSize(new Dimension(SECONDARY_PANEL_WIDTH_MIN + CONTENT_PANEL_WIDTH_MIN, size.height));
addDividerConstraintListener();
this.add(sideOptionView, BorderLayout.WEST);
this.add(splitPane, BorderLayout.CENTER);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
// 如果没有连接上,则直接退出
if (Client.isConnected()) {
ChatSender.addMsg(Wrapper.logoutRequest(LocalData.get().getId()));
}
// 否则,窗口关闭的时候发送登出信息
super.windowClosing(e);
}
});
// 发送初始化请求
ChatSender.addMsg(Wrapper.initRequest(LocalData.get().getId(), global.OPT_INIT_USER));
ChatSender.addMsg(Wrapper.initRequest(LocalData.get().getId(), global.OPT_INIT_GROUP));
ChatSender.addMsg(Wrapper.initRequest(LocalData.get().getId(), global.OPT_INIT_CHAT));
}
/**
* 添加分隔条约束监听器
* 监听分隔条位置变化事件,确保分隔条不会超出最小和最大允许位置范围。
* 当窗口大小改变或分隔条位置改变时,调用constrainDividerLocation方法重新限制分隔条位置。
*/
private void addDividerConstraintListener() {
splitPane.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
// 窗口大小改变时重新计算限制
constrainDividerLocation();
}
});
splitPane.getLeftComponent().addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
constrainDividerLocation();
}
});
}
/**
* 限制分隔条位置
* 确保分隔条不会超出最小和最大允许位置范围。
* 如果当前位置小于最小位置,将分隔条位置设置为最小位置。
* 如果当前位置大于最大位置,将分隔条位置设置为最大位置。
*/
private void constrainDividerLocation() {
int totalWidth = splitPane.getWidth();
int dividerSize = splitPane.getDividerSize();
int currentLocation = splitPane.getDividerLocation();
// 计算有效位置范围
int minLocation = SECONDARY_PANEL_WIDTH_MIN;
int maxLocation = totalWidth - dividerSize - SECONDARY_PANEL_WIDTH_MIN;
// 限制分隔条位置
if (currentLocation < minLocation) {
splitPane.setDividerLocation(minLocation);
} else if (currentLocation > maxLocation) {
splitPane.setDividerLocation(maxLocation);
}
}
/**
* 切换到设置界面,当前还没做具体实现
*/
public void exchangeToSettingPage() {
}
/**
* 显示消息对话框
* 创建一个对话框,显示text内容。
* 对话框标题为"信息",大小为300x200,居中显示在主窗口上。
* 对话框内容为text,居中对齐,宽度为210px padding为10px。
* 对话框包含一个确定按钮,点击后关闭对话框。
*/
public void showMsgDialog(String text) {
JDialog inviteDialog = new JDialog(MainPage.get(), "信息", true);
inviteDialog.setSize(300, 200);
inviteDialog.setLocationRelativeTo(MainPage.get());
// 设置对话框内容
String htmlText = "<html><body style='width: 210px; padding: 10px;'>" + text + "</body></html>";
JLabel label = new JLabel(htmlText, SwingConstants.CENTER);
JButton closeBtn = new JButton("确定");
closeBtn.addActionListener(e -> inviteDialog.dispose());
JPanel panel = new JPanel(new BorderLayout());
JPanel centerPanel = new JPanel(new FlowLayout());
centerPanel.add(label);
panel.add(centerPanel, BorderLayout.CENTER);
panel.add(closeBtn, BorderLayout.SOUTH);
inviteDialog.add(panel);
// 显示对话框(会阻塞主窗口交互)
inviteDialog.setVisible(true);
}
/**
* 切换到登录界面
* 隐藏当前主窗口,显示登录界面。
*/
public void openLogInPage() {
LoginPage.get().setVisible(true);
this.dispose();
}
/**
* 切换到空白内容界面
* 清空当前内容区域,显示一个空白界面。
*/
public void exchangeToBlankContent() {
contentView.exchangeToBlank();
}
/**
* 切换到聊天房间界面
* 清空当前内容区域,显示聊天房间界面。
* 聊天房间界面显示与groupId相关的聊天内容。
*/
public void exchangeToChatRoom(String groupId) {
contentView.exchangeToChatRoom(groupId);
}
/**
* 切换到好友个人信息界面
* 清空当前内容区域,显示好友个人信息界面。
* 好友个人信息界面显示与userId相关的好友信息。
*/
public void exchangeToFriendProfile(String userId, String userName) {
contentView.exchangeToFriendProfile(userId, userName);
}
/**
* 切换到设置界面
* 清空当前内容区域,显示设置界面。
* 设置界面根据type显示不同的设置选项。
*/
public void exchangeToSettings(String type) {
contentView.exchangeToSettings(type);
}
/**
* 切换到群聊邀请请求界面
* 清空当前内容区域,显示群聊邀请请求界面。
* 群聊邀请请求界面显示与inviterId相关的群聊邀请请求,包括邀请者姓名、群聊名称等。
*/
public void showGroupInviteRequestDialog(String inviterId, String inviterName, String groupName, String groupId) {
JDialog inviteDialog = new JDialog(MainPage.get(), "群聊邀请", true);
inviteDialog.setSize(300, 200);
inviteDialog.setLocationRelativeTo(MainPage.get());
// 设置对话框内容
JLabel label = new JLabel(
"用户" + inviterName + "邀请你加入:" + groupName,
SwingConstants.CENTER);
JButton confirmBtn = new JButton("接受");
JButton closeBtn = new JButton("拒绝");
JPanel panel = new JPanel(new BorderLayout());
JPanel centerPanel = new JPanel(new FlowLayout());
centerPanel.add(label);
JPanel bottomPanel = new JPanel();
bottomPanel.add(confirmBtn);
bottomPanel.add(closeBtn);
panel.add(centerPanel, BorderLayout.CENTER);
panel.add(bottomPanel, BorderLayout.SOUTH);
inviteDialog.add(panel);
confirmBtn.addActionListener(
e -> {
ChatSender.addMsg(
new Wrapper(inviterId, LocalData.get().getId(), groupId, global.OPT_GROUP_INVITE_AGREE));
inviteDialog.dispose();
});
closeBtn.addActionListener(
e -> {
ChatSender.addMsg(
new Wrapper(inviterId, LocalData.get().getId(), groupId, global.OPT_GROUP_INVITE_REFUSE));
inviteDialog.dispose();
});
// 显示对话框(会阻塞主窗口交互)
inviteDialog.setVisible(true);
}
/**
* 切换到邀请好友界面
* 清空当前内容区域,显示邀请好友界面。
* 邀请好友界面允许用户输入好友ID,邀请好友加入当前聊天房间。
*/
public void showGroupInviteDialog() {
JDialog inviteDialog = new JDialog(MainPage.get(), "邀请好友", true);
inviteDialog.setSize(300, 200);
inviteDialog.setLocationRelativeTo(MainPage.get());
// 创建主面板
JPanel mainPanel = new JPanel();
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
// 创建ID面板
JPanel idPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
idPanel.add(new JLabel("好友ID :"));
JTextField idField = new JTextField(15);
idPanel.add(idField);
mainPanel.add(idPanel);
// 添加间隔
mainPanel.add(Box.createVerticalStrut(20));
// 添加按钮面板
JPanel buttonPanel = new JPanel();
JButton confirmButton = new JButton("确定");
confirmButton.addActionListener(e -> {
String userId = idField.getText().trim();
if (userId.isEmpty()) {
JOptionPane.showMessageDialog(inviteDialog, "好友不能为空", "警告", JOptionPane.WARNING_MESSAGE);
return;
}
// 这里添加创建群聊的逻辑
ChatSender.addMsg(Wrapper.groupInviteRequest(
userId,
LocalData.get().getId(),
LocalData.get().getCurrentChatId()));
inviteDialog.dispose();
});
buttonPanel.add(confirmButton);
mainPanel.add(buttonPanel);
inviteDialog.add(mainPanel);
inviteDialog.getRootPane().setDefaultButton(confirmButton);
inviteDialog.setVisible(true);
}
/**
* 切换到加入群聊界面
* 清空当前内容区域,显示加入群聊界面。
* 加入群聊界面允许用户输入群聊ID,申请加入群聊。
*/
public void showJoinGroupDialog() {
JDialog dialog = new JDialog(MainPage.get(), "加入群聊", true);
dialog.setSize(300, 200);
dialog.setLocationRelativeTo(MainPage.get());
// 创建主面板
JPanel mainPanel = new JPanel();
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
// 创建ID面板
JPanel idPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
idPanel.add(new JLabel("群聊ID:"));
JTextField idField = new JTextField(15);
idPanel.add(idField);
mainPanel.add(idPanel);
// 添加间隔
mainPanel.add(Box.createVerticalStrut(20));
// 添加按钮面板
JPanel buttonPanel = new JPanel();
JButton confirmButton = new JButton("确定");
confirmButton.addActionListener(e -> {
String groupId = idField.getText().trim();
if (groupId.isEmpty()) {
JOptionPane.showMessageDialog(dialog, "群聊ID不能为空", "警告", JOptionPane.WARNING_MESSAGE);
return;
}
// 发送加入群聊请求
ChatSender.addMsg(new Wrapper(null, LocalData.get().getId(), groupId, global.OPT_GROUP_JOIN));
dialog.dispose();
});
buttonPanel.add(confirmButton);
mainPanel.add(buttonPanel);
dialog.add(mainPanel);
dialog.getRootPane().setDefaultButton(confirmButton);
dialog.setVisible(true);
}
/**
* 切换到添加好友界面
* 清空当前内容区域,显示添加好友界面。
* 添加好友界面允许用户输入好友ID,申请添加好友。
*/
public void showAddFriendDialog() {
JDialog dialog = new JDialog(MainPage.get(), "添加好友", true);
dialog.setSize(300, 200);
dialog.setLocationRelativeTo(MainPage.get());
// 创建主面板
JPanel mainPanel = new JPanel();
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
// 创建ID面板
JPanel idPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
idPanel.add(new JLabel("好友ID:"));
JTextField idField = new JTextField(15);
idPanel.add(idField);
mainPanel.add(idPanel);
// 添加间隔
mainPanel.add(Box.createVerticalStrut(20));
// 添加按钮面板
JPanel buttonPanel = new JPanel();
JButton confirmButton = new JButton("确定");
confirmButton.addActionListener(e -> {
String friendId = idField.getText().trim();
if (friendId.isEmpty()) {
JOptionPane.showMessageDialog(dialog, "好友ID不能为空", "警告", JOptionPane.WARNING_MESSAGE);
return;
}
if (friendId.equals(LocalData.get().getId())) {
JOptionPane.showMessageDialog(dialog, "不能添加自己为好友", "警告", JOptionPane.WARNING_MESSAGE);
return;
}
// 发送添加好友请求
ChatSender.addMsg(new Wrapper(friendId, LocalData.get().getId(), null, global.OPT_FRIEND_ADD));
dialog.dispose();
});
buttonPanel.add(confirmButton);
mainPanel.add(buttonPanel);
dialog.add(mainPanel);
dialog.getRootPane().setDefaultButton(confirmButton);
dialog.setVisible(true);
}
/**
* 切换到创建群聊界面
* 清空当前内容区域,显示创建群聊界面。
* 创建群聊界面允许用户输入群聊ID和名称,创建一个新的群聊房间。
*/
public void showGroupCreateDialog() {
JDialog inviteDialog = new JDialog(MainPage.get(), "创建群聊", true);
inviteDialog.setSize(300, 200);
inviteDialog.setLocationRelativeTo(MainPage.get());
// 创建主面板
JPanel mainPanel = new JPanel();
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
// 创建ID面板
JPanel idPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
idPanel.add(new JLabel("群聊ID :"));
JTextField idField = new JTextField(15);
idPanel.add(idField);
mainPanel.add(idPanel);
// 创建名称面板
JPanel namePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
namePanel.add(new JLabel("群聊名称:"));
JTextField nameField = new JTextField(15);
namePanel.add(nameField);
mainPanel.add(namePanel);
// 添加间隔
mainPanel.add(Box.createVerticalStrut(20));
// 添加按钮面板
JPanel buttonPanel = new JPanel();
JButton confirmButton = new JButton("确定");
confirmButton.addActionListener(e -> {
String groupId = idField.getText().trim();
String groupName = nameField.getText().trim();
if (groupId.isEmpty() || groupName.isEmpty()) {
JOptionPane.showMessageDialog(inviteDialog, "群聊ID和名称不能为空", "警告", JOptionPane.WARNING_MESSAGE);
return;
}
if (groupId.length() < 6 || groupId.length() > 10 || !groupId.matches("[a-zA-Z0-9_]+")) {
JOptionPane.showMessageDialog(inviteDialog, "群聊ID只能包含字母大小写和数字下划线,且长度不得小于6,大于10", "警告",
JOptionPane.WARNING_MESSAGE);
return;
}
if (groupName.contains(" ")) {
JOptionPane.showMessageDialog(inviteDialog, "群聊名称不能包含空格", "警告", JOptionPane.WARNING_MESSAGE);
return;
}
// 添加创建群聊的逻辑
ChatSender.addMsg(Wrapper.createGroupRequest(LocalData.get().getId(), groupName, groupId));
inviteDialog.dispose();
});
buttonPanel.add(confirmButton);
mainPanel.add(buttonPanel);
inviteDialog.add(mainPanel);
inviteDialog.getRootPane().setDefaultButton(confirmButton);
inviteDialog.setVisible(true);
}
}
+102
View File
@@ -0,0 +1,102 @@
package client.view.login;
import client.Client;
import client.service.ChatSender;
import client.service.LocalData;
import client.view.LoginPage;
import client.view.util.DesignToken;
import server.serveice.Wrapper;
import javax.swing.*;
// 登录界面
public class SignInView extends JPanel {
private LoginPage loginPage;
JTextField account;
JTextField password;
/**
* 生成一个登录界面
* 按照原型图中设计,创建两个文本输入框,用于让用户输入id和密码。
* 创建一个按钮用于登录,为按钮添加SignIncheck的按下事件
* 创建一个注册选项按钮,按下后该界面更换为注册界面。
*/
public SignInView(LoginPage loginPage) {
this.loginPage = loginPage;
//面板
this.setLayout(null);
this.setSize(DesignToken.LOGIN_WIDTH, DesignToken.LOGIN_HEIGHT);
this.setVisible(true);
//文本展示
JLabel ja1 = new JLabel("登录");
ja1.setBounds(165, 10, 80, 80);
this.add(ja1);
JLabel ja2 = new JLabel("账户:");
ja2.setBounds(50, 80, 100, 30);
this.add(ja2);
JLabel ja3 = new JLabel("密码:");
ja3.setBounds(50, 120, 100, 30);
this.add(ja3);
//按钮设计
JButton jb1 = new JButton("注册");
jb1.setBounds(230, 190, 59, 20);
jb1.setBorderPainted(false);
jb1.addActionListener(e -> {
this.loginPage.exchangeToSignUpView();
});
this.add(jb1);
// JButton jb2=new JButton("忘记密码");
// jb2.setBounds(290,190,90,20);
// jb2.setBorderPainted(false);
// jb2.addActionListener(e -> {
//
// });
// add(jb2);
JButton jb3 = new JButton("登录");
jb3.setBounds(150, 160, 59, 50);
jb3.addActionListener(e -> {
signIncheck();
});
this.add(jb3);
//建立文本域
account = new JTextField(DesignToken.MAX_FONT_SIZE);
account.setBounds(80, 80, 220, 30);
this.add(account);
password = new JPasswordField(DesignToken.MAX_FONT_SIZE);
password.setBounds(80, 120, 220, 30);
this.add(password);
}
/**
* 获取当前页面的用户id和密码
* 如果当前服务器未在线,则直接向用户说明
* 否则,发出登录请求
*/
private void signIncheck() {
if (!Client.isConnected()) {
loginPage.showMsgDialog("当前服务器未在线,请稍后再试");
return;
}
LocalData.get().setId(account.getText());
ChatSender.addMsg(
Wrapper.loginRequest(LocalData.get().getId(), password.getText())
);
}
}
+120
View File
@@ -0,0 +1,120 @@
package client.view.login;
import client.Client;
import client.service.ChatSender;
import client.service.LocalData;
import client.view.LoginPage;
import client.view.util.DesignToken;
import server.serveice.Wrapper;
import javax.swing.*;
public class SignUpView extends JPanel {
private LoginPage loginPage;
private JTextField account;
private JTextField password;
private JTextField name;
/**
* 生成一个注册界面
* 按照原型图中设计,创建三个文本输入框,用于让用户输入id,密码,用户名
* 创建一个按钮用于注册
* 创建一个登录选项按钮,按下后该界面更换为登录界面。
*/
public SignUpView(LoginPage loginPage) {
this.loginPage = loginPage;
//功能面板的建立
this.setLayout(null);
this.setSize(DesignToken.LOGIN_WIDTH, DesignToken.LOGIN_HEIGHT);
this.setVisible(true);
//文本
JLabel ja1 = new JLabel("注册");
ja1.setBounds(165, 10, 80, 80);
this.add(ja1);
JLabel ja2 = new JLabel("账户:");
ja2.setBounds(50, 80, 100, 30);
this.add(ja2);
JLabel ja3 = new JLabel("密码:");
ja3.setBounds(50, 120, 100, 30);
this.add(ja3);
JLabel ja4 = new JLabel("名字:");
ja4.setBounds(50, 160, 100, 30);
this.add(ja4);
//监听按钮
JButton jb1 = new JButton("注册");
jb1.setBounds(150, 200, 59, 50);
this.add(jb1);
jb1.addActionListener(e -> {
this.SignUpcheck();
});
JButton jb2 = new JButton("已有账号?去登录");
jb2.setBounds(220, 230, 150, 20);
jb2.setBorderPainted(false);
jb2.addActionListener(e -> {
this.loginPage.exchangeToSignInView();
});
this.add(jb2);
//文本域
account = new JTextField(DesignToken.MAX_FONT_SIZE);
account.setBounds(80, 80, 220, 30);
this.add(account);
password = new JTextField(DesignToken.MAX_FONT_SIZE);
password.setBounds(80, 120, 220, 30);
this.add(password);
name = new JTextField(DesignToken.MAX_FONT_SIZE);
name.setBounds(80, 160, 220, 30);
this.add(name);
}
/**
* 获取当前界面的文本的内容
* 检查服务器是否链接
* 检查注册的idnamepassword是否合法
* 都合适的话,发送注册请求。
*/
private void SignUpcheck() {
if (!Client.isConnected()) {
loginPage.showMsgDialog("当前服务器未在线,请稍后再试");
return;
}
String userId = account.getText();
String userName = name.getText();
String userPassword = password.getText();
if (userId.length() < 6 || userId.length() > 10 || !userId.matches("[a-zA-Z0-9_]+")) {
loginPage.showMsgDialog("用户id只能包含字母大小写和数字下划线,且长度不得小于6,大于10");
return;
}
if (userName.contains(" ")) {
loginPage.showMsgDialog("用户名不能包含空格");
return;
}
if (userPassword.contains(" ")) {
loginPage.showMsgDialog("密码不能包含空格");
return;
}
LocalData.get().setId(userId);
LocalData.get().setUserName(userId, userName);
ChatSender.addMsg(Wrapper.registerRequest(
LocalData.get().getId(),
password.getText(),
name.getText()));
}
}
+530
View File
@@ -0,0 +1,530 @@
package client.view.main;
import client.service.ChatSender;
import client.service.LocalData;
import client.view.util.CircleCharIcon2;
import client.view.util.DesignToken;
import server.serveice.Wrapper;
import util.MsgUtil;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import static client.view.util.DesignToken.*;
// 群聊信息组件,包含底部的打字框和滚动的信息聊天信息
public class ChatInfoView extends JPanel {
private static volatile ChatInfoView instance;
public static ChatInfoView get() {
if (instance == null) {
synchronized (ChatInfoView.class) {
if (instance == null) {
instance = new ChatInfoView();
}
}
}
return instance;
}
// 界面组件
private JPanel messagePanel; // 使用JPanel来承载消息,可以自定义布局
private List<JPanel> bubbles;
private JScrollPane messageScrollPane;
private JTextArea inputArea;
private JScrollPane inputScrollPane;
private JButton sendButton;
private JPanel inputPanel;
private JPanel buttonPanel;
// 样式相关
private SimpleDateFormat timeFormat;
private Color userColor = Color.decode(DesignToken.BUBBLE_COLOR_GREEN); // 用户消息气泡颜色
private Color otherColor = Color.decode(DesignToken.BUBBLE_COLOR_WHITE); // 他人消息气泡颜色(白色)
private Color systemColor = Color.decode(DesignToken.BACKGROUND_COLOR); // 系统消息背景色
// 头像颜色数组,用于不同用户的头像显示
private final Color[] avatarColors = {
Color.decode(DesignToken.BUBBLE_COLOR_BLUE),
Color.decode(DesignToken.BUBBLE_COLOR_GRAY),
Color.decode(DesignToken.BUBBLE_COLOR_RED),
Color.decode(DesignToken.BUBBLE_COLOR_YELLOW),
Color.decode(DesignToken.BUBBLE_COLOR_WHITE),
};
/**
* 构造函数
* 初始化界面组件和布局
*/
private ChatInfoView() {
setLayout(new BorderLayout(0, 0));
// 初始化时间格式
timeFormat = new SimpleDateFormat("HH:mm");
// 初始化消息显示区域
initMessagePanel();
// 创建输入区域
initInputPanel();
}
/**
* 初始化消息面板
* 包含滚动条和消息气泡容器
*/
private void initMessagePanel() {
bubbles = new ArrayList<>();
// 创建消息面板,使用垂直箱式布局
messagePanel = new JPanel();
messagePanel.setLayout(new BoxLayout(messagePanel, BoxLayout.Y_AXIS));
messagePanel.setBackground(systemColor);
messagePanel.setBorder(new EmptyBorder(10, 10, 10, 10));
// 添加一个弹性空间,让新消息从底部开始
messagePanel.add(Box.createVerticalGlue());
// 添加滚动条
messageScrollPane = new JScrollPane(messagePanel);
messageScrollPane.setBorder(new EmptyBorder(0, 0, 0, 0));
messageScrollPane.getVerticalScrollBar().setUnitIncrement(16);
messageScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
// 设置视口的背景色
JViewport viewport = messageScrollPane.getViewport();
viewport.setBackground(systemColor);
// 添加到主面板
this.add(messageScrollPane, BorderLayout.CENTER);
}
/**
* 初始化输入面板
* 包含输入文本框和发送按钮
*/
private void initInputPanel() {
// 创建输入面板
inputPanel = new JPanel(new BorderLayout(5, 5));
inputPanel.setBackground(systemColor);
inputPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
// 创建输入文本框
inputArea = new JTextArea(3, 20);
inputArea.setFont(new Font(DEFAULT_FONT, Font.PLAIN, DesignToken.FONT_SIZE));
inputArea.setLineWrap(true);
inputArea.setWrapStyleWord(true);
inputArea.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(Color.decode(DesignToken.EDGE_COLOR), 1),
BorderFactory.createEmptyBorder(8, 8, 8, 8)));
// 设置提示文本
inputArea.setToolTipText("输入消息,按Enter发送,Ctrl+Enter换行");
// 添加滚动条到输入框
inputScrollPane = new JScrollPane(inputArea);
inputScrollPane.setBorder(null);
// 创建发送按钮
sendButton = createSendButton();
// 创建按钮面板
buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 5));
buttonPanel.setBackground(systemColor);
buttonPanel.add(sendButton);
setupListeners();
// 添加组件到输入面板
inputPanel.add(inputScrollPane, BorderLayout.CENTER);
inputPanel.add(buttonPanel, BorderLayout.SOUTH);
// 添加到主面板
this.add(inputPanel, BorderLayout.SOUTH);
}
/**
* 更新主题
* 当主题变化时调用,用于更新所有组件的颜色
*/
public void updateTheme() {
// 更新颜色变量
userColor = Color.decode(DesignToken.BUBBLE_COLOR_GREEN);
otherColor = Color.decode(DesignToken.BUBBLE_COLOR_WHITE);
systemColor = Color.decode(DesignToken.BACKGROUND_COLOR);
// 更新组件背景
if (messagePanel != null)
messagePanel.setBackground(systemColor);
if (messageScrollPane != null && messageScrollPane.getViewport() != null) {
messageScrollPane.getViewport().setBackground(systemColor);
}
if (inputPanel != null)
inputPanel.setBackground(systemColor);
if (buttonPanel != null)
buttonPanel.setBackground(systemColor);
if (sendButton != null)
sendButton.setBackground(userColor);
if (inputArea != null) {
inputArea.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(Color.decode(DesignToken.EDGE_COLOR), 1),
BorderFactory.createEmptyBorder(8, 8, 8, 8)));
}
// 刷新当前聊天记录
if (LocalData.get().getCurrentChatId() != null) {
init(LocalData.get().getCurrentChatId());
}
}
/**
* 创建发送按钮
* 按钮文本为“发送”,字体为加粗,大小为 DesignToken.FONT_SIZE
* 背景颜色为 DesignToken.BUBBLE_COLOR_GREEN,前景颜色为黑色
* 点击时背景颜色为 DesignToken.BUBBLE_COLOR_GREEN,松开时恢复为 DesignToken.BUBBLE_COLOR_GREEN
* 鼠标悬停时背景颜色为 DesignToken.BUBBLE_COLOR_GREEN
*
* @return 发送按钮
*/
private JButton createSendButton() {
JButton button = new JButton("发送");
button.setFont(new Font(DEFAULT_FONT, Font.BOLD, DesignToken.FONT_SIZE));
button.setBackground(userColor);
button.setForeground(Color.BLACK); // 强制设置字体颜色为黑色,确保在绿色背景下清晰可见
button.setFocusPainted(false);
button.setBorder(BorderFactory.createEmptyBorder(10, 25, 10, 25));
button.setCursor(new Cursor(Cursor.HAND_CURSOR));
// 鼠标悬停效果
button.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseEntered(java.awt.event.MouseEvent evt) {
button.setBackground(Color.decode(DesignToken.BUBBLE_COLOR_GREEN));
}
public void mouseExited(java.awt.event.MouseEvent evt) {
button.setBackground(userColor);
}
public void mousePressed(java.awt.event.MouseEvent evt) {
button.setBackground(userColor);
}
public void mouseReleased(java.awt.event.MouseEvent evt) {
button.setBackground(userColor);
}
});
return button;
}
/**
* 消息发送事件
* 点击发送按钮或按下Enter键发送消息
*/
private void setupListeners() {
// 发送按钮点击事件
sendButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
sendMessage();
}
});
// 回车发送消息,Ctrl+Enter换行
inputArea.addKeyListener(new java.awt.event.KeyAdapter() {
@Override
public void keyPressed(java.awt.event.KeyEvent e) {
if (e.getKeyCode() == java.awt.event.KeyEvent.VK_ENTER) {
if (e.isControlDown()) {
// Ctrl+Enter 换行
inputArea.append("\n");
} else {
// Enter 发送消息
e.consume(); // 防止默认的换行行为
sendMessage();
}
}
}
});
// 窗口显示时自动聚焦到输入框
addAncestorListener(new AncestorListener() {
@Override
public void ancestorAdded(AncestorEvent e) {
inputArea.requestFocusInWindow();
}
@Override
public void ancestorRemoved(AncestorEvent event) {
}
@Override
public void ancestorMoved(AncestorEvent event) {
}
});
}
/**
* 发送信息
* 先调用使用DataManager进行信息发送操作
* 如果发送成功,则更新界面,添加信息到消息历史上
* 如果发送未成功,则忽略这个操作(发射未成功表示程序出现了问题,在控制态输出问题)
*/
private void sendMessage() {
String text = inputArea.getText().trim();
if (!text.isEmpty()) {
String id = LocalData.get().getId();
String currentChatId = LocalData.get().getCurrentChatId();
String message = MsgUtil.combineMsg(id, LocalData.get().getUserName(id), text);
// 检查是群聊还是私聊
if (LocalData.get().getGroupData(currentChatId) != null) {
// 群聊
ChatSender.addMsg(Wrapper.groupChat(message, id, currentChatId));
} else {
// 私聊:发送纯文本
ChatSender.addMsg(Wrapper.privateChat(text, id, currentChatId));
}
// 暂时保存消息
LocalData.get().addChatMsg(
LocalData.get().getCurrentChatId(),
message);
// 添加用户消息
addUserMessage(text);
// 清空输入框
inputArea.setText("");
// 滚动到底部
scrollToBottom();
}
// 聚焦回输入框
inputArea.requestFocusInWindow();
}
/**
* 添加用户消息
* 用户消息右对齐
*
* @param content 消息内容
*/
public void addUserMessage(String content) {
// 用户消息右对齐
addMessageBubble(true, "", content);
}
/**
* 添加他人消息
* 他人消息左对齐
*
* @param senderName 发送者名称
* @param content 消息内容
*/
public void addOtherUserMessage(String senderName, String content) {
// 他人消息左对齐
addMessageBubble(false, senderName, content);
}
/**
* 刷新当前聊天信息
* 从本地数据中获取聊天记录,根据发送者ID判断是用户消息还是他人消息
*
* @param chatId 聊天ID
*/
public void setChatInfo(String chatId) {
// 清空当前消息
messagePanel.removeAll();
messagePanel.add(Box.createVerticalGlue());
bubbles.clear();
// 从本地数据中获取聊天记录
List<String> messages = LocalData.get().getChatMsg(chatId);
if (messages != null) {
String myId = LocalData.get().getId();
for (String msg : messages) {
String[] split = MsgUtil.splitMsg(msg);
// split[0] = senderId, split[1] = senderName, split[2] = content
if (split.length >= 3) {
if (split[0].equals(myId)) {
addUserMessage(split[2]);
} else {
addOtherUserMessage(split[1], split[2]);
}
}
}
}
// 刷新消息面板
messagePanel.revalidate();
messagePanel.repaint();
scrollToBottom();
}
/**
* 添加系统消息
* 系统消息居中显示,字体为斜体,颜色为灰色
*
* @param content 系统消息内容
*/
public void addSystemMessage(String content) {
SwingUtilities.invokeLater(() -> {
// 系统消息
JPanel systemPanel = new JPanel();
systemPanel.setLayout(new BorderLayout());
systemPanel.setBackground(new Color(236, 236, 236));
systemPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 40));
JLabel systemLabel = new JLabel(content);
systemLabel.setFont(new Font(DEFAULT_FONT, Font.ITALIC, DesignToken.FONT_SIZE_SMALL));
systemLabel.setForeground(Color.GRAY);
systemLabel.setHorizontalAlignment(SwingConstants.CENTER);
systemPanel.add(systemLabel, BorderLayout.CENTER);
// 添加到消息面板顶部
messagePanel.add(systemPanel, 0);
messagePanel.revalidate();
messagePanel.repaint();
});
}
/**
* 添加消息气泡
* 根据是否是用户消息,创建不同的消息气泡样式
*
* @param isSelf 是否是用户消息
* @param senderName 发送者名称
* @param content 消息内容
*/
public void addMessageBubble(boolean isSelf, String senderName, String content) {
SwingUtilities.invokeLater(() -> {
// 创建消息气泡面板
JPanel messageBubblePanel = new JPanel();
messageBubblePanel.setLayout(new BorderLayout(8, 0));
messageBubblePanel.setBackground(systemColor);
messageBubblePanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 200));
// 添加头像
JLabel avatarPanel = new JLabel(new CircleCharIcon2(
avatarColors[Math.abs(senderName.hashCode()) % avatarColors.length],
Color.WHITE,
senderName.substring(0, 1).toUpperCase(),
40));
// 创建消息内容面板
JPanel contentPanel = new JPanel();
contentPanel.setLayout(new BorderLayout(0, 5));
contentPanel.setOpaque(false);
// 创建发送者标签和时间标签
String time = timeFormat.format(new Date());
JLabel infoLabel = new JLabel(senderName + " " + time);
infoLabel.setFont(new Font(DEFAULT_FONT, Font.PLAIN, FONT_SIZE_SMALL));
infoLabel.setForeground(Color.GRAY);
// 创建消息气泡
JTextArea messageLabel = new JTextArea(content);
messageLabel.setEditable(false);
messageLabel.setLineWrap(true);
messageLabel.setWrapStyleWord(true);
messageLabel.setFont(new Font(DEFAULT_FONT, Font.PLAIN, FONT_SIZE));
messageLabel.setBorder(BorderFactory.createEmptyBorder(10, 15, 10, 15));
// 设置气泡颜色和样式
if (isSelf) {
// 用户消息:右对齐,绿色气泡
messageLabel.setBackground(userColor);
messageLabel.setForeground(Color.WHITE);
infoLabel.setHorizontalAlignment(SwingConstants.RIGHT);
contentPanel.add(infoLabel, BorderLayout.NORTH);
contentPanel.add(messageLabel, BorderLayout.CENTER);
// 右对齐布局
messageBubblePanel.add(contentPanel, BorderLayout.CENTER);
messageBubblePanel.add(avatarPanel, BorderLayout.EAST);
} else {
// 他人消息:左对齐,白色气泡
messageLabel.setBackground(otherColor);
messageLabel.setForeground(Color.decode(DesignToken.COLOR_FONT_BLACK));
messageLabel.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(Color.decode(EDGE_COLOR), 1),
BorderFactory.createEmptyBorder(10, 15, 10, 15)));
infoLabel.setHorizontalAlignment(SwingConstants.LEFT);
contentPanel.add(infoLabel, BorderLayout.NORTH);
contentPanel.add(messageLabel, BorderLayout.CENTER);
// 左对齐布局
messageBubblePanel.add(avatarPanel, BorderLayout.WEST);
messageBubblePanel.add(contentPanel, BorderLayout.CENTER);
}
// 设置消息气泡的最大宽度(防止过宽)
int maxBubbleWidth = 350;
messageLabel.setSize(new Dimension(maxBubbleWidth, Integer.MAX_VALUE));
int preferredHeight = messageLabel.getPreferredSize().height;
messageLabel.setPreferredSize(new Dimension(maxBubbleWidth, preferredHeight));
// 将其添加到消息列表当中
bubbles.add(messageBubblePanel);
// 添加到消息面板顶部(新消息在顶部显示)
messagePanel.add(messageBubblePanel);
messagePanel.revalidate();
messagePanel.repaint();
});
}
/**
* 移除所有消息气泡(bubbles)
* 清空输入栏的内容
*/
public void removeAllMessageBubble() {
for (JPanel panel : bubbles) {
messagePanel.remove(panel);
}
bubbles.clear();
inputArea.setText("");
}
/**
* 根据群聊id在DataManager中选择并加载群聊信息
*
* @param groupId 群聊id
*/
public void init(String groupId) {
this.removeAllMessageBubble();
List<String> messages = LocalData.get().getChatMsg(groupId);
for (String text : messages) {
String[] msgs = MsgUtil.splitMsg(text);
if (msgs[0].equals(LocalData.get().getId())) {
addUserMessage(msgs[2]);
} else {
addMessageBubble(msgs[0].equals(LocalData.get().getId()), msgs[1], msgs[2]);
}
}
messagePanel.revalidate();
messagePanel.repaint();
}
private void scrollToBottom() {
SwingUtilities.invokeLater(() -> {
JScrollBar vertical = messageScrollPane.getVerticalScrollBar();
vertical.setValue(vertical.getMaximum());
});
}
}
+84
View File
@@ -0,0 +1,84 @@
package client.view.main;
import client.service.LocalData;
import client.view.util.DesignToken;
import client.view.util.LimitSizePanel;
import javax.swing.*;
import java.awt.*;
import static client.view.util.DesignToken.*;
/**
* 内容界面组件
* 用于显示聊天、群聊、好友个人信息等内容。
* 点击不同选项可以切换到对应的功能界面。
*/
public class ContentView extends LimitSizePanel {
private ChatInfoView chatInfoView;
private GroupInfoView groupInfoView;
public ContentView() {
super(CONTENT_PANEL_WIDTH_MIN);
this.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
this.setLayout(new BorderLayout());
chatInfoView = ChatInfoView.get();
chatInfoView.setPreferredSize(new Dimension(GROUP_CHAT_PANEL_WIDTH, this.getHeight()));
chatInfoView.setMinimumSize(new Dimension(GROUP_CHAT_PANEL_WIDTH, this.getHeight()));
groupInfoView = GroupInfoView.get();
groupInfoView.setPreferredSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, this.getHeight()));
exchangeToBlank();
}
public void exchangeToBlank() {
this.removeAll();
}
/**
* 将内容组件更改为群聊组件
* 依据groupId来从DataManager中获取群聊信息
* 更新 chatInfoView groupInfoView两个组件
*/
public void exchangeToChatRoom(String groupId) {
this.removeAll();
chatInfoView.init(groupId);
this.add(chatInfoView, BorderLayout.CENTER);
if (LocalData.get().getGroupData(groupId) != null) {
groupInfoView.updateInfo();
this.add(groupInfoView, BorderLayout.EAST);
}
this.revalidate();
this.repaint();
}
/**
* 将内容组件更改为好友个人信息组件
* 依据userId和userName来创建FriendProfileView组件
* 更新 chatInfoView groupInfoView两个组件
*/
public void exchangeToFriendProfile(String userId, String userName) {
this.removeAll();
FriendProfileView profileView = new FriendProfileView(userId, userName);
this.add(profileView, BorderLayout.CENTER);
this.revalidate();
this.repaint();
}
/**
* 将内容组件更改为设置组件
* 依据type来创建SettingsView组件
* 更新 chatInfoView groupInfoView两个组件
*/
public void exchangeToSettings(String type) {
this.removeAll();
SettingsView settingsView = new SettingsView(type);
this.add(settingsView, BorderLayout.CENTER);
this.revalidate();
this.repaint();
}
}
+95
View File
@@ -0,0 +1,95 @@
package client.view.main;
import client.view.MainPage;
import client.view.util.CircleCharIcon2;
import client.view.util.DesignToken;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
/**
* 好友列表项组件
* 用于显示好友列表中的每个好友项,包括好友头像、好友名称等。
* 点击好友项可以进入好友个人信息界面。
*/
public class FriendListItem extends JPanel {
private final String userId;
private final String userName;
JLabel icon;
JPanel centerPanel;
JLabel titleLabel;
/**
* 构造好友列表项组件
*
* @param userId 好友用户ID
* @param userName 好友用户名
*/
public FriendListItem(String userId, String userName) {
this.userId = userId;
this.userName = userName;
// 设置布局管理器
setLayout(new BorderLayout(10, 0));
setBorder(BorderFactory.createEmptyBorder(8, 10, 8, 10));
setMaximumSize(new Dimension(Integer.MAX_VALUE, 70));
setPreferredSize(new Dimension(200, 70));
// 左侧图标
icon = new JLabel(new CircleCharIcon2(Color.LIGHT_GRAY, Color.WHITE,
userName.substring(0, 1).toUpperCase(), 40));
icon.setPreferredSize(new Dimension(40, 40));
// 中间区域 - 好友名称
centerPanel = new JPanel(new GridLayout(1, 1));
titleLabel = new JLabel(userName);
titleLabel.setFont(new Font(DesignToken.DEFAULT_FONT, Font.BOLD, 14));
centerPanel.add(titleLabel);
// 组装
this.add(icon, BorderLayout.WEST);
this.add(centerPanel, BorderLayout.CENTER);
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
enterFriendProfile();
}
@Override
public void mouseEntered(MouseEvent e) {
setBackground(UIManager.getColor("List.selectionBackground")); // 悬停效果
}
@Override
public void mouseExited(MouseEvent e) {
setBackground(UIManager.getColor("Panel.background")); // 恢复原背景
}
@Override
public void mousePressed(MouseEvent e) {
setBackground(UIManager.getColor("List.selectionInactiveBackground")); // 点击效果
}
@Override
public void mouseReleased(MouseEvent e) {
if (contains(e.getPoint())) {
setBackground(UIManager.getColor("List.selectionBackground"));
} else {
setBackground(UIManager.getColor("Panel.background"));
}
}
});
}
/**
* 进入好友个人信息界面
* 点击好友项时,切换到好友个人信息界面,显示与该好友相关的个人信息。
*/
public void enterFriendProfile() {
MainPage.get().exchangeToFriendProfile(userId, userName);
}
}
+127
View File
@@ -0,0 +1,127 @@
package client.view.main;
import client.service.LocalData;
import client.view.util.CircleCharIcon2;
import client.view.util.DesignToken;
import server.data.UserData;
import javax.swing.*;
import java.awt.*;
/**
* 好友个人信息视图
* 显示好友的头像、名称和ID,以及操作按钮(发送消息、删除好友)
* 好友头像:显示好友的圆形头像
* 好友名称:显示好友的名称,字体为默认字体,大小为24号,加粗
* 用户ID:显示好友的唯一标识符,字体为默认字体,大小为16号,颜色为灰色
* 发送消息按钮:点击后可以发送消息给好友
* 删除好友按钮:点击后可以删除好友关系
*/
public class FriendProfileView extends JPanel {
private String userId;
private String userName;
public FriendProfileView(String userId, String userName) {
this.userId = userId;
this.userName = userName;
initUI();
}
/**
* 初始化UI组件
* 设置布局为网格Bag布局,背景颜色为白色
* 好友头像:显示好友的圆形头像,大小为80x80
* 好友名称:显示好友的名称,字体为默认字体,大小为24号,加粗
* 用户ID:显示好友的唯一标识符,字体为默认字体,大小为16号,颜色为灰色
* 发送消息按钮:点击后可以发送消息给好友,大小为120x40,背景颜色为蓝色,文字颜色为白色
* 删除好友按钮:点击后可以删除好友关系,大小为120x40,背景颜色为红色,文字颜色为白色
*/
private void initUI() {
setLayout(new GridBagLayout());
setBackground(Color.WHITE);
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
gbc.insets = new Insets(10, 10, 10, 10);
gbc.anchor = GridBagConstraints.CENTER;
// 好友头像
JLabel icon = new JLabel(new CircleCharIcon2(Color.ORANGE, Color.WHITE,
userName.substring(0, 1).toUpperCase(), 80));
add(icon, gbc);
// 好友名称
gbc.gridy++;
JLabel nameLabel = new JLabel(userName);
nameLabel.setFont(new Font(DesignToken.DEFAULT_FONT, Font.BOLD, 24));
add(nameLabel, gbc);
// 用户ID
gbc.gridy++;
JLabel idLabel = new JLabel("ID: " + userId);
idLabel.setForeground(Color.GRAY);
add(idLabel, gbc);
// 获取并显示详细信息
UserData friendData = LocalData.get().getUserDetail(userId);
if (friendData != null) {
addInfoLabel(friendData.getEmail(), gbc);
addInfoLabel(friendData.getBirthday(), gbc);
addInfoLabel(friendData.getAddress(), gbc);
addInfoLabel(friendData.getSignature(), gbc);
}
// 发送消息按钮
gbc.gridy++;
gbc.insets = new Insets(30, 10, 10, 10);
JButton sendBtn = new JButton("发送消息");
sendBtn.setPreferredSize(new Dimension(120, 40));
sendBtn.setBackground(new Color(0, 122, 255));
sendBtn.setForeground(Color.WHITE);
sendBtn.setFocusPainted(false);
sendBtn.addActionListener(e -> {
// 更新消息列表
SecondaryOptionView.get().updateMessageList(userId, userName, "", 0);
// 更新当前聊天ID
client.service.LocalData.get().setCurrentChatId(userId);
// 切换到聊天房间
client.view.MainPage.get().exchangeToChatRoom(userId);
});
add(sendBtn, gbc);
// 好友操作按钮
gbc.gridy++;
gbc.insets = new Insets(10, 10, 10, 10);
JButton deleteBtn = new JButton("删除好友");
deleteBtn.setPreferredSize(new Dimension(120, 40));
deleteBtn.setBackground(new Color(220, 53, 69));
deleteBtn.setForeground(Color.WHITE);
deleteBtn.setFocusPainted(false);
deleteBtn.addActionListener(e -> {
JOptionPane.showMessageDialog(this, "删除好友功能开发中...");
});
add(deleteBtn, gbc);
}
/**
* 添加详细信息标签
* 检查文本是否为空,如果不为空则添加到面板中
* 标签字体为默认字体,大小为14号,颜色为深灰色
*
* @param text 要添加的详细信息文本
* @param gbc 网格BagConstraints对象,用于布局
*/
private void addInfoLabel(String text, GridBagConstraints gbc) {
if (text != null && !text.isEmpty()) {
gbc.gridy++;
gbc.insets = new Insets(2, 10, 2, 10);
JLabel label = new JLabel(text);
label.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 14));
label.setForeground(Color.DARK_GRAY);
add(label, gbc);
}
}
}
+366
View File
@@ -0,0 +1,366 @@
package client.view.main;
import client.service.ChatSender;
import client.service.LocalData;
import client.view.MainPage;
import client.view.util.DesignToken;
import server.data.GroupData;
import server.serveice.Wrapper;
import javax.swing.*;
import java.awt.*;
/**
* 聊天信息组件,用于展示当前聊天室的信息
*/
public class GroupInfoView extends JScrollPane {
private static volatile GroupInfoView instance;
public static GroupInfoView get() {
if (instance == null) {
synchronized (GroupInfoView.class) {
if (instance == null) {
instance = new GroupInfoView();
}
}
}
return instance;
}
private final JPanel mainPanel;
private final JPanel groupInfoPanel;
private final JPanel groupMemberPanel;
public GroupInfoView() {
mainPanel = new JPanel();
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); // 垂直布局
setPreferredSize(new Dimension(DesignToken.INFO_PANEL_WIDTH, DesignToken.WINDOW_ORI_HEIGHT));
setBackground(Color.GRAY);
// 设置滚动策略
setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
groupInfoPanel = createGroupInfoPanel();
groupMemberPanel = createGroupMemberPanel();
mainPanel.add(groupInfoPanel);
mainPanel.add(groupInfoPanel);
mainPanel.add(createInviteButton());
mainPanel.add(createExitButton());
mainPanel.add(groupMemberPanel);
mainPanel.add(groupMemberPanel);
mainPanel.setMinimumSize(
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, 0));
mainPanel.setPreferredSize(
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, DesignToken.WINDOW_ORI_HEIGHT));
this.setViewportView(mainPanel);
}
/**
* 创建聊天成员信息面板
*/
public JPanel createGroupMemberPanel() {
JPanel memberInfoPanel = new JPanel();
memberInfoPanel.setPreferredSize(
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, DesignToken.WINDOW_ORI_HEIGHT - 50));
memberInfoPanel.setMinimumSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, 0));
memberInfoPanel.setLayout(new BoxLayout(memberInfoPanel, BoxLayout.Y_AXIS));
// 添加成员标题
JLabel memberTitle = new JLabel("群成员");
memberTitle.setAlignmentX(Component.CENTER_ALIGNMENT);
memberTitle.setFont(new Font("微软雅黑", Font.BOLD, 16));
memberTitle.setForeground(Color.WHITE);
memberTitle.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0));
memberInfoPanel.add(memberTitle);
// 添加分隔线
JSeparator separator = new JSeparator();
separator.setForeground(Color.DARK_GRAY);
separator.setAlignmentX(Component.CENTER_ALIGNMENT);
separator.setMaximumSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 20, 1));
memberInfoPanel.add(separator);
return memberInfoPanel;
}
/**
* 创建群信息组件
*/
public JPanel createGroupInfoPanel() {
JPanel groupInfoPanel = new JPanel();
groupInfoPanel.setPreferredSize(
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, DesignToken.WINDOW_ORI_HEIGHT - 50));
groupInfoPanel.setMinimumSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, 0));
groupInfoPanel.setLayout(new BoxLayout(groupInfoPanel, BoxLayout.Y_AXIS));
// 添加标题
JLabel titleLabel = new JLabel("群聊信息");
titleLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 18));
titleLabel.setBorder(BorderFactory.createEmptyBorder(10, 0, 15, 0));
groupInfoPanel.add(titleLabel);
return groupInfoPanel;
}
/**
* 创建一个邀请成员的按钮
*/
public JButton createInviteButton() {
JButton inviteButton = new JButton("邀请");
inviteButton.setAlignmentX(Component.CENTER_ALIGNMENT); // 居中对齐
inviteButton.setPreferredSize(
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 20, 30));
inviteButton.setMaximumSize(
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 20, 30));
inviteButton.setMargin(
new Insets(5, 10, 5, 10));
inviteButton.addActionListener(e -> MainPage.get().showGroupInviteDialog());
return inviteButton;
}
/**
* 创建一个退出群聊的按钮
*/
public JButton createExitButton() {
JButton exitButton = new JButton("退出");
exitButton.setAlignmentX(Component.CENTER_ALIGNMENT); // 居中对齐
exitButton.setPreferredSize(
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 20, 30));
exitButton.setMaximumSize(
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 20, 30));
exitButton.setMargin(
new Insets(5, 10, 5, 10));
exitButton.addActionListener(e -> {
ChatSender.addMsg(Wrapper.groupQuitRequest(
LocalData.get().getId(),
LocalData.get().getCurrentChatId()));
});
return exitButton;
}
/**
* 更新 groupInfoPanel 和 groupMemberPanel 这两个组件
*/
public void updateInfo() {
groupInfoPanel.removeAll();
groupMemberPanel.removeAll();
String currentChatId = LocalData.get().getCurrentChatId();
if (currentChatId == null || currentChatId.isEmpty()) {
// 如果没有当前群聊,显示提示信息
JLabel noGroupLabel = new JLabel("未选择群聊");
noGroupLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
noGroupLabel.setForeground(Color.GRAY);
groupInfoPanel.add(noGroupLabel);
groupInfoPanel.revalidate();
groupInfoPanel.repaint();
groupMemberPanel.revalidate();
groupMemberPanel.repaint();
return;
}
GroupData groupData = LocalData.get().getGroupData(currentChatId);
if (groupData == null) {
JLabel errorLabel = new JLabel("群聊数据不存在");
errorLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
errorLabel.setForeground(Color.RED);
groupInfoPanel.add(errorLabel);
groupInfoPanel.revalidate();
groupInfoPanel.repaint();
groupMemberPanel.revalidate();
groupMemberPanel.repaint();
return;
}
// 更新群聊信息面板
updateGroupInfoPanel(groupData);
// 更新群成员面板
updateGroupMemberPanel(groupData);
groupInfoPanel.revalidate();
groupInfoPanel.repaint();
groupMemberPanel.revalidate();
groupMemberPanel.repaint();
}
/**
* 更新群聊信息面板内容
*/
private void updateGroupInfoPanel(GroupData groupData) {
// 添加标题
JLabel titleLabel = new JLabel("群聊信息");
titleLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 18));
titleLabel.setBorder(BorderFactory.createEmptyBorder(10, 0, 15, 0));
groupInfoPanel.add(titleLabel);
// 群聊名称
JLabel nameLabel = new JLabel("群名: " + groupData.getGroupName());
nameLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
nameLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
nameLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0));
groupInfoPanel.add(nameLabel);
// 群聊ID
JLabel idLabel = new JLabel("群ID: " + groupData.getGroupId());
idLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
idLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12));
idLabel.setForeground(Color.DARK_GRAY);
idLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0));
groupInfoPanel.add(idLabel);
// 成员数量
JLabel memberCountLabel = new JLabel("成员: " + (groupData.getMembers() != null ? groupData.getMembers().size() : 0) + "");
memberCountLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
memberCountLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12));
memberCountLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
groupInfoPanel.add(memberCountLabel);
// 添加分隔线
JSeparator separator = new JSeparator();
separator.setForeground(Color.GRAY);
separator.setAlignmentX(Component.CENTER_ALIGNMENT);
separator.setMaximumSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 30, 1));
groupInfoPanel.add(separator);
}
/**
* 更新群成员面板内容
*/
private void updateGroupMemberPanel(GroupData groupData) {
// 添加成员标题
JLabel memberTitle = new JLabel("群成员 (" + (groupData.getMembers() != null ? groupData.getMembers().size() : 0) + ")");
memberTitle.setAlignmentX(Component.CENTER_ALIGNMENT);
memberTitle.setFont(new Font("微软雅黑", Font.BOLD, 16));
memberTitle.setForeground(Color.WHITE);
memberTitle.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0));
groupMemberPanel.add(memberTitle);
// 添加分隔线
JSeparator separator = new JSeparator();
separator.setForeground(Color.DARK_GRAY);
separator.setAlignmentX(Component.CENTER_ALIGNMENT);
separator.setMaximumSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 20, 1));
groupMemberPanel.add(separator);
if (groupData.getMembers() == null || groupData.getMembers().isEmpty()) {
JLabel noMemberLabel = new JLabel("暂无成员");
noMemberLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
noMemberLabel.setForeground(Color.LIGHT_GRAY);
noMemberLabel.setBorder(BorderFactory.createEmptyBorder(20, 0, 0, 0));
groupMemberPanel.add(noMemberLabel);
return;
}
// 添加成员列表
for (GroupData.GroupMember memberId : groupData.getMembers()) {
JPanel memberItemPanel =
createMemberItemPanel(memberId.id);
groupMemberPanel.add(memberItemPanel);
}
// 添加底部空白,确保内容居中
groupMemberPanel.add(Box.createVerticalGlue());
}
/**
* 创建单个成员信息面板
*/
private JPanel createMemberItemPanel(String memberId) {
JPanel memberItemPanel = new JPanel();
memberItemPanel.setLayout(new BoxLayout(memberItemPanel, BoxLayout.X_AXIS));
memberItemPanel.setBackground(new Color(40, 40, 40));
memberItemPanel.setBorder(BorderFactory.createEmptyBorder(8, 15, 8, 15));
memberItemPanel.setMaximumSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, 50));
memberItemPanel.setAlignmentX(Component.CENTER_ALIGNMENT);
// 成员头像(使用圆形标签模拟)
JLabel avatarLabel = new JLabel();
avatarLabel.setOpaque(true);
avatarLabel.setBackground(getMemberColor(memberId));
avatarLabel.setPreferredSize(new Dimension(30, 30));
avatarLabel.setMinimumSize(new Dimension(30, 30));
avatarLabel.setMaximumSize(new Dimension(30, 30));
avatarLabel.setBorder(BorderFactory.createLineBorder(Color.WHITE, 1));
// 设置圆形头像
avatarLabel.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(Color.WHITE, 1),
BorderFactory.createEmptyBorder(2, 2, 2, 2)
));
// 成员信息
JPanel infoPanel = new JPanel();
infoPanel.setLayout(new BoxLayout(infoPanel, BoxLayout.Y_AXIS));
infoPanel.setBackground(new Color(40, 40, 40));
infoPanel.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 0));
// 成员名称
String memberName = LocalData.get().getUserName(memberId);
if (memberName == null) {
memberName = "用户" + memberId.substring(0, Math.min(6, memberId.length()));
}
JLabel nameLabel = new JLabel(memberName);
nameLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
nameLabel.setForeground(Color.WHITE);
nameLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
// 成员ID
JLabel idLabel = new JLabel("ID: " + memberId.substring(0, Math.min(10, memberId.length())));
idLabel.setFont(new Font("微软雅黑", Font.PLAIN, 10));
idLabel.setForeground(Color.LIGHT_GRAY);
idLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
infoPanel.add(nameLabel);
infoPanel.add(idLabel);
memberItemPanel.add(avatarLabel);
memberItemPanel.add(infoPanel);
memberItemPanel.add(Box.createHorizontalGlue());
// 如果是当前用户,添加标识
if (memberId.equals(LocalData.get().getId())) {
JLabel meLabel = new JLabel("(我)");
meLabel.setFont(new Font("微软雅黑", Font.ITALIC, 11));
meLabel.setForeground(new Color(100, 150, 255));
memberItemPanel.add(meLabel);
}
return memberItemPanel;
}
/**
* 根据用户ID生成固定颜色(用于头像背景)
*/
private Color getMemberColor(String memberId) {
// 简单的哈希算法生成固定颜色
int hash = memberId.hashCode();
int r = (hash & 0xFF0000) >> 16;
int g = (hash & 0x00FF00) >> 8;
int b = hash & 0x0000FF;
// 确保颜色不太暗
r = Math.max(r, 50);
g = Math.max(g, 50);
b = Math.max(b, 50);
return new Color(r, g, b);
}
}
+214
View File
@@ -0,0 +1,214 @@
package client.view.main;
import client.service.LocalData;
import client.view.MainPage;
import client.view.util.CircleCharIcon2;
import client.view.util.DesignToken;
import client.view.util.RoundedRectCharIcon;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
// 群聊列表项组件
public class GroupListItem extends JPanel implements Comparable<String> {
private int unread;
private final String groupId;
JLabel icon;
JLabel badge;
JPanel centerPanel;
JLabel titleLabel;
JPanel rightPanel; // 用于放置徽章
/**
* 构造函数
*
* @param groupId 群聊ID
* @param title 群聊标题
* @param unread 未读消息数量
*/
public GroupListItem(String groupId, String title, int unread) {
this.groupId = groupId;
this.unread = unread;
// 设置布局管理器
setLayout(new BorderLayout(10, 0));
setBorder(BorderFactory.createEmptyBorder(8, 10, 8, 10));
setMaximumSize(new Dimension(Integer.MAX_VALUE, 70));
setPreferredSize(new Dimension(200, 70));
// 左侧图标
boolean isGroup = LocalData.get().getGroupData(groupId) != null;
if (isGroup) {
icon = new JLabel(new RoundedRectCharIcon(Color.decode(DesignToken.BUBBLE_COLOR_BLUE), Color.WHITE,
title.substring(0, 1).toUpperCase(), 40));
} else {
icon = new JLabel(new CircleCharIcon2(Color.ORANGE, Color.WHITE,
title.substring(0, 1).toUpperCase(), 40));
}
icon.setPreferredSize(new Dimension(40, 40));
// 中间区域 - 群聊标题
centerPanel = new JPanel(new GridLayout(1, 1));
titleLabel = new JLabel(title);
titleLabel.setFont(new Font(DesignToken.DEFAULT_FONT, Font.BOLD, 14));
centerPanel.add(titleLabel);
// 右侧区域 - 未读消息徽章
rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0));
rightPanel.setOpaque(false);
badge = new JLabel();
badge.setForeground(Color.WHITE);
badge.setBackground(Color.RED);
badge.setOpaque(true);
badge.setHorizontalAlignment(SwingConstants.CENTER);
badge.setFont(new Font("Microsoft YaHei", Font.BOLD, 10));
badge.setBorder(BorderFactory.createEmptyBorder(2, 6, 2, 6));
badge.setPreferredSize(new Dimension(20, 20));
updateBadge(); // 初始化徽章显示状态
rightPanel.add(badge);
// 组装
this.add(icon, BorderLayout.WEST);
this.add(centerPanel, BorderLayout.CENTER);
this.add(rightPanel, BorderLayout.EAST);
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
enterGroup();
}
@Override
public void mouseEntered(MouseEvent e) {
setBackground(new Color(240, 240, 240)); // 悬停效果
}
@Override
public void mouseExited(MouseEvent e) {
setBackground(UIManager.getColor("Panel.background")); // 恢复原背景
}
@Override
public void mousePressed(MouseEvent e) {
setBackground(new Color(220, 220, 220)); // 点击效果
}
@Override
public void mouseReleased(MouseEvent e) {
if (contains(e.getPoint())) {
setBackground(new Color(240, 240, 240));
} else {
setBackground(UIManager.getColor("Panel.background"));
}
}
});
}
/**
* 更新徽章显示
*/
private void updateBadge() {
if (unread > 0) {
String badgeText = unread > 99 ? "99+" : String.valueOf(unread);
badge.setText(badgeText);
badge.setVisible(true);
// 根据文本长度调整徽章大小
FontMetrics fm = badge.getFontMetrics(badge.getFont());
int width = fm.stringWidth(badgeText) + 12;
badge.setPreferredSize(new Dimension(width, 20));
} else {
badge.setVisible(false);
}
}
/**
* 更新组件
* 将组件上对应的信息修改。
* newUnread 是加在原有的unread上的
*/
public void updateUI(String name, int newUnread) {
titleLabel.setText(name);
// 更新图标的首字母显示
boolean isGroup = LocalData.get().getGroupData(groupId) != null;
if (isGroup) {
icon.setIcon(new RoundedRectCharIcon(Color.decode(DesignToken.BUBBLE_COLOR_BLUE), Color.WHITE,
name.substring(0, 1).toUpperCase(), 40));
} else {
icon.setIcon(new CircleCharIcon2(Color.ORANGE, Color.WHITE,
name.substring(0, 1).toUpperCase(), 40));
}
unread += newUnread;
updateBadge();
}
/**
* 点击事件
* 当点击之后,使用UIUpdater来更新ContentView的UI
* 使得其加载新的群聊信息
* 清空unread为0
* UIUpdater在ContentView展示当前群聊信息的时候不会更新这个群聊的未读信息数量。
*/
public void enterGroup() {
unread = 0;
LocalData.get().setCurrentChatId(groupId);
updateBadge(); // 更新徽章显示
String name;
if (LocalData.get().getFriends().containsKey(groupId)) {
name = LocalData.get().getFriends().get(groupId);
} else {
name = LocalData.get().getGroupName(groupId);
}
updateUI(name, 0);
MainPage.get().exchangeToChatRoom(groupId);
if (LocalData.get().getGroupData(groupId) != null) {
System.out.println(
"进入群聊:" + groupId +
",人数:" + LocalData.get().getGroupData(groupId).getMemberCount() +
", 信息数量: " + LocalData.get().getChatMsg(groupId).size());
} else {
System.out.println("进入私聊:" + groupId);
}
MainPage.get().revalidate(); // 重新计算布局
MainPage.get().repaint();
}
/**
* 获取未读消息数量
*/
public int getUnread() {
return unread;
}
/**
* 获取群组ID
*/
public String getGroupId() {
return groupId;
}
/**
* 实现Comparable接口,用于排序
* 按照群组ID进行排序
*
* @param o 要比较的对象
* @return 比较结果
*/
@Override
public int compareTo(String o) {
return this.groupId.compareTo(o);
}
}
@@ -0,0 +1,347 @@
package client.view.main;
import client.view.MainPage;
import client.view.util.LimitSizePanel;
import client.service.LocalData;
import javax.swing.*;
import java.awt.*;
import java.util.LinkedHashMap;
import java.util.Map;
import static client.view.util.DesignToken.SECONDARY_PANEL_WIDTH_MIN;
/**
* 二级菜单栏组件
* 用于显示聊天、好友、设置等二级选项。
* 点击不同选项可以切换到对应的功能界面。
*/
public class SecondaryOptionView extends LimitSizePanel {
private static volatile SecondaryOptionView instance;
public static SecondaryOptionView get() {
if (instance == null) {
synchronized (SecondaryOptionView.class) {
if (instance == null) {
instance = new SecondaryOptionView();
}
}
}
return instance;
}
// 二级选项模式
private enum Mode {
MESSAGE, FRIEND, GROUP, SETTING
}
// 当前二级选项模式
private Mode currentMode = Mode.MESSAGE;
// 这是一个群聊ID,组件的映射
private Map<String, GroupListItem> listItems;
// 关于好友ID,组件的映射
private JPanel chatContainer;
// 好友列表滚动面板
private JScrollPane scrollPane;
// 创建群聊按钮
private JButton createGroupButton;
// 二级选项标题标签
private JLabel titleLabel;
private SecondaryOptionView() {
super(SECONDARY_PANEL_WIDTH_MIN);
init();
listItems = new LinkedHashMap<>();
// 添加右侧边框分割线
this.setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, UIManager.getColor("Component.borderColor")));
setLayout(new BorderLayout());
// 创建顶部面板
JPanel topPanel = new JPanel(new BorderLayout());
topPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); // 增加内边距
titleLabel = new JLabel("消息"); // 或者 "群聊"/"好友",根据当前视图动态变化更好,这里先用通用标题
titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 16));
// 调整“+”按钮样式,使其更像一个功能图标
createGroupButton.setMargin(new Insets(2, 6, 2, 6));
createGroupButton.setFocusPainted(false);
topPanel.add(titleLabel, BorderLayout.WEST);
topPanel.add(createGroupButton, BorderLayout.EAST);
// 创建群聊容器
chatContainer = new JPanel();
chatContainer.setLayout(new BoxLayout(chatContainer, BoxLayout.Y_AXIS));
// 创建滚动面板
scrollPane = new JScrollPane(chatContainer);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.setBorder(BorderFactory.createEmptyBorder()); // 移除默认边框
// 自定义滚动条UI
JScrollBar verticalScrollBar = scrollPane.getVerticalScrollBar();
verticalScrollBar.setUnitIncrement(16);
verticalScrollBar.setPreferredSize(new Dimension(10, 0));
// 组装界面
JPanel headerContainer = new JPanel(new BorderLayout());
headerContainer.add(topPanel, BorderLayout.CENTER);
// 添加底部分割线,同时为了更好的层次感,也可以考虑添加顶部分割线(如果需要与标题栏分隔)
// 这里我们给上下都添加分割线,确保 headerContainer 与上面的 Window Title 和下面的 List 都有分隔线
headerContainer
.setBorder(BorderFactory.createMatteBorder(1, 0, 1, 0, UIManager.getColor("Component.borderColor")));
this.add(headerContainer, BorderLayout.NORTH);
this.add(scrollPane, BorderLayout.CENTER);
}
/**
* 创建一个用于创建群聊的按钮组件
* 按下这个组件后弹出一个创建群聊的对话框,填写完成后进行创建群聊的操作(调用DataManager的createGroupChat方法)
* 如果失败,则放弃创建群聊。
*
* @return 群聊创建按钮组件
*/
private JButton createGroupCreateBtn() {
JButton groupCreateBtn = new JButton("+");
return groupCreateBtn;
}
/**
* 初始化所有组件
* 读取DataManager的信息,而后群聊信息创建为GroupListItem,加入到类中对应的列表中
* 并将其添加到chatContainer中
* 同样,好友信息创建为FriendListItem,加入到类中对应的列表中
* 并添加到friendContainer中
* 最后使用exchangeToGroupChat将群聊列表加入到聊天项容器中
* 使用createGroupCreateBtn创建群聊创建按钮,并添加到chatContainer中
*/
public void init() {
createGroupButton = createGroupCreateBtn();
createGroupButton.addActionListener(e -> {
showAddMenu(createGroupButton);
});
}
/**
* 显示添加菜单
* 根据当前模式(群聊/好友/设置),显示不同的添加选项
* 例如,在群聊模式下,显示创建群聊和加入群聊选项
* 在好友模式下,显示添加好友选项
* 在设置模式下,显示不同的设置选项
*/
private void showAddMenu(Component invoker) {
JPopupMenu popupMenu = new JPopupMenu();
if (currentMode == Mode.GROUP) {
JMenuItem createGroupItem = new JMenuItem("创建群聊");
createGroupItem.addActionListener(e -> MainPage.get().showGroupCreateDialog());
popupMenu.add(createGroupItem);
JMenuItem joinGroupItem = new JMenuItem("加入群聊");
joinGroupItem.addActionListener(e -> {
MainPage.get().showJoinGroupDialog();
});
popupMenu.add(joinGroupItem);
} else if (currentMode == Mode.FRIEND) {
JMenuItem addFriendItem = new JMenuItem("添加好友");
addFriendItem.addActionListener(e -> {
MainPage.get().showAddFriendDialog();
});
popupMenu.add(addFriendItem);
}
popupMenu.show(invoker, 0, invoker.getHeight());
}
/**
* 更新消息列表的UI
* 根据传入的groupId,更新对应groupListItems的群聊列表项的UI
* 这里需要先删除createGroupButton按钮,而后进行更新操作后再添加回来。
* 如果没有对应的id,则创建新的GroupListItem,并添加到groupListItems中
*/
public void updateGroupList(String groupId, String title, int unreadCount) {
if (listItems.containsKey(groupId)) {
listItems.get(groupId).updateUI(title, unreadCount);
} else {
GroupListItem item = new GroupListItem(groupId, title, 0);
listItems.put(groupId, item);
}
// 只有在 MESSAGE 模式下才更新 UI 容器
if (currentMode == Mode.MESSAGE) {
// 简单处理:重新加载所有 item 保证顺序,或者只添加新的
// 为了简单,如果它不在容器里,加进去
GroupListItem item = listItems.get(groupId);
boolean alreadyIn = false;
for (Component c : chatContainer.getComponents()) {
if (c == item) {
alreadyIn = true;
break;
}
}
if (!alreadyIn) {
chatContainer.add(item);
}
chatContainer.revalidate();
chatContainer.repaint();
}
}
/**
* 更新消息列表(兼容群聊和私聊)
*/
public void updateMessageList(String id, String name, String content, int unread) {
updateGroupList(id, name, unread);
}
/**
* 如果当前处于群聊模式,刷新群聊列表
* 用于处理新加入群聊时的列表更新
*/
public void refreshIfInGroupMode() {
if (currentMode == Mode.GROUP) {
exchangeToGroupList();
}
}
/**
* 删除指定的群聊列表
* 如果当前模式是CHAT,且groupId存在于listItems中,
* 则从chatContainer中移除对应的GroupListItem组件,
* 并从listItems中删除该条目。
* 最后调用chatContainer的revalidate和repaint方法更新UI。
*/
public void removeGroupListItem(String groupId) {
if (listItems.containsKey(groupId)) {
GroupListItem item = listItems.get(groupId);
listItems.remove(groupId);
if (currentMode == Mode.MESSAGE || currentMode == Mode.GROUP) {
chatContainer.remove(item);
chatContainer.revalidate();
chatContainer.repaint();
}
}
}
/**
* 切换到设置列表模式
* 将当前模式设置为SETTING,更新标题为"设置",隐藏创建群聊按钮,清空聊天项容器。
* 然后添加个人信息和关于软件的设置项按钮到聊天项容器中。
* 最后调用revalidate和repaint方法更新UI。
*/
public void exchangeToSettingList() {
currentMode = Mode.SETTING;
titleLabel.setText("设置");
createGroupButton.setVisible(false);
chatContainer.removeAll();
// 添加设置项
chatContainer.add(createSettingItem("个人信息", "info"));
chatContainer.add(createSettingItem("关于软件", "about"));
chatContainer.revalidate();
chatContainer.repaint();
MainPage.get().exchangeToBlankContent(); // 右侧清空或显示默认页
}
/**
* 创建一个设置项按钮
* 按钮的文本为text,类型为type。
* 按钮的对齐方式为居中对齐,最大宽度为Integer.MAX_VALUE,高度为50。
* 点击按钮时,调用MainPage的exchangeToSettings方法,传入type参数。
*
* @param text 按钮的文本
* @param type 按钮的类型
* @return 一个设置项按钮组件
*/
private JButton createSettingItem(String text, String type) {
JButton btn = new JButton(text);
btn.setAlignmentX(Component.CENTER_ALIGNMENT);
btn.setMaximumSize(new Dimension(Integer.MAX_VALUE, 50));
btn.setFocusPainted(false);
btn.setBackground(Color.WHITE);
btn.addActionListener(e -> MainPage.get().exchangeToSettings(type));
return btn;
}
/**
* 切换到消息列表模式
* 将当前模式设置为MESSAGE,更新标题为"消息",隐藏创建群聊按钮,清空聊天项容器。
* 然后遍历listItems中的所有GroupListItem组件,添加到聊天项容器中。
* 最后调用chatContainer的revalidate和repaint方法更新UI。
*/
public void exchangeToMessageList() {
currentMode = Mode.MESSAGE;
titleLabel.setText("消息");
createGroupButton.setVisible(false);
chatContainer.removeAll();
// 恢复消息列表
for (GroupListItem item : listItems.values()) {
chatContainer.add(item);
}
chatContainer.revalidate();
chatContainer.repaint();
}
/**
* 切换到群聊列表模式
* 将当前模式设置为GROUP,更新标题为"群聊",显示创建群聊按钮,清空聊天项容器。
* 然后遍历LocalData中的所有群聊,创建GroupListItem组件并添加到聊天项容器中。
* 最后调用chatContainer的revalidate和repaint方法更新UI。
*/
public void exchangeToGroupList() {
currentMode = Mode.GROUP;
titleLabel.setText("群聊");
createGroupButton.setVisible(true);
chatContainer.removeAll();
java.util.List<server.data.GroupData> groups = LocalData.get().getAllGroups();
if (groups != null) {
for (server.data.GroupData group : groups) {
// 这里我们复用GroupListItem,未读数设为0
GroupListItem item = new GroupListItem(group.getGroupId(), group.getGroupName(), 0);
chatContainer.add(item);
}
}
chatContainer.revalidate();
chatContainer.repaint();
}
/**
* 切换到好友列表模式
* 将当前模式设置为FRIEND,更新标题为"好友",隐藏创建群聊按钮,清空聊天项容器。
* 然后遍历LocalData中的好友列表,创建FriendListItem组件并添加到聊天项容器中。
* 最后调用chatContainer的revalidate和repaint方法更新UI。
*/
public void exchangeToFriendList() {
currentMode = Mode.FRIEND;
titleLabel.setText("好友");
createGroupButton.setVisible(true); // 显示加号按钮
chatContainer.removeAll();
Map<String, String> friends = LocalData.get().getFriends();
if (friends != null) {
for (Map.Entry<String, String> entry : friends.entrySet()) {
FriendListItem item = new FriendListItem(entry.getKey(), entry.getValue());
chatContainer.add(item);
}
}
chatContainer.revalidate();
chatContainer.repaint();
MainPage.get().exchangeToBlankContent();
}
}
+198
View File
@@ -0,0 +1,198 @@
package client.view.main;
import client.service.ChatSender;
import client.service.LocalData;
import client.view.MainPage;
import client.view.util.DesignToken;
import server.data.UserData;
import server.serveice.Wrapper;
import javax.swing.*;
import java.awt.*;
/**
* 设置界面组件
* 用于显示用户个人信息和关于 LocalChatApp 的设置选项。
* 包括用户ID、用户名、退出登录等功能。
*/
public class SettingsView extends JPanel {
public SettingsView(String type) {
initUI(type);
}
/**
* 初始化设置界面组件
* 用于根据界面类型显示不同的设置内容。
* 如果是 "info" 类型,显示用户个人信息,包括用户ID和用户名。
* 如果是 "about" 类型,显示关于 LocalChatApp 的信息,包括版本号等。
* @param type 界面类型,"info" 显示个人信息,"about" 显示关于 LocalChatApp 的信息
*/
private void initUI(String type) {
setLayout(new BorderLayout());
setBackground(Color.WHITE);
JPanel contentPanel = new JPanel();
contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
contentPanel.setBorder(BorderFactory.createEmptyBorder(40, 40, 40, 40));
contentPanel.setBackground(Color.WHITE);
if ("info".equals(type)) {
addInfoContent(contentPanel);
} else if ("about".equals(type)) {
addAboutContent(contentPanel);
}
add(contentPanel, BorderLayout.CENTER);
}
/**
* 添加个人信息内容到设置界面
* 包括用户ID、用户名、邮箱、生日、地址、签名等。
* @param panel 用于添加组件的面板
*/
private void addInfoContent(JPanel panel) {
JLabel title = new JLabel("个人信息");
title.setFont(new Font(DesignToken.DEFAULT_FONT, Font.BOLD, 24));
title.setAlignmentX(Component.LEFT_ALIGNMENT);
panel.add(title);
panel.add(Box.createVerticalStrut(20));
String myId = LocalData.get().getId();
UserData myData = LocalData.get().getUserDetail(myId);
if (myData == null) {
// 如果数据尚未同步,使用本地基本信息创建一个临时对象
myData = new UserData(LocalData.get().getUserName(myId), myId, null);
}
addLabel(panel, "用户ID:", myId);
addLabel(panel, "用户名:", myData.getNickname());
// 可编辑字段
JTextField emailField = addEditableField(panel, "邮箱:", myData.getEmail());
JTextField birthdayField = addEditableField(panel, "生日:", myData.getBirthday());
JTextField addressField = addEditableField(panel, "地址:", myData.getAddress());
JTextField signatureField = addEditableField(panel, "个性签名:", myData.getSignature());
panel.add(Box.createVerticalStrut(30));
// 保存修改按钮
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
btnPanel.setBackground(Color.WHITE);
btnPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
// 保存修改按钮
JButton saveBtn = new JButton("保存修改");
final UserData currentData = myData;
saveBtn.addActionListener(e -> {
// 更新本地对象字段
currentData.setEmail(emailField.getText().trim());
currentData.setBirthday(birthdayField.getText().trim());
currentData.setAddress(addressField.getText().trim());
currentData.setSignature(signatureField.getText().trim());
// 发送更新请求
ChatSender.addMsg(Wrapper.updateUserDetailRequest(myId, currentData));
// 更新本地缓存(虽然服务器会广播回来,但本地先更新体验更好)
LocalData.get().updateUserDetails(currentData);
JOptionPane.showMessageDialog(this, "个人信息已保存", "提示", JOptionPane.INFORMATION_MESSAGE);
});
// 退出登录按钮
JButton logoutBtn = new JButton("退出登录");
logoutBtn.addActionListener(e -> {
int confirm = JOptionPane.showConfirmDialog(this, "确定要退出登录吗?", "提示", JOptionPane.YES_NO_OPTION);
if (confirm == JOptionPane.YES_OPTION) {
MainPage.get().openLogInPage();
}
});
// 添加按钮到面板
btnPanel.add(saveBtn);
btnPanel.add(logoutBtn);
panel.add(btnPanel);
}
/**
* 添加可编辑文本字段到设置界面
* 用于用户输入个人信息的编辑。
* @param panel 用于添加组件的面板
* @param labelText 标签文本,描述字段的作用
* @param value 初始文本字段值
* @return 新创建的 JTextField 对象
*/
private JTextField addEditableField(JPanel panel, String labelText, String value) {
JPanel fieldPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
fieldPanel.setBackground(Color.WHITE);
fieldPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
JLabel label = new JLabel(labelText);
label.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 16));
label.setPreferredSize(new Dimension(80, 30));
JTextField textField = new JTextField(value, 20);
textField.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 14));
fieldPanel.add(label);
fieldPanel.add(textField);
panel.add(fieldPanel);
return textField;
}
/**
* 添加关于 LocalChatApp 的内容到设置界面
* 包括版本号、开发团队、应用描述等。
* @param panel 用于添加组件的面板
*/
private void addAboutContent(JPanel panel) {
JLabel title = new JLabel("关于 LocalChatApp");
title.setFont(new Font(DesignToken.DEFAULT_FONT, Font.BOLD, 24));
title.setAlignmentX(Component.LEFT_ALIGNMENT);
panel.add(title);
panel.add(Box.createVerticalStrut(30));
JLabel version = new JLabel("版本: v1.0.0");
version.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 16));
panel.add(version);
panel.add(Box.createVerticalStrut(10));
JLabel author = new JLabel("开发团队: 添砖加瓦小组");
author.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 16));
panel.add(author);
panel.add(Box.createVerticalStrut(10));
JLabel desc = new JLabel("<html><body><p style='width:300px'>基于Java Swing和Socket开发的本地局域网聊天室。</p></body></html>");
desc.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 14));
panel.add(desc);
}
/**
* 添加标签到设置界面
* 用于显示键值对信息,例如用户ID和用户名。
* @param panel 用于添加组件的面板
* @param key 键,例如 "用户ID:"
* @param value 值,例如 "10086"
*/
private void addLabel(JPanel panel, String key, String value) {
// 创建一个行面板,用于添加键值对标签
JPanel row = new JPanel(new FlowLayout(FlowLayout.LEFT));
row.setBackground(Color.WHITE);
row.setAlignmentX(Component.LEFT_ALIGNMENT);
// 键标签
JLabel k = new JLabel(key);
k.setFont(new Font(DesignToken.DEFAULT_FONT, Font.BOLD, 14));
k.setPreferredSize(new Dimension(80, 30));
// 值标签
JLabel v = new JLabel(value);
v.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 14));
// 添加键值对标签到行面板
row.add(k);
row.add(v);
panel.add(row);
}
}
+177
View File
@@ -0,0 +1,177 @@
package client.view.main;
import com.formdev.flatlaf.FlatDarkLaf;
import com.formdev.flatlaf.FlatLaf;
import com.formdev.flatlaf.FlatLightLaf;
import client.view.util.CircleCharIcon2;
import client.view.util.DesignToken;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
/**
* 左侧侧边栏组件
* 包含消息、好友、群聊、设置、黑暗模式切换按钮
*/
public class SideOptionView extends JPanel {
private static volatile SideOptionView instance;
/**
* 获取单例实例
*
* @return 单例实例
*/
public static SideOptionView get() {
if (instance == null) {
synchronized (SideOptionView.class) {
if (instance == null) {
instance = new SideOptionView();
}
}
}
return instance;
}
/**
* 按照原型图,生成三个按钮组件:群聊,好友,设置组件
* 每个组件都设置一个图标
* 为每一个按钮配置一个事件
* 群聊:exchangeToChatPage()/UIUpdate
* 设置: exchangeToSettingPage()/UIUpdate。这个当前还没做,提示用户正在制作中。
* 现在暂时没有设置相关功能
*/
public SideOptionView() {
this.setLayout(new GridLayout(5, 1));
// this.setBackground(Color.GRAY);
// 添加右侧边框分割线
this.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 0, 1, UIManager.getColor("Component.borderColor")),
BorderFactory.createEmptyBorder(5, 5, 5, 5)));
// 初始化五个核心按钮:消息、好友、群聊、设置、黑暗模式切换按钮
initMessageButton();
initFriendButton();
initGroupButton();
initSettingButton();
initDarkModeButton();
}
/**
* 初始化消息按钮
* 点击后切换到消息页面
*/
private void initMessageButton() {
JButton messageBtn = createIconButton("", Color.LIGHT_GRAY);
messageBtn.setToolTipText("消息");
messageBtn.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
SecondaryOptionView.get().exchangeToMessageList();
}
});
this.add(messageBtn);
}
/**
* 初始化好友按钮
* 点击后切换到好友页面
*/
private void initFriendButton() {
JButton friendBtn = createIconButton("", Color.LIGHT_GRAY);
friendBtn.setToolTipText("好友");
friendBtn.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
SecondaryOptionView.get().exchangeToFriendList();
}
});
this.add(friendBtn);
}
/**
* 初始化群聊按钮
* 点击后切换到群聊页面
*/
private void initGroupButton() {
JButton groupBtn = createIconButton("", Color.LIGHT_GRAY);
groupBtn.setToolTipText("群聊");
groupBtn.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
SecondaryOptionView.get().exchangeToGroupList();
}
});
this.add(groupBtn);
}
/**
* 初始化设置按钮
* 点击后切换到设置页面
*/
private void initSettingButton() {
JButton settingBtn = createIconButton("", Color.LIGHT_GRAY);
settingBtn.setToolTipText("设置");
settingBtn.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
SecondaryOptionView.get().exchangeToSettingList();
}
});
this.add(settingBtn);
}
/**
* 初始化黑暗模式切换按钮
*/
private void initDarkModeButton() {
JButton darkModeBtn = createIconButton("", Color.DARK_GRAY);
darkModeBtn.setToolTipText("切换模式");
darkModeBtn.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (FlatLaf.isLafDark()) {
try {
DesignToken.setDarkMode(false);
FlatLightLaf.setup();
FlatLaf.updateUI();
ChatInfoView.get().updateTheme();
// 切换图标
darkModeBtn.setIcon(new CircleCharIcon2(Color.DARK_GRAY, Color.WHITE, "", 36));
darkModeBtn.setToolTipText("切换到黑暗模式");
} catch (Exception ex) {
ex.printStackTrace();
}
} else {
try {
DesignToken.setDarkMode(true);
FlatDarkLaf.setup();
FlatLaf.updateUI();
ChatInfoView.get().updateTheme();
// 切换图标
darkModeBtn.setIcon(new CircleCharIcon2(Color.LIGHT_GRAY, Color.BLACK, "", 36));
darkModeBtn.setToolTipText("切换到明亮模式");
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
});
this.add(darkModeBtn);
}
/**
* 创建带圆形图标的按钮
*/
private JButton createIconButton(String text, Color bgColor) {
JButton button = new JButton();
button.setPreferredSize(new Dimension(40, 40));
button.setIcon(new CircleCharIcon2(bgColor, Color.WHITE, text, 36));
button.setBorderPainted(false);
button.setContentAreaFilled(false);
button.setFocusPainted(false);
return button;
}
}
+50
View File
@@ -0,0 +1,50 @@
package client.view.util;
import javax.swing.*;
import java.awt.*;
public class CircleCharIcon2 implements Icon {
private Color circleColor;
private Color textColor;
private String character;
private int size;
public CircleCharIcon2(Color circleColor, Color textColor, String character, int size) {
this.circleColor = circleColor;
this.textColor = textColor;
this.character = character;
this.size = size;
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
Graphics2D g2d = (Graphics2D) g;
// 开启抗锯齿
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 绘制圆形
g2d.setColor(circleColor);
g2d.fillOval(x, y, size - 1, size - 1);
// 绘制字符
g2d.setColor(textColor);
g2d.setFont(new Font("Microsoft YaHei", Font.BOLD, size / 2));
FontMetrics fm = g2d.getFontMetrics();
int textWidth = fm.stringWidth(character);
int textHeight = fm.getAscent();
int textX = x + (size - textWidth) / 2;
int textY = y + (size - textHeight) / 2 + (int) (fm.getAscent() / 1.5);
g2d.drawString(character, textX, textY);
}
@Override
public int getIconWidth() {
return size;
}
@Override
public int getIconHeight() {
return size;
}
}
+97
View File
@@ -0,0 +1,97 @@
package client.view.util;
import java.io.Serializable;
/**
* 用于设定 UI元素的各种变量
*/
public class DesignToken implements Serializable {
private static final long serialVersionUID = -3791869536619757015L;
//============================窗口大小==============================
// 主界面大小
public final static int WINDOW_ORI_WIDTH = 1200;
public final static int WINDOW_ORI_HEIGHT = 600;
public final static int WINDOW_MIN_WIDTH = 800;
public final static int WINDOW_MIN_HEIGHT = 600;
// 登录界面大小
public final static int LOGIN_WIDTH = 400;
public final static int LOGIN_HEIGHT = 300;
// 主界面组件大小
public final static int SIDE_PANEL_WIDTH = 50;
public final static int SECONDARY_PANEL_WIDTH = 200;
public final static int SECONDARY_PANEL_WIDTH_MIN = 150;
public final static int CONTENT_PANEL_WIDTH = 570;
// 内容组件大小
public final static int CHAT_PANEL_WIDTH = 400;
public final static int INPUT_AREA_HEIGHT = 100;
public final static int MSG_BUBBLE_WIDTH = 50;
public final static int INFO_PANEL_WIDTH = 150;
public final static int CONTENT_PANEL_WIDTH_MIN = 300;
public final static int GROUP_INFO_PANEL_WIDTH = 240;
public final static int GROUP_CHAT_PANEL_WIDTH = 210;
//============================字体==================================
public static int FONT_SIZE = 14;
public static int FONT_SIZE_SMALL = 12;
public static int FONT_SIZE_TITLE = 20;
public static int FONT_SIZE_TITLE_MIN = 18;
public static String DEFAULT_FONT = "微软雅黑";
//=============================颜色=================================
// public static String COLOR_BACKGROUND = "#ddebee";
public static String BUBBLE_COLOR_GREEN = "#5aca58";
public static String BUBBLE_COLOR_WHITE = "#c8cdcd";
public static String BUBBLE_COLOR_GRAY = "#8a8a8a";
public static String BUBBLE_COLOR_RED = "#ff4d4d";
public static String BUBBLE_COLOR_BLUE = "#4097bc";
public static String BUBBLE_COLOR_YELLOW = "#f7b500";
public static String BACKGROUND_COLOR = "#c7d9d9";
public static String EDGE_COLOR = "#aab7b7";
public static String COLOR_FONT_BLUE = "#4097bc";// blue
public static String COLOR_FONT_BLACK = "#000000";// black
public static String COLOR_FONT_WHITE = "#ffffff";// white
public static String COLOR_FONT_GRAY = "#8a8a8a";// gray
public static String COLOR_FONT_GREEN = "#008000";// green
public static String COLOR_FONT_RED = "#ff0000";// red
public static String COLOR_FONT_YELLOW = "#ffff00";// yellow
public static String COLOR_FONT_ORANGE = "#ff8c00";// orange
public static String COLOR_FONT_PURPLE = "#800080";// purple
public static String COLOR_HEAD_BLACK = "#000000"; // black
public static String COLOR_HEAD_GRAY = "#8a8a8a"; // gray
public static String COLOR_HEAD_WHITE = "#ffffff"; // white
public static String COLOR_HEAD_BLUE = "#4097bc"; // blue
public static String COLOR_HEAD_GREEN = "#008000"; // green
public static String COLOR_HEAD_RED = "#ff0000"; // red
public static String COLOR_HEAD_YELLOW = "#ffff00"; // yellow
public static String COLOR_HEAD_ORANGE = "#ff8c00"; // orange
public static String COLOR_HEAD_PURPLE = "#800080"; // purple
public static void setDarkMode(boolean isDark) {
if (isDark) {
BACKGROUND_COLOR = "#3c3f41";
EDGE_COLOR = "#5e6060";
BUBBLE_COLOR_WHITE = "#505050"; // 白色气泡颜色(其他用户)
COLOR_FONT_BLACK = "#dddddd"; // 黑色字体颜色(其他用户)
BUBBLE_COLOR_GREEN = "#2e7d32"; // 绿色气泡颜色(自己)
} else {
BACKGROUND_COLOR = "#c7d9d9";
EDGE_COLOR = "#aab7b7";
BUBBLE_COLOR_WHITE = "#c8cdcd";
COLOR_FONT_BLACK = "#000000";
BUBBLE_COLOR_GREEN = "#5aca58";
}
}
//=============================文本域输入字数限制=================================
public static int MAX_FONT_SIZE = 20;
}
+32
View File
@@ -0,0 +1,32 @@
package client.view.util;
import javax.swing.*;
import java.awt.*;
/**
* 一个可以限制最小宽度的面板,由于splitPane中
*/
public class LimitSizePanel extends JPanel {
private int minWidth;
public LimitSizePanel(int minWidth) {
super();
this.minWidth = minWidth;
}
@Override
public Dimension getMinimumSize() {
// 设置最小尺寸
Dimension dim = super.getMinimumSize();
dim.width = Math.max(dim.width, minWidth);
return dim;
}
@Override
public Dimension getPreferredSize() {
// 设置首选尺寸
Dimension dim = super.getPreferredSize();
dim.width = Math.max(dim.width, minWidth * 2);
return dim;
}
}
@@ -0,0 +1,81 @@
package client.view.util;
import javax.swing.*;
import java.awt.*;
/**
* 圆角矩形字符图标
*/
public class RoundedRectCharIcon implements Icon {
private Color bgColor; // 背景颜色
private Color textColor; // 字体颜色
private String character; // 显示的字符
private int size; // 图标大小
private int arc; // 圆角大小
/**
* 构造函数
*
* @param bgColor 背景颜色
* @param textColor 字体颜色
* @param character 显示的字符
* @param size 图标大小
*/
public RoundedRectCharIcon(Color bgColor, Color textColor, String character, int size) {
this.bgColor = bgColor;
this.textColor = textColor;
this.character = character;
this.size = size;
this.arc = size / 3; // 圆角大小
}
/**
* 绘制图标
*
* @param c 组件
* @param g 图形上下文
* @param x 图标左上角的 x 坐标
* @param y 图标左上角的 y 坐标
*/
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
Graphics2D g2d = (Graphics2D) g;
// 开启抗锯齿
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 绘制圆角矩形
g2d.setColor(bgColor);
g2d.fillRoundRect(x, y, size - 1, size - 1, arc, arc);
// 绘制字符
g2d.setColor(textColor);
g2d.setFont(new Font("Microsoft YaHei", Font.BOLD, size / 2));
FontMetrics fm = g2d.getFontMetrics();
int textWidth = fm.stringWidth(character);
int textHeight = fm.getAscent();
int textX = x + (size - textWidth) / 2;
int textY = y + (size - textHeight) / 2 + (int) (fm.getAscent() / 1.5);
g2d.drawString(character, textX, textY);
}
/**
* 获取图标宽度
*
* @return 图标宽度
*/
@Override
public int getIconWidth() {
return size;
}
/**
* 获取图标高度
*
* @return 图标高度
*/
@Override
public int getIconHeight() {
return size;
}
}
+103
View File
@@ -0,0 +1,103 @@
package global;
/**
* 全局配置类
* 全局变量均在此处,不要随意更改!!!局部变量命名不要有冲突
*/
public class global {
// ====================== 1. 网络与基础配置 ======================
public static final int SERVER_PORT = 1145; // 服务端监听端口,两端统一
public static final String LOCAL_HOST = "127.0.0.1"; // 本地IP,客户端连接服务端用
public static final String SERVER_ACCOUNT = "admin"; // 服务端账户名
public static final int AUTO_SAVE_PERIOD = 1200; // 自动保存周期(单位:秒)
public static final String DATA_SPLIT = "\uE000"; // 数据分隔符
// ====================== 2. 客户端请求操作码 (OPT) ======================
// 注:常量值需保持唯一,避免冲突
// ------ 账户注册与登录 (1-4) ------
public static final int OPT_REGISTER = 1; // 注册
public static final int OPT_REGISTER_SUCCESS = 11; // 注册成功
public static final int OPT_REGISTER_FAILED_ACC = 12; // 注册失败:账号存在
public static final int OPT_REGISTER_FAILED_FORMAT = 13;// 注册失败:格式错误
public static final int OPT_LOGIN = 2; // 登录
public static final int OPT_LOGIN_SUCCESS = 25; // 登录成功
public static final int OPT_ERROR_NOT_LOGIN = 21; // 错误:还未登录
public static final int OPT_LOGIN_FAILED_PWD = 22; // 登录失败:密码错误
public static final int OPT_LOGIN_FAILED_ACC = 23; // 登录失败:账号错误
public static final int OPT_LOGIN_FAILED_REPEATED = 24; // 登录失败:重复登录
public static final int OPT_LOGOUT = 3; // 登出
public static final int OPT_DELETE_ACCOUNT = 4; // 注销/删除账号
// ------ 用户信息维护 (41-49) ------
public static final int OPT_UPDATE_NICKNAME = 41; // 更新昵称
public static final int OPT_UPDATE_PASSWORD = 42; // 更新密码
public static final int OPT_USER_UPDATE_NAME_FAILED = 411; // 更新昵称失败
public static final int OPT_USER_UPDATE_PASSWORD_FAILED = 412; // 更新密码失败
public static final int OPT_USER_UPDATE_NAME_FAILED_WRONG_FORMAT = 413; // 更新昵称失败:格式错误
// ------ 群组功能 (5-7) ------
public static final int OPT_GROUP_CREATE = 5; // 创建群聊
public static final int OPT_GROUP_CREATE_SUCCESS = 51; // 创建群聊成功
public static final int OPT_GROUP_INVITE = 6; // 邀请加入群聊
public static final int OPT_GROUP_INVITE_AGREE = 61; // 同意加入群聊
public static final int OPT_GROUP_INVITE_REFUSE = 62; // 拒绝加入群聊
public static final int OPT_GROUP_INVITE_OFFLINE = 63; // 邀请加入群聊失败:用户离线
public static final int OPT_GROUP_JOIN = 64; // 申请加入群聊
public static final int OPT_GROUP_JOIN_SUCCESS = 65; // 申请加入群聊成功
public static final int OPT_GROUP_JOIN_FAILED = 66; // 申请加入群聊失败
public static final int OPT_GROUP_QUIT = 7; // 退出群聊
public static final int OPT_GROUP_DISBAND = 71; // 解散群聊
public static final int OPT_GROUP_UPDATE_NAME = 72; // 更新群聊名字
public static final int OPT_GROUP_UPDATE_OWNER = 73; // 更新群聊拥有者
// ------ 好友功能 (67-69) ------
public static final int OPT_FRIEND_ADD = 67; // 申请添加好友
public static final int OPT_FRIEND_ADD_SUCCESS = 68; // 申请添加好友成功
public static final int OPT_FRIEND_ADD_FAILED = 69; // 申请添加好友失败
public static final int OPT_FRIEND_ADD_AGREE = 681; // 同意添加好友
public static final int OPT_FRIEND_ADD_REFUSE = 682; // 拒绝添加好友
// ------ 聊天消息 (8) ------
public static final int OPT_CHAT = 8; // 群聊消息
public static final int OPT_PRIVATE_CHAT = 81; // 私聊消息
// ------ 数据初始化与同步 (9) ------
public static final int OPT_INIT_CHAT = 9; // 初始化:群聊历史消息
public static final int OPT_INIT_USER = 91; // 初始化:在线用户列表
public static final int OPT_INIT_GROUP = 92; // 初始化:群组列表
public static final int SERVER_MESSAGE = 93; // 服务器系统消息
// 扩展用户信息 (v2)
public static final int OPT_INIT_USER_DETAIL = 94; // 初始化:用户详细信息
public static final int OPT_UPDATE_USER_DETAIL = 95; // 更新:用户详细信息
// ------ 系统控制 ------
public static final int OPT_EXIT = 999; // 服务器关闭通知
public static final int OPT_QUEST_WRONG = 404; // 请求错误
// ====================== 3. 响应提示信息 (MSG) ======================
// 配合结果码,客户端直接展示给用户
public static final String MSG_SUCCESS = "操作成功";
public static final String MSG_ACCOUNT_EXIST = "注册失败:该账户已存在(账户唯一)";
public static final String MSG_ACCOUNT_NOT_EXIST = "登录失败:该账户不存在";
public static final String MSG_PWD_ERROR = "登录失败:密码与账户不匹配";
public static final String MSG_UNKNOWN_OPT = "请求失败:未知的操作类型";
public static final String MSG_DATA_ERROR = "请求失败:数据格式错误";
// ====================== 4. 聊天应用配置 ======================
public static final int MAX_MSG_SEND_GAP = 10; // 消息最大发送事件间隔/秒
public static final int DISCONNECT_TIMEOUT = 30; // 断开连接超时时间/秒
public static final String DEFAULT_GROUP_ID = "group_default"; // 默认群ID
public static final String CHAT_MSG_PREFIX = "【系统消息】"; // 系统消息前缀
// ====================== 5. 运行时状态标识 ======================
public final static boolean IS_ONLINE = true;
public final static boolean IS_OFFLINE = false;
}
+64
View File
@@ -0,0 +1,64 @@
package server;
import server.data.ServerData;
import util.FileUtil;
import java.util.Scanner;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Server {
public static void main(String[] args) {
//启动服务器
ServerMainThread serverThread = new ServerMainThread();
serverThread.start();
//启动定时任务:保存服务器数据
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, r -> {
Thread t = new Thread(r);
t.setDaemon(true); // 设置为守护线程
return t;
});
// 每隔半小时执行一次
scheduler.scheduleAtFixedRate(() -> {
System.out.println("保存数据中");
FileUtil.saveServerData();
}, 1200, 1200, TimeUnit.SECONDS);
Scanner sc = new Scanner(System.in);
while (true) {
// System.out.print("SERVER_CMD>>");
String cmd = sc.nextLine();
switch (cmd) {
case "shutdown":
serverThread.shutdown();
System.out.println("=======服务器已关闭=======");
System.exit(0);
break;
case "groupInfo":
System.out.println("=======群聊列表=======");
ServerData.getInstance().getServerGroups().values().forEach(
group -> System.out.println(group.getGroupId() + " " + group.getGroupName())
);
break;
case "chatThreadStatus":
System.out.println("=======聊天线程状态=======");
System.out.println(ServerMainThread.getChatThreadPoolStatus());
break;
case "receiveThreadStatus":
System.out.println("=======接收线程状态=======");
System.out.println(ServerMainThread.getReceiveThreadPoolStatus());
break;
case "blockingQueueStatus":
System.out.println("=======阻塞队列状态=======");
System.out.println(ServerMainThread.getBlockingQueueStatus());
break;
default:
System.out.println("无效指令");
}
}
}
}
+224
View File
@@ -0,0 +1,224 @@
package server;
import server.serveice.*;
import util.FileUtil;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import static global.global.SERVER_PORT;
public class ServerMainThread extends Thread {
// 服务器运行状态
private static volatile boolean running = true;
// 线程池:用于处理聊天消息的线程池
private static ExecutorService chatThreadPool;
// 线程池:用于接收客户端消息的线程池
private static ExecutorService receiveThreadPool;
// 用于存储每个客户端的消息队列
private static ConcurrentHashMap<Socket, ArrayBlockingQueue<Wrapper>> msgQueues;
// 核心:启动服务、监听端口、循环接收客户端连接
@Override
public void run() {
System.out.println("加载本地数据成功");
// 初始化推送信息线程池
chatThreadPool = new ThreadPoolExecutor(
10, // 核心线程数
50, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new SynchronousQueue<>(), // 直接提交队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
receiveThreadPool = new ThreadPoolExecutor(
10, // 核心线程数
50, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new SynchronousQueue<>(), // 直接提交队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 初始化消息队列
msgQueues = new ConcurrentHashMap<>();
System.out.println("初始化信息线程池成功");
System.out.println("服务器启动成功");
try {
ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
while (running) {
// 扫描端口,接收链接请求,如果有链接请求,则尝试链接
Socket clientSocket = serverSocket.accept();
System.out.println("有新的用户端连接: " + clientSocket.getPort());
// 创建线程,处理客户端请求
ArrayBlockingQueue<Wrapper> threadQueue = new ArrayBlockingQueue<>(40);
msgQueues.put(clientSocket, threadQueue);
ClientChatThread clientChatThread = new ClientChatThread(clientSocket, threadQueue);
chatThreadPool.submit(clientChatThread);
Thread.sleep(200);
ClientReceiveThread clientReceiveThread = new ClientReceiveThread(clientSocket, threadQueue);
receiveThreadPool.submit(clientReceiveThread);
System.out.println("创建线程成功");
}
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}
// 关闭服务器
public void shutdown() {
FileUtil.saveServerData();
// 向所有用户发送服务器关闭信息。
if (msgQueues != null) {
Wrapper exitMsg = new Wrapper(global.global.OPT_EXIT);
for (ArrayBlockingQueue<Wrapper> queue : msgQueues.values()) {
try {
queue.put(exitMsg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
running = false;
}
// 检查服务器是否运行
public static boolean isRunning() {
return running;
}
// 检查chatThreadPool状态
public static Map<String, Object> getChatThreadPoolStatus() {
Map<String, Object> status = new HashMap<>();
if (chatThreadPool == null) {
status.put("error", "线程池未初始化");
return status;
}
// 检查是否为 ThreadPoolExecutor
if (chatThreadPool instanceof ThreadPoolExecutor) {
ThreadPoolExecutor tpe = (ThreadPoolExecutor) chatThreadPool;
status.put("poolSize", tpe.getPoolSize());
status.put("activeCount", tpe.getActiveCount());
status.put("corePoolSize", tpe.getCorePoolSize());
status.put("maximumPoolSize", tpe.getMaximumPoolSize());
status.put("largestPoolSize", tpe.getLargestPoolSize());
status.put("queueSize", tpe.getQueue().size());
status.put("completedTaskCount", tpe.getCompletedTaskCount());
status.put("taskCount", tpe.getTaskCount());
status.put("isShutdown", tpe.isShutdown());
status.put("isTerminated", tpe.isTerminated());
}
// 检查是否为 ForkJoinPool
else if (chatThreadPool instanceof ForkJoinPool) {
ForkJoinPool fjp = (ForkJoinPool) chatThreadPool;
status.put("poolSize", fjp.getPoolSize());
status.put("activeCount", fjp.getActiveThreadCount());
status.put("parallelism", fjp.getParallelism());
status.put("runningThreadCount", fjp.getRunningThreadCount());
status.put("queuedTaskCount", fjp.getQueuedTaskCount());
status.put("queuedSubmissionCount", fjp.getQueuedSubmissionCount());
status.put("stealCount", fjp.getStealCount());
}
// 其他类型的 ExecutorService
else {
// 使用反射尝试获取信息
status.put("type", chatThreadPool.getClass().getName());
status.put("info", "无法直接获取详细状态");
}
return status;
}
// 检查receiveThreadPool的状态
public static Map<String, Object> getReceiveThreadPoolStatus() {
Map<String, Object> status = new HashMap<>();
if (receiveThreadPool == null) {
status.put("error", "线程池未初始化");
return status;
}
// 检查是否为 ThreadPoolExecutor
if (receiveThreadPool instanceof ThreadPoolExecutor) {
ThreadPoolExecutor tpe = (ThreadPoolExecutor) receiveThreadPool;
status.put("poolSize", tpe.getPoolSize());
status.put("activeCount", tpe.getActiveCount());
status.put("corePoolSize", tpe.getCorePoolSize());
status.put("maximumPoolSize", tpe.getMaximumPoolSize());
status.put("largestPoolSize", tpe.getLargestPoolSize());
status.put("queueSize", tpe.getQueue().size());
status.put("completedTaskCount", tpe.getCompletedTaskCount());
status.put("taskCount", tpe.getTaskCount());
status.put("isShutdown", tpe.isShutdown());
status.put("isTerminated", tpe.isTerminated());
}
// 检查是否为 ForkJoinPool
else if (receiveThreadPool instanceof ForkJoinPool) {
ForkJoinPool fjp = (ForkJoinPool) receiveThreadPool;
status.put("poolSize", fjp.getPoolSize());
status.put("activeCount", fjp.getActiveThreadCount());
status.put("parallelism", fjp.getParallelism());
status.put("runningThreadCount", fjp.getRunningThreadCount());
status.put("queuedTaskCount", fjp.getQueuedTaskCount());
status.put("queuedSubmissionCount", fjp.getQueuedSubmissionCount());
status.put("stealCount", fjp.getStealCount());
}
// 其他类型的 ExecutorService
else {
// 使用反射尝试获取信息
status.put("type", receiveThreadPool.getClass().getName());
status.put("info", "无法直接获取详细状态");
}
return status;
}
// 检查阻塞队列状态
public static List<String> getBlockingQueueStatus() {
List<String> queueStatus = new ArrayList<>();
if (msgQueues == null) {
queueStatus.add("阻塞队列未初始化");
return queueStatus;
}
msgQueues.forEach(
(key, value) -> queueStatus.add(key + ": " + value.size()));
return queueStatus;
}
/**
* 用于删除不需要的阻塞队列
* 这里只能ClientChatThread调用
*
* @param socket 对应的客户端的socket
*/
public static void dropMsgQueue(Socket socket) {
if (msgQueues == null) {
System.out.println("意外:主线程为初始化的情况下调用了dropMsgQueue");
return;
}
if (msgQueues.containsKey(socket)) {
msgQueues.remove(socket);
} else {
System.out.println("意外:尝试删除不存在的阻塞队列");
}
}
}
+106
View File
@@ -0,0 +1,106 @@
package server.data;
import java.io.Serializable;
import java.util.TreeSet;
/**
* 群聊信息类,用于保存聊天室的具体信息。
*/
public class GroupData implements Serializable {
private static final long serialVersionUID = 4303981922076715842L;
public class GroupMember implements Comparable<GroupMember>, Serializable {
private static final long serialVersionUID = 585007162886079570L;
public String id;
public boolean isOut;
public GroupMember(String id) {
this.id = id;
isOut = true;
}
@Override
public int compareTo(GroupMember o) {
return this.id.compareTo(o.id);
}
}
// 群聊id
private String groupId;
private String groupName;
private GroupMember groupOwner;
private TreeSet<GroupMember> members;
public GroupData(String groupId, String groupName, String groupOwner) {
this.groupName = groupName;
this.groupId = groupId;
this.groupOwner = new GroupMember(groupOwner);
members = new TreeSet<>();
}
public GroupData(String groupId) {
this.groupId = groupId;
this.groupName = "TEMP_TEST";
}
// 添加组员
public void addMember(String id) {
members.add(new GroupMember(id));
}
// 移除组员
public boolean removeMember(String id) {
GroupMember temp = new GroupMember(id);
if (members.contains(temp)) {
members.remove(new GroupMember(id));
return true;
} else {
// System.out.println("组员移除失败. groupId: " + groupId);
return false;
}
}
public int getMemberCount() {
return members.size();
}
// 获取群聊id
public String getGroupId() {
return groupId;
}
// 获取群聊名称
public String getGroupName() {
return groupName;
}
// 获取群主
public GroupMember getGroupOwner() {
return groupOwner;
}
// 获取群成员
public TreeSet<GroupMember> getMembers() {
return members;
}
// 设置群主
public void setGroupOwner(String id) {
this.groupOwner = new GroupMember(id);
}
public void setGroupName(String groupName) {
this.groupName = groupName;
}
@Override
public String toString() {
return "GroupData{" +
"groupId='" + groupId + '\'' +
", groupName='" + groupName + '\'' +
", groupOwner=" + groupOwner.id +
", members count=" + members.size() +
'}';
}
}
+294
View File
@@ -0,0 +1,294 @@
package server.data;
import util.FileUtil;
import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
// 服务器数据,单个服务器仅对应一个服务器数据集合。
// 允许存储,用于数据持久化
// 辅助进行数据核验
public class ServerData implements Serializable {
private static final long serialVersionUID = 5016807647175865383L;
private static volatile ServerData instance = null;
// 获取唯一的serverData对象(线程安全的懒加载)
public static ServerData getInstance() {
if (instance == null) {
synchronized (ServerData.class) {
if (instance == null) {
instance = new ServerData();
instance.loadData();
}
}
}
return instance;
}
// 重置实例(用于测试或重新加载)
public static void resetInstance() {
synchronized (ServerData.class) {
instance = null;
}
}
private Map<String, UserData> serverUsers;
private Map<String, GroupData> serverGroups;
private transient boolean dataLoaded = false;
// 处理服务器信息的主类
public ServerData() {
// 初始化空数据
serverUsers = new ConcurrentHashMap<>();
serverGroups = new ConcurrentHashMap<>();
}
// 显式加载数据的方法
public void loadData() {
if (!dataLoaded) {
synchronized (this) {
if (!dataLoaded) {
ServerData loadedData = FileUtil.loadServerData();
if (loadedData != null) {
// 如果加载到了数据,合并到当前实例
if (loadedData.getServerUsers() != null) {
this.serverUsers = loadedData.getServerUsers();
}
if (loadedData.getServerGroups() != null) {
this.serverGroups = loadedData.getServerGroups();
}
System.out.println("服务器数据加载成功,用户数: " +
(serverUsers != null ? serverUsers.size() : 0) +
", 群组数: " + (serverGroups != null ? serverGroups.size() : 0));
} else {
System.out.println("未找到数据文件或加载失败,使用初始化空数据");
}
dataLoaded = true;
}
}
}
}
// 验证数据是否已正确初始化
private void validateData() {
if (serverUsers == null) {
serverUsers = new ConcurrentHashMap<>();
}
if (serverGroups == null) {
serverGroups = new ConcurrentHashMap<>();
}
}
// 保存服务器数据到本地
public void saveServerData() {
validateData(); // 确保数据有效
FileUtil.saveServerData();
}
// 添加用户
public void addUser(UserData userData) {
serverUsers.put(userData.getUserId(), userData);
}
// 移除用户
public void removeUser(String userId) {
serverUsers.remove(userId);
serverGroups.values().forEach(groupData -> {
groupData.removeMember(userId);
if (groupData.getMemberCount() == 0) {
removeGroup(groupData.getGroupId());
}
});
}
// 修改用户名字
public void updateUserName(String userId, String newName) {
serverUsers.get(userId).setNikename(newName);
}
public void updateUserPwd(String userId, String newPwd) {
serverUsers.get(userId).setPassword(newPwd);
}
// 添加群聊
public void addGroup(GroupData groupData) {
serverGroups.put(groupData.getGroupId(), groupData);
// 更新关联用户信息
groupData.getMembers().forEach(member -> {
addGroupToUser(member.id, groupData.getGroupId());
});
}
public void addGroupToUser(String userId, String groupId) {
serverUsers.get(userId).addGroupId(groupId);
}
// 移除群聊
public void removeGroup(String groupId) {
GroupData groupData = serverGroups.remove(groupId);
groupData.getMembers().forEach(member -> {
serverUsers.get(member.id).removeGroup(groupData.getGroupId());
});
}
// 更新群聊信息
public void updateGroupName(String groupId, String newName) {
serverGroups.get(groupId).setGroupName(newName);
}
public void updateGroupOwner(String groupId, String newOwnerId) {
serverGroups.get(groupId).setGroupOwner(newOwnerId);
}
// 添加群聊成员
public void addUserToGroup(String groupId, String userId) {
serverGroups.get(groupId).addMember(userId);
}
// 移除群聊成员
public void removeUserFromGroup(String groupId, String userId) {
serverGroups.values().forEach(groupData -> {
if (groupData.getGroupId().equals(groupId)) {
groupData.removeMember(userId);
}
});
}
// 移除成员的群聊
public void removeGroupFromUser(String userId, String groupId) {
serverUsers.get(userId).removeGroup(groupId);
}
// 获取用户名字,不存在就回复id本身
public String getUserName(String userId) {
if (serverUsers.containsKey(userId))
return serverUsers.get(userId).getNickname();
else
return userId;
}
// 获取用户数据
public UserData getUserData(String userId) {
return serverUsers.get(userId);
}
// 获取群聊名字
public String getGroupName(String groupId) {
return serverGroups.get(groupId).getGroupName();
}
// 判断群聊是否存在
public boolean containsGroup(String groupId) {
return serverGroups.containsKey(groupId);
}
// 获取用户群聊id组
public TreeSet<String> getUserGroups(String userId) {
if (userId == null) {
return new TreeSet<>();
} else {
return serverUsers.get(userId).getGroupIds();
}
}
// 获取群聊的成员组
public TreeSet<GroupData.GroupMember> getGroupUsers(String groupId) {
return serverGroups.get(groupId).getMembers();
}
// 获取群聊的成员id组
public List<String> getGroupMembersId(String groupId) {
List<String> members = new ArrayList<>();
serverGroups.get(groupId).getMembers().forEach(groupMember -> {
members.add(groupMember.id);
});
return members;
}
/**
* 判断账号是否存在
*
* @param userId 账号
* @return true:存在 false:不存在
*/
public boolean IsAccountExist(String userId) {
return serverUsers.containsKey(userId);
}
/**
* 校验账号与密码是否匹配
*
* @param userId 待校验的用户账号
* @param password 待校验的用户密码
* @return 密码匹配返回true,不匹配返回false
*/
public boolean AccountAndPasswordIsMatch(String userId, String password) {
if (serverUsers.get(userId).getPassword().equals(password)) {
return true;
}
return false;
}
/**
* 设置服务器用户数据
*
* @param serverUsers 服务器用户数据映射表
*/
public void setServerUsers(Map<String, UserData> serverUsers) {
this.serverUsers = serverUsers;
}
/**
* 设置服务器群聊数据
*
* @param serverGroups 服务器群聊数据映射表
*/
public void setServerGroups(Map<String, GroupData> serverGroups) {
this.serverGroups = serverGroups;
}
/**
* 获取服务器用户数据映射表
*
* @return 服务器用户数据映射表
*/
public Map<String, UserData> getServerUsers() {
return serverUsers;
}
/**
* 获取服务器用户id到用户名的映射表
*
* @return 服务器用户id到用户名的映射表
*/
public Map<String, String> getIdNameMap() {
Map<String, String> idNameMap = new HashMap<>();
serverUsers.values().forEach(user -> {
idNameMap.put(user.getUserId(), user.getNickname());
});
return idNameMap;
}
/**
* 获取服务器群聊数据映射表
*
* @return 服务器群聊数据映射表
*/
public Map<String, GroupData> getServerGroups() {
return serverGroups;
}
/**
* 获取服务器群聊数据映射表
*
* @return 服务器群聊数据映射表
*/
public GroupData getGroupById(String groupId) {
return serverGroups.get(groupId);
}
}
+158
View File
@@ -0,0 +1,158 @@
package server.data;
import java.io.Serializable;
import java.util.TreeSet;
/**
* 用户信息类,用于保存用户的具体信息。
*/
public class UserData implements Serializable, Comparable<UserData> {
// 序列化版本号,用于版本控制
private static final long serialVersionUID = 2809761558436195616L;
// 用户昵称
private String nikename;
// 用户ID
private String id;
// 用户密码
private String password;
// 所属群聊ID集合
private TreeSet<String> groupIds;
// 好友ID集合
private TreeSet<String> friendIds;
// 用户邮箱
private String email;
// 用户生日
private String birthday;
// 用户地址
private String address;
// 用户签名
private String signature;
public UserData(String nikename, String id, String password) {
this.nikename = nikename;
this.id = id;
this.password = password;
this.groupIds = new TreeSet<>();
this.friendIds = new TreeSet<>();
// 初始化扩展信息为空字符串,避免 null
this.email = "";
this.birthday = "";
this.address = "";
this.signature = "";
}
// 获取安全的副本(不包含密码),用于网络传输
public UserData getSafeCopy() {
UserData copy = new UserData(this.nikename, this.id, null);
copy.setGroupIds(new TreeSet<>(this.groupIds));
copy.setFriendIds(new TreeSet<>(this.friendIds));
copy.setEmail(this.email);
copy.setBirthday(this.birthday);
copy.setAddress(this.address);
copy.setSignature(this.signature);
return copy;
}
public String getNickname() {
return nikename;
}
public void setNikename(String nikename) {
this.nikename = nikename;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUserId() {
return id;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public TreeSet<String> getGroupIds() {
return groupIds;
}
public void setGroupIds(TreeSet<String> groupIds) {
this.groupIds = groupIds;
}
public TreeSet<String> getFriendIds() {
return friendIds;
}
public void setFriendIds(TreeSet<String> friendIds) {
this.friendIds = friendIds;
}
public void addFriend(String friendId) {
this.friendIds.add(friendId);
}
public boolean addGroupId(String groupId) {
return this.groupIds.add(groupId);
}
public boolean removeGroupId(Long groupId) {
if (groupIds.contains(groupId.toString())) {
groupIds.remove(groupId.toString());
return true;
}
return false;
}
@Override
public int compareTo(UserData o) {
return this.id.compareTo(o.id);
}
public void removeGroup(String groupId) {
this.groupIds.remove(groupId);
}
// 扩展信息的 Getter 和 Setter
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getBirthday() {
return birthday;
}
public void setBirthday(String birthday) {
this.birthday = birthday;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getSignature() {
return signature;
}
public void setSignature(String signature) {
this.signature = signature;
}
}
+993
View File
@@ -0,0 +1,993 @@
package server.serveice;
import global.global;
import server.ServerMainThread;
import server.data.*;
import util.FileUtil;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.*;
/**
* 不可独立创建线程,这个会在内部自主创建线程。
* 每个客户端连接对应一个线程,用于处理客户端的请求。
* 线程启动后,会进入一个循环,等待服务端发送的消息。
* 收到消息后,会根据消息的操作码,调用对应的处理方法。
* 处理完成后,会将结果发送给客户端。
* 线程会在以下情况下退出:
* 1. 服务端关闭
* 2. 客户端主动关闭
* 3. 线程被中断
*/
public class ClientChatThread implements Runnable {
// 群在线用户存储:全局静态、线程安全
// key:用户id value:用户在线输出流
private static final Map<String, ObjectOutputStream> USER_ONLINE_MAP = new ConcurrentHashMap<>();
// 与客户端相连的套接字
private final Socket clientSocket;
// 是否登录
private boolean isLogin = false;
// 用户的账户
private String userId;
// 阻塞队列,用于进行线程的信息交流
private BlockingQueue<Wrapper> messageQueue;
private ObjectOutputStream oos;
private volatile boolean isRunning = true;
/**
* 创建一个数据发送线程,并且附带创建一个信息接收线程。
* 由于两个线程总是同步创建和销毁的,因此不进行单独创建。
*
* @param clientSocket 与客户端相连的套接字
*/
public ClientChatThread(Socket clientSocket, BlockingQueue<Wrapper> threadQueue)
throws IOException {
this.clientSocket = clientSocket;
this.isLogin = false;
this.userId = null;
messageQueue = threadQueue;
}
// 线程核心:聊天业务主流程
@Override
public void run() {
try {
oos = new ObjectOutputStream(clientSocket.getOutputStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
while (ServerMainThread.isRunning() && this.isRunning) {
try {
Wrapper msg = messageQueue.take();
handleReceiveMsg(msg);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
/**
* 处理服务端接收的消息,按规则解析。在解析出结果后,调用下面的内容来处理数据体
*
* @param msg 服务端接收的消息
*/
private void handleReceiveMsg(Wrapper msg) {
int opt = msg.getOperation();
String senderId = msg.getSenderId();
switch (opt) {
// 登录请求
case global.OPT_REGISTER:
handleRegisterRequest(msg);
break;
case global.OPT_LOGIN:
handleLogInRequest(msg);
break;
case global.OPT_LOGOUT:
handleLogOutRequest(senderId);
break;
case global.OPT_DELETE_ACCOUNT:
handleDeleteUserRequest(senderId);
break;
case global.OPT_UPDATE_NICKNAME:
handleUpdateUserNameRequest(msg);
break;
case global.OPT_UPDATE_PASSWORD:
handleUpdateUserPwdRequest(msg);
break;
case global.OPT_GROUP_CREATE:
handleCreateGroupRequest((String) msg.getData(), msg.getGroupId());
break;
case global.OPT_GROUP_INVITE:
handleInviteRequest(msg);
break;
case global.OPT_GROUP_JOIN:
handleGroupJoinRequest(msg);
break;
case global.OPT_FRIEND_ADD:
handleFriendAddRequest(msg);
break;
case global.OPT_FRIEND_ADD_AGREE:
handleFriendAddAgree(msg);
break;
case global.OPT_FRIEND_ADD_REFUSE:
handleFriendAddRefuse(msg);
break;
case global.OPT_GROUP_INVITE_AGREE:
// 用户加入群聊
handleJoinGroupRequest(senderId, msg.getGroupId());
break;
case global.OPT_GROUP_INVITE_REFUSE:
// 拒绝加入群聊
sendToUser(Wrapper.serverResponse(global.OPT_GROUP_INVITE_REFUSE), (String) msg.getData());
break;
case global.OPT_GROUP_QUIT:
handleQuitGroupRequest(senderId, msg.getGroupId());
break;
case global.OPT_GROUP_DISBAND:
handleDeleteGroupRequest(msg.getGroupId());
break;
case global.OPT_CHAT:
sendToGroupExceptSelf(msg, msg.getGroupId());
FileUtil.addChatMessage(msg.getGroupId(), (String) msg.getData());
break;
case global.OPT_PRIVATE_CHAT:
handlePrivateChatRequest(msg);
break;
case global.OPT_GROUP_UPDATE_NAME:
handleUpdateGroupNameRequest(msg);
break;
case global.OPT_GROUP_UPDATE_OWNER:
handleUpdateGroupOwnerRequest(msg);
break;
case global.OPT_INIT_CHAT:
handleInitChatRequest();
break;
case global.OPT_INIT_GROUP:
handleInitGroupRequest();
break;
case global.OPT_INIT_USER:
// 发送用户 ID-Name 映射
sendToSelf(Wrapper.initResponse(ServerData.getInstance().getIdNameMap()));
break;
case global.OPT_INIT_USER_DETAIL:
// 发送所有用户详细信息
handleInitUserDetailRequest();
break;
case global.OPT_UPDATE_USER_DETAIL:
// 处理更新用户详细信息
handleUpdateUserDetailRequest(msg);
break;
}
}
/**
* 处理注册请求
* 1. 检查用户ID是否为空
* 2. 检查用户名是否为空
* 3. 检查密码是否为空
* 4. 检查用户ID是否已存在
* 5. 添加新用户到ServerData
* 6. 发送注册成功消息给客户端
*
* @param msg 注册请求消息
*/
private void handleRegisterRequest(Wrapper msg) {
String[] data = (String[]) msg.getData();
String userId = msg.getSenderId();
String nikname = data[0];
String password = data[1];
// 数据为空的情况
if (userId == null || nikname == null || password == null) {
sendToSelf(Wrapper.serverResponse(global.OPT_QUEST_WRONG));
return;
}
// 账户存在的情况
if (ServerData.getInstance().IsAccountExist(userId)) {
sendToSelf(Wrapper.serverResponse(global.OPT_REGISTER_FAILED_ACC));
return;
}
ServerData.getInstance().addUser(new UserData(nikname, userId, password));
this.isLogin = true;
this.userId = userId;
USER_ONLINE_MAP.values().forEach(oos -> {
Map<String, String> newRegister = new HashMap<>();
newRegister.put(userId, nikname);
try {
oos.writeObject(Wrapper.initResponse(newRegister));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
// 添加注册用户的id oos映射,初始化id,群id映射
USER_ONLINE_MAP.put(this.userId, oos);
sendToSelf(Wrapper.serverResponse(global.OPT_REGISTER_SUCCESS));
}
/**
* 处理登录请求
* 1. 检查用户ID是否为空
* 2. 检查密码是否为空
* 3. 检查用户ID是否已存在
* 4. 添加新用户到ServerData
* 5. 发送登录成功消息给客户端
*
* @param msg 登录请求消息
*/
private void handleLogInRequest(Wrapper msg) {
String userId = msg.getSenderId();
String password = (String) msg.getData();
// 检测Id是否存在
if (!ServerData.getInstance().IsAccountExist(userId)) {
sendToSelf(Wrapper.serverResponse(global.OPT_LOGIN_FAILED_ACC));
return;
}
// 检测是否重复登录
if (USER_ONLINE_MAP.containsKey(userId)) {
sendToSelf(Wrapper.serverResponse(global.OPT_LOGIN_FAILED_REPEATED));
return;
}
// 检测账户密码是否相对应
else if (!ServerData.getInstance().AccountAndPasswordIsMatch(userId, password)) {
sendToSelf(Wrapper.serverResponse(global.OPT_LOGIN_FAILED_PWD));
return;
}
// 修改数据
this.isLogin = true;
this.userId = userId;
// 更新用户在线信息
USER_ONLINE_MAP.put(this.userId, oos);
sendToSelf(Wrapper.serverResponse(global.OPT_LOGIN_SUCCESS));
USER_ONLINE_MAP.put(this.userId, oos);
}
/**
* 判断用户是否已登录
*
* @return 如果用户已登录且在在线映射中,则返回true;否则返回false
*/
private boolean YesLogin() {
return isLogin && USER_ONLINE_MAP.containsKey(userId) && this.userId != null;
}
/**
* 关闭当前用户链接,关闭这个线程(接受线程在接收关闭信息时已经结束了)
*/
private void closeClient() {
// 优先结束阻塞队列
ServerMainThread.dropMsgQueue(this.clientSocket);
if (!isRunning) {
return;
}
isRunning = false;
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
System.err.println("关闭输出流异常: " + e.getMessage());
}
}
if (clientSocket != null && !clientSocket.isClosed()) {
try {
clientSocket.close();
} catch (IOException e) {
System.err.println("关闭socket异常: " + e.getMessage());
}
}
}
/**
* 处理登出请求
* 1. 检查用户是否已登录
* 2. 从在线用户映射中移除用户
* 3. 设置用户状态为未登录
* 4. 发送登出成功消息给客户端
*
* @param userId 要登出的用户ID
*/
private void handleLogOutRequest(String userId) {
if (!YesLogin()) {
// 出现错误,现在还未登录
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
return;
}
USER_ONLINE_MAP.remove(this.userId);
isLogin = false;
this.userId = null;
sendToSelf(Wrapper.serverResponse(global.OPT_LOGOUT));
try {
Thread.sleep(100); // 短暂延迟
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
closeClient();
}
/**
* 处理删除用户请求
* 1. 检查用户是否已登录
* 2. 从在线用户映射中移除用户
* 3. 从服务器数据中删除用户
* 4. 设置用户状态为未登录
* 5. 发送删除成功消息给客户端
*
* @param userId 要删除的用户ID
*/
private void handleDeleteUserRequest(String userId) {
if (!YesLogin()) {
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
return;
}
USER_ONLINE_MAP.remove(this.userId);
ServerData.getInstance().removeUser(userId);
isLogin = false;
this.userId = null;
sendToSelf(Wrapper.serverResponse(global.OPT_DELETE_ACCOUNT));
try {
Thread.sleep(100); // 短暂延迟
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
closeClient();
}
/**
* 处理初始化所有用户详细信息的请求
* 1. 检查用户是否已登录
* 2. 从服务器数据中获取所有用户的详细信息
* 3. 发送安全的用户详细信息副本给客户端
*/
private void handleInitUserDetailRequest() {
if (!YesLogin())
return;
Map<String, UserData> allUsers = ServerData.getInstance().getServerUsers();
Map<String, UserData> safeUsers = new HashMap<>();
for (Map.Entry<String, UserData> entry : allUsers.entrySet()) {
// 只发送安全的副本(无密码)
safeUsers.put(entry.getKey(), entry.getValue().getSafeCopy());
}
sendToSelf(Wrapper.initUserDetailResponse(safeUsers));
}
/**
* 处理更新用户详细信息的请求
* 1. 检查用户是否已登录
* 2. 检查更新数据是否包含有效用户ID
* 3. 从服务器数据中获取原始用户数据
* 4. 更新服务器上的用户详细信息
* 5. 广播更新给所有在线用户
*
* @param msg 包含更新用户详细信息的消息
*/
private void handleUpdateUserDetailRequest(Wrapper msg) {
if (!YesLogin())
return;
UserData updatedData = (UserData) msg.getData();
if (updatedData == null || !updatedData.getUserId().equals(userId)) {
return; // 安全校验:只能更新自己的信息
}
// 获取服务器上的原始数据
UserData serverData = ServerData.getInstance().getUserData(userId);
if (serverData != null) {
// 更新字段
serverData.setEmail(updatedData.getEmail());
serverData.setBirthday(updatedData.getBirthday());
serverData.setAddress(updatedData.getAddress());
serverData.setSignature(updatedData.getSignature());
// 注意:不更新密码和昵称(有专门的接口),也不更新关系链
// 广播更新给所有在线用户
UserData safeCopy = serverData.getSafeCopy();
Wrapper updateMsg = Wrapper.updateUserDetailResponse(safeCopy);
for (ObjectOutputStream oos : USER_ONLINE_MAP.values()) {
try {
synchronized (oos) {
oos.writeObject(updateMsg);
oos.flush();
oos.reset();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 处理申请加入群聊请求
* 1. 检查用户是否已登录
* 2. 检查群聊是否存在
* 3. 检查用户是否已加入该群聊
* 4. 加入群聊
* 5. 通知群内其他成员有新人加入
*
* @param msg 包含群聊ID的消息
*/
private void handleGroupJoinRequest(Wrapper msg) {
if (!YesLogin()) {
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
return;
}
String userId = msg.getSenderId();
String groupId = msg.getGroupId();
GroupData group = ServerData.getInstance().getGroupById(groupId);
if (group == null) {
sendToSelf(Wrapper.serverResponse(global.OPT_GROUP_JOIN_FAILED));
return;
}
// 检查是否已经是成员
if (group.getMembers().contains(group.new GroupMember(userId))) {
// 已经是成员,视为成功
sendToSelf(Wrapper.initResponse(group));
return;
}
// 添加成员
group.addMember(userId);
ServerData.getInstance().getUserData(userId).addGroupId(groupId);
// 1. 通知自己加入成功 (发送群组信息)
sendToSelf(Wrapper.initResponse(group));
// 2. 通知群内其他成员有新人加入
sendToGroupExceptSelf(Wrapper.initResponse(group), groupId);
}
/**
* 处理添加好友请求
* 1. 检查用户是否已登录
* 2. 检查好友是否存在
* 3. 检查是否已添加该好友
* 4. 转发请求给目标用户(如果在线)
*
* @param msg 包含好友ID的消息
*/
private void handleFriendAddRequest(Wrapper msg) {
if (!YesLogin()) {
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
return;
}
String userId = msg.getSenderId();
String friendId = (String) msg.getData(); // 目标好友ID
if (userId.equals(friendId)) {
// 不能添加自己
sendToSelf(Wrapper.serverResponse(global.OPT_FRIEND_ADD_FAILED));
return;
}
UserData friendData = ServerData.getInstance().getUserData(friendId);
if (friendData == null) {
sendToSelf(Wrapper.serverResponse(global.OPT_FRIEND_ADD_FAILED));
return;
}
UserData myData = ServerData.getInstance().getUserData(userId);
// 检查是否已经是好友
if (myData.getFriendIds().contains(friendId)) {
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "你们已经是好友了"));
return;
}
// 如果对方在线,转发请求
if (USER_ONLINE_MAP.containsKey(friendId)) {
try {
// 发送给目标用户:Sender=userId, Data=NickName
USER_ONLINE_MAP.get(friendId)
.writeObject(new Wrapper(myData.getNickname(), userId, null, global.OPT_FRIEND_ADD));
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "好友请求已发送"));
} catch (IOException e) {
e.printStackTrace();
}
} else {
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "用户不在线,无法发送请求"));
}
}
private void handleFriendAddAgree(Wrapper msg) {
String myId = msg.getSenderId();
String friendId = (String) msg.getData(); // 发起者的ID
UserData myData = ServerData.getInstance().getUserData(myId);
UserData friendData = ServerData.getInstance().getUserData(friendId);
if (friendData == null)
return;
// 双向添加
myData.addFriend(friendId);
friendData.addFriend(myId);
// 通知自己成功
Map<String, String> friendInfo = new HashMap<>();
friendInfo.put(friendId, friendData.getNickname());
sendToSelf(new Wrapper(friendInfo, global.SERVER_ACCOUNT, null, global.OPT_FRIEND_ADD_SUCCESS));
// 通知对方成功
if (USER_ONLINE_MAP.containsKey(friendId)) {
Map<String, String> myInfo = new HashMap<>();
myInfo.put(myId, myData.getNickname());
try {
USER_ONLINE_MAP.get(friendId).writeObject(
new Wrapper(myInfo, global.SERVER_ACCOUNT, null, global.OPT_FRIEND_ADD_SUCCESS));
// 还可以发个系统消息提示
USER_ONLINE_MAP.get(friendId).writeObject(
Wrapper.serverResponse(global.SERVER_MESSAGE, myData.getNickname() + " 同意了你的好友请求"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 处理好友添加拒绝请求
* 1. 检查用户是否已登录
* 2. 检查好友是否存在
* 3. 通知拒绝者好友请求已被拒绝
*
* @param msg 包含拒绝好友ID的消息
*/
private void handleFriendAddRefuse(Wrapper msg) {
String myId = msg.getSenderId();
String friendId = (String) msg.getData(); // 发起者的ID
UserData myData = ServerData.getInstance().getUserData(myId);
if (USER_ONLINE_MAP.containsKey(friendId)) {
try {
USER_ONLINE_MAP.get(friendId).writeObject(
new Wrapper(myData.getNickname(), myId, null, global.OPT_FRIEND_ADD_REFUSE));
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 处理私聊请求
* 1. 检查用户是否已登录
* 2. 检查接收者是否存在
* 3. 检查接收者是否在线
* 4. 转发消息给接收者
*
* @param msg 包含接收者ID和消息内容的消息
*/
private void handlePrivateChatRequest(Wrapper msg) {
String receiverId = msg.getGroupId(); // 接收者ID
if (USER_ONLINE_MAP.containsKey(receiverId)) {
try {
// 转发消息给接收者
USER_ONLINE_MAP.get(receiverId).writeObject(msg);
} catch (IOException e) {
e.printStackTrace();
}
} else {
// 对方不在线
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "对方不在线"));
}
}
/**
* 处理更新用户信息请求
* 1. 检查用户是否已登录
* 2. 更新用户昵称
* 3. 通知所有连接的客户端更新用户信息
*
* @param wrapper 包含新昵称的消息
*/
private void handleUpdateUserNameRequest(Wrapper wrapper) {
if (!YesLogin()) {
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
return;
}
String newName = (String) wrapper.getData();
ServerData.getInstance().updateUserName(this.userId, newName);
Map<String, String> idNameMap = new HashMap<>();
idNameMap.put(this.userId, newName);
// OPT_INIT_USER
sentToConnectedGroups(Wrapper.initResponse(idNameMap), this.userId);
}
/**
* 处理更新用户密码请求
* 1. 检查用户是否已登录
* 2. 更新用户密码
* 3. 通知客户端密码更新成功
*
* @param wrapper 包含新密码的消息
*/
private void handleUpdateUserPwdRequest(Wrapper wrapper) {
if (!YesLogin()) {
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
return;
}
String newPwd = (String) wrapper.getData();
ServerData.getInstance().updateUserPwd(this.userId, newPwd);
sendToSelf(Wrapper.serverResponse(global.OPT_UPDATE_PASSWORD));
}
/**
* 处理创建群组请求
* 1. 检查用户是否已登录
* 2. 检查群组名称是否为空
* 3. 检查群组ID是否重复
* 4. 创建新的群组
* 5. 通知客户端群组创建成功
*
* @param groupName 群组名称
* @param groupId 群组ID
*/
private void handleCreateGroupRequest(String groupName, String groupId) {
if (!YesLogin()) {
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
return;
}
// 如果信息为空
if (groupName == null) {
sendToSelf(Wrapper.serverResponse(global.OPT_QUEST_WRONG));
return;
}
// 如果已经有对应的id的群聊
if (ServerData.getInstance().containsGroup(groupId)) {
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "群聊已存在,id重复"));
return;
}
// 处理数据调用serverData的addGroup
GroupData groupData = new GroupData(groupId, groupName, userId);
groupData.addMember(userId);
ServerData.getInstance().addGroup(groupData);
// 调用serverData的addUser
ServerData.getInstance().addUserToGroup(groupId, userId);
sendToSelf(Wrapper.serverResponse(global.OPT_GROUP_CREATE_SUCCESS));
sendToSelf(Wrapper.initResponse(groupData));
}
/**
* 处理邀请加入群组请求
* 1. 检查用户是否已登录
* 2. 检查邀请人是否是自己
* 3. 检查用户是否已在群聊中
* 4. 检查用户是否在线
* 5. 通知被邀请人加入群组
*
* @param inviteMsg 包含被邀请人ID和群组ID的消息
*/
private void handleInviteRequest(Wrapper inviteMsg) {
String inviteId = (String) inviteMsg.getData();
String theGroupId = inviteMsg.getGroupId();
String senderId = inviteMsg.getSenderId();
// 如果邀请人是自己
if (senderId.equals(inviteId)) {
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "不能邀请自己加入群聊"));
return;
}
// 如果邀请人已经在群聊中
if (ServerData.getInstance().getGroupMembersId(theGroupId).contains(inviteId)) {
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "该用户已在群聊中"));
return;
}
// 如果用户不在线
if (!USER_ONLINE_MAP.containsKey(inviteId)) {
sendToSelf(Wrapper.serverResponse(
global.SERVER_MESSAGE,
"用户(" + ServerData.getInstance().getUserName(inviteId) + ")不在线!"));
return;
}
// 通知被邀请人加入群组
sendToUser(Wrapper.groupInviteRequest(
ServerData.getInstance().getGroupName(theGroupId), senderId, theGroupId),
inviteId);
}
/**
* 处理加入群组请求
* 1. 检查用户是否已登录
* 2. 检查用户是否已在群聊中
* 3. 检查群组是否存在
* 4. 加入群组
* 5. 通知群组内所有人用户加入
*
* @param userId 用户ID
* @param groupId 群组ID
*/
private void handleJoinGroupRequest(String userId, String groupId) {
if (!YesLogin()) {
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
return;
}
ServerData.getInstance().addUserToGroup(groupId, userId);
ServerData.getInstance().addGroupToUser(userId, groupId);
Wrapper wrapper = Wrapper.initResponse(ServerData.getInstance().getGroupById(groupId));
// 向组内所有人推送更新。(人员加入)
sendToGroup(wrapper, groupId);
}
/**
* 处理退出群组请求
* 1. 检查用户是否已登录
* 2. 检查用户是否已在群聊中
* 3. 从群组中移除用户
* 4. 从用户群组列表中移除群组
* 5. 如果群组为空,则删除群组
* 6. 通知群组内其他用户更新
*
* @param userId 用户ID
* @param groupId 群组ID
*/
private void handleQuitGroupRequest(String userId, String groupId) {
if (!YesLogin()) {
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
return;
}
ServerData.getInstance().removeUserFromGroup(groupId, userId);
ServerData.getInstance().removeGroupFromUser(userId, groupId);
if (ServerData.getInstance().getGroupById(groupId).getMemberCount() == 0) {
// 没有人的群聊就删掉
ServerData.getInstance().removeGroup(groupId);
} else {
// 不为空则需要推送更新
Wrapper wrapper = Wrapper.initResponse(ServerData.getInstance().getGroupById(groupId));
// 向组内其它所有人推送更新。
sendToGroupExceptSelf(wrapper, groupId);
}
// 退出人也要进行更新
sendToSelf(new Wrapper(null, null, groupId, global.OPT_GROUP_QUIT));
}
/**
* 处理删除群组请求
* 1. 检查用户是否已登录
* 2. 检查群组是否存在
* 3. 检查用户是否是群组所有者
* 4. 删除群组
* 5. 通知所有连接的客户端群组已删除
*
* @param groupId 群组ID
*/
private void handleDeleteGroupRequest(String groupId) {
if (!YesLogin()) {
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
return;
}
Wrapper wrapper = new Wrapper(null, null, groupId, global.OPT_GROUP_QUIT);
// 向所有人推送更新。
ServerData.getInstance().removeGroup(groupId);
sendToGroup(wrapper, groupId);
}
/**
* 处理更新群组名称请求
* 1. 检查用户是否已登录
* 2. 更新群组名称
* 3. 通知所有连接的客户端更新群组名称
*
* @param groupUpdateMsg 包含群组ID和新名称的消息
*/
private void handleUpdateGroupNameRequest(Wrapper groupUpdateMsg) {
if (!YesLogin()) {
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
return;
}
ServerData.getInstance().updateGroupName(groupUpdateMsg.getGroupId(),
(String) groupUpdateMsg.getData());
sendToGroup(groupUpdateMsg, groupUpdateMsg.getGroupId());
}
/**
* 处理更新群组管理员请求
* 1. 检查用户是否已登录
* 2. 检查管理员是否是自己
* 3. 更新群组管理员
* 4. 通知群组内所有成员更新管理员
*
* @param groupUpdateMsg 包含新管理员ID和群组ID的消息
*/
private void handleUpdateGroupOwnerRequest(Wrapper groupUpdateMsg) {
if (!YesLogin()) {
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
return;
}
ServerData.getInstance().updateGroupOwner(groupUpdateMsg.getGroupId(), (String) groupUpdateMsg.getData());
sendToGroup(groupUpdateMsg, groupUpdateMsg.getGroupId());
}
/**
* 处理初始化聊天消息请求
* 1. 检查用户是否已登录
* 2. 遍历用户所属的所有群组
* 3. 加载群组聊天记录
* 4. 向用户发送群组聊天记录初始化消息
*/
private void handleInitChatRequest() {
for (String groupId : ServerData.getInstance().getUserGroups(userId)) {
Wrapper wrapper = Wrapper.initResponse(FileUtil.loadGroupChatMsg(groupId), groupId);
sendToSelf(wrapper);
}
}
/**
* 处理初始化群组请求
* 1. 检查用户是否已登录
* 2. 遍历用户所属的所有群组
* 3. 加载群组信息
* 4. 向用户发送群组初始化消息
*/
private void handleInitGroupRequest() {
for (String groupId : ServerData.getInstance().getUserGroups(userId)) {
Wrapper wrapper = Wrapper.initResponse(ServerData.getInstance().getGroupById(groupId));
sendToSelf(wrapper);
}
}
/**
* 向指定用户广播消息
* 1. 遍历用户ID列表
* 2. 检查用户是否在线
* 3. 向在线用户发送消息
*
* @param userIds 接收消息的用户ID列表
* @param wrapper 要发送的消息包装对象
*/
public static void broadcastMsg(String[] userIds, Wrapper wrapper) {
for (String userId : userIds) {
ObjectOutputStream oos = USER_ONLINE_MAP.get(userId);
if (oos == null)
continue;
try {
synchronized (oos) {
oos.writeObject(wrapper);
oos.flush();
oos.reset();
}
} catch (IOException e) {
Thread.currentThread().interrupt();
System.err.println("send error");
}
}
}
/**
* 向用户发送消息
* 1. 检查用户是否在线
* 2. 向在线用户发送消息
*
* @param o 要发送的消息包装对象
*/
private void sendToSelf(Wrapper o) {
try {
synchronized (oos) {
oos.writeObject(o);
oos.flush();
oos.reset();
}
} catch (IOException e) {
Thread.currentThread().interrupt();
System.out.println("send error");
}
}
/**
* 向指定用户发送消息
* 1. 检查用户是否在线
* 2. 向在线用户发送消息
*
* @param wrapper 要发送的消息包装对象
* @param userId 接收消息的用户ID
*/
private void sendToUser(Wrapper wrapper, String userId) {
try {
ObjectOutputStream oos = USER_ONLINE_MAP.get(userId);
if (oos != null) {
synchronized (oos) {
oos.writeObject(wrapper);
oos.flush();
oos.reset();
}
}
} catch (IOException e) {
Thread.currentThread().interrupt();
System.err.println("send error: " + userId);
}
}
/**
* 向指定群聊发送消息
* 1. 遍历群聊成员ID列表
* 2. 检查成员是否在线
* 3. 向在线成员发送消息
*
* @param wrapper 要发送的消息包装对象
* @param groupId 接收消息的群聊ID
*/
private void sendToGroup(Wrapper wrapper, String groupId) {
List<String> members = ServerData.getInstance().getGroupMembersId(groupId);
for (String member : members) {
sendToUser(wrapper, member);
}
}
/**
* 向指定群聊发送消息(排除发送者)
* 1. 遍历群聊成员ID列表
* 2. 检查成员是否在线
* 3. 向在线成员发送消息(排除发送者)
*
* @param wrapper 要发送的消息包装对象
* @param groupId 接收消息的群聊ID
*/
private void sendToGroupExceptSelf(Wrapper wrapper, String groupId) {
List<String> members = ServerData.getInstance().getGroupMembersId(groupId);
for (String member : members) {
if (!member.equals(userId)) {
sendToUser(wrapper, member);
}
}
}
/**
* 向用户所属的所有群聊发送消息
* 1. 遍历用户所属的所有群组
* 2. 向每个群组发送消息
*
* @param wrapper 要发送的消息包装对象
* @param userId 接收消息的用户ID
*/
private void sentToConnectedGroups(Wrapper wrapper, String userId) {
TreeSet<String> groups = ServerData.getInstance().getUserGroups(userId);
groups.forEach(groupId -> sendToGroup(wrapper, groupId));
}
}
@@ -0,0 +1,80 @@
package server.serveice;
import global.global;
import server.ServerMainThread;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;
import java.net.SocketException;
import java.util.concurrent.BlockingQueue;
public class ClientReceiveThread implements Runnable {
private BlockingQueue<Wrapper> messageQueue;
private volatile boolean isRunning;
// 接收线程专属资源:构造器传入,仅用于接收消息
private final Socket clientSocket;
ObjectInputStream ois;
// 构造器:初始化套接字资源
public ClientReceiveThread(
Socket clientSocket,
BlockingQueue<Wrapper> messageQueue) throws IOException {
this.messageQueue = messageQueue;
this.clientSocket = clientSocket;
isRunning = true;
}
// 线程核心:循环接收服务端消息(历史/广播),打印展示
@Override
public void run() {
try {
ois = new ObjectInputStream(clientSocket.getInputStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
// 当主线程和自身都在跑的时候
while (ServerMainThread.isRunning() && this.isRunning) {
// 接收服务端消息
// 接收到后将其添加到阻塞队列中
try {
Wrapper msg = (Wrapper) ois.readObject();
// 如果收到的是关闭信息,则这个循环结束后关闭自身
if (msg.getOperation() == global.OPT_LOGOUT) {
isRunning = false;
System.out.println("接受线程已结束");
}
// 添加信息到阻塞队列。
messageQueue.put(msg);
} catch (InterruptedException e) {
System.out.println("消息队列被中断");
e.printStackTrace();
} catch (IOException e) {
if (isConnectionClosed(e)) {
// 链接断开则结束链接
// isRunning =false;
System.out.println(clientSocket.getPort() + ": 连接已断开");
break;
} else {
System.out.println("发送消息时发生IO异常: " + e.getMessage());
e.printStackTrace();
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
private boolean isConnectionClosed(IOException e) {
// 根据异常类型判断连接是否断开
return e instanceof SocketException ||
e.getMessage() != null && (e.getMessage().contains("Connection reset") ||
e.getMessage().contains("Broken pipe") ||
e.getMessage().contains("Connection refused") ||
e.getMessage().contains("Software caused connection abort"));
}
}
+210
View File
@@ -0,0 +1,210 @@
package server.serveice;
import global.global;
import server.data.GroupData;
import server.data.UserData;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
// 打包类,用于将数据打包传输
/**
* Wrapper类的构造方法这里设置为私有,需要时请使用/创建对应的静态构造方法来获取对应的信息载体。
* 详情请查阅源代码。
*/
public class Wrapper implements Serializable {
private static final long serialVersionUID = 7499350690768481854L;
private Object data;
private String senderId;
private String groupId;
private int operation;
public Wrapper(Object data, String senderId, String groupId, int operation) {
this.data = data;
this.senderId = senderId;
this.groupId = groupId;
this.operation = operation;
}
public Object getData() {
return data;
}
public String getSenderId() {
return senderId;
}
public String getGroupId() {
return groupId;
}
public int getOperation() {
return operation;
}
// 一下静态方法都是用于便捷创建消息体的方法。字如其名,当你需要创造相应的信息载体的时候,请使用对应的方法。
// 如果有其它需要,请在这里手动添加新的静态方法用于创建对应数据类别。
// 需要注意的时,有有一部分的类型的信息体是客户端和服务器端都可以发送的,但是他们发送具有不同的含义。
// 注册请求
public static Wrapper registerRequest(String[] nickNameAndpwsd, String senderId) {
return new Wrapper(nickNameAndpwsd, senderId, null, global.OPT_REGISTER);
}
// 注册请求
public static Wrapper registerRequest(String senderId, String password, String username) {
String[] temp = new String[] { username, password };
return new Wrapper(temp, senderId, null, global.OPT_REGISTER);
}
// 登录请求
public static Wrapper loginRequest(String senderId, String password) {
return new Wrapper(password, senderId, null, global.OPT_LOGIN);
}
// 服务器回复,用于回复一些简单的关于操作流的信息
public static Wrapper serverResponse(int opt) {
return new Wrapper(null, global.SERVER_ACCOUNT, null, opt);
}
// 服务器回复,带文本信息
public static Wrapper serverResponse(int opt, String msg) {
return new Wrapper(msg, global.SERVER_ACCOUNT, null, opt);
}
// 构造一个只包含操作码的消息(用于 OPT_EXIT 等)
public Wrapper(int operation) {
this.data = null;
this.senderId = global.SERVER_ACCOUNT;
this.groupId = null;
this.operation = operation;
}
// 登出请求
public static Wrapper logoutRequest(String senderId) {
return new Wrapper(null, senderId, null, global.OPT_LOGOUT);
}
// 初始化请求,请求服务器发送账户和当前的群聊数据
public static Wrapper initRequest(String senderId, int opt) {
return new Wrapper(null, senderId, null, opt);
}
// 初始化回复,将群聊信息回复给客户端
public static Wrapper initResponse(GroupData groupData) {
return new Wrapper(groupData, global.SERVER_ACCOUNT, null, global.OPT_INIT_GROUP);
}
// 初始化回复,将聊天记录回复给客户端
public static Wrapper initResponse(List<String> chatRecords, String groupId) {
return new Wrapper(chatRecords, global.SERVER_ACCOUNT, groupId, global.OPT_INIT_CHAT);
}
// 将用户id/名字回复给客户端。
public static Wrapper initResponse(Map<String, String> idNameMap) {
return new Wrapper(idNameMap, global.SERVER_ACCOUNT, null, global.OPT_INIT_USER);
}
// 更新用户昵称请求
public static Wrapper updateUserNameRequest(String senderId, String nickName) {
return new Wrapper(nickName, senderId, null, global.OPT_UPDATE_NICKNAME);
}
// 申请加入群聊
public static Wrapper joinGroupRequest(String senderId, String groupId) {
return new Wrapper(null, senderId, groupId, global.OPT_GROUP_JOIN);
}
// 申请添加好友
public static Wrapper addFriendRequest(String senderId, String friendId) {
// friendId 放在 data 中
return new Wrapper(friendId, senderId, null, global.OPT_FRIEND_ADD);
}
// 更新用户密码请求
public static Wrapper updateUserPwdRequest2(String senderId, String newPwd) {
return new Wrapper(newPwd, senderId, null, global.OPT_UPDATE_PASSWORD);
}
// 创建群聊请求
public static Wrapper createGroupRequest(String senderId, String groupName, String groupId) {
return new Wrapper(groupName, senderId, groupId, global.OPT_GROUP_CREATE);
}
// 邀请加入群聊请求
public static Wrapper groupInviteRequest(String invitedIdOrGroupName, String senderId, String groupId) {
return new Wrapper(invitedIdOrGroupName, senderId, groupId, global.OPT_GROUP_INVITE);
}
// 退出群聊请求
public static Wrapper groupQuitRequest(String senderId, String groupId) {
return new Wrapper(null, senderId, groupId, global.OPT_GROUP_QUIT);
}
// 退出群聊响应
public static Wrapper groupQuitResponse(String quitMemberId, String groupId) {
return new Wrapper(quitMemberId, global.SERVER_ACCOUNT, groupId, global.OPT_GROUP_QUIT);
}
// 解散群聊请求
public static Wrapper groupDisbandRequest(String senderId, String groupId) {
return new Wrapper(null, senderId, groupId, global.OPT_GROUP_DISBAND);
}
// 更新群聊名字请求
public static Wrapper groupUpdateNameRequest(String senderId, String groupId, String groupName) {
return new Wrapper(groupName, senderId, groupId, global.OPT_GROUP_UPDATE_NAME);
}
// 更新群聊群主请求
public static Wrapper groupUpdateOwnerRequest(String senderId, String groupId, String ownerId) {
return new Wrapper(ownerId, senderId, groupId, global.OPT_GROUP_UPDATE_OWNER);
}
// 聊天信息
public static Wrapper groupChat(String text, String senderId, String groupId) {
return new Wrapper(text, senderId, groupId, global.OPT_CHAT);
}
// 私聊信息
public static Wrapper privateChat(String text, String senderId, String receiverId) {
// 私聊数据存储在 data/friends/chat_data 中
// 为了复用字段,我们将 receiverId 放在 groupId 字段中作为目标ID
return new Wrapper(text, senderId, receiverId, global.OPT_PRIVATE_CHAT);
}
// 同意添加好友
public static Wrapper friendAddAgree(String senderId, String friendId) {
return new Wrapper(friendId, senderId, null, global.OPT_FRIEND_ADD_AGREE);
}
// 拒绝添加好友
public static Wrapper friendAddRefuse(String senderId, String friendId) {
return new Wrapper(friendId, senderId, null, global.OPT_FRIEND_ADD_REFUSE);
}
// 创建简单指令回复
public static Wrapper simpleRequest(String senderId, String groupId, int opt) {
return new Wrapper(null, senderId, groupId, opt);
}
// 初始化用户详细信息回复(Map<String, UserData>
public static Wrapper initUserDetailResponse(Map<String, UserData> userDetails) {
return new Wrapper(userDetails, global.SERVER_ACCOUNT, null, global.OPT_INIT_USER_DETAIL);
}
// 更新用户详细信息请求
public static Wrapper updateUserDetailRequest(String senderId, UserData userData) {
return new Wrapper(userData, senderId, null, global.OPT_UPDATE_USER_DETAIL);
}
// 更新用户详细信息响应(服务端广播)
public static Wrapper updateUserDetailResponse(UserData userData) {
return new Wrapper(userData, global.SERVER_ACCOUNT, null, global.OPT_UPDATE_USER_DETAIL);
}
}
+104
View File
@@ -0,0 +1,104 @@
package test;
import server.data.ServerData;
import util.FileUtil;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.junit.jupiter.api.Assertions.*;
public class MainTest {
@Test
public void testLoadEmptyFile() throws IOException {
// 测试加载空文件的情况
Path dataPath = Paths.get(FileUtil.DATA_FILE, FileUtil.GROUPS_DIR, FileUtil.SERVER_DATA_FILENAME);
File folder = dataPath.getParent().toFile();
if (!folder.exists()) {
folder.mkdirs();
}
// 创建空文件
Files.write(dataPath, new byte[0]);
System.out.println("Created empty data file: " + dataPath.toAbsolutePath());
// 重置实例,强制重新加载
ServerData.resetInstance();
// 测试获取实例是否返回非空对象
ServerData data = ServerData.getInstance();
assertNotNull(data);
assertNotNull(data.getServerGroups()); // 确保群组映射不为空
// assertTrue(data.getServerGroups().isEmpty()); // 可能会初始化空映射
System.out.println("Test passed: Empty file handled correctly.");
// 删除测试文件
Files.deleteIfExists(dataPath);
}
@Test
public void testLoadMissingFile() {
// 测试加载缺失文件的情况
Path dataPath = Paths.get(FileUtil.DATA_FILE, FileUtil.GROUPS_DIR, FileUtil.SERVER_DATA_FILENAME);
try {
Files.deleteIfExists(dataPath);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("Ensured data file is missing.");
// 重置实例,强制重新加载
ServerData.resetInstance();
// 测试获取实例是否返回非空对象
ServerData data = ServerData.getInstance();
assertNotNull(data);
assertNotNull(data.getServerGroups()); // 确保群组映射不为空
assertTrue(data.getServerGroups().isEmpty()); // 可能会初始化空映射
System.out.println("Test passed: Missing file handled correctly.");
}
@Test
public void testLoadValidFile() throws IOException {
// 测试加载有效文件的情况
System.out.println("Starting testLoadValidFile...");
// 测试加载有效文件的情况
Path dataPath = Paths.get(FileUtil.DATA_FILE, FileUtil.GROUPS_DIR, FileUtil.SERVER_DATA_FILENAME);
try {
Files.deleteIfExists(dataPath);
} catch (IOException e) {
e.printStackTrace();
}
// 初始化 ServerData 实例,确保其为空
ServerData.resetInstance();
ServerData data = ServerData.getInstance();
System.out.println("Initialized ServerData for saving.");
// 保存 ServerData 实例,确保文件存在
data.saveServerData();
System.out.println("Saved ServerData.");
// 重置实例,强制重新加载
ServerData.resetInstance();
System.out.println("Reset instance. Now calling getInstance() to trigger load...");
// 测试获取实例是否返回非空对象
// 确保加载的 ServerData 实例不为空
ServerData loaded = ServerData.getInstance();
assertNotNull(loaded);
System.out.println("Test passed: Valid file loaded correctly.");
}
}
+32
View File
@@ -0,0 +1,32 @@
package util;
import java.io.Serializable;
public class DataObj implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String name;
private String message;
public DataObj(){}
public DataObj(String id, String name, String message) {
this.id = id;
this.name = name;
this.message = message;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public String getMessage() {
return message;
}
}
+219
View File
@@ -0,0 +1,219 @@
package util;
import server.data.ServerData;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
/*
*通用文件IO工具类
*将聊天记录等信息写入本地文件
*/
public class FileUtil {
// 数据文件夹
public static final String DATA_FILE = "data";
// 子文件夹
public static final String GROUPS_DIR = "groups";
public static final String FRIENDS_DIR = "friends";
// 服务器数据文件名
public static final String SERVER_DATA_FILENAME = "server_data.data";
// 聊天历史消息存储文件夹名
public static final String CHAT_DATA_DIRNAME = "chat_data";
// 辅助方法:获取Group ServerData路径
private static Path getGroupServerDataPath() {
return Paths.get(DATA_FILE, GROUPS_DIR, SERVER_DATA_FILENAME);
}
// 辅助方法:获取Friend ServerData路径
private static Path getFriendServerDataPath() {
return Paths.get(DATA_FILE, FRIENDS_DIR, SERVER_DATA_FILENAME);
}
// 辅助方法:获取Chat Data路径
private static Path getChatDataPath(String id, boolean isGroup) {
String subDir = isGroup ? GROUPS_DIR : FRIENDS_DIR;
return Paths.get(DATA_FILE, subDir, CHAT_DATA_DIRNAME, id + ".txt");
}
/**
* 读取文件中的serverData信息,返回ServerData对象
* 从groups和friends文件夹分别读取并合并
*/
public static ServerData loadServerData() {
ServerData finalData = new ServerData();
boolean loadedAny = false;
// 1. Load Group Data
ServerData groupData = loadDataFromFile(getGroupServerDataPath());
if (groupData != null) {
if (groupData.getServerGroups() != null) {
finalData.setServerGroups(groupData.getServerGroups());
}
loadedAny = true;
}
// 2. 加载好友数据
ServerData friendData = loadDataFromFile(getFriendServerDataPath());
if (friendData != null) {
if (friendData.getServerUsers() != null) {
finalData.setServerUsers(friendData.getServerUsers());
}
loadedAny = true;
}
return loadedAny ? finalData : null;
}
private static ServerData loadDataFromFile(Path path) {
if (!Files.exists(path) || path.toFile().length() == 0)
return null;
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(new FileInputStream(path.toFile())))) {
return (ServerData) ois.readObject();
} catch (Exception e) {
System.err.println("加载数据失败 (" + path + "): " + e.getMessage());
return null;
}
}
/**
* 使用静态方法获取serverData对象,将serverData中的信息写入文件中
*/
public static void saveServerData() {
ServerData data = ServerData.getInstance();
// Save Groups
ServerData groupData = new ServerData();
groupData.setServerGroups(data.getServerGroups());
// 清空 Users,避免重复存储到groups文件
groupData.setServerUsers(null);
saveDataToFile(getGroupServerDataPath(), groupData);
// Save Friends
ServerData friendData = new ServerData();
friendData.setServerUsers(data.getServerUsers());
// 清空 Groups,避免重复存储到friends文件
friendData.setServerGroups(null);
saveDataToFile(getFriendServerDataPath(), friendData);
System.out.println("服务器数据保存成功");
}
private static void saveDataToFile(Path path, ServerData data) {
File file = path.toFile();
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
synchronized (ServerData.class) {
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream(file)))) {
oos.writeObject(data);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 检查数据文件是否存在 (任意一个存在即认为存在)
*/
public static boolean isDataFileExists() {
return Files.exists(getGroupServerDataPath()) || Files.exists(getFriendServerDataPath());
}
/**
* 获取数据文件大小(字节)
*/
public static long getDataFileSize() {
long size = 0;
File gFile = getGroupServerDataPath().toFile();
if (gFile.exists())
size += gFile.length();
File fFile = getFriendServerDataPath().toFile();
if (fFile.exists())
size += fFile.length();
return size;
}
// ===================================聊天信息=======================
/**
* 将聊天信息追加写入文件
*
* @param groupId 群聊id 或 用户id
* @param senderId 发送者id
* @param senderName 发送者名字
* @param content 发送内容
*/
public static void addChatMessage(String groupId, String senderId, String senderName, String content) {
String msg = MsgUtil.combineMsg(senderId, senderName, content);
writeChatMsg(groupId, msg);
}
public static void addChatMessage(String groupId, String contentFromWrapper) {
writeChatMsg(groupId, contentFromWrapper);
}
private static void writeChatMsg(String id, String content) {
// 判断是群聊还是私聊
// 通过 ServerGroups 来判断,如果在群组Map中,则是群聊,否则认为是私聊
boolean isGroup = false;
if (ServerData.getInstance().getServerGroups() != null) {
isGroup = ServerData.getInstance().getServerGroups().containsKey(id);
}
Path path = getChatDataPath(id, isGroup);
File file = path.toFile();
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
try (FileWriter fw = new FileWriter(file, true);
BufferedWriter bw = new BufferedWriter(fw)) {
// 写入文本并换行
bw.write(content);
bw.newLine(); // 跨平台换行
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 读取某个群的所有聊天记录
*
* @return 历史消息列表,无内容/文件不存在返回空List,绝不返回null
*/
public static List<String> loadGroupChatMsg(String groupId) {
// 判断是群聊还是私聊
boolean isGroup = false;
if (ServerData.getInstance().getServerGroups() != null) {
isGroup = ServerData.getInstance().getServerGroups().containsKey(groupId);
}
Path path = getChatDataPath(groupId, isGroup);
List<String> lines;
try {
lines = Files.readAllLines(path);
} catch (NoSuchFileException e) {
lines = new ArrayList<>();
} catch (IOException e) {
throw new RuntimeException(e);
}
if (lines == null) {
lines = new ArrayList<>();
}
return lines;
}
}
+47
View File
@@ -0,0 +1,47 @@
package util;
import java.io.Serializable;
import java.util.regex.Pattern;
import static global.global.DATA_SPLIT;
/**
* 工具类,用于将聊过天记录拆分/整合
*/
public class MsgUtil implements Serializable {
public static final int SENDERID_POS = 0;
public static final int SENDER_NAME_POS = 1;
public static final int TIME_POS = 2;
public static final int CONTENT_POS = 3;
private static final Pattern SPLIT_PATTERN = Pattern.compile(Pattern.quote(DATA_SPLIT));
public static String combineMsg(String senderId, String senderName, String content) {
return senderId + DATA_SPLIT + senderName + DATA_SPLIT + content;
}
public static String[] splitMsg(String msg) {
if (msg == null) {
return new String[0];
}
// 使用 -1 作为 limit 参数,保留空字符串
return SPLIT_PATTERN.split(msg, -1);
}
public static String getSenderName(String msg) {
return splitMsg(msg)[SENDER_NAME_POS];
}
public static String getSenderId(String msg) {
return splitMsg(msg)[SENDERID_POS];
}
public static String getTime(String msg) {
return splitMsg(msg)[TIME_POS];
}
public static String getContent(String msg) {
return splitMsg(msg)[CONTENT_POS];
}
}