commit e2f4e823232bb55f21f24af77b3eb930ced3dd2b Author: 梁显优 <237809796@qq.com> Date: Sat Nov 19 16:00:50 2022 +0800 . diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c7919df --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/out/ +*.iml \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/.idea/artifacts/z_im_jar.xml b/.idea/artifacts/z_im_jar.xml new file mode 100644 index 0000000..074cfd6 --- /dev/null +++ b/.idea/artifacts/z_im_jar.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/out/artifacts/z_im_jar + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..a1757ae --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/lib.xml b/.idea/libraries/lib.xml new file mode 100644 index 0000000..f0c5707 --- /dev/null +++ b/.idea/libraries/lib.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..3d3ab27 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..657000d --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..9787561 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1668815275339 + + + 1668826741516 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/conf/application.xml b/conf/application.xml new file mode 100644 index 0000000..6f6c74e --- /dev/null +++ b/conf/application.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/config.properties b/conf/config.properties new file mode 100644 index 0000000..37ce8d3 --- /dev/null +++ b/conf/config.properties @@ -0,0 +1,2 @@ +# swear +# swearbasepath=/opt/swear diff --git a/conf/logging.properties b/conf/logging.properties new file mode 100644 index 0000000..a9d6087 --- /dev/null +++ b/conf/logging.properties @@ -0,0 +1,16 @@ +handlers=java.util.logging.ConsoleHandler +############################################################ +.level=FINEST +java.level=INFO +javax.level=INFO +com.sun.level=INFO +sun.level=INFO +jdk.level=INFO +java.util.logging.FileHandler.level=FINE +#10M +java.util.logging.FileHandler.limit=10485760 +java.util.logging.FileHandler.count=10000 +java.util.logging.FileHandler.encoding=UTF-8 +java.util.logging.FileHandler.pattern=${APP_HOME}/logs-%m/log-%d.log +java.util.logging.FileHandler.append=true +java.util.logging.ConsoleHandler.level=FINEST diff --git a/conf/persistence.xml b/conf/persistence.xml new file mode 100644 index 0000000..34f0677 --- /dev/null +++ b/conf/persistence.xml @@ -0,0 +1,15 @@ + + + + + ALL + + + + + + + + diff --git a/src/com/zchd/base/AppListener.java b/src/com/zchd/base/AppListener.java new file mode 100644 index 0000000..185f4be --- /dev/null +++ b/src/com/zchd/base/AppListener.java @@ -0,0 +1,32 @@ +package com.zchd.base; + +import com.zchd.base.util.QueueTask; +import org.redkale.boot.Application; +import org.redkale.boot.ApplicationListener; + +import javax.annotation.Resource; +import java.io.File; +import java.util.logging.Logger; + +/** + * 服务监听 + * + * @author: liangxy. + */ +public class AppListener implements ApplicationListener { + + private Logger logger = Logger.getLogger(this.getClass().getSimpleName()); + + @Resource(name = "APP_HOME") + protected File APP_HOME; + + @Override + public void preStart(Application application) { + + } + + @Override + public void preShutdown(Application application) { + QueueTask.destroys(); + } +} diff --git a/src/com/zchd/base/BaseBean.java b/src/com/zchd/base/BaseBean.java new file mode 100644 index 0000000..797125c --- /dev/null +++ b/src/com/zchd/base/BaseBean.java @@ -0,0 +1,24 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.zchd.base; + +import org.redkale.convert.json.JsonConvert; +import org.redkale.service.RetResult; + +import java.io.Serializable; + +/** + * @author zhangjx + */ +public abstract class BaseBean implements Serializable { + + public final static RetResult RESULT_SUCCESS = RetResult.success(); + + @Override + public String toString() { + return JsonConvert.root().convertTo(this); + } +} diff --git a/src/com/zchd/base/BaseService.java b/src/com/zchd/base/BaseService.java new file mode 100644 index 0000000..5dc2f93 --- /dev/null +++ b/src/com/zchd/base/BaseService.java @@ -0,0 +1,52 @@ +package com.zchd.base; + +import com.zdemo.cachex.MyRedisCacheSource; +import com.zdemo.zhub.ZHubClient; +import org.redkale.convert.json.JsonConvert; +import org.redkale.service.AbstractService; +import org.redkale.service.RetResult; +import org.redkale.source.DataSource; +import org.redkale.util.Sheet; + +import javax.annotation.Resource; +import java.io.File; +import java.util.List; +import java.util.logging.Logger; + +public class BaseService extends AbstractService { + + protected final Logger logger = Logger.getLogger(this.getClass().getSimpleName()); + protected final static RetResult RET_SUCCESS = RetResult.success(); + protected final static RetResult RET_EMPTY_SHEET = RetResult.success(new Sheet<>(0, List.of())); + protected final static RetResult RET_EMPTY_LIST = RetResult.success(List.of()); + protected final JsonConvert convert = JsonConvert.root(); + + @Resource(name = "zhub") + protected ZHubClient zhub; + + @Resource(name = "int_cache") + protected MyRedisCacheSource intCache; + + @Resource(name = "long_cache") + protected MyRedisCacheSource longCache; + + @Resource(name = "str_cache") + protected MyRedisCacheSource strCache; + + @Resource(name = "z_im") + protected DataSource zimSource; + + @Resource(name = "APP_HOME") + protected File APP_HOME; + + @Resource(name = "APP_NAME") + protected String APP_NAME = ""; + + protected RetResult retError(String info) { + return new RetResult<>(100, info); + } + + protected RetResult retError(int code, String info) { + return new RetResult<>(code, info); + } +} diff --git a/src/com/zchd/base/BaseServlet.java b/src/com/zchd/base/BaseServlet.java new file mode 100644 index 0000000..3d1ee6b --- /dev/null +++ b/src/com/zchd/base/BaseServlet.java @@ -0,0 +1,49 @@ +package com.zchd.base; + +import org.redkale.convert.json.JsonConvert; +import org.redkale.net.http.HttpRequest; +import org.redkale.net.http.HttpResponse; +import org.redkale.net.http.HttpServlet; +import org.redkale.service.RetResult; + +import javax.annotation.Resource; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class BaseServlet extends HttpServlet { + + protected final Logger logger = Logger.getLogger(this.getClass().getSimpleName()); + + @Resource(name = "APP_NAME") + protected String APP_NAME = ""; + + @Resource + protected JsonConvert convert; + + @Override + protected void preExecute(HttpRequest request, HttpResponse response) throws IOException { + + super.preExecute(request, response); + } + + @Override + protected void authenticate(HttpRequest request, HttpResponse response) throws IOException { + response.nextEvent(); + } + + + @Override + public void execute(HttpRequest request, HttpResponse response) throws IOException { + try { + super.execute(request, response); + } catch (Exception e) { + logger.log(Level.WARNING, "", e); + RetResult result = RetResult.success(); + result.setRetcode(100); + result.setRetinfo("操作失败"); + response.finishJson(result); + return; + } + } +} diff --git a/src/com/zchd/base/RetCodes.java b/src/com/zchd/base/RetCodes.java new file mode 100644 index 0000000..48331cf --- /dev/null +++ b/src/com/zchd/base/RetCodes.java @@ -0,0 +1,477 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.zchd.base; + +import com.zchd.base.util.Utils; +import org.redkale.service.RetLabel; +import org.redkale.service.RetResult; + +import java.text.MessageFormat; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * @author zhangjx + */ +@SuppressWarnings("unchecked") +public abstract class RetCodes { + + //2000_0001 - 2999_9999 预留给 Redkale的扩展包redkalex使用 + //3000_0001 - 7999_9999 为平台系统使用 + //8000_0001 - 9999_9999 为OSS系统使用 + //------------------------------------- 通用模块 ----------------------------------------- + @RetLabel("参数无效") + public static final int RET_PARAMS_ILLEGAL = 30010001; + + @RetLabel("无上传文件") + public static final int RET_UPLOAD_NOFILE = 30010002; + + @RetLabel("上传文件过大") + public static final int RET_UPLOAD_FILETOOBIG = 30010003; + + @RetLabel("上传文件不是图片") + public static final int RET_UPLOAD_NOTIMAGE = 30010004; + + @RetLabel("文件写入失败") + public static final int RET_FILE_WRITE_ERROR = 30010005; + + @RetLabel("系统内部异常") + public static final int RET_INNER_ILLEGAL = 30010006; + + @RetLabel("调用远程接口异常") + public static final int RET_REMOTE_ILLEGAL = 30010007; + + @RetLabel("调用远程接口超时") + public static final int RET_REMOTE_TIMEOUT = 30010008; + + @RetLabel("重复操作") + public static final int RET_REPEAT_ILLEGAL = 30010009; + + @RetLabel("操作失败") + public static final int RET_UNKNOWN_ERROR = 30010010; + + @RetLabel("操作失败,包含敏感词") + public static final int RET_SWEAR_ERROR = 30010011; + + @RetLabel("网络繁忙,请稍后再试") + public static final int RET_FREQUENCY_ERROR = 30010012; + + @RetLabel("不在白名单内") + public static final int RET_FREQUENCY_NOT_WHITE = 30010013; + + @RetLabel("您操作速度太快,请稍后再试") + public static final int RET_FREQUENCY_SPEED_FAST = 30010014; + + @RetLabel("权限不足") + public static final int RET_AUTH_ILLEGAL = 30010015; + + @RetLabel("请求参数长度超出限制(2M)") + public static final int RET_REQUEST_PARAM_LENGTH_OUT = 30010016; + + @RetLabel("手速太快,请稍后再试") + public static final int RET_REQUEST_UNIQUE = 30010017; + + //------------------------------------- 用户模块 ----------------------------------------- + @RetLabel("未登陆") + public static final int RET_USER_UNLOGIN = 30020001; + + @RetLabel("用户登录失败") + public static final int RET_USER_LOGIN_FAIL = 30020002; + + @RetLabel("用户被禁用") + public static final int RET_USER_FREEZED = 30020005; + + @RetLabel("用户权限不够") + public static final int RET_USER_AUTH_ILLEGAL = 30020006; + + @RetLabel("用户不存在") + public static final int RET_USER_NOTEXISTS = 30020007; + + @RetLabel("用户状态异常") + public static final int RET_USER_STATUS_ILLEGAL = 30020008; + + @RetLabel("用户注册参数无效") + public static final int RET_USER_SIGNUP_ILLEGAL = 30020009; + + @RetLabel("用户性别参数无效") + public static final int RET_USER_GENDER_ILLEGAL = 30020010; + + @RetLabel("用户名无效") + public static final int RET_USER_USERNAME_ILLEGAL = 30020011; + + @RetLabel("用户账号无效") + public static final int RET_USER_ACCOUNT_ILLEGAL = 30020012; + + @RetLabel("手机号已存在") + public static final int RET_USER_ACCOUNT_EXISTS = 30020013; + + @RetLabel("手机号码无效") + public static final int RET_USER_MOBILE_ILLEGAL = 30020014; + + @RetLabel("手机号码已存在") + public static final int RET_USER_MOBILE_EXISTS = 30020015; + + @RetLabel("手机验证码发送过于频繁") + public static final int RET_USER_MOBILE_SMSFREQUENT = 30020016; + + @RetLabel("邮箱地址无效") + public static final int RET_USER_EMAIL_ILLEGAL = 30020017; + + @RetLabel("邮箱地址已存在") + public static final int RET_USER_EMAIL_EXISTS = 30020018; + + @RetLabel("微信绑定号无效") + public static final int RET_USER_WXID_ILLEGAL = 30020019; + + @RetLabel("微信绑定号已存在") + public static final int RET_USER_WXID_EXISTS = 30020020; + + @RetLabel("绑定微信号失败") + public static final int RET_USER_WXID_BIND_FAIL = 30020021; + + @RetLabel("QQ绑定号无效") + public static final int RET_USER_QQID_ILLEGAL = 30020022; + + @RetLabel("QQ绑定号已存在") + public static final int RET_USER_QQID_EXISTS = 30020023; + + @RetLabel("绑定QQ号失败") + public static final int RET_USER_QQID_BIND_FAIL = 30020024; + + @RetLabel("获取绑定QQ信息失败") + public static final int RET_USER_QQID_INFO_FAIL = 30020025; + + @RetLabel("验证码无效") + public static final int RET_USER_RANDCODE_ILLEGAL = 30020026; //邮件或者短信验证码 + + @RetLabel("验证码已过期") + public static final int RET_USER_RANDCODE_EXPIRED = 30020027; //邮件或者短信验证码 + + @RetLabel("验证码错误或失效") + public static final int RET_USER_CAPTCHA_ILLEGAL = 30020028; //图片验证码 + + @RetLabel("用户类型无效") + public static final int RET_USER_TYPE_ILLEGAL = 30020029; + + @RetLabel("账号已在其他设备登录") + public static final int RET_USER_LOGIN_ILLEGAL = 30020030; + + @RetLabel("今日已签到") + public static final int RET_QUESTS_DUTY_EXISTS = 30020031; + + @RetLabel("父账号不存在") + public static final int RET_USER_PARENT_ILLEGAL = 30020032; + + @RetLabel("手机号码所在运营商不存在") + public static final int RET_USER_MOBILE_NONET = 30020033; + + @RetLabel("用户等级不够") + public static final int RET_USER_LEVEL_ILLEGAL = 30020036; + + @RetLabel("银行密码错误") + public static final int RET_USER_BANKPWD_ILLEGAL = 30020037; + + @RetLabel("用户已绑定在其他设备上了") + public static final int RET_USER_APPTOKEN_BINDED = 30020039; + + @RetLabel("短信发送失败") + public static final int RET_SMS_SEND_ERROR = 30020041; + + @RetLabel("同一设备注册次数达到上限") + public static final int RET_USER_REG_APPSAME_LIMIT = 30020042; + + @RetLabel("您的账号已被限制登录") + public static final int RET_USER_LOGINORREG_LIMIT = 30020043; + + @RetLabel("城信APP绑定号已存在") + public static final int RET_USER_CXID_EXISTS = 30020044; + + @RetLabel("禁止循环代理") + public static final int RET_USER_AGENCY_REPEAT = 30020045; + + @RetLabel("没有可提取的奖励") + public static final int RET_USER_PROFIT_MONEY_ILLEGAL = 30020046; + + @RetLabel("收款资料不全") + public static final int RET_USER_PROFIT_CARD_ILLEGAL = 30020047; + + @RetLabel("用户生日无效") + public static final int RET_USER_BIRTHDAY_ILLEGAL = 30020048; + + @RetLabel("举报参数无效") + public static final int RET_USER_REPORT_ILLEGAL = 30020049; + + @RetLabel("邀请码无效") + public static final int RET_USER_INVITE_ILLEGAL = 30020050; + + @RetLabel("此用户昵称已存在") + public static final int RET_USERNAME_EXISTS = 30020051; + + @RetLabel("昵称不可用") + public static final int RET_USERNAME_ILLEGAL = 30020052; + + @RetLabel("用户地址无效") + public static final int RET_ADDR_ILLEGAL = 30020053; + + @RetLabel("用户头像无效") + public static final int RET_FACE_ILLEGAL = 30020054; + + @RetLabel("用户封面无效") + public static final int RET_USERCOVER_ILLEGAL = 30020055; + + @RetLabel("第三方用户信息获取失败") + public static final int RET_JTOKEN_ILLEGAL = 30020056; + + @RetLabel("用户年龄参数无效") + public static final int RET_USER_AGE_ILLEGAL = 30020057; + + @RetLabel("接口调用Token无效") + public static final int RET_USERCREATE_TOKEN_ILLEGAL = 30020057; + + @RetLabel("抽奖券不足") + public static final int RET_LUCK_DRAW_TICKET_ILLEGAL = 30020058; + + @RetLabel("您的账号已被冻结") + public static final int RET_USER_STATUS_FROZEN = 30020059; + + @RetLabel("您已被禁言") + public static final int RET_USER_STATUS_FREEZE = 30020060; + + @RetLabel("邀请码已达到使用上限") + public static final int RET_USER_INVITE_UPPER = 30020061; + + @RetLabel("用户未激活") + public static final int RET_USER_STATUS_PENDING = 30020062; + + @RetLabel("用户未进行身份认证") + public static final int RET_USER_IDENTITY_NOT_PROVED = 30020063; + + @RetLabel("解绑后没有其它登录方式,不允许解绑") + public static final int RET_USER_NOT_UNBIND = 30020065; + + @RetLabel("已是推广员,请勿重复申请") + public static final int RET_USER_ALREADY_PROMOTER = 30020066; + + @RetLabel("请充值会员") + public static final int RET_USER_VIP_ERROR = 30020067; + + @RetLabel("已举报过,请勿重复举报") + public static final int RET_USER_ALREADY_REPORT = 30020068; + + @RetLabel("Apple Id绑定号已存在") + public static final int RET_USER_APPLEID_EXISTS = 30020069; + + @RetLabel("您的登录状态已过期") + public static final int RET_USER_LOGIN_EXPIRE = 30020070; + + @RetLabel("手机号已绑定其它第三方账号") + public static final int RET_USER_MOBILE_BINDED = 30020071; + + @RetLabel("名片背景图无效") + public static final int RET_USER_CARD_IMG_ERROR = 30020072; + + @RetLabel("未查询到用户收货地址") + public static final int RET_USER_ADDRESS_NOT_EXISTS = 30020073; + + @RetLabel("7天内只能修改1次,请稍后再试~") + public static final int RET_USER_CHANGE_NAME_COUNT = 30020074; + + @RetLabel("开通VIP解锁多张封面特权") + public static final int RET_USER_BATCH_COVER_NO_VIP = 30020075; + + @RetLabel("手机号已绑定其它账号") + public static final int RET_MOBILE_BIND_OTHER = 30020076; + + @RetLabel("手机号已注册V运动账号,是否确认绑定") + public static final int RET_MOBILE_EXISTS_CONFIRM = 30020077; + + @RetLabel("您的账号长时间未登录,为保证账号安全,请重新输入账号密码") + public static final int RET_ACCOUNT_LOGIN_EXPIRE = 30020078; + + + //----------------------------------------头衔认证------------------------------------------------------------------- + @RetLabel("已申请或拥有此头衔") + public static final int RET_IDENTITY_ALREADY_APPLY = 40100001; + @RetLabel("不满足申请条件") + public static final int RET_IDENTITY_CANNOT_APPLY = 40100002; + @RetLabel("佩戴头衔超出限制") + public static final int RET_IDENTITY_WEAR_ERROR = 40100003; + @RetLabel("自定义头衔昵称为空") + public static final int RET_IDENTITY_CUSTOM_NAME_ERROR = 40100004; + @RetLabel("自定义头衔名称不能与提供的名称重复") + public static final int RET_IDENTITY_NAME_REPEAT = 40100005; + @RetLabel("未获得头衔") + public static final int RET_IDENTITY_NOT_HAD = 40100006; + @RetLabel("文件大小超过限制(3M)") + public static final int RET_IDENTITY_FILE_LENGTH_OUT = 40100007; + @RetLabel("文件类型超出限制(jpeg,jpg,png)") + public static final int RET_IDENTITY_FILE_SUFFIX_OUT = 40100008; + @RetLabel("未获取到身份证图片") + public static final int RET_IDENTITY_FILE_NOTEXIST = 40100009; + @RetLabel("填写信息与照片不符") + public static final int RET_IDENTITY_FILE_NOTOK = 40100010; + @RetLabel("上送人脸核身信息失败") + public static final int RET_IDENTITY_FILE_UPLOADFACEID = 40100011; + @RetLabel("身份证人像面信息读取失败") + public static final int RET_IDENTITY_FACEID_FRONT_ERROR = 40100012; + @RetLabel("身份证国徽面信息读取失败") + public static final int RET_IDENTITY_FACEID_BACK_ERROR = 40100013; + @RetLabel("未查询到实名认证申请记录") + public static final int RET_IDENTITY_FACEID_INFO_ERROR = 40100014; + @RetLabel("人脸核身识别失败") + public static final int RET_IDENTITY_FACEID_IERROR = 40100015; + @RetLabel("人脸核身返回名称或身份证号码不匹配") + public static final int RET_IDENTITY_FACEID_NOTOK = 40100016; + @RetLabel("Base64转文件失败") + public static final int RET_IDENTITY_FILE_CREATE_ERROR = 40100017; + @RetLabel("该身份证已与其他彩虹号绑定,如有疑问,请联系客服") + public static final int RET_IDENTITY_REPEAT_ERROR = 40100018; + @RetLabel("申请材料无效") + public static final int RET_IDENTITY_APPLY_FILE_ERROR = 40100019; + + //----------------------------------------------消息------------------------------------------------------------- + + @RetLabel("消息参数不正确") + public static final int RET_MESSAGE_ILLEGAL = 40150001; + + @RetLabel("空白消息") + public static final int RET_MESSAGE_EMPTY = 40150002; + + @RetLabel("消息内容超长") + public static final int RET_MESSAGE_TOO_LONG = 40150003; + + @RetLabel("还不是好友关系") + public static final int RET_MESSAGE_NOT_FRIEND = 40150004; + + @RetLabel("消息含有敏感词") + public static final int RET_MESSAGE_HAS_SENSITIVE_WORD = 40150005; + + @RetLabel("图片信息有误") + public static final int RET_MESSAGE_PICTURE_ERROR = 40150006; + + @RetLabel("用户已被禁言") + public static final int RET_MESSAGE_ACCOUNT_BANNED = 40150007; + + //-----------------------------------------push----------------------------------------------------- + @RetLabel("通知未开启") + public static final int RET_PUSH_NOT_OPEN = 40160001; + + //-----------------------------------------通用----------------------------------------------------- + @RetLabel("访问内容不存在") + public static final int RES_NON_EXISTENT = 40180001; + + //-----------------------------------------其他临时----------------------------------------------------- + @RetLabel("内容已被锁定") + public static final int RET_ARTICLE_LOCKED = 40400001; + + @RetLabel("身份证信息已存在") + public static final int RET_IDCARD_EXISTS = 40400002; + + protected static final Map> rets = RetLabel.RetLoader.loadMap(RetCodes.class); + + protected static final Map defret = rets.get(""); + public static final RetResult RET_SUCCESS = RetResult.success(); + + public static RetResult retResult(int retcode) { + if (retcode == 0) { + return RET_SUCCESS; + } + return new RetResult(retcode, retInfo(retcode)); + } + + public static RetResult retResult(String locale, int retcode) { + if (retcode == 0) { + return RET_SUCCESS; + } + return new RetResult(retcode, retInfo(locale, retcode)); + } + + public static RetResult retResult(int retcode, Object... args) { + if (retcode == 0) { + return RET_SUCCESS; + } + if (args == null || args.length < 1) { + return new RetResult(retcode, retInfo(retcode)); + } + String info = MessageFormat.format(retInfo(retcode), args); + return new RetResult(retcode, info); + } + + public static RetResult retResult(String locale, int retcode, Object... args) { + if (retcode == 0) { + return RET_SUCCESS; + } + if (args == null || args.length < 1) { + return new RetResult(retcode, retInfo(locale, retcode)); + } + String info = MessageFormat.format(retInfo(locale, retcode), args); + return new RetResult(retcode, info); + } + + public static CompletableFuture> retResultFuture(int retcode) { + return CompletableFuture.completedFuture(retResult(retcode)); + } + + public static CompletableFuture> retResultFuture(String locale, int retcode) { + return CompletableFuture.completedFuture(retResult(locale, retcode)); + } + + public static CompletableFuture> retResultFuture(int retcode, Object... args) { + return CompletableFuture.completedFuture(retResult(retcode, args)); + } + + public static CompletableFuture> retResultFuture(String locale, int retcode, Object... args) { + return CompletableFuture.completedFuture(retResult(locale, retcode, args)); + } + + public static RetResult set(RetResult result, int retcode, Object... args) { + if (retcode == 0) { + return result.retcode(0).retinfo(""); + } + if (args == null || args.length < 1) { + return result.retcode(retcode).retinfo(retInfo(retcode)); + } + String info = MessageFormat.format(retInfo(retcode), args); + return result.retcode(retcode).retinfo(info); + } + + public static RetResult set(RetResult result, String locale, int retcode, Object... args) { + if (retcode == 0) { + return result.retcode(0).retinfo(""); + } + if (args == null || args.length < 1) { + return result.retcode(retcode).retinfo(retInfo(locale, retcode)); + } + String info = MessageFormat.format(retInfo(locale, retcode), args); + return result.retcode(retcode).retinfo(info); + } + + public static String retInfo(int retcode) { + if (retcode == 0) { + return "Success"; + } + return defret.getOrDefault(retcode, "Error"); + } + + public static String retInfo(String locale, int retcode) { + if (locale == null || locale.isEmpty()) { + return retInfo(retcode); + } + if (retcode == 0) { + return "Success"; + } + String key = locale == null ? "" : locale; + Map map = rets.get(key); + if (map == null) { + return "Error"; + } + return map.getOrDefault(retcode, "Error"); + } + + public static RetResult retResult(RetResult codeRet) { + return Utils.copy(new RetResult<>(), codeRet); + } +} diff --git a/src/com/zchd/base/info/Swear.java b/src/com/zchd/base/info/Swear.java new file mode 100644 index 0000000..a00bfe1 --- /dev/null +++ b/src/com/zchd/base/info/Swear.java @@ -0,0 +1,16 @@ +package com.zchd.base.info; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * 敏感词配置检查 + */ +@Target(FIELD) +@Retention(RUNTIME) +public @interface Swear { + String name() default ""; +} diff --git a/src/com/zchd/base/info/SwearRpcReq.java b/src/com/zchd/base/info/SwearRpcReq.java new file mode 100644 index 0000000..21955ba --- /dev/null +++ b/src/com/zchd/base/info/SwearRpcReq.java @@ -0,0 +1,33 @@ +package com.zchd.base.info; + +import lombok.Getter; +import lombok.Setter; +import org.redkale.util.Comment; + +import java.io.Serializable; + +/** + * @author YLZ FUTURE MADE + * @date 2021/10/15 10:42 + */ +@Getter +@Setter +public class SwearRpcReq implements Serializable { + + @Comment("敏感词内容") + private String content; + @Comment("敏感词检测类型 MinMatchTYpe:最小匹配规则,如:敏感词库[\"中国\",\"中国人\"],语句:\"我是中国人\",匹配结果:我是[中国]人\n" + + " * MaxMatchType:最大匹配规则,如:敏感词库[\"中国\",\"中国人\"],语句:\"我是中国人\",匹配结果:我是[中国人]") + private int matchType = 1; + @Comment("替换的敏感字") + private char replaceChar = '*'; + @Comment("替换的敏感词") + private String replace = ""; + + public static SwearRpcReq build(String content) { + SwearRpcReq bean = new SwearRpcReq(); + bean.setContent(content); + return bean; + } + +} \ No newline at end of file diff --git a/src/com/zchd/base/info/SwearWordService.java b/src/com/zchd/base/info/SwearWordService.java new file mode 100644 index 0000000..27da72f --- /dev/null +++ b/src/com/zchd/base/info/SwearWordService.java @@ -0,0 +1,168 @@ +package com.zchd.base.info; + +import com.zchd.base.BaseService; +import com.zchd.base.RetCodes; +import com.zchd.base.util.FileKit; +import com.zchd.base.util.QueueTasks; +import com.zchd.base.util.Utils; +import org.redkale.net.http.RestMapping; +import org.redkale.net.http.RestService; +import org.redkale.service.RetResult; +import org.redkale.util.AnyValue; + +import javax.annotation.Resource; +import java.io.*; +import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + + +@RestService(name = "swearword", comment = "敏感词服务") +public class SwearWordService extends BaseService { + + protected final Logger logger = Logger.getLogger(this.getClass().getSimpleName()); + + @Resource(name = "APP_HOME") + protected File APP_HOME; + + @Resource(name = "property.swearbasepath") + private String swearbasepath; + + private Set words = new HashSet<>(); + + @Override + public void init(AnyValue config) { + QueueTasks.add(() -> { + reload(); + zhub.subscribe("sport:swearword-reload", x -> { + reload(); + }); + }); + } + + @RestMapping(name = "reload", comment = "加载敏感词") + public RetResult reload() { + if (Utils.isEmpty(swearbasepath)) { + return RET_SUCCESS; + } + String swearpath = swearbasepath.replace("${APP_HOME}", APP_HOME.getPath()); + File dir = new File(swearpath); + if (!dir.exists()) { + return null; + } + words.clear(); + for (File file : dir.listFiles()) { + if (!file.isFile()) continue; + try { + FileInputStream in = new FileInputStream(file); + LineNumberReader reader = new LineNumberReader(new InputStreamReader(in, "UTF-8")); + String line; + int n = words.size(); + while ((line = reader.readLine()) != null) { + words.add(Utils.unicodeToCn(line).trim()); + } + words.remove(""); + words.remove("null"); + words.remove("9"); + logger.log(Level.INFO, String.format("loaded swear file :%s, add new words: %d", file.getName(), words.size() - n)); + reader.close(); + } catch (FileNotFoundException | UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + return RET_SUCCESS; + } + + @RestMapping(name = "check", auth = false) + public RetResult check(String str) { + for (String word : words) { + if (str.contains(word)) { + RetResult result = new RetResult<>(); + result.setRetcode(RetCodes.RET_SWEAR_ERROR); + result.setRetinfo(word); + FileKit.append(result.getRetinfo(), new File("/tmp/swearword.txt")); + return result; + } + } + + return RET_SUCCESS; + } + + /** + * 实体中属性敏感词 检查 + * 调用前需配置实体属性 @Swear + * + * @param obj + * @return + */ + public RetResult _checkBean(Object obj) { + if (obj == null) { + return RET_SUCCESS; + } + + Class clazz = obj.getClass(); + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + Swear swear = field.getAnnotation(Swear.class); + if (swear != null) { + try { + field.setAccessible(true); + Object str = field.get(obj); + RetResult result = str instanceof String ? check((String) str) : RetResult.success(); + if (!result.isSuccess()) { + String name = swear.name(); + if (!Utils.isEmpty(name)) { + result.setRetinfo(name + "包含敏感词[" + result.getRetinfo() + "]"); + } + + return result; + } + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + } + + return RET_SUCCESS; + } + + public RetResult checkBean(Object obj) { + if (obj == null) { + return RET_SUCCESS; + } + + Class clazz = obj.getClass(); + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + Swear swear = field.getAnnotation(Swear.class); + if (swear != null) { + try { + field.setAccessible(true); + String str = (String) field.get(obj); + + RetResult result; + do { + result = str instanceof String ? check(str) : RetResult.success(); + if (!result.isSuccess()) { + FileKit.append(result.getRetinfo(), new File("/tmp/swearword.txt")); + str = str.replace(result.getRetinfo(), "*"); + } else { + field.set(obj, str); + } + } while (!result.isSuccess()); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + } + + return RET_SUCCESS; + } + + +} diff --git a/src/com/zchd/base/util/FileKit.java b/src/com/zchd/base/util/FileKit.java new file mode 100644 index 0000000..92a4039 --- /dev/null +++ b/src/com/zchd/base/util/FileKit.java @@ -0,0 +1,160 @@ +package com.zchd.base.util; + + +import org.redkale.convert.json.JsonConvert; + +import java.io.*; +import java.lang.reflect.Type; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.file.Files; + +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static java.util.Arrays.asList; + +/** + * Created by liangxianyou at 2018/5/31 10:23. + */ +public final class FileKit { + + private FileKit() { + } + + public static void strToFile(String entityBody, File file) { + strToFile(entityBody, file, true); + } + + public static void strToFile(String entityBody, File file, boolean existDel) { + if (file.exists()) { + if (existDel) { + file.delete(); + } else { + throw new RuntimeException(file.getPath() + "已经存在"); + } + } + + if (!file.getParentFile().exists()) { + file.getParentFile().mkdirs(); + } + try (FileOutputStream out = new FileOutputStream(file)) { + out.write(entityBody.getBytes("UTF-8")); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + public static void append(String str, File file) { + if (!file.getParentFile().exists()) { + file.getParentFile().mkdirs(); + } + + try (FileOutputStream out = new FileOutputStream(file, true)) { + out.write(str.getBytes("UTF-8")); + if (!str.endsWith("\n")) { + out.write("\n".getBytes("UTF-8")); + } + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + /** + * 拷贝文件/文件目录 + * + * @param source 源文件目录 + * @param target 目标目录 + */ + private static void copyFiles(File source, File target) { + copyFiles(source, target, ""); + } + + /** + * 拷贝文件/文件目录 + * + * @param source + * @param target + * @param linkPath + */ + public static void copyFiles(File source, File target, String linkPath) { + if (source.isDirectory()) { + final String _linkPath = linkPath + File.separator + source.getName(); + asList(source.listFiles()).forEach(f -> { + copyFiles(f, target, _linkPath); + }); + } else if (source.isFile()) { + try { + String _linkPath = ""; + int index = linkPath.indexOf(File.separator, 1); + if (index > 0) { + _linkPath = linkPath.substring(index); + } + File targetFile = new File(target.toPath() + _linkPath + File.separator + source.getName()); + if (!targetFile.getParentFile().exists()) { + targetFile.getParentFile().mkdirs(); + } + + Files.copy(source.toPath(), targetFile.toPath(), REPLACE_EXISTING); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * 获取 clazz的路径,如果是jar里面的文件得到jar存放的目录,如:lib + * + * @param clazz + * @return + */ + public static String rootPath(Class clazz) { + //return clazz.getClassLoader().getResource("").getPath(); + URL url = clazz.getProtectionDomain().getCodeSource().getLocation(); + try { + String filePath = URLDecoder.decode(url.getPath(), "utf-8"); + if (filePath.endsWith(".jar")) { + return filePath.substring(0, filePath.lastIndexOf("/") + 1); + } + return filePath; + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + return ""; + } + + public static String rootPath() { + return rootPath(FileKit.class); + } + + /** + * 读取流内的所有内容 + * + * @param inputStream + * @return + * @throws IOException + */ + public static String readAll(InputStream inputStream) { + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + StringBuffer buf = new StringBuffer(); + String str; + try { + while ((str = reader.readLine()) != null) { + buf.append(str + "\n"); + } + } catch (IOException e) { + e.printStackTrace(); + } + return buf.toString(); + } + + public static T readAs(File file, Type typeToken) throws IOException { + try ( + FileInputStream inputStream = new FileInputStream(file) + ) { + return JsonConvert.root().convertFrom(typeToken, inputStream); + } + } + +} diff --git a/src/com/zchd/base/util/Kv.java b/src/com/zchd/base/util/Kv.java new file mode 100644 index 0000000..00b4e27 --- /dev/null +++ b/src/com/zchd/base/util/Kv.java @@ -0,0 +1,325 @@ +package com.zchd.base.util; + +import org.redkale.convert.json.JsonConvert; + +import javax.persistence.Id; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Created by liangxianyou@eversec.cn at 2018/3/12 14:17. + */ +public class Kv extends LinkedHashMap { + public static Kv of() { + return new Kv(); + } + + public static Kv of(Object k, Object v) { + return new Kv().set(k, v); + } + + public static Kv filter(Map map, String... fields) { + Kv kv = Kv.of(); + if (fields == null || fields.length == 0 || map == null) { + return kv; + } + + for (String field : fields) { + if (field.contains("->")) { + String[] arr = field.split("->"); + kv.put(arr[1], map.get(arr[0])); + continue; + } + + kv.put(field, map.get(field)); + } + return kv; + } + + public Kv set(K k, V v) { + put(k, v); + return this; + } + + public Kv putAll(Kv kv) { + kv.forEach((k, v) -> put(k, v)); + return this; + } + + // 将obj 属性映射到Kv 中 + public static Kv toKv(Object m, String... fields) { + Kv kv = Kv.of(); + if (m == null) { + return kv; + } + Stream.of(fields).forEach(field -> { + String filedT = field; + String filedS = field; + + try { + if (field.contains("=")) { + String[] arr = field.split("="); + filedT = arr[0]; + filedS = arr[1]; + } + + Method method = m.getClass().getMethod("get" + Utils.toUpperCaseFirst(filedS)); + if (method != null) { + kv.set(filedT, method.invoke(m)); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + new IllegalArgumentException(String.format("Kv.toKv获取 获取参数[]失败", field), e); + } + }); + + return kv; + } + + public static List toKv(Collection datas, String... fields) { + return datas.stream().map(x -> toKv(x, fields)).collect(Collectors.toList()); + } + + public static Kv toKv(Object m) { + return toKv(m, Kv.of(), m.getClass()); + } + + private static Kv toKv(Object m, Kv kv, Class clazz) { + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (!method.getName().startsWith("get") || method.getParameterCount() > 0 || "getClass".equals(method.getName())) + continue; + + String k = Utils.toLowerCaseFirst(method.getName().replaceFirst("get", "")); + if (!kv.containsKey(k) || Utils.isEmpty(kv.get(k))) { + try { + kv.set(k, method.invoke(m)); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + } + } + + for (Field field : clazz.getDeclaredFields()) { + if (field.getAnnotation(Id.class) != null) { + try { + field.setAccessible(true); + kv.set("_id", field.get(m)); + break; + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + } + + Class superclass = clazz.getSuperclass(); + if (superclass != null) { + kv = toKv(m, kv, superclass); + } + return kv; + } + + public T toBean(Class type) { + return toBean(this, type); + } + + // 首字母大写 + private static Function upFirst = (s) -> { + return s.substring(0, 1).toUpperCase() + s.substring(1); + }; + + private static Predicate isNumber = (t) -> { + return t == Integer.class || t == int.class + || t == Long.class || t == long.class + || t == Float.class || t == float.class + || t == Double.class || t == double.class + || t == Short.class || t == short.class + || t == Byte.class || t == byte.class + ; + }; + + public static T toAs(Object v, Class clazz) { + if (v == null) { + return null; + } else if (v.getClass() == clazz) { + return (T) v; + } else if (clazz == String.class) { + return (T) String.valueOf(v); + } + + Object v1 = v; + try { + + if (v.getClass() == Long.class) {//多种数值类型的处理: Long => x + switch (clazz.getSimpleName()) { + case "int", "Integer" -> v1 = (int) (long) v; + case "short", "Short" -> v1 = (short) (long) v; + case "float", "Float" -> v1 = (float) (long) v; + case "byte", "Byte" -> v1 = (byte) (long) v; + } + } else if (v.getClass() == Double.class) { + if (isNumber.test(clazz)) { + switch (clazz.getSimpleName()) { + case "long", "Long" -> v1 = (long) (double) v; + case "int", "Integer" -> v1 = (int) (double) v; + case "short", "Short" -> v1 = (short) (double) v; + case "float", "Float" -> v1 = (float) (double) v; + case "byte", "Byte" -> v1 = (byte) (double) v; + } + } else if (clazz == String.class) { + v1 = String.valueOf(v); + } + } else if (v.getClass() == String.class) { + switch (clazz.getSimpleName()) { + case "Date" -> v1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse((String) v); + case "short", "Short" -> v1 = (short) Double.parseDouble((String) v); + case "float", "Float" -> v1 = (float) Double.parseDouble((String) v); + case "int", "Integer" -> v1 = (int) Double.parseDouble((String) v); + case "long", "Long" -> v1 = (long) Double.parseDouble((String) v); + case "double", "Double" -> v1 = Double.parseDouble((String) v); + case "byte", "Byte" -> v1 = Byte.parseByte((String) v); + } + } else if (v.getClass() == Integer.class) { + switch (clazz.getSimpleName()) { + case "long", "Long" -> v1 = (long) (int) v; + case "short", "Short" -> v1 = (short) (int) v; + case "float", "Float" -> v1 = (float) (int) v; + case "byte", "Byte" -> v1 = (byte) (int) v; + } + } else if (v.getClass() == Float.class) { + switch (clazz.getSimpleName()) { + case "long", "Long" -> v1 = (long) (float) v; + case "int", "Integer" -> v1 = (int) (float) v; + case "short", "Short" -> v1 = (short) (float) v; + case "byte", "Byte" -> v1 = (byte) (float) v; + } + } else { + v1 = v; + } + } catch (ParseException e) { + e.printStackTrace(); + } + return (T) v1; + } + + public static T toBean(Map map, Class clazz) { + //按照方法名 + 类型寻找, + //按照方法名 寻找 + //+ + Object obj = null; + try { + obj = clazz.getDeclaredConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + new IllegalArgumentException("创建对象实列失败", e); // 检查clazz是否有无参构造 + } + + for (String k : (Set) map.keySet()) { + Object v = map.get(k); + if (v == null) continue; + //寻找method + try { + String methodName = "set" + upFirst.apply(k); + Class tClazz = null; + Method method = null; + try { + method = clazz.getMethod(methodName, tClazz = v.getClass()); + } catch (NoSuchMethodException e) { + //e.printStackTrace(); + } + if (method == null) { + for (Method _method : clazz.getMethods()) { + if (methodName.equals(_method.getName()) && _method.getParameterCount() == 1) { + method = _method; + tClazz = _method.getParameterTypes()[0]; + } + } + } + + if (method == null) { + for (Method _method : clazz.getMethods()) { + if (methodName.equalsIgnoreCase(_method.getName()) && _method.getParameterCount() == 1) { + method = _method; + tClazz = _method.getParameterTypes()[0]; + } + } + } + + if (method != null) { + method.invoke(obj, toAs(v, tClazz)); + } + + //没有方法,找属性注解 + /*if (method == null) { + Field field = null; + Field[] fields = clazz.getDeclaredFields(); + for (Field _field : fields) { + To to = _field.getAnnotation(To.class); + if (to != null && k.equals(to.value())) { + field = _field; + tClazz = _field.getType(); + break; + } + } + + if (field != null) { + field.setAccessible(true); + field.set(obj, toAs(v, tClazz)); + } + }*/ + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } + } + + return (T) obj; + } + + public String toString() { + return JsonConvert.root().convertTo(this); + } + + public int getInt(String key) { + return getInt(key, 0); + } + + public int getInt(String key, int defaultValue) { + V v = get(key); + if (v == null) { + return defaultValue; + } + return toAs(v, int.class); + } + + public long getLong(String key) { + return getLong(key, 0); + } + + public long getLong(String key, long defaultValue) { + V v = get(key); + if (v == null) { + return defaultValue; + } + return toAs(v, long.class); + } + + public String getStr(String key) { + return toAs(get(key), String.class); + } + + public String getStr(String key, String defaultValue) { + V v = get(key); + if (v == null) { + return defaultValue; + } + return toAs(v, String.class); + } +} \ No newline at end of file diff --git a/src/com/zchd/base/util/QueueTask.java b/src/com/zchd/base/util/QueueTask.java new file mode 100644 index 0000000..aa4bbb0 --- /dev/null +++ b/src/com/zchd/base/util/QueueTask.java @@ -0,0 +1,146 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.zchd.base.util; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @param 泛型 + * @author zhangjx + */ +public class QueueTask { + private static List queueTasks = new ArrayList<>(); // queueTask 实例引用 + private static final AtomicInteger counter = new AtomicInteger(); + + protected final BlockingQueue queue; + + protected final int threads; + + protected Consumer consumer; + + protected Logger logger; + + public QueueTask(int threads) { + this.threads = threads; + this.queue = new LinkedBlockingQueue<>(); + } + + public QueueTask(int threads, int queueSize) { + this.threads = threads; + this.queue = new LinkedBlockingQueue<>(queueSize); + } + + public QueueTask(int threads, Logger logger, Consumer consumer) { + this.threads = threads; + this.queue = new LinkedBlockingQueue<>(); + this.init(logger, consumer); + } + + public T poll() { + return this.queue.poll(); + } + + public T task() throws InterruptedException { + return this.queue.take(); + } + + public int size() { + return this.queue.size(); + } + + public boolean add(T data) { + return this.queue.add(data); + } + + public boolean remove(T data) { + return this.queue.remove(data); + } + + public void put(T data) throws InterruptedException { + this.queue.put(data); + } + + public void init(Logger logger, Consumer consumer) { + this.logger = logger; + this.consumer = consumer; + Runnable task = () -> { + T data; + try { + while ((data = queue.take()) != null) { + try { + consumer.accept(data); + } catch (Throwable e) { + if (logger != null) logger.log(Level.SEVERE, "QueueTask Data[" + + (data == null ? null : data.getClass().getSimpleName()) + "](" + data + ") consume error", e); + } + } + } catch (InterruptedException ex) { + } + }; + for (int i = 0; i < threads; i++) { + Thread thread = new Thread(task); + thread.setName("QueueTask-" + i + "-Thread"); + thread.setDaemon(true); + thread.start(); + } + counter.addAndGet(threads); + queueTasks.add(this); + } + + public void destroy() { + int count = 0; + while (count < 50) { + if (queue.size() > 0) { + try { + Thread.sleep(200); + } catch (Exception e) { + break; + } + count++; + } else { + count = Integer.MAX_VALUE; + } + } + counter.addAndGet(-threads); + } + + public static int runningThreads() { + return counter.get(); + } + + // 队列堆叠信息获取 + public static List pileup() { + Map map = new HashMap(); + for (QueueTask queueTask : queueTasks) { + map.put(queueTask.logger.getName(), queueTask.queue.size()); + } + + List kvs = Utils.toList(queueTasks, x -> { + Kv kv = Kv.of(); + kv.set("name", x.logger.getName().replace("_DynLocal", "")); + kv.set("threadcount", x.threads); + kv.set("stack", x.queue.size()); + return kv; + }); + + return kvs; + } + + public static void destroys() { + for (QueueTask queueTask : queueTasks) { + queueTask.destroy(); + } + } +} diff --git a/src/com/zchd/base/util/QueueTasks.java b/src/com/zchd/base/util/QueueTasks.java new file mode 100644 index 0000000..95dc5a6 --- /dev/null +++ b/src/com/zchd/base/util/QueueTasks.java @@ -0,0 +1,38 @@ +package com.zchd.base.util; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Logger; + +/** + * 公共异步队列 + * + * @author: liangxy. + */ +public class QueueTasks { + + private static final QueueTask queueTask = new QueueTask<>(1); + + static { + queueTask.init(Logger.getLogger(QueueTasks.class.getSimpleName()), Runnable::run); + } + + public static void add(Runnable runnable) { + queueTask.queue.add(runnable); + } + + // -------------------------- 支持返回结果的任务队列 ----------------------------- + private static ExecutorService executor = Executors.newFixedThreadPool(1); + + public static CompletableFuture submit(Runnable task) { + return CompletableFuture.runAsync(() -> { + try { + executor.submit(task).get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + }); + } +} diff --git a/src/com/zchd/base/util/Utils.java b/src/com/zchd/base/util/Utils.java new file mode 100644 index 0000000..242efb5 --- /dev/null +++ b/src/com/zchd/base/util/Utils.java @@ -0,0 +1,1282 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.zchd.base.util; + +import org.redkale.boot.LogFileHandler; +import org.redkale.source.*; +import org.redkale.util.*; + +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.math.BigInteger; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAccessor; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.logging.LogManager; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static java.util.Arrays.asList; + +/** + * @author zhangjx + */ +public final class Utils { + + public static void main(String[] args) { + System.out.println(md5("123456")); + } + + public static final String HEADNAME_WS_SNCP_ADDRESS = "WS-SncpAddress"; + + private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + private Utils() { + } + + public static void initLogConfig() { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + final PrintStream ps = new PrintStream(out); + ps.println("handlers = java.util.logging.ConsoleHandler"); + ps.println(".level = FINEST"); + ps.println("java.util.logging.ConsoleHandler.level = FINEST"); + ps.println("java.util.logging.ConsoleHandler.formatter = " + LogFileHandler.LoggingFormater.class.getName()); + LogManager.getLogManager().readConfiguration(new ByteArrayInputStream(out.toByteArray())); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 获取当天yyyyMMddHHmmss格式的long值 + * + * @return yyyyMMddHHmmss格式的long值 + */ + public static long datetime14() { + LocalDateTime day = LocalDateTime.now(); + return day.getYear() * 10000_000000L + day.getMonthValue() * 100_000000 + day.getDayOfMonth() * 1000000 + + day.getHour() * 10000 + day.getMinute() * 100 + day.getSecond(); + } + + /** + * 获取当天yyMMddHHmmss格式的long值 + * + * @return yyMMddHHmmss格式的long值 + */ + public static long datetime12() { + LocalDateTime day = LocalDateTime.now(); + return day.getYear() % 100 * 10000_000000L + day.getMonthValue() * 100_000000 + day.getDayOfMonth() * 1000000 + + day.getHour() * 10000 + day.getMinute() * 100 + day.getSecond(); + } + + /** + * @param time 时间戳 + * @return yyyyMMddHHmm格式的long值 + */ + public static long yyyyMMddHHmm(long time) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(time); + TimeZone timeZone = TimeZone.getTimeZone(ZoneId.systemDefault()); + cal.setTimeZone(timeZone); + return cal.get(Calendar.YEAR) * 100000000L + (cal.get(Calendar.MONTH) + 1) * 1000000L + cal.get(Calendar.DAY_OF_MONTH) * 10000L + cal.get(Calendar.HOUR_OF_DAY) * 100L + cal.get(Calendar.MINUTE); + } + + /** + * @param yyyyMMddHHmm yyyyMMddHHmm格式的long值 + * @return 对应的时间戳 + */ + public static long yyyyMMddHHmmToMillis(long yyyyMMddHHmm) { + LocalDateTime time = LocalDateTime.parse(String.valueOf(yyyyMMddHHmm), DateTimeFormatter.ofPattern("yyyyMMddHHmm")); + return Utils.getEpochMilliByTime(time); + } + + public static byte[] encodeBySHA1(String key, String content) { + SecretKeySpec signKey = new SecretKeySpec(key.getBytes(), "HmacSHA1"); + try { + Mac mac = Mac.getInstance("HmacSHA1"); + mac.init(signKey); + return mac.doFinal(content.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + public static String encodeByBase64WithUrlSafe(byte[] content) { + return Base64.getEncoder().encodeToString(content).replaceAll("\\+", "-").replace("/", "_"); + } + + /** + * 判断对象是否为空 + * + * @param obj 待判断的对象 + * @return + */ + public static boolean isEmpty(Object obj) { + if (obj == null) { + return true; + } else if (obj instanceof String) { + return ((String) obj).trim().isEmpty(); + } else if (obj instanceof Collection) { + return ((Collection) obj).isEmpty(); + } else if (obj instanceof Map) { + return ((Map) obj).isEmpty(); + } else if (obj.getClass().isArray() && Array.getLength(obj) == 0) { + return true; + } + + return false; + } + + private static Map reproduceMap = new HashMap<>(); + + /** + * @param d 目标对象 + * @param s 源对象 + * @param 目标对象的数据类型 + * @param 源对象的数据类型 + * @return + */ + public static D copy(D d, S s) { + String reproductKey = d.getClass().getName() + "_" + s.getClass().getName(); + + Reproduce reproduce = reproduceMap.get(reproductKey); + if (reproduce == null) { + if (reproduce == null) { + reproduceMap.put(reproductKey, reproduce = (Reproduce) Reproduce.create(d.getClass(), s.getClass())); + } + } + + return reproduce.apply(d, s); + } + + /** + * 将字符串第一个字母转大写 + * + * @param str 待转换字符串 + * @return + */ + public static String toUpperCaseFirst(String str) { + Objects.requireNonNull(str); + return str.substring(0, 1).toUpperCase() + str.substring(1); + } + + /** + * 将字符串第一个字母转小写 + * + * @param str 待转换字符串 + * @return + */ + public static String toLowerCaseFirst(String str) { + Objects.requireNonNull(str); + return str.substring(0, 1).toLowerCase() + str.substring(1); + } + + /** + * 判断字符串是否由数字组成 + * + * @param str + * @return + */ + public static boolean isNumeric(String str) { + if (isEmpty(str)) { + return false; + } + + for (int i = 0; i < str.length(); i++) { + if (!Character.isDigit(str.charAt(i))) { + return false; + } + } + return true; + } + + public static List strToArr(String str) { + return strToArr(str, String.class); + } + + public static List strToArr(String str, Class clazz) { + if (isEmpty(str)) { + return new ArrayList<>(0); + } + List list = Arrays.stream(str.split(",")) + .filter(f -> !isEmpty(f)) + .map(x -> Kv.toAs(x.trim(), clazz)) + .collect(Collectors.toList()); + return list; + } + + public static String arrToStr(Object[] array) { + if (array == null) { + return ""; + } + return arrToStr(asList(array)); + } + + public static String trim(String str, String substr) { + if (str == null || str.length() == 0) { + return ""; + } + if (str.startsWith(substr)) { + str = str.substring(substr.length()); + } + if (str.endsWith(substr)) { + str = str.substring(0, str.length() - substr.length()); + } + return str; + } + + public static String join(Object[] elems, String sep) { + return join(asList(elems), sep); + } + + public static String join(Collection elems, String sep) { + if (elems == null || elems.isEmpty()) { + return ""; + } + if (sep == null) { + sep = ","; + } + + StringBuilder buf = new StringBuilder(); + for (Object elem : elems) { + buf.append(elem).append(sep); + } + buf.deleteCharAt(buf.length() - 1); + return buf.toString(); + } + + public static String joinToSql(Object[] elems) { + return joinToSql(asList(elems)); + } + + public static String joinToSql(Collection elems) { + if (elems == null || elems.isEmpty()) { + return ""; + } + + StringBuilder buf = new StringBuilder(); + for (Object elem : elems) { + buf.append("'").append(elem).append("',"); + } + buf.deleteCharAt(buf.length() - 1); + return buf.toString(); + } + + public static String arrToStr(Collection array) { + if (Utils.isEmpty(array)) { + return ""; + } + StringBuilder builder = new StringBuilder(); + array.stream().filter(f -> !isEmpty(f)).forEach(x -> builder.append(",").append(x instanceof String ? x : x.toString())); + return builder.append(",").toString(); + } + + public static String arrToSqlStr(Collection array) { + if (Utils.isEmpty(array)) { + return ""; + } + + StringBuilder buf = new StringBuilder(); + array.forEach(x -> buf.append(x).append(",")); + buf.deleteCharAt(buf.length() - 1); + + return buf.toString(); + } + + + public static List parseHtmlImage(String html) { + Pattern pattern = Pattern.compile("(?<=( ls = new ArrayList<>(); + while (match.find()) { + ls.add(match.group()); + } + return ls; + } + + /** + * 根据pattern格式化给定时间 + * + * @param accessor 指定时间 + * @param pattern 格式化pattern + * @return + */ + public static String formatByPattern(TemporalAccessor accessor, String pattern) { + if (Utils.isEmpty(pattern)) pattern = "yyyy-MM-dd"; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + return formatter.format(accessor); + } + + public static String formatByPattern(long millis, String pattern) { + if (Utils.isEmpty(pattern)) pattern = "yyyy-MM-dd HH:mm:ss"; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + return formatter.format(LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault())); + } + + /** + * 获取给定时间距离1970-1-1 00:00:00的毫秒数 + * + * @param time 时间 + * @return + */ + public static long getEpochMilliByTime(LocalDateTime time) { + return time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } + + /** + * @param rs + * @param type + * @param + * @return + */ + public static List queryList(ResultSet rs, Class type) { + try { + List list = new ArrayList(); + ResultSetMetaData metaData = rs.getMetaData(); + int count = metaData.getColumnCount(); + while (rs.next()) { + Kv row = Kv.of(); + for (int i = 1; i <= count; i++) { + String columnTypeName = metaData.getColumnTypeName(i); + //String columnName = metaData.getColumnName(i); + String columnLabel = metaData.getColumnLabel(i); + row.put(columnLabel, null); + + if (rs.getObject(i) != null) { + switch (columnTypeName) { + case "DATETIME", "TIMESTAMP", "DATE" -> row.put(columnLabel, rs.getTimestamp(i).getTime()); + default -> row.put(columnLabel, rs.getObject(i)); + } + } + } + list.add((Map.class == type || Kv.class == type) ? row : Kv.toBean(row, type)); + } + + return list; + } catch (SQLException e) { + e.printStackTrace(); + return null; + } + } + + /** + * 查询 第一条的 第一列数据 值 + * + * @param rs + * @param type + * @param + * @return + */ + public static T findColumn(ResultSet rs, Class type) { + try { + Object v = null; + while (rs.next()) { + ResultSetMetaData metaData = rs.getMetaData(); + int count = metaData.getColumnCount(); + + for (int i = 1; i <= count; i++) { + String columnTypeName = metaData.getColumnTypeName(i); + if (rs.getObject(i) != null) { + switch (columnTypeName) { + case "DATETIME", "TIMESTAMP", "DATE" -> v = rs.getTimestamp(i).getTime(); + default -> v = rs.getObject(i); + } + } + break; + } + } + + return Kv.toAs(v, type); + } catch (SQLException e) { + e.printStackTrace(); + return null; + } + } + + public static void batchQueryExecute(DataSource dataSource, Class clz, SelectColumn column, FilterNode node, Flipper flipper, Consumer> consumer) { + Number count = dataSource.getNumberResult(clz, FilterFunc.COUNT, null, node); + if (count == null) + return; + for (int offset = flipper.getOffset(); offset < count.intValue(); offset = offset + flipper.getLimit()) { + flipper.setOffset(offset); + consumer.accept(dataSource.queryList(clz, column, flipper, node)); + } + } + + public static void batchQueryExecute(DataSource dataSource, int limit, Class clz, SelectColumn column, FilterNode node, Consumer> consumer) { + batchQueryExecute(dataSource, clz, column, node, new Flipper(limit), consumer); + } + + public static void batchExecute(Collection data, int limit, Consumer> consumer) { + for (int offset = 0; offset < data.size(); offset = offset + limit) { + consumer.accept(data.stream().skip(offset).limit(limit).collect(Collectors.toCollection(HashSet::new))); + } + } + + /** + * List 混排 + * + * @param list + * @return + */ + public static List mix(List list) { + int len = list.size(); + Random random = new Random(); + for (int i = 0; i < len; i++) { + int r = random.nextInt(len); + if (i == r) continue; + + T x = list.get(i); + list.set(i, list.get(r)); + list.set(r, x); + } + return list; + } + + @Comment("获取集合随机元素") + public static List randomItems(List list, int len) { + List randoms = getRandoms(list.size(), len); + List items = new ArrayList<>(randoms.size()); + + randoms.forEach(x -> items.add(list.get(x))); + return items; + } + + @Comment("获取随机数集合") + private static List getRandoms(int max, int len) { + Set randoms = new HashSet<>(); + Random random = new Random(); + while (randoms.size() < len && randoms.size() < max) { + randoms.add(random.nextInt(max)); + } + List list = randoms.stream().collect(Collectors.toList()); + return mix(list); + } + + /** + * unicode转中文 + * + * @param str + * @return + */ + public static String unicodeToCn(String str) { + Pattern pattern = Pattern.compile("(\\\\u(\\p{XDigit}{4}))"); + Matcher matcher = pattern.matcher(str); + char ch; + while (matcher.find()) { + ch = (char) Integer.parseInt(matcher.group(2), 16); + str = str.replace(matcher.group(1), ch + ""); + } + return str; + } + + /** + * 计算字符串的字符长度 + * + * @param value + * @return + */ + public static int strLength(String value) { + int valueLength = 0; + String chinese = "[\u4e00-\u9fa5]"; + for (int i = 0; i < value.length(); i++) { + String temp = value.substring(i, i + 1); + if (temp.matches(chinese)) { + valueLength += 2; + } else { + valueLength += 1; + } + } + return valueLength; + } + + @Comment("解析文本得到 @[用户ID:用户名称] 的用户内容") + public static List parseNoticeUser(String content) { + if (Utils.isEmpty(content)) { + return new ArrayList<>(0); + } + + List ls = new ArrayList<>(); + Pattern compile = Pattern.compile("(?<=@\\[)\\d+:[A-Za-z0-9_\\u2E80-\\u9FFF]+(?=])"); + Matcher matcher = compile.matcher(content); + while (matcher.find()) { + ls.add(matcher.group()); + } + return ls; + } + + public static String randomIP() { + // aaa.aaa.aaa.aaa + StringBuilder buf = new StringBuilder(); + + Random r = new Random(); + buf.append("x").append("."); + buf.append(r.nextInt(255)).append("."); + buf.append(r.nextInt(255)).append("."); + buf.append(r.nextInt(255)); + + int n = r.nextInt(50);// + System.out.println(n / 10f); + + return buf.toString(); + } + + public static String fmt36(int n) { + return Integer.toString(n, 36); + } + + public static String fmt36(long n) { + return Long.toString(n, 36); + } + + public static Map toMap(Collection list, Function fun) { + Map map = new HashMap<>(list.size()); + for (V v : list) { + if (v == null) { + continue; + } + map.put(fun.apply(v), v); + } + return map; + } + + public static Map toMap(Collection list, Function fun, Function fun2) { + Map map = new HashMap<>(list.size()); + for (V v : list) { + if (v == null) { + continue; + } + map.put(fun.apply(v), fun2.apply(v)); + } + return map; + } + + public static List toList(Collection list, Function fun) { + if (list == null || list.isEmpty()) { + return new ArrayList<>(); + } + List list1 = new ArrayList<>(list.size()); + list.forEach(x -> list1.add(fun.apply(x))); + return list1; + } + + public static Set toSet(Collection list, Function fun) { + if (list == null || list.isEmpty()) { + return new HashSet<>(); + } + Set set = new HashSet<>(list.size()); + list.forEach(x -> set.add(fun.apply(x))); + return set; + } + + public static List filter(Collection list, Predicate predicate) { + if (list == null || list.isEmpty()) { + return new ArrayList<>(); + } + List list1 = new ArrayList<>(list.size()); + list.forEach(x -> { + if (!predicate.test(x)) { + return; + } + + list1.add(x); + }); + + return list1; + } + + public static List filterToList(Collection list, Predicate predicate, Function fun) { + if (list == null || list.isEmpty()) { + return new ArrayList<>(); + } + List list1 = new ArrayList<>(list.size()); + + list.forEach(x -> { + if (!predicate.test(x)) { + return; + } + + list1.add(fun.apply(x)); + }); + + return list1; + } + + public static Set filterToSet(Collection list, Predicate predicate, Function fun) { + if (list == null || list.isEmpty()) { + return new HashSet<>(); + } + Set set = new HashSet<>(list.size()); + + list.forEach(x -> { + if (!predicate.test(x)) { + return; + } + set.add(fun.apply(x)); + }); + + return set; + } + + public static Map> group(Collection list, Function fun) { + if (list == null || list.isEmpty()) { + return new HashMap<>(); + } + return list.stream().collect(Collectors.groupingBy(fun)); + } + + public static Map> group(Collection list, Function fun, Function fun2) { + if (list == null || list.isEmpty()) { + return new HashMap<>(); + } + Map> group = group(list, fun); + Map> _group = new HashMap<>(); + group.forEach((k, v) -> _group.put(k, toList(v, fun2))); + return _group; + } + + public static String getHtmlBody(String html) { + String s = html.replaceAll("\n", ""); + int bodyIndex = s.indexOf(""); + if (bodyIndex > -1) { + bodyIndex = bodyIndex + 6; + int lastIndexOf = s.lastIndexOf(""); + if (lastIndexOf < bodyIndex) lastIndexOf = s.length(); + s = s.substring(bodyIndex, lastIndexOf); + } + return s; + } + + public static String getHtmlText(String html) { + return html.replaceAll("<([^ \\f\\n\\r\\t\\v<]| )+>", ""); + } + + + //获取指定日期的 前/后 几天日期 + public static int plusDays(int date, int daynum) { + if (daynum == 0 || date == 0) { + return date; + } + + LocalDate ld = LocalDate.parse(String.valueOf(date), DateTimeFormatter.ofPattern("yyyyMMdd")); + ld = ld.plusDays(daynum); + return ld.getYear() * 10000 + ld.getMonthValue() * 100 + ld.getDayOfMonth(); + } + + //获取指定日期的 前/后 几天日期 + public static long plusDays(long datetime, int daynum) { + if (daynum == 0 || datetime == 0) { + return datetime; + } + + ZoneId zoneId = ZoneId.systemDefault(); + LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(datetime), zoneId); + time = time.plusDays(daynum); + return time.toInstant(ZoneOffset.of("+8")).toEpochMilli(); + } + + //获取两个日期相差天数 date1 - date2 + public static long getDifferDate(int date, int date2) { + LocalDate dateFirst = LocalDate.parse(String.valueOf(date), DateTimeFormatter.ofPattern("yyyyMMdd")); + LocalDate dateSecond = LocalDate.parse(String.valueOf(date2), DateTimeFormatter.ofPattern("yyyyMMdd")); + return dateSecond.until(dateFirst, ChronoUnit.DAYS); + } + + //获取两个日期相差天数 startTime - endTime + public static int getDifferDate(long startTime, long endTime) { + ZoneId zoneId = ZoneId.systemDefault(); + LocalDateTime startDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(startTime), zoneId); + LocalDateTime endDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(endTime), zoneId); + return Math.toIntExact(endDateTime.until(startDateTime, ChronoUnit.DAYS)); + } + + // ----------------- + private static final MessageDigest sha1; + private static final MessageDigest md5; + private static final String AES_KEY = "HAOGAME_20200721"; + private static final Cipher aesEncrypter; //加密 + private static final Cipher aesDecrypter; //解密 + + static { + MessageDigest d = null; + try { + d = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException ex) { + throw new Error(ex); + } + sha1 = d; + try { + d = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException ex) { + throw new Error(ex); + } + md5 = d; + + Cipher cipher = null; + final SecretKeySpec aesKey = new SecretKeySpec(AES_KEY.getBytes(), "AES"); + try { + cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.ENCRYPT_MODE, aesKey); + } catch (Exception e) { + throw new Error(e); + } + aesEncrypter = cipher; //加密 + try { + cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.DECRYPT_MODE, aesKey); + } catch (Exception e) { + throw new Error(e); + } + aesDecrypter = cipher; //解密 + } + + //AES加密 + public static String encryptAES(String value) { + if (value == null || value.isEmpty()) { + return value; + } + try { + synchronized (aesEncrypter) { + return Utility.binToHexString(aesEncrypter.doFinal(value.getBytes())); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + //AES解密 + public static String decryptAES(String value) { + if (value == null || value.isEmpty()) { + return value; + } + byte[] hex = Utility.hexToBin(value); + try { + synchronized (aesEncrypter) { + return new String(aesDecrypter.doFinal(hex)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Comment("将相差时间戳转换为 x年x个月x天x小时x分钟x秒") + public static String fomartTimediff(long time) { + long second = 1000; + long minute = second * 60; + long hour = minute * 60; + long day = hour * 24; + long month = day * 31; + long year = month * 12; + + int num = 0; + StringBuilder sb = new StringBuilder(); + //年 + long value = time / year; + if (value > 0 && time > 0) { + sb.append(value).append("年"); + time -= value * year; + num++; + } + + //月 + value = time / month; + if (value > 0 && time > 0) { + sb.append(value).append("个月"); + time -= value * month; + + if (++num >= 2) { + return sb.toString(); + } + } + + //天 + value = time / day; + if (value > 0 && time > 0) { + sb.append(value).append("天"); + time -= value * day; + + if (++num >= 2) { + return sb.toString(); + } + } + + //小时 + value = time / hour; + if (value > 0 && time > 0) { + sb.append(value).append("小时"); + time -= value * hour; + + if (++num >= 2) { + return sb.toString(); + } + } + + //分钟 + value = time / minute; + if (value > 0 && time > 0) { + sb.append(value).append("分钟"); + } + + return sb.toString(); + } + + @Comment("GET请求参数转换为字符,结果:p1=v1&p2=v2&p3=v3") + public static String convertHttpParams(Map map, boolean encode) { + if (map == null || map.isEmpty()) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + map.forEach((k, v) -> { + if (Utils.isEmpty(k) || Utils.isEmpty(v)) { + return; + } + String value = String.valueOf(v); + if (encode) { + value = URLEncoder.encode(String.valueOf(v), StandardCharsets.UTF_8); + } + sb.append("&").append(k).append("=").append(value); + }); + return sb.length() > 0 ? sb.substring(1) : ""; + } + + @Comment("对象转GET请求参数转换为字符,结果:p1=v1&p2=v2&p3=v3") + public static String convertHttpParams(Object o, List removeFields) { + Class c = o.getClass(); + Field[] fields = c.getDeclaredFields(); + Map map = new TreeMap(); + for (Field field : fields) { + field.setAccessible(true); + String name = field.getName(); + if (!Utils.isEmpty(removeFields) && removeFields.contains(name)) { + continue; + } + Object value = null; + try { + value = field.get(o); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + if (value != null) { + map.put(name, value); + } + } + return convertHttpParams(map, false); + } + + public static Sheet querySheetBySql(DataSource dataSource, String sql, Flipper flipper, Class type) { + DataJdbcSource jdbcSource = (DataJdbcSource) dataSource; + //总条数 + int total = jdbcSource.directQuery("select count(0) c from (" + sql + ") c", rs -> { + int result = 0; + try { + while (rs.next()) { + result = rs.getInt(1); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return result; + }); + + if (total == 0) { + return new Sheet<>(0, new ArrayList<>()); + } + + flipper = flipper == null ? new Flipper() : flipper; + if (flipper.getLimit() == 0) { + sql = String.format("%s %s ", sql, Utils.isEmpty(flipper.getSort()) ? "" : "order by " + flipper.getSort()); + } else { + sql = String.format("%s %s limit %s,%s ", sql, Utils.isEmpty(flipper.getSort()) ? "" : "order by " + flipper.getSort(), flipper.getOffset(), flipper.getLimit()); + } + List dataList = jdbcSource.directQuery(sql, rs -> Utils.queryList(rs, type)); + return new Sheet<>(total, dataList); + } + + @Comment("获取AppToken统一规则") + public static String getAppToken(Map header) { + String apptoken = ""; + String appos = header.get("appos"); + if ("android".equals(appos)) { + if (!Utils.isEmpty(header.get("oaid"))) { + apptoken = header.get("oaid"); + } else if (!Utils.isEmpty(header.get("imei"))) { + apptoken = String.format("IMEI_%s", header.get("imei")); + } else if (!Utils.isEmpty(header.get("androidid"))) { + apptoken = String.format("ANDROIDID_%s", header.get("androidid")); + } + } else { + apptoken = header.getOrDefault("idfa", ""); + } + + if (apptoken.isEmpty()) { + apptoken = header.getOrDefault("apptoken", ""); + } + return apptoken; + } + + + private static Random random = new Random(); + + public static int randomNum(int len) { + int rs = random.nextInt(9); + if (rs == 0) rs = 1; + for (int i = 0; i < len - 1; i++) { //总长度为6 + rs = rs * 10 + random.nextInt(9); + } + return rs; + } + + + public static String md5(String plainText) { + byte[] secretBytes = null; + try { + secretBytes = MessageDigest.getInstance("md5").digest( + plainText.getBytes()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5加密失败!", e); + } + String md5code = new BigInteger(1, secretBytes).toString(16); + for (int i = 0; i < 32 - md5code.length(); i++) { + md5code = "0" + md5code; + } + return md5code; + } + + public static byte[] encodeBySHA256(String key, String content) { + SecretKeySpec signKey = new SecretKeySpec(key.getBytes(), "HmacSHA256"); + try { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(signKey); + return mac.doFinal(content.getBytes("UTF-8")); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public static String encodeByBase64(byte[] content) { + final Base64.Encoder encoder = Base64.getEncoder(); + try { + return encoder.encodeToString(content).toLowerCase(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public static String getFormattedText(byte[] bytes) { + int len = bytes.length; + StringBuilder buf = new StringBuilder(len * 2); + // 把密文转换成十六进制的字符串形式 + for (int j = 0; j < len; j++) { + buf.append(HEX_DIGITS[(bytes[j] >> 4) & 0x0f]); + buf.append(HEX_DIGITS[bytes[j] & 0x0f]); + } + return buf.toString(); + } + + + /** + * 根据身份证号码计算年龄 + * + * @param idNumber 考虑到了15位身份证,但不一定存在 + */ + + public static int getAgeByIDNumber(String idNumber) { + String dateStr; + if (idNumber.length() == 15) { + dateStr = "19" + idNumber.substring(6, 12); + } else if (idNumber.length() == 18) { + dateStr = idNumber.substring(6, 14); + } else {//默认是合法身份证号,但不排除有意外发生 + return 0; + } + + + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd"); + try { + Date birthday = simpleDateFormat.parse(dateStr); + return getAgeByDate(birthday); + } catch (ParseException e) { + return 0; + } + + + } + + /** + * 根据生日计算年龄 + * + * @param dateStr 这样格式的生日 1990-01-01 + */ + + public static int getAgeByDateString(String dateStr) { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + try { + Date birthday = simpleDateFormat.parse(dateStr); + return getAgeByDate(birthday); + } catch (ParseException e) { + return -1; + } + } + + + public static int getAgeByDate(Date birthday) { + Calendar calendar = Calendar.getInstance(); + + //calendar.before()有的点bug + if (calendar.getTimeInMillis() - birthday.getTime() < 0L) { + return 0; + } + + + int yearNow = calendar.get(Calendar.YEAR); + int monthNow = calendar.get(Calendar.MONTH); + int dayOfMonthNow = calendar.get(Calendar.DAY_OF_MONTH); + + calendar.setTime(birthday); + + + int yearBirthday = calendar.get(Calendar.YEAR); + int monthBirthday = calendar.get(Calendar.MONTH); + int dayOfMonthBirthday = calendar.get(Calendar.DAY_OF_MONTH); + + int age = yearNow - yearBirthday; + + + if (monthNow <= monthBirthday && monthNow == monthBirthday && dayOfMonthNow < dayOfMonthBirthday || monthNow < monthBirthday) { + age--; + } + + return age; + } + + + /** + * @return 本周的结束时间 + * @description:获取本周的结束时 + */ + public static long getWeekEnd() { + // 当周结束时间 + Calendar currentDate = Calendar.getInstance(); + currentDate.setFirstDayOfWeek(Calendar.MONDAY); + currentDate.set(Calendar.HOUR_OF_DAY, 23); + currentDate.set(Calendar.MINUTE, 59); + currentDate.set(Calendar.SECOND, 59); + currentDate.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY); + + return currentDate.getTime().getTime(); + } + + /** + * @return 本周周一凌晨零点 + */ + public static long getWeekStart() { + Calendar currentDate = Calendar.getInstance(); + currentDate.setFirstDayOfWeek(Calendar.MONDAY); + currentDate.set(Calendar.HOUR_OF_DAY, 0); + currentDate.set(Calendar.MINUTE, 0); + currentDate.set(Calendar.SECOND, 0); + currentDate.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); + + return currentDate.getTime().getTime(); + } + + /** + * @description:获取半年前的时间 + */ + public static long getSixMonthsAgo() { + Calendar c = Calendar.getInstance(); + c.setTime(new Date()); + c.add(Calendar.MONTH, -5); + Date m3 = c.getTime(); + return m3.getTime(); + } + + /** + * 获取两时间戳之间的月份 + */ + public static List getMonthBetween(long minDate, long maxDate) { + ArrayList result = new ArrayList(); + SimpleDateFormat sdf = new SimpleDateFormat("MM");//格式化为年月 + + Calendar min = Calendar.getInstance(); + Calendar max = Calendar.getInstance(); + + min.setTime(new Date(minDate)); + min.set(min.get(Calendar.YEAR), min.get(Calendar.MONTH), 1); + + max.setTime(new Date(maxDate)); + max.set(max.get(Calendar.YEAR), max.get(Calendar.MONTH), 2); + + Calendar curr = max; + while (curr.after(min)) { + result.add(Integer.parseInt(sdf.format(curr.getTime()))); + curr.add(Calendar.MONTH, -1); + } + return result; + } + + public static int getMonthEndTime(int yyyyMMdd) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); + try { + Date parse = sdf.parse(yyyyMMdd + ""); + Calendar calendar = new GregorianCalendar(); + calendar.setTime(parse); + calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH)); + return Integer.parseInt(sdf.format(calendar.getTime())); + } catch (ParseException e) { + e.printStackTrace(); + } + return 0; + } + + /** + * 获取指定月份所有的日期 + */ + public static List getMonthAllTime(int date) { + List list = new ArrayList<>(); + String YEAR = String.valueOf(date).substring(0, 4); + String MONTH = String.valueOf(date).substring(4, 6); + SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd"); + Calendar c = Calendar.getInstance(); + c.set(Calendar.YEAR, Integer.parseInt(YEAR)); + c.set(Calendar.MONTH, Integer.parseInt(MONTH) - 1); + c.add(Calendar.MONTH, 0); + c.set(Calendar.DAY_OF_MONTH, 1);//设置为1号,当前日期既为本月第一天 + String first = format.format(c.getTime()); + String substring = first.substring(0, 6); + c.set(Calendar.DAY_OF_MONTH, c.getActualMaximum(Calendar.DAY_OF_MONTH)); + String last = format.format(c.getTime()); + String lastday = last.substring(6, 8); + + for (int i = 1; i <= Integer.parseInt(lastday); i++) { + String time; + if (i < 10) { + time = substring + "0" + i; + } else { + time = substring + i; + } + list.add(Integer.parseInt(time)); + } + return list; + } + + @Comment("交集") + public static List intersectionList(List list, List list2) { + return list.stream().filter(v -> list2.contains(v)).toList(); + } + + @Comment("差集") + public static List differenceList(List list, List list2) { + // list => {1,2,3} list2 => {1,2} result => {3} + return list.stream().filter(v -> !list2.contains(v)).toList(); + } + + @Comment("并集") + public static List unionList(List list, List list2) { + List all = list.parallelStream().toList(); + List temp = list2.parallelStream().toList(); + all.addAll(temp); + return all; + } + + + @Comment("去重并集") + public static List distinctUnionList(List list, List list2) { + return unionList(list, list2).stream().distinct().toList(); + } + + private static final List list = List.of(Integer.class, int.class, Long.class, long.class, Float.class, float.class, Double.class, double.class + , Short.class, short.class, Byte.class, byte.class, Boolean.class, boolean.class, String.class); + + public static void resetGender(Object o) throws Exception { + resetField(o, "gender", resetGenderFun); + } + + public static void resetField(Object o, String fieldName, Function resetFun) throws Exception { + if (o == null) { + return; + } + if (o instanceof Map) { + Map map = (Map) o; + + for (Map.Entry entry : map.entrySet()) { + String k = entry.getKey(); + Object v = entry.getValue(); + if (fieldName.equals(k)) { + map.put(fieldName, resetFun.apply(v)); + } else { + try { + resetField(v, fieldName, resetFun); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + } else if (o instanceof Collection) { + Collection c = (Collection) o; + for (Object x : c) { + try { + resetField(x, fieldName, resetFun); + } catch (Exception e) { + e.printStackTrace(); + } + } + + } else if (!list.contains(o.getClass())) { + Field[] fields = o.getClass().getDeclaredFields(); + for (Field field : fields) { + + if (fieldName.equals(field.getName())) { + Object o1 = o.getClass().getMethod("get" + Utils.toUpperCaseFirst(field.getName())).invoke(o); + Object o2 = resetFun.apply(o1); + o.getClass().getMethod("set" + Utils.toUpperCaseFirst(field.getName()), field.getType()).invoke(o, o2); + } else if (!list.contains(field.getType())) { + Object u = o.getClass().getMethod("get" + Utils.toUpperCaseFirst(field.getName())).invoke(o); + resetField(u, fieldName, resetFun); + } + } + } + + } + + + private static final Function resetGenderFun = (oldValue -> { + if (oldValue instanceof Short || oldValue instanceof Integer) { + short i = Short.parseShort(oldValue.toString()); + return (short) (i / 2); + } + return oldValue; + }); + +} diff --git a/src/com/zchd/zim/ImAccountService.java b/src/com/zchd/zim/ImAccountService.java new file mode 100644 index 0000000..bdc7a8a --- /dev/null +++ b/src/com/zchd/zim/ImAccountService.java @@ -0,0 +1,269 @@ +package com.zchd.zim; + +import com.zchd.base.BaseService; +import com.zchd.base.util.Kv; +import com.zchd.zim.bean.ImBean; +import com.zchd.zim.bean.SubscribeBean; +import com.zchd.zim.entity.AppInfo; +import com.zchd.zim.entity.ChannelUser; +import com.zchd.zim.entity.ImUser; +import com.zdemo.cachex.MyRedisCacheSource; +import com.zdemo.zhub.RpcResult; +import org.redkale.convert.json.JsonConvert; +import org.redkale.net.http.RestMapping; +import org.redkale.net.http.RestService; +import org.redkale.service.RetResult; +import org.redkale.source.*; +import org.redkale.util.AnyValue; +import org.redkale.util.Comment; +import org.redkale.util.TypeToken; +import org.redkale.util.Utility; + +import javax.annotation.Resource; +import java.io.Serializable; +import java.util.List; +import java.util.concurrent.CompletableFuture; + + +@RestService(name = "im_account", comment = "账号服务") +public class ImAccountService extends BaseService { + + + @Resource(name = "str_cache") + protected MyRedisCacheSource strCache; + @Resource + protected ImMessageMonitor messageMonitor; + @Resource + protected ImAccountService accountService; + + @Override + public void init(AnyValue config) { + + CompletableFuture.runAsync(() -> { + List list = zimSource.queryColumnList("userid", ImUser.class, new Flipper(1, "userid DESC"), (FilterBean) null); + int userid = 10000; + if (!list.isEmpty()) { + userid = Kv.toAs(list.get(0), int.class); + } + intCache.set("im:max-userid", userid); + }); + + // 游戏用户注册 + zhub.rpcSubscribe("im:account-register", new TypeToken() { + }, r -> { + ImBean bean = r.getValue(); + RetResult result = register(bean); + if (!result.isSuccess()) { + return r.buildError(result.getRetinfo()); + } + return r.buildResp(result.getResult()); + }); + + // 获取游戏用户 IM-TOKEN + zhub.rpcSubscribe("im:account-token", new TypeToken() { + }, r -> { + ImBean bean = r.getValue(); + RetResult result = getToken(bean); + if (!result.isSuccess()) { + return r.buildError(result.getRetinfo()); + } + return r.buildResp(result.getResult()); + }); + + // 为用户新增订阅频道 + zhub.rpcSubscribe("im:subscribe", new TypeToken() { + }, r -> { + SubscribeBean bean = r.getValue(); + RetResult result = subscribe(bean); + return r.buildResp(result.getResult()); + }); + + // 为用户取消订阅频道 + zhub.rpcSubscribe("im:unsubscribe", new TypeToken() { + }, r -> { + SubscribeBean bean = r.getValue(); + RetResult result = unsubscribe(bean); + return r.buildResp(result.getResult()); + }); + + // 禁言 todo: ①禁言时长 ②禁言指定频道 + zhub.rpcSubscribe("im:banned-talk", new TypeToken() { + }, r -> { + SubscribeBean bean = r.getValue(); + String imtoken = bean.getImtoken(); + + ImUser user = accountService.currentImUser(imtoken); + int userid = user.getUserid(); + zimSource.updateColumn(ImUser.class, userid, ColumnValue.create("status", ImUser.STATUS_FREEZE_ACTIVE)); + intCache.setBit("im:banned-talk", userid, true); + return r.buildResp(); + }); + + // 取消禁言 + zhub.rpcSubscribe("im:unbanned-talk", new TypeToken() { + }, r -> { + SubscribeBean bean = r.getValue(); + String imtoken = bean.getImtoken(); + + ImUser user = accountService.currentImUser(imtoken); + int userid = user.getUserid(); + zimSource.updateColumn(ImUser.class, userid, ColumnValue.create("status", 10)); + intCache.setBit("im:banned-talk", userid, false); + return r.buildResp(); + }); + } + + + @Comment("获取当前用户ID") + public ImUser currentImUser(String token) { + + ImUser user = strCache.hget("im:user-token", token, new TypeToken() { + }.getType()); + if (user == null) { + user = zimSource.find(ImUser.class, FilterNode.create("imtoken", token)); + strCache.hset("im:user-token", token, JsonConvert.root(), user); + } + return user; + } + + /*@Comment("获取当前用户ID") + public int currentImUserId(String token) { + return currentImUser(token).getUserid(); + }*/ + + + /* + Url: http://127.0.0.1:8021/im_account/register + Body: {"appid":"krlq07sx","appsecret":"79eb45ebdabc4d90bfb69949b8fd767e","guserid":"S_C_01_R0001","gender":10,"nickname":"M","face":"http://xxxxxx/xxx"} + Body: {"appid":"krlq07sx","appsecret":"79eb45ebdabc4d90bfb69949b8fd767e","guserid":"S_C_01_R0002","gender":20,"nickname":"V","face":"http://xxxxxx/xxx"} + */ + @RestMapping(name = "register", auth = false, comment = "获取当前登录用户ID") + public RetResult register(ImBean bean) { + RetResult result = check(bean); + if (!result.isSuccess()) { + return result; + } + + // 检查有无当前用户 + FilterNode node = FilterNode.create("guserid", bean.getGuserid()).and("appid", bean.getAppid()); + ImUser user = zimSource.find(ImUser.class, node); + + if (user == null) { + user = ImUser.buildImUser(bean.getGuserid(), bean.getAppid()); + int userid = (int) intCache.incr("im:max-userid"); + user.setUserid(userid); + + user.setImtoken(Utility.uuid()); // 生成IM-TOKEN + zimSource.insert(user); + +// intCache.set("im:account-user-token:" + user.getImtoken(), userid); + strCache.hset("im:user-token", user.getImtoken(), JsonConvert.root(), user); + + } + + return RetResult.success(Kv.of("token", user.getImtoken())); + } + + /* + Url: http://127.0.0.1:8021/im_account/register + Body: {"appid":"krlq07sx","appsecret":"79eb45ebdabc4d90bfb69949b8fd767e","guserid":"S_C_01_R0001"} + Body: {"appid":"krlq07sx","appsecret":"79eb45ebdabc4d90bfb69949b8fd767e","guserid":"S_C_01_R0002"} + */ + @RestMapping(name = "get_token", auth = false, comment = "获取平台识别Token") + public RetResult getToken(ImBean bean) { + RetResult result = check(bean); + if (!result.isSuccess()) { + return result; + } + + FilterNode node = FilterNode.create("appid", bean.getAppid()).and("guserid", bean.getGuserid()); + Serializable imtoken = zimSource.findColumn(ImUser.class, "imtoken", node); + + return RetResult.success(Kv.of("token", imtoken)); + } + + /* + Url: http://127.0.0.1:8021/im_app/subscribe + Body: {"imtoken":"krlq07sxS_C_01_R00011627473704040","channeltype":"Word","channelvalue":"W_C_01_001"} + Body: {"imtoken":"krlq07sxS_C_01_R00021627474715105","channeltype":"Word","channelvalue":"W_C_01_001"} + */ + @RestMapping(name = "subscribe", auth = false, comment = "给用户订阅频道信息") + public RetResult subscribe(SubscribeBean bean) { + ImUser user = accountService.currentImUser(bean.getImtoken()); + int userid = user.getUserid(); + try { + // 添加数据库订阅关系 + intCache.tryLock("im:channel-create:" + userid, 500); + FilterNode node1 = FilterNode.create("appid", user.getAppid()) + .and("channeltype", bean.getChanneltype()) + .and("channelvalue", bean.getChannelvalue()) + .and("userid", userid); + if (!zimSource.exists(ChannelUser.class, node1)) { + ChannelUser record = ChannelUser.buildChannelUser(userid, user.getAppid(), bean); + zimSource.insert(record); + + // 确保写入库后,再发送订阅事件 + messageMonitor.subscribeChannel(userid, record.buildChannelid(), true); + } + } finally { + intCache.unlock("im:channel-create:" + userid); + } + return RET_SUCCESS; + } + + + /* + Url: http://127.0.0.1:8021/im_app/unsubscribe + Body: {"imtoken":"krlq07sxS_C_01_R00011627473704040","channeltype":"Word","channelvalue":"W_C_01_001"} + Body: {"imtoken":"krlq07sxS_C_01_R00021627474715105","channeltype":"Word","channelvalue":"W_C_01_001"} + */ + @RestMapping(name = "unsubscribe", auth = false, comment = "取消频道订阅") + public RetResult unsubscribe(SubscribeBean bean) { + ImUser user = accountService.currentImUser(bean.getImtoken()); + int userid = user.getUserid(); + + + String channelid = user.getAppid() + "-" + bean.getChanneltype() + "-" + bean.getChannelvalue(); + messageMonitor.subscribeChannel(userid, channelid, false); + + // 删除数据库订阅关系 + FilterNode node1 = FilterNode.create("appid", user.getAppid()) + .and("channeltype", bean.getChanneltype()) + .and("channelvalue", bean.getChannelvalue()) + .and("userid", userid); + zimSource.delete(ChannelUser.class, node1); + return RET_SUCCESS; + } + + @Comment("平台安全校验") + public RetResult check(ImBean bean) { + FilterNode node = FilterNode.create("appid", bean.getAppid()).and("appsecret", bean.getAppsecret()).and("status", 10); + if (!zimSource.exists(AppInfo.class, node)) { + return retError("平台信息校验失败"); + } + return RET_SUCCESS; + } + + + // ---------------------------------- 测试 ---------------------------------- + + @RestMapping(name = "test_account_register", auth = false, comment = "rpc测试") + public RpcResult testAccountRegister(ImBean bean) { + return zhub.rpc("im:account-register", bean); + } + + @RestMapping(name = "test_account_token", auth = false, comment = "rpc测试") + public RpcResult testAccountToken(ImBean bean) { + return zhub.rpc("im:account-token", bean); + } + + @RestMapping(name = "test_subscribe", auth = false, comment = "rpc测试") + public RpcResult testSubscribe(SubscribeBean bean) { + return zhub.rpc("im:subscribe", bean); + } + + @RestMapping(name = "test_unsubscribe", auth = false, comment = "rpc测试") + public RpcResult testUnsubscribe(SubscribeBean bean) { + return zhub.rpc("im:unsubscribe", bean); + } +} diff --git a/src/com/zchd/zim/ImChatService.java b/src/com/zchd/zim/ImChatService.java new file mode 100644 index 0000000..37809a5 --- /dev/null +++ b/src/com/zchd/zim/ImChatService.java @@ -0,0 +1,193 @@ +package com.zchd.zim; + +import com.zchd.base.BaseService; +import com.zchd.base.util.Kv; +import com.zchd.base.util.QueueTask; +import com.zchd.zim.entity.ChannelMessage; +import com.zchd.zim.entity.ChannelUser; +import com.zchd.zim.entity.FriendMessage; +import com.zchd.zim.entity.ImUser; +import com.zdemo.zhub.ZHubClient; +import net.tccn.timer.Timers; +import org.redkale.net.http.RestService; +import org.redkale.net.http.WebSocketNode; +import org.redkale.source.ColumnValue; +import org.redkale.source.DataSource; +import org.redkale.source.FilterNode; +import org.redkale.source.Flipper; +import org.redkale.util.AnyValue; +import org.redkale.util.Comment; + +import javax.annotation.Resource; +import javax.persistence.Transient; +import java.util.List; + +import static org.redkale.source.FilterExpress.*; + +@RestService(name = "im_chat_x", comment = "IM消息总线") +public class ImChatService extends BaseService { + + @Resource(name = "im_chat") + protected WebSocketNode wsnode; + @Resource(name = "z_im") + protected DataSource zimSource; + @Resource(name = "zhub") + protected ZHubClient zhub; + + @Transient //消息发送 + protected QueueTask msgQueue = new QueueTask<>(20); + @Transient //消息存储 + protected QueueTask dbQueue = new QueueTask<>(1); + + + @Override + public void init(AnyValue config) { + msgQueue.init(logger, Runnable::run); + dbQueue.init(logger, Runnable::run); + } + + @Comment("发送私聊消息") + public void sendMsg(FriendMessage bean, int uid) { + wsnode.sendMessage(buildFriendMessageDeail(bean), uid).thenAccept(x -> { + if (x != 0) { + return; + } + +// intCache.incrHm("im:heartbeat:" + uid, "messagecount", -1); + // 更新状态失败, 200ms 后重试(当数据未写入的时候,更新失败) + Timers.tryDelay(() -> { + FilterNode node = FilterNode.create("senduserid", bean.getSenduserid()).and("receiveuserid", uid); + int updateColumn = zimSource.updateColumn(FriendMessage.class, node, ColumnValue.create("status", 10)); + + return updateColumn != 0; + }, 200, 3); + }); + } + + @Comment("拉取离线消息") + public void pullOfflineMsg(int userid) { + msgQueue.add(() -> { + // 拉取私聊离线消息 + List list = zimSource.queryList(FriendMessage.class, new Flipper(0, "createtime"), + FilterNode.create("receiveuserid", userid).and("status", 20)); + + list.forEach(x -> { + Integer join = wsnode.sendMessage(buildFriendMessageDeail(x), userid).join(); + if (join == 0) { + zimSource.updateColumn(FriendMessage.class, x.getMessageid(), ColumnValue.create("status", 10)); + } + }); + + // 拉取频道离线消息 + List userGroups = userChannels(userid); + for (ChannelUser userGroup : userGroups) { + Long lastAcceptTime = longCache.getHm("im:channel:" + userGroup.buildChannelid(), userGroup.getUserid()); + if (lastAcceptTime == null || lastAcceptTime == 0) { + lastAcceptTime = userGroup.getCreatetime(); + } + + // 获取当前频道的数据列表 + FilterNode node1 = FilterNode.create("appid", userGroup.getAppid()) + .and("channeltype", userGroup.getChanneltype()) + .and("channelvalue", userGroup.getChannelvalue()) + .and("senduserid", NOTEQUAL, userid) + .and("createtime", GREATERTHAN, lastAcceptTime) + .and("createtime", LESSTHAN, System.currentTimeMillis()) + .and("status", NOTEQUAL, 80); + try { + List messages = zimSource.queryList(ChannelMessage.class, new Flipper(50, "createtime desc"), node1); + + // 获取最新的消息集合后,从时间最早的开始推送 + for (int i = messages.size() - 1; i > -1; i--) { + ChannelMessage message = messages.get(i); + int status = wsnode.sendMessage(buildMessageDetail(message), userid).join(); + if (status != 0) { + return; + } + + // 每次推完一个消息后将拉取时间修改为当前消息的发送时间 + lastAcceptTime = message.getCreatetime(); + } + } finally { + + longCache.setHm("im:channel:" + userGroup.buildChannelid(), userGroup.getUserid(), lastAcceptTime); + // 变更最后拉取时间 + /*FilterNode node = FilterNode.create("appid", userGroup.getAppid()) + .and("channeltype", userGroup.getChanneltype()) + .and("channelvalue", userGroup.getChannelvalue()).and("userid", userid); + zimSource.updateColumn(ChannelUser.class, node, ColumnValue.create("lastaccepttime", lastAcceptTime)); // todo 中途失败时间有问题*/ + } + } + }); + } + + public List userChannels(int userid) { + FilterNode node = FilterNode.create("userid", userid) + .and(FilterNode.create("status", 10).or("status", 40)); + return zimSource.queryList(ChannelUser.class, node); + } + + @Comment("用户IM是否在线") + public boolean userOnline(int userid) { + return intCache.getBit("im:user", userid); + } + + @Comment("用户在线直接发送, 如果不在线先记录到数据库") + public void sendFriendMessage(FriendMessage message) { + dbQueue.add(() -> { + zimSource.insert(message); + }); + + int receiveuserid = message.getReceiveuserid(); + if (userOnline(receiveuserid)) { + zhub.publish("im:friend:" + receiveuserid, message); + } + } + + public void sendChannelMessage(ChannelMessage message) { + // 记录消息,发送群消息 + dbQueue.add(() -> { + zimSource.insert(message); + }); + + zhub.broadcast("im:channel:" + message.buildChannelid(), message); + } + + private String getGuserid(int userid) { + ImUser user = zimSource.find(ImUser.class, userid); + return user.getGuserid(); + } + + public int getUserid(String appid, String guserid) { + ImUser user = zimSource.find(ImUser.class, FilterNode.create("appid", appid).and("guserid", guserid)); + return user.getUserid(); + } + + public Kv buildFriendMessageDeail(FriendMessage message) { + Kv detail = Kv.toKv(message, "content", "messageid", "sendtime"); + detail.set("sendguserid", getGuserid(message.getSenduserid())); + Kv data = Kv.of("detail", detail).set("type", "friend-text"); + return data; + } + + public Kv buildBackMessage(FriendMessage message, String mck) { + Kv detail = Kv.toKv(message, "content", "messageid", "sendtime"); + detail.set("sendguserid", getGuserid(message.getSenduserid())); + Kv data = Kv.of("detail", detail).set("type", "friend-text").set("mck", mck); + return data; + } + + public Kv buildMessageDetail(ChannelMessage message) { + Kv detail = Kv.toKv(message, "content", "messageid", "sendtime", "channeltype", "channelvalue"); + detail.set("sendguserid", getGuserid(message.getSenduserid())); + Kv data = Kv.of("detail", detail).set("type", "channel-text"); + return data; + } + + public Kv buildBackMessage(ChannelMessage message, String mck) { + Kv detail = Kv.toKv(message, "content", "messageid", "sendtime", "channeltype", "channelvalue"); + detail.set("sendguserid", getGuserid(message.getSenduserid())); + Kv data = Kv.of("detail", detail).set("type", "channel-text").set("mck", mck); + return data; + } +} diff --git a/src/com/zchd/zim/ImChatWebSocket.java b/src/com/zchd/zim/ImChatWebSocket.java new file mode 100644 index 0000000..0ae2c2d --- /dev/null +++ b/src/com/zchd/zim/ImChatWebSocket.java @@ -0,0 +1,182 @@ +package com.zchd.zim; + +import com.zchd.base.info.SwearWordService; +import com.zchd.base.util.Kv; +import com.zchd.base.util.Utils; +import com.zchd.zim.entity.ChannelMessage; +import com.zchd.zim.entity.FriendMessage; +import com.zchd.zim.entity.ImUser; +import com.zdemo.cachex.MyRedisCacheSource; +import org.redkale.net.http.HttpRequest; +import org.redkale.net.http.RestOnMessage; +import org.redkale.net.http.RestWebSocket; +import org.redkale.net.http.WebSocket; +import org.redkale.service.RetResult; +import org.redkale.source.DataSource; +import org.redkale.util.Comment; + +import javax.annotation.Resource; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/* +* ws://127.0.0.1:8091/im_chat?imtoken=xx +* */ +@RestWebSocket(name = "im_chat", comment = "IM即使通讯", anyuser = true) +public class ImChatWebSocket extends WebSocket { + + @Resource(name = "z_im") + protected DataSource zimSource; + + @Resource(name = "int_cache") + protected MyRedisCacheSource intCache; + + @Resource + protected ImAccountService accountService; + @Resource + protected ImChatService chatService; + @Resource + protected ImMessageMonitor messageMonitor; + @Resource + protected SwearWordService swearWordService; + + @Override + protected CompletableFuture onOpen(HttpRequest request) { + return CompletableFuture.supplyAsync(() -> { + String token = request.getHeader("imtoken", request.getParameter("imtoken")); + return token; + }); + } + + @Override + protected CompletableFuture createUserid() { + return CompletableFuture.supplyAsync(() -> { + ImUser user = accountService.currentImUser(getSessionid()); + int userid = user.getUserid(); + + + if (userid > 0) { + WebSocket socket = findLocalWebSocket(userid); + if (socket != null) { + forceCloseWebSocket(userid); + } + + setAttribute("appid", user.getAppid()); + setAttribute("guserid", user.getGuserid()); + } + + return userid; + }); + } + + @Override + public CompletableFuture onConnected() { + return CompletableFuture.runAsync(() -> { + final int userid = (int) getUserid(); + getLogger().info("im:state-change:" + userid + "---ws connected---"); + intCache.setBit("im:user", userid, true); + // 上线开启订阅 + messageMonitor.online(userid); + // 拉取离线消息 + chatService.pullOfflineMsg(userid); + }); + } + + /* + {friend:{bean:{"type": "friend-text","content": "发给V的私聊"},extmap:{"mck":"11001and1234567860","guserid": "S_C_01_R0002"}}} + {friend:{bean:{"type": "friend-text","content": "发给M的私聊"},extmap:{"mck":"11001and1234567861","guserid": "S_C_01_R0001"}}} + */ + @RestOnMessage(name = "friend", comment = "私聊消息") + public void friend(@Comment("content、type") Map bean, + @Comment("mck、guserid") Map extmap) { + int userid = (int) getUserid(); + String guserid = extmap.get("guserid"); + String appid = (String) getAttribute("appid"); + String mck = extmap.get("mck"); + + int targetguserid = chatService.getUserid(appid, guserid); + FriendMessage message = FriendMessage.buildFriendMessage(bean.get("content"), userid, targetguserid); + + // 禁言检查 + if (intCache.getBit("im:banned-talk", userid)) { + sendTip("发送失败,你已被禁言", mck, 3002); + return; + } + + // 检查敏感词 :若加入消息撤回,可使用延时检查(后置审核),检查到违禁词 撤回消息 + RetResult check = swearWordService.check(message.getContent()); + if (!check.isSuccess()) { + sendTip("发送消息含有敏感词", mck, 3001); + return; + } + + // 发送消息 + chatService.sendFriendMessage(message); + + // 回执 + Kv kv = chatService.buildBackMessage(message, mck); + send(kv); + } + + /* + {channel:{bean:{"type": "channel-test","content": "asfsafasfsaf"},extmap:{"mck":"11001and1234567860","channeltype": "world","channelvalue": "2"}}} + {channel:{bean:{"type": "channel-test","content": "这是世界频道消息V"},extmap:{"mck":"11001and1234567860","channeltype": "Word","channelvalue": "W_C_01_001"}}} + */ + @RestOnMessage(name = "channel", comment = "频道消息") + public void channel(@Comment("content、type") Map bean, + @Comment("mck、channeltype、channelvalue") Map extmap) { + int userid = (int) getUserid(); + String appid = (String) getAttribute("appid"); + + String channeltype = extmap.get("channeltype"); + String channelvalue = extmap.get("channelvalue"); + String mck = extmap.get("mck"); + + // 禁言检查 + if (intCache.getBit("im:banned-talk", userid)) { + sendTip("发送失败,你已被禁言", mck, 3002); + return; + } + // 检查敏感词 :若加入消息撤回,可使用延时检查(后置审核),检查到违禁词 撤回消息 + RetResult check = swearWordService.check(bean.get("content")); + if (!check.isSuccess()) { + sendTip("发送消息含有敏感词", mck, 3001); + return; + } + + ChannelMessage message = new ChannelMessage(); + message.setContent(bean.get("content")); + message.setAppid(appid); + message.setSenduserid(userid); + message.setChanneltype(channeltype); + message.setChannelvalue(channelvalue); + message.setCreatetime(System.currentTimeMillis()); + message.setStatus((short) 20); + message.setMessageid(Utils.fmt36(message.getSenduserid()) + "-" + Utils.fmt36(message.getCreatetime())); + + // 发送 + chatService.sendChannelMessage(message); + + // 回执 + Kv kv = chatService.buildBackMessage(message, mck); + send(kv); + } + + @Override + public CompletableFuture onClose(int code, String reason) { + final int userid = (int) getUserid(); + getLogger().info("im:state-change:" + userid + "---close---" + code + "---" + reason); + intCache.setBit("im:user:", userid, false); + // 取消用户订阅 + messageMonitor.offline(userid); + return null; + } + + @Comment("发送提示信息") + public void sendTip(String tipinfo, String mck, int code) { + Kv tip = Kv.of("type", "tip") + .set("mck", mck).set("code", code) + .set("info", tipinfo); + send(tip); + } +} diff --git a/src/com/zchd/zim/ImMessageMonitor.java b/src/com/zchd/zim/ImMessageMonitor.java new file mode 100644 index 0000000..4d63855 --- /dev/null +++ b/src/com/zchd/zim/ImMessageMonitor.java @@ -0,0 +1,166 @@ +package com.zchd.zim; + +import com.zchd.base.BaseService; +import com.zchd.base.util.Kv; +import com.zchd.base.util.QueueTask; +import com.zchd.base.util.Utils; +import com.zchd.zim.entity.ChannelMessage; +import com.zchd.zim.entity.ChannelUser; +import com.zchd.zim.entity.FriendMessage; +import com.zdemo.cachex.MyRedisCacheSource; +import org.redkale.net.http.RestService; +import org.redkale.net.http.WebSocketNode; +import org.redkale.util.AnyValue; +import org.redkale.util.Comment; +import org.redkale.util.TypeToken; +import org.redkale.util.Utility; + +import javax.annotation.Resource; +import java.util.BitSet; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +@RestService(name = "im_message_monitor", comment = "总线消息订阅管理") +public class ImMessageMonitor extends BaseService { + + @Resource + protected ImChatService chatService; + + @Resource(name = "im_chat") + protected WebSocketNode wsnode; + + @Resource(name = "int_cache") + protected MyRedisCacheSource intCache; + @Resource(name = "long_cache") + protected MyRedisCacheSource longCache; + + protected BitSet bitSet = new BitSet(); // IM用户连接当前实例记录 + + protected ConcurrentHashMap> channelSubscribe = new ConcurrentHashMap<>(); + + protected final QueueTask messageQueue = new QueueTask<>(1); + + @Override + public void init(AnyValue config) { + messageQueue.init(Logger.getLogger(this.getClass().getSimpleName()), Runnable::run); + + // 订阅用户动态广播消息, 用户未连接当前IM 服务实例,不做通道订阅 + zhub.subscribe("im:channel", new TypeToken>() { + }, para -> { + int userid = para.getInt("userid"); + if (!bitSet.get(userid)) { + return; + } + + String channelid = para.get("channelid"); + String type = para.get("type"); + switch (type) { + case "subscribe" -> subscribeChannel(userid, channelid); + case "unsubscribe" -> unsubscribeChannel(userid, channelid); + } + }); + } + + @Comment("用户上线") + public void online(int userid) { + // 设置用户在当前实例标记 + bitSet.set(userid); + + // 开启频道订阅 + List list = chatService.userChannels(userid); + list.forEach(x -> { + subscribeChannel(userid, x.buildChannelid()); + }); + + // 开启对点订阅 + zhub.subscribe("im:friend:" + userid, new TypeToken() { + }, x -> { + messageQueue.add(() -> { + chatService.sendMsg(x, userid); + }); + }); + } + + @Comment("用户下线") + public void offline(int userid) { + // 设置用户在当前实例标记 + bitSet.clear(userid); + + // 取消点对点订阅 + zhub.unsubscribe("im:friend:" + userid); + + // 取消频道订阅 + List list = chatService.userChannels(userid); + list.forEach(x -> { + unsubscribeChannel(userid, x.buildChannelid()); + }); + } + + @Comment("订阅频道") + private void subscribeChannel(int userid, String channelid) { + Set uids = channelSubscribe.get(channelid); + if (uids == null) { + uids = new HashSet<>(); + + zhub.subscribe("im:channel:" + channelid, new TypeToken() { + }, x -> { + messageQueue.add(() -> { + Set _uids = channelSubscribe.get(channelid); + _uids.forEach(uid -> { + // 自己发的频道消息 不发送给自己 + if (x.getSenduserid() == uid) { + return; + } + + wsnode.sendMessage(chatService.buildMessageDetail(x), uid).thenAccept(status -> { + // 完成群发消息同步 + if (status != 0) { + return; + } + + // todo:并发场景下,会影响拉取离线消息 + longCache.setHm("im:channel:" + x.buildChannelid(), uid, System.currentTimeMillis()); + }); + }); + }); + }); + } + + uids.add(userid); + channelSubscribe.put(channelid, uids); + } + + @Comment("取消订阅频道") + private void unsubscribeChannel(int userid, String channelid) { + Set uids = channelSubscribe.get(channelid); + if (uids == null) { + return; + } + // 先移除用户 + uids.remove(userid); + // 当主题订阅没有人在线,取消总线订阅 + if (uids.isEmpty()) { + channelSubscribe.remove(channelid); + zhub.unsubscribe("im:channel:" + channelid); + return; + } + channelSubscribe.put(channelid, uids); + } + + public void subscribeChannel(int userid, String channelid, boolean subscribe) { + Kv kv = Kv.of("userid", userid) + .set("channelid", channelid) + .set("type", subscribe ? "subscribe" : "unsubscribe"); + zhub.broadcast("im:channel", kv); + } + + public static void main(String[] args) { + long millis = System.currentTimeMillis(); + System.out.println(Utils.fmt36(millis)); + System.out.println(millis); + System.out.println(Utility.uuid()); + } +} diff --git a/src/com/zchd/zim/bean/ImBean.java b/src/com/zchd/zim/bean/ImBean.java new file mode 100644 index 0000000..a30bc7f --- /dev/null +++ b/src/com/zchd/zim/bean/ImBean.java @@ -0,0 +1,17 @@ +package com.zchd.zim.bean; + +import com.zchd.base.BaseBean; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class ImBean extends BaseBean { + + private String appid; + + private String appsecret; + + private String guserid; + +} diff --git a/src/com/zchd/zim/bean/SubscribeBean.java b/src/com/zchd/zim/bean/SubscribeBean.java new file mode 100644 index 0000000..955b1d5 --- /dev/null +++ b/src/com/zchd/zim/bean/SubscribeBean.java @@ -0,0 +1,17 @@ +package com.zchd.zim.bean; + +import com.zchd.base.BaseBean; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class SubscribeBean extends BaseBean { + + private String imtoken = ""; + + private String channeltype = ""; + + private String channelvalue = ""; + +} diff --git a/src/com/zchd/zim/entity/AppInfo.java b/src/com/zchd/zim/entity/AppInfo.java new file mode 100644 index 0000000..6494228 --- /dev/null +++ b/src/com/zchd/zim/entity/AppInfo.java @@ -0,0 +1,34 @@ +package com.zchd.zim.entity; + +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.Column; +import javax.persistence.Id; +import javax.persistence.Table; + +@Setter +@Getter +@Table(comment = "平台信息表") +public class AppInfo { + + @Id + @Column(comment = "[平台标识]") + private String appid = ""; + + @Column(comment = "[平台名称]") + private String appname = ""; + + @Column(comment = "[校验码]") + private String appsecret = ""; + + @Column(comment = "[创建时间]") + private long createtime; + + @Column(comment = "[修改时间]") + private long updatetime; + + @Column(comment = "[状态]10正常, 40停用,80删除") + private short status; + +} diff --git a/src/com/zchd/zim/entity/ChannelMessage.java b/src/com/zchd/zim/entity/ChannelMessage.java new file mode 100644 index 0000000..61f78ab --- /dev/null +++ b/src/com/zchd/zim/entity/ChannelMessage.java @@ -0,0 +1,44 @@ +package com.zchd.zim.entity; + +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.Column; +import javax.persistence.Id; +import javax.persistence.Table; + +@Setter +@Getter +@Table(comment = "频道内容表") +public class ChannelMessage { + + @Id + @Column(comment = "[消息ID]") + private String messageid; + + @Column(comment = "[频道内容]") + private String content = ""; + + @Column(comment = "[平台ID]") + private String appid; + + @Column(comment = "[平台频道类型]") + private String channeltype; + + @Column(comment = "[平台频道ID]") + private String channelvalue; + + @Column(comment = "[发送人]") + private int senduserid; + + @Column(comment = "[创建时间]") + private long createtime; + + @Column(comment = "[状态]10正常,40发送失败,80屏蔽") + private short status; + + public String buildChannelid() { + return appid + "-" + channeltype + "-" + channelvalue; + } + +} diff --git a/src/com/zchd/zim/entity/ChannelUser.java b/src/com/zchd/zim/entity/ChannelUser.java new file mode 100644 index 0000000..eacd4f4 --- /dev/null +++ b/src/com/zchd/zim/entity/ChannelUser.java @@ -0,0 +1,63 @@ +package com.zchd.zim.entity; + +import com.zchd.zim.bean.SubscribeBean; +import lombok.Getter; +import lombok.Setter; +import org.redkale.util.Utility; + +import javax.persistence.Column; +import javax.persistence.Id; +import javax.persistence.Table; + +@Setter +@Getter +@Table(comment = "用户组信息表") +public class ChannelUser { + + @Id + @Column(comment = "[记录ID]") + private String cuid; + + @Column(comment = "[平台ID]") + private String appid = ""; + + @Column(comment = "[平台频道ID]") + private String channelvalue = ""; + + @Column(comment = "[平台频道类型]") + private String channeltype = ""; + + @Column(comment = "[订阅用户]") + private int userid; + + @Column(comment = "[创建时间]") + private long createtime; + + @Column(comment = "[最后接收时间]") + private long lastaccepttime; + + @Column(comment = "[状态]10正常,40禁言,41踢除") + private short status; + + public ChannelUser() { + } + + // 频道的订阅唯一KEY + public String buildChannelid() { + return appid + '-' + channeltype + '-' + channelvalue; + } + + public static ChannelUser buildChannelUser(int userid, String appid, SubscribeBean bean) { + ChannelUser record = new ChannelUser(); + record.userid = userid; + record.appid = appid; + record.channeltype = bean.getChanneltype(); + record.channelvalue = bean.getChannelvalue(); + record.createtime = System.currentTimeMillis(); + record.lastaccepttime = record.getCreatetime(); + record.cuid = Utility.uuid(); + record.status = 10; + + return record; + } +} diff --git a/src/com/zchd/zim/entity/FriendMessage.java b/src/com/zchd/zim/entity/FriendMessage.java new file mode 100644 index 0000000..3d8e113 --- /dev/null +++ b/src/com/zchd/zim/entity/FriendMessage.java @@ -0,0 +1,47 @@ +package com.zchd.zim.entity; + +import com.zchd.base.util.Utils; +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.Column; +import javax.persistence.Id; +import javax.persistence.Table; + +@Setter +@Getter +@Table(comment = "私聊内容表") +public class FriendMessage { + + @Id + @Column(comment = "[私聊ID]发送人-时间戳") + private String messageid = ""; + + @Column(comment = "[私聊内容]") + private String content = ""; + + @Column(comment = "[发送人]") + private int senduserid; + + @Column(comment = "[接收人]") + private int receiveuserid; + + @Column(comment = "[创建时间]") + private long createtime; + + @Column(comment = "[状态]10已发送,20未发送") + private short status; + + public static FriendMessage buildFriendMessage(String content, int senduserid, int receiveuserid) { + FriendMessage message = new FriendMessage(); + message.content = content; + message.senduserid = senduserid; + message.receiveuserid = receiveuserid; + + message.createtime = System.currentTimeMillis(); + message.status = 20; + message.messageid = Utils.fmt36(senduserid) + "-" + Utils.fmt36(message.createtime); + return message; + } + +} diff --git a/src/com/zchd/zim/entity/ImUser.java b/src/com/zchd/zim/entity/ImUser.java new file mode 100644 index 0000000..a861cb2 --- /dev/null +++ b/src/com/zchd/zim/entity/ImUser.java @@ -0,0 +1,50 @@ +package com.zchd.zim.entity; + +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.Column; +import javax.persistence.Id; +import javax.persistence.Table; +import java.io.Serializable; + +@Setter +@Getter +@Table(comment = "IM用户") +public class ImUser implements Serializable { + + public static final short STATUS_FREEZE_ACTIVE = 41;//禁言 + + @Id + @Column(comment = "[用户标识]") + private int userid; + + @Column(comment = "[平台自用用户标识]") + private String guserid = ""; + + @Column(comment = "[关联平台标识]") + private String appid = ""; + + @Column(comment = "[创建时间]") + private long createtime; + + @Column(comment = "[修改时间]") + private long updatetime; + + @Column(comment = "[接收时间]") + private long lastaccepttime; + + private String imtoken; + + @Column(comment = "[状态]10正常,40停用,80删除") + private short status; + + public static ImUser buildImUser(String guserid, String appid) { + ImUser user = new ImUser(); + user.guserid = guserid; + user.appid = appid; + user.status = 10; + user.createtime = System.currentTimeMillis(); + return user; + } +}