Jelajahi Sumber

first commit

xiewenjie 3 tahun lalu
melakukan
adcff7364d
89 mengubah file dengan 17091 tambahan dan 0 penghapusan
  1. 2 0
      .gitignore
  2. 1 0
      jar.bat
  3. 1 0
      jar.sh
  4. 154 0
      pom.xml
  5. 1 0
      run.bat
  6. 3 0
      run.sh
  7. 36 0
      src/main/java/com/fdkk/BootApplication.java
  8. 97 0
      src/main/java/com/fdkk/component/handler/BindHandler.java
  9. 50 0
      src/main/java/com/fdkk/component/handler/BizHandler.java
  10. 50 0
      src/main/java/com/fdkk/component/handler/BizPrivateMsgHandler.java
  11. 72 0
      src/main/java/com/fdkk/component/handler/ClosedHandler.java
  12. 35 0
      src/main/java/com/fdkk/component/handler/annotation/ImHandler.java
  13. 78 0
      src/main/java/com/fdkk/component/message/BindMessageListener.java
  14. 32 0
      src/main/java/com/fdkk/component/message/PushMessageListener.java
  15. 72 0
      src/main/java/com/fdkk/component/push/DefaultMessagePusher.java
  16. 47 0
      src/main/java/com/fdkk/component/push/MessagePusher.java
  17. 58 0
      src/main/java/com/fdkk/component/redis/KeyValueRedisTemplate.java
  18. 55 0
      src/main/java/com/fdkk/component/redis/SignalRedisTemplate.java
  19. 62 0
      src/main/java/com/fdkk/config/RedisConfig.java
  20. 56 0
      src/main/java/com/fdkk/config/SwaggerConfig.java
  21. 89 0
      src/main/java/com/fdkk/config/SystemConfig.java
  22. 74 0
      src/main/java/com/fdkk/config/properties/ImProperties.java
  23. 33 0
      src/main/java/com/fdkk/constants/Constants.java
  24. 48 0
      src/main/java/com/fdkk/controller/admin/NavigationController.java
  25. 46 0
      src/main/java/com/fdkk/controller/admin/SessionController.java
  26. 69 0
      src/main/java/com/fdkk/controller/api/APNsController.java
  27. 83 0
      src/main/java/com/fdkk/controller/api/MessageController.java
  28. 254 0
      src/main/java/com/fdkk/entity/Session.java
  29. 55 0
      src/main/java/com/fdkk/repository/SessionRepository.java
  30. 105 0
      src/main/java/com/fdkk/server/coder/AppMessageDecoder.java
  31. 56 0
      src/main/java/com/fdkk/server/coder/AppMessageEncoder.java
  32. 44 0
      src/main/java/com/fdkk/server/coder/WebMessageDecoder.java
  33. 47 0
      src/main/java/com/fdkk/server/coder/WebMessageEncoder.java
  34. 42 0
      src/main/java/com/fdkk/server/constant/CIMConstant.java
  35. 13 0
      src/main/java/com/fdkk/server/constant/ChannelAttr.java
  36. 95 0
      src/main/java/com/fdkk/server/group/SessionGroup.java
  37. 13 0
      src/main/java/com/fdkk/server/group/TagSessionGroup.java
  38. 329 0
      src/main/java/com/fdkk/server/handler/IMNioSocketAcceptor.java
  39. 36 0
      src/main/java/com/fdkk/server/handler/IMRequestHandler.java
  40. 71 0
      src/main/java/com/fdkk/server/handler/LoggingHandler.java
  41. 228 0
      src/main/java/com/fdkk/server/model/Message.java
  42. 62 0
      src/main/java/com/fdkk/server/model/Ping.java
  43. 47 0
      src/main/java/com/fdkk/server/model/Pong.java
  44. 160 0
      src/main/java/com/fdkk/server/model/ReplyBody.java
  45. 107 0
      src/main/java/com/fdkk/server/model/SentBody.java
  46. 39 0
      src/main/java/com/fdkk/server/model/Transportable.java
  47. 15 0
      src/main/java/com/fdkk/server/model/proto/Message.proto
  48. 1643 0
      src/main/java/com/fdkk/server/model/proto/MessageProto.java
  49. 13 0
      src/main/java/com/fdkk/server/model/proto/ReplyBody.proto
  50. 1304 0
      src/main/java/com/fdkk/server/model/proto/ReplyBodyProto.java
  51. 10 0
      src/main/java/com/fdkk/server/model/proto/SentBody.proto
  52. 1008 0
      src/main/java/com/fdkk/server/model/proto/SentBodyProto.java
  53. 50 0
      src/main/java/com/fdkk/service/SessionService.java
  54. 89 0
      src/main/java/com/fdkk/service/impl/SessionServiceImpl.java
  55. 78 0
      src/main/java/com/fdkk/util/JSONUtils.java
  56. 65 0
      src/main/resources/application.properties
  57. 68 0
      src/main/resources/i18n/messages.properties
  58. 12 0
      src/main/resources/logback-spring.xml
  59. 44 0
      src/main/resources/page/console/header.html
  60. 25 0
      src/main/resources/page/console/index.html
  61. 22 0
      src/main/resources/page/console/nav.html
  62. 128 0
      src/main/resources/page/console/session/manage.html
  63. 184 0
      src/main/resources/page/console/webclient/index.html
  64. 384 0
      src/main/resources/page/ftl/spring.ftl
  65. 209 0
      src/main/resources/page/messsage/index.html
  66. 6 0
      src/main/resources/static/bootstrap-3.3.7-dist/css/bootstrap.min.css
  67. TEMPAT SAMPAH
      src/main/resources/static/bootstrap-3.3.7-dist/fonts/glyphicons-halflings-regular.eot
  68. 288 0
      src/main/resources/static/bootstrap-3.3.7-dist/fonts/glyphicons-halflings-regular.svg
  69. TEMPAT SAMPAH
      src/main/resources/static/bootstrap-3.3.7-dist/fonts/glyphicons-halflings-regular.ttf
  70. TEMPAT SAMPAH
      src/main/resources/static/bootstrap-3.3.7-dist/fonts/glyphicons-halflings-regular.woff
  71. TEMPAT SAMPAH
      src/main/resources/static/bootstrap-3.3.7-dist/fonts/glyphicons-halflings-regular.woff2
  72. 7 0
      src/main/resources/static/bootstrap-3.3.7-dist/js/bootstrap.min.js
  73. 13 0
      src/main/resources/static/bootstrap-3.3.7-dist/js/npm.js
  74. 1 0
      src/main/resources/static/css/common.css
  75. TEMPAT SAMPAH
      src/main/resources/static/image/favicon.ico
  76. TEMPAT SAMPAH
      src/main/resources/static/image/icon.png
  77. TEMPAT SAMPAH
      src/main/resources/static/image/icon/kankan_icon.ico
  78. 1 0
      src/main/resources/static/image/icon/online.svg
  79. TEMPAT SAMPAH
      src/main/resources/static/image/icon_loading_small.gif
  80. TEMPAT SAMPAH
      src/main/resources/static/image/log.png
  81. TEMPAT SAMPAH
      src/main/resources/static/image/pattern.png
  82. 7 0
      src/main/resources/static/js/bootstrap.min.js
  83. 1 0
      src/main/resources/static/js/common.js
  84. 230 0
      src/main/resources/static/js/im/cim.web.sdk.js
  85. 2774 0
      src/main/resources/static/js/im/message.js
  86. 2622 0
      src/main/resources/static/js/im/replybody.js
  87. 2568 0
      src/main/resources/static/js/im/sentbody.js
  88. 2 0
      src/main/resources/static/js/jquery-3.3.1.min.js
  89. 13 0
      src/main/resources/static/js/jquery-ui.min.js

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+target/
+.idea/

+ 1 - 0
jar.bat

@@ -0,0 +1 @@
+mvn clean package

+ 1 - 0
jar.sh

@@ -0,0 +1 @@
+mvn clean package

+ 154 - 0
pom.xml

@@ -0,0 +1,154 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.fdkk</groupId>
+    <artifactId>im-boot-server</artifactId>
+    <version>1.0.0</version>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.5.2</version>
+        <relativePath/>
+    </parent>
+
+    <properties>
+        <java.version>1.8</java.version>
+        <netty.version>4.1.65.Final</netty.version>
+        <protobuf.version>3.17.0</protobuf.version>
+        <mysql.jdbc.version>8.0.22</mysql.jdbc.version>
+        <common.pool.version>2.8.0</common.pool.version>
+        <swagger.version>3.0.0</swagger.version>
+    </properties>
+    <dependencies>
+
+
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-freemarker</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-boot-starter</artifactId>
+            <version>${swagger.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-pool2</artifactId>
+            <version>${common.pool.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <version>${mysql.jdbc.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-handler</artifactId>
+            <version>${netty.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-buffer</artifactId>
+            <version>${netty.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-codec</artifactId>
+            <version>${netty.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-codec-http</artifactId>
+            <version>${netty.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-common</artifactId>
+            <version>${netty.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-transport</artifactId>
+            <version>${netty.version}</version>
+        </dependency>
+        <!-- linux下有效. 其他linux平台自行修改对应的classifier -->
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-transport-native-epoll</artifactId>
+            <classifier>linux-x86_64</classifier>
+            <version>${netty.version}</version>
+        </dependency>
+        <!--- ##################使用netty本SDK时的配置  end ##################-->
+
+
+
+        <dependency>
+            <groupId>com.google.protobuf</groupId>
+            <artifactId>protobuf-java</artifactId>
+            <version>${protobuf.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.eatthepath</groupId>
+            <artifactId>pushy</artifactId>
+            <version>0.15.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.7</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+    </dependencies>
+
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <includeSystemScope>true</includeSystemScope>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 1 - 0
run.bat

@@ -0,0 +1 @@
+java -Dcom.sun.akuma.Daemon=daemonized -Dspring.profiles.active=pro -jar ./cim-boot-server-4.0.0.jar

+ 3 - 0
run.sh

@@ -0,0 +1,3 @@
+#! /bin/bash  
+
+java -Dcom.sun.akuma.Daemon=daemonized -Dspring.profiles.active=pro -jar ./cim-boot-server-4.0.0.jar &

+ 36 - 0
src/main/java/com/fdkk/BootApplication.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk;
+
+import com.fdkk.config.properties.ImProperties;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+
+@SpringBootApplication
+@EnableConfigurationProperties({
+		ImProperties.class})
+public class BootApplication {
+	public static void main(String[] args) {
+		SpringApplication.run(BootApplication.class, args);
+	}
+}

+ 97 - 0
src/main/java/com/fdkk/component/handler/BindHandler.java

@@ -0,0 +1,97 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.component.handler;
+
+import com.fdkk.component.handler.annotation.ImHandler;
+import com.fdkk.component.redis.SignalRedisTemplate;
+import com.fdkk.entity.Session;
+import com.fdkk.server.constant.ChannelAttr;
+import com.fdkk.server.group.SessionGroup;
+import com.fdkk.server.handler.IMRequestHandler;
+import com.fdkk.server.model.ReplyBody;
+import com.fdkk.server.model.SentBody;
+import com.fdkk.service.SessionService;
+import io.netty.channel.Channel;
+import org.springframework.http.HttpStatus;
+
+import javax.annotation.Resource;
+
+/**
+ * 客户长连接 账户绑定实现
+ */
+@ImHandler(key = "client_bind")
+public class BindHandler implements IMRequestHandler {
+
+	@Resource
+	private SessionService sessionService;
+
+	@Resource
+	private SessionGroup sessionGroup;
+
+	@Resource
+	private SignalRedisTemplate signalRedisTemplate;
+
+	@Override
+	public void process(Channel channel, SentBody body) {
+
+		ReplyBody reply = new ReplyBody();
+		reply.setKey(body.getKey());
+		reply.setCode(HttpStatus.OK.value());
+		reply.setTimestamp(System.currentTimeMillis());
+
+		String uid = body.get("uid");
+		Session session = new Session();
+		session.setUid(uid);
+		session.setNid(channel.attr(ChannelAttr.ID).get());
+		session.setDeviceId(body.get("deviceId"));
+		session.setChannel(body.get("channel"));
+		session.setDeviceName(body.get("deviceName"));
+		session.setAppVersion(body.get("appVersion"));
+		session.setOsVersion(body.get("osVersion"));
+		session.setLanguage(body.get("language"));
+
+		channel.attr(ChannelAttr.UID).set(uid);
+		channel.attr(ChannelAttr.CHANNEL).set(session.getChannel());
+		channel.attr(ChannelAttr.DEVICE_ID).set(session.getDeviceId());
+		channel.attr(ChannelAttr.LANGUAGE).set(session.getLanguage());
+
+		/*
+		 *存储到数据库
+		 */
+		sessionService.add(session);
+
+		/*
+		 * 添加到内存管理
+		 */
+		sessionGroup.add(channel);
+
+		/*
+		 *向客户端发送bind响应
+		 */
+		channel.writeAndFlush(reply);
+
+		/*
+		 * 发送上线事件到集群中的其他实例,控制其他设备下线
+		 */
+		signalRedisTemplate.bind(session);
+	}
+}

+ 50 - 0
src/main/java/com/fdkk/component/handler/BizHandler.java

@@ -0,0 +1,50 @@
+package com.fdkk.component.handler;
+
+import com.fdkk.component.handler.annotation.ImHandler;
+import com.fdkk.component.push.DefaultMessagePusher;
+import com.fdkk.entity.Session;
+import com.fdkk.server.handler.IMRequestHandler;
+import com.fdkk.server.model.Message;
+import com.fdkk.server.model.SentBody;
+import com.fdkk.service.SessionService;
+import io.netty.channel.Channel;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+/**
+ * @author Xiewj
+ * @date 2021/11/5
+ */
+@ImHandler(key = "client_biz")
+public class BizHandler implements IMRequestHandler {
+   @Resource
+   private SessionService sessionService;
+   @Resource
+   private DefaultMessagePusher defaultMessagePusher;
+
+   @Override
+   public void process(Channel channel, SentBody sentBody) {
+      List<Session> all = sessionService.findAll();
+      String content = sentBody.get("content");
+      String uid = sentBody.get("uid");
+      all.forEach(a->{
+         /*
+          *广播所有用户
+          */
+         if(a.getState()==0){
+         Message message = new Message();
+         message.setSender(uid);
+         message.setReceiver(a.getUid());
+         message.setAction("biz");
+         message.setContent(content);
+         message.setFormat("0");
+         message.setTitle("我是业务标题");
+         message.setExtra("我是业务扩展");
+         message.setId(System.currentTimeMillis());
+         defaultMessagePusher.push(message);
+         }
+      });
+      System.out.println("我是业务");
+   }
+}

+ 50 - 0
src/main/java/com/fdkk/component/handler/BizPrivateMsgHandler.java

@@ -0,0 +1,50 @@
+package com.fdkk.component.handler;
+
+import com.fdkk.component.handler.annotation.ImHandler;
+import com.fdkk.component.push.DefaultMessagePusher;
+import com.fdkk.entity.Session;
+import com.fdkk.server.handler.IMRequestHandler;
+import com.fdkk.server.model.Message;
+import com.fdkk.server.model.SentBody;
+import com.fdkk.service.SessionService;
+import io.netty.channel.Channel;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+/**
+ * @author Xiewj
+ * @date 2021/11/5
+ */
+@ImHandler(key = "client_private_biz")
+public class BizPrivateMsgHandler implements IMRequestHandler {
+   @Resource
+   private SessionService sessionService;
+   @Resource
+   private DefaultMessagePusher defaultMessagePusher;
+
+   @Override
+   public void process(Channel channel, SentBody sentBody) {
+      List<Session> all = sessionService.findAll();
+      String content = sentBody.get("content");
+      String uid = sentBody.get("uid");
+      all.forEach(a->{
+         /*
+          *广播所有用户
+          */
+         if(a.getState()==0){
+         Message message = new Message();
+         message.setSender(uid);
+         message.setReceiver(a.getUid());
+         message.setAction("biz");
+         message.setContent(content);
+         message.setFormat("0");
+         message.setTitle("我是业务标题");
+         message.setExtra("我是业务扩展");
+         message.setId(System.currentTimeMillis());
+         defaultMessagePusher.push(message);
+         }
+      });
+      System.out.println("我是业务");
+   }
+}

+ 72 - 0
src/main/java/com/fdkk/component/handler/ClosedHandler.java

@@ -0,0 +1,72 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.component.handler;
+
+import com.fdkk.component.handler.annotation.ImHandler;
+import com.fdkk.entity.Session;
+import com.fdkk.server.constant.ChannelAttr;
+import com.fdkk.server.group.SessionGroup;
+import com.fdkk.server.handler.IMRequestHandler;
+import com.fdkk.server.model.SentBody;
+import com.fdkk.service.SessionService;
+import io.netty.channel.Channel;
+
+import javax.annotation.Resource;
+import java.util.Objects;
+
+/**
+ * 连接断开时,更新用户相关状态
+ */
+@ImHandler(key = "client_closed")
+public class ClosedHandler implements IMRequestHandler {
+
+	@Resource
+	private SessionService sessionService;
+
+	@Resource
+	private SessionGroup sessionGroup;
+
+	@Override
+	public void process(Channel channel, SentBody message) {
+
+		String uid = channel.attr(ChannelAttr.UID).get();
+
+		if (uid == null){
+			return;
+		}
+
+		String nid = channel.attr(ChannelAttr.ID).get();
+
+		sessionGroup.remove(channel);
+
+		/*
+		 * ios开启了apns也需要显示在线,因此不删记录
+		 */
+		if (!Objects.equals(channel.attr(ChannelAttr.CHANNEL).get(), Session.CHANNEL_IOS)){
+			sessionService.delete(uid,nid);
+			return;
+		}
+
+		sessionService.updateState(uid,nid, Session.STATE_INACTIVE);
+	}
+
+}

+ 35 - 0
src/main/java/com/fdkk/component/handler/annotation/ImHandler.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.component.handler.annotation;
+
+import org.springframework.stereotype.Component;
+
+import java.lang.annotation.*;
+
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Component
+
+public @interface ImHandler {
+    String key();
+}

+ 78 - 0
src/main/java/com/fdkk/component/message/BindMessageListener.java

@@ -0,0 +1,78 @@
+package com.fdkk.component.message;
+
+import com.fdkk.entity.Session;
+import com.fdkk.server.constant.ChannelAttr;
+import com.fdkk.server.group.SessionGroup;
+import com.fdkk.server.model.Message;
+import com.fdkk.util.JSONUtils;
+import io.netty.channel.Channel;
+import org.springframework.data.redis.connection.MessageListener;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 集群环境下,监控多设备登录情况,控制是否其余终端下线的逻辑
+ */
+@Component
+public class BindMessageListener implements MessageListener {
+
+    private static final String FORCE_OFFLINE_ACTION = "999";
+
+    private static final String SYSTEM_ID = "0";
+
+    /*
+     一个账号只能在同一个类型的终端登录
+     如: 多个android或ios不能同时在线
+         一个android或ios可以和web,桌面同时在线
+     */
+    private final Map<String,String[]> conflictMap = new HashMap<>();
+
+    @Resource
+    private SessionGroup sessionGroup;
+
+    public BindMessageListener(){
+        conflictMap.put(Session.CHANNEL_ANDROID,new String[]{Session.CHANNEL_ANDROID,Session.CHANNEL_IOS});
+        conflictMap.put(Session.CHANNEL_IOS,new String[]{Session.CHANNEL_ANDROID,Session.CHANNEL_IOS});
+        conflictMap.put(Session.CHANNEL_WINDOWS,new String[]{Session.CHANNEL_WINDOWS,Session.CHANNEL_WEB,Session.CHANNEL_MAC});
+        conflictMap.put(Session.CHANNEL_WEB,new String[]{Session.CHANNEL_WINDOWS,Session.CHANNEL_WEB,Session.CHANNEL_MAC});
+        conflictMap.put(Session.CHANNEL_MAC,new String[]{Session.CHANNEL_WINDOWS,Session.CHANNEL_WEB,Session.CHANNEL_MAC});
+    }
+
+    @Override
+    public void onMessage(org.springframework.data.redis.connection.Message redisMessage, byte[] bytes) {
+
+        Session session = JSONUtils.fromJson(redisMessage.getBody(), Session.class);
+        String uid = session.getUid();
+        String[] conflictChannels = conflictMap.get(session.getChannel());
+
+        Collection<Channel> channelList = sessionGroup.find(uid,conflictChannels);
+
+        channelList.removeIf(channel -> session.getNid().equals(channel.attr(ChannelAttr.ID).get()));
+
+        /*
+         * 获取到其他在线的终端连接,提示账号再其他终端登录
+         */
+        channelList.forEach(channel -> {
+
+            if (Objects.equals(session.getDeviceId(),channel.attr(ChannelAttr.DEVICE_ID).get())){
+                channel.close();
+                return;
+            }
+
+            Message message = new Message();
+            message.setAction(FORCE_OFFLINE_ACTION);
+            message.setReceiver(uid);
+            message.setSender(SYSTEM_ID);
+            message.setContent(session.getDeviceName());
+            channel.writeAndFlush(message);
+            channel.close();
+        });
+
+
+    }
+}

+ 32 - 0
src/main/java/com/fdkk/component/message/PushMessageListener.java

@@ -0,0 +1,32 @@
+package com.fdkk.component.message;
+
+import com.fdkk.server.group.SessionGroup;
+import com.fdkk.server.model.Message;
+import com.fdkk.util.JSONUtils;
+import org.springframework.data.redis.connection.MessageListener;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+
+/**
+ * 集群环境下,监听redis队列,广播消息到每个实例进行推送
+ * 如果使用MQ的情况也,最好替换为MQ消息队列
+ */
+@Component
+public class PushMessageListener implements MessageListener {
+
+    @Resource
+    private SessionGroup sessionGroup;
+
+    @Override
+    public void onMessage(org.springframework.data.redis.connection.Message redisMessage, byte[] bytes) {
+
+        Message message = JSONUtils.fromJson(redisMessage.getBody(), Message.class);
+
+        String uid = message.getReceiver();
+
+        sessionGroup.write(uid,message);
+
+    }
+}

+ 72 - 0
src/main/java/com/fdkk/component/push/DefaultMessagePusher.java

@@ -0,0 +1,72 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.component.push;
+
+import com.fdkk.component.redis.KeyValueRedisTemplate;
+import com.fdkk.component.redis.SignalRedisTemplate;
+import com.fdkk.server.model.Message;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/*
+ * 消息发送实现类
+ *
+ */
+@Component
+public class DefaultMessagePusher implements MessagePusher {
+
+
+	@Resource
+	private SignalRedisTemplate signalRedisTemplate;
+
+	@Resource
+	private KeyValueRedisTemplate keyValueRedisTemplate;
+
+	/**
+	 * 向用户发送消息
+	 *
+	 * @param message
+	 */
+	@Override
+	public final void push(Message message) {
+
+		String uid = message.getReceiver();
+
+		/*
+		 * 通过发送redis广播,到集群中的每台实例,获得当前UID绑定了连接并推送
+		 * @see com.farsunset.hoxin.component.message.PushMessageListener
+		 */
+		signalRedisTemplate.push(message);
+
+	}
+	/*
+	 * 向群组发送消息
+	 * TODO后期可新增此业务
+	 * @param msg
+	 */
+	@Override
+	public void pushGroup(Message message) {
+		String Groupid = message.getReceiver();
+
+	}
+}

+ 47 - 0
src/main/java/com/fdkk/component/push/MessagePusher.java

@@ -0,0 +1,47 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.component.push;
+
+
+import com.fdkk.server.model.Message;
+
+/*
+ * 消息发送实接口
+ *
+ */
+public interface MessagePusher {
+
+	/*
+	 * 向用户发送消息
+	 *
+	 * @param msg
+	 */
+	void push(Message msg);
+
+
+	/*
+	 * 向群组发送消息
+	 *
+	 * @param msg
+	 */
+	void pushGroup(Message msg);
+}

+ 58 - 0
src/main/java/com/fdkk/component/redis/KeyValueRedisTemplate.java

@@ -0,0 +1,58 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.component.redis;
+
+import com.fdkk.constants.Constants;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+
+
+@Component
+public class KeyValueRedisTemplate extends StringRedisTemplate {
+
+	public KeyValueRedisTemplate(RedisConnectionFactory connectionFactory) {
+		super(connectionFactory);
+	}
+
+	public void set(String key ,String value) {
+		super.boundValueOps(key).set(value);
+	}
+
+	public String get(String key) {
+		return super.boundValueOps(key).get();
+	}
+
+	public String getDeviceToken(String uid){
+		return super.boundValueOps(String.format(Constants.APNS_DEVICE_TOKEN,uid)).get();
+	}
+
+	public void openApns(String uid,String deviceToken){
+		super.boundValueOps(String.format(Constants.APNS_DEVICE_TOKEN,uid)).set(deviceToken);
+	}
+
+	public void closeApns(String uid){
+		super.delete(String.format(Constants.APNS_DEVICE_TOKEN,uid));
+	}
+
+
+}

+ 55 - 0
src/main/java/com/fdkk/component/redis/SignalRedisTemplate.java

@@ -0,0 +1,55 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.component.redis;
+
+import com.fdkk.constants.Constants;
+import com.fdkk.entity.Session;
+import com.fdkk.server.model.Message;
+import com.fdkk.util.JSONUtils;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SignalRedisTemplate extends StringRedisTemplate {
+
+	public SignalRedisTemplate(LettuceConnectionFactory connectionFactory) {
+		super(connectionFactory);
+		connectionFactory.setValidateConnection(true);
+	}
+
+	/**
+	 * 消息发送到 集群中的每个实例,获取对应长连接进行消息写入
+	 * @param message
+	 */
+	public void push(Message message) {
+		super.convertAndSend(Constants.PUSH_MESSAGE_INNER_QUEUE, JSONUtils.toJSONString(message));
+	}
+
+	/**
+	 * 消息发送到 集群中的每个实例,解决多终端在线冲突问题
+	 * @param session
+	 */
+	public void bind(Session session) {
+		super.convertAndSend(Constants.BIND_MESSAGE_INNER_QUEUE, JSONUtils.toJSONString(session));
+	}
+}

+ 62 - 0
src/main/java/com/fdkk/config/RedisConfig.java

@@ -0,0 +1,62 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.config;
+
+import com.fdkk.constants.Constants;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.MessageListener;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.listener.ChannelTopic;
+import org.springframework.data.redis.listener.RedisMessageListenerContainer;
+
+import java.util.Objects;
+
+
+@Configuration
+@AutoConfigureBefore(RedisAutoConfiguration.class)
+public class RedisConfig {
+
+    @Autowired
+    public RedisConfig(LettuceConnectionFactory connectionFactory, @Value("${spring.profiles.active}") String profile){
+        if (Objects.equals("dev",profile)){
+            connectionFactory.setValidateConnection(true);
+        }
+    }
+
+    @Bean
+    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory,
+                                                                       MessageListener pushMessageListener,
+                                                                       MessageListener bindMessageListener){
+        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
+        container.setConnectionFactory(connectionFactory);
+        container.addMessageListener(pushMessageListener,new ChannelTopic(Constants.PUSH_MESSAGE_INNER_QUEUE));
+        container.addMessageListener(bindMessageListener,new ChannelTopic(Constants.BIND_MESSAGE_INNER_QUEUE));
+        return container;
+    }
+
+}

+ 56 - 0
src/main/java/com/fdkk/config/SwaggerConfig.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.oas.annotations.EnableOpenApi;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.plugins.Docket;
+
+@EnableOpenApi
+@Configuration
+public class SwaggerConfig {
+
+    @Bean
+    public Docket userApiDocket(ApiInfo apiInfo) {
+        return new Docket(DocumentationType.OAS_30)
+                .apiInfo(apiInfo)
+                .groupName("APP接口")
+                .select()
+                .apis(RequestHandlerSelectors.basePackage("com.farsunset.cim.mvc.controller.api"))
+                .build();
+    }
+
+    @Bean
+    public ApiInfo apiInfo() {
+        return new ApiInfoBuilder()
+                .title("CIM Push Service APIs.")
+                .description("CIM客户端接口文档")
+                .version("2.0")
+                .build();
+    }
+
+}

+ 89 - 0
src/main/java/com/fdkk/config/SystemConfig.java

@@ -0,0 +1,89 @@
+package com.fdkk.config;
+
+import com.fdkk.component.handler.annotation.ImHandler;
+import com.fdkk.config.properties.ImProperties;
+import com.fdkk.server.group.SessionGroup;
+import com.fdkk.server.group.TagSessionGroup;
+import com.fdkk.server.handler.IMNioSocketAcceptor;
+import com.fdkk.server.handler.IMRequestHandler;
+import com.fdkk.server.model.SentBody;
+import com.fdkk.service.SessionService;
+import io.netty.channel.Channel;
+import org.springframework.boot.context.event.ApplicationStartedEvent;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+public class SystemConfig implements IMRequestHandler, ApplicationListener<ApplicationStartedEvent> {
+
+	@Resource
+	private ApplicationContext applicationContext;
+
+	@Resource
+	private SessionService sessionService;
+
+	private final HashMap<String, IMRequestHandler> handlerMap = new HashMap<>();
+
+	@Bean
+	public SessionGroup sessionGroup() {
+		return new SessionGroup();
+	}
+
+	@Bean
+	public TagSessionGroup tagSessionGroup() {
+		return new TagSessionGroup();
+	}
+
+
+	@Bean(destroyMethod = "destroy")
+	public IMNioSocketAcceptor getNioSocketAcceptor(ImProperties properties) {
+
+		return new IMNioSocketAcceptor.Builder()
+				.setAppPort(properties.getAppPort())
+				.setWebsocketPort(properties.getWebsocketPort())
+				.setOuterRequestHandler(this)
+				.build();
+
+	}
+
+	@Override
+	public void process(Channel channel, SentBody body) {
+
+        IMRequestHandler handler = handlerMap.get(body.getKey());
+
+		if(handler == null) {return ;}
+
+		handler.process(channel, body);
+
+	}
+	/*
+	 * springboot启动完成之后再启动cim服务的,避免服务正在重启时,客户端会立即开始连接导致意外异常发生.
+	 */
+	@Override
+	public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) {
+
+		Map<String, IMRequestHandler> beans =  applicationContext.getBeansOfType(IMRequestHandler.class);
+
+		for (Map.Entry<String, IMRequestHandler> entry : beans.entrySet()) {
+
+			IMRequestHandler handler = entry.getValue();
+
+			ImHandler annotation = handler.getClass().getAnnotation(ImHandler.class);
+
+			if (annotation != null){
+				handlerMap.put(annotation.key(),handler);
+			}
+		}
+
+
+		applicationContext.getBean(IMNioSocketAcceptor.class).bind();
+
+		sessionService.deleteLocalhost();
+	}
+}

+ 74 - 0
src/main/java/com/fdkk/config/properties/ImProperties.java

@@ -0,0 +1,74 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.config.properties;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+
+@ConfigurationProperties(prefix = "im")
+public class ImProperties {
+
+    private final App app = new App();
+
+    private final Websocket websocket = new Websocket();
+
+    public App getApp() {
+        return app;
+    }
+
+    public Websocket getWebsocket() {
+        return websocket;
+    }
+
+    public static class App {
+
+        private Integer port;
+
+        public void setPort(Integer port) {
+            this.port = port;
+        }
+
+        public Integer getPort() {
+            return port;
+        }
+    }
+
+    public static class Websocket {
+        private Integer port;
+
+        public Integer getPort() {
+            return port;
+        }
+
+        public void setPort(Integer port) {
+            this.port = port;
+        }
+    }
+
+    public Integer getAppPort() {
+        return  app.port;
+    }
+
+    public Integer getWebsocketPort() {
+        return  websocket.port;
+    }
+}

+ 33 - 0
src/main/java/com/fdkk/constants/Constants.java

@@ -0,0 +1,33 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.constants;
+
+public interface Constants {
+
+	String PUSH_MESSAGE_INNER_QUEUE = "signal/channel/PUSH_MESSAGE_INNER_QUEUE";
+
+	String BIND_MESSAGE_INNER_QUEUE = "signal/channel/BIND_MESSAGE_INNER_QUEUE";
+
+	String PING_MESSAGE_INNER_QUEUE = "signal/channel/PING_MESSAGE_INNER_QUEUE";
+
+	String APNS_DEVICE_TOKEN = "APNS_OPEN_%s";
+}

+ 48 - 0
src/main/java/com/fdkk/controller/admin/NavigationController.java

@@ -0,0 +1,48 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.controller.admin;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.servlet.ModelAndView;
+
+@Controller
+public class NavigationController {
+
+	@GetMapping(value = "/")
+	public ModelAndView index(ModelAndView model) {
+		model.setViewName("console/index");
+		return model;
+	}
+
+	@GetMapping(value = "/webclient")
+	public ModelAndView webclient(ModelAndView model) {
+		model.setViewName("messsage/index");
+		return model;
+	}
+
+	@GetMapping(value = "/message")
+	public ModelAndView message(ModelAndView model) {
+		model.setViewName("messsage/index");
+		return model;
+	}
+}

+ 46 - 0
src/main/java/com/fdkk/controller/admin/SessionController.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.controller.admin;
+
+import com.fdkk.service.SessionService;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import javax.annotation.Resource;
+
+@Controller
+@RequestMapping("/console/session")
+public class SessionController {
+
+	@Resource
+	private SessionService sessionService;
+
+	@GetMapping(value = "/list")
+	public String list(Model model) {
+		model.addAttribute("sessionList", sessionService.findAll());
+		return "console/session/manage";
+
+	}
+
+}

+ 69 - 0
src/main/java/com/fdkk/controller/api/APNsController.java

@@ -0,0 +1,69 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.controller.api;
+
+import com.fdkk.service.SessionService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+@RestController
+@RequestMapping("/apns")
+@Api(produces = "application/json", tags = "APNs推送相关")
+public class APNsController {
+
+	@ApiOperation(httpMethod = "POST", value = "开启apns")
+
+
+	@ApiImplicitParams({
+			@ApiImplicitParam(name = "deviceToken", value = "APNs的deviceToken", paramType = "query", dataTypeClass = String.class, required = true, example = ""),
+			@ApiImplicitParam(name = "uid", value = "用户ID", paramType = "query", dataTypeClass = String.class,example = "0")
+	})
+	@PostMapping(value = "/open")
+	public ResponseEntity<Void> open(@RequestParam String uid , @RequestParam String deviceToken) {
+
+		sessionService.openApns(uid,deviceToken);
+
+		return ResponseEntity.ok().build();
+	}
+
+	@Resource
+	private SessionService sessionService;
+
+	@ApiOperation(httpMethod = "POST", value = "关闭apns")
+	@ApiImplicitParam(name = "uid", value = "用户ID", paramType = "query", dataTypeClass = String.class,example = "0")
+	@PostMapping(value = "/close")
+	public ResponseEntity<Void> close(@RequestParam String uid) {
+
+		sessionService.closeApns(uid);
+
+		return ResponseEntity.ok().build();
+	}
+}

+ 83 - 0
src/main/java/com/fdkk/controller/api/MessageController.java

@@ -0,0 +1,83 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.controller.api;
+
+import com.fdkk.component.push.DefaultMessagePusher;
+import com.fdkk.server.model.Message;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+
+@RestController
+@RequestMapping("/api/message")
+@Api(produces = "application/json", tags = "消息相关接口" )
+public class MessageController  {
+
+	@Resource
+	private DefaultMessagePusher defaultMessagePusher;
+
+	@ApiOperation(httpMethod = "POST", value = "发送消息")
+	@ApiImplicitParams({
+			@ApiImplicitParam(name = "sender", value = "发送者UID", paramType = "query", dataTypeClass = String.class, required = true, example = ""),
+			@ApiImplicitParam(name = "receiver", value = "接收者UID", paramType = "query", dataTypeClass = String.class, required = true, example = ""),
+			@ApiImplicitParam(name = "action", value = "消息动作", paramType = "query", dataTypeClass = String.class, required = true, example = ""),
+			@ApiImplicitParam(name = "title", value = "消息标题", paramType = "query", dataTypeClass = String.class, example = ""),
+			@ApiImplicitParam(name = "content", value = "消息内容", paramType = "query", dataTypeClass = String.class,  example = ""),
+			@ApiImplicitParam(name = "format", value = "消息格式", paramType = "query", dataTypeClass = String.class,  example = ""),
+			@ApiImplicitParam(name = "extra", value = "扩展字段", paramType = "query", dataTypeClass = String.class, example = ""),
+	})
+	@PostMapping(value = "/send")
+	public ResponseEntity<Long> send(@RequestParam String sender ,
+									 @RequestParam String receiver ,
+									 @RequestParam String action ,
+									 @RequestParam(required = false) String title ,
+									 @RequestParam(required = false) String content ,
+									 @RequestParam(required = false) String format ,
+									 @RequestParam(required = false) String extra)  {
+
+		Message message = new Message();
+		message.setSender(sender);
+		message.setReceiver(receiver);
+		message.setAction(action);
+		message.setContent(content);
+		message.setFormat(format);
+		message.setTitle(title);
+		message.setExtra(extra);
+
+		message.setId(System.currentTimeMillis());
+
+		defaultMessagePusher.push(message);
+
+		return ResponseEntity.ok(message.getId());
+	}
+
+
+}

+ 254 - 0
src/main/java/com/fdkk/entity/Session.java

@@ -0,0 +1,254 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.entity;
+
+
+import javax.persistence.*;
+import java.io.Serializable;
+
+@Entity
+@Table(name = "t_session")
+public class Session implements Serializable {
+
+    private static final transient long serialVersionUID = 1L;
+    public static final transient int STATE_ACTIVE = 0;
+    public static final transient int STATE_APNS = 1;
+    public static final transient int STATE_INACTIVE = 2;
+
+    public static final transient String CHANNEL_IOS = "ios";
+    public static final transient String CHANNEL_ANDROID = "android";
+    public static final transient String CHANNEL_WINDOWS = "windows";
+    public static final transient String CHANNEL_MAC = "mac";
+    public static final transient String CHANNEL_WEB = "web";
+
+    /**
+     * 数据库主键ID
+     */
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id")
+    private Long id;
+
+    /**
+     * session绑定的用户账号
+     */
+    @Column(name = "uid")
+    private String uid;
+
+    /**
+     * session在本台服务器上的ID
+     */
+    @Column(name = "nid",length = 32,nullable = false)
+    private String nid;
+
+    /**
+     * 客户端ID (设备号码+应用包名),ios为deviceToken
+     */
+
+    @Column(name = "device_id",length = 64,nullable = false)
+    private String deviceId;
+
+    /**
+     * 终端设备型号
+     */
+    @Column(name = "device_name")
+    private String deviceName;
+
+    /**
+     * session绑定的服务器IP
+     */
+    @Column(name = "host",length = 15,nullable = false)
+    private String host;
+
+    /**
+     * 终端设备类型
+     */
+    @Column(name = "channel",length = 10,nullable = false)
+    private String channel;
+
+    /**
+     * 终端应用版本
+     */
+    @Column(name = "app_version")
+    private String appVersion;
+
+    /**
+     * 终端系统版本
+     */
+    @Column(name = "os_version")
+    private String osVersion;
+
+    /**
+     * 终端语言
+     */
+    @Column(name = "language")
+    private String language;
+
+    /**
+     * 登录时间
+     */
+    @Column(name = "bind_time")
+    private Long bindTime;
+
+    /**
+     * 经度
+     */
+    @Column(name = "longitude")
+    private Double longitude;
+
+    /**
+     * 维度
+     */
+    @Column(name = "latitude")
+    private Double latitude;
+
+    /**
+     * 位置
+     */
+    @Column(name = "location")
+    private String location;
+
+    /**
+     * 状态
+     */
+    private int state;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getUid() {
+        return uid;
+    }
+
+    public void setUid(String uid) {
+        this.uid = uid;
+    }
+
+    public String getNid() {
+        return nid;
+    }
+
+    public void setNid(String nid) {
+        this.nid = nid;
+    }
+
+    public String getDeviceId() {
+        return deviceId;
+    }
+
+    public void setDeviceId(String deviceId) {
+        this.deviceId = deviceId;
+    }
+
+    public String getHost() {
+        return host;
+    }
+
+    public void setHost(String host) {
+        this.host = host;
+    }
+
+    public String getChannel() {
+        return channel;
+    }
+
+    public void setChannel(String channel) {
+        this.channel = channel;
+    }
+
+    public String getDeviceName() {
+        return deviceName;
+    }
+
+    public void setDeviceName(String deviceName) {
+        this.deviceName = deviceName;
+    }
+
+    public String getAppVersion() {
+        return appVersion;
+    }
+
+    public void setAppVersion(String appVersion) {
+        this.appVersion = appVersion;
+    }
+
+    public String getOsVersion() {
+        return osVersion;
+    }
+
+    public void setOsVersion(String osVersion) {
+        this.osVersion = osVersion;
+    }
+
+    public Long getBindTime() {
+        return bindTime;
+    }
+
+    public void setBindTime(Long bindTime) {
+        this.bindTime = bindTime;
+    }
+
+    public Double getLongitude() {
+        return longitude;
+    }
+
+    public void setLongitude(Double longitude) {
+        this.longitude = longitude;
+    }
+
+    public Double getLatitude() {
+        return latitude;
+    }
+
+    public void setLatitude(Double latitude) {
+        this.latitude = latitude;
+    }
+
+    public String getLocation() {
+        return location;
+    }
+
+    public void setLocation(String location) {
+        this.location = location;
+    }
+
+    public String getLanguage() {
+        return language;
+    }
+
+    public void setLanguage(String language) {
+        this.language = language;
+    }
+
+    public int getState() {
+        return state;
+    }
+
+    public void setState(int state) {
+        this.state = state;
+    }
+}

+ 55 - 0
src/main/java/com/fdkk/repository/SessionRepository.java

@@ -0,0 +1,55 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.repository;
+
+import com.fdkk.entity.Session;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+
+@Repository
+@Transactional(rollbackFor = Exception.class)
+public interface SessionRepository extends JpaRepository<Session, Long> {
+
+	@Modifying
+	@Query("delete from Session where uid = ?1 and nid = ?2")
+	void delete(String uid,String nid);
+
+	@Modifying
+	@Query("delete from Session where host = ?1 ")
+	void deleteAll(String host);
+
+	@Modifying
+	@Query("update Session set state = ?3 where uid = ?1 and nid = ?2")
+	void updateState(String uid,String nid,int state);
+
+	@Modifying
+	@Query("update Session set state = " + Session.STATE_APNS + " where uid = ?1 and channel = ?2")
+	void openApns(String uid,String channel);
+
+	@Modifying
+	@Query("update Session set state = " + Session.STATE_ACTIVE + " where uid = ?1 and channel = ?2")
+	void closeApns(String uid,String channel);
+}

+ 105 - 0
src/main/java/com/fdkk/server/coder/AppMessageDecoder.java

@@ -0,0 +1,105 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.server.coder;
+
+import com.fdkk.server.constant.CIMConstant;
+import com.fdkk.server.constant.ChannelAttr;
+import com.fdkk.server.model.Pong;
+import com.fdkk.server.model.SentBody;
+import com.fdkk.server.model.proto.SentBodyProto;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.ByteToMessageDecoder;
+
+import java.util.List;
+
+/**
+ * 服务端接收消息路由解码,通过消息类型分发到不同的真正解码器
+ */
+public class AppMessageDecoder extends ByteToMessageDecoder {
+
+	@Override
+	protected void decode(ChannelHandlerContext context, ByteBuf buffer, List<Object> queue) throws Exception {
+
+		context.channel().attr(ChannelAttr.PING_COUNT).set(null);
+
+		/*
+		 * 消息体不足3位,发生断包情况
+		 */
+		if (buffer.readableBytes() < CIMConstant.DATA_HEADER_LENGTH) {
+			return;
+		}
+
+		buffer.markReaderIndex();
+
+		byte type = buffer.readByte();
+
+		byte lv = buffer.readByte();
+		byte hv = buffer.readByte();
+		int length = getContentLength(lv, hv);
+
+		/*
+		 * 发生断包情况,等待接收完成
+		 */
+		if (buffer.readableBytes() < length) {
+			buffer.resetReaderIndex();
+			return;
+		}
+
+		byte[] dataBytes = new byte[length];
+		buffer.readBytes(dataBytes);
+
+
+		Object message = mappingMessageObject(dataBytes, type);
+
+		queue.add(message);
+	}
+
+	public Object mappingMessageObject(byte[] data, byte type) throws Exception {
+
+		if (CIMConstant.DATA_TYPE_PONG == type) {
+			return Pong.getInstance();
+		}
+
+		SentBodyProto.Model bodyProto = SentBodyProto.Model.parseFrom(data);
+		SentBody body = new SentBody();
+		body.setKey(bodyProto.getKey());
+		body.setTimestamp(bodyProto.getTimestamp());
+		body.putAll(bodyProto.getDataMap());
+
+		return body;
+	}
+
+	/**
+	 * 解析消息体长度
+	 * 最大消息长度为2个字节表示的长度,即为65535
+	 * @param lv 低位1字节消息长度
+	 * @param hv 高位1字节消息长度
+	 * @return 消息的真实长度
+	 */
+	private int getContentLength(byte lv, byte hv) {
+		int l = (lv & 0xff);
+		int h = (hv & 0xff);
+		return l | h << 8;
+	}
+
+}

+ 56 - 0
src/main/java/com/fdkk/server/coder/AppMessageEncoder.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.server.coder;
+
+import com.fdkk.server.constant.CIMConstant;
+import com.fdkk.server.model.Transportable;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.MessageToByteEncoder;
+
+/**
+ * 服务端发送消息前编码
+ */
+public class AppMessageEncoder extends MessageToByteEncoder<Transportable> {
+
+	@Override
+	protected void encode(final ChannelHandlerContext ctx, final Transportable data, ByteBuf out){
+		byte[] body = data.getBody();
+		byte[] header = createHeader(data.getType(), body.length);
+		out.writeBytes(header);
+		out.writeBytes(body);
+	}
+
+
+	/**
+	 * 创建消息头,结构为 TLV格式(Tag,Length,Value)
+	 * 第一字节为消息类型
+	 * 第二,三字节为消息长度分隔为高低位2个字节
+	 */
+	private byte[] createHeader(byte type, int length) {
+		byte[] header = new byte[CIMConstant.DATA_HEADER_LENGTH];
+		header[0] = type;
+		header[1] = (byte) (length & 0xff);
+		header[2] = (byte) ((length >> 8) & 0xff);
+		return header;
+	}
+}

+ 44 - 0
src/main/java/com/fdkk/server/coder/WebMessageDecoder.java

@@ -0,0 +1,44 @@
+package com.fdkk.server.coder;
+
+import com.fdkk.server.constant.CIMConstant;
+import com.fdkk.server.constant.ChannelAttr;
+import com.fdkk.server.model.Pong;
+import com.fdkk.server.model.SentBody;
+import com.fdkk.server.model.proto.SentBodyProto;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufInputStream;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.MessageToMessageDecoder;
+import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
+
+import java.io.InputStream;
+import java.util.List;
+
+public class WebMessageDecoder extends MessageToMessageDecoder<BinaryWebSocketFrame> {
+
+    @Override
+    protected void decode(ChannelHandlerContext context, BinaryWebSocketFrame frame, List<Object> list) throws Exception {
+
+        context.channel().attr(ChannelAttr.PING_COUNT).set(null);
+
+        ByteBuf buffer = frame.content();
+
+        byte type = buffer.readByte();
+
+        if (CIMConstant.DATA_TYPE_PONG == type) {
+            list.add(Pong.getInstance());
+            return;
+        }
+
+        InputStream inputStream = new ByteBufInputStream(buffer);
+
+        SentBodyProto.Model bodyProto = SentBodyProto.Model.parseFrom(inputStream);
+
+        SentBody body = new SentBody();
+        body.setKey(bodyProto.getKey());
+        body.setTimestamp(bodyProto.getTimestamp());
+        body.putAll(bodyProto.getDataMap());
+
+        list.add(body);
+    }
+}

+ 47 - 0
src/main/java/com/fdkk/server/coder/WebMessageEncoder.java

@@ -0,0 +1,47 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.server.coder;
+
+import com.fdkk.server.model.Transportable;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.MessageToMessageEncoder;
+import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
+
+import java.util.List;
+
+/**
+ * websocket发送消息前编码
+ */
+public class WebMessageEncoder extends MessageToMessageEncoder<Transportable> {
+
+	@Override
+	protected void encode(ChannelHandlerContext ctx, Transportable data, List<Object> out){
+		byte[] body = data.getBody();
+		ByteBufAllocator allocator = ctx.channel().config().getAllocator();
+		ByteBuf buffer = allocator.buffer(body.length + 1);
+		buffer.writeByte(data.getType());
+		buffer.writeBytes(body);
+		out.add(new BinaryWebSocketFrame(buffer));
+	}
+}

+ 42 - 0
src/main/java/com/fdkk/server/constant/CIMConstant.java

@@ -0,0 +1,42 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.server.constant;
+
+/**
+ * 常量
+ */
+public interface CIMConstant {
+
+	/**
+	 消息头长度为3个字节,第一个字节为消息类型,第二,第三字节 转换int后为消息长度
+	 */
+	byte DATA_HEADER_LENGTH = 3;
+
+	byte DATA_TYPE_PONG  = 0;
+	byte DATA_TYPE_PING  = 1;
+	byte DATA_TYPE_MESSAGE = 2;
+	byte DATA_TYPE_SENT = 3;
+	byte DATA_TYPE_REPLY = 4;
+
+	String CLIENT_CONNECT_CLOSED = "client_closed";
+
+}

+ 13 - 0
src/main/java/com/fdkk/server/constant/ChannelAttr.java

@@ -0,0 +1,13 @@
+package com.fdkk.server.constant;
+
+import io.netty.util.AttributeKey;
+
+public interface ChannelAttr {
+    AttributeKey<Integer> PING_COUNT = AttributeKey.valueOf("ping_count");
+    AttributeKey<String> UID = AttributeKey.valueOf("uid");
+    AttributeKey<String> CHANNEL = AttributeKey.valueOf("channel");
+    AttributeKey<String> ID = AttributeKey.valueOf("id");
+    AttributeKey<String> DEVICE_ID = AttributeKey.valueOf("device_id");
+    AttributeKey<String> TAG = AttributeKey.valueOf("tag");
+    AttributeKey<String> LANGUAGE = AttributeKey.valueOf("language");
+}

+ 95 - 0
src/main/java/com/fdkk/server/group/SessionGroup.java

@@ -0,0 +1,95 @@
+package com.fdkk.server.group;
+
+
+import com.fdkk.server.constant.ChannelAttr;
+import com.fdkk.server.model.Message;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+public class SessionGroup extends ConcurrentHashMap<String, Collection<Channel>> {
+
+    private static final Collection<Channel> EMPTY_LIST = new LinkedList<>();
+
+    private final transient ChannelFutureListener remover = new ChannelFutureListener() {
+        @Override
+        public void operationComplete(ChannelFuture future){
+            future.removeListener(this);
+            remove(future.channel());
+        }
+    };
+
+    protected String getKey(Channel channel){
+        return channel.attr(ChannelAttr.UID).get();
+    }
+
+    public void remove(Channel channel){
+
+        String uid = getKey(channel);
+
+        if(uid == null){
+            return;
+        }
+
+        Collection<Channel> collections = getOrDefault(uid,EMPTY_LIST);
+
+        collections.remove(channel);
+
+        if (collections.isEmpty()){
+            remove(uid);
+        }
+    }
+
+
+    public void add(Channel channel){
+
+        String uid = getKey(channel);
+
+        if (uid == null || !channel.isActive()){
+            return;
+        }
+
+        channel.closeFuture().addListener(remover);
+
+        Collection<Channel> collections = this.putIfAbsent(uid,new ConcurrentLinkedQueue<>(Collections.singleton(channel)));
+        if (collections != null){
+            collections.add(channel);
+        }
+
+        if (!channel.isActive()){
+            remove(channel);
+        }
+    }
+
+
+    public void write(String key, Message message){
+        find(key).forEach(channel -> channel.writeAndFlush(message));
+    }
+
+    public void write(String key, Message message, Predicate<Channel> matcher){
+        find(key).stream().filter(matcher).forEach(channel -> channel.writeAndFlush(message));
+    }
+
+    public void write(String key, Message message, Collection<String> excludedSet){
+        find(key).stream().filter(channel -> excludedSet == null || !excludedSet.contains(channel.attr(ChannelAttr.UID).get())).forEach(channel -> channel.writeAndFlush(message));
+    }
+
+    public void write(Message message){
+        this.write(message.getReceiver(),message);
+    }
+
+    public Collection<Channel> find(String key){
+        return this.getOrDefault(key,EMPTY_LIST);
+    }
+
+    public Collection<Channel> find(String key,String... channel){
+        List<String> channels = Arrays.asList(channel);
+        return find(key).stream().filter(item -> channels.contains(item.attr(ChannelAttr.CHANNEL).get())).collect(Collectors.toList());
+    }
+}

+ 13 - 0
src/main/java/com/fdkk/server/group/TagSessionGroup.java

@@ -0,0 +1,13 @@
+package com.fdkk.server.group;
+
+
+import com.fdkk.server.constant.ChannelAttr;
+import io.netty.channel.Channel;
+
+public class TagSessionGroup extends SessionGroup {
+
+    @Override
+    protected String getKey(Channel channel){
+        return channel.attr(ChannelAttr.TAG).get();
+    }
+}

+ 329 - 0
src/main/java/com/fdkk/server/handler/IMNioSocketAcceptor.java

@@ -0,0 +1,329 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.server.handler;
+
+import com.fdkk.server.coder.AppMessageDecoder;
+import com.fdkk.server.coder.AppMessageEncoder;
+import com.fdkk.server.coder.WebMessageDecoder;
+import com.fdkk.server.coder.WebMessageEncoder;
+import com.fdkk.server.constant.CIMConstant;
+import com.fdkk.server.constant.ChannelAttr;
+import com.fdkk.server.model.Ping;
+import com.fdkk.server.model.SentBody;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.*;
+import io.netty.channel.ChannelHandler.Sharable;
+import io.netty.channel.epoll.EpollEventLoopGroup;
+import io.netty.channel.epoll.EpollServerSocketChannel;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import io.netty.handler.stream.ChunkedWriteHandler;
+import io.netty.handler.timeout.IdleState;
+import io.netty.handler.timeout.IdleStateEvent;
+import io.netty.handler.timeout.IdleStateHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Duration;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+@Sharable
+public class IMNioSocketAcceptor extends SimpleChannelInboundHandler<SentBody>{
+	private static final Logger LOGGER = LoggerFactory.getLogger(IMNioSocketAcceptor.class);
+
+    private static final int PONG_TIME_OUT_COUNT = 3;
+	private final ThreadFactory bossThreadFactory;
+	private final ThreadFactory workerThreadFactory;
+
+	private EventLoopGroup appBossGroup;
+	private EventLoopGroup appWorkerGroup;
+
+	private EventLoopGroup webBossGroup;
+	private EventLoopGroup webWorkerGroup;
+
+	private final Integer appPort;
+	private final Integer webPort;
+	private final IMRequestHandler outerRequestHandler;
+
+	private final ChannelHandler loggingHandler = new LoggingHandler();
+
+	/**
+	 *  读空闲时间(秒)
+	 */
+	public final Duration writeIdle = Duration.ofSeconds(45);
+
+	/**
+	 *  写接空闲时间(秒)
+	 */
+	public final Duration readIdle = Duration.ofSeconds(60);
+
+	public IMNioSocketAcceptor(Builder builder){
+		this.webPort = builder.webPort;
+		this.appPort = builder.appPort;
+		this.outerRequestHandler = builder.outerRequestHandler;
+
+		bossThreadFactory = r -> {
+			Thread thread = new Thread(r);
+			thread.setName("nio-boss-");
+			return thread;
+		};
+		workerThreadFactory = r -> {
+			Thread thread = new Thread(r);
+			thread.setName("nio-worker-");
+			return thread;
+		};
+
+	}
+
+	private void createWebEventGroup(){
+		if (isLinuxSystem()){
+			webBossGroup = new EpollEventLoopGroup(bossThreadFactory);
+			webWorkerGroup = new EpollEventLoopGroup(workerThreadFactory);
+		}else {
+			webBossGroup = new NioEventLoopGroup(bossThreadFactory);
+			webWorkerGroup = new NioEventLoopGroup(workerThreadFactory);
+		}
+	}
+
+	private void createAppEventGroup(){
+		if (isLinuxSystem()){
+			appBossGroup = new EpollEventLoopGroup(bossThreadFactory);
+			appWorkerGroup = new EpollEventLoopGroup(workerThreadFactory);
+		}else {
+			appBossGroup = new NioEventLoopGroup(bossThreadFactory);
+			appWorkerGroup = new NioEventLoopGroup(workerThreadFactory);
+		}
+	}
+
+	public void bind() {
+
+		if (appPort != null){
+			bindAppPort();
+		}
+
+		if (webPort != null){
+			bindWebPort();
+		}
+	}
+
+	public void destroy(EventLoopGroup bossGroup , EventLoopGroup workerGroup) {
+		if(bossGroup != null && !bossGroup.isShuttingDown() && !bossGroup.isShutdown() ) {
+			try {bossGroup.shutdownGracefully();}catch(Exception ignore) {}
+		}
+
+		if(workerGroup != null && !workerGroup.isShuttingDown() && !workerGroup.isShutdown() ) {
+			try {workerGroup.shutdownGracefully();}catch(Exception ignore) {}
+		}
+	}
+
+    public void destroy() {
+    	this.destroy(appBossGroup,appWorkerGroup);
+		this.destroy(webBossGroup,webWorkerGroup);
+	}
+
+	private void bindAppPort(){
+		createAppEventGroup();
+		ServerBootstrap bootstrap = createServerBootstrap(appBossGroup,appWorkerGroup);
+		bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
+			@Override
+			public void initChannel(SocketChannel ch){
+				ch.pipeline().addLast(new AppMessageDecoder());
+				ch.pipeline().addLast(new AppMessageEncoder());
+				ch.pipeline().addLast(loggingHandler);
+				ch.pipeline().addLast(new IdleStateHandler(readIdle.getSeconds(), writeIdle.getSeconds(), 0, TimeUnit.SECONDS));
+				ch.pipeline().addLast(IMNioSocketAcceptor.this);
+			}
+		});
+
+		ChannelFuture channelFuture = bootstrap.bind(appPort).syncUninterruptibly();
+		channelFuture.channel().newSucceededFuture().addListener(future -> {
+			String logBanner = "\n\n" +
+					"* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n" +
+					"*                                                                                   *\n" +
+					"*                                                                                   *\n" +
+					"*                   App Socket Server started on port {}.                        *\n" +
+					"*                                                                                   *\n" +
+					"*                                                                                   *\n" +
+					"* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n";
+			LOGGER.info(logBanner, appPort);
+		});
+		channelFuture.channel().closeFuture().addListener(future -> this.destroy(appBossGroup,appWorkerGroup));
+	}
+
+	private void bindWebPort(){
+		createWebEventGroup();
+		ServerBootstrap bootstrap = createServerBootstrap(webBossGroup,webWorkerGroup);
+		bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
+
+			@Override
+			public void initChannel(SocketChannel ch){
+				ch.pipeline().addLast(new HttpServerCodec());
+				ch.pipeline().addLast(new ChunkedWriteHandler());
+				ch.pipeline().addLast(new HttpObjectAggregator(65536));
+				ch.pipeline().addLast(new WebSocketServerProtocolHandler("/",false));
+				ch.pipeline().addLast(new WebMessageDecoder());
+				ch.pipeline().addLast(new WebMessageEncoder());
+				ch.pipeline().addLast(loggingHandler);
+				ch.pipeline().addLast(new IdleStateHandler(readIdle.getSeconds(), writeIdle.getSeconds(), 0, TimeUnit.SECONDS));
+				ch.pipeline().addLast(IMNioSocketAcceptor.this);
+			}
+
+		});
+
+		ChannelFuture channelFuture = bootstrap.bind(webPort).syncUninterruptibly();
+		channelFuture.channel().newSucceededFuture().addListener(future -> {
+			String logBanner = "\n\n" +
+					"* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n" +
+					"*                                                                                   *\n" +
+					"*                                                                                   *\n" +
+					"*                   Websocket Server started on port {}.                         *\n" +
+					"*                                                                                   *\n" +
+					"*                                                                                   *\n" +
+					"* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n";
+			LOGGER.info(logBanner, webPort);
+		});
+		channelFuture.channel().closeFuture().addListener(future -> this.destroy(webBossGroup,webWorkerGroup));
+	}
+
+	private ServerBootstrap createServerBootstrap(EventLoopGroup bossGroup,EventLoopGroup workerGroup){
+		ServerBootstrap bootstrap = new ServerBootstrap();
+		bootstrap.group(bossGroup, workerGroup);
+		bootstrap.childOption(ChannelOption.TCP_NODELAY, true);
+		bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
+		bootstrap.channel(isLinuxSystem() ? EpollServerSocketChannel.class : NioServerSocketChannel.class);
+		return bootstrap;
+	}
+
+	@Override
+	protected void channelRead0(ChannelHandlerContext ctx, SentBody body) {
+		/*
+		 * 有业务层去处理其他的sentBody
+		 */
+		outerRequestHandler.process(ctx.channel(), body);
+	}
+
+	@Override
+	public void channelActive(ChannelHandlerContext ctx) {
+		ctx.channel().attr(ChannelAttr.ID).set(ctx.channel().id().asShortText());
+	}
+
+	@Override
+	public void channelInactive(ChannelHandlerContext ctx) {
+
+		if (ctx.channel().attr(ChannelAttr.UID) == null){
+			return;
+		}
+
+		SentBody body = new SentBody();
+		body.setKey(CIMConstant.CLIENT_CONNECT_CLOSED);
+		outerRequestHandler.process(ctx.channel(), body);
+	}
+
+	@Override
+	public void userEventTriggered(ChannelHandlerContext ctx, Object evt){
+
+		if (! (evt instanceof IdleStateEvent)){
+			return;
+		}
+
+		IdleStateEvent idleEvent = (IdleStateEvent) evt;
+
+		String uid = ctx.channel().attr(ChannelAttr.UID).get();
+
+		/*
+		 * 关闭未认证的连接
+		 */
+		if (idleEvent.state() == IdleState.WRITER_IDLE && uid == null) {
+			ctx.close();
+			return;
+		}
+
+		/*
+		 * 已经认证的连接发送心跳请求
+		 */
+		if (idleEvent.state() == IdleState.WRITER_IDLE && uid != null) {
+
+			Integer pingCount = ctx.channel().attr(ChannelAttr.PING_COUNT).get();
+			ctx.channel().attr(ChannelAttr.PING_COUNT).set(pingCount == null ? 1 : pingCount + 1);
+
+			ctx.channel().writeAndFlush(Ping.getInstance());
+
+			return;
+		}
+
+		/*
+		 * 如果心跳请求发出30秒内没收到响应,则关闭连接
+		 */
+		Integer pingCount = ctx.channel().attr(ChannelAttr.PING_COUNT).get();
+		if (idleEvent.state() == IdleState.READER_IDLE && pingCount != null && pingCount >= PONG_TIME_OUT_COUNT) {
+			ctx.close();
+			LOGGER.info("{} pong timeout.",ctx.channel());
+		}
+	}
+
+	@Override
+	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+		LOGGER.warn("EXCEPTION",cause);
+	}
+
+
+	public static class Builder{
+
+		private Integer appPort;
+		private Integer webPort;
+		private IMRequestHandler outerRequestHandler;
+
+		public Builder setAppPort(Integer appPort) {
+			this.appPort = appPort;
+			return this;
+		}
+
+		public Builder setWebsocketPort(Integer port) {
+			this.webPort = port;
+			return this;
+		}
+
+		/**
+		 * 设置应用层的sentBody处理handler
+		 */
+		public Builder setOuterRequestHandler(IMRequestHandler outerRequestHandler) {
+			this.outerRequestHandler = outerRequestHandler;
+			return this;
+		}
+
+		public IMNioSocketAcceptor build(){
+			return new IMNioSocketAcceptor(this);
+		}
+
+	}
+
+	private boolean isLinuxSystem(){
+		String osName = System.getProperty("os.name").toLowerCase();
+		return osName.contains("linux");
+	}
+
+}

+ 36 - 0
src/main/java/com/fdkk/server/handler/IMRequestHandler.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.server.handler;
+
+/**
+ *  请求处理接口,所有的请求实现必须实现此接口
+ */
+import com.fdkk.server.model.SentBody;
+import io.netty.channel.Channel;
+
+public interface IMRequestHandler {
+
+	/**
+	 * 处理收到客户端从长链接发送的数据
+	 */
+	void process(Channel channel, SentBody body);
+}

+ 71 - 0
src/main/java/com/fdkk/server/handler/LoggingHandler.java

@@ -0,0 +1,71 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.server.handler;
+
+
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelPromise;
+import io.netty.handler.logging.LogLevel;
+
+@ChannelHandler.Sharable
+public class LoggingHandler extends io.netty.handler.logging.LoggingHandler {
+
+	public LoggingHandler() {
+		super(LogLevel.INFO);
+	}
+
+	@Override
+	public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
+		ctx.fireChannelRegistered();
+	}
+
+	@Override
+	public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
+		ctx.fireChannelUnregistered();
+	}
+
+	@Override
+	public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
+		ctx.deregister(promise);
+	}
+
+	@Override
+	public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
+		ctx.fireChannelReadComplete();
+		ctx.flush();
+	}
+
+	@Override
+	public void flush(ChannelHandlerContext ctx) throws Exception {
+		ctx.flush();
+	}
+	/**
+	 * exceptionCaught exception 异常 Caught 抓住
+	 * 抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接
+	 */
+	@Override
+	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+		//super.exceptionCaught(ctx, cause);
+		ctx.close();
+	}
+}

+ 228 - 0
src/main/java/com/fdkk/server/model/Message.java

@@ -0,0 +1,228 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.server.model;
+
+
+import com.fdkk.server.constant.CIMConstant;
+import com.fdkk.server.model.proto.MessageProto;
+
+import java.io.Serializable;
+
+/**
+ * 消息对象
+ */
+public class Message implements Serializable, Transportable,Cloneable {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 消息类型,用户自定义消息类别
+	 */
+	private long id;
+
+	/**
+	 * 消息类型,用户自定义消息类别
+	 */
+	private String action;
+	/**
+	 * 消息标题
+	 */
+	private String title;
+	/**
+	 * 消息类容,于action 组合为任何类型消息,content 根据 format 可表示为 text,json ,xml数据格式
+	 */
+	private String content;
+
+	/**
+	 * 消息发送者账号
+	 */
+	private String sender;
+	/**
+	 * 消息发送者接收者
+	 */
+	private String receiver;
+
+	/**
+	 * content 内容格式
+	 */
+	private String format;
+
+	/**
+	 * 附加内容 内容
+	 */
+	private String extra;
+
+	private long timestamp;
+
+	public Message() {
+		timestamp = System.currentTimeMillis();
+	}
+
+
+	public long getId() {
+		return id;
+	}
+
+
+	public void setId(long id) {
+		this.id = id;
+	}
+
+
+	public long getTimestamp() {
+		return timestamp;
+	}
+
+	public void setTimestamp(long timestamp) {
+		this.timestamp = timestamp;
+	}
+
+	public String getAction() {
+		return action;
+	}
+
+	public void setAction(String action) {
+		this.action = action;
+	}
+
+	public String getTitle() {
+		return title;
+	}
+
+	public void setTitle(String title) {
+		this.title = title;
+	}
+
+	public String getContent() {
+		return content;
+	}
+
+	public void setContent(String content) {
+		this.content = content;
+	}
+
+	public String getSender() {
+		return sender;
+	}
+
+	public void setSender(String sender) {
+		this.sender = sender;
+	}
+
+	public String getReceiver() {
+		return receiver;
+	}
+
+	public void setReceiver(String receiver) {
+		this.receiver = receiver;
+	}
+
+	public String getFormat() {
+		return format;
+	}
+
+	public void setFormat(String format) {
+		this.format = format;
+	}
+
+	public String getExtra() {
+		return extra;
+	}
+
+	public void setExtra(String extra) {
+		this.extra = extra;
+	}
+
+	@Override
+	public String toString() {
+		StringBuffer buffer = new StringBuffer();
+		buffer.append("#Message#").append("\n");
+		buffer.append("id:").append(id).append("\n");
+		buffer.append("action:").append(action).append("\n");
+		buffer.append("title:").append(title).append("\n");
+		buffer.append("content:").append(content).append("\n");
+		buffer.append("extra:").append(extra).append("\n");
+		buffer.append("sender:").append(sender).append("\n");
+		buffer.append("receiver:").append(receiver).append("\n");
+		buffer.append("format:").append(format).append("\n");
+		buffer.append("timestamp:").append(timestamp);
+		return buffer.toString();
+	}
+
+	public boolean isNotEmpty(String txt) {
+		return txt != null && txt.trim().length() != 0;
+	}
+
+	@Override
+	public Message clone(){
+		Message message = new Message();
+		message.id = id;
+		message.action = action;
+		message.title = title;
+		message.content = content;
+		message.sender = sender;
+		message.receiver = receiver;
+		message.extra = extra;
+		message.format = format;
+		message.timestamp = timestamp;
+		return message;
+	}
+	@Override
+	public byte[] getBody() {
+		MessageProto.Model.Builder builder = MessageProto.Model.newBuilder();
+		builder.setId(id);
+		builder.setAction(action);
+		builder.setSender(sender);
+		builder.setTimestamp(timestamp);
+
+		/*
+		 * 下面字段可能为空
+		 */
+
+		if (receiver != null){
+			builder.setReceiver(receiver);
+		}
+
+		if (content != null) {
+			builder.setContent(content);
+		}
+
+		if (title != null) {
+			builder.setTitle(title);
+		}
+
+		if (extra != null) {
+			builder.setExtra(extra);
+		}
+
+		if (format != null) {
+			builder.setFormat(format);
+		}
+
+		return builder.build().toByteArray();
+	}
+
+	@Override
+	public byte getType() {
+		return CIMConstant.DATA_TYPE_MESSAGE;
+	}
+}

+ 62 - 0
src/main/java/com/fdkk/server/model/Ping.java

@@ -0,0 +1,62 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.server.model;
+
+
+import com.fdkk.server.constant.CIMConstant;
+
+import java.io.Serializable;
+
+/**
+ * 服务端心跳请求
+ */
+public class Ping implements Serializable, Transportable {
+
+	private static final long serialVersionUID = 1L;
+	private static final String TAG = "PING";
+	private static final String DATA = "PING";
+	private static final Ping INSTANCE = new Ping();
+
+	private Ping() {
+
+	}
+
+	public static Ping getInstance() {
+		return INSTANCE;
+	}
+
+	@Override
+	public String toString() {
+		return TAG;
+	}
+
+	@Override
+	public byte[] getBody() {
+		return DATA.getBytes();
+	}
+
+	@Override
+	public byte getType() {
+		return CIMConstant.DATA_TYPE_PING;
+	}
+
+}

+ 47 - 0
src/main/java/com/fdkk/server/model/Pong.java

@@ -0,0 +1,47 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.server.model;
+
+import java.io.Serializable;
+
+/**
+ * 客户端心跳响应
+ */
+public class Pong implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+	private static final String TAG = "PONG";
+	private static final Pong INSTANCE = new Pong();
+
+	private Pong() {
+	}
+
+	public static Pong getInstance() {
+		return INSTANCE;
+	}
+
+	@Override
+	public String toString() {
+		return TAG;
+	}
+
+}

+ 160 - 0
src/main/java/com/fdkk/server/model/ReplyBody.java

@@ -0,0 +1,160 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.server.model;
+
+
+import com.fdkk.server.constant.CIMConstant;
+import com.fdkk.server.model.proto.ReplyBodyProto;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 请求应答对象
+ *
+ */
+public class ReplyBody implements Serializable, Transportable {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 请求key
+	 */
+	private String key;
+
+	/**
+	 * 返回码
+	 */
+	private String code;
+
+	/**
+	 * 返回说明
+	 */
+	private String message;
+
+	/**
+	 * 返回数据集合
+	 */
+	private final HashMap<String, String> data = new HashMap<>();
+
+	private long timestamp;
+
+	public ReplyBody() {
+		timestamp = System.currentTimeMillis();
+	}
+
+	public long getTimestamp() {
+		return timestamp;
+	}
+
+	public void setTimestamp(long timestamp) {
+		this.timestamp = timestamp;
+	}
+
+	public String getKey() {
+		return key;
+	}
+
+	public void setKey(String key) {
+		this.key = key;
+	}
+
+	public void put(String k, String v) {
+		if (v != null && k != null) {
+			data.put(k, v);
+		}
+	}
+
+	public void putAll(Map<String, String> map) {
+		data.putAll(map);
+	}
+
+	public String get(String k) {
+		return data.get(k);
+	}
+
+	public void remove(String k) {
+		data.remove(k);
+	}
+
+	public String getMessage() {
+		return message;
+	}
+
+	public void setMessage(String message) {
+		this.message = message;
+	}
+
+	public Set<String> getKeySet() {
+		return data.keySet();
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+	public void setCode(int code) {
+		this.code = String.valueOf(code);
+	}
+
+	@Override
+	public String toString() {
+		StringBuffer buffer = new StringBuffer();
+		buffer.append("[ReplyBody]").append("\n");
+		buffer.append("key:").append(this.getKey()).append("\n");
+		buffer.append("timestamp:").append(timestamp).append("\n");
+		buffer.append("code:").append(code).append("\n");
+
+		buffer.append("data:{");
+		data.forEach((k, v) -> buffer.append("\n").append(k).append(":").append(v));
+		buffer.append("\n}");
+
+		return buffer.toString();
+	}
+
+	@Override
+	public byte[] getBody() {
+		ReplyBodyProto.Model.Builder builder = ReplyBodyProto.Model.newBuilder();
+		builder.setCode(code);
+		if (message != null) {
+			builder.setMessage(message);
+		}
+		if (!data.isEmpty()) {
+			builder.putAllData(data);
+		}
+		builder.setKey(key);
+		builder.setTimestamp(timestamp);
+
+		return builder.build().toByteArray();
+	}
+
+	@Override
+	public byte getType() {
+		return CIMConstant.DATA_TYPE_REPLY;
+	}
+
+}

+ 107 - 0
src/main/java/com/fdkk/server/model/SentBody.java

@@ -0,0 +1,107 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.server.model;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * java |android 客户端请求结构
+ *
+ */
+public class SentBody implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	private String key;
+
+	private final HashMap<String, String> data = new HashMap<>();
+
+	private long timestamp;
+
+	public String getKey() {
+		return key;
+	}
+
+	public String get(String k) {
+		return data.get(k);
+	}
+
+	public Integer getInteger(String k) {
+		return data.containsKey(k) ? Integer.parseInt(data.get(k)) : null;
+	}
+
+	public Long getLong(String k) {
+		return data.containsKey(k) ? Long.parseLong(data.get(k)) : null;
+	}
+
+	public Double getDouble(String k) {
+		return data.containsKey(k) ? Double.parseDouble(data.get(k)) : null;
+	}
+
+	public long getTimestamp() {
+		return timestamp;
+	}
+
+	public void setTimestamp(long timestamp) {
+		this.timestamp = timestamp;
+	}
+
+	public void setKey(String key) {
+		this.key = key;
+	}
+
+	public void remove(String k) {
+		data.remove(k);
+	}
+
+	public void put(String k, String v) {
+		if (v != null && k != null) {
+			data.put(k, v);
+		}
+	}
+
+	public void putAll(Map<String, String> map) {
+		data.putAll(map);
+	}
+
+	public Set<String> getKeySet() {
+		return data.keySet();
+	}
+
+	@Override
+	public String toString() {
+		StringBuffer buffer = new StringBuffer();
+		buffer.append("[SentBody]").append("\n");
+		buffer.append("key:").append(key).append("\n");
+		buffer.append("timestamp:").append(timestamp).append("\n");
+
+		buffer.append("data:{");
+		data.forEach((k, v) -> buffer.append("\n").append(k).append(":").append(v));
+		buffer.append("\n}");
+
+		return buffer.toString();
+	}
+
+}

+ 39 - 0
src/main/java/com/fdkk/server/model/Transportable.java

@@ -0,0 +1,39 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.server.model;
+
+/**
+ * 需要向另一端发送的结构体
+ */
+public interface Transportable {
+	/**
+	 * 消息体字节数组
+	 * @return
+	 */
+	byte[] getBody();
+
+	/**
+	 * 消息类型
+	 * @return
+	 */
+	byte getType();
+}

+ 15 - 0
src/main/java/com/fdkk/server/model/proto/Message.proto

@@ -0,0 +1,15 @@
+syntax = "proto3";
+package com.farsunset.cim.sdk.server.model.proto;
+option java_outer_classname="MessageProto";
+message Model {
+   int64 id = 1;
+   string action = 2;
+   string content = 3;
+   string sender = 4;
+   string receiver = 5;
+   string extra = 6;
+   string title = 7;
+   string format = 8;
+   int64 timestamp = 9;
+}
+	 

File diff ditekan karena terlalu besar
+ 1643 - 0
src/main/java/com/fdkk/server/model/proto/MessageProto.java


+ 13 - 0
src/main/java/com/fdkk/server/model/proto/ReplyBody.proto

@@ -0,0 +1,13 @@
+syntax = "proto3";
+package com.farsunset.cim.sdk.server.model.proto;
+option java_outer_classname="ReplyBodyProto";
+
+message Model {
+   string key = 1;
+   string code = 2;
+   string message = 3;
+   int64 timestamp =4;
+   map<string,string> data =5;
+   
+}
+	 

File diff ditekan karena terlalu besar
+ 1304 - 0
src/main/java/com/fdkk/server/model/proto/ReplyBodyProto.java


+ 10 - 0
src/main/java/com/fdkk/server/model/proto/SentBody.proto

@@ -0,0 +1,10 @@
+syntax = "proto3";
+package com.farsunset.cim.sdk.server.model.proto;
+option java_outer_classname="SentBodyProto";
+
+message Model {
+   string key = 1;
+   int64 timestamp =2;
+   map<string,string> data =3;
+   
+}

File diff ditekan karena terlalu besar
+ 1008 - 0
src/main/java/com/fdkk/server/model/proto/SentBodyProto.java


+ 50 - 0
src/main/java/com/fdkk/service/SessionService.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.service;
+
+
+import com.fdkk.entity.Session;
+
+import java.util.List;
+
+/**
+ * 存储连接信息,便于查看用户的链接信息
+ */
+public interface SessionService {
+
+	void add(Session session);
+
+	void delete(String uid,String nid);
+
+	/**
+	 * 删除本机的连接记录
+	 */
+	void deleteLocalhost();
+
+	void updateState(String uid,String nid,int state);
+
+	void openApns(String uid,String deviceToken);
+
+	void closeApns(String uid);
+
+	List<Session> findAll();
+}

+ 89 - 0
src/main/java/com/fdkk/service/impl/SessionServiceImpl.java

@@ -0,0 +1,89 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.service.impl;
+
+import com.fdkk.component.redis.KeyValueRedisTemplate;
+import com.fdkk.entity.Session;
+import com.fdkk.repository.SessionRepository;
+import com.fdkk.service.SessionService;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.List;
+
+@Service
+public class SessionServiceImpl implements SessionService {
+
+    @Resource
+    private SessionRepository sessionRepository;
+
+    @Resource
+    private KeyValueRedisTemplate keyValueRedisTemplate;
+
+
+    private final String host;
+
+    public SessionServiceImpl() throws UnknownHostException {
+        host = InetAddress.getLocalHost().getHostAddress();
+    }
+
+    @Override
+    public void add(Session session) {
+        session.setBindTime(System.currentTimeMillis());
+        session.setHost(host);
+        sessionRepository.save(session);
+    }
+
+    @Override
+    public void delete(String uid, String nid) {
+        sessionRepository.delete(uid,nid);
+    }
+
+    @Override
+    public void deleteLocalhost() {
+        sessionRepository.deleteAll(host);
+    }
+
+    @Override
+    public void updateState(String uid, String nid, int state) {
+        sessionRepository.updateState(uid,nid,state);
+    }
+
+    @Override
+    public void openApns(String uid,String deviceToken) {
+        keyValueRedisTemplate.openApns(uid,deviceToken);
+        sessionRepository.openApns(uid,Session.CHANNEL_IOS);
+    }
+
+    @Override
+    public void closeApns(String uid) {
+        keyValueRedisTemplate.closeApns(uid);
+        sessionRepository.closeApns(uid,Session.CHANNEL_IOS);
+    }
+
+    @Override
+    public List<Session> findAll() {
+        return sessionRepository.findAll();
+    }
+}

+ 78 - 0
src/main/java/com/fdkk/util/JSONUtils.java

@@ -0,0 +1,78 @@
+/*
+ * Copyright 2013-2019 Xia Jun(3979434@qq.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ ***************************************************************************************
+ *                                                                                     *
+ *                        Website : http://www.farsunset.com                           *
+ *                                                                                     *
+ ***************************************************************************************
+ */
+package com.fdkk.util;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.json.JsonReadFeature;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+
+import java.io.IOException;
+import java.util.List;
+
+public final class JSONUtils {
+    private static final JsonMapper OBJECT_MAPPER = JsonMapper.builder()
+            .enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS)
+            .enable(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER)
+            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+            .serializationInclusion(JsonInclude.Include.NON_NULL)
+            .build();
+
+
+    public static String toJSONString(Object data) {
+
+        try {
+            return OBJECT_MAPPER.writeValueAsString(data);
+        } catch (JsonProcessingException e) {
+            throw new IllegalArgumentException(e);
+        }
+
+    }
+
+    public static <T> T fromJson(String str, Class<T> clazz) {
+        try {
+            return OBJECT_MAPPER.readValue(str, clazz);
+        } catch (IOException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    public static <T> T fromJson(byte[] data, Class<T> clazz) {
+        try {
+            return OBJECT_MAPPER.readValue(data, clazz);
+        } catch (IOException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+
+    public static <T> List<T> parseList(String str, Class<T> clazz) {
+        JavaType javaType = OBJECT_MAPPER.getTypeFactory().constructCollectionLikeType(List.class,clazz);
+        try {
+            return OBJECT_MAPPER.readValue(str, javaType);
+        } catch (IOException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+}

+ 65 - 0
src/main/resources/application.properties

@@ -0,0 +1,65 @@
+server.port=8881
+spring.profiles.active=dev
+
+##################################################################
+#                         JDBC Config                            #
+##################################################################
+spring.datasource.url = jdbc:mysql://127.0.0.1:3306/cim?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
+spring.datasource.username = root
+spring.datasource.password = 123456
+spring.datasource.type=com.zaxxer.hikari.HikariDataSource
+spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
+
+spring.datasource.hikari.minimum-idle=5
+spring.datasource.hikari.maximum-pool-size=10
+spring.datasource.hikari.auto-commit=true
+spring.datasource.hikari.idle-timeout=30000
+spring.datasource.hikari.pool-name=MASTER_HIKARI_POOL
+spring.datasource.hikari.max-lifetime=120000
+spring.datasource.hikari.connection-timeout=30000
+spring.datasource.hikari.connection-test-query=SELECT 1
+spring.datasource.hikari.validation-timeout=600000
+
+##################################################################
+#                         JPA Config                             #
+##################################################################
+spring.jpa.database = MYSQL
+spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
+spring.jpa.hibernate.ddl-auto = update
+spring.jpa.open-in-view = false
+spring.jpa.hibernate.naming.implicit-strategy= org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
+spring.jpa.hibernate.naming.physical-strategy= org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
+
+##################################################################
+#                         Redis Config                           #
+##################################################################
+spring.redis.host=127.0.0.1
+spring.redis.password=1234
+spring.redis.database=12
+spring.redis.lettuce.pool.max-active=10
+spring.redis.lettuce.pool.max-wait= 10s
+spring.redis.lettuce.pool.max-idle=5
+spring.redis.lettuce.pool.min-idle=1
+spring.redis.timeout=10s
+
+##################################################################
+#                         Freemarker Config                      #
+##################################################################
+spring.freemarker.suffix=.html
+spring.freemarker.charset=utf-8
+spring.freemarker.content-type=text/html
+spring.freemarker.cache=false
+spring.freemarker.templateLoaderPath=classpath:/page/
+spring.freemarker.settings.auto_import = /ftl/spring.ftl as spring
+spring.messages.encoding=UTF-8
+spring.messages.basename=i18n/messages
+
+
+##################################################################
+#                         IM Config                             #
+##################################################################
+
+#commented to disable this port.
+im.app.port=23456
+im.websocket.port=34567
+

+ 68 - 0
src/main/resources/i18n/messages.properties

@@ -0,0 +1,68 @@
+module.common.html.title = IM管理系统
+module.common.account = 帐号
+module.common.password = 密码
+module.common.save = 保存
+module.common.id = ID
+module.common.type =类型
+module.common.name =名称
+module.common.time =时间
+module.common.operation =操作
+module.common.query =查询
+module.common.look =查看
+module.common.preview =预览
+module.common.send =发送
+module.common.saveing = 正在保存,请稍候......
+module.common.add = 添加
+module.common.update = 修改
+module.common.delete = 删除
+module.common.save.success = 保存成功
+module.common.delete.success = 删除成功
+module.common.delete.loading =正在删除,请稍候......
+module.common.loading = 加载中,请稍候......
+module.common.description = 说明
+module.common.state = 状态
+module.common.content = 内容
+module.common.location = 位置
+module.common.longitude = 经度
+module.common.latitude = 纬度
+module.common.sort = 排序
+module.common.code = 编号
+module.common.import = 导入
+module.common.headlogo = 头像
+module.common.homepage = 主页
+module.common.website = 网址
+module.common.text = 文字
+module.common.language = 语言
+
+module.global.error.500.hint = 服务程序发生内部错误
+module.global.error.400.hint = 请求参数类型不正确
+module.global.error.404.hint = 访问的资源不存在
+module.global.pager.next = 下一页
+module.global.pager.previou = 上一页
+module.global.pager.description = 共{0}条记录,{1}页
+
+module.console.about = 关于
+module.console.about.version = 1.0.0
+module.console.about.author = 作者: nobody
+module.console.about.author.wechat = 微信: nobody
+module.console.about.author.qq = Q Q: nobody
+module.console.about.copyright = @
+module.console.menu.onlineuser = 在线用户
+
+module.console.cimsession.sending = 正在发送,请稍候......
+module.console.cimsession.send.success = 发送成功
+module.console.cimsession.logo =头像
+module.console.cimsession.account =帐号
+module.console.cimsession.nid = 连接ID
+module.console.cimsession.channel = 终端
+module.console.cimsession.app.version =应用版本
+module.console.cimsession.os.version =系统版本
+module.console.cimsession.deviceid =设备编号
+module.console.cimsession.device.name =终端型号
+module.console.cimsession.online.time =在线时长(秒)
+module.console.cimsession.time.format ={0}秒
+
+module.console.cimsession.send.message =发送消息
+module.console.cimsession.receiver = 接收帐号
+module.console.cimsession.message = 消息内容
+

+ 12 - 0
src/main/resources/logback-spring.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+        <withJansi>true</withJansi>
+        <encoder>
+            <pattern>[%thread] %d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %cyan(%logger{32}) - %msg %n</pattern>
+        </encoder>
+    </appender>
+    <root level="info">
+        <appender-ref ref="console"/>
+    </root>
+</configuration>

+ 44 - 0
src/main/resources/page/console/header.html

@@ -0,0 +1,44 @@
+<!-- header -->
+<div id="_main_header_banner" class="header">
+    <h3 style="float:left;color:#ffffff;line-height: 60px;margin-left: 30px;font-family: 楷体;font-size: 32px">体验页面</h3>
+	<div class="btn-group" style=" float: right;margin-top: 33px;margin-right:20px;">
+		<button class="btn btn-info" onclick="doShowDialog('aboutDialog')">
+			   <span class="glyphicon glyphicon-info-sign"></span>&nbsp;<@spring.message "module.console.about"/>
+		</button>
+	</div>
+	<div class="header_liner"></div>
+</div>
+
+<div class="modal fade" id="aboutDialog" tabindex="-1" role="dialog">
+	<div class="modal-dialog" style="width: 420px;">
+		<div class="modal-content">
+			<div class="modal-header" >
+				<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+                <h4 class="modal-title"><@spring.message "module.common.html.title"/></h4>
+			</div>
+			<div class="modal-body">
+			    <div style="text-align: center;border:none;height: 150px;">
+<!--			        <img src="/image/icon.png" width="100px" height="100px" style="box-shadow: 0px 0px 7px 2px #d1d3d6;border: 1px solid #dfdfe0;padding: 10px;border-radius: 100px;margin-top: 20px;"/>-->
+					<h4 style="margin-top: 15px;"><@spring.message "module.console.about.version"/></h4>
+				</div>
+				<ul class="list-group" style="margin-top: 20px;">
+					<li class="list-group-item" style="border-radius: 0px;">
+						<@spring.message "module.console.about.copyright"/>
+					</li>
+					<li class="list-group-item" style="border-radius: 0px;">
+						<@spring.message "module.console.about.author"/>
+					</li>
+					<li class="list-group-item" style="border-radius: 0px;">
+						<@spring.message "module.console.about.author.qq"/>
+					</li>
+					<li class="list-group-item" style="border-radius: 0px;">
+						<@spring.message "module.console.about.author.wechat"/>
+					</li>
+				</ul>
+        	</div>
+		</div>
+	</div>
+</div>
+
+
+<div id="global_mask" style="display: none; position: absolute; top: 0px; left: 0px; z-index: 9999; background-color: rgb(20, 20, 20); opacity: 0.5; width: 100%; height: 100%; overflow: hidden; background-position: initial initial; background-repeat: initial initial;"></div>

+ 25 - 0
src/main/resources/page/console/index.html

@@ -0,0 +1,25 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+	<head>
+		<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+		<title><@spring.message "module.common.html.title"/></title>
+		<link rel="shortcut icon" href="/image/favicon.ico" type="image/x-icon">
+        <link rel="stylesheet" href="/bootstrap-3.3.7-dist/css/bootstrap.min.css" />
+		<link rel="stylesheet" href="/css/common.css" />
+		<script type="text/javascript" src="/js/jquery-3.3.1.min.js"></script>
+		<script type="text/javascript" src="/bootstrap-3.3.7-dist/js/bootstrap.min.js"></script>
+		<script type="text/javascript" src="/js/common.js"></script>
+	</head>
+	<body class="web-app ui-selectable">
+
+           <#include "/console/header.html">
+           <#include "/console/nav.html">
+			<div id="mainWrapper" style="padding-top: 25px;" >
+			</div>
+			 <script>
+		      $('#indexMenu').addClass('current');
+		      </script>
+	</body>
+	
+           
+</html>

+ 22 - 0
src/main/resources/page/console/nav.html

@@ -0,0 +1,22 @@
+
+<div id="_main_nav" class="ui-vnav">
+	<ul class="ui-nav-inner">
+		<li style="height: 50px;text-align: center;margin-top: 10px;">
+				<a type="button" target="_blank" href="/webclient" class="btn btn-danger" >
+					<span class="glyphicon glyphicon-globe"></span> 网页终端
+				</a>		 
+		</li>
+		<li style="height: 50px;text-align: center;margin-top: 10px;">
+			<a type="button" target="_blank" href="/swagger-ui/index.html" class="btn btn-success" >
+				<span class="glyphicon glyphicon-leaf"></span> 接口文档
+			</a>
+		</li>
+		<li style="border-bottom: 1px solid #D1D6DA;"></li>
+		<li  class="ui-item" id="sessionMenu">
+			<a href="/console/session/list">
+				<img src="/image/icon/online.svg" style="margin-top:-2px;" width="24px" height="24px"/>
+				<span class="ui-text"><@spring.message "module.console.menu.onlineuser"/></span>
+			</a>
+		</li>
+	</ul>
+</div>

+ 128 - 0
src/main/resources/page/console/session/manage.html

@@ -0,0 +1,128 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+<title><@spring.message "module.common.html.title"/></title>
+<link rel="shortcut icon" href="/image/favicon.ico" type="image/x-icon">
+<link rel="stylesheet" href="/bootstrap-3.3.7-dist/css/bootstrap.min.css" />
+<link rel="stylesheet" href="/css/common.css" />
+<script type="text/javascript" src="/js/jquery-3.3.1.min.js"></script>
+<script type="text/javascript" src="/bootstrap-3.3.7-dist/js/bootstrap.min.js"></script>
+<script type="text/javascript" src="/js/common.js"></script>
+<script type="text/javascript" src="/js/jquery-ui.min.js"></script>
+
+<script>
+function showMessageDialog(account){
+  $('#messageDialog').modal('show');
+  $('#Saccount').val(account);
+}
+
+function doSendMessage(){
+    var message = $('#message').val();
+    var account = $('#Saccount').val();
+    if($.trim(message)===''){
+       return;
+    }
+    showProcess("<@spring.message 'module.console.cimsession.sending'/>");
+    $.post("/api/message/send", {content:message,action:2,sender:'system',receiver:account,format:'0'},
+	   function(data){
+	      hideProcess();
+	      showSTip("<@spring.message 'module.console.cimsession.send.success'/>");
+	      doHideDialog("messageDialog");
+     });
+}
+  
+</script>
+</head>
+<body class="web-app ui-selectable">
+
+<#include "/console/header.html">
+<#include "/console/nav.html">
+
+<div id="mainWrapper">
+
+	<div class="lay-main-toolbar"></div>
+	<div>
+		<form action="/console/session/list.action" method="post" id="searchForm" >
+			<input type="hidden" name="currentPage" id="currentPage" value="0"/>
+			<table style="width: 100%" class="utable">
+				<thead>
+					<tr class="tableHeader">
+						<th width="8%"><@spring.message 'module.console.cimsession.account'/></th>
+						<th width="8%"><@spring.message 'module.console.cimsession.nid'/></th>
+						<th width="8%"><@spring.message 'module.console.cimsession.channel'/></th>
+						<th width="15%"><@spring.message 'module.console.cimsession.deviceid'/></th>
+						<th width="15%"><@spring.message 'module.console.cimsession.device.name'/></th>
+						<th width="8%"><@spring.message 'module.console.cimsession.app.version'/></th>
+						<th width="8%"><@spring.message 'module.console.cimsession.os.version'/></th>
+						<th width="10%"><@spring.message 'module.common.language'/></th>
+						<th width="10%"><@spring.message 'module.console.cimsession.online.time'/></th>
+						<th width="10%"><@spring.message "module.common.operation"/></th>
+					</tr>
+				</thead>
+				<tbody>
+               		<#list sessionList as cimsession>
+						<tr style="height: 50px;">
+						    <td>${cimsession.uid! }</td>
+							<td><#if cimsession.nid??>${cimsession.nid}</#if></td>
+							<td>${cimsession.channel! }</td>
+							<td>${cimsession.deviceId! }</td>
+							<td>${cimsession.deviceName! }</td>
+							<td>${cimsession.appVersion! }</td>
+	                        <td>${cimsession.osVersion! }</td>
+							<td>${cimsession.language! }</td>
+							<td>
+								<@spring.messageArgs  "module.console.cimsession.time.format",[((.now?long - cimsession.bindTime)/1000)?round?c]  />
+							</td>
+							<td>
+								<div class="btn-group btn-group-xs">
+									<button type="button" class="btn btn-primary" style="padding: 5px;" onclick="showMessageDialog('${cimsession.uid!}')">
+										<span class="glyphicon glyphicon-send" style="top:2px;"></span>
+										<@spring.message 'module.console.cimsession.send.message'/>
+									</button>
+								</div>
+							</td>
+				 		</tr>
+					</#list>
+				</tbody>
+			</table>
+		</form>
+	</div>
+</div>
+<div class="modal fade" id="messageDialog" tabindex="-1" role="dialog" >
+	<div class="modal-dialog" style="width: 420px;">
+		<div class="modal-content">
+			<div class="modal-header">
+				<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+                <h4 class="modal-title"><@spring.message 'module.console.cimsession.send.message'/></h4>
+			</div>
+			<div class="modal-body">
+				<div class="form-groupBuy">
+					<label for="Amobile">
+						<@spring.message 'module.console.cimsession.receiver'/>
+					</label>
+					<input type="text" class="form-control" id="Saccount" name="account"
+						style="width: 100%; font-size: 20px; font-weight: bold;height:40px;"
+						disabled="disabled" />
+				</div>
+				<div class="form-groupBuy" style="margin-top: 20px;">
+					<label for="exampleInputFile">
+						<@spring.message 'module.console.cimsession.message'/>
+					</label>
+					<textarea rows="10" style="width: 100%; height: 200px;" id="message" name="message" class="form-control"></textarea>
+				</div>
+			</div>
+			<div class="modal-footer" style="padding: 5px 10px; text-align: center;">
+				<button type="button" class="btn btn-success btn-lg" style="width: 200px;" onclick="doSendMessage()">
+					<span class="glyphicon glyphicon-send" style="top:2px;"></span><@spring.message 'module.common.send'/>
+				</button>
+			</div>
+		</div>
+	</div>
+</div>
+
+<script>
+$('#sessionMenu').addClass('current');
+</script>
+</body>
+</html>

+ 184 - 0
src/main/resources/page/console/webclient/index.html

@@ -0,0 +1,184 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+<head>
+<meta charset="utf-8"/>
+<title> Webclient  </title>
+        <link rel="shortcut icon" href="/image/favicon.ico" type="image/x-icon">
+		<link charset="utf-8" rel="stylesheet" 	href="/bootstrap-3.3.7-dist/css/bootstrap.min.css" />
+		<link charset="utf-8" rel="stylesheet" href="/css/common.css" />
+		<script type="text/javascript" 	src="/js/jquery-3.3.1.min.js"></script>
+		<script type="text/javascript" src="/bootstrap-3.3.7-dist/js/bootstrap.min.js"></script>
+		<script type="text/javascript" src="/js/common.js"></script>
+		<script type="text/javascript" src="/js/im/message.js"></script>
+		<script type="text/javascript" src="/js/im/replybody.js"></script>
+		<script type="text/javascript" src="/js/im/sentbody.js"></script>
+		<script type="text/javascript" src="/js/im/cim.web.sdk.js"></script>
+</head>
+
+
+<script>
+
+   /***********************************推送配置开始**************************/
+
+   /**  当socket连接成功回调 **/
+   function onConnectFinished(){
+	   CIMPushManager.bind($('#account').val());
+   }
+
+   /** 当收到请求回复时候回调  **/
+   function onReplyReceived(reply)
+   {
+	 console.log(reply);
+     if(reply.key==='client_bind' && reply.code === "200" )
+     {
+        hideProcess();
+
+        $('#LoginDialog').fadeOut();
+
+
+        $('#MessageDialog').fadeIn();
+        $('#MessageDialog').addClass("in");
+        $("#current_account").text($('#account').val());
+
+     }
+   }
+
+   /** 当收到消息时候回调  **/
+
+   function onMessageReceived(message)
+   {
+	   console.log(message);
+       /*
+        账户在其他地方登录了
+        */
+	   if(message.action === ACTION_999){
+	       $('#MessageDialog').fadeOut();
+		   $('#LoginDialog').fadeIn();
+	       $('#LoginDialog').addClass("in");
+		   return ;
+	   }
+
+	   showNotification(message.content);
+	   var time = new Date(message.timestamp).toLocaleString();
+       $("#messageList").prepend("<div class='alert alert-info' >"+time+"</p></p>"+message.content+"</div>");
+   }
+
+
+   /***********************************推送配置结束**************************/
+
+
+
+   /***********************************业务配置开始**************************/
+
+    function doLogin(){
+
+		    if($.trim($('#account').val()) =='' ){
+		       return;
+		    }
+		    showProcess('正在接入请稍后......');
+		    /**登录成功后创建连接****/
+		    CIMPushManager.connect();
+	}
+
+
+
+    $(document).ready(function(){
+    	$('#LoginDialog').fadeIn();
+        $('#LoginDialog').addClass("in");
+		$('#host').val(CIM_HOST);
+		$('#port').val(CIM_PORT);
+		initNotification();
+    });
+
+
+    function initNotification(){
+    	//判断浏览器是否支持桌面通知
+    	if (window.Notification) {
+    	    var notification = window.Notification;
+    	    if (notification.permission == "default") {
+    	    	 notification.requestPermission(function(permission) {
+     	        });
+    	    }
+    	}
+    }
+
+    function showNotification(msg){
+    	var notify = new Notification("系统消息", {
+    	        body: msg,
+    	        icon: '/image/icon.png',
+    	        tag: 1
+    	});
+
+    	notify.onshow = function() {
+    	        setTimeout(function() {
+    	        	notify.close();
+    	        }, 3000);
+    	}
+    }
+
+   /***********************************业务配置结束**************************/
+</script>
+
+
+<body style="width: 600px;">
+
+
+ <div class="modal fade" id="LoginDialog" tabindex="-1" role="dialog" data-backdrop="static">
+ <div class="modal-dialog" style="width: 400px;margin: 64px auto;">
+		<div class="modal-content" >
+			<div class="modal-body" style="padding:0px;" >
+            <div  style="height:200px;text-align: center; background: #5FA0D3; color: #ffffff; border: 0px; border-top-left-radius: 4px; border-top-right-radius: 4px;">
+	        <img src="/image/icon.png" style="height: 72px;width: 72px;margin-top:40px;"/>
+	        <div style="margin-top: 20px; color: #ffffff;font-size: 16px;">请输入一个帐号用于登录,随后接收推送消息</div>
+ 		    </div>
+
+	        	<div class="input-group" style="margin-top: 30px;margin-left:10px;margin-right:10px;margin-bottom:30px;">
+	        	  <span class="input-group-addon">账号</span>
+				  <input type="text" class="form-control" id="account" maxlength="32" placeholder="帐号(数字或者英文字母)"
+					style="display: inline; width: 100%; height: 50px;" />
+				</div>
+
+
+				<div class="alert alert-success" role="alert" style="margin: 0 10px;">
+					cim.web.sdk.js中设置cim服务的IP(域名)和端口
+				</div>
+
+				<div class="input-group" style="margin-top: 30px;margin-left:10px;margin-right:10px;margin-bottom:30px;">
+					<span class="input-group-addon">host</span>
+					<input type="text" class="form-control" id="host" maxlength="32" readonly = "readonly"
+						   style="display: inline; width: 100%; height: 50px;" />
+				</div>
+
+				<div class="input-group" style="margin-top: 30px;margin-left:10px;margin-right:10px;margin-bottom:30px;">
+					<span class="input-group-addon">port</span>
+					<input type="text" class="form-control" id="port" maxlength="32" readonly = "readonly"
+						   style="display: inline; width: 100%; height: 50px;" />
+				</div>
+			</div>
+			<div class="modal-footer" style="text-align: center;">
+				<a type="button" class="btn btn-success btn-lg" onclick="doLogin()"
+					style="width: 300px;">登录</a>
+			</div>
+      </div>
+      </div>
+</div>
+
+<!-- 消息提示页面 -->
+ <div class="modal fade" data-backdrop="static" id="MessageDialog" tabindex="-1" role="dialog" >
+	 <div class="alert alert-success" role="alert">
+		 通过<a href="/console/session/list" class="alert-link" target="_blank" >控制台</a>或者<a href="/swagger-ui/index.html" target="_blank"  class="alert-link">调用接口</a>发送消息
+	 </div>
+ <div class="modal-dialog" style="width: 720px;margin: 30px auto;">
+		<div class="modal-content" >
+		 <div class="modal-header" style="text-align: center;">
+				<span style="float: left;">消息显示面板</span>
+				<span style="float: right;color: #4caf50;">当前帐号:<span id="current_account"></span></span>
+			</div>
+			<div class="modal-body" id="messageList" style="min-height: 600px;" >
+		    </div>
+      </div>
+      </div>
+</div>
+
+</body>
+</html>

+ 384 - 0
src/main/resources/page/ftl/spring.ftl

@@ -0,0 +1,384 @@
+<#ftl strip_whitespace=true>
+<#--
+ * spring.ftl
+ *
+ * This file consists of a collection of FreeMarker macros aimed at easing
+ * some of the common requirements of web applications - in particular
+ * handling of forms.
+ *
+ * Spring's FreeMarker support will automatically make this file and therefore
+ * all macros within it available to any application using Spring's
+ * FreeMarkerConfigurer.
+ *
+ * To take advantage of these macros, the "exposeSpringMacroHelpers" property
+ * of the FreeMarker class needs to be set to "true". This will expose a
+ * RequestContext under the name "springMacroRequestContext", as needed by
+ * the macros in this library.
+ *
+ * @author Darren Davison
+ * @author Juergen Hoeller
+ * @since 1.1
+ -->
+
+<#--
+ * message
+ *
+ * Macro to translate a message code into a message.
+ -->
+<#macro message code>${springMacroRequestContext.getMessage(code)}</#macro>
+
+<#--
+ * messageText
+ *
+ * Macro to translate a message code into a message,
+ * using the given default text if no message found.
+ -->
+<#macro messageText code, text>${springMacroRequestContext.getMessage(code, text)}</#macro>
+
+<#--
+ * messageArgs
+ *
+ * Macro to translate a message code with arguments into a message.
+ -->
+<#macro messageArgs code, args>${springMacroRequestContext.getMessage(code, args)}</#macro>
+
+<#--
+ * messageArgsText
+ *
+ * Macro to translate a message code with arguments into a message,
+ * using the given default text if no message found.
+ -->
+<#macro messageArgsText code, args, text>${springMacroRequestContext.getMessage(code, args, text)}</#macro>
+
+<#--
+ * theme
+ *
+ * Macro to translate a theme message code into a message.
+ -->
+<#macro theme code>${springMacroRequestContext.getThemeMessage(code)}</#macro>
+
+<#--
+ * themeText
+ *
+ * Macro to translate a theme message code into a message,
+ * using the given default text if no message found.
+ -->
+<#macro themeText code, text>${springMacroRequestContext.getThemeMessage(code, text)}</#macro>
+
+<#--
+ * themeArgs
+ *
+ * Macro to translate a theme message code with arguments into a message.
+ -->
+<#macro themeArgs code, args>${springMacroRequestContext.getThemeMessage(code, args)}</#macro>
+
+<#--
+ * themeArgsText
+ *
+ * Macro to translate a theme message code with arguments into a message,
+ * using the given default text if no message found.
+ -->
+<#macro themeArgsText code, args, text>${springMacroRequestContext.getThemeMessage(code, args, text)}</#macro>
+
+<#--
+ * url
+ *
+ * Takes a relative URL and makes it absolute from the server root by
+ * adding the context root for the web application.
+ -->
+<#macro url relativeUrl extra...><#if extra?? && extra?size!=0>${springMacroRequestContext.getContextUrl(relativeUrl,extra)}<#else>${springMacroRequestContext.getContextUrl(relativeUrl)}</#if></#macro>
+
+<#--
+ * bind
+ *
+ * Exposes a BindStatus object for the given bind path, which can be
+ * a bean (e.g. "person") to get global errors, or a bean property
+ * (e.g. "person.name") to get field errors. Can be called multiple times
+ * within a form to bind to multiple command objects and/or field names.
+ *
+ * This macro will participate in the default HTML escape setting for the given
+ * RequestContext. This can be customized by calling "setDefaultHtmlEscape"
+ * on the "springMacroRequestContext" context variable, or via the
+ * "defaultHtmlEscape" context-param in web.xml (same as for the JSP bind tag).
+ * Also regards a "htmlEscape" variable in the namespace of this library.
+ *
+ * Producing no output, the following context variable will be available
+ * each time this macro is referenced (assuming you import this library in
+ * your templates with the namespace 'spring'):
+ *
+ *   spring.status : a BindStatus instance holding the command object name,
+ *   expression, value, and error messages and codes for the path supplied
+ *
+ * @param path : the path (string value) of the value required to bind to.
+ *   Spring defaults to a command name of "command" but this can be overridden
+ *   by user config.
+ -->
+<#macro bind path>
+    <#if htmlEscape?exists>
+        <#assign status = springMacroRequestContext.getBindStatus(path, htmlEscape)>
+    <#else>
+        <#assign status = springMacroRequestContext.getBindStatus(path)>
+    </#if>
+    <#-- assign a temporary value, forcing a string representation for any
+    kind of variable. This temp value is only used in this macro lib -->
+    <#if status.value?exists && status.value?is_boolean>
+        <#assign stringStatusValue=status.value?string>
+    <#else>
+        <#assign stringStatusValue=status.value?default("")>
+    </#if>
+</#macro>
+
+<#--
+ * bindEscaped
+ *
+ * Similar to spring:bind, but takes an explicit HTML escape flag rather
+ * than relying on the default HTML escape setting.
+ -->
+<#macro bindEscaped path, htmlEscape>
+    <#assign status = springMacroRequestContext.getBindStatus(path, htmlEscape)>
+    <#-- assign a temporary value, forcing a string representation for any
+    kind of variable. This temp value is only used in this macro lib -->
+    <#if status.value?exists && status.value?is_boolean>
+        <#assign stringStatusValue=status.value?string>
+    <#else>
+        <#assign stringStatusValue=status.value?default("")>
+    </#if>
+</#macro>
+
+<#--
+ * formInput
+ *
+ * Display a form input field of type 'text' and bind it to an attribute
+ * of a command or bean.
+ *
+ * @param path the name of the field to bind to
+ * @param attributes any additional attributes for the element (such as class
+ *    or CSS styles or size
+ -->
+<#macro formInput path attributes="" fieldType="text">
+    <@bind path/>
+    <input type="${fieldType}" id="${status.expression?replace('[','')?replace(']','')}" name="${status.expression}" value="<#if fieldType!="password">${stringStatusValue}</#if>" ${attributes}<@closeTag/>
+</#macro>
+
+<#--
+ * formPasswordInput
+ *
+ * Display a form input field of type 'password' and bind it to an attribute
+ * of a command or bean. No value will ever be displayed. This functionality
+ * can also be obtained by calling the formInput macro with a 'type' parameter
+ * of 'password'.
+ *
+ * @param path the name of the field to bind to
+ * @param attributes any additional attributes for the element (such as class
+ *    or CSS styles or size
+ -->
+<#macro formPasswordInput path attributes="">
+    <@formInput path, attributes, "password"/>
+</#macro>
+
+<#--
+ * formHiddenInput
+ *
+ * Generate a form input field of type 'hidden' and bind it to an attribute
+ * of a command or bean. This functionality can also be obtained by calling
+ * the formInput macro with a 'type' parameter of 'hidden'.
+ *
+ * @param path the name of the field to bind to
+ * @param attributes any additional attributes for the element (such as class
+ *    or CSS styles or size
+ -->
+<#macro formHiddenInput path attributes="">
+    <@formInput path, attributes, "hidden"/>
+</#macro>
+
+<#--
+ * formTextarea
+ *
+ * Display a text area and bind it to an attribute of a command or bean.
+ *
+ * @param path the name of the field to bind to
+ * @param attributes any additional attributes for the element (such as class
+ *    or CSS styles or size
+ -->
+<#macro formTextarea path attributes="">
+    <@bind path/>
+    <textarea id="${status.expression?replace('[','')?replace(']','')}" name="${status.expression}" ${attributes}>
+${stringStatusValue}</textarea>
+</#macro>
+
+<#--
+ * formSingleSelect
+ *
+ * Show a selectbox (dropdown) input element allowing a single value to be chosen
+ * from a list of options.
+ *
+ * @param path the name of the field to bind to
+ * @param options a map (value=label) of all the available options
+ * @param attributes any additional attributes for the element (such as class
+ *    or CSS styles or size
+-->
+<#macro formSingleSelect path options attributes="">
+    <@bind path/>
+    <select id="${status.expression?replace('[','')?replace(']','')}" name="${status.expression}" ${attributes}>
+        <#if options?is_hash>
+            <#list options?keys as value>
+            <option value="${value?html}"<@checkSelected value/>>${options[value]?html}</option>
+            </#list>
+        <#else> 
+            <#list options as value>
+            <option value="${value?html}"<@checkSelected value/>>${value?html}</option>
+            </#list>
+        </#if>
+    </select>
+</#macro>
+
+<#--
+ * formMultiSelect
+ *
+ * Show a listbox of options allowing the user to make 0 or more choices from
+ * the list of options.
+ *
+ * @param path the name of the field to bind to
+ * @param options a map (value=label) of all the available options
+ * @param attributes any additional attributes for the element (such as class
+ *    or CSS styles or size
+-->
+<#macro formMultiSelect path options attributes="">
+    <@bind path/>
+    <select multiple="multiple" id="${status.expression?replace('[','')?replace(']','')}" name="${status.expression}" ${attributes}>
+        <#list options?keys as value>
+        <#assign isSelected = contains(status.actualValue?default([""]), value)>
+        <option value="${value?html}"<#if isSelected> selected="selected"</#if>>${options[value]?html}</option>
+        </#list>
+    </select>
+</#macro>
+
+<#--
+ * formRadioButtons
+ *
+ * Show radio buttons.
+ *
+ * @param path the name of the field to bind to
+ * @param options a map (value=label) of all the available options
+ * @param separator the html tag or other character list that should be used to
+ *    separate each option. Typically '&nbsp;' or '<br>'
+ * @param attributes any additional attributes for the element (such as class
+ *    or CSS styles or size
+-->
+<#macro formRadioButtons path options separator attributes="">
+    <@bind path/>
+    <#list options?keys as value>
+    <#assign id="${status.expression?replace('[','')?replace(']','')}${value_index}">
+    <input type="radio" id="${id}" name="${status.expression}" value="${value?html}"<#if stringStatusValue == value> checked="checked"</#if> ${attributes}<@closeTag/>
+    <label for="${id}">${options[value]?html}</label>${separator}
+    </#list>
+</#macro>
+
+<#--
+ * formCheckboxes
+ *
+ * Show checkboxes.
+ *
+ * @param path the name of the field to bind to
+ * @param options a map (value=label) of all the available options
+ * @param separator the html tag or other character list that should be used to
+ *    separate each option. Typically '&nbsp;' or '<br>'
+ * @param attributes any additional attributes for the element (such as class
+ *    or CSS styles or size
+-->
+<#macro formCheckboxes path options separator attributes="">
+    <@bind path/>
+    <#list options?keys as value>
+    <#assign id="${status.expression?replace('[','')?replace(']','')}${value_index}">
+    <#assign isSelected = contains(status.actualValue?default([""]), value)>
+    <input type="checkbox" id="${id}" name="${status.expression}" value="${value?html}"<#if isSelected> checked="checked"</#if> ${attributes}<@closeTag/>
+    <label for="${id}">${options[value]?html}</label>${separator}
+    </#list>
+    <input type="hidden" name="_${status.expression}" value="on"/>
+</#macro>
+
+<#--
+ * formCheckbox
+ *
+ * Show a single checkbox.
+ *
+ * @param path the name of the field to bind to
+ * @param attributes any additional attributes for the element (such as class
+ *    or CSS styles or size
+-->
+<#macro formCheckbox path attributes="">
+	<@bind path />
+    <#assign id="${status.expression?replace('[','')?replace(']','')}">
+    <#assign isSelected = status.value?? && status.value?string=="true">
+	<input type="hidden" name="_${status.expression}" value="on"/>
+	<input type="checkbox" id="${id}" name="${status.expression}"<#if isSelected> checked="checked"</#if> ${attributes}/>
+</#macro>
+
+<#--
+ * showErrors
+ *
+ * Show validation errors for the currently bound field, with
+ * optional style attributes.
+ *
+ * @param separator the html tag or other character list that should be used to
+ *    separate each option. Typically '<br>'.
+ * @param classOrStyle either the name of a CSS class element (which is defined in
+ *    the template or an external CSS file) or an inline style. If the value passed in here
+ *    contains a colon (:) then a 'style=' attribute will be used, else a 'class=' attribute
+ *    will be used.
+-->
+<#macro showErrors separator classOrStyle="">
+    <#list status.errorMessages as error>
+    <#if classOrStyle == "">
+        <b>${error}</b>
+    <#else>
+        <#if classOrStyle?index_of(":") == -1><#assign attr="class"><#else><#assign attr="style"></#if>
+        <span ${attr}="${classOrStyle}">${error}</span>
+    </#if>
+    <#if error_has_next>${separator}</#if>
+    </#list>
+</#macro>
+
+<#--
+ * checkSelected
+ *
+ * Check a value in a list to see if it is the currently selected value.
+ * If so, add the 'selected="selected"' text to the output.
+ * Handles values of numeric and string types.
+ * This function is used internally but can be accessed by user code if required.
+ *
+ * @param value the current value in a list iteration
+-->
+<#macro checkSelected value>
+    <#if stringStatusValue?is_number && stringStatusValue == value?number>selected="selected"</#if>
+    <#if stringStatusValue?is_string && stringStatusValue == value>selected="selected"</#if>
+</#macro>
+
+<#--
+ * contains
+ *
+ * Macro to return true if the list contains the scalar, false if not.
+ * Surprisingly not a FreeMarker builtin.
+ * This function is used internally but can be accessed by user code if required.
+ *
+ * @param list the list to search for the item
+ * @param item the item to search for in the list
+ * @return true if item is found in the list, false otherwise
+-->
+<#function contains list item>
+    <#list list as nextInList>
+    <#if nextInList == item><#return true></#if>
+    </#list>
+    <#return false>
+</#function>
+
+<#--
+ * closeTag
+ *
+ * Simple macro to close an HTML tag that has no body with '>' or '/>',
+ * depending on the value of a 'xhtmlCompliant' variable in the namespace
+ * of this library.
+-->
+<#macro closeTag>
+    <#if xhtmlCompliant?exists && xhtmlCompliant>/><#else>></#if>
+</#macro>

+ 209 - 0
src/main/resources/page/messsage/index.html

@@ -0,0 +1,209 @@
+<!DOCTYPE html
+	PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+
+<head>
+	<meta charset="utf-8" />
+	<title>message Html </title>
+	<link rel="shortcut icon" href="image/favicon.ico" type="image/x-icon">
+	<link rel="stylesheet" href="/bootstrap-3.3.7-dist/css/bootstrap.min.css" />
+	<link rel="stylesheet" href="/css/common.css" />
+	<script type="text/javascript" src="/js/jquery-3.3.1.min.js"></script>
+	<script type="text/javascript" src="/js/bootstrap.min.js"></script>
+	<script type="text/javascript" src="/js/common.js"></script>
+	<script type="text/javascript" src="/js/im/message.js"></script>
+	<script type="text/javascript" src="/js/im/replybody.js"></script>
+	<script type="text/javascript" src="/js/im/sentbody.js"></script>
+	<script type="text/javascript" src="/js/im/cim.web.sdk.js"></script>
+</head>
+
+
+<script>
+	/***********************************推送配置开始**************************/
+
+	/**  当socket连接成功回调 **/
+	function onConnectFinished() {
+		CIMPushManager.bind($('#account').val());
+	}
+
+	/** 当收到请求回复时候回调  **/
+	function onReplyReceived(reply) {
+		console.log(reply);
+		if (reply.key === 'client_bind' && reply.code === "200") {
+			hideProcess();
+			$('#LoginDialog').fadeOut();
+			$('#MessageDialog').fadeIn();
+			$('#MessageDialog').addClass("in");
+			$("#current_account").text($('#account').val());
+
+		}
+	}
+
+	/** 当收到消息时候回调  **/
+
+	function onMessageReceived(message) {
+		console.log(message);
+		if (message.action === ACTION_999) {
+			$('#MessageDialog').fadeOut();
+			$('#LoginDialog').fadeIn();
+			$('#LoginDialog').addClass("in");
+			return;
+		}
+
+		showNotification(message.content);
+		var time = new Date(message.timestamp).toLocaleString();
+		if(message.sender == window.localStorage.account){
+			$("#messageList").append("<div class='alert alert-info mytext' style='text-align:right'>" + time + "</p></p>" + message.content+': '+message.sender + "</div>");
+		}else{
+			$("#messageList").append("<div class='alert alert-info' >" + time + "</p></p>" +message.sender+': '+ message.content + "</div>");
+		}
+	}
+
+
+	/***********************************推送配置结束**************************/
+
+
+
+	/***********************************业务配置开始**************************/
+
+	function doLogin() {
+
+		if($.trim($('#account').val()) =='' ){
+		   return;
+		}
+		// hideProcess();
+		// $('#LoginDialog').fadeOut();
+		// $('#MessageDialog').fadeIn();
+		// $('#MessageDialog').addClass("in");
+		// $("#current_account").text($('#account').val());
+		showProcess('正在接入请稍后......');
+		/**登录成功后创建连接****/
+		CIMPushManager.connect();
+	}
+	function sendText(){
+		//发送消息
+		var content = $('#content').val()
+		console.log('content',content)
+		CIMPushManager.send(content);
+		$('#content').val("")
+		//发送成功 添加节点
+
+	}
+
+	function sendKeyDown(e){
+		if(e.keyCode == 13){
+			sendText()
+		}
+	}
+
+	$(document).ready(function () {
+		$('#LoginDialog').fadeIn();
+		$('#LoginDialog').addClass("in");
+		$('#host').val(CIM_HOST);
+		$('#port').val(CIM_PORT);
+		initNotification();
+	});
+
+
+	function initNotification() {
+		//判断浏览器是否支持桌面通知
+		if (window.Notification) {
+			var notification = window.Notification;
+			if (notification.permission == "default") {
+				notification.requestPermission(function (permission) {});
+			}
+		}
+	}
+
+	function showNotification(msg) {
+		var notify = new Notification("系统消息", {
+			body: msg,
+			icon: '/image/icon.png',
+			tag: 1
+		});
+
+		notify.onshow = function () {
+			setTimeout(function () {
+				notify.close();
+			}, 3000);
+		}
+	}
+
+	/***********************************业务配置结束**************************/
+</script>
+
+
+<body style="width: 600px;">
+
+
+	<div class="modal fade" id="LoginDialog" tabindex="-1" role="dialog" data-backdrop="static">
+		<div class="modal-dialog" style="width: 400px;margin: 64px auto;">
+			<div class="modal-content">
+				<div class="modal-body" style="padding:0px;">
+					<div
+						style="height:200px;text-align: center; background: #5FA0D3; color: #ffffff; border: 0px; border-top-left-radius: 4px; border-top-right-radius: 4px;">
+						<img src="image/log.png" style="margin-top:40px;" />
+						<div style="margin-top: 20px; color: #ffffff;font-size: 16px;">请输入一个帐号用于登录,随后接收推送消息</div>
+					</div>
+
+					<div class="input-group"
+						style="margin-top: 30px;margin-left:10px;margin-right:10px;margin-bottom:30px;">
+						<span class="input-group-addon">账号</span>
+						<input type="text" class="form-control" id="account" maxlength="32" placeholder="帐号(数字或者英文字母)"
+							style="display: inline; width: 100%; height: 50px;" />
+					</div>
+
+
+					<div class="alert alert-success" role="alert" style="margin: 0 10px;">
+						四维时代技术支持
+					</div>
+
+					<div class="input-group"
+						style="margin-top: 30px;margin-left:10px;margin-right:10px;margin-bottom:30px;">
+						<span class="input-group-addon">host</span>
+						<input type="text" class="form-control" id="host" maxlength="32" readonly="readonly"
+							style="display: inline; width: 100%; height: 50px;" />
+					</div>
+
+					<div class="input-group"
+						style="margin-top: 30px;margin-left:10px;margin-right:10px;margin-bottom:30px;">
+						<span class="input-group-addon">port</span>
+						<input type="text" class="form-control" id="port" maxlength="32" readonly="readonly"
+							style="display: inline; width: 100%; height: 50px;" />
+					</div>
+				</div>
+				<div class="modal-footer" style="text-align: center;">
+					<a type="button" class="btn btn-success btn-lg" onclick="doLogin()" style="width: 300px;">登录</a>
+				</div>
+			</div>
+		</div>
+	</div>
+
+	<!-- 消息提示页面 -->
+	<div class="modal fade" data-backdrop="static" id="MessageDialog" tabindex="-1" role="dialog">
+		<div class="alert alert-success" role="alert">
+			通过<a href="/console/session/list" class="alert-link" target="_blank">控制台</a>或者<a
+				href="/swagger-ui/index.html" target="_blank" class="alert-link">调用接口</a>发送消息
+		</div>
+		<div class="modal-dialog" style="width: 720px;margin: 30px auto;">
+			<div class="modal-content">
+				<div class="modal-header" style="text-align: center;">
+					<span style="float: left;">消息显示面板</span>
+					<span style="float: right;color: #4caf50;">当前帐号:<span id="current_account"></span></span>
+				</div>
+				<div class="modal-body"  id="messageList" style="max-height: 800px;height: 500px;overflow-y: auto;">
+				</div>
+
+				<div class="modal-input">
+					<div style=" width: 400px;margin: 0 auto;display: flex;justify-content: space-between;">
+						<input type="text" class="form-control" id="content" onKeyDown="sendKeyDown(event)" maxlength="32" placeholder="请输入发送内容" style="display: inline; width: 100%; height: 50px;" />
+						<button class="btn btn-success btn-lg" onclick="sendText()"> 发送 </button>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+
+</body>
+
+</html>

File diff ditekan karena terlalu besar
+ 6 - 0
src/main/resources/static/bootstrap-3.3.7-dist/css/bootstrap.min.css


TEMPAT SAMPAH
src/main/resources/static/bootstrap-3.3.7-dist/fonts/glyphicons-halflings-regular.eot


File diff ditekan karena terlalu besar
+ 288 - 0
src/main/resources/static/bootstrap-3.3.7-dist/fonts/glyphicons-halflings-regular.svg


TEMPAT SAMPAH
src/main/resources/static/bootstrap-3.3.7-dist/fonts/glyphicons-halflings-regular.ttf


TEMPAT SAMPAH
src/main/resources/static/bootstrap-3.3.7-dist/fonts/glyphicons-halflings-regular.woff


TEMPAT SAMPAH
src/main/resources/static/bootstrap-3.3.7-dist/fonts/glyphicons-halflings-regular.woff2


File diff ditekan karena terlalu besar
+ 7 - 0
src/main/resources/static/bootstrap-3.3.7-dist/js/bootstrap.min.js


+ 13 - 0
src/main/resources/static/bootstrap-3.3.7-dist/js/npm.js

@@ -0,0 +1,13 @@
+// This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment.
+require('../../js/transition.js');
+require('../../js/alert.js');
+require('../../js/button.js');
+require('../../js/carousel.js');
+require('../../js/collapse.js');
+require('../../js/dropdown.js');
+require('../../js/modal.js');
+require('../../js/tooltip.js');
+require('../../js/popover.js');
+require('../../js/scrollspy.js');
+require('../../js/tab.js');
+require('../../js/affix.js');

File diff ditekan karena terlalu besar
+ 1 - 0
src/main/resources/static/css/common.css


TEMPAT SAMPAH
src/main/resources/static/image/favicon.ico


TEMPAT SAMPAH
src/main/resources/static/image/icon.png


TEMPAT SAMPAH
src/main/resources/static/image/icon/kankan_icon.ico


File diff ditekan karena terlalu besar
+ 1 - 0
src/main/resources/static/image/icon/online.svg


TEMPAT SAMPAH
src/main/resources/static/image/icon_loading_small.gif


TEMPAT SAMPAH
src/main/resources/static/image/log.png


TEMPAT SAMPAH
src/main/resources/static/image/pattern.png


File diff ditekan karena terlalu besar
+ 7 - 0
src/main/resources/static/js/bootstrap.min.js


File diff ditekan karena terlalu besar
+ 1 - 0
src/main/resources/static/js/common.js


+ 230 - 0
src/main/resources/static/js/im/cim.web.sdk.js

@@ -0,0 +1,230 @@
+/*CIM服务器IP*/
+const CIM_HOST = '192.168.0.26' // window.location.hostname;
+/*
+ *服务端 websocket端口
+ */
+const CIM_PORT = 34567;
+const CIM_URI = "ws://" + CIM_HOST + ":" + CIM_PORT;
+
+const APP_VERSION = "1.0.0";
+const APP_CHANNEL = "web";
+const APP_PACKAGE = "webMessage";
+
+/*
+ *特殊的消息类型,代表被服务端强制下线
+ */
+const ACTION_999 = "999";
+const DATA_HEADER_LENGTH = 1;
+
+const MESSAGE = 2;
+const REPLY_BODY = 4;
+const SENT_BODY = 3;
+const PING = 1;
+const PONG = 0;
+/**
+ * PONG字符串转换后
+ * @type {Uint8Array}
+ */
+const PONG_BODY = new Uint8Array([80,79,78,71]);
+
+
+let socket;
+let manualStop = false;
+const CIMPushManager = {};
+CIMPushManager.connect = function () {
+    console.log('connect');
+    manualStop = false;
+    window.localStorage.account = '';
+    socket = new WebSocket(CIM_URI);
+    socket.cookieEnabled = false;
+    socket.binaryType = 'arraybuffer';
+    socket.onopen = CIMPushManager.innerOnConnectFinished;
+    socket.onmessage = CIMPushManager.innerOnMessageReceived;
+    socket.onclose = CIMPushManager.innerOnConnectionClosed;
+};
+
+CIMPushManager.bind = function (account) {
+
+    window.localStorage.account = account;
+
+    let deviceId = window.localStorage.deviceId;
+    if (deviceId === '' || deviceId === undefined) {
+        deviceId = generateUUID();
+        window.localStorage.deviceId = deviceId;
+    }
+
+    let browser = getBrowser();
+    let body = new proto.com.farsunset.cim.sdk.web.model.SentBody();
+    body.setKey("client_bind");
+    body.setTimestamp(new Date().getTime());
+    body.getDataMap().set("uid", account);
+    body.getDataMap().set("channel", APP_CHANNEL);
+    body.getDataMap().set("appVersion", APP_VERSION);
+    body.getDataMap().set("osVersion", browser.version);
+    body.getDataMap().set("packageName", APP_PACKAGE);
+    body.getDataMap().set("language", navigator.language);
+    body.getDataMap().set("deviceId", deviceId);
+    body.getDataMap().set("deviceName", browser.name);
+    CIMPushManager.sendRequest(body);
+};
+
+CIMPushManager.send = function (content) {
+    let uid = window.localStorage.account
+    let deviceId = window.localStorage.deviceId;
+    if (deviceId === '' || deviceId === undefined) {
+        deviceId = generateUUID();
+        window.localStorage.deviceId = deviceId;
+    }
+
+    let browser = getBrowser();
+    let body = new proto.com.farsunset.cim.sdk.web.model.SentBody();
+    body.setKey("client_biz");
+    body.setTimestamp(new Date().getTime());
+    body.getDataMap().set("uid", uid);
+    body.getDataMap().set("channel", APP_CHANNEL);
+    body.getDataMap().set("appVersion", APP_VERSION);
+    body.getDataMap().set("osVersion", browser.version);
+    body.getDataMap().set("packageName", APP_PACKAGE);
+    body.getDataMap().set("deviceId", deviceId);
+    body.getDataMap().set("content", content);
+    body.getDataMap().set("deviceName", browser.name);
+    body.getDataMap().set("language", navigator.language);
+    CIMPushManager.sendRequest(body);
+};
+
+CIMPushManager.stop = function () {
+    manualStop = true;
+    socket.close();
+};
+
+CIMPushManager.resume = function () {
+    manualStop = false;
+    console.log('resume')
+    CIMPushManager.connect();
+};
+
+
+CIMPushManager.innerOnConnectFinished = function () {
+    let account = window.localStorage.account;
+    if (account === '' || account === undefined) {
+        onConnectFinished();
+    } else {
+        CIMPushManager.bindAccount(account);
+    }
+};
+
+
+CIMPushManager.innerOnMessageReceived = function (e) {
+    let data = new Uint8Array(e.data);
+    let type = data[0];
+    let body = data.subarray(DATA_HEADER_LENGTH, data.length);
+
+    if (type === PING) {
+        CIMPushManager.pong();
+        return;
+    }
+
+    if (type === MESSAGE) {
+        let message = proto.com.farsunset.cim.sdk.web.model.Message.deserializeBinary(body);
+        onInterceptMessageReceived(message.toObject(false));
+        return;
+    }
+
+    if (type === REPLY_BODY) {
+        let message = proto.com.farsunset.cim.sdk.web.model.ReplyBody.deserializeBinary(body);
+        /**
+         * 将proto对象转换成json对象,去除无用信息
+         */
+        let reply = {};
+        reply.code = message.getCode();
+        reply.key = message.getKey();
+        reply.message = message.getMessage();
+        reply.timestamp = message.getTimestamp();
+        reply.data = {};
+
+        /**
+         * 注意,遍历map这里的参数 value在前key在后
+         */
+        message.getDataMap().forEach(function (v, k) {
+            reply.data[k] = v;
+        });
+
+        onReplyReceived(reply);
+    }
+};
+
+CIMPushManager.innerOnConnectionClosed = function (e) {
+    if (!manualStop) {
+        let time = Math.floor(Math.random() * (30 - 15 + 1) + 15);
+        setTimeout(function () {
+            console.log('innerOnConnectionClosed')
+            CIMPushManager.connect();
+        }, time);
+    }
+};
+
+CIMPushManager.sendRequest = function (body) {
+    let data = body.serializeBinary();
+    let protobuf = new Uint8Array(data.length + 1);
+    protobuf[0] = SENT_BODY;
+    protobuf.set(data, 1);
+    socket.send(protobuf);
+};
+
+CIMPushManager.pong = function () {
+    let pong =  new Uint8Array(PONG_BODY.byteLength + 1);
+    pong[0] = PONG;
+    pong.set(PONG_BODY,1);
+    socket.send(pong);
+};
+
+function onInterceptMessageReceived(message) {
+    /*
+     *被强制下线之后,不再继续连接服务端
+     */
+    if (message.action === ACTION_999) {
+        manualStop = true;
+    }
+    /*
+     *收到消息后,将消息发送给页面
+     */
+    if (onMessageReceived instanceof Function) {
+        onMessageReceived(message);
+    }
+}
+
+function getBrowser() {
+    let explorer = window.navigator.userAgent.toLowerCase();
+    if (explorer.indexOf("msie") >= 0) {
+        let ver = explorer.match(/msie ([\d.]+)/)[1];
+        return {name: "IE", version: ver};
+    }
+    else if (explorer.indexOf("firefox") >= 0) {
+        let ver = explorer.match(/firefox\/([\d.]+)/)[1];
+        return {name: "Firefox", version: ver};
+    }
+    else if (explorer.indexOf("chrome") >= 0) {
+        let ver = explorer.match(/chrome\/([\d.]+)/)[1];
+        return {name: "Chrome", version: ver};
+    }
+    else if (explorer.indexOf("opera") >= 0) {
+        let ver = explorer.match(/opera.([\d.]+)/)[1];
+        return {name: "Opera", version: ver};
+    }
+    else if (explorer.indexOf("Safari") >= 0) {
+        let ver = explorer.match(/version\/([\d.]+)/)[1];
+        return {name: "Safari", version: ver};
+    }
+
+    return {name: "Other", version: "1.0.0"};
+}
+
+function generateUUID() {
+    let d = new Date().getTime();
+    let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
+        let r = (d + Math.random() * 16) % 16 | 0;
+        d = Math.floor(d / 16);
+        return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
+    });
+    return uuid.replace(/-/g, '');
+}

File diff ditekan karena terlalu besar
+ 2774 - 0
src/main/resources/static/js/im/message.js


File diff ditekan karena terlalu besar
+ 2622 - 0
src/main/resources/static/js/im/replybody.js


File diff ditekan karena terlalu besar
+ 2568 - 0
src/main/resources/static/js/im/sentbody.js


File diff ditekan karena terlalu besar
+ 2 - 0
src/main/resources/static/js/jquery-3.3.1.min.js


File diff ditekan karena terlalu besar
+ 13 - 0
src/main/resources/static/js/jquery-ui.min.js