commit 4b5a62406d396ae4aa87c4bac4da51484ee547ba Author: xie.bx Date: Thu Oct 27 16:29:02 2022 +0800 1.0.0版本 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce789d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea/ + + diff --git a/commom/pom.xml b/commom/pom.xml new file mode 100644 index 0000000..c624595 --- /dev/null +++ b/commom/pom.xml @@ -0,0 +1,61 @@ + + + + lx-im + com.lx + ${im.version} + + 4.0.0 + commom + + + 8 + 8 + + + + + com.baomidou + mybatis-plus-generator + 3.3.2 + + + mysql + mysql-connector-java + compile + + + cn.hutool + hutool-all + + + org.projectlombok + lombok + 1.18.16 + + + org.apache.commons + commons-lang3 + + + org.springframework + spring-beans + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.apache.velocity + velocity + ${velocity.version} + + + mysql + mysql-connector-java + + + \ No newline at end of file diff --git a/commom/src/main/java/com/lx/common/contant/RedisKey.java b/commom/src/main/java/com/lx/common/contant/RedisKey.java new file mode 100644 index 0000000..b1ab4e4 --- /dev/null +++ b/commom/src/main/java/com/lx/common/contant/RedisKey.java @@ -0,0 +1,10 @@ +package com.lx.common.contant; + +public class RedisKey { + + public final static String IM_USER_SERVER_ID = "im:user_server_id:"; + + public final static String IM_UNREAD_MESSAGE = "im:unread_msg:"; + + public final static String IM_ALREADY_READED_MESSAGE = "im:already_read_msg"; +} diff --git a/commom/src/main/java/com/lx/common/enums/MessageStatusEnum.java b/commom/src/main/java/com/lx/common/enums/MessageStatusEnum.java new file mode 100644 index 0000000..72beb5c --- /dev/null +++ b/commom/src/main/java/com/lx/common/enums/MessageStatusEnum.java @@ -0,0 +1,36 @@ +package com.lx.common.enums; + + +public enum MessageStatusEnum { + + UNREAD(0,"未读"), + ALREADY_READ(1,"已读"); + + + private Integer code; + + private String desc; + + MessageStatusEnum(Integer index, String desc) { + this.code =index; + this.desc=desc; + } + + public static MessageStatusEnum fromCode(Integer code){ + for (MessageStatusEnum typeEnum:values()) { + if (typeEnum.code.equals(code)) { + return typeEnum; + } + } + return null; + } + + + public String getDesc() { + return desc; + } + + public Integer getCode(){ + return this.code; + } +} diff --git a/commom/src/main/java/com/lx/common/enums/MessageTypeEnum.java b/commom/src/main/java/com/lx/common/enums/MessageTypeEnum.java new file mode 100644 index 0000000..77afdf6 --- /dev/null +++ b/commom/src/main/java/com/lx/common/enums/MessageTypeEnum.java @@ -0,0 +1,37 @@ +package com.lx.common.enums; + + +public enum MessageTypeEnum { + + TEXT(0,"文字"), + FILE(1,"文件"), + IMAGE(2,"图片"), + VIDEO(3,"视频"); + + private Integer code; + + private String desc; + + MessageTypeEnum(Integer index, String desc) { + this.code =index; + this.desc=desc; + } + + public static MessageTypeEnum fromCode(Integer code){ + for (MessageTypeEnum typeEnum:values()) { + if (typeEnum.code.equals(code)) { + return typeEnum; + } + } + return null; + } + + + public String getDesc() { + return desc; + } + + public Integer getCode(){ + return this.code; + } +} diff --git a/commom/src/main/java/com/lx/common/enums/ResultCode.java b/commom/src/main/java/com/lx/common/enums/ResultCode.java new file mode 100644 index 0000000..63f6a81 --- /dev/null +++ b/commom/src/main/java/com/lx/common/enums/ResultCode.java @@ -0,0 +1,49 @@ +package com.lx.common.enums; + +/** + * 响应码枚举 + * + * @author Blue + * @date 2020/10/19 + * + **/ +public enum ResultCode { + SUCCESS(200,"成功"), + LOGIN_ERROR(400,"登录异常"), + NO_LOGIN(401,"未登录"), + FORBIDDEN(403,"禁止访问"), + NOT_FIND(404,"无法找到文件"), + PROGRAM_ERROR(500,"系统繁忙,请稍后再试"), + PASSWOR_ERROR(10001,"密码不正确"), + VERITY_CODE_NOT_EXIST(10002,"验证码不存在"), + VERITY_CODE_ERROR(10003,"验证码不正确"), + USERNAME_ALREADY_REGISTER(10004,"该用户名已注册"), + MOBILE_ALREADY_REGISTER(10005,"该手机号码已注册"), + ; + + + private int code; + private String msg; + + // 构造方法 + ResultCode(int code, String msg) { + this.code = code; + this.msg = msg; + } + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } +} + diff --git a/commom/src/main/java/com/lx/common/enums/WSCmdEnum.java b/commom/src/main/java/com/lx/common/enums/WSCmdEnum.java new file mode 100644 index 0000000..9444b44 --- /dev/null +++ b/commom/src/main/java/com/lx/common/enums/WSCmdEnum.java @@ -0,0 +1,39 @@ +package com.lx.common.enums; + +public enum WSCmdEnum { + + HEARTBEAT(0,"心跳"), + SINGLE_MESSAGE(1,"单发消息"), + GROUP_MESSAGE(2,"群发消息"); + + + private Integer code; + + private String desc; + + WSCmdEnum(Integer index, String desc) { + this.code =index; + this.desc=desc; + } + + public static WSCmdEnum fromCode(Integer code){ + for (WSCmdEnum typeEnum:values()) { + if (typeEnum.code.equals(code)) { + return typeEnum; + } + } + return null; + } + + + public String getDesc() { + return desc; + } + + public Integer getCode(){ + return this.code; + } + + +} + diff --git a/commom/src/main/java/com/lx/common/generator/CodeGenerator.java b/commom/src/main/java/com/lx/common/generator/CodeGenerator.java new file mode 100644 index 0000000..b2bacd6 --- /dev/null +++ b/commom/src/main/java/com/lx/common/generator/CodeGenerator.java @@ -0,0 +1,102 @@ +package com.lx.common.generator; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.generator.AutoGenerator; +import com.baomidou.mybatisplus.generator.InjectionConfig; +import com.baomidou.mybatisplus.generator.config.*; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import com.baomidou.mybatisplus.generator.config.rules.DateType; +import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; +import com.baomidou.mybatisplus.generator.engine.VelocityTemplateEngine; + +import java.util.ArrayList; +import java.util.List; + + +public class CodeGenerator { + public static void main(String[] args) { + // 代码生成器 + AutoGenerator mpg = new AutoGenerator(); + + // 全局配置 + GlobalConfig gc = new GlobalConfig(); + //生成的代码输出路径,自己根据需要修改 + String projectPath = "d:\\work\\project\\code"; + gc.setOutputDir(projectPath + "/src/main/java"); + gc.setAuthor("blue"); + gc.setOpen(false); + gc.setFileOverride(true); + gc.setActiveRecord(true); + gc.setBaseColumnList(true); + gc.setBaseResultMap(true); + gc.setIdType(IdType.AUTO); + gc.setDateType(DateType.ONLY_DATE); + mpg.setGlobalConfig(gc); + + // 数据源配置 + DataSourceConfig dsc = new DataSourceConfig(); + dsc.setUrl("jdbc:mysql://localhost:3306/simple-im?useUnicode=true&characterEncoding=utf-8"); + dsc.setDriverName("com.mysql.jdbc.Driver"); + dsc.setUsername("root"); + dsc.setPassword("root"); + mpg.setDataSource(dsc); + + // 包配置 + PackageConfig pc = new PackageConfig(); + pc.setModuleName(""); + pc.setParent("com.lx"); + mpg.setPackageInfo(pc); + + // 如果模板引擎是 velocity + String templatePath = "/templates/mapper.xml.vm"; + + // 自定义输出配置 + List focList = new ArrayList<>(); + // 自定义配置会被优先输出 + focList.add(new FileOutConfig(templatePath) { + @Override + public String outputFile(TableInfo tableInfo) { + // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!! + return projectPath + "/src/main/resources/mapper/" + + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; + } + }); + + // 自定义配置 + InjectionConfig cfg = new InjectionConfig() { + @Override + public void initMap() { + // to do nothing + } + }; + cfg.setFileOutConfigList(focList); + mpg.setCfg(cfg); + + // 配置模板 + TemplateConfig templateConfig = new TemplateConfig(); + templateConfig.setXml(null); + mpg.setTemplate(templateConfig); + + // 策略配置 + StrategyConfig strategy = new StrategyConfig(); + // 下划线转驼峰 + strategy.setNaming(NamingStrategy.underline_to_camel); + strategy.setColumnNaming(NamingStrategy.underline_to_camel); + strategy.setEntityTableFieldAnnotationEnable(true); + strategy.setVersionFieldName("version"); + //逻辑删除的字段 + strategy.setLogicDeleteFieldName("deleted"); + strategy.setEntityLombokModel(true); + strategy.setRestControllerStyle(true); + + + //多张表的时候直接在代码中写表名 + strategy.setInclude("friends"); + strategy.setTablePrefix(""); + mpg.setStrategy(strategy); + + mpg.setTemplateEngine(new VelocityTemplateEngine()); + mpg.execute(); + } +} diff --git a/commom/src/main/java/com/lx/common/model/im/HeartbeatInfo.java b/commom/src/main/java/com/lx/common/model/im/HeartbeatInfo.java new file mode 100644 index 0000000..4671135 --- /dev/null +++ b/commom/src/main/java/com/lx/common/model/im/HeartbeatInfo.java @@ -0,0 +1,9 @@ +package com.lx.common.model.im; + +import lombok.Data; + +@Data +public class HeartbeatInfo { + + private long userId; +} diff --git a/commom/src/main/java/com/lx/common/model/im/SendInfo.java b/commom/src/main/java/com/lx/common/model/im/SendInfo.java new file mode 100644 index 0000000..831b9e8 --- /dev/null +++ b/commom/src/main/java/com/lx/common/model/im/SendInfo.java @@ -0,0 +1,11 @@ +package com.lx.common.model.im; + +import lombok.Data; + +@Data +public class SendInfo { + + private Integer cmd; + private T data; + +} diff --git a/commom/src/main/java/com/lx/common/model/im/SingleMessageInfo.java b/commom/src/main/java/com/lx/common/model/im/SingleMessageInfo.java new file mode 100644 index 0000000..ba4383f --- /dev/null +++ b/commom/src/main/java/com/lx/common/model/im/SingleMessageInfo.java @@ -0,0 +1,21 @@ +package com.lx.common.model.im; + +import lombok.Data; + +import java.util.Date; + +@Data +public class SingleMessageInfo { + + private long id; + + private Long sendUserId; + + private Long recvUserId; + + private String content; + + private Integer type; + + private Date sendTime; +} diff --git a/commom/src/main/java/com/lx/common/result/Result.java b/commom/src/main/java/com/lx/common/result/Result.java new file mode 100644 index 0000000..4463c48 --- /dev/null +++ b/commom/src/main/java/com/lx/common/result/Result.java @@ -0,0 +1,16 @@ +package com.lx.common.result; + +import lombok.Data; + + +@Data +public class Result { + + + private int code; + + private String message; + + private T data; + +} diff --git a/commom/src/main/java/com/lx/common/result/ResultUtils.java b/commom/src/main/java/com/lx/common/result/ResultUtils.java new file mode 100644 index 0000000..040ef8d --- /dev/null +++ b/commom/src/main/java/com/lx/common/result/ResultUtils.java @@ -0,0 +1,83 @@ +package com.lx.common.result; + + +import com.lx.common.enums.ResultCode; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ResultUtils { + + + + public static final Result success(){ + Result result=new Result(); + result.setCode(ResultCode.SUCCESS.getCode()); + result.setMessage(ResultCode.SUCCESS.getMsg()); + return result; + } + + public static final Result success(T data){ + ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); + cachedThreadPool.submit(new Runnable() { + + @Override + public void run() { + System.out.println("当前线程"+Thread.currentThread().getName()); + } + }); + Result result=new Result(); + result.setCode(ResultCode.SUCCESS.getCode()); + result.setMessage(ResultCode.SUCCESS.getMsg()); + result.setData(data); + return result; + } + + public static final Result success(T data, String messsage){ + Result result=new Result(); + result.setCode(ResultCode.SUCCESS.getCode()); + result.setMessage(messsage); + result.setData(data); + return result; + } + + public static final Result success(String messsage){ + Result result=new Result(); + result.setCode(ResultCode.SUCCESS.getCode()); + result.setMessage(messsage); + return result; + } + + public static final Result error(Integer code, String messsage){ + Result result=new Result(); + result.setCode(code); + result.setMessage(messsage); + return result; + } + + + public static final Result error(ResultCode resultCode, String messsage){ + Result result=new Result(); + result.setCode(resultCode.getCode()); + result.setMessage(messsage); + return result; + } + + public static final Result error(ResultCode resultCode, String messsage,T data){ + Result result=new Result(); + result.setCode(resultCode.getCode()); + result.setMessage(messsage); + result.setData(data); + return result; + } + + public static final Result error(ResultCode resultCode){ + Result result=new Result(); + result.setCode(resultCode.getCode()); + result.setMessage(resultCode.getMsg()); + return result; + } + + + +} diff --git a/commom/src/main/java/com/lx/common/util/BeanUtils.java b/commom/src/main/java/com/lx/common/util/BeanUtils.java new file mode 100644 index 0000000..5425757 --- /dev/null +++ b/commom/src/main/java/com/lx/common/util/BeanUtils.java @@ -0,0 +1,166 @@ +package com.lx.common.util; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.util.ReflectionUtils; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class BeanUtils { + + + + private static void handleReflectionException(Exception e) { + ReflectionUtils.handleReflectionException(e); + } + + + + public static List copyProperties(List sourceList, Class clazz) { + + if(sourceList == null || sourceList.size() <= 0) { + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + for (T source : sourceList) { + result.add(copyProperties(source, clazz)); + } + + return result; + } + /** + * 如果source , 为空返回空对象 + * @param sourceList + * @param clazz + * @return + */ + public static List copyPropertiesList(List sourceList, Class clazz) { + + if(sourceList == null || sourceList.size() <= 0) { + return new ArrayList(); + } + + List result = new ArrayList<>(); + U target = null; + for (T source : sourceList) { + try { + target = clazz.newInstance(); + copyProperties(source, target); + }catch(Exception e) { + handleReflectionException(e); + return new ArrayList<>(); + } + result.add(target); + } + + return result; + } + + /** + * source空为null + * @param orig + * @param destClass + * @param + * @return + */ + public static T copyProperties(Object orig, Class destClass) { + + try { + Object target = destClass.newInstance(); + if(orig == null) { + return null; + } + copyProperties(orig, (Object)target); + return (T) target; + }catch(Exception e) { + handleReflectionException(e); + return null; + } + } + + /** + * source 为null 返回 空对象 + * @param orig + * @param destClass + * @param + * @return + */ + public static T copyProperty(Object orig, Class destClass) { + + try { + Object target = destClass.newInstance(); + if(orig == null) { + return (T)target; + } + + copyProperties(orig, (Object)target); + return (T) target; + }catch(Exception e) { + handleReflectionException(e); + Object o1 = new Object(); + try { + o1 = destClass.newInstance(); + }catch(Exception ex) { + handleReflectionException(e); + } + return (T)o1; + } + } + + public static List copyProperties(List sourceList, Class clazz, String... ignoreProperties) { + + if(sourceList == null || sourceList.size() <= 0) { + return new ArrayList(); + } + + List result = new ArrayList<>(); + for (T source : sourceList) { + result.add(copyProperties(source, clazz, ignoreProperties)); + } + + return result; + } + + public static T copyProperties(Object orig, Class destClass, String... ignoreProperties) { + + if(orig == null) { + return null; + } + + try { + Object target = destClass.newInstance(); + org.springframework.beans.BeanUtils.copyProperties(orig, (Object)target, ignoreProperties); + return (T)target; + }catch(Exception e) { + return null; + } + } + + public static String[] getNullPropertyNames(Object source) { + final BeanWrapper beanWrapper = new BeanWrapperImpl(source); + java.beans.PropertyDescriptor[] propDesc = beanWrapper.getPropertyDescriptors(); + + Set emptynames = new HashSet(); + + for(java.beans.PropertyDescriptor pd : propDesc) { + Object srcValue = beanWrapper.getPropertyValue(pd.getName()); + if (srcValue == null) {emptynames.add(pd.getName());} + } + + String[] result = new String[emptynames.size()]; + return emptynames.toArray(result); + } + + public static void copyProperties(Object orig, Object dest) { + try { + org.springframework.beans.BeanUtils.copyProperties(orig, dest); + } catch (Exception e) { + handleReflectionException(e); + } + } + +} \ No newline at end of file diff --git a/commom/src/main/java/com/lx/common/util/DateTimeUtils.java b/commom/src/main/java/com/lx/common/util/DateTimeUtils.java new file mode 100644 index 0000000..15d9072 --- /dev/null +++ b/commom/src/main/java/com/lx/common/util/DateTimeUtils.java @@ -0,0 +1,1128 @@ +package com.lx.common.util; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateUtils; + +import java.sql.Timestamp; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + * 日期处理工具类 + * + * @version 1.0 + */ +public class DateTimeUtils extends DateUtils { + + public static final String FULL_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; + public static final String FULL_DATE_FORMAT_CN = "yyyy年MM月dd日 HH时mm分ss秒"; + public static final String PART_DATE_FORMAT = "yyyy-MM-dd"; + public static final String PART_DATE_FORMAT_TWO = "yyyy/MM/dd"; + public static final String PART_DATE_FORMAT_CN = "yyyy年MM月dd日"; + public static final String PARTDATEFORMAT = "yyyyMMdd"; + public static final String YEAR_DATE_FORMAT = "yyyy"; + public static final String MONTH_DATE_FORMAT = "MM"; + public static final String DAY_DATE_FORMAT = "dd"; + public static final String WEEK_DATE_FORMAT = "week"; + + /** + * 星座 + */ + private final static int[] dayArr = new int[]{20, 19, 21, 20, 21, 22, 23, + 23, 23, 24, 23, 22}; + private final static String[] constellationArr = new String[]{"摩羯座", + "水瓶座", "双鱼座", "白羊座", "金牛座", "双子座", "巨蟹座", "狮子座", "处女座", "天秤座", + "天蝎座", "射手座", "摩羯座"}; + + + /** + * 将日期类型转换为字符串 + * + * @param date 日期 + * @param xFormat 格式 + * @return + */ + public static String getFormatDate(Date date, String xFormat) { + date = date == null ? new Date() : date; + xFormat = StringUtils.isNotEmpty(xFormat) == true ? xFormat : FULL_DATE_FORMAT; + SimpleDateFormat sdf = new SimpleDateFormat(xFormat); + return sdf.format(date); + } + + + /** + * 比较日期大小 + * + * @param dateX + * @param dateY + * @return x < y return [-1]; + * x = y return [0] ; + * x > y return [1] ; + */ + public static int compareDate(Date dateX, Date dateY) { + return dateX.compareTo(dateY); + } + + + /** + * 将日期字符串转换为日期格式类型 + * + * @param xDate + * @param xFormat 为NULL则转换如:2012-06-25 + * @return + */ + public static Date parseString2Date(String xDate, String xFormat) { + while (!isNotDate(xDate, xFormat)) { + xFormat = StringUtils.isEmpty(xFormat) == true ? PART_DATE_FORMAT : xFormat; + SimpleDateFormat sdf = new SimpleDateFormat(xFormat); + Date date = null; + try { + date = sdf.parse(xDate); + } catch (ParseException e) { + e.printStackTrace(); + return null; + } + return date; + } + return null; + } + + + /** + * 判断需要转换类型的日期字符串是否符合格式要求 + * + * @param xDate + * @return + */ + public static boolean isNotDate(String xDate, String format) { + SimpleDateFormat sdf; + try { + if (StringUtils.isEmpty(format)) { + sdf = new SimpleDateFormat(PART_DATE_FORMAT); + } else { + sdf = new SimpleDateFormat(format); + } + if (StringUtils.isEmpty(xDate)) { + return true; + } + sdf.parse(xDate); + return false; + } catch (ParseException e) { + e.printStackTrace(); + return true; + } + } + + public static boolean isDate(String xDate) { + return !isDate(xDate); + } + + + /** + * 获取俩个日期之间相差多少天 + * + * @param dateX + * @param dateY + * @return + */ + public static int getDiffDays(Date dateX, Date dateY) { + if ((dateX == null) || (dateY == null)) { + return 0; + } + + long dayX = dateX.getTime(); + long dayY = dateY.getTime(); + + return dayX > dayY ? (int) ((dayX - dayY) / (60 * 60 * 1000 * 24)) : (int) ((dayY - dayX) / (60 * 60 * 1000 * 24)); + } + + /** + * 获取俩个日期之间相差多少小时 + * + * @param dateX + * @param dateY + * @return + */ + public static int getDiffHours(Date dateX, Date dateY) { + if ((dateX == null) || (dateY == null)) { + return 0; + } + + long dayX = dateX.getTime(); + long dayY = dateY.getTime(); + + return dayX > dayY ? (int) ((dayX - dayY) / (60 * 60 * 1000)) : (int) ((dayY - dayX) / (60 * 60 * 1000)); + } + + /** + * 获取俩个日期之间相差多少小时 + * + * @param dateX + * @param dateY + * @return + */ + public static int getDiffMinute(Date dateX, Date dateY) { + if ((dateX == null) || (dateY == null)) { + return 0; + } + + long dayX = dateX.getTime(); + long dayY = dateY.getTime(); + + return dayX > dayY ? (int) ((dayX - dayY) / (60 * 1000)) : (int) ((dayY - dayX) / (60 * 1000)); + } + + /** + * 获取传值日期之后几天的日期并转换为字符串类型 + * + * @param date 需要转换的日期 date 可以为NULL 此条件下则获取当前日期 + * @param after 天数 + * @param xFormat 转换字符串类型 (可以为NULL) + * @return + */ + public static String getAfterCountDate(Date date, int after, String xFormat) { + date = date == null ? new Date() : date; + xFormat = StringUtils.isNotEmpty(xFormat) ? xFormat : PART_DATE_FORMAT; + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.DAY_OF_MONTH, after); + return getFormatDate(calendar.getTime(), xFormat); + } + + /** + * 获取传值日期之前几天的日期并转换为字符串类型 + * + * @param date 需要转换的日期 date 可以为NULL 此条件下则获取当前日期 + * @param xFormat 转换字符串类型 (可以为NULL) + * @return + */ + public static String getBeforeCountDate(Date date, int before, String xFormat) { + date = date == null ? new Date() : date; + xFormat = StringUtils.isNotEmpty(xFormat) == true ? xFormat : PART_DATE_FORMAT; + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.DAY_OF_MONTH, -before); + return getFormatDate(calendar.getTime(), xFormat); + } + + + /** + * 获取日期的参数 如:年 , 月 , 日 , 星期几 + * + * @param xDate 日期 可以为日期格式,可以是字符串格式; 为NULL或者其他格式时都判定为当前日期 + * @param xFormat 年 yyyy 月 MM 日 dd 星期 week ;其他条件下都返回0 + */ + public static int getDateTimeParam(Object xDate, String xFormat) { + xDate = xDate == null ? new Date() : xDate; + Date date = null; + if (xDate instanceof String) { + date = parseString2Date(xDate.toString(), null); + } else if (xDate instanceof Date) { + date = (Date) xDate; + } else { + date = new Date(); + } + date = date == null ? new Date() : date; + if (StringUtils.isNotEmpty(xFormat) + && (xFormat.equals(YEAR_DATE_FORMAT) + || xFormat.equals(MONTH_DATE_FORMAT) + || xFormat.equals(DAY_DATE_FORMAT))) { + return Integer.parseInt(getFormatDate(date, xFormat)); + } else if (StringUtils.isNotEmpty(xFormat) + && (WEEK_DATE_FORMAT.equals(xFormat))) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + int week = cal.get(Calendar.DAY_OF_WEEK) - 1 == 0 ? + 7 : cal.get(Calendar.DAY_OF_WEEK) - 1; + return week; + } else { + return 0; + } + } + + + /** + * 日期格式转换为时间戳 + * + * @param time + * @param format + * @return + */ + public static Long getLongTime(String time, String format) { + SimpleDateFormat sdf = new SimpleDateFormat(format); + Date date = null; + try { + date = sdf.parse(time); + return (date.getTime() / 1000); + } catch (ParseException e) { + e.printStackTrace(); + } + return null; + } + + + /** + * 获取星期字符串 + * + * @param xDate + * @return + */ + public static String getWeekString(Object xDate) { + int week = getDateTimeParam(xDate, WEEK_DATE_FORMAT); + switch (week) { + case 1: + return "星期一"; + case 2: + return "星期二"; + case 3: + return "星期三"; + case 4: + return "星期四"; + case 5: + return "星期五"; + case 6: + return "星期六"; + case 7: + return "星期日"; + default: + return ""; + } + } + + /** + * 获得十位时间 + */ + public static Long getTenBitTimestamp() { + return System.currentTimeMillis() / 1000; + } + + /** + * 获得某天的结束时间 + */ + public static Date getDateEnd(Date date) { + return new Date(date.getTime() + (86400 - 1) * 1000); + } + + /** + * 日期格式转换为毫秒 + * + * @param time + * @param format + * @return + */ + public static Long getLongDateTime(String time, String format) { + SimpleDateFormat sdf = new SimpleDateFormat(format); + Date date = null; + try { + date = sdf.parse(time); + return date.getTime(); + } catch (ParseException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 获取某天开始时间戳_10位 + */ + public static Long getStartTimestamp(Date date) { + + Calendar calendar = Calendar.getInstance(); + date = date == null ? new Date() : date; + calendar.setTime(date); + + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + + return calendar.getTime().getTime() / 1000; + } + + /** + * 获取某天结束时间戳_10位 + */ + public static Long getEndTimestamp(Date date) { + + Calendar calendar = Calendar.getInstance(); + date = date == null ? new Date() : date; + calendar.setTime(date); + + calendar.set(Calendar.HOUR_OF_DAY, 23); + calendar.set(Calendar.MINUTE, 59); + calendar.set(Calendar.SECOND, 59); + calendar.set(Calendar.MILLISECOND, 999); + + return calendar.getTime().getTime() / 1000; + } + + /** + * 获取昨天日期 + * + * @param date + * @return + */ + public static Date getYesterday(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.DAY_OF_MONTH, -1); + + calendar.set(Calendar.HOUR_OF_DAY, 9); + calendar.set(Calendar.MINUTE, 59); + calendar.set(Calendar.SECOND, 59); + calendar.set(Calendar.MILLISECOND, 999); + date = calendar.getTime(); + return date; + } + + /** + * 获取明天时间(参数时间+1天) + * + * @param date + * @return + */ + public static Date getTomorrowday(Date date) { + Calendar c = Calendar.getInstance(); + c.setTime(date); + c.add(Calendar.DAY_OF_YEAR, +1); + return c.getTime(); + } + + /* 10位int型的时间戳转换为String(yyyy-MM-dd HH:mm:ss) + * + * @param time + * @return + */ + public static String timestampToString(Integer time, String format) { + // int转long时,先进行转型再进行计算,否则会是计算结束后在转型 + long temp = (long) time * 1000; + Timestamp ts = new Timestamp(temp); + String tsStr = ""; + DateFormat dateFormat = new SimpleDateFormat(format); + try { + // 方法一 + tsStr = dateFormat.format(ts); + } catch (Exception e) { + e.printStackTrace(); + } + return tsStr; + } + + /** + * 获取某天开始时间 + */ + public static Date getStartTime(Date date) { + + Calendar calendar = Calendar.getInstance(); + date = date == null ? new Date() : date; + calendar.setTime(date); + + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + + return calendar.getTime(); + } + + /** + * 获取某天结束时间 + */ + public static Date getEndTime(Date date) { + + Calendar calendar = Calendar.getInstance(); + date = date == null ? new Date() : date; + calendar.setTime(date); + + calendar.set(Calendar.HOUR_OF_DAY, 23); + calendar.set(Calendar.MINUTE, 59); + calendar.set(Calendar.SECOND, 59); + calendar.set(Calendar.MILLISECOND, 999); + + return calendar.getTime(); + } + + /** + * Date类型转换为10位时间戳 + * + * @param time + * @return + */ + public static Integer DateToTimestamp(Date time) { + Timestamp ts = new Timestamp(time.getTime()); + + return (int) ((ts.getTime()) / 1000); + } + + /** + * 获取当前时间之前或之后几分钟 + * + * @param minute + * @return + */ + public static String getMinuteToString(int minute, Date time) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(time); + calendar.add(Calendar.MINUTE, minute); + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(calendar.getTime()); + } + + /** + * 获取当前时间之前或之后几分钟 + * + * @param minute + * @return + */ + public static Date getMinuteToTime(int minute, Date time) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(time); + calendar.add(Calendar.MINUTE, minute); + return calendar.getTime(); + } + + /** + * @return java.lang.String + * @Author 陈树森 + * @Description 将数据库里的timestamp转为指定日期数据格式字符串 + * @Date 16:11 2018/11/26 0026 + * @Param [timestamp, format] + **/ + public static String timestampToString(Timestamp timestamp, String format) { + return new SimpleDateFormat(format).format(timestamp); + } + + /** + * @return java.lang.String + * @Author 陈树森 + * @Description 将数据库里的timestamp转为默认格式 + * @Date 16:12 2018/11/26 0026 + * @Param [timestamp] + **/ + public static String timestampDefaultFormat(Timestamp timestamp) { + if (timestamp == null) { + return null; + } + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(timestamp); + } + + /** + * @return + * @Author 陈树森 + * @Description date默认格式yyyy-MM-dd HH:mm:ss + * @Date 11:48 2018/12/26 0026 + * @Param [date] + **/ + public static String dateDefaultFormat(Date date) { + if (date == null) { + return null; + } + return getFormatDate(date, "yyyy-MM-dd HH:mm:ss"); + } + + /** + * @return date + * @Description 获取当前时间N天后的时间 + * @Date 2019/01/09 13:58:00 + * @Param [day] + */ + public static Date getTimeByDay(int day) { + return getTimeByDay(new Date(), day); + } + + /** + * 获取指定时间N天后的时间 + * + * @param d1 + * @param day + * @return + */ + public static Date getTimeByDay(Date d1, int day) { + long temp1 = d1.getTime(); + temp1 = temp1 + day * 0x5265c00L; + return (new Date(temp1)); + } + + + /** + * 两个日期相差的分钟数 + * + * @param date1 + * @param date2 + * @return + */ + public static long getMinuteSpace(Date date1, Date date2) { + Calendar calendar1 = Calendar.getInstance(); + Calendar calendar2 = Calendar.getInstance(); + calendar1.setTime(date1); + calendar2.setTime(date2); + long milliseconds1 = calendar1.getTimeInMillis(); + long milliseconds2 = calendar2.getTimeInMillis(); + long diff = milliseconds2 - milliseconds1; + long diffDays = diff / (60 * 1000); + return Math.abs(diffDays); + } + + /** + * 获取当前时间的小时 + * + * @param date + * @return + */ + public static Integer getHourOfDate(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + return cal.get(Calendar.HOUR_OF_DAY); + } + + /** + * 获取指定时间,在每月的几号 + * + * @param date + * @return + */ + public static Integer getDayOfMonth(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + return cal.get(Calendar.DAY_OF_MONTH); + } + + /** + * 两个日期相差的天数 + * + * @param date1 + * @param date2 + * @return + */ + public static long getDateSpace(Date date1, Date date2) { + Calendar calendar1 = Calendar.getInstance(); + Calendar calendar2 = Calendar.getInstance(); + calendar1.setTime(date1); + calendar2.setTime(date2); + long milliseconds1 = calendar1.getTimeInMillis(); + long milliseconds2 = calendar2.getTimeInMillis(); + long diff = milliseconds2 - milliseconds1; + long diffDays = diff / (24 * 60 * 60 * 1000); + return Math.abs(diffDays); + } + + /** + * 获取今天还剩下多少秒 + * + * @return + */ + public static int getMiao(int num) { + Calendar curDate = Calendar.getInstance(); + //获取当前时间的第二天早上num点 + Calendar tommorowDate = new GregorianCalendar(curDate.get(Calendar.YEAR), curDate.get(Calendar.MONTH), curDate.get(Calendar.DATE) + 1, num, 0, 0); + return (int) (tommorowDate.getTimeInMillis() - curDate.getTimeInMillis()) / 1000; + } + + /** + * 时间相差多少秒 + */ + public static long secondsDifferen(Date d1, Date d2) { + long temp1 = d1.getTime(); + long temp2 = d2.getTime(); + return (long) ((temp2 - temp1) / 1000); + } + + /** + * 多少月后的时间 + */ + public static Date getTimeByMonth(int month) { + return getTimeByMonth(new Date(), month); + } + + public static Date getTimeByMonth(Date m1, int month) { + Calendar c = Calendar.getInstance(); + c.setTime(m1); + c.add(Calendar.MONTH, month); + Date m = c.getTime(); + return m; + } + + /** + * 天数转换为秒数 + */ + public static int dayConvertSecond(int day) { + //一天的秒数 + int secondOfDay = 60 * 60 * 24; + //计算N天的秒数 + return day * secondOfDay; + } + + /** + * 检查年月日是否合法 + * + * @param ymd + * @return + */ + public static boolean checkYearMonthDay(String ymd) { + if (ymd == null || ymd.length() == 0) { + return false; + } + String s = ymd.replaceAll("[/\\- ]", "/"); + SimpleDateFormat format = new SimpleDateFormat("yyyy/MM/dd"); + try { + Date date = format.parse(s); + return true; + } catch (ParseException e) { + return false; + } + } + + + /** + * Java通过生日计算星座 生日格式必须为 yyyy-MM-dd + * + * @param birthday + * @return + */ + public static String getConstellation(String birthday) { + int month = 0; + int day = 0; + Date birthdayDateTime = DateTimeUtils.parseString2Date(birthday, "yyyy-MM-dd"); + if (null != birthdayDateTime) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(birthdayDateTime); + month = calendar.get(Calendar.MONTH) + 1; + day = calendar.get(Calendar.DAY_OF_MONTH); + } + return day < dayArr[month - 1] ? constellationArr[month - 1] + : constellationArr[month]; + } + + /** + * 通过生日计算属相 + * + * @param year + * @return + */ + public static String getYear(int year) { + if (year < 1900) { + return "未知"; + } + int start = 1900; + String[] years = new String[]{"鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", + "猴", "鸡", "狗", "猪"}; + return years[(year - start) % years.length]; + } + + /** + * 时分秒判断大小 + * + * @param s1 时间比较 + * @param s2 + * @return boolean true 大于 false 小于 + */ + public static boolean compareVehicle(String s1, String s2) { + boolean flag = true; + SimpleDateFormat sf = new SimpleDateFormat("HH:mm"); + try { + Date date1 = sf.parse(s1); + Date date2 = sf.parse(s2); + if (date1.getTime() > date2.getTime()) { + flag = true; + } else { + flag = false; + } + } catch (ParseException e) { + e.printStackTrace(); + } + return flag; + } + + /** + * 获得某天最大时间 2018-03-20 23:59:59 + * + * @param date + * @return + */ + public static String getEndOfDay(Date date) { + Calendar calendarEnd = Calendar.getInstance(); + calendarEnd.setTime(date); + calendarEnd.set(Calendar.HOUR_OF_DAY, 23); + calendarEnd.set(Calendar.MINUTE, 59); + calendarEnd.set(Calendar.SECOND, 59); + //防止mysql自动加一秒,毫秒设为0 + calendarEnd.set(Calendar.MILLISECOND, 0); + + return dateDefaultFormat(calendarEnd.getTime()); + } + + /** + * 获得某天最小时间 2018-03-20 00:00:00 + * + * @param date + * @return + */ + public static String getFirstOfDay(Date date) { + Calendar calendarStart = Calendar.getInstance(); + calendarStart.setTime(date); + calendarStart.set(Calendar.HOUR_OF_DAY, 0); + calendarStart.set(Calendar.MINUTE, 0); + calendarStart.set(Calendar.SECOND, 0); + calendarStart.set(Calendar.MILLISECOND, 0); + return dateDefaultFormat(calendarStart.getTime()); + } + + /** + * 两个时间相差距离多少天多少小时多少分多少秒 + * + * @param str1 时间参数 1 格式:1990-01-01 12:00:00 + * @param str2 时间参数 2 格式:2009-01-01 12:00:00 + * @return long[] 返回值为:{天, 时, 分, 秒} + */ + public static long[] getDistanceTimes(Date str1, Date str2) { + DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date one = str1; + Date two = str2; + long day = 0; + long hour = 0; + long min = 0; + long sec = 0; + long time1 = one.getTime(); + long time2 = two.getTime(); + long diff; + if (time1 < time2) { + diff = time2 - time1; + } else { + diff = time1 - time2; + } + day = diff / (24 * 60 * 60 * 1000); + hour = (diff / (60 * 60 * 1000) - day * 24); + min = ((diff / (60 * 1000)) - day * 24 * 60 - hour * 60); + sec = (diff / 1000 - day * 24 * 60 * 60 - hour * 60 * 60 - min * 60); + long[] times = {day, hour, min, sec}; + return times; + } + + /** + * 获取多少秒之后的日期 + * + * @param date + * @param second + * @return + */ + public static Date getLastDateBySecond(Date date, Integer second) { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//24小时制 + //SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");//12小时制 + if (date == null) { + return null; + } + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.add(Calendar.SECOND, second);//24小时制 + date = cal.getTime(); + cal = null; + return date; + } + + /** + * 根据年龄获取年份 + * + * @param age + * @return + */ + public static String getCalculateAge(int age) { + String toDay = DateTimeUtils.getFormatDate(new Date(), DateTimeUtils.YEAR_DATE_FORMAT); + if (age > Integer.valueOf(toDay)) { + return null; + } + Integer year = Integer.valueOf(toDay) - age; + return year.toString(); + } + + /** + * 获取N天前的字符串格式日期 + * + * @param format 如:yyyy-MM-dd + * @param nDay + * @return + */ + public static String getNDayAgoByDay(String format, Integer nDay) { + Calendar calendar = Calendar.getInstance(); + SimpleDateFormat sdf = new SimpleDateFormat(format); + calendar.add(Calendar.DATE, nDay); + String ndaysAgo = sdf.format(calendar.getTime()); + return ndaysAgo; + } + + /** + * 获取时间差,不考虑时分秒 + * + * @param dateBefore + * @param dateAfter + * @return int 1、昨天 2、近7天 3、近15天 4、近30天 + */ + public static Integer getTimeScope(Date dateBefore, Date dateAfter) { + int daysDifferent = compareDays(dateBefore, dateAfter);//1、昨天 2、近7天 3、近15天 4、近30天 + if (daysDifferent == 1) { + return 1; + } else if (daysDifferent >= 2 && daysDifferent <= 7) { + return 2; + } else if (daysDifferent >= 8 && daysDifferent <= 15) { + return 3; + } + return 4; + } + + public static int compareDays(Date datebefore, Date dateAfter) { + Calendar calendar1 = Calendar.getInstance(); + Calendar calendar2 = Calendar.getInstance(); + calendar1.setTime(datebefore); + calendar2.setTime(dateAfter); + int day1 = calendar1.get(Calendar.DAY_OF_YEAR); + int day2 = calendar2.get(Calendar.DAY_OF_YEAR); + int year1 = calendar1.get(Calendar.YEAR); + int year2 = calendar2.get(Calendar.YEAR); + if (year1 > year2) { + int tempyear = year1; + int tempday = day1; + day1 = day2; + day2 = tempday; + year1 = year2; + year2 = tempyear; + } + if (year1 == year2) { + return day2 - day1; + } else { + int DayCount = 0; + for (int i = year1; i < year2; i++) { + if (i % 4 == 0 && i % 100 != 0 || i % 400 == 0) { + DayCount += 366; + } else { + DayCount += 365; + } + } + return DayCount + (day2 - day1); + } + } + + public static Date asDate(LocalDateTime localDateTime) { + return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); + } + + /** + * 小于10 追加0 + * + * @param obj + * @return + */ + public static String appendZero(Integer obj) { + if (obj < 10) { + return '0' + String.valueOf(obj); + } else { + return String.valueOf(obj); + } + } + + /* + * 功能描述:
+ * 获取时间与当前时间相差分钟数 + * @param: time + * @Return: int + * @Author: 97342 + * @Date: 2020/8/22 12:40 + */ + public static int getMinuteByNow(Date time) throws Exception { + Date date = new Date(); + long millsecond = date.getTime() - time.getTime(); + if (millsecond < 0) { + throw new Exception("时间超前异常"); + } + int minute = (int) millsecond / 60000; + return minute; + } + + /* + * 功能描述:
+ * 时间戳转LocalDateTime + * @param: time + * @Return: java.time.LocalDateTime + * @Author: 97342 + * @Date: 2020/8/27 16:17 + */ + public static LocalDateTime longToLocalDateTime(Long time) { + LocalDateTime localDateTime = new Date(time).toInstant().atOffset(ZoneOffset.of("+08:00")).toLocalDateTime(); + return localDateTime; + } + + /** + * 获取 一天的开始时间 + * + * @param localDateTime + * @return java.time.LocalDateTime + * @author BNMZY + */ + public static LocalDateTime getTimeBegin(LocalDateTime localDateTime) { + String format = DateUtil.formatLocalDateTime(localDateTime); + Date date = DateUtil.parse(format); + DateTime dateTime = DateUtil.beginOfDay(date); + return DateUtil.toLocalDateTime(dateTime); + } + + /** + * 获取 一天结束的时间 + * + * @param localDateTime + * @return java.time.LocalDateTime + * @author BNMZY + */ + public static LocalDateTime getTimeEnd(LocalDateTime localDateTime) { + String format = DateUtil.formatLocalDateTime(localDateTime); + Date date = DateUtil.parse(format); + DateTime dateTime = DateUtil.endOfDay(date); + return DateUtil.toLocalDateTime(dateTime); + } + + /** + * 指定日期所在月的第一天 + * + * @param localDateTime + * @return + */ + public static LocalDateTime getMonthStart(LocalDateTime localDateTime) { + String format = DateUtil.formatLocalDateTime(localDateTime); + Date date = DateUtil.parse(format); + DateTime dateTime = DateUtil.beginOfMonth(date); + return DateUtil.toLocalDateTime(dateTime); + } + + /** + * 功能描述:
+ * LocalDateTime 转String 格式 yyyy-MM-dd HH:mm:ss + * + * @param: time + * @Return: java.lang.String + * @Author: 97342 + * @Date: 2020/9/21 15:11 + */ + public static String LocalDateTimeToString(LocalDateTime time) { + + String format = time.format(DateTimeFormatter.ofPattern(FULL_DATE_FORMAT)); + return format; + + } + + /** + * 获取上周一的日期 + * + * @param localDateTime + * @return + */ + public static LocalDateTime getLastWeekMonday(LocalDateTime localDateTime) { + String format = DateUtil.formatLocalDateTime(localDateTime); + Date date = DateUtil.parse(format); + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.add(Calendar.DAY_OF_WEEK, -7); + date = cal.getTime(); + DateTime dateTime = DateUtil.beginOfWeek(date); + return DateUtil.toLocalDateTime(dateTime); + } + + /** + * 获取本周一的日期 + * + * @param localDateTime + * @return + */ + public static LocalDateTime getThisWeekMonday(LocalDateTime localDateTime) { + String format = DateUtil.formatLocalDateTime(localDateTime); + Date date = DateUtil.parse(format); + DateTime dateTime = DateUtil.beginOfWeek(date); + return DateUtil.toLocalDateTime(dateTime); + } + + /** + * 获取上周最后一天的日期 + * + * @param localDateTime + * @return + */ + public static LocalDateTime getLastWeekSunday(LocalDateTime localDateTime) { + String format = DateUtil.formatLocalDateTime(localDateTime); + Date date = DateUtil.parse(format); + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.add(Calendar.DAY_OF_WEEK, -7); + date = cal.getTime(); + DateTime dateTime = DateUtil.endOfWeek(date); + return DateUtil.toLocalDateTime(dateTime); + } + + /** + * 获取本月第一天的日期 + * + * @return + */ + public static LocalDateTime getFirstDayOfMonth(LocalDateTime localDateTime) { + String format = DateUtil.formatLocalDateTime(localDateTime); + Date date = DateUtil.parse(format); + DateTime dateTime = DateUtil.beginOfMonth(date); + return DateUtil.toLocalDateTime(dateTime); + } + + /** + * 获取上月第一天的日期 + * + * @return + */ + public static LocalDateTime getFirstDayLastMonth(LocalDateTime localDateTime) { + String format = DateUtil.formatLocalDateTime(localDateTime); + Date date = DateUtil.parse(format); + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.add(Calendar.MONTH, -1); + date = cal.getTime(); + DateTime dateTime = DateUtil.beginOfMonth(date); + return DateUtil.toLocalDateTime(dateTime); + } + + /** + * 获取上月最后一天的日期 + * + * @return + */ + public static LocalDateTime getLastDayLastMonth(LocalDateTime localDateTime) { + String format = DateUtil.formatLocalDateTime(localDateTime); + Date date = DateUtil.parse(format); + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.add(Calendar.MONTH, -1); + date = cal.getTime(); + DateTime dateTime = DateUtil.endOfMonth(date); + return DateUtil.toLocalDateTime(dateTime); + } + + /** + * + * @param startDay 开始日期 + * @param endDay 结束日期 + * @return 返回包含开始结束日期的集合 + */ + public static List getDays(LocalDate startDay, LocalDate endDay){ + List days = new ArrayList<>(); + LocalDate start= ObjectUtil.clone(startDay); + LocalDate end= ObjectUtil.clone(endDay); + days.add(start); + while (start.isBefore(end)){ + start=start.plusDays(1L); + days.add(start); + } + Collections.reverse(days); + return days; + } + +} \ No newline at end of file diff --git a/commom/src/main/java/com/lx/common/util/SpringContextHolder.java b/commom/src/main/java/com/lx/common/util/SpringContextHolder.java new file mode 100644 index 0000000..de7cfc9 --- /dev/null +++ b/commom/src/main/java/com/lx/common/util/SpringContextHolder.java @@ -0,0 +1,40 @@ +package com.lx.common.util; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + + +@Component +public class SpringContextHolder implements ApplicationContextAware { + + private static ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + SpringContextHolder.applicationContext = applicationContext; + } + + public static ApplicationContext getApplicationContext() { + assertApplicationContext(); + return applicationContext; + } + + public static T getBean(String beanName) { + assertApplicationContext(); + return (T) applicationContext.getBean(beanName); + } + + public static T getBean(Class requiredType) { + assertApplicationContext(); + return applicationContext.getBean(requiredType); + } + + private static void assertApplicationContext() { + if (SpringContextHolder.applicationContext == null) { + throw new RuntimeException("applicaitonContext属性为null,请检查是否注入了SpringContextHolder!"); + } + } + +} diff --git a/im-platform/pom.xml b/im-platform/pom.xml new file mode 100644 index 0000000..dc245de --- /dev/null +++ b/im-platform/pom.xml @@ -0,0 +1,83 @@ + + + + lx-im + com.lx + ${im.version} + + 4.0.0 + + im-platform + + + + + com.lx + commom + ${im.version} + + + org.springframework.boot + spring-boot + + + org.springframework.boot + spring-boot-starter-web + + + com.baomidou + mybatis-plus-boot-starter + + + com.alibaba + druid + + + mysql + mysql-connector-java + + + org.springframework.boot + spring-boot-starter-jdbc + + + io.springfox + springfox-swagger2 + ${swagger.version} + + + io.springfox + springfox-swagger-ui + ${swagger.version} + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.session + spring-session-data-redis + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + \ No newline at end of file diff --git a/im-platform/src/main/java/com/lx/implatform/ImplatformApp.java b/im-platform/src/main/java/com/lx/implatform/ImplatformApp.java new file mode 100644 index 0000000..05cc95a --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/ImplatformApp.java @@ -0,0 +1,19 @@ +package com.lx.implatform; + +import lombok.extern.slf4j.Slf4j; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + + +@Slf4j +@MapperScan(basePackages = {"com.lx.implatform.mapper"}) +@ComponentScan(basePackages={"com.lx"}) +@SpringBootApplication +public class ImplatformApp { + + public static void main(String[] args) { + SpringApplication.run(ImplatformApp.class); + } +} diff --git a/im-platform/src/main/java/com/lx/implatform/config/RedisConfig.java b/im-platform/src/main/java/com/lx/implatform/config/RedisConfig.java new file mode 100644 index 0000000..2f4712d --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/config/RedisConfig.java @@ -0,0 +1,106 @@ +package com.lx.implatform.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.SimpleCacheErrorHandler; +import org.springframework.cache.interceptor.SimpleCacheResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import javax.annotation.Resource; +import java.time.Duration; + +/** + * redis配置 + * @author zsq + */ +@EnableCaching +@Configuration +public class RedisConfig extends CachingConfigurerSupport { + + @Resource + private RedisConnectionFactory factory; + + + /** + * 重写Redis序列化方式,使用Json方式: + * 当我们的数据存储到Redis的时候,我们的键(key)和值(value)都是通过Spring提供的Serializer序列化到数据库的。RedisTemplate默认使用的是JdkSerializationRedisSerializer,StringRedisTemplate默认使用的是StringRedisSerializer。 + * Spring Data JPA为我们提供了下面的Serializer: + * GenericToStringSerializer、Jackson2JsonRedisSerializer、JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、OxmSerializer、StringRedisSerializer。 + * 在此我们将自己配置RedisTemplate并定义Serializer。 + * + * @param redisConnectionFactory + * @return + */ + @Primary + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + + // 设置值(value)的序列化采用jackson2JsonRedisSerializer + redisTemplate.setValueSerializer(jackson2JsonRedisSerializer()); + redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer()); + // 设置键(key)的序列化采用StringRedisSerializer。 + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } + + @Bean + public Jackson2JsonRedisSerializer jackson2JsonRedisSerializer(){ + Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); + ObjectMapper om = new ObjectMapper(); + om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + // 解决jackson2无法反序列化LocalDateTime的问题 + om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + om.registerModule(new JavaTimeModule()); + om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); + jackson2JsonRedisSerializer.setObjectMapper(om); + return jackson2JsonRedisSerializer; + } + + + @Bean + @Override + public CacheResolver cacheResolver() { + return new SimpleCacheResolver(cacheManager()); + } + + @Bean + @Override + public CacheErrorHandler errorHandler() { + // 用于捕获从Cache中进行CRUD时的异常的回调处理器。 + return new SimpleCacheErrorHandler(); + } + + @Bean + @Override + public CacheManager cacheManager() { + RedisCacheConfiguration cacheConfiguration = + RedisCacheConfiguration.defaultCacheConfig() + .disableCachingNullValues() + .entryTtl(Duration.ofMinutes(15)) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer())); + return RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration).build(); + + } +} diff --git a/im-platform/src/main/java/com/lx/implatform/config/SwaggerConfig.java b/im-platform/src/main/java/com/lx/implatform/config/SwaggerConfig.java new file mode 100644 index 0000000..c6866ac --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/config/SwaggerConfig.java @@ -0,0 +1,42 @@ +package com.lx.implatform.config; + +import io.swagger.annotations.ApiOperation; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + + +@Configuration +@EnableSwagger2 +public class SwaggerConfig { + + @Bean + public Docket createRestApi() { + + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .select() + //这里采用包含注解的方式来确定要显示的接口 + .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) + //这里采用包扫描的方式来确定要显示的接口 + .paths(PathSelectors.any()) + .build(); + + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("uaa service Doc") + .description("用户授权服务Api文档") + .termsOfServiceUrl("http://www.baidu.com/") + .version("1.0") + .build(); + } + +} diff --git a/im-platform/src/main/java/com/lx/implatform/config/WebSecurityConfg.java b/im-platform/src/main/java/com/lx/implatform/config/WebSecurityConfg.java new file mode 100644 index 0000000..80f36e1 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/config/WebSecurityConfg.java @@ -0,0 +1,180 @@ +package com.lx.implatform.config; + +import com.alibaba.fastjson.JSON; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.lx.common.enums.ResultCode; +import com.lx.common.result.Result; +import com.lx.common.result.ResultUtils; +import com.lx.implatform.service.IUserService; +import com.lx.implatform.session.UserSession; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.*; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; + +import java.io.PrintWriter; + +/* + * SpringSecurity安全框架配置 + * @Author Blue + * @Date 2022/10/21 + */ +@Slf4j +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled=true) +public class WebSecurityConfg extends WebSecurityConfigurerAdapter { + + + @Value("${web-ui.login-page}") + private String loginPage; + + @Qualifier("securityUserDetailsServiceImpl") + @Autowired + private UserDetailsService userDetailsService; + + @Autowired + private IUserService userService; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests() + .antMatchers("/login","/logout","/register","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**") + .permitAll() + .anyRequest() //任何其它请求 + .authenticated() //都需要身份认证 + .and() + //2、登录配置表单认证方式 + .formLogin() + .loginPage(loginPage)//自定义登录页面的url + .usernameParameter("username")//设置登录账号参数,与表单参数一致 + .passwordParameter("password")//设置登录密码参数,与表单参数一致 + .loginProcessingUrl("/login")//配置默认登录入口 + .successHandler(successHandler()) + .failureHandler(failureHandler()) + .and() + //3、注销 + .logout() + .logoutUrl("/logout") + .logoutSuccessHandler(logoutHandler()) + .permitAll() + .and() + //4、session管理 + .sessionManagement() + .invalidSessionUrl(loginPage) //失效后跳转到登陆页面 + .and() + //5、禁用跨站csrf攻击防御 + .csrf() + .disable() + .exceptionHandling() + .authenticationEntryPoint(entryPoint()); + + } + + + + @Bean + AuthenticationFailureHandler failureHandler(){ + return (request, response, exception) -> { + response.setContentType("application/json;charset=utf-8"); + PrintWriter out = response.getWriter(); + Result result = ResultUtils.error(ResultCode.LOGIN_ERROR,exception.getMessage()); + if (exception instanceof LockedException) { + result =ResultUtils.error(ResultCode.LOGIN_ERROR,"账户被锁定,请联系管理员!"); + } else if (exception instanceof CredentialsExpiredException) { + result = ResultUtils.error(ResultCode.LOGIN_ERROR,"密码过期,请联系管理员!"); + } else if (exception instanceof AccountExpiredException) { + result =ResultUtils.error(ResultCode.LOGIN_ERROR,"账户过期,请联系管理员!"); + } else if (exception instanceof DisabledException) { + result = ResultUtils.error(ResultCode.LOGIN_ERROR,"账户被禁用,请联系管理员!"); + } else if (exception instanceof BadCredentialsException) { + result =ResultUtils.error(ResultCode.LOGIN_ERROR,"用户名或者密码输入错误,请重新输入!"); + } + out.write(new ObjectMapper().writeValueAsString(result)); + out.flush(); + out.close(); + }; + } + + @Bean + AuthenticationSuccessHandler successHandler(){ + return (request, response, authentication) -> { + // 响应 + response.setContentType("application/json;charset=utf-8"); + PrintWriter out = response.getWriter(); + Result result = ResultUtils.success(); + out.write(new ObjectMapper().writeValueAsString(result)); + out.flush(); + out.close(); + }; + } + + + @Bean + LogoutSuccessHandler logoutHandler(){ + return (request, response, authentication) -> { + + User useDetail = (User)authentication.getPrincipal(); + String strJson = useDetail.getUsername(); + UserSession userSession = JSON.parseObject(strJson,UserSession.class); + log.info("{}退出", userSession.getUserName()); + + // 响应 + response.setContentType("application/json;charset=utf-8"); + PrintWriter out = response.getWriter(); + Result result = ResultUtils.success(); + out.write(new ObjectMapper().writeValueAsString(result)); + out.flush(); + out.close(); + }; + } + + @Bean + AuthenticationEntryPoint entryPoint(){ + return (request, response, exception) -> { + response.setContentType("application/json;charset=utf-8"); + PrintWriter out = response.getWriter(); + Result result = ResultUtils.error(ResultCode.NO_LOGIN); + out.write(new ObjectMapper().writeValueAsString(result)); + out.flush(); + out.close(); + }; + } + + + + + @Bean + public PasswordEncoder passwordEncoder(){ + // 使用BCrypt加密密码 + return new BCryptPasswordEncoder(); + } + + /** + * 密码加密 + * @param auth + * @throws Exception + */ + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); + } + + +} diff --git a/im-platform/src/main/java/com/lx/implatform/controller/FriendsController.java b/im-platform/src/main/java/com/lx/implatform/controller/FriendsController.java new file mode 100644 index 0000000..0a1c7ac --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/controller/FriendsController.java @@ -0,0 +1,58 @@ +package com.lx.implatform.controller; + + +import com.lx.common.result.Result; +import com.lx.common.result.ResultUtils; +import com.lx.common.util.BeanUtils; +import com.lx.implatform.entity.Friends; +import com.lx.implatform.service.IFriendsService; +import com.lx.implatform.session.SessionContext; +import com.lx.implatform.vo.FriendsVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.constraints.NotEmpty; +import java.util.List; +import java.util.stream.Collectors; + +@Api(tags = "好友相关API") +@RestController +@RequestMapping("/friends") +public class FriendsController { + + @Autowired + private IFriendsService friendsService; + + @GetMapping("/list") + @ApiOperation(value = "好友列表",notes="获取好友列表") + public Result findFriends(){ + List friendsList = friendsService.findFriendsByUserId(SessionContext.getSession().getId()); + List vos = friendsList.stream().map(f->{ + FriendsVO vo = BeanUtils.copyProperties(f,FriendsVO.class); + return vo; + }).collect(Collectors.toList()); + return ResultUtils.success(vos); + } + + + + @PostMapping("/add") + @ApiOperation(value = "添加好友",notes="双方建立好友关系") + public Result addFriends(@NotEmpty(message = "好友id不可为空") @RequestParam("friendId") Long friendId){ + friendsService.addFriends(friendId); + return ResultUtils.success(); + } + + @DeleteMapping("/delete") + @ApiOperation(value = "删除好友",notes="解除好友关系") + public Result delFriends(@NotEmpty(message = "好友id不可为空") @RequestParam("friendId") Long friendId){ + friendsService.delFriends(friendId); + return ResultUtils.success(); + } + + + +} + diff --git a/im-platform/src/main/java/com/lx/implatform/controller/RegisterController.java b/im-platform/src/main/java/com/lx/implatform/controller/RegisterController.java new file mode 100644 index 0000000..97859cb --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/controller/RegisterController.java @@ -0,0 +1,32 @@ +package com.lx.implatform.controller; + + +import com.lx.common.result.Result; +import com.lx.common.result.ResultUtils; +import com.lx.implatform.service.IUserService; +import com.lx.implatform.vo.RegisterVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + + + +@Api(tags = "用户注册") +@RestController +public class RegisterController { + + @Autowired + private IUserService userService; + + @PostMapping("/register") + @ApiOperation(value = "用户注册",notes="用户注册") + public Result register(@Valid @RequestBody RegisterVO dto){ + userService.register(dto); + return ResultUtils.success(); + } +} diff --git a/im-platform/src/main/java/com/lx/implatform/controller/SingleMessageController.java b/im-platform/src/main/java/com/lx/implatform/controller/SingleMessageController.java new file mode 100644 index 0000000..f4d1b47 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/controller/SingleMessageController.java @@ -0,0 +1,40 @@ +package com.lx.implatform.controller; + + +import com.lx.common.result.Result; +import com.lx.common.result.ResultUtils; +import com.lx.implatform.service.ISingleMessageService; +import com.lx.implatform.vo.SingleMessageVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +@Api(tags = "单发消息相关api") +@RestController +@RequestMapping("/message/single") +public class SingleMessageController { + + @Autowired + private ISingleMessageService singleMessageService; + + @PostMapping("/send") + @ApiOperation(value = "发送消息",notes="发送单人消息") + public Result register(@Valid @RequestBody SingleMessageVO vo){ + singleMessageService.sendMessage(vo); + return ResultUtils.success(); + } + + @PostMapping("/pullUnreadMessage") + @ApiOperation(value = "拉取未读消息",notes="拉取未读消息") + public Result pullUnreadMessage(){ + singleMessageService.pullUnreadMessage(); + return ResultUtils.success(); + } +} + diff --git a/im-platform/src/main/java/com/lx/implatform/controller/UserController.java b/im-platform/src/main/java/com/lx/implatform/controller/UserController.java new file mode 100644 index 0000000..9ad4e39 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/controller/UserController.java @@ -0,0 +1,59 @@ +package com.lx.implatform.controller; + + +import com.lx.common.result.Result; +import com.lx.common.result.ResultUtils; +import com.lx.common.util.BeanUtils; +import com.lx.implatform.entity.User; +import com.lx.implatform.service.IUserService; +import com.lx.implatform.session.SessionContext; +import com.lx.implatform.session.UserSession; +import com.lx.implatform.vo.UserVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.constraints.NotEmpty; +import java.util.List; + + +@Api(tags = "用户相关API") +@RestController +@RequestMapping("/user") +public class UserController { + + @Autowired + private IUserService userService; + + + @GetMapping("/online") + public Result checkOnline(@NotEmpty @RequestParam("userIds") String userIds){ + List onlineIds = userService.checkOnline(userIds); + return ResultUtils.success(onlineIds); + } + + @GetMapping("/self") + public Result findSelfInfo(){ + UserSession session = SessionContext.getSession(); + User user = userService.getById(session.getId()); + UserVO userVO = BeanUtils.copyProperties(user,UserVO.class); + return ResultUtils.success(userVO); + } + + + @GetMapping("/find/{id}") + public Result findByIde(@NotEmpty @PathVariable("id") long id){ + User user = userService.getById(id); + UserVO userVO = BeanUtils.copyProperties(user,UserVO.class); + return ResultUtils.success(userVO); + } + + + @GetMapping("/findByNickName") + @ApiOperation(value = "查找非好友用户",notes="查找非好友用户") + public Result findUnfriendsUser(@NotEmpty(message = "用户昵称不可为空") @RequestParam("nickName") String nickName){ + return ResultUtils.success( userService.findUserByNickName(nickName)); + } +} + diff --git a/im-platform/src/main/java/com/lx/implatform/entity/Friends.java b/im-platform/src/main/java/com/lx/implatform/entity/Friends.java new file mode 100644 index 0000000..63cc307 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/entity/Friends.java @@ -0,0 +1,71 @@ +package com.lx.implatform.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.activerecord.Model; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.Date; + +/** + *

+ * + *

+ * + * @author blue + * @since 2022-10-22 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("friends") +public class Friends extends Model { + + private static final long serialVersionUID=1L; + + /** + * id + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 用户id + */ + @TableField("user_id") + private Long userId; + + /** + * 好友id + */ + @TableField("friend_id") + private Long friendId; + + /** + * 用户昵称 + */ + @TableField("friend_nick_name") + private String friendNickName; + + /** + * 用户头像 + */ + @TableField("friend_head_image") + private String friendHeadImage; + + /** + * 创建时间 + */ + @TableField("created_time") + private Date createdTime; + + + @Override + protected Serializable pkVal() { + return this.id; + } + +} diff --git a/im-platform/src/main/java/com/lx/implatform/entity/SingleMessage.java b/im-platform/src/main/java/com/lx/implatform/entity/SingleMessage.java new file mode 100644 index 0000000..ddf5e38 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/entity/SingleMessage.java @@ -0,0 +1,78 @@ +package com.lx.implatform.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.activerecord.Model; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.Date; + +/** + *

+ * + *

+ * + * @author blue + * @since 2022-10-01 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("single_message") +public class SingleMessage extends Model { + + private static final long serialVersionUID=1L; + + /** + * id + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 发送用户id + */ + @TableField("send_user_id") + private Long sendUserId; + + /** + * 接收用户id + */ + @TableField("recv_user_id") + private Long recvUserId; + + /** + * 发送内容 + */ + @TableField("content") + private String content; + + /** + * 消息类型 + */ + @TableField("type") + private Integer type; + + /** + * 状态 + */ + @TableField("status") + private Integer status; + + + /** + * 发送时间 + */ + @TableField("send_time") + private Date sendTime; + + + @Override + protected Serializable pkVal() { + return this.id; + } + +} diff --git a/im-platform/src/main/java/com/lx/implatform/entity/User.java b/im-platform/src/main/java/com/lx/implatform/entity/User.java new file mode 100644 index 0000000..7902f7c --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/entity/User.java @@ -0,0 +1,77 @@ +package com.lx.implatform.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.activerecord.Model; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.Date; + +/** + *

+ * + *

+ * + * @author blue + * @since 2022-10-01 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("user") +public class User extends Model { + + private static final long serialVersionUID=1L; + + /** + * id + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 用户名 + */ + @TableField("user_name") + private String userName; + + /** + * 用户名 + */ + @TableField("nick_name") + private String nickName; + + /** + * 用户名 + */ + @TableField("head_image") + private String headImage; + + /** + * 密码(明文) + */ + @TableField("password") + private String password; + + /** + * 最后登录时间 + */ + @TableField("last_login_time") + private Date lastLoginTime; + + /** + * 创建时间 + */ + @TableField("created_time") + private Date createdTime; + + + @Override + protected Serializable pkVal() { + return this.id; + } + +} diff --git a/im-platform/src/main/java/com/lx/implatform/exception/GlobalException.java b/im-platform/src/main/java/com/lx/implatform/exception/GlobalException.java new file mode 100644 index 0000000..80cb7d2 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/exception/GlobalException.java @@ -0,0 +1,35 @@ +package com.lx.implatform.exception; + +import com.lx.common.enums.ResultCode; +import lombok.Data; + +import java.io.Serializable; + + +@Data +public class GlobalException extends RuntimeException implements Serializable { + private static final long serialVersionUID = 8134030011662574394L; + private Integer code; + private String message; + + public GlobalException(Integer code, String message){ + this.code=code; + this.message=message; + } + + public GlobalException(ResultCode resultCode, String message){ + this.code = resultCode.getCode(); + this.message=message; + } + + public GlobalException(ResultCode resultCode) { + this.code = resultCode.getCode(); + this.message = resultCode.getMsg(); + } + + public GlobalException(String message){ + this.code= ResultCode.PROGRAM_ERROR.getCode(); + this.message=message; + } + +} diff --git a/im-platform/src/main/java/com/lx/implatform/exception/GlobalExceptionHandler.java b/im-platform/src/main/java/com/lx/implatform/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..b001fb7 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/exception/GlobalExceptionHandler.java @@ -0,0 +1,98 @@ +package com.lx.implatform.exception; + +import cn.hutool.json.JSONException; +import com.lx.common.enums.ResultCode; +import com.lx.common.result.Result; +import com.lx.common.result.ResultUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.lang.reflect.UndeclaredThrowableException; +import java.time.format.DateTimeParseException; +import java.util.List; + + +@ControllerAdvice +@ResponseBody +@Slf4j +public class GlobalExceptionHandler { + + @ExceptionHandler(value = Exception.class) + public Result handleException(Exception e){ + if(e instanceof GlobalException) { + GlobalException ex = (GlobalException)e; + log.error("全局异常捕获:msg:{},log:{},{}" , ex.getMessage(), e); + return ResultUtils.error(ex.getCode(), ex.getMessage()); + } + else if(e instanceof UndeclaredThrowableException) { + GlobalException ex = (GlobalException) e.getCause(); + log.error("全局异常捕获:msg:{},log:{},{}" , ex.getMessage(), e); + return ResultUtils.error(ex.getCode(), ex.getMessage()); + }else{ + log.error("全局异常捕获:msg:{},{}" , e.getMessage(), e); + return ResultUtils.error(ResultCode.PROGRAM_ERROR); + } + } + + + /** + * 数据解析错误 + **/ + @ExceptionHandler(value = HttpMessageNotReadableException.class) + public Result handleMessageNotReadableException(HttpMessageNotReadableException e){ + log.error("全局异常捕获:msg:{}" , e.getMessage()); + Throwable t = e.getCause(); + if(t instanceof JSONException){ + t = t.getCause(); + if(t instanceof DateTimeParseException){ + return ResultUtils.error(ResultCode.PROGRAM_ERROR, "日期格式不正确"); + } + return ResultUtils.error(ResultCode.PROGRAM_ERROR, "数据格式不正确"); + } + return ResultUtils.error(ResultCode.PROGRAM_ERROR); + } + + /** + * 处理请求参数格式错误 @RequestBody上validate失败后抛出的异常 + * @param exception + * @return + */ + @ExceptionHandler(value = { MethodArgumentNotValidException.class}) + @ResponseStatus(HttpStatus.OK) + public Result handleValidationExceptionHandler(MethodArgumentNotValidException exception) { + BindingResult bindResult = exception.getBindingResult(); + String msg; + if (bindResult != null && bindResult.hasErrors()) { + msg = bindResult.getAllErrors().get(0).getDefaultMessage(); + if (msg.contains("NumberFormatException")) { + msg = "参数类型错误!"; + } + }else { + msg = "系统繁忙,请稍后重试..."; + } + return ResultUtils.error(ResultCode.PROGRAM_ERROR,msg); + } + + + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.OK) + public Result handleBindException(BindException e){ + //抛出异常可能不止一个 输出为一个List集合 + List errors = e.getAllErrors(); + //取第一个异常 + ObjectError error = errors.get(0); + //获取异常信息 + String errorMsg = error.getDefaultMessage(); + return ResultUtils.error(ResultCode.PROGRAM_ERROR,errorMsg); + } + +} diff --git a/im-platform/src/main/java/com/lx/implatform/mapper/FriendsMapper.java b/im-platform/src/main/java/com/lx/implatform/mapper/FriendsMapper.java new file mode 100644 index 0000000..978fa27 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/mapper/FriendsMapper.java @@ -0,0 +1,16 @@ +package com.lx.implatform.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.lx.implatform.entity.Friends; + +/** + *

+ * Mapper 接口 + *

+ * + * @author blue + * @since 2022-10-22 + */ +public interface FriendsMapper extends BaseMapper { + +} diff --git a/im-platform/src/main/java/com/lx/implatform/mapper/SingleMessageMapper.java b/im-platform/src/main/java/com/lx/implatform/mapper/SingleMessageMapper.java new file mode 100644 index 0000000..54181d2 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/mapper/SingleMessageMapper.java @@ -0,0 +1,16 @@ +package com.lx.implatform.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.lx.implatform.entity.SingleMessage; + +/** + *

+ * Mapper 接口 + *

+ * + * @author blue + * @since 2022-10-01 + */ +public interface SingleMessageMapper extends BaseMapper { + +} diff --git a/im-platform/src/main/java/com/lx/implatform/mapper/UserMapper.java b/im-platform/src/main/java/com/lx/implatform/mapper/UserMapper.java new file mode 100644 index 0000000..7d88b05 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/mapper/UserMapper.java @@ -0,0 +1,16 @@ +package com.lx.implatform.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.lx.implatform.entity.User; + +/** + *

+ * Mapper 接口 + *

+ * + * @author blue + * @since 2022-10-01 + */ +public interface UserMapper extends BaseMapper { + +} diff --git a/im-platform/src/main/java/com/lx/implatform/service/IFriendsService.java b/im-platform/src/main/java/com/lx/implatform/service/IFriendsService.java new file mode 100644 index 0000000..d8d4a3e --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/service/IFriendsService.java @@ -0,0 +1,26 @@ +package com.lx.implatform.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.lx.implatform.entity.Friends; + +import java.util.List; + +/** + *

+ * 服务类 + *

+ * + * @author blue + * @since 2022-10-22 + */ +public interface IFriendsService extends IService { + + Boolean isFriends(Long userId1,long userId2); + + List findFriendsByUserId(long UserId); + + void addFriends(long friendId); + + void delFriends(long friendId); + +} diff --git a/im-platform/src/main/java/com/lx/implatform/service/ISingleMessageService.java b/im-platform/src/main/java/com/lx/implatform/service/ISingleMessageService.java new file mode 100644 index 0000000..1e61190 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/service/ISingleMessageService.java @@ -0,0 +1,21 @@ +package com.lx.implatform.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.lx.implatform.entity.SingleMessage; +import com.lx.implatform.vo.SingleMessageVO; + +/** + *

+ * 服务类 + *

+ * + * @author blue + * @since 2022-10-01 + */ +public interface ISingleMessageService extends IService { + + void sendMessage(SingleMessageVO vo); + + void pullUnreadMessage(); + +} diff --git a/im-platform/src/main/java/com/lx/implatform/service/IUserService.java b/im-platform/src/main/java/com/lx/implatform/service/IUserService.java new file mode 100644 index 0000000..516ab61 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/service/IUserService.java @@ -0,0 +1,28 @@ +package com.lx.implatform.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.lx.implatform.entity.User; +import com.lx.implatform.vo.RegisterVO; +import com.lx.implatform.vo.UserVO; + +import java.util.List; + +/** + *

+ * 用户服务类 + *

+ * + * @author blue + * @since 2022-10-01 + */ +public interface IUserService extends IService { + + void register(RegisterVO registerDTO); + + User findUserByName(String username); + + List findUserByNickName(String nickname); + + List checkOnline(String userIds); + +} diff --git a/im-platform/src/main/java/com/lx/implatform/service/impl/FriendsServiceImpl.java b/im-platform/src/main/java/com/lx/implatform/service/impl/FriendsServiceImpl.java new file mode 100644 index 0000000..2185a8c --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/service/impl/FriendsServiceImpl.java @@ -0,0 +1,104 @@ +package com.lx.implatform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.lx.common.enums.ResultCode; +import com.lx.implatform.entity.Friends; +import com.lx.implatform.entity.User; +import com.lx.implatform.exception.GlobalException; +import com.lx.implatform.mapper.FriendsMapper; +import com.lx.implatform.service.IFriendsService; +import com.lx.implatform.service.IUserService; +import com.lx.implatform.session.SessionContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + *

+ * 好友服务实现类 + *

+ * + * @author blue + * @since 2022-10-22 + */ +@Service +public class FriendsServiceImpl extends ServiceImpl implements IFriendsService { + + @Autowired + private IUserService userService; + + @Override + public List findFriendsByUserId(long UserId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().eq(Friends::getUserId,UserId); + List friendsList = this.list(queryWrapper); + return friendsList; + } + + + @Transactional + @Override + public void addFriends(long friendId) { + long userId = SessionContext.getSession().getId(); + if(userId == friendId){ + throw new GlobalException(ResultCode.PROGRAM_ERROR,"不允许添加自己为好友"); + } + // 互相绑定好友关系 + bindFriends(userId,friendId); + bindFriends(friendId,userId); + } + + + @Transactional + @Override + public void delFriends(long friendId) { + long userId = SessionContext.getSession().getId(); + // 互相解除好友关系 + unbindFriends(userId,friendId); + unbindFriends(friendId,userId); + } + + + + @Override + public Boolean isFriends(Long userId1, long userId2) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(Friends::getUserId,userId1) + .eq(Friends::getFriendId,userId2); + return this.count(queryWrapper) > 0; + } + + private void bindFriends(long userId, long friendsId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(Friends::getUserId,userId) + .eq(Friends::getFriendId,friendsId); + if(this.count(queryWrapper)==0){ + Friends friends = new Friends(); + friends.setUserId(userId); + friends.setFriendId(friendsId); + User friendsInfo = userService.getById(friendsId); + friends.setFriendHeadImage(friendsInfo.getHeadImage()); + friends.setFriendNickName(friendsInfo.getNickName()); + this.save(friends); + } + } + + + private void unbindFriends(long userId, long friendsId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(Friends::getUserId,userId) + .eq(Friends::getFriendId,friendsId); + List friendsList = this.list(queryWrapper); + friendsList.stream().forEach(friends -> { + this.removeById(friends.getId()); + }); + } + + +} diff --git a/im-platform/src/main/java/com/lx/implatform/service/impl/SecurityUserDetailsServiceImpl.java b/im-platform/src/main/java/com/lx/implatform/service/impl/SecurityUserDetailsServiceImpl.java new file mode 100644 index 0000000..9896544 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/service/impl/SecurityUserDetailsServiceImpl.java @@ -0,0 +1,43 @@ +package com.lx.implatform.service.impl; + + +import com.alibaba.fastjson.JSON; +import com.lx.common.util.BeanUtils; +import com.lx.implatform.entity.User; +import com.lx.implatform.service.IUserService; +import com.lx.implatform.session.UserSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class SecurityUserDetailsServiceImpl implements UserDetailsService { + + @Autowired + private IUserService userService; + + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userService.findUserByName(username); + if(user == null) { + throw new UsernameNotFoundException("用户不存在"); + } + //定义权限列表. + List authorities = new ArrayList<>(); + // 用户可以访问的资源名称(或者说用户所拥有的权限) 注意:必须"ROLE_"开头 + authorities.add(new SimpleGrantedAuthority("ROLE_XX")); + + UserSession session = BeanUtils.copyProperties(user,UserSession.class); + String strJson = JSON.toJSONString(session); + UserDetails userDetails = new org.springframework.security.core.userdetails.User(strJson,user.getPassword(),authorities); + return userDetails; + } +} diff --git a/im-platform/src/main/java/com/lx/implatform/service/impl/SingleMessageServiceImpl.java b/im-platform/src/main/java/com/lx/implatform/service/impl/SingleMessageServiceImpl.java new file mode 100644 index 0000000..8f64f8d --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/service/impl/SingleMessageServiceImpl.java @@ -0,0 +1,95 @@ +package com.lx.implatform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.lx.common.contant.RedisKey; +import com.lx.common.enums.MessageStatusEnum; +import com.lx.common.enums.ResultCode; +import com.lx.common.model.im.SingleMessageInfo; +import com.lx.common.util.BeanUtils; +import com.lx.implatform.entity.SingleMessage; +import com.lx.implatform.exception.GlobalException; +import com.lx.implatform.mapper.SingleMessageMapper; +import com.lx.implatform.service.IFriendsService; +import com.lx.implatform.service.ISingleMessageService; +import com.lx.implatform.session.SessionContext; +import com.lx.implatform.vo.SingleMessageVO; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +/** + *

+ * 单人消息服务实现类 + *

+ * + * @author blue + * @since 2022-10-01 + */ +@Service +public class SingleMessageServiceImpl extends ServiceImpl implements ISingleMessageService { + + @Autowired + private IFriendsService friendsService; + @Autowired + private RedisTemplate redisTemplate; + + @Override + public void sendMessage(SingleMessageVO vo) { + + Long userId = SessionContext.getSession().getId(); + Boolean isFriends = friendsService.isFriends(userId,vo.getRecvUserId()); + if(!isFriends){ + throw new GlobalException(ResultCode.PROGRAM_ERROR,"您已不是对方好友,无法发送消息"); + } + + // 保存消息 + SingleMessage msg = BeanUtils.copyProperties(vo,SingleMessage.class); + msg.setSendUserId(userId); + msg.setStatus(MessageStatusEnum.UNREAD.getCode()); + msg.setSendTime(new Date()); + this.save(msg); + + // 获取对方连接的channelId + String key = RedisKey.IM_USER_SERVER_ID+msg.getRecvUserId(); + String serverId = (String)redisTemplate.opsForValue().get(key); + // 如果对方在线,将数据存储至redis,等待拉取推送 + if(!StringUtils.isEmpty(serverId)){ + String sendKey = RedisKey.IM_UNREAD_MESSAGE + serverId; + SingleMessageInfo msgInfo = BeanUtils.copyProperties(msg,SingleMessageInfo.class); + redisTemplate.opsForList().rightPush(sendKey,msgInfo); + } + } + + @Override + public void pullUnreadMessage() { + // 获取当前连接的channelId + Long userId = SessionContext.getSession().getId(); + String key = RedisKey.IM_USER_SERVER_ID+userId; + String serverId = (String)redisTemplate.opsForValue().get(key); + if(StringUtils.isEmpty(serverId)){ + throw new GlobalException(ResultCode.PROGRAM_ERROR,"用户未建立连接"); + } + // 获取当前用户所有未读消息 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().eq(SingleMessage::getRecvUserId,userId) + .eq(SingleMessage::getStatus,MessageStatusEnum.UNREAD); + List messages = this.list(queryWrapper); + + // 上传至redis,等待推送 + if(!messages.isEmpty()){ + List infos = messages.stream().map(m->{ + SingleMessageInfo msgInfo = BeanUtils.copyProperties(m,SingleMessageInfo.class); + return msgInfo; + }).collect(Collectors.toList()); + String sendKey = RedisKey.IM_UNREAD_MESSAGE + serverId; + redisTemplate.opsForList().rightPushAll(sendKey,infos.toArray()); + } + + } +} diff --git a/im-platform/src/main/java/com/lx/implatform/service/impl/UserServiceImpl.java b/im-platform/src/main/java/com/lx/implatform/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..e40c332 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/service/impl/UserServiceImpl.java @@ -0,0 +1,95 @@ +package com.lx.implatform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.lx.common.contant.RedisKey; +import com.lx.common.enums.ResultCode; +import com.lx.common.util.BeanUtils; +import com.lx.implatform.entity.User; +import com.lx.implatform.exception.GlobalException; +import com.lx.implatform.mapper.UserMapper; +import com.lx.implatform.service.IUserService; +import com.lx.implatform.vo.RegisterVO; +import com.lx.implatform.vo.UserVO; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +/** + *

+ * 用户服务实现类 + *

+ * + * @author blue + * @since 2022-10-01 + */ +@Service +public class UserServiceImpl extends ServiceImpl implements IUserService { + + @Autowired + RedisTemplate redisTemplate; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + public void register(RegisterVO vo) { + User user = findUserByName(vo.getUserName()); + if(null != user){ + throw new GlobalException(ResultCode.USERNAME_ALREADY_REGISTER); + } + user = BeanUtils.copyProperties(vo,User.class); + user.setPassword(passwordEncoder.encode(user.getPassword())); + this.save(user); + } + + @Override + public User findUserByName(String username) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().eq(User::getUserName,username); + return this.getOne(queryWrapper); + } + + + @Override + public List findUserByNickName(String nickname) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .like(User::getNickName,nickname) + .last("limit 10"); + List users = this.list(queryWrapper); + List vos = users.stream().map(u-> { + UserVO vo = BeanUtils.copyProperties(u,UserVO.class); + vo.setOnline(isOnline(u.getId())); + return vo; + }).collect(Collectors.toList()); + + return vos; + } + + + @Override + public List checkOnline(String userIds) { + String[] idArr = userIds.split(","); + List onlineIds = new LinkedList<>(); + for(String userId:idArr){ + if(isOnline(Long.parseLong(userId))){ + onlineIds.add(Long.parseLong(userId)); + } + } + return onlineIds; + } + + + private boolean isOnline(Long userId){ + String key = RedisKey.IM_USER_SERVER_ID + userId; + String serverId = (String) redisTemplate.opsForValue().get(key); + return StringUtils.isNotEmpty(serverId); + } +} diff --git a/im-platform/src/main/java/com/lx/implatform/session/SessionContext.java b/im-platform/src/main/java/com/lx/implatform/session/SessionContext.java new file mode 100644 index 0000000..c7acfeb --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/session/SessionContext.java @@ -0,0 +1,23 @@ +package com.lx.implatform.session; + +import com.alibaba.fastjson.JSON; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; + +/* + * @Description + * @Author Blue + * @Date 2022/10/21 + */ +public class SessionContext { + + + public static UserSession getSession(){ + User useDetail = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + String strJson = useDetail.getUsername(); + UserSession userSession = JSON.parseObject(strJson,UserSession.class); + return userSession; + } + + +} diff --git a/im-platform/src/main/java/com/lx/implatform/session/UserSession.java b/im-platform/src/main/java/com/lx/implatform/session/UserSession.java new file mode 100644 index 0000000..a8896d5 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/session/UserSession.java @@ -0,0 +1,11 @@ +package com.lx.implatform.session; + +import lombok.Data; + +@Data +public class UserSession { + + private Long id; + private String userName; + private String nickName; +} diff --git a/im-platform/src/main/java/com/lx/implatform/task/PullAlreadyReadMessageTask.java b/im-platform/src/main/java/com/lx/implatform/task/PullAlreadyReadMessageTask.java new file mode 100644 index 0000000..22eae64 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/task/PullAlreadyReadMessageTask.java @@ -0,0 +1,60 @@ +package com.lx.implatform.task; + +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.lx.common.contant.RedisKey; +import com.lx.common.enums.MessageStatusEnum; +import com.lx.implatform.entity.SingleMessage; +import com.lx.implatform.service.ISingleMessageService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class PullAlreadyReadMessageTask { + + private int threadNum = 8; + + private ExecutorService executorService = Executors.newFixedThreadPool(threadNum); + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private ISingleMessageService singleMessageService; + + @PostConstruct + public void init(){ + for(int i=0;i updateWrapper = new UpdateWrapper<>(); + updateWrapper.lambda().eq(SingleMessage::getId,msgId) + .set(SingleMessage::getStatus, MessageStatusEnum.ALREADY_READ.getCode()); + singleMessageService.update(updateWrapper); + log.info("消息已读,id:{}",msgId); + } + }catch (Exception e){ + log.error(e.getMessage()); + }finally { + // 下一次循环 + executorService.submit(this); + } + } + } +} diff --git a/im-platform/src/main/java/com/lx/implatform/vo/FriendsVO.java b/im-platform/src/main/java/com/lx/implatform/vo/FriendsVO.java new file mode 100644 index 0000000..193bb82 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/vo/FriendsVO.java @@ -0,0 +1,22 @@ +package com.lx.implatform.vo; + + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel("好友信息VO") +public class FriendsVO { + + @ApiModelProperty(value = "好友id") + private Long friendId; + + + @ApiModelProperty(value = "用户昵称") + private String friendNickName; + + + @ApiModelProperty(value = "用户头像") + private String friendHeadImage; +} diff --git a/im-platform/src/main/java/com/lx/implatform/vo/RegisterVO.java b/im-platform/src/main/java/com/lx/implatform/vo/RegisterVO.java new file mode 100644 index 0000000..1f43a8b --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/vo/RegisterVO.java @@ -0,0 +1,26 @@ +package com.lx.implatform.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +@Data +@ApiModel("用户注册VO") +public class RegisterVO { + + @NotEmpty(message="用户名不可为空") + @ApiModelProperty(value = "用户名") + private String userName; + + @NotEmpty(message="用户密码不可为空") + @ApiModelProperty(value = "用户密码") + private String password; + + @NotEmpty(message="用户昵称不可为空") + @ApiModelProperty(value = "用户昵称") + private String nickName; + + +} diff --git a/im-platform/src/main/java/com/lx/implatform/vo/SingleMessageVO.java b/im-platform/src/main/java/com/lx/implatform/vo/SingleMessageVO.java new file mode 100644 index 0000000..a0d9f56 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/vo/SingleMessageVO.java @@ -0,0 +1,29 @@ +package com.lx.implatform.vo; + + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Data +@ApiModel("单发消息VO") +public class SingleMessageVO { + + + @NotNull(message="接收用户id不可为空") + @ApiModelProperty(value = "接收用户id") + private Long recvUserId; + + + @NotEmpty(message="发送内容不可为空") + @ApiModelProperty(value = "发送内容") + private String content; + + @NotNull(message="发送内容不可为空") + @ApiModelProperty(value = "消息类型") + private Integer type; + +} diff --git a/im-platform/src/main/java/com/lx/implatform/vo/UnfriendsUserVO.java b/im-platform/src/main/java/com/lx/implatform/vo/UnfriendsUserVO.java new file mode 100644 index 0000000..71f59cd --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/vo/UnfriendsUserVO.java @@ -0,0 +1,27 @@ +package com.lx.implatform.vo; + + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel("非好友用户信息VO") +public class UnfriendsUserVO { + + @ApiModelProperty(value = "id") + private Long id; + + @ApiModelProperty(value = "用户名") + private String userName; + + @ApiModelProperty(value = "用户昵称") + private String nickName; + + @ApiModelProperty(value = "头像") + private String headImage; + + @ApiModelProperty(value = "是否已是好友") + private Boolean isFriend; + +} diff --git a/im-platform/src/main/java/com/lx/implatform/vo/UserVO.java b/im-platform/src/main/java/com/lx/implatform/vo/UserVO.java new file mode 100644 index 0000000..c41e715 --- /dev/null +++ b/im-platform/src/main/java/com/lx/implatform/vo/UserVO.java @@ -0,0 +1,27 @@ +package com.lx.implatform.vo; + + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel("用户信息VO") +public class UserVO { + + @ApiModelProperty(value = "id") + private Long id; + + @ApiModelProperty(value = "用户名") + private String userName; + + @ApiModelProperty(value = "用户昵称") + private String nickName; + + @ApiModelProperty(value = "头像") + private String headImage; + + @ApiModelProperty(value = "是否在线") + private Boolean online; + +} diff --git a/im-platform/src/main/resources/application.yml b/im-platform/src/main/resources/application.yml new file mode 100644 index 0000000..410fd2d --- /dev/null +++ b/im-platform/src/main/resources/application.yml @@ -0,0 +1,28 @@ +#这是配置服务的端口 +server: + port: 8888 +#配置项目的数据源 +spring: + datasource: + driver-class-name: com.mysql.jdbc.Driver + url: jdbc:mysql://localhost:3306/lx-im?useUnicode=true&characterEncoding=utf-8 + username: root + password: root + + redis: + host: 127.0.0.1 + port: 6379 + database: 1 + +mybatis-plus: + configuration: + # 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN(下划线命名) 到经典 Java 属性名 aColumn(驼峰命名) 的类似映射 + map-underscore-to-camel-case: false + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # mapper + mapper-locations: + # *.xml的具体路径 + - classpath*:mapper/*.xml + +web-ui: + login-page: http://localhost:8080 diff --git a/im-platform/src/main/resources/db/db.sql b/im-platform/src/main/resources/db/db.sql new file mode 100644 index 0000000..69045f7 --- /dev/null +++ b/im-platform/src/main/resources/db/db.sql @@ -0,0 +1,35 @@ +use `lx-im`; +create table `user`( + `id` bigint not null auto_increment primary key comment 'id', + `user_name` varchar(255) not null comment '用户名', + `nick_name` varchar(255) not null comment '用户昵称', + `head_image` varchar(255) default '' comment '用户头像', + `password` varchar(255) not null comment '密码(明文)', + `last_login_time` datetime DEFAULT null comment '最后登录时间', + `created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间', + unique key `idx_user_name`(user_name), + key `idx_nick_name`(nick_name) +); + +create table `friends`( + `id` bigint not null auto_increment primary key comment 'id', + `user_id` bigint not null comment '用户id', + `friend_id` bigint not null comment '好友id', + `friend_nick_name` varchar(255) not null comment '用户昵称', + `friend_head_image` varchar(255) default '' comment '用户头像', + `created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间', + key `idx_user_id` (`user_id`), + key `idx_friend_id` (`friend_id`) +); + + +create table `single_message`( + `id` bigint not null auto_increment primary key comment 'id', + `send_user_id` bigint not null comment '发送用户id', + `recv_user_id` bigint not null comment '接收用户id', + `content` text comment '发送内容', + `type` tinyint(1) NOT NULL comment '消息类型 0:文字 1:图片 2:文件', + `status` tinyint(1) NOT NULL comment '状态 0:未读 1:已读 ', + `send_time` datetime DEFAULT CURRENT_TIMESTAMP comment '发送时间', + key `idx_send_recv_user_id` (`send_user_id`,`recv_user_id`) +); \ No newline at end of file diff --git a/im-server/pom.xml b/im-server/pom.xml new file mode 100644 index 0000000..8189fec --- /dev/null +++ b/im-server/pom.xml @@ -0,0 +1,53 @@ + + + + lx-im + com.lx + ${im.version} + + 4.0.0 + + im-server + + + + + com.lx + commom + ${im.version} + + + org.springframework.boot + spring-boot + + + org.springframework.boot + spring-boot-starter-web + + + io.netty + netty-all + 4.1.42.Final + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + \ No newline at end of file diff --git a/im-server/src/main/java/com/lx/implatform/imserver/IMServerApp.java b/im-server/src/main/java/com/lx/implatform/imserver/IMServerApp.java new file mode 100644 index 0000000..92ed288 --- /dev/null +++ b/im-server/src/main/java/com/lx/implatform/imserver/IMServerApp.java @@ -0,0 +1,32 @@ +package com.lx.implatform.imserver; + + +import com.lx.implatform.imserver.websocket.WebsocketServer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + + +@EnableAsync +@EnableScheduling +@ComponentScan(basePackages={"com.lx"}) +@SpringBootApplication +public class IMServerApp implements CommandLineRunner { + + @Value("${websocket.port}") + private int port; + + + public static void main(String[] args) { + SpringApplication.run(IMServerApp.class); + } + + + public void run(String... args) throws Exception { + new WebsocketServer().start(port); + } +} diff --git a/im-server/src/main/java/com/lx/implatform/imserver/config/RedisConfig.java b/im-server/src/main/java/com/lx/implatform/imserver/config/RedisConfig.java new file mode 100644 index 0000000..37109d4 --- /dev/null +++ b/im-server/src/main/java/com/lx/implatform/imserver/config/RedisConfig.java @@ -0,0 +1,53 @@ +package com.lx.implatform.imserver.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import javax.annotation.Resource; + +@Configuration +public class RedisConfig { + + @Resource + private RedisConnectionFactory factory; + + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + + // 设置值(value)的序列化采用jackson2JsonRedisSerializer + redisTemplate.setValueSerializer(jackson2JsonRedisSerializer()); + redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer()); + // 设置键(key)的序列化采用StringRedisSerializer。 + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } + + @Bean + public Jackson2JsonRedisSerializer jackson2JsonRedisSerializer(){ + Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); + ObjectMapper om = new ObjectMapper(); + om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + // 解决jackson2无法反序列化LocalDateTime的问题 + om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + om.registerModule(new JavaTimeModule()); + om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); + jackson2JsonRedisSerializer.setObjectMapper(om); + return jackson2JsonRedisSerializer; + } + +} diff --git a/im-server/src/main/java/com/lx/implatform/imserver/constant/Constant.java b/im-server/src/main/java/com/lx/implatform/imserver/constant/Constant.java new file mode 100644 index 0000000..b7763a4 --- /dev/null +++ b/im-server/src/main/java/com/lx/implatform/imserver/constant/Constant.java @@ -0,0 +1,8 @@ +package com.lx.implatform.imserver.constant; + +public class Constant { + + // public static String LOCAL_SERVER_ID = UUID.randomUUID().toString(); + + +} diff --git a/im-server/src/main/java/com/lx/implatform/imserver/task/PullUnreadMessageTask.java b/im-server/src/main/java/com/lx/implatform/imserver/task/PullUnreadMessageTask.java new file mode 100644 index 0000000..d6a0bd7 --- /dev/null +++ b/im-server/src/main/java/com/lx/implatform/imserver/task/PullUnreadMessageTask.java @@ -0,0 +1,45 @@ +package com.lx.implatform.imserver.task; + + +import com.lx.common.contant.RedisKey; +import com.lx.common.enums.WSCmdEnum; +import com.lx.common.model.im.SingleMessageInfo; +import com.lx.implatform.imserver.websocket.WebsocketChannelCtxHloder; +import com.lx.implatform.imserver.websocket.WebsocketServer; +import com.lx.implatform.imserver.websocket.processor.MessageProcessor; +import com.lx.implatform.imserver.websocket.processor.ProcessorFactory; +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; + + +@Slf4j +@Component +public class PullUnreadMessageTask { + + + @Autowired + private RedisTemplate redisTemplate; + + @Scheduled(fixedRate=100) + public void pullUnreadMessage() { + + // 从redis拉取未读消息 + String key = RedisKey.IM_UNREAD_MESSAGE + WebsocketServer.LOCAL_SERVER_ID; + List messageInfos = redisTemplate.opsForList().range(key,0,-1); + for(Object o: messageInfos){ + redisTemplate.opsForList().leftPop(key); + SingleMessageInfo messageInfo = (SingleMessageInfo)o; + ChannelHandlerContext ctx = WebsocketChannelCtxHloder.getChannelCtx(messageInfo.getRecvUserId()); + if(ctx != null){ + MessageProcessor processor = ProcessorFactory.createProcessor(WSCmdEnum.SINGLE_MESSAGE); + processor.process(ctx,messageInfo); + } + } + } +} diff --git a/im-server/src/main/java/com/lx/implatform/imserver/websocket/WebSocketHandler.java b/im-server/src/main/java/com/lx/implatform/imserver/websocket/WebSocketHandler.java new file mode 100644 index 0000000..4c4a4a6 --- /dev/null +++ b/im-server/src/main/java/com/lx/implatform/imserver/websocket/WebSocketHandler.java @@ -0,0 +1,86 @@ +package com.lx.implatform.imserver.websocket; + +import com.lx.common.contant.RedisKey; +import com.lx.common.enums.WSCmdEnum; +import com.lx.common.model.im.SendInfo; +import com.lx.common.util.SpringContextHolder; +import com.lx.implatform.imserver.websocket.processor.MessageProcessor; +import com.lx.implatform.imserver.websocket.processor.ProcessorFactory; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.util.AttributeKey; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; + + +/** + * WebSocket 长连接下 文本帧的处理器 + * 实现浏览器发送文本回写 + * 浏览器连接状态监控 + */ +@Slf4j +public class WebSocketHandler extends SimpleChannelInboundHandler { + + + @Override + protected void channelRead0(ChannelHandlerContext ctx, SendInfo sendInfo) throws Exception { + // 创建处理器进行处理 + MessageProcessor processor = ProcessorFactory.createProcessor(WSCmdEnum.fromCode(sendInfo.getCmd())); + processor.process(ctx,processor.transform(sendInfo.getData())); + } + + /** + * 出现异常的处理 打印报错日志 + * + * @param ctx the ctx + * @param cause the cause + * @throws Exception the Exception + */ + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + log.error(cause.getMessage()); + //关闭上下文 + //ctx.close(); + } + + /** + * 监控浏览器上线 + * + * @param ctx the ctx + * @throws Exception the Exception + */ + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + log.info(ctx.channel().id().asLongText() + "连接"); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + + AttributeKey attr = AttributeKey.valueOf("USER_ID"); + Long userId = ctx.channel().attr(attr).get(); + // 移除channel + WebsocketChannelCtxHloder.removeChannelCtx(userId); + // 用户下线 + RedisTemplate redisTemplate = SpringContextHolder.getBean("redisTemplate"); + String key = RedisKey.IM_USER_SERVER_ID + userId; + redisTemplate.delete(key); + log.info(ctx.channel().id().asLongText() + "断开连接"); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof IdleStateEvent) { + IdleState state = ((IdleStateEvent) evt).state(); + if (state == IdleState.READER_IDLE) { + // 在规定时间内没有收到客户端的上行数据, 主动断开连接 + ctx.channel().close(); + } + } else { + super.userEventTriggered(ctx, evt); + } + + } +} \ No newline at end of file diff --git a/im-server/src/main/java/com/lx/implatform/imserver/websocket/WebsocketChannelCtxHloder.java b/im-server/src/main/java/com/lx/implatform/imserver/websocket/WebsocketChannelCtxHloder.java new file mode 100644 index 0000000..c2c0789 --- /dev/null +++ b/im-server/src/main/java/com/lx/implatform/imserver/websocket/WebsocketChannelCtxHloder.java @@ -0,0 +1,31 @@ +package com.lx.implatform.imserver.websocket; + +import io.netty.channel.ChannelHandlerContext; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class WebsocketChannelCtxHloder { + + private static Map channelMap = new ConcurrentHashMap(); + + + public static void addChannelCtx(Long userId,ChannelHandlerContext ctx){ + channelMap.put(userId,ctx); + } + + public static void removeChannelCtx(Long userId){ + channelMap.remove(userId); + } + + public static ChannelHandlerContext getChannelCtx(Long userId){ + return channelMap.get(userId); + } + + public static Set getAllChannelIds(){ + return channelMap.keySet(); + } + + +} diff --git a/im-server/src/main/java/com/lx/implatform/imserver/websocket/WebsocketServer.java b/im-server/src/main/java/com/lx/implatform/imserver/websocket/WebsocketServer.java new file mode 100644 index 0000000..a346521 --- /dev/null +++ b/im-server/src/main/java/com/lx/implatform/imserver/websocket/WebsocketServer.java @@ -0,0 +1,71 @@ +package com.lx.implatform.imserver.websocket; + +import com.lx.implatform.imserver.websocket.endecode.MessageProtocolDecoder; +import com.lx.implatform.imserver.websocket.endecode.MessageProtocolEncoder; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +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.IdleStateHandler; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public class WebsocketServer { + + public static String LOCAL_SERVER_ID = UUID.randomUUID().toString(); + + public void start(int port) { + // 服务端启动辅助类,用于设置TCP相关参数 + ServerBootstrap bootstrap = new ServerBootstrap(); + // 获取Reactor线程池 + EventLoopGroup bossGroup = new NioEventLoopGroup(); + EventLoopGroup workGroup = new NioEventLoopGroup(); + // 设置为主从线程模型 + bootstrap.group(bossGroup, workGroup) + // 设置服务端NIO通信类型 + .channel(NioServerSocketChannel.class) + // 设置ChannelPipeline,也就是业务职责链,由处理的Handler串联而成,由从线程池处理 + .childHandler(new ChannelInitializer() { + // 添加处理的Handler,通常包括消息编解码、业务处理,也可以是日志、权限、过滤等 + @Override + protected void initChannel(Channel ch) throws Exception { + // 获取职责链 + ChannelPipeline pipeline = ch.pipeline(); + // + pipeline.addLast(new IdleStateHandler(15, 0, 0, TimeUnit.SECONDS)); + pipeline.addLast("http-codec", new HttpServerCodec()); + pipeline.addLast("aggregator", new HttpObjectAggregator(65535)); + pipeline.addLast("http-chunked", new ChunkedWriteHandler()); + pipeline.addLast(new WebSocketServerProtocolHandler("/im")); + pipeline.addLast("encode",new MessageProtocolEncoder()); + pipeline.addLast("decode",new MessageProtocolDecoder()); + pipeline.addLast("handler", new WebSocketHandler()); + } + }) + // bootstrap 还可以设置TCP参数,根据需要可以分别设置主线程池和从线程池参数,来优化服务端性能。 + // 其中主线程池使用option方法来设置,从线程池使用childOption方法设置。 + // backlog表示主线程池中在套接口排队的最大数量,队列由未连接队列(三次握手未完成的)和已连接队列 + .option(ChannelOption.SO_BACKLOG, 5) + // 表示连接保活,相当于心跳机制,默认为7200s + .childOption(ChannelOption.SO_KEEPALIVE, true); + + try { + // 绑定端口,启动select线程,轮询监听channel事件,监听到事件之后就会交给从线程池处理 + Channel channel = bootstrap.bind(port).sync().channel(); + // 等待服务端口关闭 + channel.closeFuture().sync(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + // 优雅退出,释放线程池资源 + bossGroup.shutdownGracefully(); + workGroup.shutdownGracefully(); + } + } + +} diff --git a/im-server/src/main/java/com/lx/implatform/imserver/websocket/endecode/MessageProtocolDecoder.java b/im-server/src/main/java/com/lx/implatform/imserver/websocket/endecode/MessageProtocolDecoder.java new file mode 100644 index 0000000..d7e3e61 --- /dev/null +++ b/im-server/src/main/java/com/lx/implatform/imserver/websocket/endecode/MessageProtocolDecoder.java @@ -0,0 +1,19 @@ +package com.lx.implatform.imserver.websocket.endecode; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.lx.common.model.im.SendInfo; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; + +import java.util.List; + +public class MessageProtocolDecoder extends MessageToMessageDecoder { + + @Override + protected void decode(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame, List list) throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + SendInfo sendInfo = objectMapper.readValue(textWebSocketFrame.text(), SendInfo.class); + list.add(sendInfo); + } +} diff --git a/im-server/src/main/java/com/lx/implatform/imserver/websocket/endecode/MessageProtocolEncoder.java b/im-server/src/main/java/com/lx/implatform/imserver/websocket/endecode/MessageProtocolEncoder.java new file mode 100644 index 0000000..170c16d --- /dev/null +++ b/im-server/src/main/java/com/lx/implatform/imserver/websocket/endecode/MessageProtocolEncoder.java @@ -0,0 +1,20 @@ +package com.lx.implatform.imserver.websocket.endecode; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.lx.common.model.im.SendInfo; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; + +import java.util.List; + +public class MessageProtocolEncoder extends MessageToMessageEncoder { + + @Override + protected void encode(ChannelHandlerContext channelHandlerContext, SendInfo sendInfo, List list) throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + String text = objectMapper.writeValueAsString(sendInfo); + TextWebSocketFrame frame = new TextWebSocketFrame(text); + list.add(frame); + } +} diff --git a/im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/HeartbeatProcessor.java b/im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/HeartbeatProcessor.java new file mode 100644 index 0000000..79f793e --- /dev/null +++ b/im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/HeartbeatProcessor.java @@ -0,0 +1,51 @@ +package com.lx.implatform.imserver.websocket.processor; + +import cn.hutool.core.bean.BeanUtil; +import com.lx.common.contant.RedisKey; +import com.lx.common.enums.WSCmdEnum; +import com.lx.common.model.im.HeartbeatInfo; +import com.lx.common.model.im.SendInfo; +import com.lx.implatform.imserver.websocket.WebsocketChannelCtxHloder; +import com.lx.implatform.imserver.websocket.WebsocketServer; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.AttributeKey; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class HeartbeatProcessor implements MessageProcessor { + + @Autowired + RedisTemplate redisTemplate; + + @Override + public void process(ChannelHandlerContext ctx, HeartbeatInfo beatInfo) { + log.info("接收到心跳,channelId:{},userId:{}",ctx.channel().id().asLongText(),beatInfo.getUserId()); + // 绑定用户和channel + WebsocketChannelCtxHloder.addChannelCtx(beatInfo.getUserId(),ctx); + // 设置属性 + AttributeKey attr = AttributeKey.valueOf("USER_ID"); + ctx.channel().attr(attr).set(beatInfo.getUserId()); + + // 在redis上记录每个user的channelId,15秒没有心跳,则自动过期 + String key = RedisKey.IM_USER_SERVER_ID+beatInfo.getUserId(); + redisTemplate.opsForValue().set(key, WebsocketServer.LOCAL_SERVER_ID,15, TimeUnit.SECONDS); + + // 响应ws + SendInfo sendInfo = new SendInfo(); + sendInfo.setCmd(WSCmdEnum.HEARTBEAT.getCode()); + ctx.channel().writeAndFlush(sendInfo); + } + + public HeartbeatInfo transform(Object o){ + HashMap map = (HashMap)o; + HeartbeatInfo beatInfo =BeanUtil.fillBeanWithMap(map, new HeartbeatInfo(), false); + return beatInfo; + } +} diff --git a/im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/MessageProcessor.java b/im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/MessageProcessor.java new file mode 100644 index 0000000..e6c7400 --- /dev/null +++ b/im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/MessageProcessor.java @@ -0,0 +1,10 @@ +package com.lx.implatform.imserver.websocket.processor; + +import io.netty.channel.ChannelHandlerContext; + +public interface MessageProcessor { + + void process(ChannelHandlerContext ctx,T data); + + T transform(Object o); +} diff --git a/im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/ProcessorFactory.java b/im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/ProcessorFactory.java new file mode 100644 index 0000000..f402f9f --- /dev/null +++ b/im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/ProcessorFactory.java @@ -0,0 +1,23 @@ +package com.lx.implatform.imserver.websocket.processor; + +import com.lx.common.enums.WSCmdEnum; +import com.lx.common.util.SpringContextHolder; + +public class ProcessorFactory { + + public static MessageProcessor createProcessor(WSCmdEnum cmd){ + MessageProcessor processor = null; + switch (cmd){ + case HEARTBEAT: + processor = (MessageProcessor) SpringContextHolder.getApplicationContext().getBean("heartbeatProcessor"); + break; + case SINGLE_MESSAGE: + processor = (MessageProcessor)SpringContextHolder.getApplicationContext().getBean("singleMessageProcessor"); + break; + default: + break; + } + return processor; + } + +} diff --git a/im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/SingleMessageProcessor.java b/im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/SingleMessageProcessor.java new file mode 100644 index 0000000..1eaf2ad --- /dev/null +++ b/im-server/src/main/java/com/lx/implatform/imserver/websocket/processor/SingleMessageProcessor.java @@ -0,0 +1,45 @@ +package com.lx.implatform.imserver.websocket.processor; + +import cn.hutool.core.bean.BeanUtil; +import com.lx.common.contant.RedisKey; +import com.lx.common.enums.WSCmdEnum; +import com.lx.common.model.im.SendInfo; +import com.lx.common.model.im.SingleMessageInfo; +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.util.HashMap; + +@Slf4j +@Component +public class SingleMessageProcessor implements MessageProcessor { + + @Autowired + private RedisTemplate redisTemplate; + + @Async + @Override + public void process(ChannelHandlerContext ctx, SingleMessageInfo data) { + log.info("接收到消息,发送者:{},接收者:{},内容:{}",data.getSendUserId(),data.getRecvUserId(),data.getContent()); + // 推送消息到用户 + SendInfo sendInfo = new SendInfo(); + sendInfo.setCmd(WSCmdEnum.SINGLE_MESSAGE.getCode()); + sendInfo.setData(data); + ctx.channel().writeAndFlush(sendInfo); + + // 已读消息推送至redis,等待更新数据库 + String key = RedisKey.IM_ALREADY_READED_MESSAGE; + redisTemplate.opsForList().rightPush(key,data.getId()); + } + + @Override + public SingleMessageInfo transform(Object o) { + HashMap map = (HashMap)o; + SingleMessageInfo info = BeanUtil.fillBeanWithMap(map, new SingleMessageInfo(), false); + return info; + } +} diff --git a/im-server/src/main/resources/application.yml b/im-server/src/main/resources/application.yml new file mode 100644 index 0000000..261b9c2 --- /dev/null +++ b/im-server/src/main/resources/application.yml @@ -0,0 +1,11 @@ +server: + port: 8877 + +websocket: + port: 8878 + +spring: + redis: + host: 127.0.0.1 + port: 6379 + database: 1 \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..4315dd1 --- /dev/null +++ b/pom.xml @@ -0,0 +1,159 @@ + + + + 4.0.0 + lx-im + com.lx + pom + ${im.version} + + + im-platform + im-server + commom + + + + + org.projectlombok + lombok + 1.18.16 + + + cn.hutool + hutool-all + + + + com.alibaba + fastjson + + + org.apache.commons + commons-lang3 + + + + + 1.0.0 + UTF-8 + UTF-8 + 1.8 + 3.3.2 + 1.2.40 + 1.7 + 3.3.1 + 2.7.0 + 5.3.9 + 1.1.22 + 3.7.110.ALL + 3.3.3 + 3.8.1 + 1.18.16 + + + + + + + + org.springframework.boot + spring-boot-dependencies + 2.0.3.RELEASE + pom + import + + + org.projectlombok + lombok + ${lombok.version} + + + com.baomidou + mybatis-plus-boot-starter + ${mybatisplus-spring-boot-starter.version} + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + com.alibaba + fastjson + ${fastjson.version} + + + com.alibaba + druid + ${druid.version} + + + org.apache.velocity + velocity + ${velocity.version} + + + io.springfox + springfox-swagger2 + ${swagger.version} + + + io.springfox + springfox-swagger-ui + ${swagger.version} + + + cn.hutool + hutool-all + ${hutool.version} + + + org.apache.poi + poi-ooxml + 4.1.2 + + + commons-io + commons-io + 2.6 + + + dom4j + dom4j + 1.6.1 + + + org.codehaus.jackson + jackson-mapper-asl + 1.9.13 + + + javax.interceptor + javax.interceptor-api + 1.2 + + + org.springframework.security + spring-security-jwt + 1.0.10.RELEASE + + + + + + + + + + aliyun-maven + aliyun maven + http://maven.aliyun.com/nexus/content/groups/public/ + + false + + + + \ No newline at end of file