一套简单的web即时通讯——第一版

  前言

  我们之前已经实现了 WebSocket+Java 私聊、群聊实例,后面我们模仿layer弹窗,封装了一个自己的web弹窗 自定义web弹窗/层:简易风格的msg与可拖放的dialog,生成博客园文章目录弹窗,再后来就产生了将两者结合起来的想法,加上我们之前实现了一套自动生成代码的jpa究极进化版 SpringBoot系列——Spring-Data-JPA(究极进化版) 自动生成单表基础增、删、改、查接口,于是去网上搜索,参考即时通讯系统的消息存储如何建表,看下能不能把我们之前的东西稍微整合一下,将之前的写的东西应用起来学以致用,实现一套简单的web即时通讯,一版一版的升级完善。

  第一版功能

  1、实现简单的登录/注册

  2、将之前的页面改成自定义web弹窗的形式,并且完善群聊、私聊功能

 

  代码编写

  目前建了三个表,SQL如下:

  

/*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 50528
 Source Host           : localhost:3306
 Source Schema         : test

 Target Server Type    : MySQL
 Target Server Version : 50528
 File Encoding         : 65001

 Date: 09/05/2019 10:09:11
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for ims_friend
-- ----------------------------
DROP TABLE IF EXISTS `ims_friend`;
CREATE TABLE `ims_friend`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `user_id` int(11) NULL DEFAULT NULL COMMENT '用户id',
  `friend_id` int(11) NULL DEFAULT NULL COMMENT '好友id',
  `created_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `updata_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '好友表' ROW_FORMAT = Compact;

-- ----------------------------
-- Table structure for ims_friend_message
-- ----------------------------
DROP TABLE IF EXISTS `ims_friend_message`;
CREATE TABLE `ims_friend_message`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `from_user_id` int(11) NULL DEFAULT NULL COMMENT '发消息的人的id',
  `to_user_id` int(11) NULL DEFAULT NULL COMMENT '收消息的人的id',
  `content` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '消息内容',
  `is_read` int(11) NULL DEFAULT NULL COMMENT '是否已读,1是0否',
  `is_back` int(11) NULL DEFAULT NULL COMMENT '是否撤回,1是0否',
  `created_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `updata_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '好友消息表' ROW_FORMAT = Compact;

-- ----------------------------
-- Table structure for ims_user
-- ----------------------------
DROP TABLE IF EXISTS `ims_user`;
CREATE TABLE `ims_user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '帐号',
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
  `nick_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称',
  `gender` int(11) NULL DEFAULT 0 COMMENT '性别:0为男,1为女',
  `avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像',
  `email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '电子邮箱',
  `phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号码',
  `sign` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '个性签名',
  `created_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `updata_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户信息表' ROW_FORMAT = Compact;

SET FOREIGN_KEY_CHECKS = 1;
View Code

 

  自动生成代码,运行main方法执行

package cn.huanzi.ims.util;

import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * 自动生成代码
 */
public class CodeDOM {

    /**
     * 构造参数,出入表名
     */
    private CodeDOM(String tableName) {
        this.tableName = tableName;
        basePackage_ = "cn\\huanzi\\ims\\";
        package_ = basePackage_ + StringUtil.camelCaseName(tableName).toLowerCase() + "\\";
        //System.getProperty("user.dir") 获取的是项目所在路径
        basePath = System.getProperty("user.dir") + "\\src\\main\\java\\" + package_;
    }

    /**
     * 数据连接相关
     */
    private static final String URL = "jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&characterEncoding=utf-8";
    private static final String USERNAME = "root";
    private static final String PASSWORD = "123456";
    private static final String DRIVERCLASSNAME = "com.mysql.jdbc.Driver";
    /**
     * 表名
     */
    private String tableName;

    /**
     * 基础路径
     */
    private String basePackage_;
    private String package_;
    private String basePath;

    /**
     * 创建pojo实体类
     */
    private void createPojo(List<TableInfo> tableInfos) {
        File file = FileUtil.createFile(basePath + "pojo\\" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ".java");
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(
                "package " + package_.replaceAll("\\\\", ".") + "pojo;\n" +
                        "\n" +
                        "import lombok.Data;\n" +
                        "import javax.persistence.*;\n" +
                        "import java.io.Serializable;\n" +
                        "import java.util.Date;\n" +
                        "\n" +
                        "@Entity\n" +
                        "@Table(name = \"" + tableName + "\")\n" +
                        "@Data\n" +
                        "public class " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + " implements Serializable {\n"
        );
        //遍历设置属性
        for (TableInfo tableInfo : tableInfos) {
            //主键
            if ("PRI".equals(tableInfo.getColumnKey())) {
                stringBuffer.append("    @Id\n");
            }
            //自增
            if ("auto_increment".equals(tableInfo.getExtra())) {
                stringBuffer.append("    @GeneratedValue(strategy= GenerationType.IDENTITY)\n");
            }
            stringBuffer.append("    private " + StringUtil.typeMapping(tableInfo.getDataType()) + " " + StringUtil.camelCaseName(tableInfo.getColumnName()) + ";//" + tableInfo.getColumnComment() + "\n\n");
        }
        stringBuffer.append("}");
        FileUtil.fileWriter(file, stringBuffer);
    }

    /**
     * 创建vo类
     */
    private void createVo(List<TableInfo> tableInfos) {
        File file = FileUtil.createFile(basePath + "vo\\" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo.java");
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(
                "package " + package_.replaceAll("\\\\", ".") + "vo;\n" +
                        "\n" +
                        "import lombok.Data;\n" +
                        "import java.io.Serializable;\n" +
                        "import java.util.Date;\n" +
                        "\n" +
                        "@Data\n" +
                        "public class " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo implements Serializable {\n"
        );
        //遍历设置属性
        for (TableInfo tableInfo : tableInfos) {
            stringBuffer.append("    private " + StringUtil.typeMapping(tableInfo.getDataType()) + " " + StringUtil.camelCaseName(tableInfo.getColumnName()) + ";//" + tableInfo.getColumnComment() + "\n\n");
        }
        stringBuffer.append("}");
        FileUtil.fileWriter(file, stringBuffer);
    }

    /**
     * 创建repository类
     */
    private void createRepository(List<TableInfo> tableInfos) {
        File file = FileUtil.createFile(basePath + "repository\\" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Repository.java");
        StringBuffer stringBuffer = new StringBuffer();
        String t = "String";
        //遍历属性
        for (TableInfo tableInfo : tableInfos) {
            //主键
            if ("PRI".equals(tableInfo.getColumnKey())) {
                t = StringUtil.typeMapping(tableInfo.getDataType());
            }
        }
        stringBuffer.append(
                "package " + package_.replaceAll("\\\\", ".") + "repository;\n" +
                        "\n" +
                        "import " + basePackage_.replaceAll("\\\\", ".") + "common.repository.*;\n" +
                        "import " + package_.replaceAll("\\\\", ".") + "pojo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ";\n" +
                        "import org.springframework.stereotype.Repository;\n" +
                        "\n" +
                        "@Repository\n" +
                        "public interface " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Repository extends CommonRepository<" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ", " + t + "> {"
        );
        stringBuffer.append("\n");
        stringBuffer.append("}");
        FileUtil.fileWriter(file, stringBuffer);
    }

    /**
     * 创建service类
     */
    private void createService(List<TableInfo> tableInfos) {
        File file = FileUtil.createFile(basePath + "service\\" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Service.java");
        StringBuffer stringBuffer = new StringBuffer();
        String t = "String";
        //遍历属性
        for (TableInfo tableInfo : tableInfos) {
            //主键
            if ("PRI".equals(tableInfo.getColumnKey())) {
                t = StringUtil.typeMapping(tableInfo.getDataType());
            }
        }
        stringBuffer.append(
                "package " + package_.replaceAll("\\\\", ".") + "service;\n" +
                        "\n" +
                        "import " + basePackage_.replaceAll("\\\\", ".") + "common.service.*;\n" +
                        "import " + package_.replaceAll("\\\\", ".") + "pojo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ";\n" +
                        "import " + package_.replaceAll("\\\\", ".") + "vo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo;\n" +
                        "\n" +
                        "public interface " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Service extends CommonService<" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo, " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ", " + t + "> {"
        );
        stringBuffer.append("\n");
        stringBuffer.append("}");
        FileUtil.fileWriter(file, stringBuffer);

        //Impl
        File file1 = FileUtil.createFile(basePath + "service\\" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "ServiceImpl.java");
        StringBuffer stringBuffer1 = new StringBuffer();
        stringBuffer1.append(
                "package " + package_.replaceAll("\\\\", ".") + "service;\n" +
                        "\n" +
                        "import " + basePackage_.replaceAll("\\\\", ".") + "common.service.*;\n" +
                        "import " + package_.replaceAll("\\\\", ".") + "pojo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ";\n" +
                        "import " + package_.replaceAll("\\\\", ".") + "vo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo;\n" +
                        "import " + package_.replaceAll("\\\\", ".") + "repository." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Repository;\n" +
                        "import org.springframework.beans.factory.annotation.Autowired;\n" +
                        "import org.springframework.stereotype.Service;\n" +
                        "import org.springframework.transaction.annotation.Transactional;\n" +
                        "import javax.persistence.EntityManager;\n" +
                        "import javax.persistence.PersistenceContext;\n" +
                        "\n" +
                        "@Service\n" +
                        "@Transactional\n" +
                        "public class " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "ServiceImpl extends CommonServiceImpl<" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo, " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ", " + t + "> implements " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Service{"
        );
        stringBuffer1.append("\n\n");
        stringBuffer1.append(
                "    @PersistenceContext\n" +
                        "    private EntityManager em;\n");

        stringBuffer1.append("" +
                "    @Autowired\n" +
                "    private " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Repository " + StringUtil.camelCaseName(tableName) + "Repository;\n");
        stringBuffer1.append("}");
        FileUtil.fileWriter(file1, stringBuffer1);
    }

    /**
     * 创建controller类
     */
    private void createController(List<TableInfo> tableInfos) {
        File file = FileUtil.createFile(basePath + "controller\\" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Controller.java");
        StringBuffer stringBuffer = new StringBuffer();
        String t = "String";
        //遍历属性
        for (TableInfo tableInfo : tableInfos) {
            //主键
            if ("PRI".equals(tableInfo.getColumnKey())) {
                t = StringUtil.typeMapping(tableInfo.getDataType());
            }
        }
        stringBuffer.append(
                "package " + package_.replaceAll("\\\\", ".") + "controller;\n" +
                        "\n" +
                        "import " + basePackage_.replaceAll("\\\\", ".") + "common.controller.*;\n" +
                        "import " + package_.replaceAll("\\\\", ".") + "pojo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ";\n" +
                        "import " + package_.replaceAll("\\\\", ".") + "vo." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo;\n" +
                        "import " + package_.replaceAll("\\\\", ".") + "service." + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Service;\n" +
                        "import org.springframework.beans.factory.annotation.Autowired;\n" +
                        "import org.springframework.web.bind.annotation.*;\n" +
                        "\n" +
                        "@RestController\n" +
                        "@RequestMapping(\"/" + StringUtil.camelCaseName(tableName) + "/\")\n" +
                        "public class " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Controller extends CommonController<" + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Vo, " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + ", " + t + "> {"
        );
        stringBuffer.append("\n");
        stringBuffer.append("" +
                "    @Autowired\n" +
                "    private " + StringUtil.captureName(StringUtil.camelCaseName(tableName)) + "Service " + StringUtil.camelCaseName(tableName) + "Service;\n");
        stringBuffer.append("}");
        FileUtil.fileWriter(file, stringBuffer);
    }


    /**
     * 获取表结构信息
     *
     * @return list
     */
    private List<TableInfo> getTableInfo() {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        ArrayList<TableInfo> list = new ArrayList<>();
        try {
            conn = DBConnectionUtil.getConnection();
            String sql = "select column_name,data_type,column_comment,column_key,extra from information_schema.columns where table_name=?";
            ps = conn.prepareStatement(sql);
            ps.setString(1, tableName);
            rs = ps.executeQuery();
            while (rs.next()) {
                TableInfo tableInfo = new TableInfo();
                //列名,全部转为小写
                tableInfo.setColumnName(rs.getString("column_name").toLowerCase());
                //列类型
                tableInfo.setDataType(rs.getString("data_type"));
                //列注释
                tableInfo.setColumnComment(rs.getString("column_comment"));
                //主键
                tableInfo.setColumnKey(rs.getString("column_key"));
                //主键类型
                tableInfo.setExtra(rs.getString("extra"));
                list.add(tableInfo);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            assert rs != null;
            DBConnectionUtil.close(conn, ps, rs);
        }
        return list;
    }

    /**
     * file工具类
     */
    private static class FileUtil {
        /**
         * 创建文件
         *
         * @param pathNameAndFileName 路径跟文件名
         * @return File对象
         */
        private static File createFile(String pathNameAndFileName) {
            File file = new File(pathNameAndFileName);
            try {
                //获取父目录
                File fileParent = file.getParentFile();
                if (!fileParent.exists()) {
                    fileParent.mkdirs();
                }
                //创建文件
                if (!file.exists()) {
                    file.createNewFile();
                }
            } catch (Exception e) {
                file = null;
                System.err.println("新建文件操作出错");
                e.printStackTrace();
            }
            return file;
        }

        /**
         * 字符流写入文件
         *
         * @param file         file对象
         * @param stringBuffer 要写入的数据
         */
        private static void fileWriter(File file, StringBuffer stringBuffer) {
            //字符流
            try {
                FileWriter resultFile = new FileWriter(file, true);//true,则追加写入 false,则覆盖写入
                PrintWriter myFile = new PrintWriter(resultFile);
                //写入
                myFile.println(stringBuffer.toString());

                myFile.close();
                resultFile.close();
            } catch (Exception e) {
                System.err.println("写入操作出错");
                e.printStackTrace();
            }
        }
    }

    /**
     * 字符串处理工具类
     */
    private static class StringUtil {
        /**
         * 数据库类型->JAVA类型
         *
         * @param dbType 数据库类型
         * @return JAVA类型
         */
        private static String typeMapping(String dbType) {
            String javaType = "";
            if ("int|integer".contains(dbType)) {
                javaType = "Integer";
            } else if ("float|double|decimal|real".contains(dbType)) {
                javaType = "Double";
            } else if ("date|time|datetime|timestamp".contains(dbType)) {
                javaType = "Date";
            } else {
                javaType = "String";
            }
            return javaType;
        }

        /**
         * 驼峰转换为下划线
         */
        public static String underscoreName(String camelCaseName) {
            StringBuilder result = new StringBuilder();
            if (camelCaseName != null && camelCaseName.length() > 0) {
                result.append(camelCaseName.substring(0, 1).toLowerCase());
                for (int i = 1; i < camelCaseName.length(); i++) {
                    char ch = camelCaseName.charAt(i);
                    if (Character.isUpperCase(ch)) {
                        result.append("_");
                        result.append(Character.toLowerCase(ch));
                    } else {
                        result.append(ch);
                    }
                }
            }
            return result.toString();
        }

        /**
         * 首字母大写
         */
        public static String captureName(String name) {
            char[] cs = name.toCharArray();
            cs[0] -= 32;
            return String.valueOf(cs);

        }

        /**
         * 下划线转换为驼峰
         */
        public static String camelCaseName(String underscoreName) {
            StringBuilder result = new StringBuilder();
            if (underscoreName != null && underscoreName.length() > 0) {
                boolean flag = false;
                for (int i = 0; i < underscoreName.length(); i++) {
                    char ch = underscoreName.charAt(i);
                    if ("_".charAt(0) == ch) {
                        flag = true;
                    } else {
                        if (flag) {
                            result.append(Character.toUpperCase(ch));
                            flag = false;
                        } else {
                            result.append(ch);
                        }
                    }
                }
            }
            return result.toString();
        }
    }

    /**
     * JDBC连接数据库工具类
     */
    private static class DBConnectionUtil {

        {
            // 1、加载驱动
            try {
                Class.forName(DRIVERCLASSNAME);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }

        /**
         * 返回一个Connection连接
         *
         * @return
         */
        public static Connection getConnection() {
            Connection conn = null;
            // 2、连接数据库
            try {
                conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return conn;
        }

        /**
         * 关闭Connection,Statement连接
         *
         * @param conn
         * @param stmt
         */
        public static void close(Connection conn, Statement stmt) {
            try {
                conn.close();
                stmt.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        /**
         * 关闭Connection,Statement,ResultSet连接
         *
         * @param conn
         * @param stmt
         * @param rs
         */
        public static void close(Connection conn, Statement stmt, ResultSet rs) {
            try {
                close(conn, stmt);
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

    }

    /**
     * 表结构行信息实体类
     */
    private class TableInfo {
        private String columnName;
        private String dataType;
        private String columnComment;
        private String columnKey;
        private String extra;

        TableInfo() {
        }

        String getColumnName() {
            return columnName;
        }

        void setColumnName(String columnName) {
            this.columnName = columnName;
        }

        String getDataType() {
            return dataType;
        }

        void setDataType(String dataType) {
            this.dataType = dataType;
        }

        String getColumnComment() {
            return columnComment;
        }

        void setColumnComment(String columnComment) {
            this.columnComment = columnComment;
        }

        String getColumnKey() {
            return columnKey;
        }

        void setColumnKey(String columnKey) {
            this.columnKey = columnKey;
        }

        String getExtra() {
            return extra;
        }

        void setExtra(String extra) {
            this.extra = extra;
        }
    }

    /**
     * 快速创建,供外部调用,调用之前先设置一下项目的基础路径
     */
    private String create() {
        List<TableInfo> tableInfo = getTableInfo();
        createPojo(tableInfo);
        createVo(tableInfo);
        createRepository(tableInfo);
        createService(tableInfo);
        createController(tableInfo);
        return tableName + " 后台代码生成完毕!";
    }

    public static void main(String[] args) {
        String[] tables = {"ims_user", "ims_friend", "ims_friend_message"};
        for (int i = 0; i < tables.length; i++) {
            String msg = new CodeDOM(tables[i]).create();
            System.out.println(msg);
        }
    }
}
CodeDOM.java

 

  工程结构

  工程是一个springboot项目,跟我们之前一样使用lombok、thymeleaf等

  tip.js、tip.css放在static的js、css里面

 

  我们在ims_user表生成的controller、service层新增登录、登出等几个接口

@RestController
@RequestMapping("/imsUser/")
public class ImsUserController extends CommonController<ImsUserVo, ImsUser, Integer> {
    @Autowired
    private ImsUserService imsUserService;

    /**
     * 跳转登录、注册页面
     */
    @RequestMapping("loginPage.html")
    public ModelAndView loginPage() {
        return new ModelAndView("login.html");
    }

    /**
     * 跳转聊天页面
     */
    @RequestMapping("socketChart/{username}.html")
    public ModelAndView socketChartPage(@PathVariable String username) {
        return new ModelAndView("socketChart.html","username",username);
    }

    /**
     * 登录
     */
    @PostMapping("login")
    public Result<ImsUserVo> login(ImsUserVo userVo) {
        //加密后再去对比密文
        userVo.setPassword(MD5Util.getMD5(userVo.getPassword()));
        Result<List<ImsUserVo>> result = list(userVo);
        if(result.isFlag() && result.getData().size() > 0){
            ImsUserVo imsUserVo = result.getData().get(0);
            //置空隐私信息
            imsUserVo.setPassword(null);

            //add WebSocketServer.loginList
            WebSocketServer.loginList.add(imsUserVo.getUserName());
            return Result.of(imsUserVo);
        }else{
            return Result.of(null,false,"账号或密码错误!");
        }
    }

    /**
     * 登出
     */
    @RequestMapping("logout/{username}")
    public String loginOut(HttpServletRequest request, @PathVariable String username) {
        new WebSocketServer().deleteUserByUsername(username);
        return "退出成功!";
    }

    /**
     * 获取在线用户
     */
    @PostMapping("getOnlineList")
    private List<String> getOnlineList(String username) {
        List<String> list = new ArrayList<String>();
        //遍历webSocketMap
        for (Map.Entry<String, Session> entry : WebSocketServer.getSessionMap().entrySet()) {
            if (!entry.getKey().equals(username)) {
                list.add(entry.getKey());
            }
        }
        return list;
    }
}
ImsUserController.java
@Service
@Transactional
public class ImsUserServiceImpl extends CommonServiceImpl<ImsUserVo, ImsUser, Integer> implements ImsUserService{

    @PersistenceContext
    private EntityManager em;
    @Autowired
    private ImsUserRepository imsUserRepository;

    @Override
    public Result<ImsUserVo> save(ImsUserVo entityVo) {
        //先查询是否已经存在相同账号
        ImsUserVo imsUserVo = new ImsUserVo();
        imsUserVo.setUserName(entityVo.getUserName());
        if(list(imsUserVo).getData().size() > 0){
            return Result.of(null,false,"账号已存在!");
        }
        //存储密文
        entityVo.setPassword(MD5Util.getMD5(entityVo.getPassword()));
        return super.save(entityVo);
    }
}
ImsUserServiceImpl.java

 

  WebSocketServer也有优化调整

package cn.huanzi.ims.socket;

import cn.huanzi.ims.imsuser.service.ImsUserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * WebSocket服务
 */
@RestController
@RequestMapping("/websocket")
@ServerEndpoint(value = "/websocket/{username}", configurator = MyEndpointConfigure.class)
public class WebSocketServer {

    /**
     * 在线人数
     */
    private static int onlineCount = 0;

    /**
     * 在线用户的Map集合,key:用户名,value:Session对象
     */
    private static Map<String, Session> sessionMap = new HashMap<String, Session>();

    /**
     * 登录用户集合
     */
    public static List<String> loginList = new ArrayList<>();

    public static Map<String, Session> getSessionMap(){
        return sessionMap;
    }

    /**
     * 注入其他类(换成自己想注入的对象)
     */
    private ImsUserService imsUserService;

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("username") String username) {
        //在webSocketMap新增上线用户
        sessionMap.put(username, session);

        //在线人数加加
        WebSocketServer.onlineCount++;

        //通知除了自己之外的所有人
        sendOnlineCount(username, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + username + "'}");
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(Session session) {
        //下线用户名
        String logoutUserName = "";

        //从webSocketMap删除下线用户
        for (Entry<String, Session> entry : sessionMap.entrySet()) {
            if (entry.getValue() == session) {
                sessionMap.remove(entry.getKey());
                logoutUserName = entry.getKey();
                break;
            }
        }
        deleteUserByUsername(logoutUserName);
    }

    /**
     * 服务器接收到客户端消息时调用的方法
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        try {
            //JSON字符串转 HashMap
            HashMap hashMap = new ObjectMapper().readValue(message, HashMap.class);

            //消息类型
            String type = (String) hashMap.get("type");

            //来源用户
            Map srcUser = (Map) hashMap.get("srcUser");

            //目标用户
            Map tarUser = (Map) hashMap.get("tarUser");

            //如果点击的是自己,那就是群聊
            if (srcUser.get("username").equals(tarUser.get("username"))) {
                //群聊
                groupChat(session,hashMap);
            } else {
                //私聊
                privateChat(session, tarUser, hashMap);
            }

            //后期要做消息持久化

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 发生错误时调用
     */
    @OnError
    public void onError(Session session, Throwable error) {
        error.printStackTrace();
    }

    /**
     * 通知除了自己之外的所有人
     */
    private void sendOnlineCount(String username, String message) {
        for (Entry<String, Session> entry : sessionMap.entrySet()) {
            try {
                if (entry.getKey() != username) {
                    entry.getValue().getBasicRemote().sendText(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 私聊
     */
    private void privateChat(Session session, Map tarUser, HashMap hashMap) throws IOException {
        //获取目标用户的session
        Session tarUserSession = sessionMap.get(tarUser.get("username"));

        //如果不在线则发送“对方不在线”回来源用户
        if (tarUserSession == null) {
            session.getBasicRemote().sendText("{\"type\":\"0\",\"message\":\"对方不在线\"}");
        } else {
            hashMap.put("type", "1");
            tarUserSession.getBasicRemote().sendText(new ObjectMapper().writeValueAsString(hashMap));
        }
    }

    /**
     * 群聊
     */
    private void groupChat(Session session, HashMap hashMap) throws IOException {
        for (Entry<String, Session> entry : sessionMap.entrySet()) {
            //自己就不用再发送消息了
            if (entry.getValue() != session) {
                hashMap.put("type", "2");
                entry.getValue().getBasicRemote().sendText(new ObjectMapper().writeValueAsString(hashMap));
            }
        }
    }

    /**
        删除用户
     */
    public void deleteUserByUsername(String username){
        //在线人数减减
        WebSocketServer.onlineCount--;

        WebSocketServer.loginList.remove(username);

        //通知除了自己之外的所有人
        sendOnlineCount(username, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + username + "'}");
    }

}
View Code

 

  先看一下我们的自定义web弹窗的js、css,跟之前相比有一点小升级

/* web弹窗 */
.tip-msg {
    background-color: rgba(61, 61, 61, 0.93);
    color: #ffffff;
    opacity: 0;
    max-width: 200px;
    position: fixed;
    text-align: center;
    line-height: 25px;
    border-radius: 30px;
    padding: 5px 15px;
    display: inline-block;
    z-index: 10000;
}

.tip-shade {
    z-index: 9999;
    background-color: rgb(0, 0, 0);
    opacity: 0.6;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

.tip-dialog {
    z-index: 9999;
    position: fixed;
    display: block;
    background: #e9e9e9;
    border-radius: 5px;
    opacity: 0;
    border: 1px solid #dad8d8;
    box-shadow: 0px 1px 20px 2px rgb(255, 221, 221);
}

.tip-title {
    cursor: move;
    padding: 5px;
    position: relative;
    height: 25px;
    border-bottom: 1px solid #dad8d8;
    user-select: none;
}

.tip-title-text {
    margin: 0;
    padding: 0;
    font-size: 15px;
}

.tip-title-btn {
    position: absolute;
    top: 5px;
    right: 5px;
}

.tip-content {
    padding: 8px;
    position: relative;
    word-break: break-all;
    font-size: 14px;
    overflow-x: hidden;
    overflow-y: auto;
}

.tip-resize {
    position: absolute;
    width: 15px;
    height: 15px;
    right: 0;
    bottom: 0;
    cursor: se-resize;
}
tip.css
/**
 * 自定义web弹窗/层:简易风格的msg与可拖放的dialog
 * 依赖jquery
 */
var tip = {

    /**
     * 初始化
     */
    init: function () {
        var titleDiv = null;//标题元素
        var dialogDiv = null;//窗口元素
        var titleDown = false;//是否在标题元素按下鼠标
        var resizeDown = false;//是否在缩放元素按下鼠标
        var offset = {x: 0, y: 0};//鼠标按下时的坐标系/计算后的坐标
        /*
            使用 on() 方法添加的事件处理程序适用于当前及未来的元素(比如由脚本创建的新元素)。
            问题:事件绑定在div上出现div移动速度跟不上鼠标速度,导致鼠标移动太快时会脱离div,从而无法触发事件。
            解决:把事件绑定在document文档上,无论鼠标在怎么移动,始终是在文档范围之内。
        */
        //鼠标在标题元素按下
        $(document).on("mousedown", ".tip-title", function (e) {
            var event1 = e || window.event;
            titleDiv = $(this);
            dialogDiv = titleDiv.parent();
            titleDown = true;
            offset.x = e.clientX - parseFloat(dialogDiv.css("left"));
            offset.y = e.clientY - parseFloat(dialogDiv.css("top"));
        });
        //鼠标移动
        $(document).on("mousemove", function (e) {
            var event2 = e || window.event;
            var eveX = event2.clientX;             // 获取鼠标相对于浏览器x轴的位置
            var eveY = event2.clientY;             // 获取鼠标相对于浏览器Y轴的位置
            // var height = document.body.clientHeight;//表示HTML文档所在窗口的当前高度;
            // var width = document.body.clientWidth;//表示HTML文档所在窗口的当前宽度;
            var height = window.innerHeight;//浏览器窗口的内部高度;
            var width = window.innerWidth;//浏览器窗口的内部宽度;

            //在标题元素按下
            if (titleDown) {

                //处理滚动条
                if (tip.hasXScrollbar()) {
                    height = height - tip.getScrollbarWidth();
                }
                if (tip.hasYScrollbar()) {
                    width = width - tip.getScrollbarWidth();
                }

                //上边
                var top = (eveY - offset.y);
                if (top <= 0) {
                    top = 0;
                }
                if (top >= (height - dialogDiv.height())) {
                    top = height - dialogDiv.height() - 5;
                }

                //左边
                var left = (eveX - offset.x);
                if (left <= 0) {
                    left = 0;
                }
                if (left >= (width - dialogDiv.width())) {
                    left = width - dialogDiv.width() - 5;
                }
                dialogDiv.css({
                    "top": top + "px",
                    "left": left + "px"
                });
            }

            //在缩放元素按下
            if (resizeDown) {
                var newWidth = (dialogDiv.resize.width + (eveX - offset.x));
                if (dialogDiv.resize.initWidth >= newWidth) {
                    newWidth = dialogDiv.resize.initWidth;
                }
                var newHeight = (dialogDiv.resize.height + (eveY - offset.y));
                if (dialogDiv.resize.initHeight >= newHeight) {
                    newHeight = dialogDiv.resize.initHeight;
                }

                dialogDiv.css("width", newWidth + "px");
                dialogDiv.find(".tip-content").css("height", newHeight + "px");
            }
        });
        //鼠标弹起
        $(document).on("mouseup", function (e) {
            //清空对象
            titleDown = false;
            resizeDown = false;
            titleDiv = null;
            dialogDiv = null;
            offset = {x: 0, y: 0};
        });
        //阻止按钮事件冒泡
        $(document).on("mousedown", ".tip-title-min,.tip-title-max,.tip-title-close", function (e) {
            e.stopPropagation();//阻止事件冒泡
        });
        //最小化
        $(document).on("click", ".tip-title-min", function (e) {
            // var height = document.body.clientHeight;//表示HTML文档所在窗口的当前高度;
            // var width = document.body.clientWidth;//表示HTML文档所在窗口的当前宽度;
            var height = window.innerHeight;//浏览器窗口的内部高度;
            var width = window.innerWidth;//浏览器窗口的内部宽度;
            var $parent = $(this).parents(".tip-dialog");
            //显示浏览器滚动条
            document.body.parentNode.style.overflowY = "auto";

            //当前是否为最大化
            if ($parent[0].isMax) {
                $parent[0].isMax = false;
                $parent.css({
                    "top": $parent[0].topMin,
                    "left": $parent[0].leftMin,
                    "height": $parent[0].heightMin,
                    "width": $parent[0].widthMin
                });
            }
            //当前是否为最小化
            if (!$parent[0].isMin) {
                $parent[0].isMin = true;
                $parent[0].bottomMin = $parent.css("bottom");
                $parent[0].leftMin = $parent.css("left");
                $parent[0].heightMin = $parent.css("height");
                $parent[0].widthMin = $parent.css("width");
                $parent.css({
                    "top": "",
                    "bottom": "5px",
                    "left": 0,
                    "height": "30px",
                    "width": "95px"
                });
                $parent.find(".tip-title-text").css("display", "none");
                $parent.find(".tip-content").css("display", "none");
            } else {
                $parent[0].isMin = false;
                $parent.css({
                    "top": $parent[0].topMin,
                    "bottom": $parent[0].bottomMin,
                    "left": $parent[0].leftMin,
                    "height": $parent[0].heightMin,
                    "width": $parent[0].widthMin
                });
                $parent.find(".tip-title-text").css("display", "block");
                $parent.find(".tip-content").css("display", "block");
            }
        });
        //最大化
        $(document).on("click", ".tip-title-max", function (e) {
            // var height = document.body.clientHeight;//表示HTML文档所在窗口的当前高度;
            // var width = document.body.clientWidth;//表示HTML文档所在窗口的当前宽度;
            var height = window.innerHeight;//浏览器窗口的内部高度;
            var width = window.innerWidth;//浏览器窗口的内部宽度;
            var $parent = $(this).parents(".tip-dialog");
            //当前是否为最小化
            if ($parent[0].isMin) {
                $parent[0].isMin = false;
                $parent.css({
                    "top": $parent[0].topMin,
                    "bottom": $parent[0].bottomMin,
                    "left": $parent[0].leftMin,
                    "height": $parent[0].heightMin,
                    "width": $parent[0].widthMin
                });
                $parent.find(".tip-title h2").css("display", "block");
            }
            //当前是否为最大化
            if (!$parent[0].isMax) {
                //隐藏浏览器滚动条
                document.body.parentNode.style.overflowY = "hidden";
                $parent[0].isMax = true;
                $parent[0].topMin = $parent.css("top");
                $parent[0].leftMin = $parent.css("left");
                $parent[0].heightMin = $parent.css("height");
                $parent[0].widthMin = $parent.css("width");
                $parent.css({
                    "top": 0,
                    "left": 0,
                    "height": height - 5 + "px",
                    "width": width - 5 + "px"
                });
            } else {
                //显示浏览器滚动条
                document.body.parentNode.style.overflowY = "auto";
                $parent[0].isMax = false;
                $parent.css({
                    "top": $parent[0].topMin,
                    "left": $parent[0].leftMin,
                    "height": $parent[0].heightMin,
                    "width": $parent[0].widthMin
                });
            }
        });
        //缩放
        $(document).on("mousedown", ".tip-resize", function (e) {
            var event1 = e || window.event;
            dialogDiv = $(this).parent();
            resizeDown = true;
            offset.x = e.clientX;
            offset.y = e.clientY;
            //点击时的宽高
            dialogDiv.resize.width = dialogDiv.width();
            dialogDiv.resize.height = dialogDiv.find(".tip-content").height();
        });
        //关闭
        $(document).on("click", ".tip-title-close", function (e) {
            $(this).parents(".tip-dialog").parent().remove();
            //显示浏览器滚动条
            document.body.parentNode.style.overflowY = "auto";
        });
        //点击窗口优先显示
        $(document).on("click", ".tip-dialog", function (e) {
            $(".tip-dialog").css("z-index","9999");
            $(this).css("z-index","10000");
        });
    },

    /**
     * 是否存在X轴方向滚动条
     */
    hasXScrollbar: function () {
        return document.body.scrollWidth > (window.innerWidth || document.documentElement.clientWidth);
    },

    /**
     * 是否存在Y轴方向滚动条
     */
    hasYScrollbar: function () {
        return document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight);
    },

    /**
     * 计算滚动条的宽度
     */
    getScrollbarWidth: function () {
        /*
            思路:生成一个带滚动条的div,分析得到滚动条长度,然后过河拆桥
         */
        var scrollDiv = document.createElement("div");
        scrollDiv.style.cssText = 'width: 99px; height: 99px; overflow: scroll; position: absolute; top: -9999px;';
        document.body.appendChild(scrollDiv);
        var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
        document.body.removeChild(scrollDiv);

        return scrollbarWidth;

    },

    /**
     * tip提示
     * tip.msg("哈哈哈哈哈");
     * tip.msg({text:"哈哈哈哈哈",time:5000});
     */
    msg: function (setting) {
        var time = setting.time || 2000; // 显示时间(毫秒) 默认延迟2秒关闭
        var text = setting.text || setting; // 文本内容

        //组装HTML
        var tip = "<div class='tip tip-msg'>"
            + text +
            "</div>";

        //删除旧tip
        $(".tip-msg").remove();

        //添加到body
        $("body").append(tip);

        //获取jq对象
        var $tip = $(".tip-msg");

        //动画过渡
        $tip.animate({opacity: 1}, 500);

        //计算位置浏览器窗口上下、左右居中
        // var height = document.body.clientHeight;//表示HTML文档所在窗口的当前高度;
        var width = document.body.clientWidth;//表示HTML文档所在窗口的当前宽度;
        var height = window.innerHeight;//浏览器窗口的内部高度;
        // var width = window.innerWidth;//浏览器窗口的内部宽度;
        width = ((width / 2) - ($tip.css("width").replace("px", "") / 2)) / width;
        height = ((height / 2) - ($tip.css("height").replace("px", "") / 2)) / height;
        $tip.css({
            "top": (height * 100) + "%",
            "left": (width * 100) + "%"
        });

        //延迟删除
        setTimeout(function () {
            //动画过渡
            $tip.animate({opacity: 0}, 500, function () {
                $tip.remove();
            });
        }, time);
    },

    /**
     * 可拖放窗口
     * tip.dialog({title:"测试弹窗标题",content:"测试弹窗内容"});
     * tip.dialog({title:"测试弹窗标题",class:"myClassName",content:"<h1>测试弹窗内容</h1>",offset: ['100px', '50px'],area:["200px","100px"],shade:0,closeCallBack:function(){console.log('你点击了关闭按钮')}});
     */
    dialog: function (setting) {
        var title = setting.title || "这里是标题"; // 标题
        var clazz = setting.class || ""; // class
        var content = setting.content || "这里是内容"; // 内容
        var area = setting.area; // 宽高
        var offset = setting.offset || "auto"; // 位置 上、左
        var shade = setting.shade !== undefined ? setting.shade : 0.7;//遮阴 为0时无遮阴对象

        //组装HTML
        var tip = "<div>\n" +
            "    <!-- 遮阴层 -->\n" +
            "    <div class=\"tip tip-shade\"></div>\n" +
            "    <!-- 主体 -->\n" +
            "    <div class=\"tip tip-dialog " + clazz + "\">\n" +
            "        <!-- 标题 -->\n" +
            "        <div class=\"tip tip-title\">\n" +
            "            <h2 class=\"tip tip-title-text\"></h2>\n" +
            "            <div class=\"tip tip-title-btn\">\n" +
            "                <button class=\"tip tip-title-min\" title=\"最小化\">--</button>\n" +
            "                <button class=\"tip tip-title-max\" title=\"最大化\">O</button>\n" +
            "                <button class=\"tip tip-title-close\" title=\"关闭\">X</button>\n" +
            "            </div>\n" +
            "        </div>\n" +
            "        <!-- 窗口内容 -->\n" +
            "        <div class=\"tip tip-content\"></div>\n" +
            "        <!-- 右下角改变窗口大小 -->\n" +
            "        <div class=\"tip tip-resize\"></div>\n" +
            "    </div>\n" +
            "</div>";

        var $tip = $(tip);

        //添加到body
        $("body").append($tip);

        //设置遮阴
        $tip.find(".tip-shade").css("opacity", shade);
        if (shade === 0) {
            $tip.find(".tip-shade").css({
                "width": "0",
                "height": "0"
            });
        }

        //获取dialog对象
        $tip = $tip.find(".tip-dialog");

        //标题
        $tip.find(".tip-title-text").html(title);

        //内容
        $tip.find(".tip-content").append(content);

        //设置初始宽高
        if (area) {
            $tip.css({
                "width": area[0],
            });
            $tip.find(".tip-content").css({
                "height": area[1]
            });
        }

        //动画过渡
        $tip.animate({opacity: 1}, 500);

        //计算位置浏览器窗口上下、左右居中
        if (offset === "auto") {
            // var height = document.body.clientHeight;//表示HTML文档所在窗口的当前高度;
            var width = document.body.clientWidth;//表示HTML文档所在窗口的当前宽度;
            var height = window.innerHeight;//浏览器窗口的内部高度;
            // var width = window.innerWidth;//浏览器窗口的内部宽度;
            width = ((width / 2) - ($tip.css("width").replace("px", "") / 2)) / width;
            height = ((height / 2) - ($tip.css("height").replace("px", "") / 2)) / height;
            $tip.css({
                "top": (height * 100) + "%",
                "left": (width * 100) + "%"
            });
        } else if (Array.isArray(offset)) {
            $tip.css({
                "top": offset[0],
                "left": offset[1]
            });
        }

        //初始值宽高
        $tip.resize.initWidth = $tip.width();
        $tip.resize.initHeight = $tip.find(".tip-content").height();

        //绑定关闭回调
        if(setting.closeCallBack){
            $(".tip-title-close").click(function (e) {
                setting.closeCallBack();
            });
        }
    }
};

//初始化
tip.init();
tip.js

 

  接下来就是登录/注册页面跟聊天页面的HTML、JS的修改,我们先定义一个head.html作为一个公用head在其他地方引入

<!--此页面用于放置页面的公共片段(fragment)-->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="static">
    <head>
        <script th:inline="javascript">
            //项目根路径
            // ctx = /*[[@{/}]]*/'';
            ctx = [[${#request.getContextPath()}]];//应用路径
        </script>
    </head>

    <head>
        <!-- jquery -->
        <script th:src="@{/js/jquery.min.js}"></script>

        <!-- 自定义web弹窗 CSS、JS 文件 -->
        <link th:href="@{/css/tip.css}" rel="stylesheet" type="text/css"/>
        <script th:src="@{/js/tip.js}"></script>
    </head>

    <head>
        <style>
            body, html {
                margin: 0;
                padding: 0;
                background: #898f92;
            }
        </style>
    </head>

    <head>
        <script>
            /**
             * 拓展表单对象:用于将对象序列化为JSON对象
             */
            $.fn.serializeObject = function () {
                var o = {};
                var a = this.serializeArray();
                $.each(a, function () {
                    if (o[this.name]) {
                        if (!o[this.name].push) {
                            o[this.name] = [o[this.name]];
                        }
                        o[this.name].push(this.value || '');
                    } else {
                        o[this.name] = this.value || '';
                    }
                });
                return o;
            };

            /**
             * 表单自动回显
             * 依赖jqury
             * 使用参考:$("#form1").form({"id":"112","username":"ff","password":"111","type":"admin"});
             */
            $.fn.form = function (data) {
                var form = $(this);
                for (var i in data) {
                    var name = i;
                    var value = data[i];
                    if (name !== "" && value !== "") {
                        valuAtion(name, value);
                    }
                }

                function valuAtion(name, value) {
                    if (form.length < 1) {
                        return;
                    }
                    if (form.find("[name='" + name + "']").length < 1) {
                        return;
                    }
                    var input = form.find("[name='" + name + "']")[0];
                    if ($.inArray(input.type, ["text", "password", "hidden", "select-one", "textarea"]) > -1) {
                        $(input).val(value);
                    } else if (input.type == " " || input.type == "checkbox") {
                        form.find("[name='" + name + "'][value='" + value + "']").attr("checked", true);
                    }
                }

            };

            /**
             * 常用工具方法
             */
            commonUtil = {
                /**
                 * 获取当前时间,并格式化输出为:2018-05-18 14:21:46
                 * @returns {string}
                 */
                getNowTime: function () {
                    var time = new Date();
                    var year = time.getFullYear();//获取年
                    var month = time.getMonth() + 1;//或者月
                    var day = time.getDate();//或者天

                    var hour = time.getHours();//获取小时
                    var minu = time.getMinutes();//获取分钟
                    var second = time.getSeconds();//或者秒
                    var data = year + "-";
                    if (month < 10) {
                        data += "0";
                    }
                    data += month + "-";
                    if (day < 10) {
                        data += "0"
                    }
                    data += day + " ";
                    if (hour < 10) {
                        data += "0"
                    }


                    data += hour + ":";
                    if (minu < 10) {
                        data += "0"
                    }
                    data += minu + ":";
                    if (second < 10) {
                        data += "0"
                    }
                    data += second;
                    return data;
                }
            }
        </script>
    </head>

</html>
head.html

 

<!DOCTYPE >
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>登录/注册</title>
    <!-- 引入公用部分 -->
    <script th:replace="head::static"></script>

    <!-- login CSS、JS 文件 -->
    <link th:href="@{/css/login.css}" rel="stylesheet" type="text/css"/>
</head>
<body>

<script th:inline="javascript">
</script>
<!-- login CSS、JS 文件 -->
<script th:src="@{/js/login.js}"></script>
</body>
</html>
login.html
h3 {
    margin: 0 0 5px 0;
    text-align: center;
}

.login {
    width: 250px;
    background: #e9e9e9;
    border-radius: 5px;
    margin: 0 auto;
    border: 1px solid #e9e9e9;
    padding: 10px;
}

.login > form {
    margin: 0;
}

.login:focus-within {
    border: 1px solid #10b7f3;
    background: #caefff;
}

.register {
    width: 350px;
    background: #e9e9e9;
    border-radius: 5px;
    margin: 0 auto;
    border: 1px solid #e9e9e9;
    padding: 10px;
    display: none;
}

.register > form {
    margin: 0;
}

.register > table,.login > table {
    margin: 0 auto;
}

.register:focus-within {
    border: 1px solid #10b7f3;
    background: #caefff;
}
login.css
let $login = "<div class=\"login\">\n" +
    "    <h3>登录</h3>\n" +
    "    <form id=\"loginForm\">\n" +
    "    <table>\n" +
    "            <tr>\n" +
    "                <td>账号:</td>\n" +
    "                <td><input type=\"text\" name=\"userName\"/></td>\n" +
    "            </tr>\n" +
    "            <tr>\n" +
    "                <td>密码:</td>\n" +
    "                <td><input type=\"text\" name=\"password\"/></td>\n" +
    "            </tr>\n" +
    "        <tr>\n" +
    "            <td><a href=\"#\" onclick=\"switchover()\">注册</a></td>\n" +
    "            <td colspan=\"2\"><a href=\"#\" onclick=\"login()\">登录</a></td>\n" +
    "        </tr>\n" +
    "    </table>\n" +
    "    </form>\n" +
    "</div>";

let $register = "<!-- 注册 -->\n" +
    "<div class=\"register\">\n" +
    "    <h3>注册</h3>\n" +
    "\n" +
    "  <form id=\"registerForm\">\n" +
    "    <table>\n" +
    "            <tr>\n" +
    "                <td>账号:</td>\n" +
    "                <td><label for=\"userName\"></label><input type=\"text\" id=\"userName\" name=\"userName\"/></td>\n" +
    "            </tr>\n" +
    "            <tr>\n" +
    "                <td>密码:</td>\n" +
    "                <td><input type=\"text\" id=\"password\" name=\"password\"/></td>\n" +
    "            </tr>\n" +
    "            <tr>\n" +
    "                <td>昵称:</td>\n" +
    "                <td><input type=\"text\" id=\"nickName\" name=\"nickName\"/></td>\n" +
    "            </tr>\n" +
    "            <tr>\n" +
    "                <td>性别:</td>\n" +
    "                <td>\n" +
    "                    男<input type=\"radio\" name=\"gender\" value=\"1\" checked/>\n" +
    "                    女<input type=\"radio\" name=\"gender\" value=\"0\"/>\n" +
    "                </td>\n" +
    "            </tr>\n" +
    "            <tr>\n" +
    "                <td>头像:</td>\n" +
    "                <td><input type=\"text\" id=\"avatar\" name=\"avatar\"/></td>\n" +
    "            </tr>\n" +
    "            <tr>\n" +
    "                <td>电子邮箱:</td>\n" +
    "                <td><input type=\"email\" id=\"email\" name=\"email\"/></td>\n" +
    "            </tr>\n" +
    "            <tr>\n" +
    "                <td>手机号码:</td>\n" +
    "                <td><input type=\"text\" id=\"phone\" name=\"phone\"/></td>\n" +
    "            </tr>\n" +
    "            <tr>\n" +
    "                <td>个性签名:</td>\n" +
    "                <td><textarea id=\"sign\" name=\"sign\"></textarea></td>\n" +
    "            </tr>\n" +
    "            <!-- 两个隐藏时间 -->\n" +
    "            <input type=\"hidden\" id=\"createdTime\" name=\"createdTime\"/>\n" +
    "            <input type=\"hidden\" id=\"updataTime\" name=\"updataTime\"/>\n" +
    "        <tr>\n" +
    "            <td><a href=\"#\" onclick=\"switchover()\">登录</a></td>\n" +
    "            <td colspan=\"2\"><a href=\"#\" onclick=\"register()\">注册</a></td>\n" +
    "        </tr>\n" +
    "    </table>\n" +
    "    </form>\n" +
    "</div>";

tip.dialog({title: "登录/注册", content: $login + $register, shade: 0});

//切换登录、注册页面
function switchover() {
    if ($(".login").css("display") === "none") {
        $(".login").show();
        $(".register").hide();
    } else {
        $(".register").show();
        $(".login").hide();
    }
}

//提交注册
function register() {
    let newTime = commonUtil.getNowTime();
    $("#createdTime").val(newTime);
    $("#updataTime").val(newTime);
    $("#avatar").val("/image/logo.png");

    console.log($("#registerForm").serializeObject());
    $.post(ctx + "/imsUser/save", $("#registerForm").serializeObject(), function (data) {
        if (data.flag) {
            switchover();
        }
        // tip提示
        tip.msg(data.msg);
    });
}

//提交登录
function login() {
    console.log($("#loginForm").serializeObject());
    $.post(ctx + "/imsUser/login", $("#loginForm").serializeObject(), function (data) {
        if (data.flag) {
            window.location.href = ctx + "/imsUser/socketChart/" + data.data.userName + ".html"
        } else {
            // tip提示
            tip.msg(data.msg);
        }
    });
    return false;
}
login.js

 

<!DOCTYPE>
<!--解决idea thymeleaf 表达式模板报红波浪线-->
<!--suppress ALL -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>聊天页面</title>
    <!-- 引入公用部分 -->
    <script th:replace="head::static"></script>

    <!-- socketChart CSS、JS 文件 -->
    <link th:href="@{/css/socketChart.css}" rel="stylesheet" type="text/css"/>
</head>
<body>

</body>
<script type="text/javascript" th:inline="javascript">
    //登录名
    var username = /*[[${username}]]*/'';
</script>

<!-- socketChart CSS、JS 文件 -->
<script th:src="@{/js/socketChart.js}"></script>

<script type="text/javascript">
    //老的浏览器可能根本没有实现 mediaDevices,所以我们可以先设置一个空的对象
    if (navigator.mediaDevices === undefined) {
        navigator.mediaDevices = {};
    }
    //一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia
    //因为这样可能会覆盖已有的属性。这里我们只会在没有getUserMedia属性的时候添加它。
    if (navigator.mediaDevices.getUserMedia === undefined) {
        navigator.mediaDevices.getUserMedia = function (constraints) {

            // 首先,如果有getUserMedia的话,就获得它
            var getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

            // 一些浏览器根本没实现它 - 那么就返回一个error到promise的reject来保持一个统一的接口
            if (!getUserMedia) {
                return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
            }

            // 否则,为老的navigator.getUserMedia方法包裹一个Promise
            return new Promise(function (resolve, reject) {
                getUserMedia.call(navigator, constraints, resolve, reject);
            });
        }
    }
    var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;

    //开启视频
    $(document).on("click", "#videoBut", function (event) {
        console.log("开始与" + $("#toUserName").text() + "视频聊天...");
        var username = $("#toUserName").text();
        // 创建PeerConnection实例 (参数为null则没有iceserver,即使没有stunserver和turnserver,仍可在局域网下通讯)
        var pc = new webkitRTCPeerConnection(null);

        // 发送ICE候选到其他客户端
        pc.onicecandidate = function (event) {
            if (event.candidate !== null) {
                //转json字符串
                websocket.send(JSON.stringify({
                    "event": "_ice_candidate",
                    "data": {
                        "candidate": event.candidate
                    }
                }));
                websocket.send(JSON.stringify({
                    "type": "1",
                    "tarUser": {"username": tarUserName},
                    "srcUser": {"username": srcUserName},
                    "message": message
                }));
            }
        };
    });
</script>
</html>
socketChart.html
//追加tip页面
let hzGroup = "" +
    "<div id=\"hz-group\">\n" +
    "        在线人数:<span id=\"onlineCount\">0</span>\n" +
    "        <!-- 主体 -->\n" +
    "        <div id=\"hz-group-body\">\n" +
    "\n" +
    "        </div>\n" +
    "    </div>";
tip.dialog({title: "<span id=\"talks\">" + username + "</span>", content: hzGroup, offset: ["10%", "80%"], shade: 0,
    closeCallBack: function () {
        console.log("dfasdfasd")
        window.location.href = ctx + "/imsUser/logout/" + username;
    }});

//聊天页面
let hzMessage = "" +
    "    <div id=\"hz-message\">\n" +
    "        <!-- 主体 -->\n" +
    "        <div id=\"hz-message-body\">\n" +
    "        </div>\n" +
    "        <!-- 功能条 -->\n" +
    "        <div id=\"\">\n" +
    "            <button>表情</button>\n" +
    "            <button>图片</button>\n" +
    "            <button id=\"videoBut\">视频</button>\n" +
    "            <button onclick=\"send(this)\" style=\"float: right;\">发送</button>\n" +
    "        </div>\n" +
    "        <!-- 输入框 -->\n" +
    "        <div contenteditable=\"true\" id=\"hz-message-input\">\n" +
    "\n" +
    "        </div>\n" +
    "    </div>";


//消息对象数组
var msgObjArr = [];

//websocket对象
var websocket = null;

//判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
    websocket = new WebSocket("ws://localhost:10086/websocket/" + username);
} else {
    console.error("不支持WebSocket");
}

//连接发生错误的回调方法
websocket.onerror = function (e) {
    console.error("WebSocket连接发生错误");
};

//连接成功建立的回调方法
websocket.onopen = function () {
    //获取所有在线用户
    $.ajax({
        type: 'post',
        url: ctx + "/imsUser/getOnlineList",
        contentType: 'application/json;charset=utf-8',
        dataType: 'json',
        data: {username: username},
        success: function (data) {
            if (data.length) {
                //列表
                for (let i = 0; i < data.length; i++) {
                    var userName = data[i];
                    var text = "<div class=\"hz-group-list\"><img class='left' style='width: 23px;' src='https://avatars3.githubusercontent.com/u/31408183?s=40&v=4'/><span class='hz-group-list-username'>" + userName + "</span><span id=\"" + userName + "-status\" style='color: #497b0f;;'>[在线]</span><div id=\"hz-badge-" + userName + "\" class='hz-badge'>0</div></div>";
                    //把自己排在第一个
                    if (username === userName) {
                        $("#hz-group-body").prepend(text);
                    } else {
                        $("#hz-group-body").append(text);
                    }
                }

                //在线人数
                $("#onlineCount").text(data.length);
            }
        },
        error: function (xhr, status, error) {
            console.log("ajax错误!");
        }
    });
};

//接收到消息的回调方法
websocket.onmessage = function (event) {
    var messageJson = eval("(" + event.data + ")");

    //普通消息(私聊)
    if (messageJson.type === "1") {
        //来源用户
        var srcUser = messageJson.srcUser;
        //目标用户
        var tarUser = messageJson.tarUser;
        //消息
        var message = messageJson.message;

        //最加聊天数据
        setMessageInnerHTML(srcUser.username, srcUser.username, message);
    }

    //普通消息(群聊)
    if (messageJson.type === "2") {
        //来源用户
        var srcUser = messageJson.srcUser;
        //目标用户
        var tarUser = messageJson.tarUser;
        //消息
        var message = messageJson.message;

        //最加聊天数据
        setMessageInnerHTML(username, tarUser.username, message);
    }

    //对方不在线
    if (messageJson.type === "0") {
        //消息
        var message = messageJson.message;

        $("#hz-message-body").append(
            "<div class=\"hz-message-list\" style='text-align: center;'>" +
            "<div class=\"hz-message-list-text\">" +
            "<span>" + message + "</span>" +
            "</div>" +
            "</div>");
    }

    //在线人数
    if (messageJson.type === "onlineCount") {
        //取出username
        var onlineCount = messageJson.onlineCount;
        var userName = messageJson.username;
        var oldOnlineCount = $("#onlineCount").text();

        //新旧在线人数对比
        if (oldOnlineCount < onlineCount) {
            if ($("#" + userName + "-status").length > 0) {
                $("#" + userName + "-status").text("[在线]");
                $("#" + userName + "-status").css("color", "#497b0f");
            } else {
                $("#hz-group-body").append("<div class=\"hz-group-list\"><span class='hz-group-list-username'>" + userName + "</span><span id=\"" + userName + "-status\" style='color: #497b0f;'>[在线]</span><div id=\"hz-badge-" + userName + "\" class='hz-badge'>0</div></div>");
            }
        } else {
            //有人下线
            $("#" + userName + "-status").text("[离线]");
            $("#" + userName + "-status").css("color", "#9c0c0c");
        }
        $("#onlineCount").text(onlineCount);
    }

};

//连接关闭的回调方法
websocket.onclose = function () {
    //alert("WebSocket连接关闭");
};

//将消息显示在对应聊天窗口    对于接收消息来说这里的toUserName就是来源用户,对于发送来说则相反
function setMessageInnerHTML(srcUserName, msgUserName, message) {
    //判断
    var childrens = $("#hz-group-body").children(".hz-group-list");
    var isExist = false;
    for (var i = 0; i < childrens.length; i++) {
        var text = $(childrens[i]).find(".hz-group-list-username").text();
        if (text == srcUserName) {
            isExist = true;
            break;
        }
    }
    if (!isExist) {
        //追加聊天对象
        msgObjArr.push({
            toUserName: srcUserName,
            message: [{username: msgUserName, message: message, date: nowTime()}]//封装数据
        });
        $("#hz-group-body").append("<div class=\"hz-group-list\"><span class='hz-group-list-username'>" + srcUserName + "</span><span id=\"" + srcUserName + "-status\">[在线]</span><div id=\"hz-badge-" + srcUserName + "\" class='hz-badge'>0</div></div>");
    } else {
        //取出对象
        var isExist = false;
        for (var i = 0; i < msgObjArr.length; i++) {
            var obj = msgObjArr[i];
            if (obj.toUserName == srcUserName) {
                //保存最新数据
                obj.message.push({username: msgUserName, message: message, date: nowTime()});
                isExist = true;
                break;
            }
        }
        if (!isExist) {
            //追加聊天对象
            msgObjArr.push({
                toUserName: srcUserName,
                message: [{username: msgUserName, message: message, date: nowTime()}]//封装数据
            });
        }
    }

    //刚好有打开的是对应的聊天页面
    if ($(".tip" + srcUserName).length > 0) {
        $(".tip" + srcUserName + " #hz-message-body").append(
            "<div class=\"hz-message-list\">" +
            "<p class='hz-message-list-username'>" + msgUserName + "</p>" +
            "<img class='left' style='width: 23px;margin: 0 5px 0 0;' src='https://avatars3.githubusercontent.com/u/31408183?s=40&v=4'/>" +
            "<div class=\"hz-message-list-text left\">" +
            "<span>" + message + "</span>" +
            "</div>" +
            "<div style=\" clear: both; \"></div>" +
            "</div>");
    } else {
        //小圆点++
        var conut = $("#hz-badge-" + srcUserName).text();
        $("#hz-badge-" + srcUserName).text(parseInt(conut) + 1);
        $("#hz-badge-" + srcUserName).css("opacity", "1");
    }
}

//发送消息
function send(but) {
    //目标用户名
    var tarUserName = $(but).parents(".tip-dialog").find("#toUserName").text();
    //登录用户名
    var srcUserName = $("#talks").text();
    //消息
    var message = $(but).parents(".tip-dialog").find("#hz-message-input").html();

    websocket.send(JSON.stringify({
        "type": "1",
        "tarUser": {"username": tarUserName},
        "srcUser": {"username": srcUserName},
        "message": message
    }));
    $(".tip" + tarUserName + " #hz-message-body").append(
        "<div class=\"hz-message-list\">" +
        "<img class='right' style='width: 23px;margin: 0 0 0 5px;' src='https://avatars3.githubusercontent.com/u/31408183?s=40&v=4'/>" +
        "<div class=\"hz-message-list-text right\">" +
        "<span>" + message + "</span>" +
        "</div>" +
        "</div>");
    $(".tip" + tarUserName + " #hz-message-input").html("");
    //取出对象
    if (msgObjArr.length > 0) {
        var isExist = false;
        for (var i = 0; i < msgObjArr.length; i++) {
            var obj = msgObjArr[i];
            if (obj.toUserName == tarUserName) {
                //保存最新数据
                obj.message.push({username: srcUserName, message: message, date: nowTime()});
                isExist = true;
                break;
            }
        }
        if (!isExist) {
            //追加聊天对象
            msgObjArr.push({
                toUserName: tarUserName,
                message: [{username: srcUserName, message: message, date: nowTime()}]//封装数据[{username:huanzi,message:"你好,我是欢子!",date:2018-04-29 22:48:00}]
            });
        }
    } else {
        //追加聊天对象
        msgObjArr.push({
            toUserName: tarUserName,
            message: [{username: srcUserName, message: message, date: nowTime()}]//封装数据[{username:huanzi,message:"你好,我是欢子!",date:2018-04-29 22:48:00}]
        });
    }
}

//监听点击用户
$("body").on("click", ".hz-group-list", function () {
    var toUserName = $(this).find(".hz-group-list-username").text();
    //弹出聊天页面
    tip.dialog({
        title: "正在与 <span id=\"toUserName\"></span> 聊天",
        class: "tip" + toUserName,
        content: hzMessage,
        shade: 0
    });

    // $(".hz-group-list").css("background-color", "");
    // $(this).css("background-color", "whitesmoke");
    $(".tip" + toUserName + " #toUserName").text(toUserName);

    //清空小圆点
    $("#hz-badge-" + toUserName).text("0");
    $("#hz-badge-" + toUserName).css("opacity", "0");
    if (msgObjArr.length > 0) {
        for (var i = 0; i < msgObjArr.length; i++) {
            var obj = msgObjArr[i];
            if (obj.toUserName === toUserName) {
                //追加数据
                var messageArr = obj.message;
                if (messageArr.length > 0) {
                    for (var j = 0; j < messageArr.length; j++) {
                        var msgObj = messageArr[j];
                        var leftOrRight = "right";
                        var message = msgObj.message;
                        var msgUserName = msgObj.username;

                        //当聊天窗口与msgUserName的人相同,文字在左边(对方/其他人),否则在右边(自己)
                        if (msgUserName === toUserName) {
                            leftOrRight = "left";
                        }

                        //但是如果点击的是自己,群聊的逻辑就不太一样了
                        if (username === toUserName && msgUserName !== toUserName) {
                            leftOrRight = "left";
                        }

                        if (username === toUserName && msgUserName === toUserName) {
                            leftOrRight = "right";
                        }

                        var magUserName = leftOrRight === "left" ? "<p class='hz-message-list-username'>" + msgUserName + "</p>" : "";

                        $(".tip" + toUserName + " #hz-message-body").append(
                            "<div class=\"hz-message-list\">" +
                            magUserName +
                            "<img class='" + leftOrRight + "' style='width: 23px;margin: 0 5px 0 0;' src='https://avatars3.githubusercontent.com/u/31408183?s=40&v=4'/>" +
                            "<div class=\"hz-message-list-text " + leftOrRight + "\">" +
                            "<span>" + message + "</span>" +
                            "</div>" +
                            "<div style=\" clear: both; \"></div>" +
                            "</div>");
                    }
                }
                break;
            }
        }
    }
});

//获取当前时间
function nowTime() {
    var time = new Date();
    var year = time.getFullYear();//获取年
    var month = time.getMonth() + 1;//或者月
    var day = time.getDate();//或者天
    var hour = time.getHours();//获取小时
    var minu = time.getMinutes();//获取分钟
    var second = time.getSeconds();//或者秒
    var data = year + "-";
    if (month < 10) {
        data += "0";
    }
    data += month + "-";
    if (day < 10) {
        data += "0"
    }
    data += day + " ";
    if (hour < 10) {
        data += "0"
    }
    data += hour + ":";
    if (minu < 10) {
        data += "0"
    }
    data += minu + ":";
    if (second < 10) {
        data += "0"
    }
    data += second;
    return data;
}
socketChart.js
#hz-main {
    width: 700px;
    height: 500px;
    background-color: red;
    margin: 0 auto;
}

#hz-message {
    width: 500px;
    float: left;
    background-color: #B5B5B5;
}

#hz-message-body {
    width: 460px;
    height: 340px;
    background-color: #E0C4DA;
    padding: 10px 20px;
    overflow: auto;
}

#hz-message-input {
    width: 500px;
    height: 99px;
    background-color: white;
    overflow: auto;
}

#hz-group {
    width: 200px;
    height: 500px;
    background-color: rosybrown;
    float: right;
}

.hz-message-list {
    min-height: 30px;
    margin: 10px 0;
}

.hz-message-list-text {
    padding: 7px 13px;
    border-radius: 15px;
    width: auto;
    max-width: 85%;
    display: inline-block;
}

.hz-message-list-username {
    margin: 0 0 0 25px;
}

.hz-group-body {
    overflow: auto;
}

.hz-group-list {
    padding: 10px;
    line-height: 23px;
}

.hz-group-list:hover{
    background-color: whitesmoke;
}

.left {
    float: left;
    color: #595a5a;
    background-color: #ebebeb;
}

.right {
    float: right;
    color: #f7f8f8;
    background-color: #919292;
}

.hz-badge {
    width: 20px;
    height: 20px;
    background-color: #FF5722;
    border-radius: 50%;
    float: right;
    color: white;
    text-align: center;
    line-height: 20px;
    font-weight: bold;
    opacity: 0;
}
socketChart.css

 

  演示效果

  注册、登录、登出

 

  登录三个账号,上下线提示功能

  

  模拟私聊

  

  模拟群聊(目前点击自己头像是群聊频道)

 

  总结

  第一版先实现到这里。第二版预计实现消息存储、离线推送,好友分组,好友搜索、添加,以及一些其他优化;欢迎大家指出不足之处!

posted @ 2019-05-07 10:55  huanzi-qch  阅读(2045)  评论(0编辑  收藏  举报