diff --git a/code/link/link-backend/.idea/jarRepositories.xml b/code/link/link-backend/.idea/jarRepositories.xml index aa4ad020..05657f84 100644 --- a/code/link/link-backend/.idea/jarRepositories.xml +++ b/code/link/link-backend/.idea/jarRepositories.xml @@ -21,15 +21,15 @@ - - - - - + + + + + \ No newline at end of file diff --git a/code/link/link-backend/.idea/misc.xml b/code/link/link-backend/.idea/misc.xml index c7e94378..b784bbc8 100644 --- a/code/link/link-backend/.idea/misc.xml +++ b/code/link/link-backend/.idea/misc.xml @@ -8,5 +8,5 @@ - + \ No newline at end of file diff --git a/code/link/link-backend/pom.xml b/code/link/link-backend/pom.xml index 676dcab2..3e384b0f 100644 --- a/code/link/link-backend/pom.xml +++ b/code/link/link-backend/pom.xml @@ -58,11 +58,11 @@ spring-boot-starter-logging - - org.springframework.boot - spring-boot-devtools - true - + + + + + org.junit.jupiter diff --git a/code/link/link-backend/src/main/java/com/link/api/controller/AuthController.java b/code/link/link-backend/src/main/java/com/link/api/controller/AuthController.java index 4842e6c5..af1e3a4c 100644 --- a/code/link/link-backend/src/main/java/com/link/api/controller/AuthController.java +++ b/code/link/link-backend/src/main/java/com/link/api/controller/AuthController.java @@ -24,6 +24,7 @@ public class AuthController { response.put("message", "Missing parameters"); return response; } + UserInfo userInfo = UserRepository.getUserPasswordById(orgId, userId); if (userInfo != null && userInfo.getPassword().equals(password)) { String token = JwtUtil.generateToken(userId, orgId); @@ -33,6 +34,7 @@ public class AuthController { } else { response.put("message", "Invalid credentials"); } + return response; } diff --git a/code/link/link-backend/src/main/java/com/link/api/controller/ColumnConfigController.java b/code/link/link-backend/src/main/java/com/link/api/controller/ColumnConfigController.java index f8520309..2f862779 100644 --- a/code/link/link-backend/src/main/java/com/link/api/controller/ColumnConfigController.java +++ b/code/link/link-backend/src/main/java/com/link/api/controller/ColumnConfigController.java @@ -19,10 +19,7 @@ public class ColumnConfigController { */ @GetMapping("/api/columns") public Result> getColumnConfig(@RequestParam String tableName, HttpServletRequest request) { - - try { - String orgId = UserContext.getOrgId(); System.out.println("api/columns:"+orgId); List> data = JsonUtils.queryColumnConfig(orgId, tableName); @@ -32,10 +29,8 @@ public class ColumnConfigController { } } - @GetMapping("/api/columnsEdit") public Result> getColumnEditConfig(@RequestParam String tableName, HttpServletRequest request) { - try { String orgId = UserContext.getOrgId(); List> data = JsonUtils.queryColumnEditConfig(orgId, tableName); diff --git a/code/link/link-backend/src/main/java/com/link/api/controller/WebConfig.java b/code/link/link-backend/src/main/java/com/link/api/controller/WebConfig.java index bf488458..c6acaf20 100644 --- a/code/link/link-backend/src/main/java/com/link/api/controller/WebConfig.java +++ b/code/link/link-backend/src/main/java/com/link/api/controller/WebConfig.java @@ -8,7 +8,7 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOriginPatterns("http://localhost:5173", "http://localhost:8089","http://localhost:5174") // 前端地址 + .allowedOriginPatterns("*") // 前端地址 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); diff --git a/code/link/link-backend/src/main/java/com/link/api/util/PaginationUtils.java b/code/link/link-backend/src/main/java/com/link/api/util/PaginationUtils.java index cccabf46..76a96f9b 100644 --- a/code/link/link-backend/src/main/java/com/link/api/util/PaginationUtils.java +++ b/code/link/link-backend/src/main/java/com/link/api/util/PaginationUtils.java @@ -3,11 +3,15 @@ package com.link.api.util; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.link.im.manager.UserChannelManager; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; import java.sql.*; import java.sql.Date; import java.text.SimpleDateFormat; import java.util.*; +import java.util.stream.Collectors; public class PaginationUtils { @@ -470,5 +474,5 @@ public class PaginationUtils { } } -} +} diff --git a/code/link/link-backend/src/main/java/com/link/im/controller/CompanyController.java b/code/link/link-backend/src/main/java/com/link/im/controller/CompanyController.java new file mode 100644 index 00000000..9172b256 --- /dev/null +++ b/code/link/link-backend/src/main/java/com/link/im/controller/CompanyController.java @@ -0,0 +1,43 @@ +package com.link.im.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.link.api.util.DruidDataSourceManager; +import com.link.api.util.PaginationUtils; +import com.link.api.util.Result; +import com.link.api.util.UserContext; +import com.link.im.manager.UserChannelManager; +import com.link.im.utils.DbUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestController +@CrossOrigin +public class CompanyController { + + @GetMapping("/api/loadCompanyList") + public Result> getCompanyList(){ + try { + String orgId = UserContext.getOrgId(); + String sql = "select b_id,b_name from b_user where b_canuse = '1' "; + + List> maps = DbUtils.queryForList(orgId, sql); + + for (Map map : maps) { + String bId = map.get("b_id").toString(); + + map.put("_online",UserChannelManager.isOnline(bId)); + } + + return Result.ok("成功",maps); + } catch (Exception e) { + return Result.fail("公司列表查询异常:" + e.getMessage()); + } + } +} diff --git a/code/link/link-backend/src/main/java/com/link/im/handler/ImMessageProcessor.java b/code/link/link-backend/src/main/java/com/link/im/handler/ImMessageProcessor.java new file mode 100644 index 00000000..873105e3 --- /dev/null +++ b/code/link/link-backend/src/main/java/com/link/im/handler/ImMessageProcessor.java @@ -0,0 +1,72 @@ +package com.link.im.handler; + +import com.link.api.util.UserContext; +import com.link.im.manager.UserChannelManager; +import com.link.im.manager.WriteManager; +import com.link.im.model.ImRequest; +import com.link.im.model.ImResponse; +import com.link.im.utils.CommonUtils; +import com.link.im.utils.DbUtils; +import io.netty.channel.ChannelHandlerContext; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + +@Component +public class ImMessageProcessor { + public void process(ChannelHandlerContext ctx, ImRequest request) { + String type = request.getType(); + if (type == null) { + WriteManager.writeToChannel(ctx.channel(), "消息类型不能为空"); + return; + } + + switch (type) { + case "ping": + handlePing(ctx); + break; + case "login": + handleLogin(ctx, request); + break; + default: + WriteManager.writeToChannel(ctx.channel(), "未知的消息类型: " + type); + break; + } + } + + private void handlePing(ChannelHandlerContext ctx) { + WriteManager.writeToChannel(ctx.channel(), ImResponse.success("pong",null)); + } + + private void handleLogin(ChannelHandlerContext ctx,ImRequest request){ + String fromUserid = request.getFrom(); + UserChannelManager.addUser(fromUserid,ctx.channel()); + String orgId = request.getOrgId(); + + System.out.println(orgId); + + // 1.通知给好友 + String sql1 = "select b_frienduser_Id from IM_Friends where b_user_id = '" + fromUserid +"'"; + try { + List> maps = DbUtils.queryForList(orgId, sql1); + for (Map map : maps) { + String bFrienduserId = map.get("b_frienduser_Id").toString(); + if (CommonUtils.isNotEmpty(bFrienduserId)) { + boolean friendOnline = UserChannelManager.isOnline(bFrienduserId); + if(friendOnline){ + + } + } + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + // 2.通知给群组 + + // 3.通知给全部(测试用,先留,后面会删) + } + +} diff --git a/code/link/link-backend/src/main/java/com/link/im/handler/ImWebSocketHandler.java b/code/link/link-backend/src/main/java/com/link/im/handler/ImWebSocketHandler.java index b995395e..9360461b 100644 --- a/code/link/link-backend/src/main/java/com/link/im/handler/ImWebSocketHandler.java +++ b/code/link/link-backend/src/main/java/com/link/im/handler/ImWebSocketHandler.java @@ -5,6 +5,8 @@ import com.link.im.manager.GroupManager; import com.link.im.manager.UserChannelManager; import com.link.im.manager.WriteManager; import com.link.im.model.ImMessage; +import com.link.im.model.ImRequest; +import com.link.im.model.ImResponse; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; @@ -17,6 +19,7 @@ import org.slf4j.LoggerFactory; public class ImWebSocketHandler extends SimpleChannelInboundHandler { private static final Logger logger = LoggerFactory.getLogger(ImWebSocketHandler.class); private static final ObjectMapper MAPPER = new ObjectMapper(); + private final ImMessageProcessor imMessageProcessor = new ImMessageProcessor(); @Override public void handlerAdded(ChannelHandlerContext ctx) { @@ -35,14 +38,15 @@ public class ImWebSocketHandler extends SimpleChannelInboundHandler userChannelMap = new ConcurrentHashMap<>(); /** - * 添加用户与Channel绑定 + * 添加用户与Channel绑定,如果已有绑定,先移除旧的 */ public static void addUser(String userId, Channel channel) { - if (userId != null && channel != null) { - userChannelMap.put(userId, channel); + if (userId == null || channel == null) return; + + // 如果已有旧的 channel,先移除 + Channel oldChannel = userChannelMap.put(userId, channel); + + if (oldChannel != null && oldChannel != channel) { + try { + if (oldChannel.isActive()) { + oldChannel.close(); + } + logger.info("用户 {} 已有旧 channel,被替换: oldChannelId={}, newChannelId={}", + userId, oldChannel.id().asShortText(), channel.id().asShortText()); + } catch (Exception e) { + logger.error("关闭旧 channel 异常: userId={}", userId, e); + } } + + logger.info("用户上线: userId={}, channelId={}", userId, channel.id().asShortText()); } /** * 移除用户绑定 */ public static void removeUserById(String userId) { - if (userId != null) { - userChannelMap.remove(userId); + if (userId == null) return; + + Channel removed = userChannelMap.remove(userId); + if (removed != null) { + try { + if (removed.isActive()) { + removed.close(); + } + } catch (Exception e) { + logger.error("关闭 channel 异常: userId={}", userId, e); + } + logger.info("用户下线: userId={}, channelId={}", userId, removed.id().asShortText()); } } @@ -60,5 +83,4 @@ public class UserChannelManager { Channel ch = userChannelMap.get(userId); return ch != null && ch.isActive(); } - } diff --git a/code/link/link-backend/src/main/java/com/link/im/model/ImMessage.java b/code/link/link-backend/src/main/java/com/link/im/model/ImMessage.java index 12d370d5..975132d5 100644 --- a/code/link/link-backend/src/main/java/com/link/im/model/ImMessage.java +++ b/code/link/link-backend/src/main/java/com/link/im/model/ImMessage.java @@ -24,6 +24,18 @@ public class ImMessage { return type; } + @Override + public String toString() { + return "ImMessage{" + + "type='" + type + '\'' + + ", msg='" + msg + '\'' + + ", from='" + from + '\'' + + ", to='" + to + '\'' + + ", content='" + content + '\'' + + ", timestamp=" + timestamp + + '}'; + } + public void setType(String type) { this.type = type; } diff --git a/code/link/link-backend/src/main/java/com/link/im/model/ImRequest.java b/code/link/link-backend/src/main/java/com/link/im/model/ImRequest.java new file mode 100644 index 00000000..6aad21af --- /dev/null +++ b/code/link/link-backend/src/main/java/com/link/im/model/ImRequest.java @@ -0,0 +1,82 @@ +package com.link.im.model; + +public class ImRequest { + private String type; + private String from; + private String to; + private String content; + private String orgId; + private long timestamp; + + // 无参构造 + public ImRequest() {} + + // 全参构造 + public ImRequest(String type, String from, String to, String content, long timestamp,String orgId) { + this.type = type; + this.from = from; + this.to = to; + this.content = content; + this.timestamp = timestamp; + this.orgId = orgId; + } + + public String getOrgId() { + return orgId; + } + + public void setOrgId(String orgId) { + this.orgId = orgId; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getFrom() { + return from; + } + + public void setFrom(String from) { + this.from = from; + } + + public String getTo() { + return to; + } + + public void setTo(String to) { + this.to = to; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + @Override + public String toString() { + return "ImRequest{" + + "type='" + type + '\'' + + ", from='" + from + '\'' + + ", to='" + to + '\'' + + ", content='" + content + '\'' + + ", timestamp=" + timestamp + + '}'; + } +} \ No newline at end of file diff --git a/code/link/link-backend/src/main/java/com/link/im/model/ImResponse.java b/code/link/link-backend/src/main/java/com/link/im/model/ImResponse.java new file mode 100644 index 00000000..f6e695f1 --- /dev/null +++ b/code/link/link-backend/src/main/java/com/link/im/model/ImResponse.java @@ -0,0 +1,89 @@ +package com.link.im.model; + +public class ImResponse { + private String type; + private int code; + private String message; + private long timestamp; + private Object data; + + // 无参构造 + public ImResponse() {} + + // 全参构造 + public ImResponse(String type, int code, String message, long timestamp, Object data) { + this.type = type; + this.code = code; + this.message = message; + this.timestamp = timestamp; + this.data = data; + } + + public static ImResponse success(String type, Object data) { + return new ImResponse(type, 200, "success", System.currentTimeMillis(), data); + } + + public static ImResponse success(String type, String message, Object data) { + return new ImResponse(type, 200, message, System.currentTimeMillis(), data); + } + + public static ImResponse error(String type, String message) { + return new ImResponse(type, 500, message, System.currentTimeMillis(), null); + } + + public static ImResponse error(String type, int code, String message) { + return new ImResponse(type, code, message, System.currentTimeMillis(), null); + } + + // Getter & Setter + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } + + @Override + public String toString() { + return "ImResponse{" + + "type='" + type + '\'' + + ", code=" + code + + ", message='" + message + '\'' + + ", timestamp=" + timestamp + + ", data=" + data + + '}'; + } +} \ No newline at end of file diff --git a/code/link/link-backend/src/main/java/com/link/im/utils/CommonUtils.java b/code/link/link-backend/src/main/java/com/link/im/utils/CommonUtils.java new file mode 100644 index 00000000..f48ea696 --- /dev/null +++ b/code/link/link-backend/src/main/java/com/link/im/utils/CommonUtils.java @@ -0,0 +1,21 @@ +package com.link.im.utils; + +public class CommonUtils { + /** + * 判断字符串是否为空(null 或 "") + * @param str 待判断的字符串 + * @return true 如果字符串为 null 或 空串 + */ + public static boolean isEmpty(String str) { + return str == null || str.trim().isEmpty(); + } + + /** + * 判断字符串是否非空(不是 null 且不为空串) + * @param str 待判断的字符串 + * @return true 如果字符串非空 + */ + public static boolean isNotEmpty(String str) { + return !isEmpty(str); + } +} diff --git a/code/link/link-backend/src/main/java/com/link/im/utils/DbUtils.java b/code/link/link-backend/src/main/java/com/link/im/utils/DbUtils.java new file mode 100644 index 00000000..dd6564c2 --- /dev/null +++ b/code/link/link-backend/src/main/java/com/link/im/utils/DbUtils.java @@ -0,0 +1,41 @@ +package com.link.im.utils; + + +import com.link.api.util.DruidDataSourceManager; + +import java.sql.*; +import java.util.*; + +public class DbUtils { + + /** + * 根据 SQL 查询数据 + * + * @param orgCode 数据源组织编码 + * @param sql 查询 SQL + * @return List>,每一行作为 Map + */ + public static List> queryForList(String orgCode, String sql) throws SQLException { + List> resultList = new ArrayList<>(); + + try (Connection conn = DruidDataSourceManager.getConnection(orgCode); + PreparedStatement stmt = conn.prepareStatement(sql); + ResultSet rs = stmt.executeQuery()) { + + ResultSetMetaData metaData = rs.getMetaData(); + int columnCount = metaData.getColumnCount(); + + while (rs.next()) { + Map rowMap = new LinkedHashMap<>(); + for (int i = 1; i <= columnCount; i++) { + String columnName = metaData.getColumnLabel(i); + Object value = rs.getObject(i); + rowMap.put(columnName, value); + } + resultList.add(rowMap); + } + } + + return resultList; + } +} diff --git a/code/link/link-backend/src/main/resources/dbconfigs/G3HD.properties b/code/link/link-backend/src/main/resources/dbconfigs/G3HD.properties index 7506e981..c25db874 100644 --- a/code/link/link-backend/src/main/resources/dbconfigs/G3HD.properties +++ b/code/link/link-backend/src/main/resources/dbconfigs/G3HD.properties @@ -1,4 +1,4 @@ -url=jdbc:sqlserver://127.0.0.1:1433;databaseName=G3HY2025 +url=jdbc:sqlserver://192.168.1.189:1433;databaseName=G3-Link;encrypt=false username=sa password=g3soft driver=com.microsoft.sqlserver.jdbc.SQLServerDriver \ No newline at end of file diff --git a/code/link/link-backend/target/classes/com/link/api/controller/AuthController.class b/code/link/link-backend/target/classes/com/link/api/controller/AuthController.class index e69a9c6d..393969b2 100644 Binary files a/code/link/link-backend/target/classes/com/link/api/controller/AuthController.class and b/code/link/link-backend/target/classes/com/link/api/controller/AuthController.class differ diff --git a/code/link/link-backend/target/classes/com/link/api/controller/ColumnConfigController.class b/code/link/link-backend/target/classes/com/link/api/controller/ColumnConfigController.class index 50b962d5..b4151b0e 100644 Binary files a/code/link/link-backend/target/classes/com/link/api/controller/ColumnConfigController.class and b/code/link/link-backend/target/classes/com/link/api/controller/ColumnConfigController.class differ diff --git a/code/link/link-backend/target/classes/com/link/api/controller/WebConfig.class b/code/link/link-backend/target/classes/com/link/api/controller/WebConfig.class index 219b9665..3b2514fa 100644 Binary files a/code/link/link-backend/target/classes/com/link/api/controller/WebConfig.class and b/code/link/link-backend/target/classes/com/link/api/controller/WebConfig.class differ diff --git a/code/link/link-backend/target/classes/com/link/api/util/PaginationUtils.class b/code/link/link-backend/target/classes/com/link/api/util/PaginationUtils.class index 1ec6d0f6..d5435fac 100644 Binary files a/code/link/link-backend/target/classes/com/link/api/util/PaginationUtils.class and b/code/link/link-backend/target/classes/com/link/api/util/PaginationUtils.class differ diff --git a/code/link/link-backend/target/classes/com/link/im/controller/CompanyController.class b/code/link/link-backend/target/classes/com/link/im/controller/CompanyController.class new file mode 100644 index 00000000..fd04807c Binary files /dev/null and b/code/link/link-backend/target/classes/com/link/im/controller/CompanyController.class differ diff --git a/code/link/link-backend/target/classes/com/link/im/handler/ImMessageProcessor.class b/code/link/link-backend/target/classes/com/link/im/handler/ImMessageProcessor.class new file mode 100644 index 00000000..f470142a Binary files /dev/null and b/code/link/link-backend/target/classes/com/link/im/handler/ImMessageProcessor.class differ diff --git a/code/link/link-backend/target/classes/com/link/im/handler/ImWebSocketHandler.class b/code/link/link-backend/target/classes/com/link/im/handler/ImWebSocketHandler.class index 185fef10..fecb8e7d 100644 Binary files a/code/link/link-backend/target/classes/com/link/im/handler/ImWebSocketHandler.class and b/code/link/link-backend/target/classes/com/link/im/handler/ImWebSocketHandler.class differ diff --git a/code/link/link-backend/target/classes/com/link/im/manager/UserChannelManager.class b/code/link/link-backend/target/classes/com/link/im/manager/UserChannelManager.class index 6dd28505..2281d39a 100644 Binary files a/code/link/link-backend/target/classes/com/link/im/manager/UserChannelManager.class and b/code/link/link-backend/target/classes/com/link/im/manager/UserChannelManager.class differ diff --git a/code/link/link-backend/target/classes/com/link/im/model/ImMessage.class b/code/link/link-backend/target/classes/com/link/im/model/ImMessage.class index 222c1064..158194ca 100644 Binary files a/code/link/link-backend/target/classes/com/link/im/model/ImMessage.class and b/code/link/link-backend/target/classes/com/link/im/model/ImMessage.class differ diff --git a/code/link/link-backend/target/classes/com/link/im/model/ImRequest.class b/code/link/link-backend/target/classes/com/link/im/model/ImRequest.class new file mode 100644 index 00000000..c364c427 Binary files /dev/null and b/code/link/link-backend/target/classes/com/link/im/model/ImRequest.class differ diff --git a/code/link/link-backend/target/classes/com/link/im/model/ImResponse.class b/code/link/link-backend/target/classes/com/link/im/model/ImResponse.class new file mode 100644 index 00000000..2fbd0c74 Binary files /dev/null and b/code/link/link-backend/target/classes/com/link/im/model/ImResponse.class differ diff --git a/code/link/link-backend/target/classes/com/link/im/utils/CommonUtils.class b/code/link/link-backend/target/classes/com/link/im/utils/CommonUtils.class new file mode 100644 index 00000000..e66d94de Binary files /dev/null and b/code/link/link-backend/target/classes/com/link/im/utils/CommonUtils.class differ diff --git a/code/link/link-backend/target/classes/com/link/im/utils/DbUtils.class b/code/link/link-backend/target/classes/com/link/im/utils/DbUtils.class new file mode 100644 index 00000000..ff384498 Binary files /dev/null and b/code/link/link-backend/target/classes/com/link/im/utils/DbUtils.class differ diff --git a/code/link/link-backend/target/classes/dbconfigs/G3HD.properties b/code/link/link-backend/target/classes/dbconfigs/G3HD.properties index 7506e981..c25db874 100644 --- a/code/link/link-backend/target/classes/dbconfigs/G3HD.properties +++ b/code/link/link-backend/target/classes/dbconfigs/G3HD.properties @@ -1,4 +1,4 @@ -url=jdbc:sqlserver://127.0.0.1:1433;databaseName=G3HY2025 +url=jdbc:sqlserver://192.168.1.189:1433;databaseName=G3-Link;encrypt=false username=sa password=g3soft driver=com.microsoft.sqlserver.jdbc.SQLServerDriver \ No newline at end of file diff --git a/code/link/link-frontend/src/App.vue b/code/link/link-frontend/src/App.vue index 67883e76..b8b83d70 100644 --- a/code/link/link-frontend/src/App.vue +++ b/code/link/link-frontend/src/App.vue @@ -1,18 +1,15 @@ - + diff --git a/code/link/link-frontend/src/layout/index.vue b/code/link/link-frontend/src/layout/index.vue new file mode 100644 index 00000000..c404a82f --- /dev/null +++ b/code/link/link-frontend/src/layout/index.vue @@ -0,0 +1,97 @@ + + + + + + + 首页 + + + IM即时通信 + + + 登录 + + + + + + + + + diff --git a/code/link/link-frontend/src/router/index.js b/code/link/link-frontend/src/router/index.js index f89695ad..af06cfe2 100644 --- a/code/link/link-frontend/src/router/index.js +++ b/code/link/link-frontend/src/router/index.js @@ -1,14 +1,30 @@ +import { ElMessage } from 'element-plus' import { createRouter, createWebHistory } from 'vue-router' +import { useUserStore } from '@/stores/userStore' +import Login from '@/views/auth/index.vue' +import Home from '@/views/home/index.vue' import Im from '@/views/im/index.vue' - +import { useWebSocket } from '../stores/websocketStore' +// 新增登录页面 const routes = [ { path: '/', - redirect: '/im', + redirect: '/home', + }, + { + path: '/home', + component: Home, + meta: { title: '首页' }, }, { path: '/im', component: Im, + meta: { title: 'IM聊天' }, + }, + { + path: '/login', + component: Login, + meta: { title: '登录' }, }, ] @@ -17,16 +33,36 @@ const router = createRouter({ routes, }) -// 全局前置守卫 router.beforeEach((to, from, next) => { - // 设置页面标题 document.title = to.meta.title ? `${to.meta.title} - 系统名称` : '系统名称' - // 通过检查,继续跳转 + const userStore = useUserStore() + const websocket = useWebSocket() + + // 登录页直接放行,不主动断开 + if (to.path === '/login') { + next() + return + } + + if (!userStore.isLogined()) { + ElMessage.warning('请先登录') + + userStore.resetUserInfo() + next({ path: '/login', query: { redirect: to.fullPath } }) + return + } + + // 登录状态下,确保连接 + if (!websocket.isConnected.value) { + websocket.connect() + } + next() }) -router.afterEach(() => { +router.afterEach((to, from) => { + }) export default router diff --git a/code/link/link-frontend/src/stores/userStore.js b/code/link/link-frontend/src/stores/userStore.js index 419ec6f7..3a56bd79 100644 --- a/code/link/link-frontend/src/stores/userStore.js +++ b/code/link/link-frontend/src/stores/userStore.js @@ -4,18 +4,19 @@ import { ref } from 'vue' export const useUserStore = defineStore( 'userInfo', () => { - const userInfo = ref({ - orgid: '', + orgid: 'g3hd', userid: '', token: '', + username: '', }) // 设置用户信息 - function setUserInfo(orgid, userid, token) { + function setUserInfo(orgid, userid, username, token) { userInfo.value.orgid = orgid userInfo.value.userid = userid userInfo.value.token = token + userInfo.value.username = username } // 重置用户信息 diff --git a/code/link/link-frontend/src/stores/websocketStore.js b/code/link/link-frontend/src/stores/websocketStore.js index 0eff01d1..036a4140 100644 --- a/code/link/link-frontend/src/stores/websocketStore.js +++ b/code/link/link-frontend/src/stores/websocketStore.js @@ -1,7 +1,15 @@ +// stores/websocket.js import { ElNotification } from 'element-plus' -import { onScopeDispose, ref } from 'vue' +import { ref } from 'vue' +import { useUserStore } from './userStore' + +let instance = null // 单例缓存 export function useWebSocket() { + if (instance) + return instance // 已存在则直接返回 + + const userStore = useUserStore() const socket = ref(null) const isConnected = ref(false) const listeners = ref(new Set()) @@ -9,25 +17,19 @@ export function useWebSocket() { let reconnectAttempts = 0 let pingTimer = null let pongTimeout = null - const PING_INTERVAL = 10000 // 收到PONG后间隔再发送下一次PING - const PONG_TIMEOUT = 15000 // PING发送后15秒未收到PONG就重连 - + const PING_INTERVAL = 10000 + const PONG_TIMEOUT = 15000 + const currentUrl = 'ws://127.0.0.1:8090/ws' let reconnectNotification = null - let currentUrl = null let isReconnecting = false + let manualClose = false // ✅ 标记是否为手动关闭 - // -------------------- - // 私有方法 - // -------------------- + // ---------------- 清理 ---------------- function cleanup() { - if (pingTimer) { - clearTimeout(pingTimer) - pingTimer = null - } - if (pongTimeout) { - clearTimeout(pongTimeout) - pongTimeout = null - } + clearTimeout(pingTimer) + clearTimeout(pongTimeout) + pingTimer = null + pongTimeout = null } function handleDisconnect() { @@ -39,23 +41,14 @@ export function useWebSocket() { listeners.value.forEach(cb => cb(message)) } - // -------------------- - // 心跳逻辑(链式) - // -------------------- + // ---------------- 心跳 ---------------- function sendPing() { if (!socket.value || socket.value.readyState !== WebSocket.OPEN) return - try { - sendMessage('PING') - } - catch (err) { - console.error('发送 PING 失败', err) - } + sendMessage({ type: 'ping' }) - // 设置 PONG 超时 - if (pongTimeout) - clearTimeout(pongTimeout) + clearTimeout(pongTimeout) pongTimeout = setTimeout(() => { console.warn('未收到PONG,触发重连') handleReconnect() @@ -63,15 +56,8 @@ export function useWebSocket() { } function handlePong() { - // 收到 PONG,清理超时 - if (pongTimeout) { - clearTimeout(pongTimeout) - pongTimeout = null - } - - // 等 PING_INTERVAL 后再发送下一次 PING - if (pingTimer) - clearTimeout(pingTimer) + clearTimeout(pongTimeout) + clearTimeout(pingTimer) pingTimer = setTimeout(sendPing, PING_INTERVAL) } @@ -79,62 +65,57 @@ export function useWebSocket() { sendPing() } - // -------------------- - // 重连逻辑 - // -------------------- + // ---------------- 重连 ---------------- function handleReconnect() { - if (isReconnecting) - return + if (isReconnecting || manualClose) + return // ✅ 如果是手动断开则不重连 + isReconnecting = true cleanup() isConnected.value = false socket.value?.close() reconnectAttempts++ - if (reconnectNotification) - reconnectNotification.close() + reconnectNotification?.close() reconnectNotification = ElNotification({ title: '提示', message: `WebSocket 连接失败,正在第 ${reconnectAttempts} 次重连...`, type: 'warning', duration: 0, - key: 'websocket-reconnect', }) const delay = Math.min(3000 * 2 ** (reconnectAttempts - 1), 30000) setTimeout(() => { isReconnecting = false - connect(currentUrl) + connect() }, delay) } function clearReconnectNotification() { - if (reconnectNotification) { - reconnectNotification.close() - reconnectNotification = null - } + reconnectNotification?.close() + reconnectNotification = null } - // -------------------- - // 公共方法 - // -------------------- - function connect(url) { - currentUrl = url + // ---------------- 公共方法 ---------------- + function connect() { if (isConnected.value) return - if (socket.value) - disconnect() - const newSocket = new WebSocket(url) + manualClose = false // ✅ 每次连接前重置为 false + + const newSocket = new WebSocket(currentUrl) newSocket.onopen = () => { isConnected.value = true isReconnecting = false clearReconnectNotification() + if (userStore.isLogined) + sendLogin(userStore.userInfo.userid) + ElNotification({ title: '提示', - message: 'WebSocket 连接成功', + message: '上线成功', type: 'success', duration: 2000, }) @@ -144,26 +125,42 @@ export function useWebSocket() { } newSocket.onmessage = (event) => { - let message + let resData try { - message = JSON.parse(event.data) + console.log(event.data) - if (message.type === 'PONG') { - handlePong() - return - } - - notifyListeners(message) + resData = JSON.parse(event.data) } catch (err) { console.error('消息解析失败:', err) notifyListeners({ type: 'ERROR', data: '消息解析失败' }) + return + } + + if (resData.code !== 200) { + ElNotification({ + title: '错误', + message: resData.message || '发生未知错误', + type: 'error', + showClose: true, + duration: 3000, + }) + return + } + + if (resData.type === 'pong') { + handlePong() + } + else { + notifyListeners(resData) } } newSocket.onclose = () => { handleDisconnect() - if (!isReconnecting) + + // ✅ 非手动断开才重连 + if (!manualClose && !isReconnecting) handleReconnect() } @@ -175,14 +172,25 @@ export function useWebSocket() { } function disconnect() { + if (!socket.value) + return + + manualClose = true socket.value?.close() cleanup() + clearReconnectNotification() + + ElNotification({ + type: 'warning', + title: '提示', + message: '已下线', + }) } - function sendMessage(type, data) { + function sendMessage(data) { if (isConnected.value && socket.value) { - const message = { type, data } - socket.value.send(JSON.stringify(message)) + data.orgId = userStore.userInfo.orgid || '' + socket.value.send(JSON.stringify(data)) } } @@ -191,13 +199,23 @@ export function useWebSocket() { return () => listeners.value.delete(callback) } - onScopeDispose(() => disconnect()) + function sendLogin(userId) { + if (!isConnected.value) + return + sendMessage({ + type: 'login', + from: userId, + }) + } - return { + instance = { isConnected, connect, disconnect, sendMessage, onMessage, + sendLogin, } + + return instance } diff --git a/code/link/link-frontend/src/utils/axios.js b/code/link/link-frontend/src/utils/axios.js index 0a76188b..66bf30df 100644 --- a/code/link/link-frontend/src/utils/axios.js +++ b/code/link/link-frontend/src/utils/axios.js @@ -1,34 +1,51 @@ // src/utils/axios.js import axios from 'axios' +import { useUserStore } from '@/stores/userStore' const instance = axios.create({ baseURL: 'http://localhost:8089/api', // 后端接口地址 - timeout: 5000 -}) + timeout: 5000, +}) // 请求拦截器:自动添加 token instance.interceptors.request.use( - config => { - const token = localStorage.getItem('token') - if (token) { - config.headers.Authorization = `Bearer ${token}` + (config) => { + const userStore = useUserStore() + + if (userStore.userInfo.token) { + config.headers.Authorization = `Bearer ${userStore.userInfo.token}` } return config }, - error => Promise.reject(error) + error => Promise.reject(error), ) // 响应拦截器:统一处理错误或过期 token instance.interceptors.response.use( response => response, - error => { + (error) => { + const userStore = useUserStore() if (error.response && error.response.status === 401) { alert('登录失效,请重新登录') - window.localStorage.removeItem('token') + userStore.resetUserInfo() window.location.href = '/login' } return Promise.reject(error) - } + }, ) +export function axiosPost(url, data = {}, config = {}) { + return instance.post(url, data, { + headers: { 'Content-Type': 'application/json' }, + ...config, + }).then(res => res.data) +} + +export function axiosGet(url, params = {}, config = {}) { + return instance.get(url, { + params, + ...config, + }).then(res => res.data) +} + export default instance diff --git a/code/link/link-frontend/src/views/auth/index.vue b/code/link/link-frontend/src/views/auth/index.vue new file mode 100644 index 00000000..048f0fc2 --- /dev/null +++ b/code/link/link-frontend/src/views/auth/index.vue @@ -0,0 +1,127 @@ + + + + + + + + + + + Online + + + + + + + + + + + 登录 + + + + + + + + + diff --git a/code/link/link-frontend/src/views/home/index.vue b/code/link/link-frontend/src/views/home/index.vue new file mode 100644 index 00000000..314950a6 --- /dev/null +++ b/code/link/link-frontend/src/views/home/index.vue @@ -0,0 +1,67 @@ + + + + + + {{ userStore.userInfo.username }},欢迎登录 + + + IM 聊天列表(支持多公司,单公司聊天) + + + + + + + + + 聊天 + + + + + + + +