Browse Source

first commit

master
dengbiao 2 months ago
commit
6f15aa91fb
100 changed files with 7112 additions and 0 deletions
  1. +37
    -0
      Dockerfile_business
  2. +37
    -0
      Dockerfile_connect
  3. +37
    -0
      Dockerfile_logic
  4. +21
    -0
      LICENSE
  5. +100
    -0
      README.md
  6. +16
    -0
      build_docker.sh
  7. +3
    -0
      build_proto.sh
  8. +23
    -0
      chart/.helmignore
  9. +24
    -0
      chart/Chart.yaml
  10. +11
    -0
      chart/templates/configmap/configmap-prd.yaml
  11. +11
    -0
      chart/templates/configmap/configmap.yaml
  12. +44
    -0
      chart/templates/new_role/cluster_role.yaml
  13. +35
    -0
      chart/templates/role/cluster_role.yaml
  14. +52
    -0
      chart/templates/server/business.yaml
  15. +63
    -0
      chart/templates/server/connect.yaml
  16. +52
    -0
      chart/templates/server/logic.yaml
  17. +13
    -0
      chart/values.yaml
  18. +53
    -0
      cmd/business/main.go
  19. +63
    -0
      cmd/connect/main.go
  20. +3
    -0
      cmd/connect/run.sh
  21. +57
    -0
      cmd/file/main.go
  22. +54
    -0
      cmd/logic/main.go
  23. +56
    -0
      config/config.go
  24. +3
    -0
      docker/Dockerfile
  25. +2
    -0
      docs/plan.md
  26. +17
    -0
      docs/流程图/心跳.puml
  27. +31
    -0
      docs/流程图/消息单发.puml
  28. +31
    -0
      docs/流程图/消息群发.puml
  29. +15
    -0
      docs/流程图/登录.puml
  30. +16
    -0
      docs/流程图/离线消息同步.puml
  31. +138
    -0
      docs/错误处理.md
  32. +81
    -0
      go.mod
  33. +347
    -0
      go.sum
  34. +177
    -0
      internal/business/api/business_ext.go
  35. +59
    -0
      internal/business/api/business_ext_test.go
  36. +33
    -0
      internal/business/api/business_int.go
  37. +42
    -0
      internal/business/api/business_int_test.go
  38. +25
    -0
      internal/business/app/auth_app.go
  39. +72
    -0
      internal/business/app/user_app.go
  40. +25
    -0
      internal/business/comm/cloud_upload.go
  41. +20
    -0
      internal/business/comm/db/db_emoticon.go
  42. +119
    -0
      internal/business/comm/db/db_sys_cfg.go
  43. +84
    -0
      internal/business/comm/db/db_user_push_for_jg.go
  44. +28
    -0
      internal/business/comm/db/model/emoticon.go
  45. +8
    -0
      internal/business/comm/db/model/sys_cfg.go
  46. +8
    -0
      internal/business/comm/db/model/user_push_for_jg.go
  47. +8
    -0
      internal/business/comm/md/app_redis_key.go
  48. +17
    -0
      internal/business/comm/md/cfg_key.go
  49. +33
    -0
      internal/business/comm/md/file.go
  50. +16
    -0
      internal/business/comm/service/qiniu/bucket_create.go
  51. +18
    -0
      internal/business/comm/service/qiniu/bucket_delete.go
  52. +18
    -0
      internal/business/comm/service/qiniu/bucket_get_domain.go
  53. +22
    -0
      internal/business/comm/service/qiniu/init.go
  54. +42
    -0
      internal/business/comm/service/qiniu/req_img_upload.go
  55. +92
    -0
      internal/business/comm/service/svc_file_upload.go
  56. +106
    -0
      internal/business/comm/service/svc_sys_cfg_get.go
  57. +117
    -0
      internal/business/comm/utils/base64.go
  58. +369
    -0
      internal/business/comm/utils/convert.go
  59. +209
    -0
      internal/business/comm/utils/curl.go
  60. +22
    -0
      internal/business/comm/utils/file.go
  61. +245
    -0
      internal/business/comm/utils/logx/log.go
  62. +105
    -0
      internal/business/comm/utils/logx/output.go
  63. +192
    -0
      internal/business/comm/utils/logx/sugar.go
  64. +12
    -0
      internal/business/comm/utils/md5.go
  65. +46
    -0
      internal/business/comm/utils/rand.go
  66. +29
    -0
      internal/business/comm/utils/reply.go
  67. +23
    -0
      internal/business/comm/utils/serialize.go
  68. +208
    -0
      internal/business/comm/utils/time.go
  69. +7
    -0
      internal/business/domain/user/model/device.go
  70. +39
    -0
      internal/business/domain/user/model/user.go
  71. +74
    -0
      internal/business/domain/user/repo/auth_cache.go
  72. +23
    -0
      internal/business/domain/user/repo/auth_cache_test.go
  73. +19
    -0
      internal/business/domain/user/repo/auth_repo.go
  74. +25
    -0
      internal/business/domain/user/repo/im_package_dao.go
  75. +18
    -0
      internal/business/domain/user/repo/im_package_repo.go
  76. +51
    -0
      internal/business/domain/user/repo/user_cache.go
  77. +81
    -0
      internal/business/domain/user/repo/user_dao.go
  78. +44
    -0
      internal/business/domain/user/repo/user_dao_test.go
  79. +64
    -0
      internal/business/domain/user/repo/user_repo.go
  80. +132
    -0
      internal/business/domain/user/service/auth.go
  81. +12
    -0
      internal/business/domain/user/service/auth_test.go
  82. +32
    -0
      internal/connect/api.go
  83. +261
    -0
      internal/connect/conn.go
  84. +36
    -0
      internal/connect/conn_manager.go
  85. +72
    -0
      internal/connect/mq.go
  86. +105
    -0
      internal/connect/room.go
  87. +73
    -0
      internal/connect/tcp_server.go
  88. +87
    -0
      internal/connect/ws_server.go
  89. +305
    -0
      internal/logic/api/logic_ext.go
  90. +257
    -0
      internal/logic/api/logic_ext_test.go
  91. +79
    -0
      internal/logic/api/logic_int.go
  92. +128
    -0
      internal/logic/api/logic_int_test.go
  93. +85
    -0
      internal/logic/app/device_app.go
  94. +68
    -0
      internal/logic/app/friend_app.go
  95. +427
    -0
      internal/logic/app/group_app.go
  96. +110
    -0
      internal/logic/app/message_app.go
  97. +21
    -0
      internal/logic/app/room_app.go
  98. +66
    -0
      internal/logic/domain/device/device.go
  99. +78
    -0
      internal/logic/domain/device/device_dao.go
  100. +38
    -0
      internal/logic/domain/device/device_dao_test.go

+ 37
- 0
Dockerfile_business View File

@@ -0,0 +1,37 @@
# 多重构建,减少镜像大小
# 构建:使用golang:1.18版本
FROM registry.cn-shenzhen.aliyuncs.com/fnuoos-prd/golang:1.18.4 as build

# 容器环境变量添加,会覆盖默认的变量值
ENV GO111MODULE=on
ENV GOPROXY=https://goproxy.cn,direct
ENV TZ="Asia/Shanghai"
# 设置工作区
WORKDIR /go/release

# 把全部文件添加到/go/release目录
ADD . .

# 编译:把main.go编译成可执行的二进制文件,命名为zyos
RUN GOOS=linux CGO_ENABLED=0 GOARCH=amd64 go build -o zyos cmd/business/main.go

FROM ubuntu:xenial as prod
LABEL maintainer="dengbiao"

# 时区纠正
ENV TZ="Asia/Shanghai"
RUN rm -f /etc/localtime \
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo $TZ > /etc/timezone

# 在build阶段复制可执行的go二进制文件app
COPY --from=build /go/release/zyos ./zyos

# 启动服务
# 优化内核参数
#RUN sysctl -w net.ipv4.tcp_fin_timeout=15
#RUN sysctl -w net.ipv4.tcp_tw_recycle=1
#RUN sysctl -w net.ipv4.ip_local_port_range=1024 65535
#RUN sysctl -p
#CMD ["bash","-c","sysctl -w net.ipv4.tcp_timestamps=1 && sysctl -w net.ipv4.tcp_tw_reuse=1 && sysctl -w net.ipv4.tcp_tw_timeout=10 && sysctl -w net.ipv4.tcp_fin_timeout=10 && sysctl -w net.ipv4.ip_local_port_range='1024 65535' && ./zyos"]
CMD ["./zyos"]

+ 37
- 0
Dockerfile_connect View File

@@ -0,0 +1,37 @@
# 多重构建,减少镜像大小
# 构建:使用golang:1.18版本
FROM registry.cn-shenzhen.aliyuncs.com/fnuoos-prd/golang:1.18.4 as build

# 容器环境变量添加,会覆盖默认的变量值
ENV GO111MODULE=on
ENV GOPROXY=https://goproxy.cn,direct
ENV TZ="Asia/Shanghai"
# 设置工作区
WORKDIR /go/release

# 把全部文件添加到/go/release目录
ADD . .

# 编译:把main.go编译成可执行的二进制文件,命名为zyos
RUN GOOS=linux CGO_ENABLED=0 GOARCH=amd64 go build -o zyos cmd/connect/main.go

FROM ubuntu:xenial as prod
LABEL maintainer="dengbiao"

# 时区纠正
ENV TZ="Asia/Shanghai"
RUN rm -f /etc/localtime \
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo $TZ > /etc/timezone

# 在build阶段复制可执行的go二进制文件app
COPY --from=build /go/release/zyos ./zyos

# 启动服务
# 优化内核参数
#RUN sysctl -w net.ipv4.tcp_fin_timeout=15
#RUN sysctl -w net.ipv4.tcp_tw_recycle=1
#RUN sysctl -w net.ipv4.ip_local_port_range=1024 65535
#RUN sysctl -p
#CMD ["bash","-c","sysctl -w net.ipv4.tcp_timestamps=1 && sysctl -w net.ipv4.tcp_tw_reuse=1 && sysctl -w net.ipv4.tcp_tw_timeout=10 && sysctl -w net.ipv4.tcp_fin_timeout=10 && sysctl -w net.ipv4.ip_local_port_range='1024 65535' && ./zyos"]
CMD ["./zyos"]

+ 37
- 0
Dockerfile_logic View File

@@ -0,0 +1,37 @@
# 多重构建,减少镜像大小
# 构建:使用golang:1.18版本
FROM registry.cn-shenzhen.aliyuncs.com/fnuoos-prd/golang:1.18.4 as build

# 容器环境变量添加,会覆盖默认的变量值
ENV GO111MODULE=on
ENV GOPROXY=https://goproxy.cn,direct
ENV TZ="Asia/Shanghai"
# 设置工作区
WORKDIR /go/release

# 把全部文件添加到/go/release目录
ADD . .

# 编译:把main.go编译成可执行的二进制文件,命名为zyos
RUN GOOS=linux CGO_ENABLED=0 GOARCH=amd64 go build -o zyos cmd/logic/main.go

FROM ubuntu:xenial as prod
LABEL maintainer="dengbiao"

# 时区纠正
ENV TZ="Asia/Shanghai"
RUN rm -f /etc/localtime \
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo $TZ > /etc/timezone

# 在build阶段复制可执行的go二进制文件app
COPY --from=build /go/release/zyos ./zyos

# 启动服务
# 优化内核参数
#RUN sysctl -w net.ipv4.tcp_fin_timeout=15
#RUN sysctl -w net.ipv4.tcp_tw_recycle=1
#RUN sysctl -w net.ipv4.ip_local_port_range=1024 65535
#RUN sysctl -p
#CMD ["bash","-c","sysctl -w net.ipv4.tcp_timestamps=1 && sysctl -w net.ipv4.tcp_tw_reuse=1 && sysctl -w net.ipv4.tcp_tw_timeout=10 && sysctl -w net.ipv4.tcp_fin_timeout=10 && sysctl -w net.ipv4.ip_local_port_range='1024 65535' && ./zyos"]
CMD ["./zyos"]

+ 21
- 0
LICENSE View File

@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2020 Alber

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 100
- 0
README.md View File

@@ -0,0 +1,100 @@
### 简要介绍
gim是一个即时通讯服务器,代码全部使用golang完成。主要特性
1.支持tcp,websocket接入
2.离线消息同步
3.单用户多设备同时在线
4.单聊,群聊,以及房间聊天场景
5.支持服务水平扩展
6.使用领域驱动设计
gim可以作为以业务服务器的一个组件,为现有业务服务器提供im的能力,业务服务器
只需要实现business.int.proto协议中定义的GRPC接口,为gim服务提供基本的用户功能即可
### 使用技术:
数据库:MySQL+Redis
通讯框架:GRPC
长连接通讯协议:Protocol Buffers
日志框架:Zap
ORM框架:GORM
### 安装部署
1.首先安装MySQL,Redis
2.创建数据库gim,执行sql/create_table.sql,完成初始化表的创建(数据库包含提供测试的一些初始数据)
3.修改config下配置文件,使之和你本地配置一致,如果没有配置gim_env环境变量,默认会加载config/local_conf.go配置
4.分别切换到cmd的connect,logic,business目录下,执行go run main.go,启动TCP连接层服务器,WebSocket连接层服务器,逻辑层服务器,用户服务器
(注意:connect只能在linux下启动,如果想在其他平台下启动,请安装docker,执行cmd/connect/run.sh)
### 项目目录简介
项目结构遵循 https://github.com/golang-standards/project-layout
```
cmd: 服务启动入口
config: 服务配置
internal: 每个服务私有代码
pkg: 服务共有代码
sql: 项目sql文件
test: 长连接测试脚本
```
### 服务简介
1.connect
维持与客户端的TCP和WebSocket长连接,心跳,以及TCP拆包粘包,消息编解码
2.logic
设备信息,好友信息,群组信息管理,消息转发逻辑
3.business
一个简单的业务服务器服务,可以根据自己的业务需求,进行扩展,但是前提是,你的业务服务器实现了business.int.proto接口
### 客户端接入流程
1.调用LogicExt.RegisterDevice接口,完成设备注册,获取设备ID(device_id),注意,一个设备只需完成一次注册即可,后续如果本地有device_id,就不需要注册了,举个例子,如果是APP第一次安装,就需要调用这个接口,后面即便是换账号登录,也不需要重新注册。
2.调用BusinessExt.SignIn接口,完成账户登录,获取账户登录的token。
3.建立长连接,使用步骤2拿到的token,完成长连接登录。
如果是web端,需要调用建立WebSocket时,如果是APP端,就需要建立TCP长连接。
在完成建立TCP长连接时,第一个包应该是长连接登录包(SignInInput),如果信息无误,客户端就会成功建立长连接。
4.使用长连接发送消息同步包(SyncInput),完成离线消息同步,注意:seq字段是客户端接收到消息的最大同步序列号,如果用户是换设备登录或者第一次登录,seq应该传0。
接下来,用户可以使用LogicExt.SendMessage接口来发送消息,消息接收方可以使用长连接接收到对应的消息。
### 网络模型
TCP的网络层使用linux的epoll实现,相比golang原生,能减少goroutine使用,从而节省系统资源占用
### 单用户多设备支持,离线消息同步
每个用户都会维护一个自增的序列号,当用户A给用户B发送消息是,首先会获取A的最大序列号,设置为这条消息的seq,持久化到用户A的消息列表,
再通过长连接下发到用户A账号登录的所有设备,再获取用户B的最大序列号,设置为这条消息的seq,持久化到用户B的消息列表,再通过长连接下发
到用户B账号登录的所有设备。
假如用户的某个设备不在线,在设备长连接登录时,用本地收到消息的最大序列号,到服务器做消息同步,这样就可以保证离线消息不丢失。
### 读扩散和写扩散
首先解释一下,什么是读扩散,什么是写扩散
#### 读扩散
**简介**:群组成员发送消息时,先建立一个会话,都将这个消息写入这个会话中,同步离线消息时,需要同步这个会话的未同步消息
**优点**:每个消息只需要写入数据库一次就行,减少数据库访问次数,节省数据库空间
**缺点**:一个用户有n个群组,客户端每次同步消息时,要上传n个序列号,服务器要对这n个群组分别做消息同步
#### 写扩散
**简介**:在群组中,每个用户维持一个自己的消息列表,当群组中有人发送消息时,给群组的每个用户的消息列表插入一条消息即可
**优点**:每个用户只需要维护一个序列号和消息列表
**缺点**:一个群组有多少人,就要插入多少条消息,当群组成员很多时,DB的压力会增大
### 消息转发逻辑选型以及特点
#### 群组:
采用写扩散,群组成员信息持久化到数据库保存。支持消息离线同步。
#### 房间:
采用读扩散,会将消息短暂的保存到Redis,长连接登录消息同步不会同步离线消息。
### 核心流程时序图
#### 长连接登录
![登录.png](https://camo.githubusercontent.com/c3bb28e0bfe068f5ba619d571d2c665adc83138d56d1f5cb76c76c98e8d3ca74/68747470733a2f2f75706c6f61642d696d616765732e6a69616e7368752e696f2f75706c6f61645f696d616765732f353736303433392d326535346433633564643061343463312e706e673f696d6167654d6f6772322f6175746f2d6f7269656e742f7374726970253743696d61676556696577322f322f772f31323430)
#### 离线消息同步
![离线消息同步.png](https://camo.githubusercontent.com/19edb5f72f832ef38ba2152f8179f91aaa55eefc3943afd44431f68824e3387b/68747470733a2f2f75706c6f61642d696d616765732e6a69616e7368752e696f2f75706c6f61645f696d616765732f353736303433392d616135313365613064653835316531322e706e673f696d6167654d6f6772322f6175746f2d6f7269656e742f7374726970253743696d61676556696577322f322f772f31323430)
#### 心跳
![心跳.png](https://camo.githubusercontent.com/f8bbad45931b4b6c14d9ac6b4156459372593a8202ee7ac3978d4e54f79818aa/68747470733a2f2f75706c6f61642d696d616765732e6a69616e7368752e696f2f75706c6f61645f696d616765732f353736303433392d323664343931333734646133383433622e706e673f696d6167654d6f6772322f6175746f2d6f7269656e742f7374726970253743696d61676556696577322f322f772f31323430)
#### 消息单发
c1.d1和c1.d2分别表示c1用户的两个设备d1和d2,c2.d3和c2.d4同理
![消息单发.png](https://camo.githubusercontent.com/18705cdbc15e29fdabdaf473f297337ac48d06e5e86662e9f72261d910821ce4/68747470733a2f2f75706c6f61642d696d616765732e6a69616e7368752e696f2f75706c6f61645f696d616765732f353736303433392d333566316139316338643766666661362e706e673f696d6167654d6f6772322f6175746f2d6f7269656e742f7374726970253743696d61676556696577322f322f772f31323430)
#### 群组消息群发
c1,c2.c3表示一个群组中的三个用户
![消息群发.png](https://camo.githubusercontent.com/b1fdc7d86b79d9c2375c7e438f13ff6379381575d5b3873adc87ceb10d642a25/68747470733a2f2f75706c6f61642d696d616765732e6a69616e7368752e696f2f75706c6f61645f696d616765732f353736303433392d343761383763343562383939623366392e706e673f696d6167654d6f6772322f6175746f2d6f7269656e742f7374726970253743696d61676556696577322f322f772f31323430)
#### APP
基于Flutter写了一个简单的客户端
GitHub地址:https://github.com/alberliu/fim
APP下载:https://github.com/alberliu/fim/releases/download/v1.2.0/FIM.apk
APP截图:
![登录.png](https://upload-images.jianshu.io/upload_images/5760439-c8c5e61815b34687.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/310)
![好友.png](https://upload-images.jianshu.io/upload_images/5760439-9ea6a87711f8e749.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/310)
![聊天.png](https://upload-images.jianshu.io/upload_images/5760439-2f1e7da8be247e4b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/310)
![群组.png](https://upload-images.jianshu.io/upload_images/5760439-beb97223497e2ee9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/310)
![我的.png](https://upload-images.jianshu.io/upload_images/5760439-aee324007a1d2eb1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/310)
![消息.png](https://upload-images.jianshu.io/upload_images/5760439-47597c7c5859d515.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/310)
### 联系方式
![my.png](https://upload-images.jianshu.io/upload_images/5760439-484c85f9fbda35d4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/310)
### 赞赏支持
如果觉得项目对你有帮助,请支持一下
![pay.png](https://upload-images.jianshu.io/upload_images/5760439-7aac91bc83c8735f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/310)
### github
https://github.com/alberliu/gim

+ 16
- 0
build_docker.sh View File

@@ -0,0 +1,16 @@
if [[ $? -ne 0 ]]; then
exit 1
fi

server=$1
cd cmd/$server
# 打包可执行文件
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main
pwd
mv main ../../docker/
cd ../../docker/
pwd
# 构建镜像
docker build -t $1 .

kind load docker-image $server --name kind

+ 3
- 0
build_proto.sh View File

@@ -0,0 +1,3 @@
cd pkg/proto
protoc --go_out=plugins=grpc:../../../ *.proto
cd ../../

+ 23
- 0
chart/.helmignore View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

+ 24
- 0
chart/Chart.yaml View File

@@ -0,0 +1,24 @@
apiVersion: v2
name: gim
description: A Helm chart for Kubernetes

# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application

# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 1.0.0

# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.0"

+ 11
- 0
chart/templates/configmap/configmap-prd.yaml View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: config
data:
# 类属性键;每一个键都映射到一个简单的值,仅仅支持键值对,不支持嵌套
mysql: "root:ZHIoscnfnuo123@@tcp(zhios123.rwlb.rds.aliyuncs.com:3306)/gim?charset=utf8&parseTime=true"
redisIP: "116.62.62.35:6379"
redisPassword: "dengsanhu"
pushRoomSubscribeNum: "100"
pushAllSubscribeNum: "100"

+ 11
- 0
chart/templates/configmap/configmap.yaml View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: config
data:
# 类属性键;每一个键都映射到一个简单的值,仅仅支持键值对,不支持嵌套
mysql: "root:Fnuo123com@@tcp(119.23.182.117:3306)/gim?charset=utf8&parseTime=true"
redisIP: "120.24.28.6:32572"
redisPassword: ""
pushRoomSubscribeNum: "100"
pushAllSubscribeNum: "100"

+ 44
- 0
chart/templates/new_role/cluster_role.yaml View File

@@ -0,0 +1,44 @@
# 为pod中的服务赋予发现服务和读取配置的权限
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pod-role
namespace: gim
rules:
- apiGroups:
- ""
resources:
- pods
- pods/status
- services
- services/status
- endpoints
- endpoints/status
- configmaps
- configmaps/status
verbs:
- get
- list
- watch
- apiGroups:
- "discovery.k8s.io"
resources:
- endpointslices
- endpointslices/status
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: argo-namespaces-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: pod-role
subjects:
- kind: ServiceAccount
name: default
namespace: gim

+ 35
- 0
chart/templates/role/cluster_role.yaml View File

@@ -0,0 +1,35 @@
# 为pod中的服务赋予发现服务和读取配置的权限
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pod-role
namespace: gim
rules:
- apiGroups:
- ""
resources:
- pods
- pods/status
- services
- services/status
- endpoints
- endpoints/status
- configmaps
- configmaps/status
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: argo-namespaces-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: pod-role
subjects:
- kind: ServiceAccount
name: default
namespace: gim

+ 52
- 0
chart/templates/server/business.yaml View File

@@ -0,0 +1,52 @@
# deployment 配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: business-deployment
namespace: gim
labels:
app: business
spec:
replicas: 1
selector:
matchLabels:
app: business
template:
metadata:
labels:
app: business
spec:
containers:
- name: business
image: 'registry.cn-shenzhen.aliyuncs.com/fnuoos-prd/zyos-gim-business:20221128-01'
imagePullPolicy: Always
ports:
- containerPort: 8000
volumeMounts: # 映射文件为宿主机文件
- mountPath: /log/
name: log
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
volumes:
- name: log
hostPath:
path: /log/
---
# service 配置
apiVersion: v1
kind: Service
metadata:
name: business
namespace: gim
labels:
app: business # 只有设置label,才能被服务发现找到
spec:
selector:
app: business
ports:
- protocol: TCP
port: 8000
targetPort: 8000

+ 63
- 0
chart/templates/server/connect.yaml View File

@@ -0,0 +1,63 @@
# deployment 配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: connect-deployment
namespace: gim
labels:
app: connect
spec:
replicas: 1
selector:
matchLabels:
app: connect
template:
metadata:
labels:
app: connect
spec:
containers:
- name: connect
image: 'registry.cn-shenzhen.aliyuncs.com/fnuoos-prd/zyos-gim-connect:20221128-01'
imagePullPolicy: Always
ports:
- containerPort: 8000
- containerPort: 8001
- containerPort: 8002
volumeMounts: # 映射文件为宿主机文件
- mountPath: /log/
name: log
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
volumes:
- name: log
hostPath:
path: /log/
---
# service 配置
apiVersion: v1
kind: Service
metadata:
name: connect
namespace: gim
labels:
app: connect # 只有设置label,才能被服务发现找到
spec:
selector:
app: connect
ports:
- name: rpclisten
protocol: TCP
port: 8000
targetPort: 8000
- name: tcplisten
protocol: TCP
port: 8001
targetPort: 8001
- name: wslisten
protocol: TCP
port: 8002
targetPort: 8002

+ 52
- 0
chart/templates/server/logic.yaml View File

@@ -0,0 +1,52 @@
# deployment 配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: logic-deployment
namespace: gim
labels:
app: logic
spec:
replicas: 1
selector:
matchLabels:
app: logic
template:
metadata:
labels:
app: logic
spec:
containers:
- name: logic
image: 'registry.cn-shenzhen.aliyuncs.com/fnuoos-prd/zyos-gim-logic:20221128-01'
imagePullPolicy: Always # 在kind中需要指定,不然会强制到远程拉取镜像,导致部署失败
ports:
- containerPort: 8001
volumeMounts: # 映射文件为宿主机文件
- mountPath: /log/
name: log
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
volumes:
- name: log
hostPath:
path: /log/
---
# service 配置
apiVersion: v1
kind: Service
metadata:
name: logic
namespace: gim
labels:
app: logic # 只有设置label,才能被服务发现找到
spec:
selector:
app: logic
ports:
- protocol: TCP
port: 8000
targetPort: 8000

+ 13
- 0
chart/values.yaml View File

@@ -0,0 +1,13 @@
server:
connect:
name: connect
image: connect
replicas: 1
logic:
name: logic
image: logic
replicas: 1
business:
name: logic
image: logic
replicas: 1

+ 53
- 0
cmd/business/main.go View File

@@ -0,0 +1,53 @@
package main

import (
"gim/config"
"gim/internal/business/api"
"gim/pkg/db"
"gim/pkg/interceptor"
"gim/pkg/logger"
"gim/pkg/pb"
"gim/pkg/urlwhitelist"
"net"
"net/http"
_ "net/http/pprof"
"os"
"os/signal"
"syscall"

"go.uber.org/zap"
"google.golang.org/grpc"
)

func main() {
config.Init()
db.Init()

server := grpc.NewServer(grpc.UnaryInterceptor(interceptor.NewInterceptor("business_interceptor", urlwhitelist.Business)))
// 监听服务关闭信号,服务平滑重启
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGTERM)
s := <-c
logger.Logger.Info("server stop", zap.Any("signal", s))
server.GracefulStop()
}()

//runtime.SetBlockProfileRate(1)
go func() {
http.ListenAndServe(":10001", nil)
}()

pb.RegisterBusinessIntServer(server, &api.BusinessIntServer{})
pb.RegisterBusinessExtServer(server, &api.BusinessExtServer{})
listen, err := net.Listen("tcp", config.RPCListenAddr)
if err != nil {
panic(err)
}

logger.Logger.Info("rpc服务已经开启")
err = server.Serve(listen)
if err != nil {
logger.Logger.Error("serve error", zap.Error(err))
}
}

+ 63
- 0
cmd/connect/main.go View File

@@ -0,0 +1,63 @@
package main

import (
"context"
"gim/config"
"gim/internal/connect"
"gim/pkg/db"
"gim/pkg/interceptor"
"gim/pkg/logger"
"gim/pkg/pb"
"gim/pkg/rpc"
"google.golang.org/grpc"
"net"
"os"
"os/signal"
"syscall"

"go.uber.org/zap"
)

func main() {
config.Init()
db.Init()

// 启动TCP长链接服务器
go func() {
connect.StartTCPServer(config.TCPListenAddr)
}()

// 启动WebSocket长链接服务器
go func() {
connect.StartWSServer(config.WSListenAddr)
}()

// 启动服务订阅
connect.StartSubscribe()

server := grpc.NewServer(grpc.UnaryInterceptor(interceptor.NewInterceptor("connect_interceptor", nil)))

// 监听服务关闭信号,服务平滑重启
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGTERM)
s := <-c
logger.Logger.Info("server stop start", zap.Any("signal", s))
_, _ = rpc.GetLogicIntClient().ServerStop(context.TODO(), &pb.ServerStopReq{ConnAddr: config.LocalAddr})
logger.Logger.Info("server stop end")

server.GracefulStop()
}()

pb.RegisterConnectIntServer(server, &connect.ConnIntServer{})
listener, err := net.Listen("tcp", config.RPCListenAddr)
if err != nil {
panic(err)
}

logger.Logger.Info("rpc服务已经开启")
err = server.Serve(listener)
if err != nil {
logger.Logger.Error("serve error", zap.Error(err))
}
}

+ 3
- 0
cmd/connect/run.sh View File

@@ -0,0 +1,3 @@
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
echo "打包完成"
docker run -v $(pwd)/:/app -p 8080:8080 -p 8081:8081 -p 50100:50100 alpine .//app/main

+ 57
- 0
cmd/file/main.go View File

@@ -0,0 +1,57 @@
package main

import (
"gim/pkg/logger"
"gim/pkg/util"
"net/http"
"strconv"
"strings"
"time"

"go.uber.org/zap"

"github.com/gin-gonic/gin"
)

const baseUrl = "http://111.229.238.28:8085/file/"

type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}

func main() {
router := gin.Default()
router.Static("/file", "/root/file")

// Set a lower memory limit for multipart forms (default is 32 MiB)
router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// single file
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusOK, Response{Code: 1001, Message: err.Error()})
return
}

filenames := strings.Split(file.Filename, ".")
name := strconv.FormatInt(time.Now().UnixNano(), 10) + "-" + util.RandString(30) + "." + filenames[len(filenames)-1]
filePath := "/root/file/" + name
err = c.SaveUploadedFile(file, filePath)
if err != nil {
c.JSON(http.StatusOK, Response{Code: 1001, Message: err.Error()})
return
}

c.JSON(http.StatusOK, Response{
Code: 0,
Message: "success",
Data: map[string]string{"url": baseUrl + name},
})
})
err := router.Run(":8085")
if err != nil {
logger.Logger.Error("Run error", zap.Error(err))
}
}

+ 54
- 0
cmd/logic/main.go View File

@@ -0,0 +1,54 @@
package main

import (
"gim/config"
"gim/internal/logic/api"
"gim/internal/logic/app"
"gim/internal/logic/proxy"
"gim/pkg/db"
"gim/pkg/interceptor"
"gim/pkg/logger"
"gim/pkg/pb"
"gim/pkg/urlwhitelist"
"net"
"os"
"os/signal"
"syscall"

"go.uber.org/zap"
"google.golang.org/grpc"
)

func init() {
proxy.MessageProxy = app.MessageApp
proxy.DeviceProxy = app.DeviceApp
}

func main() {
config.Init()
db.Init()

server := grpc.NewServer(grpc.UnaryInterceptor(interceptor.NewInterceptor("logic_interceptor", urlwhitelist.Logic)))

// 监听服务关闭信号,服务平滑重启
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGTERM)
s := <-c
logger.Logger.Info("server stop", zap.Any("signal", s))
server.GracefulStop()
}()

pb.RegisterLogicIntServer(server, &api.LogicIntServer{})
pb.RegisterLogicExtServer(server, &api.LogicExtServer{})
listen, err := net.Listen("tcp", config.RPCListenAddr)
if err != nil {
panic(err)
}

logger.Logger.Info("rpc服务已经开启")
err = server.Serve(listen)
if err != nil {
logger.Logger.Error("serve error", zap.Error(err))
}
}

+ 56
- 0
config/config.go View File

@@ -0,0 +1,56 @@
package config

import (
"gim/pkg/logger"
"go.uber.org/zap"
"os"
)

const (
//RPCListenAddr = ":8000"
//TCPListenAddr = ":8080"
//WSListenAddr = ":8001"
RPCListenAddr = ":8000"
TCPListenAddr = ":8001"
WSListenAddr = ":8002"
)

var (
NameSpace string = "gim"
MySQL string = "root:Fnuo123com@@tcp(119.23.182.117:3306)/gim?charset=utf8&parseTime=true&loc=Local"
//MySQL string = "canal:canal@tcp(zhios123.rwlb.rds.aliyuncs.com:3306)/gim?charset=utf8&parseTime=true&loc=Local"
RedisIP string = "120.24.28.6:32572"
RedisPassword string = ""

LocalAddr string
PushRoomSubscribeNum int
PushAllSubscribeNum int
)

func Init() {
//k8sClient, err := k8sutil.GetK8sClient()
//if err != nil {
// panic(err)
//}
//configmap, err := k8sClient.CoreV1().ConfigMaps(NameSpace).Get(context.TODO(), "config", metav1.GetOptions{})
//if err != nil {
// panic(err)
//}
//
//MySQL = configmap.Data["mysql"]
//RedisIP = configmap.Data["redisIP"]
//RedisPassword = configmap.Data["redisPassword"]
//PushRoomSubscribeNum, _ = strconv.Atoi(configmap.Data["pushRoomSubscribeNum"])
//if PushRoomSubscribeNum == 0 {
// panic("PushRoomSubscribeNum == 0")
//}
//PushAllSubscribeNum, _ = strconv.Atoi(configmap.Data["pushAllSubscribeNum"])
//if PushRoomSubscribeNum == 0 {
// panic("PushAllSubscribeNum == 0")
//}

LocalAddr = os.Getenv("POD_IP") + RPCListenAddr

logger.Level = zap.DebugLevel
logger.Target = logger.Console
}

+ 3
- 0
docker/Dockerfile View File

@@ -0,0 +1,3 @@
FROM scratch as final
COPY main .
CMD ["/main"]

+ 2
- 0
docs/plan.md View File

@@ -0,0 +1,2 @@
pb编译命令
protoc --go_out=plugins=grpc:../../../ *.proto

+ 17
- 0
docs/流程图/心跳.puml View File

@@ -0,0 +1,17 @@
@startuml
participant client
participant connect
participant logic

client -> connect: 心跳包请求
connect --> client: 心跳包响应

client -> client: 等待一段时间

client -> connect: 心跳包请求
connect --> client: 心跳包响应


connect -> connect: 连续n次没有收到,释放连接
connect -> logic: 通知设备下线
@enduml

+ 31
- 0
docs/流程图/消息单发.puml View File

@@ -0,0 +1,31 @@
@startuml
participant c1.d1
participant c1.d2
participant c2.d3
participant c2.d4
participant connect
participant logic

c1.d1 -> logic: c1给c2用户发送消息
logic --> c1.d1 : 返回消息发送成功

logic -> logic: 获取c1用户下一个消息序列号
logic -> logic: 将消息持久化到c1用户的消息列表
logic -> logic: 查询c1用户其他在线设备
logic --> connect: 给设备d2发送消息
connect --> c1.d2: 给设备d2发送消息
c1.d2 ->connect : 消息ack
connect -> logic: 消息ack

logic -> logic: 获取c2用户下一个消息序列号
logic -> logic: 将消息持久化到c2用户的消息列表
logic -> logic: 查询c2用户所有在线设备
logic -> connect: 给设备d3发送消息
connect -> c2.d3: 给设备d3发送消息
c2.d3 ->connect : 消息ack
connect -> logic: 消息ack
logic -> connect: 给设备d4发送消息
connect -> c2.d4: 给设备d4发送消息
c2.d4 ->connect : 消息ack
connect -> logic: 消息ack
@enduml

+ 31
- 0
docs/流程图/消息群发.puml View File

@@ -0,0 +1,31 @@
@startuml
participant c1
participant c2
participant c3

participant connect
participant logic

c1 -> logic: 发送消息到群组
logic --> c1: 消息发送成功

logic -> logic: 查询群组所有成员

logic -> logic: 将消息持久化到c1的消息列表
logic -> connect: 发送消息给c1的其他在线设备
connect -> c1: 发送消息给c1的其他在线设备
c1 -> connect: 消息ack
connect -> logic: 消息ack

logic -> logic: 将消息持久化到c2的消息列表
logic -> connect: 发送消息给c2的其他在线设备
connect -> c2: 发送消息给c2的其他在线设备
c2 -> connect: 消息ack
connect -> logic: 消息ack

logic -> logic: 将消息持久化到c3的消息列表
logic -> connect: 发送消息给c3的其他在线设备
connect -> c3: 发送消息给c3的其他在线设备
c3 -> connect: 消息ack
connect -> logic: 消息ack
@enduml

+ 15
- 0
docs/流程图/登录.puml View File

@@ -0,0 +1,15 @@
@startuml
participant client
participant connect
participant logic

client -> connect: 设备鉴权
connect -> logic: 设备鉴权

logic -> logic: 检查token是否合法

logic --> connect: 返回校验结果
connect --> client: 返回校验结果

connect -> connect: 如果鉴权失败,断开连接
@enduml

+ 16
- 0
docs/流程图/离线消息同步.puml View File

@@ -0,0 +1,16 @@
@startuml
participant client
participant connect
participant logic

client -> connect: 离线消息同步
connect -> logic: 离线消息同步

logic -> logic: 如果seq!=0,同步序列号大于seq的消息,否则,同步用户未收到的消息

logic --> connect: 返回离线消息
connect --> client: 返回离线消息

client -> connect: 消息ack
connect -> logic: 消息ack
@enduml

+ 138
- 0
docs/错误处理.md View File

@@ -0,0 +1,138 @@
### 错误处理,链路追踪,日志打印
系统中的错误一般可以归类为两种,一种是业务定义的错误,一种就是未知的错误,在业务正式上线的时候,业务定义的错误的属于正常业务逻辑,不需要打印出来,
但是未知的错误,我们就需要打印出来,我们不仅要知道是什么错误,还要知道错误的调用堆栈,所以这里我对GRPC的错误进行了一些封装,使之包含调用堆栈。
```go
func WrapError(err error) error {
if err == nil {
return nil
}

s := &spb.Status{
Code: int32(codes.Unknown),
Message: err.Error(),
Details: []*any.Any{
{
TypeUrl: TypeUrlStack,
Value: util.Str2bytes(stack()),
},
},
}
return status.FromProto(s).Err()
}
// Stack 获取堆栈信息
func stack() string {
var pc = make([]uintptr, 20)
n := runtime.Callers(3, pc)

var build strings.Builder
for i := 0; i < n; i++ {
f := runtime.FuncForPC(pc[i] - 1)
file, line := f.FileLine(pc[i] - 1)
n := strings.Index(file, name)
if n != -1 {
s := fmt.Sprintf(" %s:%d \n", file[n:], line)
build.WriteString(s)
}
}
return build.String()
}
```
这样,不仅可以拿到错误的堆栈,错误的堆栈也可以跨RPC传输,但是,但是这样你只能拿到当前服务的堆栈,却不能拿到调用方的堆栈,就比如说,A服务调用
B服务,当B服务发生错误时,在A服务通过日志打印错误的时候,我们只打印了B服务的调用堆栈,怎样可以把A服务的堆栈打印出来。我们在A服务调用的地方也获取
一次堆栈。
```go
func WrapRPCError(err error) error {
if err == nil {
return nil
}
e, _ := status.FromError(err)
s := &spb.Status{
Code: int32(e.Code()),
Message: e.Message(),
Details: []*any.Any{
{
TypeUrl: TypeUrlStack,
Value: util.Str2bytes(GetErrorStack(e) + " --grpc-- \n" + stack()),
},
},
}
return status.FromProto(s).Err()
}

func interceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
err := invoker(ctx, method, req, reply, cc, opts...)
return gerrors.WrapRPCError(err)
}

var LogicIntClient pb.LogicIntClient

func InitLogicIntClient(addr string) {
conn, err := grpc.DialContext(context.TODO(), addr, grpc.WithInsecure(), grpc.WithUnaryInterceptor(interceptor))
if err != nil {
logger.Sugar.Error(err)
panic(err)
}

LogicIntClient = pb.NewLogicIntClient(conn)
}
```
像这样,就可以获取完整一次调用堆栈。
错误打印也没有必要在函数返回错误的时候,每次都去打印。因为错误已经包含了堆栈信息
```go
// 错误的方式
if err != nil {
logger.Sugar.Error(err)
return err
}

// 正确的方式
if err != nil {
return err
}
```
然后,我们在上层统一打印就可以
```go
func startServer {
extListen, err := net.Listen("tcp", conf.LogicConf.ClientRPCExtListenAddr)
if err != nil {
panic(err)
}
extServer := grpc.NewServer(grpc.UnaryInterceptor(LogicClientExtInterceptor))
pb.RegisterLogicClientExtServer(extServer, &LogicClientExtServer{})
err = extServer.Serve(extListen)
if err != nil {
panic(err)
}
}

func LogicClientExtInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
defer logPanic("logic_client_ext_interceptor", ctx, req, info, &err)

resp, err = handler(ctx, req)
logger.Logger.Debug("logic_client_ext_interceptor", zap.Any("info", info), zap.Any("ctx", ctx), zap.Any("req", req),
zap.Any("resp", resp), zap.Error(err))

s, _ := status.FromError(err)
if s.Code() != 0 && s.Code() < 1000 {
md, _ := metadata.FromIncomingContext(ctx)
logger.Logger.Error("logic_client_ext_interceptor", zap.String("method", info.FullMethod), zap.Any("md", md), zap.Any("req", req),
zap.Any("resp", resp), zap.Error(err), zap.String("stack", gerrors.GetErrorStack(s)))
}
return
}
```
这样做的前提就是,在业务代码中透传context,golang不像其他语言,可以在线程本地保存变量,像Java的ThreadLocal,所以只能通过函数参数的形式进行传递,im中,service层函数的第一个参数
都是context,但是dao层和cache层就不需要了,不然,显得代码臃肿。
最后可以在客户端的每次请求添加一个随机的request_id,这样客户端到服务的每次请求都可以串起来了。
```go
func getCtx() context.Context {
token, _ := util.GetToken(1, 2, 3, time.Now().Add(1*time.Hour).Unix(), util.PublicKey)
return metadata.NewOutgoingContext(context.TODO(), metadata.Pairs(
"app_id", "1",
"user_id", "2",
"device_id", "3",
"token", token,
"request_id", strconv.FormatInt(time.Now().UnixNano(), 10)))
}
```

+ 81
- 0
go.mod View File

@@ -0,0 +1,81 @@
module gim

go 1.18

require (
code.fnuoos.com/go_rely_warehouse/zyos_go_jg_push.git v1.0.5
github.com/alberliu/gn v1.10.0
github.com/gin-gonic/gin v1.7.7
github.com/go-redis/redis v6.15.9+incompatible
github.com/go-sql-driver/mysql v1.6.0
github.com/golang/protobuf v1.5.2
github.com/gorilla/websocket v1.4.2
github.com/jinzhu/gorm v1.9.16
github.com/json-iterator/go v1.1.12
go.uber.org/zap v1.21.0
google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55
google.golang.org/grpc v1.50.1
google.golang.org/protobuf v1.28.1
gopkg.in/natefinch/lumberjack.v2 v2.0.0
k8s.io/api v0.25.1
k8s.io/apimachinery v0.25.1
k8s.io/client-go v0.25.1
)

require (
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/gookit/color v1.3.6 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/syyongx/php2go v0.9.7 // indirect
github.com/tidwall/gjson v1.14.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/term v0.1.0 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.70.1 // indirect
k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
)

require (
github.com/google/go-cmp v0.5.9 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/qiniu/api.v7/v7 v7.8.2
golang.org/x/net v0.1.0 // indirect
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
)

+ 347
- 0
go.sum View File

@@ -0,0 +1,347 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
code.fnuoos.com/go_rely_warehouse/zyos_go_jg_push.git v1.0.5 h1:+MIZPOEhxTiu79CHwz5LuK5tSv40XxAHwftOUlyr+sY=
code.fnuoos.com/go_rely_warehouse/zyos_go_jg_push.git v1.0.5/go.mod h1:IEw6A61Kp2ctoeKIQMFVM/cFq9PUDAzJMb5lVXiEY2Y=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alberliu/gn v1.10.0 h1:BQcfgp1c8wc+AXvutImeJ5RAr7Lh/tc23vELJ3dgh10=
github.com/alberliu/gn v1.10.0/go.mod h1:Rm7O35H784ZsqxJr/3VcNgyHHfI7jf7GFTQ+o/fXAsg=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw=
github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM=
github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng=
github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=
github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gookit/color v1.3.6 h1:Rgbazd4JO5AgSTVGS3o0nvaSdwdrS8bzvIXwtK6OiMk=
github.com/gookit/color v1.3.6/go.mod h1:R3ogXq2B9rTbXoSHJ1HyUVAZ3poOJHpd9nQmyGZsfvQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.1.6 h1:Fx2POJZfKRQcM1pH49qSZiYeu319wji004qX+GDovrU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/qiniu/api.v7/v7 v7.8.2 h1:f08kI0MmsJNzK4sUS8bG3HDH67ktwd/ji23Gkiy2ra4=
github.com/qiniu/api.v7/v7 v7.8.2/go.mod h1:FPsIqxh1Ym3X01sANE5ZwXfLZSWoCUp5+jNI8cLo3l0=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syyongx/php2go v0.9.7 h1:boZtLbm2xYbW49mX9M7Vq2zkVhBhv3fCqs2T16d2bGA=
github.com/syyongx/php2go v0.9.7/go.mod h1:meN2eIhhUoxOd2nMxbpe8g6cFPXI5O9/UAAuz7oDdzw=
github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk=
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55 h1:U1u4KB2kx6KR/aJDjQ97hZ15wQs8ZPvDcGcRynBhkvg=
google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55/go.mod h1:45EK0dUbEZ2NHjCeAd2LXmyjAgGUGrpGROgjhC3ADck=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.25.1 h1:yL7du50yc93k17nH/Xe9jujAYrcDkI/i5DL1jPz4E3M=
k8s.io/api v0.25.1/go.mod h1:hh4itDvrWSJsmeUc28rIFNri8MatNAAxJjKcQmhX6TU=
k8s.io/apimachinery v0.25.1 h1:t0XrnmCEHVgJlR2arwO8Awp9ylluDic706WePaYCBTI=
k8s.io/apimachinery v0.25.1/go.mod h1:hqqA1X0bsgsxI6dXsJ4HnNTBOmJNxyPp8dw3u2fSHwA=
k8s.io/client-go v0.25.1 h1:uFj4AJKtE1/ckcSKz8IhgAuZTdRXZDKev8g387ndD58=
k8s.io/client-go v0.25.1/go.mod h1:rdFWTLV/uj2C74zGbQzOsmXPUtMAjSf7ajil4iJUNKo=
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ=
k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 h1:MQ8BAZPZlWk3S9K4a9NCkIFQtZShWqoha7snGixVgEA=
k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1/go.mod h1:C/N6wCaBHeBHkHUesQOQy2/MZqGgMAFPqGsGQLdbZBU=
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4=
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k=
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=

+ 177
- 0
internal/business/api/business_ext.go View File

@@ -0,0 +1,177 @@
package api

import (
"context"
"encoding/json"
"errors"
"gim/internal/business/app"
comm "gim/internal/business/comm"
"gim/internal/business/comm/utils"
"gim/internal/business/domain/user/repo"
friend2 "gim/internal/logic/domain/friend"
"gim/internal/logic/domain/group/model"
repo2 "gim/internal/logic/domain/group/repo"
"gim/internal/logic/domain/message/service"
"gim/pkg/grpclib"
"gim/pkg/pb"
"strconv"
"time"
)

type BusinessExtServer struct{}

func (s *BusinessExtServer) ComplainGroup(ctx context.Context, req *pb.ComplainGroupReq) (*pb.Empty, error) {
now := time.Now()
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}
imageList, _ := json.Marshal(req.ImageList)
groupComplain := model.GroupComplain{
GroupId: req.GroupId,
UserId: userId,
ComplainType: int(req.ComplainType),
Text: req.Text,
ImageList: string(imageList),
Status: 0,
CreateTime: now,
UpdateTime: now,
}

return new(pb.Empty), repo2.GroupComplainDao.Save(&groupComplain)
}

func (s *BusinessExtServer) IsFriends(ctx context.Context, req *pb.IsFriendsReq) (*pb.IsFriendsResp, error) {
masterId, err := grpclib.GetCtxMasterId(ctx)
if err != nil {
return nil, err
}
user, err := repo.UserRepo.GetByPhoneNumber(req.UserPhone, utils.StrToInt64(masterId))
if err != nil {
return nil, err
}
if user == nil {
return nil, errors.New("未查询到自身用户信息")
}

userFriend, err := repo.UserRepo.GetByPhoneNumber(req.FriendPhone, utils.StrToInt64(masterId))
if err != nil {
return nil, err
}
if userFriend == nil {
return nil, errors.New("未查询到好友用户信息")
}
friend, err := friend2.FriendRepo.Get(user.Id, userFriend.Id)
if err != nil {
return nil, err
}
var isFriend = int64(1)
if friend == nil || friend.Status == friend2.FriendStatusApply {
isFriend = 2
}

return &pb.IsFriendsResp{
User: &pb.User{
UserId: userFriend.Id,
Nickname: userFriend.Nickname,
Sex: userFriend.Sex,
AvatarUrl: userFriend.AvatarUrl,
MasterId: userFriend.MasterId,
IsAutoAddedFriends: int64(userFriend.IsAutoAddedFriends),
},
IsFriend: isFriend,
}, err
}

func (s *BusinessExtServer) EmoticonList(ctx context.Context, empty *pb.Empty) (*pb.EmoticonListResp, error) {
masterId, err := grpclib.GetCtxMasterId(ctx)
if err != nil {
return nil, err
}
list, err := service.EmoticonList(masterId)
if err != nil {
return nil, err
}

return &pb.EmoticonListResp{Emoticons: list}, err
}

func (s *BusinessExtServer) SignIn(ctx context.Context, req *pb.SignInReq) (*pb.SignInResp, error) {
utils.FilePutContents("sign_in", utils.SerializeStr(map[string]interface{}{
"args": req,
}))
isNew, userId, token, masterId, err := app.AuthApp.SignIn(ctx, req.PhoneNumber, req.Code, req.MasterId, req.DeviceId, req.PushAlia, req.Nickname, req.AvatarUrl)
if err != nil {
return nil, err
}
return &pb.SignInResp{
IsNew: isNew,
UserId: userId,
Token: token,
MasterId: masterId,
}, nil
}

func (s *BusinessExtServer) GetUser(ctx context.Context, req *pb.GetUserReq) (*pb.GetUserResp, error) {
var user *pb.User
var err error
if req.Phone != "" {
masterId, err := grpclib.GetCtxMasterId(ctx)
if err != nil {
return nil, err
}
useInfo, err := repo.UserRepo.GetByPhoneNumber(req.Phone, utils.StrToInt64(masterId))
user = &pb.User{
UserId: useInfo.Id,
Nickname: useInfo.Nickname,
Sex: useInfo.Sex,
AvatarUrl: useInfo.AvatarUrl,
MasterId: useInfo.MasterId,
IsAutoAddedFriends: int64(useInfo.IsAutoAddedFriends),
PhoneNumber: useInfo.PhoneNumber,
}
} else {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}
if req.UserId != 0 {
userId = req.UserId
}
user, err = app.UserApp.Get(ctx, userId)
}

return &pb.GetUserResp{User: user}, err
}

func (s *BusinessExtServer) UpdateUser(ctx context.Context, req *pb.UpdateUserReq) (*pb.Empty, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

return new(pb.Empty), app.UserApp.Update(ctx, userId, req)
}

func (s *BusinessExtServer) SearchUser(ctx context.Context, req *pb.SearchUserReq) (*pb.SearchUserResp, error) {
users, err := app.UserApp.Search(ctx, req.Key, req.MasterId)
return &pb.SearchUserResp{Users: users}, err
}

func (s *BusinessExtServer) CloudUploadFile(ctx context.Context, req *pb.CloudUploadFileReq) (*pb.CloudUploadFileResp, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}
upload, err := comm.CloudUpload.FileReqUpload(ctx, strconv.FormatInt(userId, 10), req.Dir, req.FileName, utils.StrToInt64(req.FileSize))
data := utils.SerializeStr(upload)
var result map[string]string
utils.Unserialize([]byte(data), &result)

return &pb.CloudUploadFileResp{
Method: result["method"],
Host: result["host"],
Key: result["key"],
Token: result["token"],
}, err
}

+ 59
- 0
internal/business/api/business_ext_test.go View File

@@ -0,0 +1,59 @@
package api

import (
"context"
"fmt"
"gim/pkg/pb"
"strconv"
"testing"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)

func getBusinessExtClient() pb.BusinessExtClient {
conn, err := grpc.Dial("im-rpc-business.izhyin.com:8000", grpc.WithInsecure())
if err != nil {
fmt.Println(err)
return nil
}
return pb.NewBusinessExtClient(conn)
}

func getCtx() context.Context {
token := "0"
return metadata.NewOutgoingContext(context.TODO(), metadata.Pairs(
"user_id", "1",
"device_id", "1",
"token", token,
"request_id", strconv.FormatInt(time.Now().UnixNano(), 10)))
}

func TestUserExtServer_SignIn(t *testing.T) {
resp, err := getBusinessExtClient().SignIn(getCtx(), &pb.SignInReq{
PhoneNumber: "18229775311",
DeviceId: 1,
MasterId: 123456,
})
if err != nil {
fmt.Println(err)
}
fmt.Printf("%+v\n", resp)
}

func TestUserExtServer_GetUser(t *testing.T) {
resp, err := getBusinessExtClient().GetUser(getCtx(), &pb.GetUserReq{UserId: 2})
if err != nil {
fmt.Println(err)
}
fmt.Printf("%+v\n", resp)
}

func TestUserExtServer_SearchUser(t *testing.T) {
resp, err := getBusinessExtClient().SearchUser(getCtx(), &pb.SearchUserReq{Key: "18229"})
if err != nil {
fmt.Println(err)
}
fmt.Printf("%+v\n", resp)
}

+ 33
- 0
internal/business/api/business_int.go View File

@@ -0,0 +1,33 @@
package api

import (
"context"
"gim/internal/business/app"
"gim/pkg/pb"
)

type BusinessIntServer struct{}

func (s *BusinessIntServer) MasterAuth(ctx context.Context, req *pb.MasterAuthReq) (*pb.Empty, error) {
//TODO implement me
return &pb.Empty{}, app.AuthApp.AuthMaster(ctx, req.MasterId)
}

func (*BusinessIntServer) Auth(ctx context.Context, req *pb.AuthReq) (*pb.Empty, error) {
return &pb.Empty{}, app.AuthApp.Auth(ctx, req.UserId, req.DeviceId, req.Token)
}

func (*BusinessIntServer) GetUser(ctx context.Context, req *pb.GetUserReq) (*pb.GetUserResp, error) {
user, err := app.UserApp.Get(ctx, req.UserId)
return &pb.GetUserResp{User: user}, err
}

func (*BusinessIntServer) GetUsers(ctx context.Context, req *pb.GetUsersReq) (*pb.GetUsersResp, error) {
var userIds = make([]int64, 0, len(req.UserIds))
for k := range req.UserIds {
userIds = append(userIds, k)
}

users, err := app.UserApp.GetByIds(ctx, userIds)
return &pb.GetUsersResp{Users: users}, err
}

+ 42
- 0
internal/business/api/business_int_test.go View File

@@ -0,0 +1,42 @@
package api

import (
"fmt"
"gim/pkg/pb"
"testing"

"google.golang.org/grpc"
)

func getBusinessIntClient() pb.BusinessIntClient {
//conn, err := grpc.Dial("localhost:50300", grpc.WithInsecure())
conn, err := grpc.Dial("im-rpc-business.izhyin.com:8000", grpc.WithInsecure())
if err != nil {
fmt.Println(err)
return nil
}
return pb.NewBusinessIntClient(conn)
}

func TestUserIntServer_Auth(t *testing.T) {
_, err := getBusinessIntClient().Auth(getCtx(), &pb.AuthReq{
UserId: 2,
DeviceId: 1,
Token: "0",
})
fmt.Println(err)
}

func TestUserIntServer_GetUsers(t *testing.T) {
resp, err := getBusinessIntClient().GetUsers(getCtx(), &pb.GetUsersReq{
UserIds: map[int64]int32{1: 0, 2: 0, 3: 0},
})
if err != nil {
fmt.Println(err)
return
}

for k, v := range resp.Users {
fmt.Printf("%+-5v %+v\n", k, v)
}
}

+ 25
- 0
internal/business/app/auth_app.go View File

@@ -0,0 +1,25 @@
package app

import (
"context"
"gim/internal/business/domain/user/service"
)

type authApp struct{}

var AuthApp = new(authApp)

// SignIn 长连接登录
func (*authApp) SignIn(ctx context.Context, phoneNumber, code string, masterId, deviceId int64, pushAlia, nickname, avatarUrl string) (bool, int64, string, int64, error) {
return service.AuthService.SignIn(ctx, phoneNumber, code, masterId, deviceId, pushAlia, nickname, avatarUrl)
}

// Auth 验证用户是否登录
func (*authApp) Auth(ctx context.Context, userId, deviceId int64, token string) error {
return service.AuthService.Auth(ctx, userId, deviceId, token)
}

// AuthMaster 验证站长权限
func (*authApp) AuthMaster(ctx context.Context, masterId string) error {
return service.AuthService.AuthMaster(ctx, masterId)
}

+ 72
- 0
internal/business/app/user_app.go View File

@@ -0,0 +1,72 @@
package app

import (
"context"
"gim/internal/business/comm/utils"
"gim/internal/business/domain/user/repo"
"gim/pkg/pb"
"time"
)

type userApp struct{}

var UserApp = new(userApp)

func (*userApp) Get(ctx context.Context, userId int64) (*pb.User, error) {
user, err := repo.UserRepo.Get(userId)
return user.ToProto(), err
}

func (*userApp) Update(ctx context.Context, userId int64, req *pb.UpdateUserReq) error {
utils.FilePutContents("update_info", utils.SerializeStr(map[string]interface{}{
"user_id": userId,
"nickname": req.Nickname,
"args": req,
"time": time.Now().Format("2006-01-02 15:04:05"),
}))
u, err := repo.UserRepo.Get(userId)
if err != nil {
return err
}
if u == nil {
return nil
}

u.Nickname = req.Nickname
u.Sex = req.Sex
u.AvatarUrl = req.AvatarUrl
u.Extra = req.Extra
u.UpdateTime = time.Now()

err = repo.UserRepo.Save(u)
if err != nil {
return err
}
return nil
}

func (*userApp) GetByIds(ctx context.Context, userIds []int64) (map[int64]*pb.User, error) {
users, err := repo.UserRepo.GetByIds(userIds)
if err != nil {
return nil, err
}

pbUsers := make(map[int64]*pb.User, len(users))
for i := range users {
pbUsers[users[i].Id] = users[i].ToProto()
}
return pbUsers, nil
}

func (*userApp) Search(ctx context.Context, key string, masterId int64) ([]*pb.User, error) {
users, err := repo.UserRepo.Search(key, masterId)
if err != nil {
return nil, err
}

pbUsers := make([]*pb.User, len(users))
for i, v := range users {
pbUsers[i] = v.ToProto()
}
return pbUsers, nil
}

+ 25
- 0
internal/business/comm/cloud_upload.go View File

@@ -0,0 +1,25 @@
package app

import (
"context"
"fmt"
svc "gim/internal/business/comm/service"
"gim/pkg/grpclib"
)

type cloudUpload struct{}

var CloudUpload = new(cloudUpload)

// FileReqUpload 请求文件上传
func (*cloudUpload) FileReqUpload(ctx context.Context, uid, dirName, fName string, fSize int64) (interface{}, error) {
masterId, _ := grpclib.GetCtxMasterId(ctx)

callbackUrl := svc.SysCfgGet("-1", "cloud_file_upload_call_back_url")
fileBucketHost := svc.SysCfgGet(masterId, "file_bucket_host")
fileBucketScheme := svc.SysCfgGet("-1", "file_bucket_scheme")

callbackUrl = fmt.Sprintf(callbackUrl, masterId, fileBucketHost, fileBucketScheme)

return svc.FileReqUpload(ctx, uid, dirName, fName, callbackUrl, fSize)
}

+ 20
- 0
internal/business/comm/db/db_emoticon.go View File

@@ -0,0 +1,20 @@
package db

import (
"gim/internal/business/comm/db/model"
"gim/pkg/db"
"gim/pkg/gerrors"
)

type dbEmoticon struct{}

var DbEmoticon = new(dbEmoticon)

// EmoticonFind 获取记录
func (*dbEmoticon) EmoticonFind(masterId string) (*[]model.Emoticon, error) {
var list []model.Emoticon
if err := db.DB.Where("`state` = 1 and `master_id` = ?", masterId).Order("sort desc").Find(&list).Error; err != nil {
return nil, gerrors.WrapError(err)
}
return &list, nil
}

+ 119
- 0
internal/business/comm/db/db_sys_cfg.go View File

@@ -0,0 +1,119 @@
package db

import (
"fmt"
"gim/internal/business/comm/db/model"
"gim/internal/business/comm/md"
"gim/internal/business/comm/utils"
"gim/internal/business/comm/utils/logx"
"gim/pkg/db"
"gim/pkg/gerrors"
)

type dbSysCfg struct{}

var DbSysCfg = new(dbSysCfg)

// SysCfgGetAll 获取系统配置
func (*dbSysCfg) SysCfgGetAll(masterId int64) (*[]model.SysCfg, error) {
var cfgList []model.SysCfg
if err := db.DB.Where("`master_id`=?", masterId).Find(&cfgList).Error; err != nil {
return nil, gerrors.WrapError(err)
}
return &cfgList, nil
}

// SysCfgGetOne 获取一条记录
func (*dbSysCfg) SysCfgGetOne(key string, masterId int64) (*model.SysCfg, error) {
var cfgList model.SysCfg
if err := db.DB.Where("`key` = ? and `master_id` = ?", key, masterId).First(&cfgList).Error; err != nil {
return nil, gerrors.WrapError(err)
}
return &cfgList, nil
}

// SysCfgInsert 插入一条配置信息
func (*dbSysCfg) SysCfgInsert(key, val, memo string, masterId int64) bool {
cfg := model.SysCfg{Key: key, Val: val, Memo: memo, MasterId: masterId}
err := db.DB.Create(&cfg).Error
if err != nil {
logx.Error(err)
return false
}
return true
}

// SysCfgUpdate 修改某条配置信息
func (*dbSysCfg) SysCfgUpdate(key, val, memo string, masterId int64) bool {
cfg := model.SysCfg{Key: key, Val: val, Memo: memo}
err := db.DB.Where("`key` = ? and `master_id` = ?", key, masterId).Update(&cfg).Error
if err != nil {
logx.Error(err)
return false
}
return true
}

// SysCfgGetWithDb 单条记录获取
func (*dbSysCfg) SysCfgGetWithDb(masterId string, HKey string) string {
cacheKey := fmt.Sprintf(md.AppCfgCacheKey, masterId, HKey[0:1])
get, err := db.RedisUtil.HGetString(cacheKey, HKey)
if err != nil || get == "" {
cfg, err := DbSysCfg.SysCfgGetOne(HKey, utils.StrToInt64(masterId))
if err != nil || cfg == nil {
_ = logx.Error(err)
return ""
}

// key是否存在
cacheKeyExist := false
if db.RedisUtil.Exists(cacheKey) {
cacheKeyExist = true
}

// 设置缓存
_, err = db.RedisUtil.HSet(cacheKey, HKey, cfg.Val)
if err != nil {
_ = logx.Error(err)
return ""
}
if !cacheKeyExist { // 如果是首次设置 设置过期时间
_, err := db.RedisUtil.Expire(cacheKey, md.CfgCacheTime)
if err != nil {
_ = logx.Error(err)
return ""
}
}
return cfg.Val
}
return get
}

// SysCfgFindWithDb 多条记录获取
func (*dbSysCfg) SysCfgFindWithDb(masterId string, keys ...string) map[string]string {
res := map[string]string{}
cacheKey := fmt.Sprintf(md.AppCfgCacheKey, masterId)
err := db.RedisUtil.GetJson(cacheKey, &res)
if err != nil || len(res) == 0 {
cfgList, _ := DbSysCfg.SysCfgGetAll(utils.StrToInt64(masterId))
if cfgList == nil {
return nil
}
for _, v := range *cfgList {
res[v.Key] = v.Val
}
db.RedisUtil.SetJson(cacheKey, res, md.CfgCacheTime)
}
if len(keys) == 0 {
return res
}
tmp := map[string]string{}
for _, v := range keys {
if val, ok := res[v]; ok {
tmp[v] = val
} else {
tmp[v] = ""
}
}
return tmp
}

+ 84
- 0
internal/business/comm/db/db_user_push_for_jg.go View File

@@ -0,0 +1,84 @@
package db

import (
"fmt"
"gim/internal/business/comm/db/model"
"gim/internal/business/comm/md"
"gim/internal/business/comm/utils"
"gim/internal/business/comm/utils/logx"
"gim/pkg/db"
"gim/pkg/gerrors"
"github.com/jinzhu/gorm"
)

type dbUserPushForJg struct{}

var DbUserPushForJg = new(dbUserPushForJg)

// UserPushForJgGetOne 获取一条记录
func (*dbUserPushForJg) UserPushForJgGetOne(uid string, masterId int64) (*model.UserPushForJg, error) {
var cfgList model.UserPushForJg
if err := db.DB.Where("`uid` = ? and `master_id` = ?", uid, masterId).First(&cfgList).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, gerrors.WrapError(err)
}
return &cfgList, nil
}

// UserPushForJgInsert 插入一条配置信息
func (*dbUserPushForJg) UserPushForJgInsert(uid, masterId int64, pushAlia string) bool {
cfg := model.UserPushForJg{Uid: uid, PushAlia: pushAlia, MasterId: masterId}
err := db.DB.Create(&cfg).Error
if err != nil {
logx.Error(err)
return false
}
return true
}

// UserPushForJgUpdate 修改某条配置信息
func (*dbUserPushForJg) UserPushForJgUpdate(uid, masterId int64, pushAlia string) bool {
err := db.DB.Model(&model.UserPushForJg{}).Where("`uid` = ? and `master_id` = ?", uid, masterId).Update("push_alia", pushAlia).Error
if err != nil {
logx.Error(err)
return false
}
return true
}

// UserPushForJgGetWithDb 单条记录获取
func (*dbUserPushForJg) UserPushForJgGetWithDb(masterId string, HKey string) string {
cacheKey := fmt.Sprintf(md.AppUserPushForJgCacheKey, masterId, HKey[0:1])
get, err := db.RedisUtil.HGetString(cacheKey, HKey)
if err != nil || get == "" {
cfg, err := DbUserPushForJg.UserPushForJgGetOne(HKey, utils.StrToInt64(masterId))
if err != nil || cfg == nil {
_ = logx.Error(err)
return ""
}

// key是否存在
cacheKeyExist := false
if db.RedisUtil.Exists(cacheKey) {
cacheKeyExist = true
}

// 设置缓存
_, err = db.RedisUtil.HSet(cacheKey, HKey, cfg.PushAlia)
if err != nil {
_ = logx.Error(err)
return ""
}
if !cacheKeyExist { // 如果是首次设置 设置过期时间
_, err := db.RedisUtil.Expire(cacheKey, md.CfgCacheTime)
if err != nil {
_ = logx.Error(err)
return ""
}
}
return cfg.PushAlia
}
return get
}

+ 28
- 0
internal/business/comm/db/model/emoticon.go View File

@@ -0,0 +1,28 @@
package model

import "gim/pkg/pb"

type Emoticon struct {
Id int64
MasterId int64 // 站长id
Name string // 名称
ImgUrl string // 图片地址
Memo string // 备注
Sort int // 排序
State int // 状态0关闭,1开启
CreateAt string // 创建时间
UpdateAt string // 更新时间
}

func (e *Emoticon) ToProto() *pb.Emoticon {
if e == nil {
return nil
}

return &pb.Emoticon{
Name: e.Name,
ImgUrl: e.ImgUrl,
Memo: e.Memo,
Sort: int32(e.Sort),
}
}

+ 8
- 0
internal/business/comm/db/model/sys_cfg.go View File

@@ -0,0 +1,8 @@
package model

type SysCfg struct {
Key string // 键
Val string // 值
Memo string // 备注
MasterId int64 // 站长id
}

+ 8
- 0
internal/business/comm/db/model/user_push_for_jg.go View File

@@ -0,0 +1,8 @@
package model

type UserPushForJg struct {
Id int64
Uid int64 // 用户id
PushAlia string // 用户头像
MasterId int64 // 站长id
}

+ 8
- 0
internal/business/comm/md/app_redis_key.go View File

@@ -0,0 +1,8 @@
package md

// 缓存key统一管理, %s格式化为masterId
const (
AppCfgCacheKey = "%s:gim_cfg_cache:%s" // 占位符: masterId, key的第一个字母
AppUserPushForJgCacheKey = "%s:gim_user_push_for_jg_cache:%s" // 占位符: masterId, key的第一个字母
CfgCacheTime = 0.5 * 60 * 60
)

+ 17
- 0
internal/business/comm/md/cfg_key.go View File

@@ -0,0 +1,17 @@
package md

// 获取用户的缓存key
const (
KEY_SYS_CFG_CACHE = "gim_sys_cfg_cache"

// 文件缓存的key
KEY_CFG_FILE_PVD = "file_provider" // 文件供应商
KEY_CFG_FILE_BUCKET = "file_bucket"
KEY_CFG_FILE_REGION = "file_bucket_region"
KEY_CFG_FILE_HOST = "file_bucket_host"
KEY_CFG_FILE_SCHEME = "file_bucket_scheme"
KEY_CFG_FILE_AK = "file_access_key"
KEY_CFG_FILE_SK = "file_secret_key"
KEY_CFG_FILE_MAX_SIZE = "file_user_upload_max_size"
KEY_CFG_FILE_EXT = "file_ext"
)

+ 33
- 0
internal/business/comm/md/file.go View File

@@ -0,0 +1,33 @@
package md

// 用户拥有上传权限的目录, 目录ID

const (
FILE_DIR_IMAGE = "image"
FILE_DIR_VIDEO = "video"
FILE_DIR_FILE = "file"
)

var (
FileUserDir = map[string]string{
FILE_DIR_IMAGE: "1", // 图片上传
FILE_DIR_VIDEO: "2", // 视频上传
FILE_DIR_FILE: "3", // 文件上传
}
)

// 文件回调信息
type FileCallback struct {
Uid string `json:"uid"`
DirId string `json:"dir_id"`
Provider string `json:"provider"` // 供应商
FileName string `json:"fname"` // 原文件名
FileSize string `json:"fsize"`
Hash string `json:"hash"`
Bucket string `json:"bucket"`
Mime string `json:"mime"`
Width string `json:"w,omitempty"`
Height string `json:"h,omitempty"`
Time string `json:"time"` // 默认一个小时内要上传完毕,否则超时
Sign string `json:"sign"` // 签名
}

+ 16
- 0
internal/business/comm/service/qiniu/bucket_create.go View File

@@ -0,0 +1,16 @@
package qiniu

import (
"github.com/qiniu/api.v7/v7/auth"
"github.com/qiniu/api.v7/v7/storage"
)

func BucketCreate() error {
mac := auth.New(AK, SK)
cfg := storage.Config{
// 是否使用https域名进行资源管理
UseHTTPS: false,
}
bucketManager := storage.NewBucketManager(mac, &cfg)
return bucketManager.CreateBucket("", storage.RIDHuanan)
}

+ 18
- 0
internal/business/comm/service/qiniu/bucket_delete.go View File

@@ -0,0 +1,18 @@
package qiniu

import (
"github.com/qiniu/api.v7/v7/auth"
"github.com/qiniu/api.v7/v7/storage"
)

func BucketDelete(bucketName string) error {
mac := auth.New(AK, SK)

cfg := storage.Config{
// 是否使用https域名进行资源管理
UseHTTPS: false,
}

bucketManager := storage.NewBucketManager(mac, &cfg)
return bucketManager.DropBucket(bucketName)
}

+ 18
- 0
internal/business/comm/service/qiniu/bucket_get_domain.go View File

@@ -0,0 +1,18 @@
package qiniu

import (
"github.com/qiniu/api.v7/v7/auth"
"github.com/qiniu/api.v7/v7/storage"
)

func BucketGetDomain(bucketName string) (string, error) {
mac := auth.New(AK, SK)

cfg := storage.Config{UseHTTPS: false}
bucketManager := storage.NewBucketManager(mac, &cfg)
b, err := bucketManager.ListBucketDomains(bucketName)
if err != nil {
return "", err
}
return b[0].Domain, nil
}

+ 22
- 0
internal/business/comm/service/qiniu/init.go View File

@@ -0,0 +1,22 @@
package qiniu

import (
"gim/internal/business/comm/utils"
)

var (
AK = "MmxNdai23egjNUHjdzEVaTPdPCIbWzENz9BQuak3"
SK = "mElaFlM9O16rXp-ihoQdJ9KOH56naKm3MoyQBA59"
BUCKET = "dev-fnuoos" // 桶子名称
BUCKET_SCHEME = "http"
BUCKET_REGION = "up-z2.qiniup.com"
Expires uint64 = 3600
)

func Init(ak, sk, bucket, region, scheme string) {
AK, SK, BUCKET, BUCKET_REGION, BUCKET_SCHEME = ak, sk, bucket, region, scheme
}

func Sign(t string) string {
return utils.Md5(AK + SK + t)
}

+ 42
- 0
internal/business/comm/service/qiniu/req_img_upload.go View File

@@ -0,0 +1,42 @@
package qiniu

import (
"gim/internal/business/comm/md"
"gim/internal/business/comm/utils"
"time"

"github.com/qiniu/api.v7/v7/auth/qbox"
_ "github.com/qiniu/api.v7/v7/conf"
"github.com/qiniu/api.v7/v7/storage"
)

// 请求图片上传地址信息
func ReqImgUpload(f *md.FileCallback, callbackUrl string) interface{} {
if ext := utils.FileExt(f.FileName); ext == "png" || ext == "jpg" || ext == "jpeg" || ext == "gif" || ext == "bmp" || ext == "webp" {
f.Width = "$(imageInfo.width)"
f.Height = "$(imageInfo.height)"
}
f.Provider = "qiniu"
f.FileSize = "$(fsize)"
f.Hash = "$(etag)"
f.Bucket = "$(bucket)"
f.Mime = "$(mimeType)"
f.Time = utils.Int64ToStr(time.Now().Unix())
f.Sign = Sign(f.Time)
putPolicy := storage.PutPolicy{
Scope: BUCKET + ":" + f.FileName, // 使用覆盖方式时候必须请求里面有key,否则报错
Expires: Expires,
ForceSaveKey: true,
SaveKey: f.FileName,
//MimeLimit: "image/*", // 只允许上传图片
CallbackURL: callbackUrl,
CallbackBody: utils.SerializeStr(f),
CallbackBodyType: "application/json",
}
return &struct {
Method string `json:"method"`
Key string `json:"key"`
Host string `json:"host"`
Token string `json:"token"`
}{Key: f.FileName, Method: "POST", Host: BUCKET_SCHEME + "://" + BUCKET_REGION, Token: putPolicy.UploadToken(qbox.NewMac(AK, SK))}
}

+ 92
- 0
internal/business/comm/service/svc_file_upload.go View File

@@ -0,0 +1,92 @@
package svc

import (
"context"
"errors"
"fmt"
"gim/internal/business/comm/md"
"gim/internal/business/comm/service/qiniu"
"gim/internal/business/comm/utils"
"gim/internal/business/comm/utils/logx"
"strings"
)

// 请求文件上传
func FileReqUpload(ctx context.Context, uid, dirName, fName, callbackUrl string, fSize int64) (interface{}, error) {
ext := utils.FileExt(fName)
if err := initStg(ctx, fSize, ext); err != nil {
return nil, err
}
newName := dirName + "_" + fmt.Sprintf("%010s", uid)

// 默认都加时间戳
newName += "_" + utils.FormatNanoUnix() + utils.RandString(4, "0123456789")
newName += "." + ext

f := &md.FileCallback{
Uid: uid,
DirId: md.FileUserDir[dirName],
FileName: newName,
}
return qiniu.ReqImgUpload(f, callbackUrl), nil
}

func initStg(ctx context.Context, fSize int64, ext string) error {
// 获取上传配置
stgInfo := SysCfgFind(
ctx,
md.KEY_CFG_FILE_BUCKET,
md.KEY_CFG_FILE_HOST,
md.KEY_CFG_FILE_AK,
md.KEY_CFG_FILE_SK,
md.KEY_CFG_FILE_PVD,
md.KEY_CFG_FILE_REGION,
md.KEY_CFG_FILE_MAX_SIZE,
md.KEY_CFG_FILE_EXT,
md.KEY_CFG_FILE_SCHEME,
)

if stgInfo == nil {
return errors.New("获取配置信息出错")
}
// todo 目前仅支持七牛
if v, ok := stgInfo[md.KEY_CFG_FILE_PVD]; !ok || v != "qiniu" {
return errors.New("file_provider error")
}
if v, ok := stgInfo[md.KEY_CFG_FILE_REGION]; !ok || v == "" {
return errors.New("file_bucket_region error")
}
if v, ok := stgInfo[md.KEY_CFG_FILE_AK]; !ok || v == "" {
return errors.New("file_access_key error")
}
if v, ok := stgInfo[md.KEY_CFG_FILE_SK]; !ok || v == "" {
return errors.New("file_secret_key error")
}
if v, ok := stgInfo[md.KEY_CFG_FILE_BUCKET]; !ok || v == "" {
return errors.New("file_bucket error")
}
if v, ok := stgInfo[md.KEY_CFG_FILE_SCHEME]; !ok || v == "" {
stgInfo[md.KEY_CFG_FILE_SCHEME] = "http"
SysCfgSet(ctx, md.KEY_CFG_FILE_SCHEME, stgInfo[md.KEY_CFG_FILE_SCHEME], "文件域名HTTP协议")
}
qiniu.Init(stgInfo[md.KEY_CFG_FILE_AK], stgInfo[md.KEY_CFG_FILE_SK], stgInfo[md.KEY_CFG_FILE_BUCKET], stgInfo[md.KEY_CFG_FILE_REGION], stgInfo[md.KEY_CFG_FILE_SCHEME])
if v, ok := stgInfo[md.KEY_CFG_FILE_HOST]; !ok || v == "" {
var err error
stgInfo[md.KEY_CFG_FILE_HOST], err = qiniu.BucketGetDomain(stgInfo[md.KEY_CFG_FILE_BUCKET])
if err != nil {
logx.Error(err)
return errors.New("服务器配置错误")
}
SysCfgSet(ctx, md.KEY_CFG_FILE_HOST, stgInfo[md.KEY_CFG_FILE_HOST], "文件域名地址")
}

// 检查文件大小限制
if v, ok := stgInfo[md.KEY_CFG_FILE_MAX_SIZE]; ok && v != "" && utils.StrToInt64(v) < fSize {
return errors.New("file_user_upload_max_size error")
}
// 检查文件后缀
if v, ok := stgInfo[md.KEY_CFG_FILE_EXT]; ok && v != "" && !strings.Contains(v, ext) {
return errors.New("file_ext error")
}
return nil
}

+ 106
- 0
internal/business/comm/service/svc_sys_cfg_get.go View File

@@ -0,0 +1,106 @@
package svc

import (
"context"
"fmt"
"gim/internal/business/comm/db"
"gim/internal/business/comm/md"
"gim/internal/business/comm/utils"
db2 "gim/pkg/db"
"gim/pkg/grpclib"
)

// 单挑记录获取
func SysCfgGet(masterId, key string) string {
return db.DbSysCfg.SysCfgGetWithDb(masterId, key)
}

// 多条记录获取
func SysCfgFind(ctx context.Context, keys ...string) map[string]string {
masterId, _ := grpclib.GetCtxMasterId(ctx)
tmp := SysCfgFindComm(masterId, keys...)
return tmp
}

// SysCfgGetByMasterId get one config by master id
func SysCfgGetByMasterId(masterId, key string) string {
res := SysCfgFindComm(masterId, key)
if _, ok := res[key]; !ok {
return ""
}
return res[key]
}

// SysCfgFindComm get cfg by master id
func SysCfgFindComm(masterId string, keys ...string) map[string]string {
res := map[string]string{}
cfgKey := fmt.Sprintf("%s:gim_cfg_cache", masterId)
err := db2.RedisUtil.GetJson(cfgKey, &res)
if err != nil || len(res) == 0 {
cfgList, _ := db.DbSysCfg.SysCfgGetAll(utils.StrToInt64(masterId))
if cfgList == nil {
return nil
}
for _, v := range *cfgList {
res[v.Key] = v.Val
}
db2.RedisUtil.SetJson(cfgKey, res, 1800)
}
if len(keys) == 0 {
return res
}
tmp := map[string]string{}
for _, v := range keys {
if val, ok := res[v]; ok {
tmp[v] = val
} else {
tmp[v] = ""
}
}
return tmp
}

// EgSysCfgFind 多条记录获取
func EgSysCfgFind(masterId string, keys ...string) map[string]string {
res := map[string]string{}
if len(res) == 0 {
cfgList, _ := db.DbSysCfg.SysCfgGetAll(utils.StrToInt64(masterId))
if cfgList == nil {
return nil
}
for _, v := range *cfgList {
res[v.Key] = v.Val
}
}
if len(keys) == 0 {
return res
}
tmp := map[string]string{}
for _, v := range keys {
if val, ok := res[v]; ok {
tmp[v] = val
} else {
tmp[v] = ""
}
}
return tmp
}

// SysCfgCleanCache 清理系统配置信息
func SysCfgCleanCache() {
db2.RedisUtil.Del(md.KEY_SYS_CFG_CACHE)
}

// SysCfgSet 写入系统设置
func SysCfgSet(ctx context.Context, key, val, memo string) bool {
masterId, _ := grpclib.GetCtxMasterId(ctx)
cfg, err := db.DbSysCfg.SysCfgGetOne(key, utils.StrToInt64(masterId))
if err != nil || cfg == nil {
return db.DbSysCfg.SysCfgInsert(key, val, memo, utils.StrToInt64(masterId))
}
if memo != "" && cfg.Memo != memo {
cfg.Memo = memo
}
SysCfgCleanCache()
return db.DbSysCfg.SysCfgUpdate(key, val, cfg.Memo, utils.StrToInt64(masterId))
}

+ 117
- 0
internal/business/comm/utils/base64.go View File

@@ -0,0 +1,117 @@
package utils

import (
"encoding/base64"
"fmt"
"regexp"
)

const (
Base64Std = iota
Base64Url
Base64RawStd
Base64RawUrl
)

func Base64StdEncode(str interface{}) string {
return Base64Encode(str, Base64Std)
}

func Base64StdDecode(str interface{}) string {
return Base64Decode(str, Base64Std)
}

func Base64UrlEncode(str interface{}) string {
return Base64Encode(str, Base64Url)
}

func Base64UrlDecode(str interface{}) string {
return Base64Decode(str, Base64Url)
}

func Base64RawStdEncode(str interface{}) string {
return Base64Encode(str, Base64RawStd)
}

func Base64RawStdDecode(str interface{}) string {
return Base64Decode(str, Base64RawStd)
}

func Base64RawUrlEncode(str interface{}) string {
return Base64Encode(str, Base64RawUrl)
}

func Base64RawUrlDecode(str interface{}) string {
return Base64Decode(str, Base64RawUrl)
}

func Base64Encode(str interface{}, encode int) string {
newEncode := base64Encode(encode)
if newEncode == nil {
return ""
}
switch v := str.(type) {
case string:
return newEncode.EncodeToString([]byte(v))
case []byte:
return newEncode.EncodeToString(v)
}
return newEncode.EncodeToString([]byte(fmt.Sprint(str)))
}

func Base64Decode(str interface{}, encode int) string {
var err error
var b []byte
newEncode := base64Encode(encode)
if newEncode == nil {
return ""
}
switch v := str.(type) {
case string:
b, err = newEncode.DecodeString(v)
case []byte:
b, err = newEncode.DecodeString(string(v))
default:
return ""
}
if err != nil {
return ""
}
return string(b)
}

func base64Encode(encode int) *base64.Encoding {
switch encode {
case Base64Std:
return base64.StdEncoding
case Base64Url:
return base64.URLEncoding
case Base64RawStd:
return base64.RawStdEncoding
case Base64RawUrl:
return base64.RawURLEncoding
default:
return nil
}
}

func JudgeBase64(str string) bool {
pattern := "^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$"
matched, err := regexp.MatchString(pattern, str)
if err != nil {
return false
}
if !(len(str)%4 == 0 && matched) {
return false
}
unCodeStr, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return false
}
tranStr := base64.StdEncoding.EncodeToString(unCodeStr)
//return str==base64.StdEncoding.EncodeToString(unCodeStr)
if str == tranStr {
return true
}
return false
}

+ 369
- 0
internal/business/comm/utils/convert.go View File

@@ -0,0 +1,369 @@
package utils

import (
"encoding/binary"
"encoding/json"
"fmt"
"math"
"strconv"
"strings"
)

func ToString(raw interface{}, e error) (res string) {
if e != nil {
return ""
}
return AnyToString(raw)
}

func ToInt64(raw interface{}, e error) int64 {
if e != nil {
return 0
}
return AnyToInt64(raw)
}

func Float64ToStrPrec4(f float64) string {
return strconv.FormatFloat(f, 'f', 4, 64)
}
func AnyToBool(raw interface{}) bool {
switch i := raw.(type) {
case float32, float64, int, int64, uint, uint8, uint16, uint32, uint64, int8, int16, int32:
return i != 0
case []byte:
return i != nil
case string:
if i == "false" {
return false
}
return i != ""
case error:
return false
case nil:
return true
}
val := fmt.Sprint(raw)
val = strings.TrimLeft(val, "&")
if strings.TrimLeft(val, "{}") == "" {
return false
}
if strings.TrimLeft(val, "[]") == "" {
return false
}
// ptr type
b, err := json.Marshal(raw)
if err != nil {
return false
}
if strings.TrimLeft(string(b), "\"\"") == "" {
return false
}
if strings.TrimLeft(string(b), "{}") == "" {
return false
}
return true
}

func AnyToInt64(raw interface{}) int64 {
switch i := raw.(type) {
case string:
res, _ := strconv.ParseInt(i, 10, 64)
return res
case []byte:
return BytesToInt64(i)
case int:
return int64(i)
case int64:
return i
case uint:
return int64(i)
case uint8:
return int64(i)
case uint16:
return int64(i)
case uint32:
return int64(i)
case uint64:
return int64(i)
case int8:
return int64(i)
case int16:
return int64(i)
case int32:
return int64(i)
case float32:
return int64(i)
case float64:
return int64(i)
case error:
return 0
case bool:
if i {
return 1
}
return 0
}
return 0
}

func AnyToString(raw interface{}) string {
switch i := raw.(type) {
case []byte:
return string(i)
case int:
return strconv.FormatInt(int64(i), 10)
case int64:
return strconv.FormatInt(i, 10)
case float32:
return Float64ToStr(float64(i))
case float64:
return Float64ToStr(i)
case uint:
return strconv.FormatInt(int64(i), 10)
case uint8:
return strconv.FormatInt(int64(i), 10)
case uint16:
return strconv.FormatInt(int64(i), 10)
case uint32:
return strconv.FormatInt(int64(i), 10)
case uint64:
return strconv.FormatInt(int64(i), 10)
case int8:
return strconv.FormatInt(int64(i), 10)
case int16:
return strconv.FormatInt(int64(i), 10)
case int32:
return strconv.FormatInt(int64(i), 10)
case string:
return i
case error:
return i.Error()
case bool:
return strconv.FormatBool(i)
}
return fmt.Sprintf("%#v", raw)
}

func AnyToFloat64(raw interface{}) float64 {
switch i := raw.(type) {
case []byte:
f, _ := strconv.ParseFloat(string(i), 64)
return f
case int:
return float64(i)
case int64:
return float64(i)
case float32:
return float64(i)
case float64:
return i
case uint:
return float64(i)
case uint8:
return float64(i)
case uint16:
return float64(i)
case uint32:
return float64(i)
case uint64:
return float64(i)
case int8:
return float64(i)
case int16:
return float64(i)
case int32:
return float64(i)
case string:
f, _ := strconv.ParseFloat(i, 64)
return f
case bool:
if i {
return 1
}
}
return 0
}

func ToByte(raw interface{}, e error) []byte {
if e != nil {
return []byte{}
}
switch i := raw.(type) {
case string:
return []byte(i)
case int:
return Int64ToBytes(int64(i))
case int64:
return Int64ToBytes(i)
case float32:
return Float32ToByte(i)
case float64:
return Float64ToByte(i)
case uint:
return Int64ToBytes(int64(i))
case uint8:
return Int64ToBytes(int64(i))
case uint16:
return Int64ToBytes(int64(i))
case uint32:
return Int64ToBytes(int64(i))
case uint64:
return Int64ToBytes(int64(i))
case int8:
return Int64ToBytes(int64(i))
case int16:
return Int64ToBytes(int64(i))
case int32:
return Int64ToBytes(int64(i))
case []byte:
return i
case error:
return []byte(i.Error())
case bool:
if i {
return []byte("true")
}
return []byte("false")
}
return []byte(fmt.Sprintf("%#v", raw))
}

func Int64ToBytes(i int64) []byte {
var buf = make([]byte, 8)
binary.BigEndian.PutUint64(buf, uint64(i))
return buf
}

func BytesToInt64(buf []byte) int64 {
return int64(binary.BigEndian.Uint64(buf))
}

func StrToInt(s string) int {
res, _ := strconv.Atoi(s)
return res
}

func StrToInt64(s string) int64 {
res, _ := strconv.ParseInt(s, 10, 64)
return res
}

func Float32ToByte(float float32) []byte {
bits := math.Float32bits(float)
bytes := make([]byte, 4)
binary.LittleEndian.PutUint32(bytes, bits)

return bytes
}

func ByteToFloat32(bytes []byte) float32 {
bits := binary.LittleEndian.Uint32(bytes)
return math.Float32frombits(bits)
}

func Float64ToByte(float float64) []byte {
bits := math.Float64bits(float)
bytes := make([]byte, 8)
binary.LittleEndian.PutUint64(bytes, bits)
return bytes
}

func ByteToFloat64(bytes []byte) float64 {
bits := binary.LittleEndian.Uint64(bytes)
return math.Float64frombits(bits)
}

func Float64ToStr(f float64) string {
return strconv.FormatFloat(f, 'f', 2, 64)
}
func Float64ToStrPrec1(f float64) string {
return strconv.FormatFloat(f, 'f', 1, 64)
}
func Float64ToStrByPrec(f float64, prec int) string {
return strconv.FormatFloat(f, 'f', prec, 64)
}

func Float32ToStr(f float32) string {
return Float64ToStr(float64(f))
}

func StrToFloat64(s string) float64 {
res, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0
}
return res
}
func StrToFormat(s string, prec int) string {
ex := strings.Split(s, ".")
if len(ex) == 2 {
if StrToFloat64(ex[1]) == 0 { //小数点后面为空就是不要小数点了
return ex[0]
}
//看取多少位
str := ex[1]
str1 := str
if prec < len(str) {
str1 = str[0:prec]
} else {
for i := 0; i < prec-len(str); i++ {
str1 += "0"
}
}
if prec > 0 {
return ex[0] + "." + str1
} else {
return ex[0]
}
}
return s
}

func StrToFloat32(s string) float32 {
res, err := strconv.ParseFloat(s, 32)
if err != nil {
return 0
}
return float32(res)
}

func StrToBool(s string) bool {
b, _ := strconv.ParseBool(s)
return b
}

func BoolToStr(b bool) string {
if b {
return "true"
}
return "false"
}

func FloatToInt64(f float64) int64 {
return int64(f)
}

func IntToStr(i int) string {
return strconv.Itoa(i)
}

func Int64ToStr(i int64) string {
return strconv.FormatInt(i, 10)
}

func IntToFloat64(i int) float64 {
s := strconv.Itoa(i)
res, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0
}
return res
}
func Int64ToFloat64(i int64) float64 {
s := strconv.FormatInt(i, 10)
res, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0
}
return res
}

+ 209
- 0
internal/business/comm/utils/curl.go View File

@@ -0,0 +1,209 @@
package utils

import (
"bytes"
"crypto/tls"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"sort"
"strings"
"time"
)

var CurlDebug bool

func CurlGet(router string, header map[string]string) ([]byte, error) {
return curl(http.MethodGet, router, nil, header)
}
func CurlGetJson(router string, body interface{}, header map[string]string) ([]byte, error) {
return curl_new(http.MethodGet, router, body, header)
}

// 只支持form 与json 提交, 请留意body的类型, 支持string, []byte, map[string]string
func CurlPost(router string, body interface{}, header map[string]string) ([]byte, error) {
return curl(http.MethodPost, router, body, header)
}

func CurlPut(router string, body interface{}, header map[string]string) ([]byte, error) {
return curl(http.MethodPut, router, body, header)
}

// 只支持form 与json 提交, 请留意body的类型, 支持string, []byte, map[string]string
func CurlPatch(router string, body interface{}, header map[string]string) ([]byte, error) {
return curl(http.MethodPatch, router, body, header)
}

// CurlDelete is curl delete
func CurlDelete(router string, body interface{}, header map[string]string) ([]byte, error) {
return curl(http.MethodDelete, router, body, header)
}

func curl(method, router string, body interface{}, header map[string]string) ([]byte, error) {
var reqBody io.Reader
contentType := "application/json"
switch v := body.(type) {
case string:
reqBody = strings.NewReader(v)
case []byte:
reqBody = bytes.NewReader(v)
case map[string]string:
val := url.Values{}
for k, v := range v {
val.Set(k, v)
}
reqBody = strings.NewReader(val.Encode())
contentType = "application/x-www-form-urlencoded"
case map[string]interface{}:
val := url.Values{}
for k, v := range v {
val.Set(k, v.(string))
}
reqBody = strings.NewReader(val.Encode())
contentType = "application/x-www-form-urlencoded"
}
if header == nil {
header = map[string]string{"Content-Type": contentType}
}
if _, ok := header["Content-Type"]; !ok {
header["Content-Type"] = contentType
}
resp, er := CurlReq(method, router, reqBody, header)
if er != nil {
return nil, er
}
res, err := ioutil.ReadAll(resp.Body)
if CurlDebug {
blob := SerializeStr(body)
if contentType != "application/json" {
blob = HttpBuild(body)
}
fmt.Printf("\n\n=====================\n[url]: %s\n[time]: %s\n[method]: %s\n[content-type]: %v\n[req_header]: %s\n[req_body]: %#v\n[resp_err]: %v\n[resp_header]: %v\n[resp_body]: %v\n=====================\n\n",
router,
time.Now().Format("2006-01-02 15:04:05.000"),
method,
contentType,
HttpBuildQuery(header),
blob,
err,
SerializeStr(resp.Header),
string(res),
)
}
resp.Body.Close()
return res, err
}

func curl_new(method, router string, body interface{}, header map[string]string) ([]byte, error) {
var reqBody io.Reader
contentType := "application/json"

if header == nil {
header = map[string]string{"Content-Type": contentType}
}
if _, ok := header["Content-Type"]; !ok {
header["Content-Type"] = contentType
}
resp, er := CurlReq(method, router, reqBody, header)
if er != nil {
return nil, er
}
res, err := ioutil.ReadAll(resp.Body)
if CurlDebug {
blob := SerializeStr(body)
if contentType != "application/json" {
blob = HttpBuild(body)
}
fmt.Printf("\n\n=====================\n[url]: %s\n[time]: %s\n[method]: %s\n[content-type]: %v\n[req_header]: %s\n[req_body]: %#v\n[resp_err]: %v\n[resp_header]: %v\n[resp_body]: %v\n=====================\n\n",
router,
time.Now().Format("2006-01-02 15:04:05.000"),
method,
contentType,
HttpBuildQuery(header),
blob,
err,
SerializeStr(resp.Header),
string(res),
)
}
resp.Body.Close()
return res, err
}

func CurlReq(method, router string, reqBody io.Reader, header map[string]string) (*http.Response, error) {
req, _ := http.NewRequest(method, router, reqBody)
if header != nil {
for k, v := range header {
req.Header.Set(k, v)
}
}
// 绕过github等可能因为特征码返回503问题
// https://www.imwzk.com/posts/2021-03-14-why-i-always-get-503-with-golang/
defaultCipherSuites := []uint16{0xc02f, 0xc030, 0xc02b, 0xc02c, 0xcca8, 0xcca9, 0xc013, 0xc009,
0xc014, 0xc00a, 0x009c, 0x009d, 0x002f, 0x0035, 0xc012, 0x000a}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
CipherSuites: append(defaultCipherSuites[8:], defaultCipherSuites[:8]...),
},
},
// 获取301重定向
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
return client.Do(req)
}

// 组建get请求参数,sortAsc true为小到大,false为大到小,nil不排序 a=123&b=321
func HttpBuildQuery(args map[string]string, sortAsc ...bool) string {
str := ""
if len(args) == 0 {
return str
}
if len(sortAsc) > 0 {
keys := make([]string, 0, len(args))
for k := range args {
keys = append(keys, k)
}
if sortAsc[0] {
sort.Strings(keys)
} else {
sort.Sort(sort.Reverse(sort.StringSlice(keys)))
}
for _, k := range keys {
str += "&" + k + "=" + args[k]
}
} else {
for k, v := range args {
str += "&" + k + "=" + v
}
}
return str[1:]
}

func HttpBuild(body interface{}, sortAsc ...bool) string {
params := map[string]string{}
if args, ok := body.(map[string]interface{}); ok {
for k, v := range args {
params[k] = AnyToString(v)
}
return HttpBuildQuery(params, sortAsc...)
}
if args, ok := body.(map[string]string); ok {
for k, v := range args {
params[k] = AnyToString(v)
}
return HttpBuildQuery(params, sortAsc...)
}
if args, ok := body.(map[string]int); ok {
for k, v := range args {
params[k] = AnyToString(v)
}
return HttpBuildQuery(params, sortAsc...)
}
return AnyToString(body)
}

+ 22
- 0
internal/business/comm/utils/file.go View File

@@ -0,0 +1,22 @@
package utils

import (
"os"
"path"
"strings"
"time"
)

// 获取文件后缀
func FileExt(fname string) string {
return strings.ToLower(strings.TrimLeft(path.Ext(fname), "."))
}

func FilePutContents(fileName string, content string) {
fd, _ := os.OpenFile("./tmp/"+fileName+".log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
fd_time := time.Now().Format("2006-01-02 15:04:05")
fd_content := strings.Join([]string{"[", fd_time, "] ", content, "\n"}, "")
buf := []byte(fd_content)
fd.Write(buf)
fd.Close()
}

+ 245
- 0
internal/business/comm/utils/logx/log.go View File

@@ -0,0 +1,245 @@
package logx

import (
"os"
"strings"
"time"

"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

type LogConfig struct {
AppName string `yaml:"app_name" json:"app_name" toml:"app_name"`
Level string `yaml:"level" json:"level" toml:"level"`
StacktraceLevel string `yaml:"stacktrace_level" json:"stacktrace_level" toml:"stacktrace_level"`
IsStdOut bool `yaml:"is_stdout" json:"is_stdout" toml:"is_stdout"`
TimeFormat string `yaml:"time_format" json:"time_format" toml:"time_format"` // second, milli, nano, standard, iso,
Encoding string `yaml:"encoding" json:"encoding" toml:"encoding"` // console, json
Skip int `yaml:"skip" json:"skip" toml:"skip"`

IsFileOut bool `yaml:"is_file_out" json:"is_file_out" toml:"is_file_out"`
FileDir string `yaml:"file_dir" json:"file_dir" toml:"file_dir"`
FileName string `yaml:"file_name" json:"file_name" toml:"file_name"`
FileMaxSize int `yaml:"file_max_size" json:"file_max_size" toml:"file_max_size"`
FileMaxAge int `yaml:"file_max_age" json:"file_max_age" toml:"file_max_age"`
}

var (
l *LogX = defaultLogger()
conf *LogConfig
)

// default logger setting
func defaultLogger() *LogX {
conf = &LogConfig{
Level: "debug",
StacktraceLevel: "error",
IsStdOut: true,
TimeFormat: "standard",
Encoding: "console",
Skip: 2,
}
writers := []zapcore.WriteSyncer{os.Stdout}
lg, lv := newZapLogger(setLogLevel(conf.Level), setLogLevel(conf.StacktraceLevel), conf.Encoding, conf.TimeFormat, conf.Skip, zapcore.NewMultiWriteSyncer(writers...))
zap.RedirectStdLog(lg)
return &LogX{logger: lg, atomLevel: lv}
}

// initial standard log, if you don't init, it will use default logger setting
func InitDefaultLogger(cfg *LogConfig) {
var writers []zapcore.WriteSyncer
if cfg.IsStdOut || (!cfg.IsStdOut && !cfg.IsFileOut) {
writers = append(writers, os.Stdout)
}
if cfg.IsFileOut {
writers = append(writers, NewRollingFile(cfg.FileDir, cfg.FileName, cfg.FileMaxSize, cfg.FileMaxAge))
}

lg, lv := newZapLogger(setLogLevel(cfg.Level), setLogLevel(cfg.StacktraceLevel), cfg.Encoding, cfg.TimeFormat, cfg.Skip, zapcore.NewMultiWriteSyncer(writers...))
zap.RedirectStdLog(lg)
if cfg.AppName != "" {
lg = lg.With(zap.String("app", cfg.AppName)) // 加上应用名称
}
l = &LogX{logger: lg, atomLevel: lv}
}

// create a new logger
func NewLogger(cfg *LogConfig) *LogX {
var writers []zapcore.WriteSyncer
if cfg.IsStdOut || (!cfg.IsStdOut && !cfg.IsFileOut) {
writers = append(writers, os.Stdout)
}
if cfg.IsFileOut {
writers = append(writers, NewRollingFile(cfg.FileDir, cfg.FileName, cfg.FileMaxSize, cfg.FileMaxAge))
}

lg, lv := newZapLogger(setLogLevel(cfg.Level), setLogLevel(cfg.StacktraceLevel), cfg.Encoding, cfg.TimeFormat, cfg.Skip, zapcore.NewMultiWriteSyncer(writers...))
zap.RedirectStdLog(lg)
if cfg.AppName != "" {
lg = lg.With(zap.String("app", cfg.AppName)) // 加上应用名称
}
return &LogX{logger: lg, atomLevel: lv}
}

// create a new zaplog logger
func newZapLogger(level, stacktrace zapcore.Level, encoding, timeType string, skip int, output zapcore.WriteSyncer) (*zap.Logger, *zap.AtomicLevel) {
encCfg := zapcore.EncoderConfig{
TimeKey: "T",
LevelKey: "L",
NameKey: "N",
CallerKey: "C",
MessageKey: "M",
StacktraceKey: "S",
LineEnding: zapcore.DefaultLineEnding,
EncodeCaller: zapcore.ShortCallerEncoder,
EncodeDuration: zapcore.NanosDurationEncoder,
EncodeLevel: zapcore.LowercaseLevelEncoder,
}
setTimeFormat(timeType, &encCfg) // set time type
atmLvl := zap.NewAtomicLevel() // set level
atmLvl.SetLevel(level)
encoder := zapcore.NewJSONEncoder(encCfg) // 确定encoder格式
if encoding == "console" {
encoder = zapcore.NewConsoleEncoder(encCfg)
}
return zap.New(zapcore.NewCore(encoder, output, atmLvl), zap.AddCaller(), zap.AddStacktrace(stacktrace), zap.AddCallerSkip(skip)), &atmLvl
}

// set log level
func setLogLevel(lvl string) zapcore.Level {
switch strings.ToLower(lvl) {
case "panic":
return zapcore.PanicLevel
case "fatal":
return zapcore.FatalLevel
case "error":
return zapcore.ErrorLevel
case "warn", "warning":
return zapcore.WarnLevel
case "info":
return zapcore.InfoLevel
default:
return zapcore.DebugLevel
}
}

// set time format
func setTimeFormat(timeType string, z *zapcore.EncoderConfig) {
switch strings.ToLower(timeType) {
case "iso": // iso8601 standard
z.EncodeTime = zapcore.ISO8601TimeEncoder
case "sec": // only for unix second, without millisecond
z.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendInt64(t.Unix())
}
case "second": // unix second, with millisecond
z.EncodeTime = zapcore.EpochTimeEncoder
case "milli", "millisecond": // millisecond
z.EncodeTime = zapcore.EpochMillisTimeEncoder
case "nano", "nanosecond": // nanosecond
z.EncodeTime = zapcore.EpochNanosTimeEncoder
default: // standard format
z.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format("2006-01-02 15:04:05.000"))
}
}
}

func GetLevel() string {
switch l.atomLevel.Level() {
case zapcore.PanicLevel:
return "panic"
case zapcore.FatalLevel:
return "fatal"
case zapcore.ErrorLevel:
return "error"
case zapcore.WarnLevel:
return "warn"
case zapcore.InfoLevel:
return "info"
default:
return "debug"
}
}

func SetLevel(lvl string) {
l.atomLevel.SetLevel(setLogLevel(lvl))
}

// temporary add call skip
func AddCallerSkip(skip int) *LogX {
l.logger.WithOptions(zap.AddCallerSkip(skip))
return l
}

// permanent add call skip
func AddDepth(skip int) *LogX {
l.logger = l.logger.WithOptions(zap.AddCallerSkip(skip))
return l
}

// permanent add options
func AddOptions(opts ...zap.Option) *LogX {
l.logger = l.logger.WithOptions(opts...)
return l
}

func AddField(k string, v interface{}) {
l.logger.With(zap.Any(k, v))
}

func AddFields(fields map[string]interface{}) *LogX {
for k, v := range fields {
l.logger.With(zap.Any(k, v))
}
return l
}

// Normal log
func Debug(e interface{}, args ...interface{}) error {
return l.Debug(e, args...)
}
func Info(e interface{}, args ...interface{}) error {
return l.Info(e, args...)
}
func Warn(e interface{}, args ...interface{}) error {
return l.Warn(e, args...)
}
func Error(e interface{}, args ...interface{}) error {
return l.Error(e, args...)
}
func Panic(e interface{}, args ...interface{}) error {
return l.Panic(e, args...)
}
func Fatal(e interface{}, args ...interface{}) error {
return l.Fatal(e, args...)
}

// Format logs
func Debugf(format string, args ...interface{}) error {
return l.Debugf(format, args...)
}
func Infof(format string, args ...interface{}) error {
return l.Infof(format, args...)
}
func Warnf(format string, args ...interface{}) error {
return l.Warnf(format, args...)
}
func Errorf(format string, args ...interface{}) error {
return l.Errorf(format, args...)
}
func Panicf(format string, args ...interface{}) error {
return l.Panicf(format, args...)
}
func Fatalf(format string, args ...interface{}) error {
return l.Fatalf(format, args...)
}

func formatFieldMap(m FieldMap) []Field {
var res []Field
for k, v := range m {
res = append(res, zap.Any(k, v))
}
return res
}

+ 105
- 0
internal/business/comm/utils/logx/output.go View File

@@ -0,0 +1,105 @@
package logx

import (
"bytes"
"io"
"os"
"path/filepath"
"time"

"gopkg.in/natefinch/lumberjack.v2"
)

// output interface
type WriteSyncer interface {
io.Writer
Sync() error
}

// split writer
func NewRollingFile(dir, filename string, maxSize, MaxAge int) WriteSyncer {
s, err := os.Stat(dir)
if err != nil || !s.IsDir() {
os.RemoveAll(dir)
if err := os.MkdirAll(dir, 0766); err != nil {
panic(err)
}
}
return newLumberjackWriteSyncer(&lumberjack.Logger{
Filename: filepath.Join(dir, filename),
MaxSize: maxSize, // megabytes, MB
MaxAge: MaxAge, // days
LocalTime: true,
Compress: false,
})
}

type lumberjackWriteSyncer struct {
*lumberjack.Logger
buf *bytes.Buffer
logChan chan []byte
closeChan chan interface{}
maxSize int
}

func newLumberjackWriteSyncer(l *lumberjack.Logger) *lumberjackWriteSyncer {
ws := &lumberjackWriteSyncer{
Logger: l,
buf: bytes.NewBuffer([]byte{}),
logChan: make(chan []byte, 5000),
closeChan: make(chan interface{}),
maxSize: 1024,
}
go ws.run()
return ws
}

func (l *lumberjackWriteSyncer) run() {
ticker := time.NewTicker(1 * time.Second)

for {
select {
case <-ticker.C:
if l.buf.Len() > 0 {
l.sync()
}
case bs := <-l.logChan:
_, err := l.buf.Write(bs)
if err != nil {
continue
}
if l.buf.Len() > l.maxSize {
l.sync()
}
case <-l.closeChan:
l.sync()
return
}
}
}

func (l *lumberjackWriteSyncer) Stop() {
close(l.closeChan)
}

func (l *lumberjackWriteSyncer) Write(bs []byte) (int, error) {
b := make([]byte, len(bs))
for i, c := range bs {
b[i] = c
}
l.logChan <- b
return 0, nil
}

func (l *lumberjackWriteSyncer) Sync() error {
return nil
}

func (l *lumberjackWriteSyncer) sync() error {
defer l.buf.Reset()
_, err := l.Logger.Write(l.buf.Bytes())
if err != nil {
return err
}
return nil
}

+ 192
- 0
internal/business/comm/utils/logx/sugar.go View File

@@ -0,0 +1,192 @@
package logx

import (
"errors"
"fmt"
"strconv"

"go.uber.org/zap"
)

type LogX struct {
logger *zap.Logger
atomLevel *zap.AtomicLevel
}

type Field = zap.Field
type FieldMap map[string]interface{}

// 判断其他类型--start
func getFields(msg string, format bool, args ...interface{}) (string, []Field) {
var str []interface{}
var fields []zap.Field
if len(args) > 0 {
for _, v := range args {
if f, ok := v.(Field); ok {
fields = append(fields, f)
} else if f, ok := v.(FieldMap); ok {
fields = append(fields, formatFieldMap(f)...)
} else {
str = append(str, AnyToString(v))
}
}
if format {
return fmt.Sprintf(msg, str...), fields
}
str = append([]interface{}{msg}, str...)
return fmt.Sprintln(str...), fields
}
return msg, []Field{}
}

func (l *LogX) Debug(s interface{}, args ...interface{}) error {
es, e := checkErr(s)
if es != "" {
msg, field := getFields(es, false, args...)
l.logger.Debug(msg, field...)
}
return e
}
func (l *LogX) Info(s interface{}, args ...interface{}) error {
es, e := checkErr(s)
if es != "" {
msg, field := getFields(es, false, args...)
l.logger.Info(msg, field...)
}
return e
}
func (l *LogX) Warn(s interface{}, args ...interface{}) error {
es, e := checkErr(s)
if es != "" {
msg, field := getFields(es, false, args...)
l.logger.Warn(msg, field...)
}
return e
}
func (l *LogX) Error(s interface{}, args ...interface{}) error {
es, e := checkErr(s)
if es != "" {
msg, field := getFields(es, false, args...)
l.logger.Error(msg, field...)
}
return e
}
func (l *LogX) DPanic(s interface{}, args ...interface{}) error {
es, e := checkErr(s)
if es != "" {
msg, field := getFields(es, false, args...)
l.logger.DPanic(msg, field...)
}
return e
}
func (l *LogX) Panic(s interface{}, args ...interface{}) error {
es, e := checkErr(s)
if es != "" {
msg, field := getFields(es, false, args...)
l.logger.Panic(msg, field...)
}
return e
}
func (l *LogX) Fatal(s interface{}, args ...interface{}) error {
es, e := checkErr(s)
if es != "" {
msg, field := getFields(es, false, args...)
l.logger.Fatal(msg, field...)
}
return e
}

func checkErr(s interface{}) (string, error) {
switch e := s.(type) {
case error:
return e.Error(), e
case string:
return e, errors.New(e)
case []byte:
return string(e), nil
default:
return "", nil
}
}

func (l *LogX) LogError(err error) error {
return l.Error(err.Error())
}

func (l *LogX) Debugf(msg string, args ...interface{}) error {
s, f := getFields(msg, true, args...)
l.logger.Debug(s, f...)
return errors.New(s)
}

func (l *LogX) Infof(msg string, args ...interface{}) error {
s, f := getFields(msg, true, args...)
l.logger.Info(s, f...)
return errors.New(s)
}

func (l *LogX) Warnf(msg string, args ...interface{}) error {
s, f := getFields(msg, true, args...)
l.logger.Warn(s, f...)
return errors.New(s)
}

func (l *LogX) Errorf(msg string, args ...interface{}) error {
s, f := getFields(msg, true, args...)
l.logger.Error(s, f...)
return errors.New(s)
}

func (l *LogX) DPanicf(msg string, args ...interface{}) error {
s, f := getFields(msg, true, args...)
l.logger.DPanic(s, f...)
return errors.New(s)
}

func (l *LogX) Panicf(msg string, args ...interface{}) error {
s, f := getFields(msg, true, args...)
l.logger.Panic(s, f...)
return errors.New(s)
}

func (l *LogX) Fatalf(msg string, args ...interface{}) error {
s, f := getFields(msg, true, args...)
l.logger.Fatal(s, f...)
return errors.New(s)
}

func AnyToString(raw interface{}) string {
switch i := raw.(type) {
case []byte:
return string(i)
case int:
return strconv.FormatInt(int64(i), 10)
case int64:
return strconv.FormatInt(i, 10)
case float32:
return strconv.FormatFloat(float64(i), 'f', 2, 64)
case float64:
return strconv.FormatFloat(i, 'f', 2, 64)
case uint:
return strconv.FormatInt(int64(i), 10)
case uint8:
return strconv.FormatInt(int64(i), 10)
case uint16:
return strconv.FormatInt(int64(i), 10)
case uint32:
return strconv.FormatInt(int64(i), 10)
case uint64:
return strconv.FormatInt(int64(i), 10)
case int8:
return strconv.FormatInt(int64(i), 10)
case int16:
return strconv.FormatInt(int64(i), 10)
case int32:
return strconv.FormatInt(int64(i), 10)
case string:
return i
case error:
return i.Error()
}
return fmt.Sprintf("%#v", raw)
}

+ 12
- 0
internal/business/comm/utils/md5.go View File

@@ -0,0 +1,12 @@
package utils

import (
"crypto/md5"
"encoding/hex"
)

func Md5(str string) string {
h := md5.New()
h.Write([]byte(str))
return hex.EncodeToString(h.Sum(nil))
}

+ 46
- 0
internal/business/comm/utils/rand.go View File

@@ -0,0 +1,46 @@
package utils

import (
crand "crypto/rand"
"fmt"
"math/big"
"math/rand"
"time"
)

func RandString(l int, c ...string) string {
var (
chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
str string
num *big.Int
)
if len(c) > 0 {
chars = c[0]
}
chrLen := int64(len(chars))
for len(str) < l {
num, _ = crand.Int(crand.Reader, big.NewInt(chrLen))
str += string(chars[num.Int64()])
}
return str
}

func RandNum() string {
seed := time.Now().UnixNano() + rand.Int63()
return fmt.Sprintf("%05v", rand.New(rand.NewSource(seed)).Int31n(1000000))
}

//x的y次方
func RandPow(l int) string {
var i = "1"
for j := 0; j < l; j++ {
i += "0"
}
k := StrToInt64(i)
n := rand.New(rand.NewSource(time.Now().UnixNano())).Int63n(k)
ls := "%0" + IntToStr(l) + "v"
str := fmt.Sprintf(ls, n)
//min := int(math.Pow10(l - 1))
//max := int(math.Pow10(l) - 1)
return str
}

+ 29
- 0
internal/business/comm/utils/reply.go View File

@@ -0,0 +1,29 @@
package utils

import (
"errors"
"fmt"
)

type Error string

func (err Error) Error() string { return string(err) }

var ErrNil = errors.New("redigo: nil returned")

func Bytes(reply interface{}, err error) ([]byte, error) {
if err != nil {
return nil, err
}
switch reply := reply.(type) {
case []byte:
return reply, nil
case string:
return []byte(reply), nil
case nil:
return nil, ErrNil
case Error:
return nil, reply
}
return nil, fmt.Errorf("redigo: unexpected type for Bytes, got type %T", reply)
}

+ 23
- 0
internal/business/comm/utils/serialize.go View File

@@ -0,0 +1,23 @@
package utils

import (
"encoding/json"
)

func Serialize(data interface{}) []byte {
res, err := json.Marshal(data)
if err != nil {
return []byte{}
}
return res
}

func Unserialize(b []byte, dst interface{}) {
if err := json.Unmarshal(b, dst); err != nil {
dst = nil
}
}

func SerializeStr(data interface{}, arg ...interface{}) string {
return string(Serialize(data))
}

+ 208
- 0
internal/business/comm/utils/time.go View File

@@ -0,0 +1,208 @@
package utils

import (
"errors"
"fmt"
"strconv"
"strings"
"time"
)

func StrToTime(s string) (int64, error) {
// delete all not int characters
if s == "" {
return time.Now().Unix(), nil
}
r := make([]rune, 14)
l := 0
// 过滤除数字以外的字符
for _, v := range s {
if '0' <= v && v <= '9' {
r[l] = v
l++
if l == 14 {
break
}
}
}
for l < 14 {
r[l] = '0' // 补0
l++
}
t, err := time.Parse("20060102150405", string(r))
if err != nil {
return 0, err
}
return t.Unix(), nil
}

func TimeToStr(unixSecTime interface{}, layout ...string) string {
i := AnyToInt64(unixSecTime)
if i == 0 {
return ""
}
f := "2006-01-02 15:04:05"
if len(layout) > 0 {
f = layout[0]
}
return time.Unix(i, 0).Format(f)
}

func FormatNanoUnix() string {
return strings.Replace(time.Now().Format("20060102150405.0000000"), ".", "", 1)
}

func TimeParse(format, src string) (time.Time, error) {
return time.ParseInLocation(format, src, time.Local)
}

func TimeParseStd(src string) time.Time {
t, _ := TimeParse("2006-01-02 15:04:05", src)
return t
}

func TimeStdParseUnix(src string) int64 {
t, err := TimeParse("2006-01-02 15:04:05", src)
if err != nil {
return 0
}
return t.Unix()
}

// 获取一个当前时间 时间间隔 时间戳
func GetTimeInterval(unit string, amount int) (startTime, endTime int64) {
t := time.Now()
nowTime := t.Unix()
tmpTime := int64(0)
switch unit {
case "years":
tmpTime = time.Date(t.Year()+amount, t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location()).Unix()
case "months":
tmpTime = time.Date(t.Year(), t.Month()+time.Month(amount), t.Day(), t.Hour(), 0, 0, 0, t.Location()).Unix()
case "days":
tmpTime = time.Date(t.Year(), t.Month(), t.Day()+amount, t.Hour(), 0, 0, 0, t.Location()).Unix()
case "hours":
tmpTime = time.Date(t.Year(), t.Month(), t.Day(), t.Hour()+amount, 0, 0, 0, t.Location()).Unix()
}
if amount > 0 {
startTime = nowTime
endTime = tmpTime
} else {
startTime = tmpTime
endTime = nowTime
}
return
}

// 几天前
func TimeInterval(newTime int) string {
now := time.Now().Unix()
newTime64 := AnyToInt64(newTime)
if newTime64 >= now {
return "刚刚"
}
interval := now - newTime64
switch {
case interval < 60:
return AnyToString(interval) + "秒前"
case interval < 60*60:
return AnyToString(interval/60) + "分前"
case interval < 60*60*24:
return AnyToString(interval/60/60) + "小时前"
case interval < 60*60*24*30:
return AnyToString(interval/60/60/24) + "天前"
case interval < 60*60*24*30*12:
return AnyToString(interval/60/60/24/30) + "月前"
default:
return AnyToString(interval/60/60/24/30/12) + "年前"
}
}

// 时分秒字符串转时间戳,传入示例:8:40 or 8:40:10
func HmsToUnix(str string) (int64, error) {
t := time.Now()
arr := strings.Split(str, ":")
if len(arr) < 2 {
return 0, errors.New("Time format error")
}
h, _ := strconv.Atoi(arr[0])
m, _ := strconv.Atoi(arr[1])
s := 0
if len(arr) == 3 {
s, _ = strconv.Atoi(arr[3])
}
formatted1 := fmt.Sprintf("%d%02d%02d%02d%02d%02d", t.Year(), t.Month(), t.Day(), h, m, s)
res, err := time.ParseInLocation("20060102150405", formatted1, time.Local)
if err != nil {
return 0, err
} else {
return res.Unix(), nil
}
}

// 获取特定时间范围
func GetTimeRange(s string) map[string]int64 {
t := time.Now()
var stime, etime time.Time
switch s {
case "today":
stime = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
etime = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, t.Location())
case "yesterday":
stime = time.Date(t.Year(), t.Month(), t.Day()-1, 0, 0, 0, 0, t.Location())
etime = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
case "within_seven_days":
// 前6天0点
stime = time.Date(t.Year(), t.Month(), t.Day()-6, 0, 0, 0, 0, t.Location())
// 明天 0点
etime = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, t.Location())
case "current_month":
stime = GetFirstDateOfMonth(t)
etime = time.Now()
case "last_month":
etime = GetFirstDateOfMonth(t)
monthTimes := TimeStdParseUnix(etime.Format("2006-01-02 15:04:05")) - 86400
times, _ := UnixToTime(Int64ToStr(monthTimes))
stime = GetFirstDateOfMonth(times)

}

return map[string]int64{
"start": stime.Unix(),
"end": etime.Unix(),
}
}

//时间戳转时间格式
func UnixToTime(e string) (datatime time.Time, err error) {
data, err := strconv.ParseInt(e, 10, 64)
datatime = time.Unix(data, 0)
return
}

//获取传入的时间所在月份的第一天,即某月第一天的0点。如传入time.Now(), 返回当前月份的第一天0点时间。
func GetFirstDateOfMonth(d time.Time) time.Time {
d = d.AddDate(0, 0, -d.Day()+1)
return GetZeroTime(d)
}

//获取传入的时间所在月份的最后一天,即某月最后一天的0点。如传入time.Now(), 返回当前月份的最后一天0点时间。
func GetLastDateOfMonth(d time.Time) time.Time {
return GetFirstDateOfMonth(d).AddDate(0, 1, -1)
}

//获取某一天的0点时间
func GetZeroTime(d time.Time) time.Time {
return time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, 0, d.Location())
}

//获取当月某天的某个时间的时间
func GetDayToTime(day, timeStr string) string {
if timeStr == "" {
timeStr = "00:00:00"
}
year := time.Now().Year()
month := time.Now().Format("01")
times := fmt.Sprintf("%s-%s-%s %s", IntToStr(year), month, day, timeStr)
return times
}

+ 7
- 0
internal/business/domain/user/model/device.go View File

@@ -0,0 +1,7 @@
package model

type Device struct {
Type int32 // 设备类型,1:Android;2:IOS;3:Windows; 4:MacOS;5:Web
Token string // token
Expire int64 // 过期时间
}

+ 39
- 0
internal/business/domain/user/model/user.go View File

@@ -0,0 +1,39 @@
package model

import (
"gim/pkg/pb"
"time"
)

// User 账户
type User struct {
Id int64 // 用户id
PhoneNumber string // 手机号
Nickname string // 昵称
Sex int32 // 性别,1:男;2:女
AvatarUrl string // 用户头像
Extra string // 附加属性
CreateTime time.Time // 创建时间
UpdateTime time.Time // 更新时间
MasterId int64 // 站长id
IsAutoAddedFriends int // 是否自动被添加好友
}

func (u *User) ToProto() *pb.User {
if u == nil {
return nil
}

return &pb.User{
UserId: u.Id,
Nickname: u.Nickname,
Sex: u.Sex,
AvatarUrl: u.AvatarUrl,
Extra: u.Extra,
CreateTime: u.CreateTime.Unix(),
UpdateTime: u.UpdateTime.Unix(),
MasterId: u.MasterId,
IsAutoAddedFriends: int64(u.IsAutoAddedFriends),
PhoneNumber: u.PhoneNumber,
}
}

+ 74
- 0
internal/business/domain/user/repo/auth_cache.go View File

@@ -0,0 +1,74 @@
package repo

import (
"encoding/json"
"gim/internal/business/domain/user/model"
"gim/pkg/db"
"gim/pkg/gerrors"
"gim/pkg/util"
"strconv"

"github.com/go-redis/redis"
)

const (
AuthKey = "auth:"
)

type authCache struct{}

var AuthCache = new(authCache)

func (*authCache) Get(userId, deviceId int64) (*model.Device, error) {
bytes, err := db.RedisCli.HGet(AuthKey+strconv.FormatInt(userId, 10), strconv.FormatInt(deviceId, 10)).Bytes()
if err != nil && err != redis.Nil {
return nil, gerrors.WrapError(err)
}
if err == redis.Nil {
return nil, nil
}

var device model.Device
err = json.Unmarshal(bytes, &device)
if err != nil {
return nil, gerrors.WrapError(err)
}
return &device, nil
}

func (*authCache) Set(userId, deviceId int64, device model.Device) error {
bytes, err := json.Marshal(device)
if err != nil {
return gerrors.WrapError(err)
}

_, err = db.RedisCli.HSet(AuthKey+strconv.FormatInt(userId, 10), strconv.FormatInt(deviceId, 10), bytes).Result()
if err != nil {
return gerrors.WrapError(err)
}
return nil
}

func (*authCache) GetAll(userId int64) (map[int64]model.Device, error) {
result, err := db.RedisCli.HGetAll(AuthKey + strconv.FormatInt(userId, 10)).Result()
if err != nil {
return nil, gerrors.WrapError(err)
}

var devices = make(map[int64]model.Device, len(result))

for k, v := range result {
deviceId, err := strconv.ParseInt(k, 10, 64)
if err != nil {
return nil, gerrors.WrapError(err)
}

var device model.Device
err = json.Unmarshal(util.Str2bytes(v), &device)
if err != nil {
return nil, gerrors.WrapError(err)
}
devices[deviceId] = device
}
return devices, nil
}

+ 23
- 0
internal/business/domain/user/repo/auth_cache_test.go View File

@@ -0,0 +1,23 @@
package repo

import (
"fmt"
"gim/internal/business/domain/user/model"
"testing"
)

func TestAuthCache_Get(t *testing.T) {
fmt.Println(AuthCache.Get(1, 1))
}

func TestAuthCache_Set(t *testing.T) {
fmt.Println(AuthCache.Set(1, 1, model.Device{
Type: 1,
Token: "111",
Expire: 111,
}))
}

func TestAuthCache_GetAll(t *testing.T) {
fmt.Println(AuthCache.GetAll(1))
}

+ 19
- 0
internal/business/domain/user/repo/auth_repo.go View File

@@ -0,0 +1,19 @@
package repo

import "gim/internal/business/domain/user/model"

type authRepo struct{}

var AuthRepo = new(authRepo)

func (*authRepo) Get(userId, deviceId int64) (*model.Device, error) {
return AuthCache.Get(userId, deviceId)
}

func (*authRepo) Set(userId, deviceId int64, device model.Device) error {
return AuthCache.Set(userId, deviceId, device)
}

func (*authRepo) GetAll(userId int64) (map[int64]model.Device, error) {
return AuthCache.GetAll(userId)
}

+ 25
- 0
internal/business/domain/user/repo/im_package_dao.go View File

@@ -0,0 +1,25 @@
package repo

import (
"gim/internal/business/domain/user/model"
"gim/pkg/db"
"gim/pkg/gerrors"
"github.com/jinzhu/gorm"
)

type imPackageDao struct{}

var ImPackageDao = new(imPackageDao)

// Get 获取套餐包详情
func (*imPackageDao) Get(id int64) (*model.ImPackage, error) {
var imPackage = model.ImPackage{Id: id}
err := db.DB.First(&imPackage).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, gerrors.WrapError(err)
}
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &imPackage, err
}

+ 18
- 0
internal/business/domain/user/repo/im_package_repo.go View File

@@ -0,0 +1,18 @@
package repo

import (
"gim/internal/business/domain/user/model"
)

type imPackageRepo struct{}

var ImPackageRepo = new(imPackageRepo)

// Get 获取单个套餐包
func (*imPackageRepo) Get(id int64) (*model.ImPackage, error) {
imPackage, err := ImPackageDao.Get(id)
if err != nil {
return nil, err
}
return imPackage, err
}

+ 51
- 0
internal/business/domain/user/repo/user_cache.go View File

@@ -0,0 +1,51 @@
package repo

import (
"gim/internal/business/domain/user/model"
"gim/pkg/db"
"gim/pkg/gerrors"
"strconv"
"time"

"github.com/go-redis/redis"
)

const (
UserKey = "user:"
UserExpire = 2 * time.Hour
)

type userCache struct{}

var UserCache = new(userCache)

// Get 获取用户缓存
func (c *userCache) Get(userId int64) (*model.User, error) {
var user model.User
err := db.RedisUtil.Get(UserKey+strconv.FormatInt(userId, 10), &user)
if err != nil && err != redis.Nil {
return nil, gerrors.WrapError(err)
}
if err == redis.Nil {
return nil, nil
}
return &user, nil
}

// Set 设置用户缓存
func (c *userCache) Set(user model.User) error {
err := db.RedisUtil.Set(UserKey+strconv.FormatInt(user.Id, 10), user, UserExpire)
if err != nil {
return gerrors.WrapError(err)
}
return nil
}

// Del 删除用户缓存
func (c *userCache) Del(userId int64) error {
_, err := db.RedisCli.Del(UserKey + strconv.FormatInt(userId, 10)).Result()
if err != nil {
return gerrors.WrapError(err)
}
return nil
}

+ 81
- 0
internal/business/domain/user/repo/user_dao.go View File

@@ -0,0 +1,81 @@
package repo

import (
"gim/internal/business/domain/user/model"
"gim/pkg/db"
"gim/pkg/gerrors"
"time"

"github.com/jinzhu/gorm"
)

type userDao struct{}

var UserDao = new(userDao)

// Add 插入一条用户信息
func (*userDao) Add(user model.User) (int64, error) {
user.CreateTime = time.Now()
user.UpdateTime = time.Now()
err := db.DB.Create(&user).Error
if err != nil {
return 0, gerrors.WrapError(err)
}
return user.Id, nil
}

// Get 获取用户信息
func (*userDao) Get(userId int64) (*model.User, error) {
var user = model.User{Id: userId}
err := db.DB.First(&user).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, gerrors.WrapError(err)
}
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &user, err
}

// Save 保存
func (*userDao) Save(user *model.User) error {
err := db.DB.Save(user).Error
if err != nil {
return gerrors.WrapError(err)
}
return nil
}

// GetByPhoneNumber 根据手机号获取用户信息
func (*userDao) GetByPhoneNumber(phoneNumber string, masterId int64) (*model.User, error) {
var user model.User
err := db.DB.First(&user, "phone_number = ? and master_id = ?", phoneNumber, masterId).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, gerrors.WrapError(err)
}
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &user, err
}

// GetByIds 获取用户信息
func (*userDao) GetByIds(userIds []int64) ([]model.User, error) {
var users []model.User
err := db.DB.Find(&users, "id in (?)", userIds).Error
if err != nil {
return nil, gerrors.WrapError(err)
}
return users, err
}

// Search 查询用户,这里简单实现,生产环境建议使用ES
func (*userDao) Search(key string, masterId int64) ([]model.User, error) {
var users []model.User
key = "%" + key + "%"
err := db.DB.Where("phone_number like ? or nickname like ? ", key, key).Where("master_id = ?", masterId).Find(&users).Error
if err != nil {
return nil, gerrors.WrapError(err)
}
return users, nil
}

+ 44
- 0
internal/business/domain/user/repo/user_dao_test.go View File

@@ -0,0 +1,44 @@
package repo

import (
"fmt"
"gim/internal/business/domain/user/model"
"gim/pkg/db"
"testing"
)

func init() {
fmt.Println("init db")
db.InitByTest()
}

func TestUserDao_Add(t *testing.T) {
id, err := UserDao.Add(model.User{
PhoneNumber: "18829291351",
Nickname: "Alber",
Sex: 1,
AvatarUrl: "AvatarUrl",
Extra: "Extra",
})
fmt.Printf("%+v\n %+v\n ", id, err)
}

func TestUserDao_Get(t *testing.T) {
user, err := UserDao.Get(1)
fmt.Printf("%+v\n %+v\n ", user, err)
}

func TestUserDao_GetByIds(t *testing.T) {
users, err := UserDao.GetByIds([]int64{1, 2, 3})
fmt.Printf("%+v\n %+v\n ", users, err)
}

func TestUserDao_GetByPhoneNumber(t *testing.T) {
user, err := UserDao.GetByPhoneNumber("18829291351", 123456)
fmt.Printf("%+v\n %+v\n ", user, err)
}

func TestUserDao_Search(t *testing.T) {
users, err := UserDao.Search("18229", 123456)
fmt.Printf("%+v\n %+v\n ", users, err)
}

+ 64
- 0
internal/business/domain/user/repo/user_repo.go View File

@@ -0,0 +1,64 @@
package repo

import (
"gim/internal/business/domain/user/model"
)

type userRepo struct{}

var UserRepo = new(userRepo)

// Get 获取单个用户
func (*userRepo) Get(userId int64) (*model.User, error) {
user, err := UserCache.Get(userId)
if err != nil {
return nil, err
}
if user != nil {
return user, nil
}

user, err = UserDao.Get(userId)
if err != nil {
return nil, err
}

if user != nil {
err = UserCache.Set(*user)
if err != nil {
return nil, err
}
}
return user, err
}

func (*userRepo) GetByPhoneNumber(phoneNumber string, masterId int64) (*model.User, error) {
return UserDao.GetByPhoneNumber(phoneNumber, masterId)
}

// GetByIds 获取多个用户
func (*userRepo) GetByIds(userIds []int64) ([]model.User, error) {
return UserDao.GetByIds(userIds)
}

// Search 搜索用户
func (*userRepo) Search(key string, masterId int64) ([]model.User, error) {
return UserDao.Search(key, masterId)
}

// Save 保存用户
func (*userRepo) Save(user *model.User) error {
userId := user.Id
err := UserDao.Save(user)
if err != nil {
return err
}

if userId != 0 {
err = UserCache.Del(user.Id)
if err != nil {
return err
}
}
return nil
}

+ 132
- 0
internal/business/domain/user/service/auth.go View File

@@ -0,0 +1,132 @@
package service

import (
"context"
"errors"
"fmt"
"gim/internal/business/comm/db"
"gim/internal/business/comm/utils"
"gim/internal/business/domain/user/model"
"gim/internal/business/domain/user/repo"
"gim/pkg/gerrors"
"gim/pkg/pb"
"gim/pkg/rpc"
"gim/pkg/util"
"strconv"
"time"
)

type authService struct{}

var AuthService = new(authService)

// SignIn 登录
func (*authService) SignIn(ctx context.Context, phoneNumber, code string, masterId, deviceId int64, pushAlia, nickname, avatarUrl string) (bool, int64, string, int64, error) {
if !Verify(phoneNumber, code) {
return false, 0, "", 0, gerrors.ErrBadCode
}

user, err := repo.UserRepo.GetByPhoneNumber(phoneNumber, masterId)
if err != nil {
return false, 0, "", 0, err
}

var isNew = false
if user == nil {
user = &model.User{
PhoneNumber: phoneNumber,
MasterId: masterId,
Nickname: nickname,
AvatarUrl: avatarUrl,
CreateTime: time.Now(),
UpdateTime: time.Now(),
}
err := repo.UserRepo.Save(user)
if err != nil {
return false, 0, "", 0, err
}
isNew = true
}

resp, err := rpc.GetLogicIntClient().GetDevice(ctx, &pb.GetDeviceReq{DeviceId: deviceId})
if err != nil {
return false, 0, "", 0, err
}

// 方便测试
//token := "0"
token := util.RandString(40)
err = repo.AuthRepo.Set(user.Id, resp.Device.DeviceId, model.Device{
Type: resp.Device.Type,
Token: token,
Expire: time.Now().AddDate(0, 3, 0).Unix(),
})
if err != nil {
return false, 0, "", 0, err
}

if pushAlia != "" {
userPushForJg, err := db.DbUserPushForJg.UserPushForJgGetOne(strconv.FormatInt(user.Id, 10), masterId)
if err != nil {
return false, 0, "", 0, err
}
if userPushForJg == nil {
save := db.DbUserPushForJg.UserPushForJgInsert(user.Id, masterId, pushAlia)
if !save {
return false, 0, "", 0, errors.New("插入user_push_for_jg记录失败")
}
} else {
update := db.DbUserPushForJg.UserPushForJgUpdate(user.Id, masterId, pushAlia)
if !update {
return false, 0, "", 0, errors.New("修改user_push_for_jg记录失败")
}
}
}
return isNew, user.Id, token, masterId, nil
}

func Verify(phoneNumber, code string) bool {
// 假装他成功了
return true
}

// Auth 验证用户是否登录
func (*authService) Auth(ctx context.Context, userId, deviceId int64, token string) error {
device, err := repo.AuthRepo.Get(userId, deviceId)
if err != nil {
return err
}

if device == nil {
return gerrors.ErrUnauthorized
}

if device.Expire < time.Now().Unix() {
return gerrors.ErrUnauthorized
}

if device.Token != token {
return gerrors.ErrUnauthorized
}
return nil
}

// AuthMaster 验证站长权限
func (*authService) AuthMaster(ctx context.Context, masterId string) error {
master, err := repo.MasterRepo.Get(utils.StrToInt64(masterId))
fmt.Println(">>>>>>>>>>>>>>AuthMaster>>>>>>>>>>>>>>>", master, err)

if err != nil {
return err
}
if master == nil {
return gerrors.ErrMasterUnauthorized
}
packageExpireTime, _ := utils.StrToTime(master.PackageExpireTime)
fmt.Println(">>>>>>>>>>>>>>packageExpireTime>>>>>>>>>>>>>>>", packageExpireTime, time.Now().Unix())
if packageExpireTime < time.Now().Unix() {
return gerrors.ErrMasterUnauthorized
}

return nil
}

+ 12
- 0
internal/business/domain/user/service/auth_test.go View File

@@ -0,0 +1,12 @@
package service

import (
"testing"
)

func TestAuthService_SignIn(t *testing.T) {
}

func TestAuthService_Auth(t *testing.T) {

}

+ 32
- 0
internal/connect/api.go View File

@@ -0,0 +1,32 @@
package connect

import (
"context"
"gim/pkg/grpclib"
"gim/pkg/logger"
"gim/pkg/pb"

"go.uber.org/zap"
)

type ConnIntServer struct{}

// DeliverMessage 投递消息
func (s *ConnIntServer) DeliverMessage(ctx context.Context, req *pb.DeliverMessageReq) (*pb.Empty, error) {
resp := &pb.Empty{}

// 获取设备对应的TCP连接
conn := GetConn(req.DeviceId)
if conn == nil {
logger.Logger.Warn("GetConn warn", zap.Int64("device_id", req.DeviceId))
return resp, nil
}

if conn.DeviceId != req.DeviceId {
logger.Logger.Warn("GetConn warn", zap.Int64("device_id", req.DeviceId))
return resp, nil
}

conn.Send(pb.PackageType_PT_MESSAGE, grpclib.GetCtxRequestId(ctx), req.MessageSend, nil)
return resp, nil
}

+ 261
- 0
internal/connect/conn.go View File

@@ -0,0 +1,261 @@
package connect

import (
"container/list"
"context"
"gim/config"
"gim/pkg/grpclib"
"gim/pkg/logger"
"gim/pkg/pb"
"gim/pkg/rpc"
"sync"
"time"

"go.uber.org/zap"

"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"

"github.com/alberliu/gn"
"github.com/gorilla/websocket"
)

const (
CoonTypeTCP int8 = 1 // tcp连接
ConnTypeWS int8 = 2 // websocket连接
)

type Conn struct {
CoonType int8 // 连接类型
TCP *gn.Conn // tcp连接
WSMutex sync.Mutex // WS写锁
WS *websocket.Conn // websocket连接
UserId int64 // 用户ID
DeviceId int64 // 设备ID
RoomId int64 // 订阅的房间ID
Element *list.Element // 链表节点
}

// Write 写入数据
func (c *Conn) Write(bytes []byte) error {
if c.CoonType == CoonTypeTCP {
return c.TCP.WriteWithEncoder(bytes)
} else if c.CoonType == ConnTypeWS {
return c.WriteToWS(bytes)
}
logger.Logger.Error("unknown conn type", zap.Any("conn", c))
return nil
}

// WriteToWS 消息写入WebSocket
func (c *Conn) WriteToWS(bytes []byte) error {
c.WSMutex.Lock()
defer c.WSMutex.Unlock()

err := c.WS.SetWriteDeadline(time.Now().Add(10 * time.Millisecond))
if err != nil {
return err
}
return c.WS.WriteMessage(websocket.BinaryMessage, bytes)
}

// Close 关闭
func (c *Conn) Close() error {
// 取消设备和连接的对应关系
if c.DeviceId != 0 {
DeleteConn(c.DeviceId)
}

// 取消订阅,需要异步出去,防止重复加锁造成死锁
go func() {
SubscribedRoom(c, 0)
}()

if c.DeviceId != 0 {
_, _ = rpc.GetLogicIntClient().Offline(context.TODO(), &pb.OfflineReq{
UserId: c.UserId,
DeviceId: c.DeviceId,
ClientAddr: c.GetAddr(),
})
}

if c.CoonType == CoonTypeTCP {
c.TCP.Close()
} else if c.CoonType == ConnTypeWS {
return c.WS.Close()
}
return nil
}

func (c *Conn) GetAddr() string {
if c.CoonType == CoonTypeTCP {
return c.TCP.GetAddr()
} else if c.CoonType == ConnTypeWS {
return c.WS.RemoteAddr().String()
}
return ""
}

// HandleMessage 消息处理
func (c *Conn) HandleMessage(bytes []byte) {
var input = new(pb.Input)
err := proto.Unmarshal(bytes, input)
if err != nil {
logger.Logger.Error("unmarshal error", zap.Error(err))
return
}
logger.Logger.Debug("HandleMessage", zap.Any("input", input))

// 对未登录的用户进行拦截
if input.Type != pb.PackageType_PT_SIGN_IN && c.UserId == 0 {
// 应该告诉用户没有登录
return
}

switch input.Type {
case pb.PackageType_PT_SIGN_IN:
c.SignIn(input)
case pb.PackageType_PT_SYNC:
c.Sync(input)
case pb.PackageType_PT_HEARTBEAT:
c.Heartbeat(input)
case pb.PackageType_PT_MESSAGE:
c.MessageACK(input)
case pb.PackageType_PT_SUBSCRIBE_ROOM:
c.SubscribedRoom(input)
default:
logger.Logger.Error("handler switch other")
}
}

// Send 下发消息
func (c *Conn) Send(pt pb.PackageType, requestId int64, message proto.Message, err error) {
var output = pb.Output{
Type: pt,
RequestId: requestId,
}

if err != nil {
status, _ := status.FromError(err)
output.Code = int32(status.Code())
output.Message = status.Message()
}

if message != nil {
msgBytes, err := proto.Marshal(message)
if err != nil {
logger.Sugar.Error(err)
return
}
output.Data = msgBytes
}

outputBytes, err := proto.Marshal(&output)
if err != nil {
logger.Sugar.Error(err)
return
}

err = c.Write(outputBytes)
if err != nil {
logger.Sugar.Error(err)
c.Close()
return
}
}

// SignIn 登录
func (c *Conn) SignIn(input *pb.Input) {
var signIn pb.SignInInput
err := proto.Unmarshal(input.Data, &signIn)
if err != nil {
logger.Sugar.Error(err)
return
}

_, err = rpc.GetLogicIntClient().ConnSignIn(grpclib.ContextWithRequestId(context.TODO(), input.RequestId), &pb.ConnSignInReq{
UserId: signIn.UserId,
DeviceId: signIn.DeviceId,
Token: signIn.Token,
ConnAddr: config.LocalAddr,
ClientAddr: c.GetAddr(),
})

c.Send(pb.PackageType_PT_SIGN_IN, input.RequestId, nil, err)
if err != nil {
return
}

c.UserId = signIn.UserId
c.DeviceId = signIn.DeviceId
SetConn(signIn.DeviceId, c)
}

// Sync 消息同步
func (c *Conn) Sync(input *pb.Input) {
var sync pb.SyncInput
err := proto.Unmarshal(input.Data, &sync)
if err != nil {
logger.Sugar.Error(err)
return
}

resp, err := rpc.GetLogicIntClient().Sync(grpclib.ContextWithRequestId(context.TODO(), input.RequestId), &pb.SyncReq{
UserId: c.UserId,
DeviceId: c.DeviceId,
Seq: sync.Seq,
})

var message proto.Message
if err == nil {
message = &pb.SyncOutput{Messages: resp.Messages, HasMore: resp.HasMore}
}
c.Send(pb.PackageType_PT_SYNC, input.RequestId, message, err)
}

// Heartbeat 心跳
func (c *Conn) Heartbeat(input *pb.Input) {
c.Send(pb.PackageType_PT_HEARTBEAT, input.RequestId, nil, nil)

logger.Sugar.Infow("heartbeat", "device_id", c.DeviceId, "user_id", c.UserId)
}

// MessageACK 消息收到回执
func (c *Conn) MessageACK(input *pb.Input) {
var messageACK pb.MessageACK
err := proto.Unmarshal(input.Data, &messageACK)
if err != nil {
logger.Sugar.Error(err)
return
}

_, _ = rpc.GetLogicIntClient().MessageACK(grpclib.ContextWithRequestId(context.TODO(), input.RequestId), &pb.MessageACKReq{
UserId: c.UserId,
DeviceId: c.DeviceId,
DeviceAck: messageACK.DeviceAck,
ReceiveTime: messageACK.ReceiveTime,
})
}

// SubscribedRoom 订阅房间
func (c *Conn) SubscribedRoom(input *pb.Input) {
var subscribeRoom pb.SubscribeRoomInput
err := proto.Unmarshal(input.Data, &subscribeRoom)
if err != nil {
logger.Sugar.Error(err)
return
}

SubscribedRoom(c, subscribeRoom.RoomId)
c.Send(pb.PackageType_PT_SUBSCRIBE_ROOM, input.RequestId, nil, nil)
_, err = rpc.GetLogicIntClient().SubscribeRoom(context.TODO(), &pb.SubscribeRoomReq{
UserId: c.UserId,
DeviceId: c.DeviceId,
RoomId: subscribeRoom.RoomId,
Seq: subscribeRoom.Seq,
ConnAddr: config.LocalAddr,
})
if err != nil {
logger.Logger.Error("SubscribedRoom error", zap.Error(err))
}
}

+ 36
- 0
internal/connect/conn_manager.go View File

@@ -0,0 +1,36 @@
package connect

import (
"gim/pkg/pb"
"sync"
)

var ConnsManager = sync.Map{}

// SetConn 存储
func SetConn(deviceId int64, conn *Conn) {
ConnsManager.Store(deviceId, conn)
}

// GetConn 获取
func GetConn(deviceId int64) *Conn {
value, ok := ConnsManager.Load(deviceId)
if ok {
return value.(*Conn)
}
return nil
}

// DeleteConn 删除
func DeleteConn(deviceId int64) {
ConnsManager.Delete(deviceId)
}

// PushAll 全服推送
func PushAll(message *pb.MessageSend) {
ConnsManager.Range(func(key, value interface{}) bool {
conn := value.(*Conn)
conn.Send(pb.PackageType_PT_MESSAGE, 0, message, nil)
return true
})
}

+ 72
- 0
internal/connect/mq.go View File

@@ -0,0 +1,72 @@
package connect

import (
"gim/config"
"gim/pkg/db"
"gim/pkg/logger"
"gim/pkg/mq"
"gim/pkg/pb"
"time"

"github.com/go-redis/redis"

"go.uber.org/zap"
"google.golang.org/protobuf/proto"
)

// StartSubscribe 启动MQ消息处理逻辑
func StartSubscribe() {
pushRoomPriorityChannel := db.RedisCli.Subscribe(mq.PushRoomPriorityTopic).Channel()
pushRoomChannel := db.RedisCli.Subscribe(mq.PushRoomTopic).Channel()
for i := 0; i < config.PushRoomSubscribeNum; i++ {
go handlePushRoomMsg(pushRoomPriorityChannel, pushRoomChannel)
}

pushAllChannel := db.RedisCli.Subscribe(mq.PushAllTopic).Channel()
for i := 0; i < config.PushAllSubscribeNum; i++ {
go handlePushAllMsg(pushAllChannel)
}
}

func handlePushRoomMsg(priorityChannel, channel <-chan *redis.Message) {
for {
select {
case msg := <-priorityChannel:
handlePushRoom([]byte(msg.Payload))
default:
select {
case msg := <-channel:
handlePushRoom([]byte(msg.Payload))
default:
time.Sleep(100 * time.Millisecond)
continue
}
}
}
}

func handlePushAllMsg(channel <-chan *redis.Message) {
for msg := range channel {
handlePushAll([]byte(msg.Payload))
}
}

func handlePushRoom(bytes []byte) {
var msg pb.PushRoomMsg
err := proto.Unmarshal(bytes, &msg)
if err != nil {
logger.Logger.Error("handlePushRoom error", zap.Error(err))
return
}
PushRoom(msg.RoomId, msg.MessageSend)
}

func handlePushAll(bytes []byte) {
var msg pb.PushAllMsg
err := proto.Unmarshal(bytes, &msg)
if err != nil {
logger.Logger.Error("handlePushRoom error", zap.Error(err))
return
}
PushAll(msg.MessageSend)
}

+ 105
- 0
internal/connect/room.go View File

@@ -0,0 +1,105 @@
package connect

import (
"container/list"
"gim/pkg/pb"
"sync"
)

var RoomsManager sync.Map

// SubscribedRoom 订阅房间
func SubscribedRoom(conn *Conn, roomId int64) {
if roomId == conn.RoomId {
return
}

oldRoomId := conn.RoomId
// 取消订阅
if oldRoomId != 0 {
value, ok := RoomsManager.Load(oldRoomId)
if !ok {
return
}
room := value.(*Room)
room.Unsubscribe(conn)

if room.Conns.Front() == nil {
RoomsManager.Delete(oldRoomId)
}
return
}

// 订阅
if roomId != 0 {
value, ok := RoomsManager.Load(roomId)
var room *Room
if !ok {
room = NewRoom(roomId)
RoomsManager.Store(roomId, room)
} else {
room = value.(*Room)
}
room.Subscribe(conn)
return
}
}

// PushRoom 房间消息推送
func PushRoom(roomId int64, message *pb.MessageSend) {
value, ok := RoomsManager.Load(roomId)
if !ok {
return
}

value.(*Room).Push(message)
}

type Room struct {
RoomId int64 // 房间ID
Conns *list.List // 订阅房间消息的连接
lock sync.RWMutex
}

func NewRoom(roomId int64) *Room {
return &Room{
RoomId: roomId,
Conns: list.New(),
}
}

// Subscribe 订阅房间
func (r *Room) Subscribe(conn *Conn) {
r.lock.Lock()
defer r.lock.Unlock()

conn.Element = r.Conns.PushBack(conn)
conn.RoomId = r.RoomId
}

// Unsubscribe 取消订阅
func (r *Room) Unsubscribe(conn *Conn) {
r.lock.Lock()
defer r.lock.Unlock()

r.Conns.Remove(conn.Element)
conn.Element = nil
conn.RoomId = 0
}

// Push 推送消息到房间
func (r *Room) Push(message *pb.MessageSend) {
r.lock.RLock()
defer r.lock.RUnlock()

element := r.Conns.Front()
for {
conn := element.Value.(*Conn)
conn.Send(pb.PackageType_PT_MESSAGE, 0, message, nil)

element = element.Next()
if element == nil {
break
}
}
}

+ 73
- 0
internal/connect/tcp_server.go View File

@@ -0,0 +1,73 @@
package connect

import (
"context"
"gim/pkg/logger"
"gim/pkg/pb"
"gim/pkg/rpc"
"time"

"go.uber.org/zap"

"github.com/alberliu/gn"
)

var encoder = gn.NewHeaderLenEncoder(2, 1024)

var server *gn.Server

// StartTCPServer 启动TCP服务器
func StartTCPServer(addr string) {
gn.SetLogger(logger.Sugar)

var err error
server, err = gn.NewServer(addr, &handler{},
gn.WithDecoder(gn.NewHeaderLenDecoder(2)),
gn.WithEncoder(gn.NewHeaderLenEncoder(2, 1024)),
gn.WithReadBufferLen(256),
gn.WithTimeout(11*time.Minute),
gn.WithAcceptGNum(10),
gn.WithIOGNum(100))
if err != nil {
logger.Sugar.Error(err)
panic(err)
}

server.Run()
}

type handler struct{}

func (*handler) OnConnect(c *gn.Conn) {
// 初始化连接数据
conn := &Conn{
CoonType: CoonTypeTCP,
TCP: c,
}
c.SetData(conn)
logger.Logger.Debug("connect:", zap.Int32("fd", c.GetFd()), zap.String("addr", c.GetAddr()))
}

func (*handler) OnMessage(c *gn.Conn, bytes []byte) {
conn := c.GetData().(*Conn)
conn.HandleMessage(bytes)
}

func (*handler) OnClose(c *gn.Conn, err error) {
conn, ok := c.GetData().(*Conn)
if !ok || conn == nil {
return
}
logger.Logger.Debug("close", zap.String("addr", c.GetAddr()), zap.Int64("user_id", conn.UserId),
zap.Int64("device_id", conn.DeviceId), zap.Error(err))

DeleteConn(conn.DeviceId)

if conn.UserId != 0 {
_, _ = rpc.GetLogicIntClient().Offline(context.TODO(), &pb.OfflineReq{
UserId: conn.UserId,
DeviceId: conn.DeviceId,
ClientAddr: c.GetAddr(),
})
}
}

+ 87
- 0
internal/connect/ws_server.go View File

@@ -0,0 +1,87 @@
package connect

import (
"gim/pkg/logger"
"gim/pkg/util"
"io"
"net/http"
"strings"
"time"

"go.uber.org/zap"

"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 65536,
CheckOrigin: func(r *http.Request) bool {
return true
},
}

// StartWSServer 启动WebSocket服务器
func StartWSServer(address string) {
http.HandleFunc("/ws", wsHandler)
logger.Logger.Info("websocket server start")
err := http.ListenAndServe(address, nil)
if err != nil {
panic(err)
}
}

func wsHandler(w http.ResponseWriter, r *http.Request) {
wsConn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
logger.Sugar.Error(err)
return
}

conn := &Conn{
CoonType: ConnTypeWS,
WS: wsConn,
}
DoConn(conn)
}

// DoConn 处理连接
func DoConn(conn *Conn) {
defer util.RecoverPanic()

for {
err := conn.WS.SetReadDeadline(time.Now().Add(12 * time.Minute))
if err != nil {
HandleReadErr(conn, err)
return
}
_, data, err := conn.WS.ReadMessage()
if err != nil {
HandleReadErr(conn, err)
return
}

conn.HandleMessage(data)
}
}

// HandleReadErr 读取conn错误
func HandleReadErr(conn *Conn, err error) {
logger.Logger.Debug("read tcp error:", zap.Int64("user_id", conn.UserId),
zap.Int64("device_id", conn.DeviceId), zap.Error(err))
str := err.Error()
// 服务器主动关闭连接
if strings.HasSuffix(str, "use of closed network connection") {
return
}

conn.Close()
// 客户端主动关闭连接或者异常程序退出
if err == io.EOF {
return
}
// SetReadDeadline 之后,超时返回的错误
if strings.HasSuffix(str, "i/o timeout") {
return
}
}

+ 305
- 0
internal/logic/api/logic_ext.go View File

@@ -0,0 +1,305 @@
package api

import (
"context"
"gim/internal/business/comm/utils"
"gim/internal/logic/app"
"gim/pkg/grpclib"
"gim/pkg/pb"
"google.golang.org/protobuf/proto"
)

type LogicExtServer struct{}

func (s *LogicExtServer) SetGroupAddFriend(ctx context.Context, req *pb.SetGroupAddFriendReq) (*pb.Empty, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}
err = app.GroupApp.SetGroupAddFriend(ctx, userId, req)
if err != nil {
return nil, err
}
return &pb.Empty{}, nil
}

func (s *LogicExtServer) SetGroupRemoveBannedMembers(ctx context.Context, in *pb.SetGroupMemberRemoveBannedReq) (*pb.Empty, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

err = app.GroupApp.SetGroupMemberRemoveBanned(ctx, userId, in.GroupId, in.RemoveUserIds)
if err != nil {
return nil, err
}
return &pb.Empty{}, nil
}

// SetGroupBannedMembers 设置禁言
func (s *LogicExtServer) SetGroupBannedMembers(ctx context.Context, in *pb.SetGroupMemberBannedReq) (*pb.SetGroupMemberBannedResp, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

var isAllMemberBanned = false
if in.IsAllMemberBanned == pb.AllMemberBannedType_YES_All_Member_Banned {
isAllMemberBanned = true
}
members, err := app.GroupApp.SetGroupMemberBanned(ctx, userId, in.GroupId, in.UserIds, isAllMemberBanned)
return &pb.SetGroupMemberBannedResp{Members: members, IsAllMemberBanned: in.IsAllMemberBanned}, err
}

// GetGroupBannedMembers 获取群组禁言列表
func (s *LogicExtServer) GetGroupBannedMembers(ctx context.Context, in *pb.GetGroupBannedMembersReq) (*pb.GetGroupBannedMembersResp, error) {
members, err := app.GroupApp.GetBannedMembers(ctx, in.GroupId)
return &pb.GetGroupBannedMembersResp{Members: members}, err
}

func (s *LogicExtServer) SendRedPacket(ctx context.Context, in *pb.SendRedPacketReq) (*pb.SendRedPacketResp, error) {
userId, deviceId, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}
if in.MessageContentBack != "" {
buf, err := proto.Marshal(&pb.Text{
Text: in.MessageContentBack,
})
if err != nil {
return nil, err
}
in.MessageContent = buf
}
sender := pb.Sender{
SenderType: pb.SenderType_ST_USER,
SenderId: userId,
DeviceId: deviceId,
}
seq, err := app.MessageApp.SendRedPackage(ctx, &sender, in)
if err != nil {
return nil, err
}
return &pb.SendRedPacketResp{Seq: seq}, nil
}

func (s *LogicExtServer) RecallMessage(ctx context.Context, in *pb.RecallMessageReq) (*pb.RecallMessageResp, error) {
userId, deviceId, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}
if in.MessageContentBack != "" {
buf, err := proto.Marshal(&pb.RECALL{RecallSeq: utils.StrToInt64(in.MessageContentBack)})
if err != nil {
return nil, err
}
in.MessageContent = buf
}
sender := pb.Sender{
SenderType: pb.SenderType_ST_USER,
SenderId: userId,
DeviceId: deviceId,
}
utils.FilePutContents("RecallMessage", utils.SerializeStr(map[string]interface{}{
"args": in,
}))
seq, err := app.MessageApp.RecallMessage(ctx, &sender, in)
if err != nil {
return nil, err
}
return &pb.RecallMessageResp{Seq: seq}, nil
}

// RegisterDevice 注册设备
func (*LogicExtServer) RegisterDevice(ctx context.Context, in *pb.RegisterDeviceReq) (*pb.RegisterDeviceResp, error) {
deviceId, err := app.DeviceApp.Register(ctx, in)
return &pb.RegisterDeviceResp{DeviceId: deviceId}, err
}

// SendMessage 发送消息
func (*LogicExtServer) SendMessage(ctx context.Context, in *pb.SendMessageReq) (*pb.SendMessageResp, error) {
userId, deviceId, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}
if in.MessageContentBack != "" {
buf, err := proto.Marshal(&pb.Text{
Text: in.MessageContentBack,
})
if err != nil {
return nil, err
}
in.MessageContent = buf
}
sender := pb.Sender{
SenderType: pb.SenderType_ST_USER,
SenderId: userId,
DeviceId: deviceId,
}
seq, err := app.MessageApp.SendMessage(ctx, &sender, in)
if err != nil {
return nil, err
}
return &pb.SendMessageResp{Seq: seq}, nil
}

// PushRoom 推送房间
func (s *LogicExtServer) PushRoom(ctx context.Context, req *pb.PushRoomReq) (*pb.Empty, error) {
userId, deviceId, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}
return &pb.Empty{}, app.RoomApp.Push(ctx, &pb.Sender{
SenderType: pb.SenderType_ST_USER,
SenderId: userId,
DeviceId: deviceId,
}, req)
}

func (s *LogicExtServer) AddFriend(ctx context.Context, in *pb.AddFriendReq) (*pb.Empty, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

err = app.FriendApp.AddFriend(ctx, userId, in.FriendId, in.Remarks, in.Description)
if err != nil {
return nil, err
}

return &pb.Empty{}, nil
}

func (s *LogicExtServer) AgreeAddFriend(ctx context.Context, in *pb.AgreeAddFriendReq) (*pb.Empty, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

err = app.FriendApp.AgreeAddFriend(ctx, userId, in.UserId, in.Remarks)
if err != nil {
return nil, err
}

return &pb.Empty{}, nil
}

func (s *LogicExtServer) DeleteFriend(ctx context.Context, in *pb.DeleteFriendReq) (*pb.Empty, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}
err = app.FriendApp.DeleteFriend(ctx, userId, in.UserId)
if err != nil {
return nil, err
}

return &pb.Empty{}, nil
}

func (s *LogicExtServer) SetFriend(ctx context.Context, req *pb.SetFriendReq) (*pb.SetFriendResp, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

app.FriendApp.SetFriend(ctx, userId, req)
if err != nil {
return nil, err
}
return &pb.SetFriendResp{}, nil
}

func (s *LogicExtServer) GetFriends(ctx context.Context, in *pb.Empty) (*pb.GetFriendsResp, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}
friends, err := app.FriendApp.List(ctx, userId)
return &pb.GetFriendsResp{Friends: friends}, err
}

// CreateGroup 创建群组
func (*LogicExtServer) CreateGroup(ctx context.Context, in *pb.CreateGroupReq) (*pb.CreateGroupResp, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

groupId, err := app.GroupApp.CreateGroup(ctx, userId, in)
return &pb.CreateGroupResp{GroupId: groupId}, err
}

// UpdateGroup 更新群组
func (*LogicExtServer) UpdateGroup(ctx context.Context, in *pb.UpdateGroupReq) (*pb.Empty, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

return &pb.Empty{}, app.GroupApp.Update(ctx, userId, in)
}

// GetGroup 获取群组信息
func (*LogicExtServer) GetGroup(ctx context.Context, in *pb.GetGroupReq) (*pb.GetGroupResp, error) {
group, memberType, userStatusType, err := app.GroupApp.GetGroup(ctx, in.GroupId)
return &pb.GetGroupResp{Group: group, MemberType: memberType, GroupUserStatusType: userStatusType}, err
}

// GetGroups 获取用户加入的所有群组
func (*LogicExtServer) GetGroups(ctx context.Context, in *pb.Empty) (*pb.GetGroupsResp, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

groups, err := app.GroupApp.GetUserGroups(ctx, userId)
return &pb.GetGroupsResp{Groups: groups}, err
}

func (s *LogicExtServer) AddGroupMembers(ctx context.Context, in *pb.AddGroupMembersReq) (*pb.AddGroupMembersResp, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}
masterId, err := grpclib.GetCtxMasterId(ctx)
if err != nil {
return nil, err
}

userIds, err := app.GroupApp.AddMembers(ctx, userId, in.GroupId, in.UserIds, masterId)
return &pb.AddGroupMembersResp{UserIds: userIds}, err
}

// UpdateGroupMember 更新群组成员信息
func (*LogicExtServer) UpdateGroupMember(ctx context.Context, in *pb.UpdateGroupMemberReq) (*pb.Empty, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}
return &pb.Empty{}, app.GroupApp.UpdateMember(ctx, in, userId)
}

// DeleteGroupMember 删除群组成员
func (*LogicExtServer) DeleteGroupMember(ctx context.Context, in *pb.DeleteGroupMemberReq) (*pb.Empty, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

err = app.GroupApp.DeleteMember(ctx, in.GroupId, in.UserId, userId)
return &pb.Empty{}, err
}

// GetGroupMembers 获取群组成员信息
func (s *LogicExtServer) GetGroupMembers(ctx context.Context, in *pb.GetGroupMembersReq) (*pb.GetGroupMembersResp, error) {
members, err := app.GroupApp.GetMembers(ctx, in.GroupId, in.Limit)
utils.FilePutContents("GetGroupMembers", utils.SerializeStr(map[string]interface{}{
"req": map[string]interface{}{
"group_id": in.GroupId,
"limit": in.Limit,
},
"resp": members,
}))
return &pb.GetGroupMembersResp{Members: members}, err
}

+ 257
- 0
internal/logic/api/logic_ext_test.go View File

@@ -0,0 +1,257 @@
package api

import (
"context"
"fmt"
"gim/config"
"gim/internal/logic/domain/message/model"
"gim/pkg/db"
"gim/pkg/pb"
"gim/pkg/util"
"github.com/jinzhu/gorm"
"strconv"
"testing"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/proto"
)

func getLogicExtClient() pb.LogicExtClient {
conn, err := grpc.Dial("im-rpc-logic.izhyin.com:8003", grpc.WithInsecure())
if err != nil {
fmt.Println(err)
return nil
}
return pb.NewLogicExtClient(conn)
}

// deprecated:

func getCtx() context.Context {
token := "ICXKTELAGMMJYXITSIOUNXFHYMTCWJHMJCIRZLPX"
return metadata.NewOutgoingContext(context.TODO(), metadata.Pairs(
"user_id", "2",
"device_id", "5",
"token", token,
"master_id", "123456",
"request_id", strconv.FormatInt(time.Now().UnixNano(), 10)))
}

func TestLogicExtServer_RegisterDevice(t *testing.T) {
resp, err := getLogicExtClient().RegisterDevice(context.TODO(),
&pb.RegisterDeviceReq{
Type: 1,
Brand: "huawei",
Model: "huawei P30",
SystemVersion: "1.0.0",
SdkVersion: "1.0.0",
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_SendMessage(t *testing.T) {
buf, err := proto.Marshal(&pb.Text{
Text: "hello world!",
})
if err != nil {
fmt.Println(err)
return
}
var one = string(buf)
fmt.Printf(one)
resp, err := getLogicExtClient().SendMessage(getCtx(),
&pb.SendMessageReq{
ReceiverType: pb.ReceiverType_RT_USER,
ReceiverId: 21,
ToUserIds: nil,
MessageType: pb.MessageType_MT_TEXT,
MessageContent: buf,
IsPersist: true,
SendTime: util.UnixMilliTime(time.Now()),
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_protoUnmarshal(t *testing.T) {
db.InitMysql(config.MySQL)
var msg model.Message
if err := db.DB.Table("message_000").Where("`id` = 194581").First(&msg).Error; err != nil {
if err == gorm.ErrRecordNotFound {
fmt.Println(err)
return
}
}

var temp pb.RED_PACKAGE
//var temp1 pb.RED_PACKAGE
err := proto.Unmarshal(msg.Content, &temp)
if err != nil {
fmt.Println(err)
return
}

//err = proto.Unmarshal(temp.Data, &temp1)
//if err != nil {
// fmt.Println(err)
// return
//}

fmt.Println(temp)
fmt.Printf("!!!!")
}

func TestLogicExtServer_SendImageMessage(t *testing.T) {
buf, err := proto.Marshal(&pb.Image{
Id: "",
Width: 0,
Height: 0,
Url: "https://img.iplaysoft.com/wp-content/uploads/2019/free-images/free_stock_photo.jpg",
ThumbnailUrl: "",
})
if err != nil {
fmt.Println(err)
return
}
resp, err := getLogicExtClient().SendMessage(getCtx(),
&pb.SendMessageReq{
ReceiverType: pb.ReceiverType_RT_USER,
ReceiverId: 1,
ToUserIds: nil,
MessageType: pb.MessageType_MT_IMAGE,
MessageContent: buf,
IsPersist: true,
SendTime: util.UnixMilliTime(time.Now()),
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_CreateGroup(t *testing.T) {
resp, err := getLogicExtClient().CreateGroup(getCtx(),
&pb.CreateGroupReq{
Name: "10",
Introduction: "10",
Extra: "10",
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_UpdateGroup(t *testing.T) {
resp, err := getLogicExtClient().UpdateGroup(getCtx(),
&pb.UpdateGroupReq{
GroupId: 2,
Name: "11",
Introduction: "11",
Extra: "11",
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_GetGroup(t *testing.T) {
resp, err := getLogicExtClient().GetGroup(getCtx(),
&pb.GetGroupReq{
GroupId: 2,
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_GetUserGroups(t *testing.T) {
resp, err := getLogicExtClient().GetGroups(getCtx(), &pb.Empty{})
if err != nil {
fmt.Println(err)
return
}
// todo 不能获取用户所在的超大群组
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_AddGroupMember(t *testing.T) {
resp, err := getLogicExtClient().AddGroupMembers(getCtx(),
&pb.AddGroupMembersReq{
GroupId: 2,
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_UpdateGroupMember(t *testing.T) {
resp, err := getLogicExtClient().UpdateGroupMember(getCtx(),
&pb.UpdateGroupMemberReq{
GroupId: 2,
UserId: 3,
Remarks: "2",
Extra: "2",
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_DeleteGroupMember(t *testing.T) {
resp, err := getLogicExtClient().DeleteGroupMember(getCtx(),
&pb.DeleteGroupMemberReq{
GroupId: 10,
UserId: 1,
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_GetGroupMembers(t *testing.T) {
resp, err := getLogicExtClient().GetGroupMembers(getCtx(),
&pb.GetGroupMembersReq{
GroupId: 2,
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_AddFriend(t *testing.T) {
resp, err := getLogicExtClient().AddFriend(getCtx(),
&pb.AddFriendReq{
FriendId: 2,
Remarks: "weihan",
Description: "hello",
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

+ 79
- 0
internal/logic/api/logic_int.go View File

@@ -0,0 +1,79 @@
package api

import (
"context"
"gim/internal/logic/app"
"gim/pkg/logger"
"gim/pkg/pb"
)

type LogicIntServer struct{}

// ConnSignIn 设备登录
func (*LogicIntServer) ConnSignIn(ctx context.Context, req *pb.ConnSignInReq) (*pb.Empty, error) {
return &pb.Empty{},
app.DeviceApp.SignIn(ctx, req.UserId, req.DeviceId, req.Token, req.ConnAddr, req.ClientAddr)
}

// Sync 设备同步消息
func (*LogicIntServer) Sync(ctx context.Context, req *pb.SyncReq) (*pb.SyncResp, error) {
return app.MessageApp.Sync(ctx, req.UserId, req.Seq)
}

// MessageACK 设备收到消息ack
func (*LogicIntServer) MessageACK(ctx context.Context, req *pb.MessageACKReq) (*pb.Empty, error) {
return &pb.Empty{}, app.MessageApp.MessageAck(ctx, req.UserId, req.DeviceId, req.DeviceAck)
}

// Offline 设备离线
func (*LogicIntServer) Offline(ctx context.Context, req *pb.OfflineReq) (*pb.Empty, error) {
return &pb.Empty{}, app.DeviceApp.Offline(ctx, req.DeviceId, req.ClientAddr)
}

func (s *LogicIntServer) SubscribeRoom(ctx context.Context, req *pb.SubscribeRoomReq) (*pb.Empty, error) {
return &pb.Empty{}, app.RoomApp.SubscribeRoom(ctx, req)
}

// SendMessage 发送消息
func (*LogicIntServer) SendMessage(ctx context.Context, req *pb.SendMessageReq) (*pb.SendMessageResp, error) {
sender := pb.Sender{
SenderType: pb.SenderType_ST_BUSINESS,
SenderId: 0,
DeviceId: 0,
}

seq, err := app.MessageApp.SendMessage(ctx, &sender, req)
if err != nil {
return nil, err
}
return &pb.SendMessageResp{Seq: seq}, nil
}

// PushRoom 推送房间
func (s *LogicIntServer) PushRoom(ctx context.Context, req *pb.PushRoomReq) (*pb.Empty, error) {
return &pb.Empty{}, app.RoomApp.Push(ctx, &pb.Sender{
SenderType: pb.SenderType_ST_BUSINESS,
}, req)
}

// PushAll 全服推送
func (s *LogicIntServer) PushAll(ctx context.Context, req *pb.PushAllReq) (*pb.Empty, error) {
return &pb.Empty{}, app.MessageApp.PushAll(ctx, req)
}

// GetDevice 获取设备信息
func (*LogicIntServer) GetDevice(ctx context.Context, req *pb.GetDeviceReq) (*pb.GetDeviceResp, error) {
device, err := app.DeviceApp.GetDevice(ctx, req.DeviceId)
return &pb.GetDeviceResp{Device: device}, err
}

// ServerStop 服务停止
func (s *LogicIntServer) ServerStop(ctx context.Context, in *pb.ServerStopReq) (*pb.Empty, error) {
go func() {
err := app.DeviceApp.ServerStop(ctx, in.ConnAddr)
if err != nil {
logger.Sugar.Error(err)
}
}()
return &pb.Empty{}, nil
}

+ 128
- 0
internal/logic/api/logic_int_test.go View File

@@ -0,0 +1,128 @@
package api

import (
"context"
"fmt"
"gim/pkg/logger"
"gim/pkg/pb"
"gim/pkg/util"
"testing"
"time"

"google.golang.org/protobuf/proto"

"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)

func getLogicIntClient() pb.LogicIntClient {
conn, err := grpc.Dial("111.229.238.28:50000", grpc.WithInsecure())
if err != nil {
logger.Sugar.Error(err)
return nil
}
return pb.NewLogicIntClient(conn)
}

func TestLogicIntServer_SignIn(t *testing.T) {
token := ""

resp, err := getLogicIntClient().ConnSignIn(context.TODO(),
&pb.ConnSignInReq{
DeviceId: 1,
UserId: 1,
Token: token,
ConnAddr: "127.0.0.1:5000",
})
if err != nil {
logger.Sugar.Error(err)
return
}
logger.Sugar.Info(resp)
}

func TestLogicIntServer_Sync(t *testing.T) {
resp, err := getLogicIntClient().Sync(metadata.NewOutgoingContext(context.TODO(), metadata.Pairs("key", "val")),
&pb.SyncReq{
UserId: 1,
DeviceId: 1,
Seq: 0,
})
if err != nil {
logger.Sugar.Error(err)
return
}
logger.Sugar.Info(resp)
}

func TestLogicIntServer_MessageACK(t *testing.T) {
resp, err := getLogicIntClient().MessageACK(metadata.NewOutgoingContext(context.TODO(), metadata.Pairs("key", "val")),
&pb.MessageACKReq{
UserId: 1,
DeviceId: 1,
DeviceAck: 1,
ReceiveTime: 1,
})
if err != nil {
logger.Sugar.Error(err)
return
}
logger.Sugar.Info(resp)
}

func TestLogicIntServer_Offline(t *testing.T) {
resp, err := getLogicIntClient().Offline(metadata.NewOutgoingContext(context.TODO(), metadata.Pairs("key", "val")),
&pb.OfflineReq{
UserId: 1,
DeviceId: 1,
})
if err != nil {
logger.Sugar.Error(err)
return
}
logger.Sugar.Info(resp)
}

func TestLogicIntServer_PushRoom(t *testing.T) {
buf, err := proto.Marshal(&pb.Text{
Text: "hello alber ",
})
if err != nil {
fmt.Println(err)
return
}
resp, err := getLogicIntClient().PushRoom(getCtx(),
&pb.PushRoomReq{
RoomId: 1,
MessageType: pb.MessageType_MT_TEXT,
MessageContent: buf,
SendTime: util.UnixMilliTime(time.Now()),
IsPersist: true,
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicIntServer_PushAll(t *testing.T) {
buf, err := proto.Marshal(&pb.Text{
Text: "hello alber ",
})
if err != nil {
fmt.Println(err)
return
}
resp, err := getLogicIntClient().PushAll(getCtx(),
&pb.PushAllReq{
MessageType: pb.MessageType_MT_TEXT,
MessageContent: buf,
SendTime: util.UnixMilliTime(time.Now()),
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

+ 85
- 0
internal/logic/app/device_app.go View File

@@ -0,0 +1,85 @@
package app

import (
"context"
devicedomain "gim/internal/logic/domain/device"
"gim/pkg/gerrors"
"gim/pkg/pb"
)

type deviceApp struct{}

var DeviceApp = new(deviceApp)

// Register 注册设备
func (*deviceApp) Register(ctx context.Context, in *pb.RegisterDeviceReq) (int64, error) {
device := devicedomain.Device{
Type: in.Type,
Brand: in.Brand,
Model: in.Model,
SystemVersion: in.SystemVersion,
SDKVersion: in.SdkVersion,
}

// 判断设备信息是否合法
if !device.IsLegal() {
return 0, gerrors.ErrBadRequest
}

err := devicedomain.DeviceRepo.Save(&device)
if err != nil {
return 0, err
}

return device.Id, nil
}

// SignIn 登录
func (*deviceApp) SignIn(ctx context.Context, userId, deviceId int64, token string, connAddr string, clientAddr string) error {
return devicedomain.DeviceService.SignIn(ctx, userId, deviceId, token, connAddr, clientAddr)
}

// Offline 设备离线
func (*deviceApp) Offline(ctx context.Context, deviceId int64, clientAddr string) error {
device, err := devicedomain.DeviceRepo.Get(deviceId)
if err != nil {
return err
}
if device == nil {
return nil
}

if device.ClientAddr != clientAddr {
return nil
}
device.Status = devicedomain.DeviceOffLine

err = devicedomain.DeviceRepo.Save(device)
if err != nil {
return err
}
return nil
}

// ListOnlineByUserId 获取用户所有在线设备
func (*deviceApp) ListOnlineByUserId(ctx context.Context, userId int64) ([]*pb.Device, error) {
return devicedomain.DeviceService.ListOnlineByUserId(ctx, userId)
}

// GetDevice 获取设备信息
func (*deviceApp) GetDevice(ctx context.Context, deviceId int64) (*pb.Device, error) {
device, err := devicedomain.DeviceRepo.Get(deviceId)
if err != nil {
return nil, err
}
if device == nil {
return nil, gerrors.ErrDeviceNotExist
}

return device.ToProto(), err
}

// ServerStop connect服务停止
func (*deviceApp) ServerStop(ctx context.Context, connAddr string) error {
return devicedomain.DeviceService.ServerStop(ctx, connAddr)
}

+ 68
- 0
internal/logic/app/friend_app.go View File

@@ -0,0 +1,68 @@
package app

import (
"context"
frienddomain "gim/internal/logic/domain/friend"
"gim/pkg/pb"
"time"
)

type friendApp struct{}

var FriendApp = new(friendApp)

// List 获取好友列表
func (s *friendApp) List(ctx context.Context, userId int64) ([]*pb.Friend, error) {
return frienddomain.FriendService.List(ctx, userId)
}

// AddFriend 添加好友
func (*friendApp) AddFriend(ctx context.Context, userId, friendId int64, remarks, description string) error {
return frienddomain.FriendService.AddFriend(ctx, userId, friendId, remarks, description)
}

// DeleteFriend 添加好友
func (*friendApp) DeleteFriend(ctx context.Context, userId, friendId int64) error {
return frienddomain.FriendService.DeleteFriend(ctx, userId, friendId)
}

// AgreeAddFriend 同意添加好友
func (*friendApp) AgreeAddFriend(ctx context.Context, userId, friendId int64, remarks string) error {
return frienddomain.FriendService.AgreeAddFriend(ctx, userId, friendId, remarks)
}

// SetFriend 设置好友信息
func (*friendApp) SetFriend(ctx context.Context, userId int64, req *pb.SetFriendReq) error {
friend, err := frienddomain.FriendRepo.Get(userId, req.FriendId)
if err != nil {
return err
}
if friend == nil {
return nil
}

friend.Remarks = req.Remarks
friend.Extra = req.Extra
friend.UpdateTime = time.Now()

err = frienddomain.FriendRepo.Save(friend)
if err != nil {
return err
}
return nil
}

// SendToFriend 消息发送至好友
func (*friendApp) SendToFriend(ctx context.Context, sender *pb.Sender, req *pb.SendMessageReq) (int64, error) {
return frienddomain.FriendService.SendToFriend(ctx, sender, req)
}

// RecallMessageSendToFriend 撤回消息发送至好友
func (*friendApp) RecallMessageSendToFriend(ctx context.Context, sender *pb.Sender, req *pb.RecallMessageReq) (int64, error) {
return frienddomain.FriendService.RecallMessageSendToFriend(ctx, sender, req)
}

// RedPackageMessageSendToFriend 红包发送至好友
func (*friendApp) RedPackageMessageSendToFriend(ctx context.Context, sender *pb.Sender, req *pb.SendRedPacketReq) (int64, error) {
return frienddomain.FriendService.RedPackageMessageSendToFriend(ctx, sender, req)
}

+ 427
- 0
internal/logic/app/group_app.go View File

@@ -0,0 +1,427 @@
package app

import (
"context"
"database/sql"
"errors"
"gim/internal/business/comm/utils"
repo2 "gim/internal/business/domain/user/repo"
"gim/internal/logic/domain/group/model"
"gim/internal/logic/domain/group/repo"
"gim/pkg/db"
"gim/pkg/grpclib"
"gim/pkg/pb"
"strings"
)

type groupApp struct{}

var GroupApp = new(groupApp)

// CreateGroup 创建群组
func (*groupApp) CreateGroup(ctx context.Context, userId int64, in *pb.CreateGroupReq) (int64, error) {
masterId, err := grpclib.GetCtxMasterId(ctx)
if err != nil {
return 0, err
}
group := model.CreateGroup(userId, utils.StrToInt64(masterId), in)
err = repo.GroupRepo.Save(group)
if err != nil {
return 0, err
}
in.MemberIds = append(in.MemberIds, userId)
err = group.PushAddMember(ctx, userId, in.MemberIds, false)
return group.Id, nil
}

// GetGroup 获取群组信息
func (*groupApp) GetGroup(ctx context.Context, groupId int64) (*pb.Group, pb.MemberType, pb.GroupUserStatusType, error) {
group, err := repo.GroupRepo.Get(groupId)
memberType := pb.MemberType_GMT_UNKNOWN
userStatusType := pb.GroupUserStatusType_GROUP_USER_STATUS_NORMAL
if err != nil {
return nil, memberType, userStatusType, err
}
masterId, err := grpclib.GetCtxMasterId(ctx)
if err != nil {
return nil, memberType, userStatusType, err
}
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, memberType, userStatusType, err
}
if !strings.Contains(group.Name, "官方") && !strings.Contains(group.Name, "运营中心") && masterId == "68823769" {
for _, member := range group.Members {
if member.UserId == userId && member.MemberType != int(pb.MemberType_GMT_ADMIN) {
group.Name = "我的推荐人群"
}
}
}
for _, member := range group.Members {
if member.UserId == userId {
memberType = pb.MemberType(member.MemberType)
userStatusType = pb.GroupUserStatusType(member.Status)
}
}
return group.ToProto(), memberType, userStatusType, nil
}

// GetUserGroups 获取用户加入的群组列表
func (*groupApp) GetUserGroups(ctx context.Context, userId int64) ([]*pb.Group, error) {
masterId, err := grpclib.GetCtxMasterId(ctx)
if err != nil {
return nil, err
}
//user, err := repo2.UserRepo.Get(userId)
//if err != nil {
// return nil, err
//}

groups, err := repo.GroupUserRepo.ListByUserId(userId)
if err != nil {
return nil, err
}

pbGroups := make([]*pb.Group, len(groups))
for i := range groups {
pbGroups[i] = groups[i].ToProto()
//if !strings.Contains(pbGroups[i].Introduction, user.PhoneNumber) && !strings.Contains(pbGroups[i].Name, "官方") && !strings.Contains(pbGroups[i].Name, "运营中心") && masterId == "68823769" {
// pbGroups[i].Name = "我的推荐人群"
//}
groupUser, err := repo.GroupUserRepo.Get(pbGroups[i].GroupId, userId)
if err != nil {
return nil, err
}
if !strings.Contains(pbGroups[i].Name, "官方") && !strings.Contains(pbGroups[i].Name, "运营中心") && masterId == "68823769" {
if groupUser.MemberType != int(pb.MemberType_GMT_ADMIN) {
pbGroups[i].Name = "我的推荐人群"
}
}
}
return pbGroups, nil
}

// Update 更新群组
func (*groupApp) Update(ctx context.Context, userId int64, update *pb.UpdateGroupReq) error {
group, err := repo.GroupRepo.Get(update.GroupId)
if err != nil {
return err
}
var isUpdateIntroduction = false
if group.Introduction != update.Introduction {
isUpdateIntroduction = true
}
err = group.Update(ctx, update)
if err != nil {
return err
}

err = repo.GroupRepo.Save(group)
if err != nil {
return err
}

err = group.PushUpdate(ctx, userId, isUpdateIntroduction)
if err != nil {
return err
}

return nil
}

// AddMembers 添加群组成员
func (*groupApp) AddMembers(ctx context.Context, userId, groupId int64, userIds []int64, masterId string) ([]int64, error) {
group, err := repo.GroupRepo.Get(groupId)
if err != nil {
return nil, err
}
master, err := repo2.MasterRepo.Get(utils.StrToInt64(masterId))
if err != nil {
return nil, err
}
imPackage, err := repo2.ImPackageRepo.Get(master.PackageId)
if err != nil {
return nil, err
}
if imPackage.GroupNum < int64(group.UserNum)+int64(len(userIds)) {
return nil, errors.New("群人员数量已满!")
}

existIds, addedIds, err := group.AddMembers(ctx, userIds)
if err != nil {
return nil, err
}
err = repo.GroupRepo.Save(group)
if err != nil {
return nil, err
}

err = group.PushAddMember(ctx, userId, addedIds, true)
if err != nil {
return nil, err
}
return existIds, nil
}

// UpdateMember 更新群组用户
func (*groupApp) UpdateMember(ctx context.Context, in *pb.UpdateGroupMemberReq, optId int64) error {
group, err := repo.GroupRepo.Get(in.GroupId)
if err != nil {
return err
}
err = group.UpdateMember(ctx, in)
if err != nil {
return err
}
err = repo.GroupRepo.Save(group)
if err != nil {
return err
}

err = group.PushUpdateMember(ctx, optId, in.UserId, int32(in.MemberType))
if err != nil {
return err
}
return nil
}

// DeleteMember 删除群组成员
func (*groupApp) DeleteMember(ctx context.Context, groupId int64, userId int64, optId int64) error {
group, err := repo.GroupRepo.Get(groupId)
if err != nil {
return err
}
err = group.DeleteMember(ctx, userId)
if err != nil {
return err
}

group.UserNum-- //群组人数减一
err = repo.GroupRepo.Save(group)
if err != nil {
return err
}

if userId == optId { //自己退出群聊,无需发送消息
return nil
}
err = group.PushDeleteMember(ctx, optId, userId)
if err != nil {
return err
}
return nil
}

// GetMembers 获取群组成员
func (*groupApp) GetMembers(ctx context.Context, groupId, limit int64) ([]*pb.GroupMember, error) {
if limit == 0 {
group, err := repo.GroupRepo.Get(groupId)
if err != nil {
return nil, err
}
return group.GetMembers(ctx)
} else {
group, err := repo.GroupRepo.GetLimit(groupId, limit)
if err != nil {
return nil, err
}
return group.GetMembers(ctx)
}
}

// SendMessage 发送群组消息
func (*groupApp) SendMessage(ctx context.Context, sender *pb.Sender, req *pb.SendMessageReq) (int64, error) {
group, err := repo.GroupRepo.Get(req.ReceiverId)
if err != nil {
return 0, err
}

return group.SendMessage(ctx, sender, req)
}

// GetBannedMembers 获取群组禁言成员
func (*groupApp) GetBannedMembers(ctx context.Context, groupId int64) ([]*pb.GroupMember, error) {
// 查询被禁言的成员列表
group, err := repo.GroupRepo.GetForBanned(groupId)
if err != nil {
return nil, err
}
return group.GetMembers(ctx)
}

// SetGroupMemberBanned 设置禁言
func (*groupApp) SetGroupMemberBanned(ctx context.Context, optId, groupId int64, userIds []int64, isAllMemberBanned bool) ([]*pb.GroupMember, error) {
groupUser, err := repo.GroupUserRepo.Get(groupId, optId)
if err != nil {
return nil, err
}
if groupUser == nil {
return nil, errors.New("未查询到群组用户信息")
}
if groupUser.MemberType != int(pb.MemberType_GMT_ADMIN) && groupUser.MemberType != int(pb.MemberType_GMT_MANAGE) {
return nil, errors.New("非管理员操作")
}

group, err := repo.GroupRepo.Get(groupId)
if err != nil {
return nil, err
}
if isAllMemberBanned {
//设置全员禁言
//1、更新 `group` 的 is_all_member_banned 状态
group.IsAllMemberBanned = int32(pb.AllMemberBannedType_YES_All_Member_Banned)
err := repo.GroupRepo.Save(group)
if err != nil {
return nil, err
}

//2、发送推送消息
err = group.PushGroupMemberBanned(ctx, optId, 0, isAllMemberBanned)
if err != nil {
return nil, err
}
} else {
//设置成员禁言
//1、更新 `group` 的 is_all_member_banned 状态
if group.IsAllMemberBanned != int32(pb.AllMemberBannedType_NOT_All_Member_Banned) {
group.IsAllMemberBanned = int32(pb.AllMemberBannedType_NOT_All_Member_Banned)
err := repo.GroupRepo.Save(group)
if err != nil {
return nil, err
}
//TODO::推送消息
err = group.PushGroupMemberBanned(ctx, optId, -1, isAllMemberBanned)
if err != nil {
return nil, err
}
}

//2、更新 `group_user` 的 status 状态
if len(userIds) == 0 {
db.DB.Model(model.GroupUser{}).Where("group_id = ?", groupId).Updates(model.GroupUserV2{Status: sql.NullInt32{Int32: int32(pb.GroupUserStatusType_GROUP_USER_STATUS_NORMAL), Valid: true}})
} else {
db.DB.Model(model.GroupUser{}).Where("user_id in (?) and group_id = ?", userIds, groupId).Updates(model.GroupUserV2{Status: sql.NullInt32{Int32: int32(pb.GroupUserStatusType_GROUP_USER_STATUS_Banned), Valid: true}})
db.DB.Model(model.GroupUser{}).Where("(user_id) not in (?) and group_id = ?", userIds, groupId).Updates(model.GroupUserV2{Status: sql.NullInt32{Int32: int32(pb.GroupUserStatusType_GROUP_USER_STATUS_NORMAL), Valid: true}})
}

//3、删除缓存
err := repo.GroupCache.Del(groupId)
if err != nil {
return nil, err
}

//4、发送推送消息
for _, u := range userIds {
err = group.PushGroupMemberBanned(ctx, optId, u, isAllMemberBanned)
if err != nil {
return nil, err
}
}
}

// 查询被禁言的成员列表
group, err = repo.GroupRepo.GetForBanned(groupId)
if err != nil {
return nil, err
}
return group.GetMembers(ctx)
}

// SetGroupMemberRemoveBanned 设置取消禁言
func (*groupApp) SetGroupMemberRemoveBanned(ctx context.Context, optId, groupId int64, removeUserIds []int64) error {
groupUser, err := repo.GroupUserRepo.Get(groupId, optId)
if err != nil {
return err
}
if groupUser == nil {
return errors.New("未查询到群组用户信息")
}
if groupUser.MemberType != int(pb.MemberType_GMT_ADMIN) && groupUser.MemberType != int(pb.MemberType_GMT_MANAGE) {
return errors.New("非管理员操作")
}

group, err := repo.GroupRepo.Get(groupId)
if err != nil {
return err
}

if len(removeUserIds) > 0 {
//1、更新 `group_user` 的 status 状态
db.DB.Model(model.GroupUser{}).Where("user_id in (?) and group_id = ?", removeUserIds, groupId).Updates(model.GroupUserV2{Status: sql.NullInt32{Int32: int32(pb.GroupUserStatusType_GROUP_USER_STATUS_NORMAL), Valid: true}})

//2、删除缓存
err := repo.GroupCache.Del(groupId)
if err != nil {
return err
}

//3、给取消禁言用户推送消息
for _, u := range removeUserIds {
err = group.PushGroupMemberRemoveBanned(ctx, optId, u)
if err != nil {
return err
}
}
}

return nil
}

// RecallSendMessage 撤回发送消息
func (*groupApp) RecallSendMessage(ctx context.Context, sender *pb.Sender, req *pb.RecallMessageReq) (int64, error) {
group, err := repo.GroupRepo.Get(req.ReceiverId)
if err != nil {
return 0, err
}

return group.RecallSendMessage(ctx, sender, req)
}

// SendRedPackage 发送红包消息
func (*groupApp) SendRedPackage(ctx context.Context, sender *pb.Sender, req *pb.SendRedPacketReq) (int64, error) {
group, err := repo.GroupRepo.Get(req.ReceiverId)
if err != nil {
return 0, err
}

return group.SendRedPackage(ctx, sender, req)
}

// SetGroupAddFriend 设置是否加好友
func (*groupApp) SetGroupAddFriend(ctx context.Context, optId int64, req *pb.SetGroupAddFriendReq) error {
//1、判断是否为管理员操作
groupUser, err := repo.GroupUserRepo.Get(req.GroupId, optId)
if err != nil {
return err
}
if groupUser == nil {
return errors.New("未查询到群组用户信息")
}
if groupUser.MemberType != int(pb.MemberType_GMT_ADMIN) && groupUser.MemberType != int(pb.MemberType_GMT_MANAGE) {
return errors.New("非管理员操作")
}

//group, err := repo.GroupRepo.Get(req.GroupId)
//if err != nil {
// return err
//}

//2、更改群组信息
db.DB.Model(model.Group{}).Where("id = ?", req.GroupId).Updates(model.Group{IsAllAddFriend: int32(req.IsAllAddFriend)})

//3、删除缓存
err = repo.GroupCache.Del(req.GroupId)
if err != nil {
return err
}

////4、发送推送消息
//for _, u := range userIds {
// err = group.PushGroupMemberBanned(ctx, optId, u, isAllMemberBanned)
// if err != nil {
// return nil, err
// }
//}

return nil
}

+ 110
- 0
internal/logic/app/message_app.go View File

@@ -0,0 +1,110 @@
package app

import (
"context"
"gim/internal/logic/domain/message/service"
"gim/pkg/pb"
"time"

"google.golang.org/protobuf/proto"
)

type messageApp struct{}

var MessageApp = new(messageApp)

// SendToUser 发送消息给用户
func (*messageApp) SendToUser(ctx context.Context, sender *pb.Sender, toUserId int64, req *pb.SendMessageReq) (int64, error) {
return service.MessageService.SendToUser(ctx, sender, toUserId, req)
}

// RecallMessageSendToUser 撤回消息发送给用户
func (*messageApp) RecallMessageSendToUser(ctx context.Context, sender *pb.Sender, toUserId int64, req *pb.RecallMessageReq, sendTime time.Time) (int64, error) {
return service.MessageService.RecallMessageSendToUser(ctx, sender, toUserId, req, sendTime)
}

// SendRedPackageToUser 发送红包给用户
func (*messageApp) SendRedPackageToUser(ctx context.Context, sender *pb.Sender, toUserId int64, req *pb.SendRedPacketReq) (int64, error) {
return service.MessageService.SendRedPackageToUser(ctx, sender, toUserId, req)
}

// PushToUser 推送消息给用户
func (*messageApp) PushToUser(ctx context.Context, userId int64, code pb.PushCode, message proto.Message, isPersist bool) error {
return service.PushService.PushToUser(ctx, userId, code, message, isPersist)
}

// PushAll 全服推送
func (*messageApp) PushAll(ctx context.Context, req *pb.PushAllReq) error {
return service.PushService.PushAll(ctx, req)
}

// Sync 消息同步
func (*messageApp) Sync(ctx context.Context, userId, seq int64) (*pb.SyncResp, error) {
return service.MessageService.Sync(ctx, userId, seq)
}

// MessageAck 收到消息回执
func (*messageApp) MessageAck(ctx context.Context, userId, deviceId, ack int64) error {
return service.DeviceAckService.Update(ctx, userId, deviceId, ack)
}

// SendMessage 发送消息
func (s *messageApp) SendMessage(ctx context.Context, sender *pb.Sender, req *pb.SendMessageReq) (int64, error) {
// 如果发送者是用户,需要补充用户的信息
service.MessageService.AddSenderInfo(sender)

switch req.ReceiverType {
// 消息接收者为用户
case pb.ReceiverType_RT_USER:
// 发送者为用户
if sender.SenderType == pb.SenderType_ST_USER {
return FriendApp.SendToFriend(ctx, sender, req)
} else {
return s.SendToUser(ctx, sender, req.ReceiverId, req)
}
// 消息接收者是群组
case pb.ReceiverType_RT_GROUP:
return GroupApp.SendMessage(ctx, sender, req)
}
return 0, nil
}

// RecallMessage 撤回消息
func (s *messageApp) RecallMessage(ctx context.Context, sender *pb.Sender, req *pb.RecallMessageReq) (int64, error) {
// 如果发送者是用户,需要补充用户的信息
service.MessageService.AddSenderInfo(sender)
switch req.ReceiverType {
// 消息接收者为用户
case pb.ReceiverType_RT_USER:
// 发送者为用户
if sender.SenderType == pb.SenderType_ST_USER {
return FriendApp.RecallMessageSendToFriend(ctx, sender, req)
} else {
//return s.RecallMessageSendToUser(ctx, sender, req.ReceiverId, req, true)
}
// 消息接收者是群组
case pb.ReceiverType_RT_GROUP:
return GroupApp.RecallSendMessage(ctx, sender, req)
}
return 0, nil
}

// SendRedPackage 发送红包消息
func (s *messageApp) SendRedPackage(ctx context.Context, sender *pb.Sender, req *pb.SendRedPacketReq) (int64, error) {
// 如果发送者是用户,需要补充用户的信息
service.MessageService.AddSenderInfo(sender)
switch req.ReceiverType {
// 消息接收者为用户
case pb.ReceiverType_RT_USER:
// 发送者为用户
if sender.SenderType == pb.SenderType_ST_USER {
return FriendApp.RedPackageMessageSendToFriend(ctx, sender, req)
} else {
return s.SendRedPackageToUser(ctx, sender, req.ReceiverId, req)
}
// 消息接收者是群组
case pb.ReceiverType_RT_GROUP:
return GroupApp.SendRedPackage(ctx, sender, req)
}
return 0, nil
}

+ 21
- 0
internal/logic/app/room_app.go View File

@@ -0,0 +1,21 @@
package app

import (
"context"
"gim/internal/logic/domain/room"
"gim/pkg/pb"
)

type roomApp struct{}

var RoomApp = new(roomApp)

// Push 推送房间消息
func (s *roomApp) Push(ctx context.Context, sender *pb.Sender, req *pb.PushRoomReq) error {
return room.RoomService.Push(ctx, sender, req)
}

// SubscribeRoom 订阅房间
func (s *roomApp) SubscribeRoom(ctx context.Context, req *pb.SubscribeRoomReq) error {
return room.RoomService.SubscribeRoom(ctx, req)
}

+ 66
- 0
internal/logic/domain/device/device.go View File

@@ -0,0 +1,66 @@
package device

import (
"gim/pkg/pb"
"time"
)

const (
DeviceOnLine = 1 // 设备在线
DeviceOffLine = 0 // 设备离线
)

// Device 设备
type Device struct {
Id int64 // 设备id
UserId int64 // 用户id
Type int32 // 设备类型,1:Android;2:IOS;3:Windows; 4:MacOS;5:Web
Brand string // 手机厂商
Model string // 机型
SystemVersion string // 系统版本
SDKVersion string // SDK版本
Status int32 // 在线状态,0:离线;1:在线
ConnAddr string // 连接层服务层地址
ClientAddr string // 客户端地址
CreateTime time.Time // 创建时间
UpdateTime time.Time // 更新时间
}

func (d *Device) ToProto() *pb.Device {
return &pb.Device{
DeviceId: d.Id,
UserId: d.UserId,
Type: d.Type,
Brand: d.Brand,
Model: d.Model,
SystemVersion: d.SystemVersion,
SdkVersion: d.SDKVersion,
Status: d.Status,
ConnAddr: d.ConnAddr,
ClientAddr: d.ClientAddr,
CreateTime: d.CreateTime.Unix(),
UpdateTime: d.UpdateTime.Unix(),
}
}

func (d *Device) IsLegal() bool {
if d.Type == 0 || d.Brand == "" || d.Model == "" ||
d.SystemVersion == "" || d.SDKVersion == "" {
return false
}
return true
}

func (d *Device) Online(userId int64, connAddr string, clientAddr string) {
d.UserId = userId
d.ConnAddr = connAddr
d.ClientAddr = clientAddr
d.Status = DeviceOnLine
}

func (d *Device) Offline(userId int64, connAddr string, clientAddr string) {
d.UserId = userId
d.ConnAddr = connAddr
d.ClientAddr = clientAddr
d.Status = DeviceOnLine
}

+ 78
- 0
internal/logic/domain/device/device_dao.go View File

@@ -0,0 +1,78 @@
package device

import (
"gim/internal/business/domain/user/model"
"gim/pkg/db"
"gim/pkg/gerrors"
"time"

"github.com/jinzhu/gorm"
)

type deviceDao struct{}

var DeviceDao = new(deviceDao)

// Save 插入一条设备信息
func (*deviceDao) Save(device *Device) error {
device.CreateTime = time.Now()
device.UpdateTime = time.Now()
err := db.DB.Save(&device).Error
if err != nil {
return gerrors.WrapError(err)
}
return nil
}

// Get 获取设备
func (*deviceDao) Get(deviceId int64) (*Device, error) {
var device = Device{Id: deviceId}
err := db.DB.First(&device).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, gerrors.WrapError(err)
}
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &device, nil
}

// ListOnlineByUserId 查询用户所有的在线设备
func (*deviceDao) ListOnlineByUserId(userId int64) ([]Device, error) {
var devices []Device
err := db.DB.Find(&devices, "user_id = ? and status = ?", userId, DeviceOnLine).Error
if err != nil {
return nil, gerrors.WrapError(err)
}
return devices, nil
}

// ListOffLineByUserId 查询用户所有的离线设备
func (*deviceDao) ListOffLineByUserId(userId int64) ([]Device, error) {
var devices []Device
err := db.DB.Find(&devices, "user_id = ? and status = ?", userId, DeviceOnLine).Error
if err != nil {
return nil, gerrors.WrapError(err)
}
return devices, nil
}

// ListOnlineByConnAddr 查询用户所有的在线设备
func (*deviceDao) ListOnlineByConnAddr(connAddr string) ([]Device, error) {
var devices []Device
err := db.DB.Find(&devices, "conn_addr = ? and status = ?", connAddr, DeviceOnLine).Error
if err != nil {
return nil, gerrors.WrapError(err)
}
return devices, nil
}

// UpdateStatus 更新在线状态
func (*deviceDao) UpdateStatus(deviceId int64, connAddr string, status int) (int64, error) {
db := db.DB.Model(&model.Device{}).Where("id = ? and conn_addr = ?", deviceId, connAddr).
Update("status", status)
if db.Error != nil {
return 0, gerrors.WrapError(db.Error)
}
return db.RowsAffected, nil
}

+ 38
- 0
internal/logic/domain/device/device_dao_test.go View File

@@ -0,0 +1,38 @@
package device

import (
"fmt"
"gim/pkg/db"
"testing"
)

func init() {
fmt.Println("start")
db.InitByTest()
}

func TestDeviceDao_Add(t *testing.T) {
device := Device{
UserId: 1,
Type: 1,
Brand: "huawei",
Model: "huawei P10",
SystemVersion: "8.0.0",
SDKVersion: "1.0.0",
Status: 1,
}
err := DeviceDao.Save(&device)
fmt.Println(err)
fmt.Println(device)
}

func TestDeviceDao_Get(t *testing.T) {
device, err := DeviceDao.Get(1)
fmt.Printf("%+v\n %+v\n", device, err)
}

func TestDeviceDao_ListOnlineByUserId(t *testing.T) {
devices, err := DeviceDao.ListOnlineByUserId(1)
fmt.Println(err)
fmt.Printf("%+v \n", devices)
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save