├── .air.toml
├── .gitignore
├── LICENSE
├── README.md
├── api
└── v1
│ ├── chat.go
│ ├── community.go
│ ├── relation.go
│ ├── upload.go
│ └── user.go
├── api_test.http
├── config
└── config.toml
├── database
└── quick_chat.sql
├── go.mod
├── go.sum
├── images
├── chat.png
├── contact.png
├── login.png
├── massage.png
└── video.png
├── main.go
├── middleware
├── cors.go
├── jwt.go
└── middleware.go
├── model
├── Base.go
├── Chat.go
├── Community.go
├── Relation.go
└── User.go
├── router
├── chat.go
├── community.go
├── relation.go
├── router.go
├── upload.go
└── user.go
├── service
└── chat.go
├── utils
├── config.go
├── cron.go
├── db.go
├── jwt.go
├── logger.go
├── path.go
├── status
│ └── status.go
└── utils.go
└── web
├── .env.dev
├── .env.prod
├── .eslintrc-auto-import.json
├── .eslintrc.cjs
├── .gitignore
├── .prettierrc.json
├── .vscode
└── extensions.json
├── auto-imports.d.ts
├── components.d.ts
├── env.d.ts
├── index.html
├── package-lock.json
├── package.json
├── public
├── favicon.ico
└── simple-peer.main.js
├── src
├── App.vue
├── api
│ ├── common.ts
│ ├── contact.ts
│ └── user.ts
├── components
│ ├── ButtonBox
│ │ └── ButtonBox.vue
│ ├── HeadPortrait
│ │ └── HeadPortrait.vue
│ └── SvgIcon
│ │ └── SvgIcon.vue
├── hooks
│ ├── use-chat.ts
│ └── use-common.ts
├── layouts
│ └── NavigationBar
│ │ └── NavigationBar.vue
├── main.ts
├── plugins
│ └── lib.ts
├── router
│ └── index.ts
├── stores
│ ├── contact.ts
│ └── user.ts
├── styles
│ └── index.less
├── utils
│ ├── constant.ts
│ ├── http-request.ts
│ ├── is-dev.ts
│ ├── storage.ts
│ ├── wait.ts
│ ├── web-rtc.ts
│ └── websocket.ts
└── views
│ ├── HomeView
│ ├── HomeChatView.vue
│ ├── HomeContactView.vue
│ ├── HomeMomentView.vue
│ ├── HomeMyView.vue
│ ├── HomeView.vue
│ └── components
│ │ ├── AddModal
│ │ └── AddModal.vue
│ │ ├── ChatFrame
│ │ └── ChatFrame.vue
│ │ ├── DetailBox
│ │ └── DetailBox.vue
│ │ └── ItemRecord
│ │ └── ItemRecord.vue
│ └── LoginView
│ ├── LoginBox.vue
│ ├── LoginView.vue
│ └── RegisterBox.vue
├── tsconfig.config.json
├── tsconfig.json
├── vite.config.ts
├── vue.config.js
└── yarn.lock
/.air.toml:
--------------------------------------------------------------------------------
1 | root = "."
2 | tmp_dir = "tmp"
3 |
4 | [build]
5 | cmd = "go build -o ./tmp/main ."
6 | bin = "tmp/main"
7 | full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
8 | include_ext = ["go", "tpl", "tmpl", "html"]
9 | exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules", "web"]
10 | include_dir = []
11 | exclude_file = []
12 | log = "air.log"
13 | delay = 1000 # ms
14 | stop_on_error = true
15 | send_interrupt = false
16 | kill_delay = 500 # ms
17 |
18 | [log]
19 | time = false
20 |
21 | [color]
22 | main = "magenta"
23 | watcher = "cyan"
24 | build = "yellow"
25 | runner = "green"
26 |
27 | [misc]
28 | clean_on_exit = true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | tmp
2 | logs
3 | *.log
4 | assets
5 | main
6 | tls.key
7 | tls.pem
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Boda Lü
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # quick-chat
2 | web端音视频实时通讯的demo项目,适合学习研究和二次开发。
3 |
4 | ## 预览
5 | **预览地址**:[https://lvboda.cn/quick-chat](https://lvboda.cn/quick-chat)
6 |
7 | **测试用户1账号/密码**:useruser1/123456
8 |
9 | **测试用户2账号/密码**:useruser2/123456
10 |
11 | > 注意:请不要在一个浏览器同时登陆两个账号!
12 |
13 |
14 |
15 | 
16 | **登陆**
17 |
18 | 
19 | **消息**
20 |
21 | 
22 | **联系**
23 |
24 | 
25 | **文字**
26 |
27 | 
28 | **视频**
29 |
30 |
31 |
32 | ## 功能
33 | - [x] 注册
34 | - [x] 登陆
35 | - [ ] 退出
36 | - [x] 添加好友
37 | - [ ] 添加群
38 | - [x] 好友验证
39 | - [ ] 群验证
40 | - [x] 文字实时通讯
41 | - [x] 视频实时通讯
42 | - [ ] 音频实时通讯
43 | - [ ] 群文字实时通讯
44 | - [ ] 群视频实时通讯
45 | - [ ] 群文字实时通讯
46 | - [ ] 图片/文件传输
47 | - [ ] 朋友圈
48 |
49 | > 群聊的相关功能以及图片/文件传输后端都已经实现,前端部分未开发,如果有需要请自行二次开发。
50 | >
51 | > 音频实时通讯的逻辑与视频实时通讯逻辑相同,如果有需要请自行二次开发。
52 |
53 | ## 开发
54 | ### 目录结构
55 | ```
56 | ├── api apis
57 | ├── assets 静态文件存储目录
58 | ├── config 配置文件目录
59 | ├── database DDL
60 | ├── logs log存储目录
61 | ├── middleware 中间件
62 | ├── model model层
63 | ├── router 路由表
64 | ├── service service层
65 | ├── utils 工具包
66 | ├── web web端
67 | │ ├── dist 打包输出目录
68 | │ ├── node_modules 三方库
69 | │ ├── public 静态资源
70 | │ ├── src 前端开发目录
71 | │ │ ├── api 接口
72 | │ │ ├── assets 静态资源
73 | │ │ ├── components 通用组件
74 | │ │ ├── hooks hooks
75 | │ │ ├── layouts 布局组件
76 | │ │ ├── plugins 插件
77 | │ │ ├── router 路由
78 | │ │ ├── stores 全局数据存储
79 | │ │ ├── styles 通用样式
80 | │ │ ├── utils 工具包
81 | │ │ ├── views 页面
82 | │ │ ├── App.vue 根组件
83 | │ │ └── main.ts 入口
84 | │ └── .env.* 环境变量
85 | ├── .air.toml air配置文件
86 | ├── api_test.http 接口测试文件
87 | ├── main.go 入口
88 | ├── go.mod
89 | ├── go.sum
90 | ```
91 |
92 | ### 技术选型
93 | **后端**:go + gin + gorm + jwt-go + websocket ...
94 |
95 | **前端**:ts + vue + vue-router + pinia + axios + simple-peer ...
96 |
97 | **数据库**:mysql
98 |
99 | ### 本地运行
100 |
101 | #### 后端
102 | ``` bash
103 | # 安装依赖
104 | go mod download
105 |
106 | # 安装air,出现版本号即为成功,若未安装成功请查看官方文档
107 | curl -sSfL https://raw.githubusercontent.com/cosmtrek/air/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
108 |
109 | air -v
110 |
111 | # 启动
112 | air
113 | ```
114 |
115 | > 注意:本项目后端使用air进行热重启,需要在本地全局安装air并确保配置了air环境变量,否则启动后端服务可能会出现路径问题,[air官方文档](https://github.com/cosmtrek/air)。
116 |
117 | #### 前端
118 | ``` bash
119 | # 安装依赖
120 | npm install
121 |
122 | # 启动
123 | npm run dev
124 | ```
125 |
126 | #### 数据库
127 | ``` sql
128 | -- 创建库
129 | CREATE DATABASE quick_chat
130 |
131 | USE quick_chat
132 |
133 | -- 执行DDL,文件位置:./database/quick_chat.sql
134 | SOURCE quick_chat.sql
135 | ```
136 |
137 | > 注意:后端没有使用orm框架的自动迁移,需要提前手动建表。
138 |
139 | ### 开发思路
140 | 思路与更详细的开发流程请查看文章。
141 |
142 | ## 许可
143 |
144 | [MIT](./LICENSE)
145 |
146 | Copyright (c) 2022 - Boda Lü
147 |
--------------------------------------------------------------------------------
/api/v1/chat.go:
--------------------------------------------------------------------------------
1 | package apiV1
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/gorilla/websocket"
8 | "github.com/lvboda/quick-chat/service"
9 | "github.com/lvboda/quick-chat/utils"
10 | )
11 |
12 | func Chat(c *gin.Context) {
13 | conn, err := createConnect(c)
14 | if err != nil {
15 | utils.Logger.Errorln("ws:创建websocket连接发生错误: ", err)
16 | return
17 | }
18 |
19 | service.Chat(c, conn)
20 | }
21 |
22 | // createConnect 创建websocket连接
23 | func createConnect(c *gin.Context) (*websocket.Conn, error) {
24 | var upgrader = websocket.Upgrader{
25 | ReadBufferSize: 1024,
26 | WriteBufferSize: 1024,
27 | CheckOrigin: func(r *http.Request) bool { return true },
28 | }
29 |
30 | return upgrader.Upgrade(c.Writer, c.Request, nil)
31 | }
32 |
--------------------------------------------------------------------------------
/api/v1/community.go:
--------------------------------------------------------------------------------
1 | package apiV1
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/lvboda/quick-chat/model"
8 | "github.com/lvboda/quick-chat/utils"
9 | "github.com/lvboda/quick-chat/utils/status"
10 | )
11 |
12 | // CreateCommunity 新建群聊
13 | func CreateCommunity(c *gin.Context) {
14 | var community model.CommunityEntity
15 | var query struct {
16 | CommunityId string
17 | }
18 |
19 | if err := c.ShouldBindJSON(&community); err != nil {
20 | c.AbortWithStatusJSON(http.StatusBadRequest, status.GetResponse(status.ERROR_REQUEST_PARAM, err, nil))
21 | return
22 | }
23 |
24 | if !utils.CheckAuthByUserId(c, community.OwnerId) {
25 | c.AbortWithStatusJSON(http.StatusBadRequest, status.GetResponse(status.ERROR_USER_NO_RIGHT, nil, nil))
26 | return
27 | }
28 |
29 | query.CommunityId = community.CommunityId
30 | if _, code := community.SelectBy(query); code == status.SUCCESS {
31 | c.AbortWithStatusJSON(http.StatusBadRequest, status.GetResponse(status.ERROR_COMMUNITY_ID_USED, nil, nil))
32 | return
33 | }
34 |
35 | if code := community.Insert(); code != status.SUCCESS {
36 | c.AbortWithStatusJSON(http.StatusInternalServerError, status.GetResponse(status.ERROR_COMMUNITY_CREATE, nil, nil))
37 | return
38 | }
39 |
40 | c.JSON(http.StatusOK, status.GetResponse(status.SUCCESS, nil, nil))
41 | }
42 |
43 | // EditCommunityById 修改群聊信息
44 | func EditCommunityById(c *gin.Context) {
45 | var community model.CommunityEntity
46 | var query struct {
47 | CommunityId string
48 | }
49 | query.CommunityId = c.Param("cid")
50 |
51 | if err := c.ShouldBindJSON(&community); err != nil || query.CommunityId == "" {
52 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_REQUEST_PARAM, err, nil))
53 | return
54 | }
55 |
56 | v, code := community.SelectBy(query)
57 | if code != status.SUCCESS {
58 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_COMMUNITY_NOT_EXIST, nil, nil))
59 | return
60 | }
61 |
62 | if !utils.CheckAuthByUserId(c, v.OwnerId) {
63 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_USER_NO_RIGHT, nil, nil))
64 | return
65 | }
66 |
67 | if code := community.Update(query.CommunityId); code != status.SUCCESS {
68 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_COMMUNITY_UPDATE, nil, nil))
69 | return
70 | }
71 |
72 | c.JSON(http.StatusOK, status.GetResponse(status.SUCCESS, nil, nil))
73 | }
74 |
75 | // RemoveCommunityById 解散群
76 | func RemoveCommunityById(c *gin.Context) {
77 | var community model.CommunityEntity
78 | var query struct {
79 | CommunityId string
80 | }
81 | query.CommunityId = c.Param("cid")
82 |
83 | if query.CommunityId == "" {
84 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_REQUEST_PARAM, nil, nil))
85 | return
86 | }
87 |
88 | v, code := community.SelectBy(query)
89 | if code != status.SUCCESS {
90 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_COMMUNITY_NOT_EXIST, nil, nil))
91 | return
92 | }
93 |
94 | if !utils.CheckAuthByUserId(c, v.OwnerId) {
95 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_USER_NO_RIGHT, nil, nil))
96 | return
97 | }
98 |
99 | if code := community.Delete(query.CommunityId); code != status.SUCCESS {
100 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_COMMUNITY_DELETE, nil, nil))
101 | return
102 | }
103 |
104 | c.JSON(http.StatusOK, status.GetResponse(status.SUCCESS, nil, nil))
105 | }
106 |
107 | // QueryCommunityById 查询群聊
108 | func QueryCommunityByCid(c *gin.Context) {
109 | var community model.CommunityEntity
110 | var query struct {
111 | CommunityId string
112 | }
113 | query.CommunityId = c.Param("cid")
114 |
115 | if res, code := community.SelectBy(query); code != status.SUCCESS {
116 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_COMMUNITY_NOT_EXIST, nil, nil))
117 | return
118 | } else {
119 | c.JSON(http.StatusOK, status.GetResponse(code, nil, res))
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/api/v1/relation.go:
--------------------------------------------------------------------------------
1 | package apiV1
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/lvboda/quick-chat/model"
8 | "github.com/lvboda/quick-chat/utils"
9 | "github.com/lvboda/quick-chat/utils/status"
10 | )
11 |
12 | // SendValidate 发送验证信息
13 | func SendValidate(c *gin.Context) {
14 | var relation model.RelationEntity
15 | var query struct {
16 | UserId string `binding:"required"`
17 | FriendId string `binding:"required"`
18 | Memo string `binding:"required"`
19 | RoleType int `binding:"required"`
20 | }
21 |
22 | if err := c.ShouldBindJSON(&query); err != nil {
23 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_REQUEST_PARAM, err, nil))
24 | return
25 | }
26 |
27 | if !utils.CheckAuthByUserId(c, query.UserId) {
28 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_USER_NO_RIGHT, nil, nil))
29 | return
30 | }
31 |
32 | relation.UserId = query.UserId
33 | relation.FriendId = query.FriendId
34 | relation.Memo = query.Memo
35 | relation.RoleType = query.RoleType
36 | relation.RelationType = 1
37 |
38 | if code := relation.Insert(); code != status.SUCCESS {
39 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_RELATION_VALIDATE_SEND, nil, nil))
40 | return
41 | }
42 |
43 | c.JSON(http.StatusOK, status.GetResponse(status.SUCCESS, nil, nil))
44 | }
45 |
46 | // AddRelation 添加关系
47 | func AddRelation(c *gin.Context) {
48 | var relation model.RelationEntity
49 | var query struct {
50 | UserId string `binding:"required"`
51 | FriendId string `binding:"required"`
52 | RoleType int `binding:"required"`
53 | }
54 |
55 | if err := c.ShouldBindJSON(&query); err != nil {
56 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_REQUEST_PARAM, err, nil))
57 | return
58 | }
59 |
60 | if query.RoleType == 1 && !utils.CheckAuthByUserId(c, query.UserId) {
61 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_USER_NO_RIGHT, nil, nil))
62 | return
63 | }
64 |
65 | relation.UserId = query.FriendId
66 | relation.FriendId = query.UserId
67 | relation.RoleType = query.RoleType
68 | relation.RelationType = 2
69 | if code := relation.AddFriend(); code != status.SUCCESS {
70 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_RELATION_ADD, nil, nil))
71 | return
72 | }
73 |
74 | c.JSON(http.StatusOK, status.GetResponse(status.SUCCESS, nil, nil))
75 | }
76 |
77 | // RemoveRelation 删除关系
78 | func RemoveRelation(c *gin.Context) {
79 | var relation model.RelationEntity
80 | var query struct {
81 | UserId string `binding:"required"`
82 | FriendId string `binding:"required"`
83 | RoleType int `binding:"required"`
84 | }
85 |
86 | if err := c.ShouldBindJSON(&query); err != nil {
87 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_REQUEST_PARAM, err, nil))
88 | return
89 | }
90 |
91 | if query.RoleType == 1 && !utils.CheckAuthByUserId(c, query.UserId) {
92 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_USER_NO_RIGHT, nil, nil))
93 | return
94 | }
95 |
96 | relation.UserId = query.FriendId
97 | relation.FriendId = query.UserId
98 | relation.RoleType = query.RoleType
99 | relation.RelationType = 3
100 | if code := relation.RemoveFriend(); code != status.SUCCESS {
101 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_RELATION_DELETE, nil, nil))
102 | return
103 | }
104 |
105 | c.JSON(http.StatusOK, status.GetResponse(status.SUCCESS, nil, nil))
106 | }
107 |
108 | // QueryList 查询关系信息列表
109 | func QueryRelationList(c *gin.Context) {
110 | var relation model.RelationEntity
111 | var query struct {
112 | FriendId string
113 | RelationType int
114 | RoleType int
115 | }
116 |
117 | if err := c.ShouldBindJSON(&query); err != nil {
118 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_REQUEST_PARAM, err, nil))
119 | return
120 | }
121 |
122 | if query.RoleType == 1 && !utils.CheckAuthByUserId(c, query.FriendId) {
123 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_USER_NO_RIGHT, nil, nil))
124 | return
125 | }
126 |
127 | if res, code := relation.SelectListBy(query, query.RoleType); code != status.SUCCESS {
128 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_RELATION_VALIDATE_SELECT, nil, nil))
129 | return
130 | } else {
131 | c.JSON(http.StatusOK, status.GetResponse(code, nil, res))
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/api/v1/upload.go:
--------------------------------------------------------------------------------
1 | package apiV1
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 | "time"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/lvboda/quick-chat/utils"
10 | "github.com/lvboda/quick-chat/utils/status"
11 | )
12 |
13 | // Upload 上传文件
14 | func UploadFile(c *gin.Context) {
15 | file, err := c.FormFile("file")
16 | if err != nil {
17 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_FILE_PARSE, err, nil))
18 | return
19 | }
20 |
21 | // 文件路径命名规则: /assets/fulltime/年-月-日/fileName__uuid.suffix
22 | date := time.Now().Format("2006-01-02")
23 | src := utils.CreateSafeFilePath([]string{utils.StaticAssetsPath, "fulltime", date}, utils.ToHashFileName(file.Filename))
24 |
25 | err = c.SaveUploadedFile(file, src)
26 | if err != nil {
27 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_FILE_UPLOAD, nil, nil))
28 | return
29 | }
30 |
31 | c.JSON(http.StatusOK, status.GetResponse(status.SUCCESS, nil, strings.TrimPrefix(src, ".")))
32 | }
33 |
34 | // Upload 上传临时文件
35 | func UploadTmpFile(c *gin.Context) {
36 | file, err := c.FormFile("file")
37 | if err != nil {
38 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_FILE_PARSE, err, nil))
39 | return
40 | }
41 |
42 | // 文件路径命名规则: /assets/tmp/年-月-日/fileName__uuid.suffix
43 | date := time.Now().Format("2006-01-02")
44 | src := utils.CreateSafeFilePath([]string{utils.StaticAssetsPath, "tmp", date}, utils.ToHashFileName(file.Filename))
45 |
46 | err = c.SaveUploadedFile(file, src)
47 | if err != nil {
48 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_FILE_UPLOAD, nil, nil))
49 | return
50 | }
51 |
52 | c.JSON(http.StatusOK, status.GetResponse(status.SUCCESS, nil, strings.TrimPrefix(src, ".")))
53 | }
54 |
--------------------------------------------------------------------------------
/api/v1/user.go:
--------------------------------------------------------------------------------
1 | package apiV1
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/lvboda/quick-chat/model"
8 | "github.com/lvboda/quick-chat/utils"
9 | "github.com/lvboda/quick-chat/utils/status"
10 | )
11 |
12 | // Register 注册
13 | func Register(c *gin.Context) {
14 | var user model.UserEntity
15 | var query struct {
16 | UserId string
17 | }
18 |
19 | if err := c.ShouldBindJSON(&user); err != nil {
20 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_REQUEST_PARAM, err, nil))
21 | return
22 | }
23 |
24 | query.UserId = user.UserId
25 | if _, code := user.SelectBy(query); code == status.SUCCESS {
26 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_USERNAME_USED, nil, nil))
27 | return
28 | }
29 |
30 | if code := user.Insert(); code != status.SUCCESS {
31 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_USER_REGISTER, nil, nil))
32 | return
33 | }
34 |
35 | c.JSON(http.StatusOK, status.GetResponse(status.SUCCESS, nil, nil))
36 | }
37 |
38 | // Login 登录
39 | func Login(c *gin.Context) {
40 | var user model.UserEntity
41 | var query struct {
42 | UserId string `binding:"required,min=6,max=15"`
43 | Password string `binding:"required,min=6,max=15"`
44 | }
45 |
46 | if err := c.ShouldBindJSON(&query); err != nil {
47 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_REQUEST_PARAM, err, nil))
48 | return
49 | }
50 |
51 | data, code := user.SelectBy(query)
52 | if code != status.SUCCESS {
53 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_PASSWORD_WRONG, nil, nil))
54 | return
55 | }
56 |
57 | token, err := utils.CreateToken(data.UserId, data.Password)
58 | if err != nil {
59 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_TOKEN_CREATE, err, nil))
60 | return
61 | }
62 |
63 | data.Password = ""
64 | resData, _ := utils.MergeJson(data, map[string]any{"token": token})
65 |
66 | c.JSON(http.StatusOK, status.GetResponse(status.SUCCESS, nil, resData))
67 | }
68 |
69 | // QueryUserByUid 查询用户信息
70 | func QueryUserByUid(c *gin.Context) {
71 | var user model.UserEntity
72 | var query struct {
73 | UserId string
74 | }
75 | query.UserId = c.Param("uid")
76 |
77 | if res, code := user.SelectBy(query); code != status.SUCCESS {
78 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_USER_NOT_EXIST, nil, nil))
79 | return
80 | } else {
81 | c.JSON(http.StatusOK, status.GetResponse(code, nil, res))
82 | }
83 | }
84 |
85 | // EditUserById 修改用户信息
86 | func EditUserById(c *gin.Context) {
87 | var user model.UserEntity
88 | uid := c.Param("uid")
89 |
90 | if err := c.ShouldBindJSON(&user); err != nil || uid == "" {
91 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_REQUEST_PARAM, err, nil))
92 | return
93 | }
94 |
95 | if !utils.CheckAuthByUserId(c, uid) {
96 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_USER_NO_RIGHT, nil, nil))
97 | return
98 | }
99 |
100 | if code := user.Update(uid); code != status.SUCCESS {
101 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_USER_UPDATE, nil, nil))
102 | return
103 | }
104 |
105 | c.JSON(http.StatusOK, status.GetResponse(status.SUCCESS, nil, nil))
106 | }
107 |
108 | // RemoveUserById 注销用户
109 | func RemoveUserById(c *gin.Context) {
110 | var user model.UserEntity
111 | uid := c.Param("uid")
112 |
113 | if uid == "" {
114 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_REQUEST_PARAM, nil, nil))
115 | return
116 | }
117 |
118 | if !utils.CheckAuthByUserId(c, uid) {
119 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_USER_NO_RIGHT, nil, nil))
120 | return
121 | }
122 |
123 | if code := user.Delete(uid); code != status.SUCCESS {
124 | c.AbortWithStatusJSON(http.StatusOK, status.GetResponse(status.ERROR_USER_DELETE, nil, nil))
125 | return
126 | }
127 |
128 | c.JSON(http.StatusOK, status.GetResponse(status.SUCCESS, nil, nil))
129 | }
130 |
--------------------------------------------------------------------------------
/api_test.http:
--------------------------------------------------------------------------------
1 | ### 登录 ok
2 | POST https://www.lvboda.cn:1001/api/v1/user/login HTTP/1.1
3 | Content-Type: application/json
4 |
5 | {
6 | "userId": "useruser2",
7 | "password": "123456"
8 | }
9 |
10 | ### 注册 ok
11 | POST https://www.lvboda.cn:1001/api/v1/user/register HTTP/1.1
12 | content-type: application/json
13 |
14 | {
15 | "nickName": "用户1",
16 | "userRole": 1,
17 | "gender": 1,
18 | "userId": "useruser1",
19 | "password": "123456"
20 | }
21 |
22 | ### 查单条 ok
23 | GET http://localhost:1001/api/v1/user/useruser1 HTTP/1.1
24 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ0ZXN0dGVzdDEiLCJwYXNzd29yZCI6IjEyMzQ1NiIsImV4cCI6MTY2MzY2MzM3NCwiaXNzIjoicXVpY2stY2hhdCIsIm5iZiI6MTY2MzU3Njg3NH0.YEd0XsFZdvsHsgicvNz5Pf9l-JmD1OJI9KaMTdSO5pw
25 |
26 | ### 修改个人信息 ok
27 | PUT http://localhost:1001/api/v1/user/useruser2 HTTP/1.1
28 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c2VydXNlcjIiLCJwYXNzd29yZCI6IjEyMzQ1NiIsImV4cCI6MTY2MzkyNDQ1NSwiaXNzIjoicXVpY2stY2hhdCIsIm5iZiI6MTY2MzgzNzk1NX0.xwyIEGqaMDqcGfdQ38JGTWSWdz8WsMiJA1HQtc7idHw
29 | content-type: application/json
30 |
31 | {
32 | "nickName": "修改用户2",
33 | "userRole": 1,
34 | "gender": 1,
35 | "userId": "useruser2",
36 | "password": "12345678"
37 | }
38 |
39 | ### 注销 ok
40 | DELETE http://localhost:1001/api/v1/user/useruser2 HTTP/1.1
41 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c2VydXNlcjIiLCJwYXNzd29yZCI6IjEyMzQ1NiIsImV4cCI6MTY2MzkyNDQ1NSwiaXNzIjoicXVpY2stY2hhdCIsIm5iZiI6MTY2MzgzNzk1NX0.xwyIEGqaMDqcGfdQ38JGTWSWdz8WsMiJA1HQtc7idHw
42 |
43 | ### 发送验证信息 ok
44 | POST http://localhost:1001/api/v1/relation/validate HTTP/1.1
45 | Content-Type: application/json
46 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c2VydXNlcjIiLCJwYXNzd29yZCI6IjEyMzQ1NiIsImV4cCI6MTY2MzkyNDQ1NSwiaXNzIjoicXVpY2stY2hhdCIsIm5iZiI6MTY2MzgzNzk1NX0.xwyIEGqaMDqcGfdQ38JGTWSWdz8WsMiJA1HQtc7idHw
47 |
48 | {
49 | "userId": "useruser2",
50 | "friendId": "community1",
51 | "memo": "我是user2,想加群",
52 | "roleType": 1
53 | }
54 |
55 | ### 查询验证列表 ok
56 | POST http://localhost:1001/api/v1/relation/list HTTP/1.1
57 | Content-Type: application/json
58 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c2VydXNlcjIiLCJwYXNzd29yZCI6IjEyMzQ1NiIsImV4cCI6MTY2MzkyNDQ1NSwiaXNzIjoicXVpY2stY2hhdCIsIm5iZiI6MTY2MzgzNzk1NX0.xwyIEGqaMDqcGfdQ38JGTWSWdz8WsMiJA1HQtc7idHw
59 |
60 | {
61 | "friendId": "community1",
62 | "relationType": 1,
63 | "roleType": 2
64 | }
65 |
66 | ### 查询好友列表 ok
67 | POST https://localhost:1001/api/v1/relation/list HTTP/1.1
68 | Content-Type: application/json
69 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c2VydXNlcjEiLCJwYXNzd29yZCI6IjEyMzQ1NiIsImV4cCI6MTY2NDExNjA1NywiaXNzIjoicXVpY2stY2hhdCIsIm5iZiI6MTY2NDAyOTU1N30.IisHKH5rSEKT956KYwGb_OrsByzeJHZxsvh0UykhL3k
70 |
71 | {
72 | "friendId": "useruser1",
73 | "relationType": 1,
74 | "roleType": 1
75 | }
76 |
77 | ### 添加好友 ok
78 | POST http://localhost:1001/api/v1/relation HTTP/1.1
79 | Content-Type: application/json
80 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c2VydXNlcjIiLCJwYXNzd29yZCI6IjEyMzQ1NiIsImV4cCI6MTY2MzkyMDQ4OCwiaXNzIjoicXVpY2stY2hhdCIsIm5iZiI6MTY2MzgzMzk4OH0.YzA0I26tBY5g18bNoebUY_sJg1NagS7lQvivzdnrrZ8
81 |
82 | {
83 | "userId": "community1",
84 | "friendId": "useruser2",
85 | "roleType": 2
86 | }
87 |
88 | ### 删除好友 ok
89 | DELETE http://localhost:1001/api/v1/relation HTTP/1.1
90 | Content-Type: application/json
91 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c2VydXNlcjIiLCJwYXNzd29yZCI6IjEyMzQ1NiIsImV4cCI6MTY2MzkyMDQ4OCwiaXNzIjoicXVpY2stY2hhdCIsIm5iZiI6MTY2MzgzMzk4OH0.YzA0I26tBY5g18bNoebUY_sJg1NagS7lQvivzdnrrZ8
92 |
93 | {
94 | "userId": "community1",
95 | "friendId": "useruser2",
96 | "roleType": 2
97 | }
98 |
99 | ### 新建群
100 | POST http://localhost:1001/api/v1/community HTTP/1.1
101 | Content-Type: application/json
102 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c2VydXNlcjIiLCJwYXNzd29yZCI6IjEyMzQ1NiIsImV4cCI6MTY2MzkyMDQ4OCwiaXNzIjoicXVpY2stY2hhdCIsIm5iZiI6MTY2MzgzMzk4OH0.YzA0I26tBY5g18bNoebUY_sJg1NagS7lQvivzdnrrZ8
103 |
104 | {
105 | "communityId": "community3",
106 | "name": "群3",
107 | "ownerId": "useruser2"
108 | }
109 |
110 | ### 修改群
111 | PUT http://localhost:1001/api/v1/community/community1 HTTP/1.1
112 | Content-Type: application/json
113 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c2VydXNlcjIiLCJwYXNzd29yZCI6IjEyMzQ1NiIsImV4cCI6MTY2MzkyMzcwMiwiaXNzIjoicXVpY2stY2hhdCIsIm5iZiI6MTY2MzgzNzIwMn0.RlmeOIjeF8NJfYmkDpVdQ_u2-Wgh3eI30z4-MAqQEes
114 |
115 | {
116 | "communityId": "community3",
117 | "name": "群hhhh",
118 | "ownerId": "useruser2"
119 | }
120 |
121 | ### 删除群
122 | DELETE http://localhost:1001/api/v1/community/097acdee8d5a4c2c9cd8767b8b3876a3 HTTP/1.1
123 | Content-Type: application/json
124 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c2VydXNlcjIiLCJwYXNzd29yZCI6IjEyMzQ1NiIsImV4cCI6MTY2MzkyMjM3NywiaXNzIjoicXVpY2stY2hhdCIsIm5iZiI6MTY2MzgzNTg3N30.GpZEk8Vi3IJFbOXN3mVFCjUlu0Gx4gm3w4-9sS4nkoA
125 |
126 | ### 查群
127 | GET http://localhost:1001/api/v1/community/community2 HTTP/1.1
128 | Content-Type: application/json
129 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c2VydXNlcjIiLCJwYXNzd29yZCI6IjEyMzQ1NiIsImV4cCI6MTY2MzkyMzcwMiwiaXNzIjoicXVpY2stY2hhdCIsIm5iZiI6MTY2MzgzNzIwMn0.RlmeOIjeF8NJfYmkDpVdQ_u2-Wgh3eI30z4-MAqQEes
--------------------------------------------------------------------------------
/config/config.toml:
--------------------------------------------------------------------------------
1 | [server]
2 | mode = "release"
3 | port = ":3001"
4 | jwt_key = "quick-chat"
5 | token_aging = 86400
6 |
7 | [database]
8 | db = "mysql"
9 | host = "127.0.0.1"
10 | port = "3306"
11 | user = "root"
12 | password = "BD1010110"
13 | name = "quick_chat"
--------------------------------------------------------------------------------
/database/quick_chat.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Navicat Premium Data Transfer
3 |
4 | Source Server : localhost
5 | Source Server Type : MySQL
6 | Source Server Version : 80023
7 | Source Host : localhost:3306
8 | Source Schema : quick_chat
9 |
10 | Target Server Type : MySQL
11 | Target Server Version : 80023
12 | File Encoding : 65001
13 |
14 | Date: 18/09/2022 15:51:54
15 | */
16 |
17 | SET NAMES utf8mb4;
18 | SET FOREIGN_KEY_CHECKS = 0;
19 |
20 | -- ----------------------------
21 | -- Table structure for chat_record
22 | -- ----------------------------
23 | DROP TABLE IF EXISTS `chat_record`;
24 | CREATE TABLE `chat_record` (
25 | `id` varchar(32) NOT NULL COMMENT '聊天记录id',
26 | `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
27 | `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
28 | `deleted_at` datetime(3) DEFAULT NULL COMMENT '删除时间',
29 | `user_relation_id` varchar(36) NOT NULL COMMENT '关系id',
30 | `record_type` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '消息类型: 1文本类型 2语音',
31 | `record` varchar(255) DEFAULT NULL COMMENT '聊天内容',
32 | `extend` varchar(100) NOT NULL DEFAULT '' COMMENT '扩展字段',
33 | PRIMARY KEY (`id`)
34 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天记录表';
35 |
36 | -- ----------------------------
37 | -- Table structure for community
38 | -- ----------------------------
39 | DROP TABLE IF EXISTS `community`;
40 | CREATE TABLE `community` (
41 | `id` varchar(32) NOT NULL COMMENT 'id',
42 | `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
43 | `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
44 | `deleted_at` datetime(3) DEFAULT NULL COMMENT '删除时间',
45 | `community_id` varchar(32) DEFAULT NULL COMMENT '群聊id',
46 | `name` varchar(32) DEFAULT NULL COMMENT '群名称',
47 | `owner_id` varchar(32) DEFAULT NULL COMMENT '群主id',
48 | `face` varchar(100) DEFAULT NULL COMMENT '群头像',
49 | `memo` varchar(100) DEFAULT NULL COMMENT '群描述',
50 | `extend` varchar(100) NOT NULL DEFAULT '' COMMENT '扩展字段',
51 | PRIMARY KEY (`id`)
52 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='群聊表';
53 |
54 | -- ----------------------------
55 | -- Table structure for user_base
56 | -- ----------------------------
57 | DROP TABLE IF EXISTS `user_base`;
58 | CREATE TABLE `user_base` (
59 | `id` varchar(32) NOT NULL COMMENT '用户id',
60 | `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
61 | `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
62 | `deleted_at` datetime(3) DEFAULT NULL COMMENT '删除时间',
63 | `nick_name` varchar(32) NOT NULL DEFAULT '' COMMENT '用户昵称',
64 | `user_id` varchar(32) NOT NULL DEFAULT '' COMMENT '用户id',
65 | `password` varchar(100) NOT NULL DEFAULT '' COMMENT '用户密码',
66 | `user_role` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '用户类型: 1正常用户 2封禁用户 3管理员',
67 | `gender` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '用户性别: 1男 2女',
68 | `signature` varchar(255) NOT NULL DEFAULT '' COMMENT '用户个人签名',
69 | `mobile` varchar(16) NOT NULL DEFAULT '' COMMENT '手机号码',
70 | `face` varchar(100) NOT NULL DEFAULT '' COMMENT '头像',
71 | `extend1` varchar(100) NOT NULL DEFAULT '' COMMENT '扩展字段1',
72 | `extend2` varchar(100) NOT NULL DEFAULT '' COMMENT '扩展字段2',
73 | PRIMARY KEY (`id`)
74 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户基础信息表';
75 |
76 | -- ----------------------------
77 | -- Table structure for user_relation
78 | -- ----------------------------
79 | DROP TABLE IF EXISTS `user_relation`;
80 | CREATE TABLE `user_relation` (
81 | `id` varchar(32) NOT NULL COMMENT '关系id',
82 | `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
83 | `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
84 | `deleted_at` datetime(3) DEFAULT NULL COMMENT '删除时间',
85 | `user_id` varchar(32) NOT NULL COMMENT '用户id',
86 | `friend_id` varchar(32) NOT NULL COMMENT '好友id',
87 | `relation_type` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '关系类型: 1验证 2双向关系 3单项被删除关系',
88 | `role_type` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '角色类型: 1好友 2群聊',
89 | `memo` varchar(120) DEFAULT NULL COMMENT '描述',
90 | `extend` varchar(100) NOT NULL DEFAULT '' COMMENT '扩展字段',
91 | PRIMARY KEY (`id`)
92 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户关系表';
93 |
94 | SET FOREIGN_KEY_CHECKS = 1;
95 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/lvboda/quick-chat
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible
7 | github.com/gin-contrib/cors v1.4.0
8 | github.com/gin-gonic/gin v1.8.1
9 | github.com/google/uuid v1.3.0
10 | github.com/gorilla/websocket v1.5.0
11 | github.com/pelletier/go-toml/v2 v2.0.5
12 | github.com/sirupsen/logrus v1.9.0
13 | gopkg.in/fatih/set.v0 v0.2.1
14 | gorm.io/driver/mysql v1.3.6
15 | gorm.io/gorm v1.23.8
16 | github.com/robfig/cron v1.2.0
17 | )
18 |
19 | require (
20 | github.com/gin-contrib/sse v0.1.0 // indirect
21 | github.com/go-playground/locales v0.14.0 // indirect
22 | github.com/go-playground/universal-translator v0.18.0 // indirect
23 | github.com/go-playground/validator/v10 v10.11.1 // indirect
24 | github.com/go-sql-driver/mysql v1.6.0 // indirect
25 | github.com/goccy/go-json v0.9.11 // indirect
26 | github.com/jinzhu/inflection v1.0.0 // indirect
27 | github.com/jinzhu/now v1.1.5 // indirect
28 | github.com/json-iterator/go v1.1.12 // indirect
29 | github.com/leodido/go-urn v1.2.1 // indirect
30 | github.com/mattn/go-isatty v0.0.16 // indirect
31 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
32 | github.com/modern-go/reflect2 v1.0.2 // indirect
33 | github.com/ugorji/go/codec v1.2.7 // indirect
34 | golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect
35 | golang.org/x/net v0.0.0-20220909164309-bea034e7d591 // indirect
36 | golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 // indirect
37 | golang.org/x/text v0.3.7 // indirect
38 | google.golang.org/protobuf v1.28.1 // indirect
39 | gopkg.in/yaml.v2 v2.4.0 // indirect
40 | )
41 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
2 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
8 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
9 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
10 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
11 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
12 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
13 | github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
14 | github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
15 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
16 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
17 | github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
18 | github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
19 | github.com/gitstliu/go-id-worker v0.0.0-20190725025543-5a5fe074e612 h1:bj4VCB0+U71rerlqgqStb+YbKOgyTGgC8RvYz7Q1Z0s=
20 | github.com/gitstliu/go-id-worker v0.0.0-20190725025543-5a5fe074e612/go.mod h1:L314R1i++xJdNYcENOLVOpmlY5Iz5nlfacJ18bVQw7k=
21 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
22 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
23 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
24 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
25 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
26 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
27 | github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
28 | github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
29 | github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
30 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
31 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
32 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
33 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
34 | github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
35 | github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
36 | github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
37 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
38 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
39 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
40 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
41 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
42 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
43 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
44 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
45 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
46 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
47 | github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
48 | github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
49 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
50 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
51 | github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
52 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
53 | github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
54 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
55 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
56 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
57 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
58 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
59 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
60 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
61 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
62 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
63 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
64 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
65 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
66 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
67 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
68 | github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
69 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
70 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
71 | github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
72 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
73 | github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
74 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
75 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
76 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
77 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
78 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
79 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
80 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
81 | github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
82 | github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
83 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
84 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
85 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
86 | github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
87 | github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
88 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
89 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
90 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
91 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
92 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
93 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
94 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
95 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
96 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
97 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
98 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
99 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
100 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
101 | github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
102 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
103 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
104 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
105 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
106 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
107 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
108 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
109 | golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
110 | golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
111 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
112 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
113 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
114 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
115 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
116 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
117 | golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI=
118 | golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
119 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
120 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
121 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
122 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
123 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
124 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
125 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
126 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
127 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
128 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
129 | golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 h1:ohgcoMbSofXygzo6AD2I1kz3BFmW1QArPYTtwEM3UXc=
130 | golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
131 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
132 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
133 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
134 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
135 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
136 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
137 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
138 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
139 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
140 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
141 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
142 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
143 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
144 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
145 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
146 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
147 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
148 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
149 | gopkg.in/fatih/set.v0 v0.2.1 h1:Xvyyp7LXu34P0ROhCyfXkmQCAoOUKb1E2JS9I7SE5CY=
150 | gopkg.in/fatih/set.v0 v0.2.1/go.mod h1:5eLWEndGL4zGGemXWrKuts+wTJR0y+w+auqUJZbmyBg=
151 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
152 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
153 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
154 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
155 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
156 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
157 | gorm.io/driver/mysql v1.3.6 h1:BhX1Y/RyALb+T9bZ3t07wLnPZBukt+IRkMn8UZSNbGM=
158 | gorm.io/driver/mysql v1.3.6/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
159 | gorm.io/gorm v1.23.8 h1:h8sGJ+biDgBA1AD1Ha9gFCx7h8npU7AsLdlkX0n2TpE=
160 | gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
161 |
--------------------------------------------------------------------------------
/images/chat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lvboda/quick-chat/8be65b27faae70756b8c708a4d1756f0122d6a9f/images/chat.png
--------------------------------------------------------------------------------
/images/contact.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lvboda/quick-chat/8be65b27faae70756b8c708a4d1756f0122d6a9f/images/contact.png
--------------------------------------------------------------------------------
/images/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lvboda/quick-chat/8be65b27faae70756b8c708a4d1756f0122d6a9f/images/login.png
--------------------------------------------------------------------------------
/images/massage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lvboda/quick-chat/8be65b27faae70756b8c708a4d1756f0122d6a9f/images/massage.png
--------------------------------------------------------------------------------
/images/video.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lvboda/quick-chat/8be65b27faae70756b8c708a4d1756f0122d6a9f/images/video.png
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/lvboda/quick-chat/middleware"
5 | "github.com/lvboda/quick-chat/router"
6 | "github.com/lvboda/quick-chat/utils"
7 | )
8 |
9 | func main() {
10 | bootstrap()
11 | }
12 |
13 | func bootstrap() {
14 | app := utils.CreateApp()
15 |
16 | middleware.RegisterMiddleware(app)
17 | router.RegisterRoutes(app)
18 |
19 | app.RunTLS(utils.GetConfig().Server.Port, utils.CertFilePath, utils.KeyFilePath)
20 | }
21 |
--------------------------------------------------------------------------------
/middleware/cors.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gin-contrib/cors"
7 | "github.com/gin-gonic/gin"
8 | )
9 |
10 | func corsMiddleware() gin.HandlerFunc {
11 | return cors.New(
12 | cors.Config{
13 | AllowOrigins: []string{"*"},
14 | AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
15 | AllowHeaders: []string{"*", "Authorization"},
16 | ExposeHeaders: []string{"Content-Length", "text/plain", "Authorization", "Content-Type"},
17 | AllowCredentials: true,
18 | MaxAge: 12 * time.Hour,
19 | },
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/middleware/jwt.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 |
7 | "github.com/gin-gonic/gin"
8 | "github.com/lvboda/quick-chat/utils"
9 | "github.com/lvboda/quick-chat/utils/status"
10 | )
11 |
12 | func jwtMiddleware() gin.HandlerFunc {
13 | return func(c *gin.Context) {
14 | tokenList := strings.Split(c.Request.Header.Get("Authorization"), " ")
15 |
16 | // 跳过注册和登录
17 | if strings.Contains(c.Request.URL.Path, "/user/register") || strings.Contains(c.Request.URL.Path, "/user/login") || strings.Contains(c.Request.URL.Path, "/chat") {
18 | c.Next()
19 | return
20 | }
21 |
22 | // 跳过ws和静态资源
23 | if strings.Contains(c.Request.URL.Path, "/chat") || strings.Contains(c.Request.URL.Path, "/assets") {
24 | c.Next()
25 | return
26 | }
27 |
28 | // 判断token格式
29 | if len(tokenList) == 0 || len(tokenList) != 2 || tokenList[0] != "Bearer" {
30 | c.AbortWithStatusJSON(http.StatusUnauthorized, status.GetResponse(status.ERROR_TOKEN_TYPE_WRONG, nil, nil))
31 | return
32 | }
33 |
34 | // 过期或其他错误
35 | if claims, code := utils.ParseToken(tokenList[1]); code != status.SUCCESS {
36 | c.AbortWithStatusJSON(http.StatusUnauthorized, status.GetResponse(code, nil, nil))
37 | return
38 | } else {
39 | // next
40 | c.Set("claims", claims)
41 | c.Next()
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/middleware/middleware.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | )
6 |
7 | func RegisterMiddleware(app *gin.Engine) {
8 | app.Use(corsMiddleware())
9 | app.Use(jwtMiddleware())
10 | }
11 |
--------------------------------------------------------------------------------
/model/Base.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | type BaseEntity struct {
10 | Id string `gorm:"primarykey;type:varchar(32);not null" json:"id"`
11 | CreatedAt time.Time `json:"createAt"`
12 | UpdatedAt time.Time `json:"updateAt"`
13 | DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"`
14 | }
15 |
--------------------------------------------------------------------------------
/model/Chat.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "encoding/json"
5 | "sync"
6 |
7 | "github.com/gorilla/websocket"
8 | "github.com/lvboda/quick-chat/utils"
9 | "gopkg.in/fatih/set.v0"
10 | )
11 |
12 | // 一个用户对应一个node
13 | type Node struct {
14 | // websocket连接
15 | Conn *websocket.Conn
16 | // 数据存储队列
17 | DataQueue chan []byte
18 | // 群id的set
19 | GroupSets set.Interface
20 | }
21 |
22 | // NewNode 创建新的node
23 | func NewNode(conn *websocket.Conn) *Node {
24 | return &Node{
25 | Conn: conn,
26 | DataQueue: make(chan []byte, 50),
27 | GroupSets: set.New(set.ThreadSafe),
28 | }
29 | }
30 |
31 | // 传输消息结构
32 | type Message struct {
33 | // 消息id
34 | Id string `json:"id"`
35 | // 发送者id
36 | SenderId string `json:"senderId"`
37 | // 接受者id
38 | ReceiverId string `json:"receiverId"`
39 | // 消息内容
40 | Content string `json:"content"`
41 | // 附加信息
42 | Extra string `json:"extra"`
43 | // 消息类型 前端用来判断 后端不处理
44 | ContentType int `json:"contentType"`
45 | // 处理类型 见status
46 | ProcessType int `json:"processType"`
47 | // 发送时间
48 | SendTime string `json:"sendTime"`
49 | // 源数据
50 | Resource []byte `json:"resource"`
51 | }
52 |
53 | // ToMessage []byte转message
54 | func ToMessage(data []byte) Message {
55 | var message Message
56 | err := json.Unmarshal(data, &message)
57 | if err != nil && string(data) != "heart" {
58 | utils.Logger.Errorln("ws:json转Message结构体发生错误: ", err)
59 | }
60 |
61 | message.Resource = data
62 | return message
63 | }
64 |
65 | // 离线消息存储
66 | type OfflineGroup struct {
67 | OfflineMap map[string][]Message
68 | Locker sync.RWMutex
69 | }
70 |
71 | // NewOfflineGroup 新建OfflineGroup 全局维护一个
72 | func NewOfflineGroup() *OfflineGroup {
73 | var flag bool
74 | var og *OfflineGroup
75 | return func() *OfflineGroup {
76 | if flag {
77 | return og
78 | } else {
79 | flag = true
80 | og = &OfflineGroup{
81 | OfflineMap: map[string][]Message{},
82 | Locker: sync.RWMutex{},
83 | }
84 | return og
85 | }
86 | }()
87 | }
88 |
89 | // Add 添加消息
90 | func (og *OfflineGroup) Add(msg Message) {
91 | og.Locker.Lock()
92 | og.OfflineMap[msg.ReceiverId] = append(og.OfflineMap[msg.ReceiverId], msg)
93 | og.Locker.Unlock()
94 | }
95 |
96 | // Delete 删除消息
97 | func (og *OfflineGroup) Delete(id string) {
98 | og.Locker.Lock()
99 | delete(og.OfflineMap, id)
100 | og.Locker.Unlock()
101 | }
102 |
103 | // 全局node存储 {key:uid value:node}
104 | type NodeGroup struct {
105 | NodeMap map[string]*Node
106 | Locker sync.RWMutex
107 | }
108 |
109 | // NewNodeGroup 新建NodeGroup 全局维护一个
110 | func NewNodeGroup() *NodeGroup {
111 | var flag bool
112 | var ng *NodeGroup
113 | return func() *NodeGroup {
114 | if flag {
115 | return ng
116 | } else {
117 | flag = true
118 | ng = &NodeGroup{
119 | NodeMap: make(map[string]*Node),
120 | Locker: sync.RWMutex{},
121 | }
122 | return ng
123 | }
124 | }()
125 | }
126 |
127 | // Add 添加node
128 | func (ng *NodeGroup) Add(id string, conn *websocket.Conn) (node *Node, ok bool) {
129 | ng.Locker.Lock()
130 | if _, has := ng.NodeMap[id]; !has {
131 | node = NewNode(conn)
132 | ng.NodeMap[id] = node
133 | ok = true
134 | }
135 | ng.Locker.Unlock()
136 | return
137 | }
138 |
139 | // Delete 删除node
140 | func (ng *NodeGroup) Delete(id string) {
141 | ng.Locker.Lock()
142 | if _, ok := ng.NodeMap[id]; ok {
143 | ng.NodeMap[id].Conn.Close()
144 | delete(ng.NodeMap, id)
145 | }
146 | ng.Locker.Unlock()
147 | }
148 |
149 | // SendMessage 发送消息
150 | func (ng *NodeGroup) SendMessage(msg Message) (ok bool) {
151 | ng.Locker.Lock()
152 | if node, has := ng.NodeMap[msg.ReceiverId]; has {
153 | node.DataQueue <- msg.Resource
154 | ok = true
155 | }
156 | ng.Locker.Unlock()
157 | return
158 | }
159 |
160 | // SendGroupMessage 发送群消息
161 | func (ng *NodeGroup) SendGroupMessage(msg Message) {
162 | ng.Locker.Lock()
163 | for _, node := range ng.NodeMap {
164 | if node.GroupSets.Has(msg.ReceiverId) {
165 | node.DataQueue <- msg.Resource
166 | }
167 | }
168 | ng.Locker.Unlock()
169 | }
170 |
--------------------------------------------------------------------------------
/model/Community.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "github.com/lvboda/quick-chat/utils"
5 | "github.com/lvboda/quick-chat/utils/status"
6 | )
7 |
8 | type CommunityEntity struct {
9 | BaseEntity
10 | CommunityId string `gorm:"type:varchar(32);not null;INDEX" json:"communityId" binding:"required,min=6,max=12" label:"群id"`
11 | Name string `gorm:"type:varchar(32);not null" json:"Name" binding:"required,min=1,max=12" label:"群名称"`
12 | OwnerId string `gorm:"type:varchar(32);not null" json:"ownerId" binding:"required" label:"群主id"`
13 | Face string `gorm:"type:varchar(100);not null" json:"face" label:"群头像"`
14 | Memo string `gorm:"type:varchar(100);" json:"memo" label:"群描述"`
15 | Extend string `gorm:"-" json:"extend"`
16 | }
17 |
18 | func (CommunityEntity) TableName() string {
19 | return "community"
20 | }
21 |
22 | // Insert 增
23 | func (community CommunityEntity) Insert() int {
24 | community.Id = utils.UUID()
25 |
26 | if err := utils.Db.Create(&community).Error; err != nil {
27 | return status.ERROR
28 | }
29 | return status.SUCCESS
30 | }
31 |
32 | // Delete 删
33 | func (community CommunityEntity) Delete(cid string) int {
34 | if err := utils.Db.Where("community_id = ? ", cid).Delete(&community).Error; err != nil {
35 | return status.ERROR
36 | }
37 | return status.SUCCESS
38 | }
39 |
40 | // Update 改
41 | func (community CommunityEntity) Update(cid string) int {
42 | updateFields := map[string]any{
43 | "name": community.Name,
44 | "owner_id": community.OwnerId,
45 | "face": community.Face,
46 | "memo": community.Memo,
47 | }
48 |
49 | if err := utils.Db.Model(&community).Where("community_id = ?", cid).Updates(&updateFields).Error; err != nil {
50 | return status.ERROR
51 | }
52 | return status.SUCCESS
53 | }
54 |
55 | // SelectBy 查单条
56 | func (community CommunityEntity) SelectBy(query any) (CommunityEntity, int) {
57 | var res CommunityEntity
58 |
59 | if err := utils.Db.Where(query).First(&res).Error; err != nil {
60 | return res, status.ERROR
61 | }
62 | return res, status.SUCCESS
63 | }
64 |
--------------------------------------------------------------------------------
/model/Relation.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "github.com/lvboda/quick-chat/utils"
5 | "github.com/lvboda/quick-chat/utils/status"
6 | "gorm.io/gorm"
7 | )
8 |
9 | type RelationEntity struct {
10 | BaseEntity
11 | UserId string `gorm:"type:varchar(32);not null" json:"userId" binding:"required,min=6,max=15" label:"用户id"`
12 | FriendId string `gorm:"type:varchar(32);not null" json:"friendId" binding:"required,min=6,max=15" label:"好友id"`
13 | RelationType int `gorm:"type:int;DEFAULT:1" json:"relationType" binding:"required" label:"关系类型"`
14 | RoleType int `gorm:"type:int;DEFAULT:1" json:"roleType" binding:"required" label:"角色类型"`
15 | Memo string `gorm:"type:varchar(120);DEFAULT:NULL" json:"memo" label:"描述"`
16 | Extend string `gorm:"-" json:"extend"`
17 | FriendInfo UserEntity `gorm:"foreignKey:UserId;references:UserId;" json:"friendInfo"`
18 | ProposerInfo UserEntity `gorm:"foreignKey:UserId;references:FriendId;" json:"proposerInfo"`
19 | CommunityInfo CommunityEntity `gorm:"foreignKey:CommunityId;references:FriendId;" json:"communityInfo"`
20 | }
21 |
22 | func (RelationEntity) TableName() string {
23 | return "user_relation"
24 | }
25 |
26 | // Insert 增
27 | func (relation RelationEntity) Insert() int {
28 | relation.Id = utils.UUID()
29 |
30 | if err := utils.Db.Create(&relation).Error; err != nil {
31 | return status.ERROR
32 | }
33 | return status.SUCCESS
34 | }
35 |
36 | // Delete 删
37 | func (relation RelationEntity) Delete(id string) int {
38 | if err := utils.Db.Where("id = ? ", id).Delete(&relation).Error; err != nil {
39 | return status.ERROR
40 | }
41 | return status.SUCCESS
42 | }
43 |
44 | // Update 改
45 | func (relation RelationEntity) Update(query any, args ...any) int {
46 | updateFields := map[string]any{
47 | "user_id": relation.UserId,
48 | "friend_id": relation.FriendId,
49 | "relation_type": relation.RelationType,
50 | }
51 |
52 | if err := utils.Db.Model(&relation).Where(query, args...).Updates(&updateFields).Error; err != nil {
53 | return status.ERROR
54 | }
55 | return status.SUCCESS
56 | }
57 |
58 | // SelectBy 查单条
59 | func (relation RelationEntity) SelectBy(query any, role int) (RelationEntity, int) {
60 | var res RelationEntity
61 |
62 | db := utils.Db.Where(query)
63 | if err := utils.If(role == 1, db.Preload("FriendInfo").Find(&res), db.Preload("ProposerInfo").Preload("CommunityInfo").First(&res)).Error; err != nil {
64 | return res, status.ERROR
65 | }
66 | return res, status.SUCCESS
67 | }
68 |
69 | // SelectListBy 查多条
70 | func (relation RelationEntity) SelectListBy(query any, role int) ([]RelationEntity, int) {
71 | var res []RelationEntity
72 |
73 | db := utils.Db.Where(query)
74 | if err := utils.If(role == 1, db.Preload("FriendInfo").Find(&res), db.Preload("ProposerInfo").Preload("CommunityInfo").Find(&res)).Error; err != nil {
75 | return res, status.ERROR
76 | }
77 | return res, status.SUCCESS
78 | }
79 |
80 | // AddFriend 添加好友
81 | func (relation RelationEntity) AddFriend() int {
82 | err := utils.Db.Transaction(func(tx *gorm.DB) error {
83 | updateFields := map[string]any{
84 | "user_id": relation.UserId,
85 | "friend_id": relation.FriendId,
86 | "relation_type": relation.RelationType,
87 | }
88 |
89 | if err := tx.Model(&relation).Where("user_id = ? AND friend_id = ?", relation.UserId, relation.FriendId).Updates(&updateFields).Error; err != nil {
90 | return err
91 | }
92 |
93 | relation.Id = utils.UUID()
94 | temp := relation.UserId
95 | relation.UserId = relation.FriendId
96 | relation.FriendId = temp
97 | if err := tx.Create(&relation).Error; err != nil {
98 | return err
99 | }
100 |
101 | return nil
102 | })
103 |
104 | if err != nil {
105 | return status.ERROR
106 | }
107 | return status.SUCCESS
108 | }
109 |
110 | // RemoveFriend 删除好友
111 | func (relation RelationEntity) RemoveFriend() int {
112 | err := utils.Db.Transaction(func(tx *gorm.DB) error {
113 | updateFields := map[string]any{
114 | "user_id": relation.UserId,
115 | "friend_id": relation.FriendId,
116 | "relation_type": relation.RelationType,
117 | }
118 |
119 | if err := tx.Model(&relation).Where("user_id = ? AND friend_id = ?", relation.UserId, relation.FriendId).Updates(&updateFields).Error; err != nil {
120 | return err
121 | }
122 |
123 | if err := tx.Where("user_id = ? AND friend_id = ?", relation.FriendId, relation.UserId).Delete(&relation).Error; err != nil {
124 | return err
125 | }
126 |
127 | return nil
128 | })
129 |
130 | if err != nil {
131 | return status.ERROR
132 | }
133 | return status.SUCCESS
134 | }
135 |
--------------------------------------------------------------------------------
/model/User.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "github.com/lvboda/quick-chat/utils"
5 | "github.com/lvboda/quick-chat/utils/status"
6 | )
7 |
8 | type UserEntity struct {
9 | BaseEntity
10 | NickName string `gorm:"type:varchar(32);not null;INDEX" json:"nickName" binding:"required,min=1,max=12" label:"昵称"`
11 | UserId string `gorm:"type:varchar(32);not null " json:"userId" binding:"required,min=6,max=15" label:"用户id"`
12 | Password string `gorm:"type:varchar(100);not null " json:"password" binding:"required,min=6,max=15" label:"密码"`
13 | UserRole int `gorm:"type:int;DEFAULT:1" json:"userRole" binding:"required" label:"角色类型"`
14 | Gender int `gorm:"type:int;DEFAULT:1" json:"gender" label:"性别"`
15 | Signature string `gorm:"type:varchar(255) " json:"signature" label:"个人签名"`
16 | Mobile string `gorm:"type:varchar(16) " json:"mobile" label:"电话"`
17 | Face string `gorm:"type:varchar(255) " json:"face" label:"头像"`
18 | }
19 |
20 | func (UserEntity) TableName() string {
21 | return "user_base"
22 | }
23 |
24 | // Insert 增
25 | func (user UserEntity) Insert() int {
26 | user.Id = utils.UUID()
27 |
28 | if err := utils.Db.Create(&user).Error; err != nil {
29 | return status.ERROR
30 | }
31 | return status.SUCCESS
32 | }
33 |
34 | // Delete 删
35 | func (user UserEntity) Delete(uid string) int {
36 | if err := utils.Db.Where("user_id = ? ", uid).Delete(&user).Error; err != nil {
37 | return status.ERROR
38 | }
39 | return status.SUCCESS
40 | }
41 |
42 | // Update 改
43 | func (user UserEntity) Update(uid string) int {
44 | updateFields := map[string]any{
45 | "nick_name": user.NickName,
46 | "password": user.Password,
47 | "gender": user.Gender,
48 | "signature": user.Signature,
49 | "mobile": user.Mobile,
50 | "face": user.Face,
51 | }
52 |
53 | if err := utils.Db.Model(&user).Where("user_id = ?", uid).Updates(&updateFields).Error; err != nil {
54 | return status.ERROR
55 | }
56 | return status.SUCCESS
57 | }
58 |
59 | // SelectBy 查单条
60 | func (user UserEntity) SelectBy(query any) (UserEntity, int) {
61 | var res UserEntity
62 |
63 | if err := utils.Db.Where(query).First(&res).Error; err != nil {
64 | return res, status.ERROR
65 | }
66 | return res, status.SUCCESS
67 | }
68 |
--------------------------------------------------------------------------------
/router/chat.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | apiV1 "github.com/lvboda/quick-chat/api/v1"
6 | )
7 |
8 | // registerWebsocketRouter 注册chat模块路由
9 | func registerChatRoutes(router *gin.RouterGroup) {
10 | router.GET("/chat/:uid", apiV1.Chat)
11 | }
12 |
--------------------------------------------------------------------------------
/router/community.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | apiV1 "github.com/lvboda/quick-chat/api/v1"
6 | )
7 |
8 | // registerCommunityRoutes 注册群聊模块路由
9 | func registerCommunityRoutes(router *gin.RouterGroup) {
10 | router.GET("/community/:cid", apiV1.QueryCommunityByCid)
11 | router.POST("/community", apiV1.CreateCommunity)
12 | router.PUT("/community/:cid", apiV1.EditCommunityById)
13 | router.DELETE("/community/:cid", apiV1.RemoveCommunityById)
14 | }
15 |
--------------------------------------------------------------------------------
/router/relation.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | apiV1 "github.com/lvboda/quick-chat/api/v1"
6 | )
7 |
8 | // registerRelationRoutes 注册关系模块路由
9 | func registerRelationRoutes(router *gin.RouterGroup) {
10 | router.POST("/relation/validate", apiV1.SendValidate)
11 | router.POST("/relation", apiV1.AddRelation)
12 | router.DELETE("/relation", apiV1.RemoveRelation)
13 | router.POST("/relation/list", apiV1.QueryRelationList)
14 | }
15 |
--------------------------------------------------------------------------------
/router/router.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/lvboda/quick-chat/utils"
6 | )
7 |
8 | // RegisterRoutes 注册路由
9 | func RegisterRoutes(app *gin.Engine) {
10 | registerStaticRoutes(app)
11 | registerV1Routes(app)
12 | }
13 |
14 | // registerStaticRoutes 注册静态资源路由
15 | func registerStaticRoutes(app *gin.Engine) {
16 | app.Static("/assets", utils.StaticAssetsPath)
17 | }
18 |
19 | // registerV1Routes 路由分组v1
20 | func registerV1Routes(app *gin.Engine) {
21 | router := app.Group("/api/v1")
22 | registerUserRoutes(router)
23 | registerUploadRoutes(router)
24 | registerRelationRoutes(router)
25 | registerChatRoutes(router)
26 | registerCommunityRoutes(router)
27 | }
28 |
--------------------------------------------------------------------------------
/router/upload.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | apiV1 "github.com/lvboda/quick-chat/api/v1"
6 | )
7 |
8 | // registerUploadRoutes 注册上传模块路由
9 | func registerUploadRoutes(router *gin.RouterGroup) {
10 | router.POST("/upload", apiV1.UploadFile)
11 | router.POST("/upload/tmp", apiV1.UploadTmpFile)
12 | }
13 |
--------------------------------------------------------------------------------
/router/user.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | apiV1 "github.com/lvboda/quick-chat/api/v1"
6 | )
7 |
8 | // registerUserRoutes 注册用户模块路由
9 | func registerUserRoutes(router *gin.RouterGroup) {
10 | router.GET("/user/:uid", apiV1.QueryUserByUid)
11 | router.PUT("/user/:uid", apiV1.EditUserById)
12 | router.DELETE("/user/:uid", apiV1.RemoveUserById)
13 | router.POST("/user/register", apiV1.Register)
14 | router.POST("/user/login", apiV1.Login)
15 | }
16 |
--------------------------------------------------------------------------------
/service/chat.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/gorilla/websocket"
6 | "github.com/lvboda/quick-chat/model"
7 | "github.com/lvboda/quick-chat/utils"
8 | "github.com/lvboda/quick-chat/utils/status"
9 | )
10 |
11 | var globalNodeGroup = model.NewNodeGroup()
12 | var globalOfflineGroup = model.NewOfflineGroup()
13 | var closeChan = make(chan bool)
14 |
15 | func Chat(c *gin.Context, conn *websocket.Conn) {
16 | uid := c.Param("uid")
17 |
18 | // if ok := utils.CheckAuthByUserId(c, uid); !ok {
19 | // return
20 | // }
21 |
22 | node, ok := globalNodeGroup.Add(uid, conn)
23 | if !ok {
24 | return
25 | }
26 |
27 | utils.Logger.Infof("ws:用户%s连接成功", uid)
28 |
29 | go sendLoop(uid, node)
30 | go receiveLoop(uid, node)
31 |
32 | pushGroupId(uid, node)
33 | pushOfflineMsg(uid)
34 | wait(uid)
35 | }
36 |
37 | // pushGroupId 添加群id
38 | func pushGroupId(uid string, node *model.Node) {
39 | var query struct {
40 | FriendId string
41 | RelationType int
42 | RoleType int
43 | }
44 | query.FriendId = uid
45 | query.RelationType = 2
46 | query.RoleType = 2
47 | relationList, _ := model.RelationEntity{}.SelectListBy(query, query.RoleType)
48 |
49 | for _, relation := range relationList {
50 | if relation.CommunityInfo.CommunityId != "" {
51 | node.GroupSets.Add(relation.CommunityInfo.CommunityId)
52 | }
53 | }
54 | }
55 |
56 | // pushOfflineMsg 离线消息推送
57 | func pushOfflineMsg(uid string) {
58 | if msgQueue, has := globalOfflineGroup.OfflineMap[uid]; has {
59 | for _, msg := range msgQueue {
60 | sendMessage(msg)
61 | }
62 |
63 | globalOfflineGroup.Delete(uid)
64 | }
65 | }
66 |
67 | // sendLoop 发送线程
68 | func sendLoop(uid string, node *model.Node) {
69 | for {
70 | data := <-node.DataQueue
71 | err := node.Conn.WriteMessage(websocket.TextMessage, data)
72 |
73 | if err != nil {
74 | closeChan <- true
75 | return
76 | }
77 | }
78 | }
79 |
80 | // receiveLoop 接收线程
81 | func receiveLoop(uid string, node *model.Node) {
82 | for {
83 | _, data, err := node.Conn.ReadMessage()
84 | if err != nil {
85 | closeChan <- true
86 | return
87 | }
88 |
89 | dispatchProcess(data)
90 | }
91 | }
92 |
93 | // dispatchProcess 分发不同的处理函数
94 | func dispatchProcess(data []byte) {
95 | msg := model.ToMessage(data)
96 |
97 | switch msg.ProcessType {
98 | case status.WS_PROCESS_SINGLE_MSG:
99 | sendMessage(msg)
100 | case status.WS_PROCESS_GROUP_MSG:
101 | sendGroupMessage(msg)
102 | case status.WS_PROCESS_CLOSE:
103 | closeChan <- true
104 | case status.WS_PROCESS_HEART:
105 | // ❤️
106 | }
107 | }
108 |
109 | // sendMessage 发送消息
110 | func sendMessage(msg model.Message) {
111 | if ok := globalNodeGroup.SendMessage(msg); !ok {
112 | globalOfflineGroup.Add(msg)
113 | }
114 | }
115 |
116 | // sendGroupMessage 发送群消息
117 | func sendGroupMessage(msg model.Message) {
118 | globalNodeGroup.SendGroupMessage(msg)
119 | }
120 |
121 | // wait 阻塞主线程 直到close
122 | func wait(uid string) {
123 | for {
124 | if <-closeChan {
125 | globalNodeGroup.Delete(uid)
126 | utils.Logger.Infof("ws:用户%s断开连接", uid)
127 | return
128 | }
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/utils/config.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/pelletier/go-toml/v2"
7 | )
8 |
9 | type config struct {
10 | Server struct {
11 | Mode string
12 | Port string
13 | JwtKey string `toml:"jwt_key"`
14 | TokenAging int64 `toml:"token_aging"`
15 | }
16 |
17 | Database struct {
18 | Db string
19 | Name string
20 | Host string
21 | Port string
22 | User string
23 | Password string
24 | }
25 | }
26 |
27 | var conf config
28 |
29 | func initConfig() {
30 | file, err := os.ReadFile(ConfigFilePath)
31 |
32 | if err != nil {
33 | Logger.Fatalln("配置文件读取错误: ", err)
34 | }
35 |
36 | err = toml.Unmarshal(file, &conf)
37 |
38 | if err != nil {
39 | Logger.Fatalln("配置文件解析错误: ", err)
40 | }
41 | }
42 |
43 | func GetConfig() config {
44 | return conf
45 | }
46 |
--------------------------------------------------------------------------------
/utils/cron.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "os"
5 | "time"
6 |
7 | "github.com/robfig/cron"
8 | )
9 |
10 | func initCron() {
11 | c := cron.New()
12 | registerCrons(c)
13 | c.Start()
14 | }
15 |
16 | func registerCrons(c *cron.Cron) {
17 | c.AddFunc("0 0 1 * * ?", tmpFileClearCron)
18 | }
19 |
20 | func tmpFileClearCron() {
21 | tmpFileDirPath := CreateSafeFilePath([]string{StaticAssetsPath, "tmp"}, "")
22 | dirList, _ := os.ReadDir(tmpFileDirPath)
23 | for _, dir := range dirList {
24 | dirCreateTime, err := time.Parse("2006-01-02", dir.Name())
25 |
26 | if err != nil {
27 | os.RemoveAll(tmpFileDirPath + dir.Name())
28 | Logger.Errorln("assets: 临时文件夹命名错误:", dir.Name()+err.Error())
29 | return
30 | }
31 |
32 | if dirCreateTime.Unix() < time.Now().Unix()-60*60*24*7 {
33 | os.RemoveAll(tmpFileDirPath + dir.Name())
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/utils/db.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "gorm.io/driver/mysql"
8 | "gorm.io/gorm"
9 | )
10 |
11 | var Db *gorm.DB
12 | var DbErr error
13 |
14 | func initDB() {
15 | dbConfig := GetConfig().Database
16 | dns := fmt.Sprintf(
17 | "%s:%s@(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local",
18 | dbConfig.User,
19 | dbConfig.Password,
20 | dbConfig.Host,
21 | dbConfig.Port,
22 | dbConfig.Name,
23 | )
24 |
25 | Db, DbErr = gorm.Open(mysql.Open(dns), &gorm.Config{
26 | SkipDefaultTransaction: true,
27 | })
28 | if DbErr != nil {
29 | Logger.Fatalln("数据库连接错误: ", DbErr)
30 | }
31 |
32 | sqlDb, sqlDbErr := Db.DB()
33 | if sqlDbErr != nil {
34 | Logger.Fatalln("数据库连接错误: ", sqlDbErr)
35 | }
36 |
37 | // SetMaxIdleCons 设置连接池中的最大闲置连接数。
38 | // SetMaxOpenCons 设置数据库的最大连接数量。
39 | // SetConnMaxLifetime 设置连接的最大可复用时间。
40 | sqlDb.SetMaxIdleConns(10)
41 | sqlDb.SetMaxOpenConns(100)
42 | sqlDb.SetConnMaxLifetime(10 * time.Second)
43 | }
44 |
--------------------------------------------------------------------------------
/utils/jwt.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/dgrijalva/jwt-go"
7 | "github.com/lvboda/quick-chat/utils/status"
8 | )
9 |
10 | type claims struct {
11 | UserId string `json:"userId"`
12 | Password string `json:"password"`
13 | jwt.StandardClaims
14 | }
15 |
16 | // ToClaims 转换为claims
17 | func ToClaims(data any) (res *claims, isOk bool) {
18 | if v, ok := data.(*claims); ok {
19 | return v, true
20 | } else {
21 | return
22 | }
23 | }
24 |
25 | // CreateToken 生成token
26 | func CreateToken(uid string, password string) (string, error) {
27 | serverConf := GetConfig().Server
28 |
29 | claims := claims{
30 | UserId: uid,
31 | Password: password,
32 | StandardClaims: jwt.StandardClaims{
33 | NotBefore: time.Now().Unix() - 100,
34 | ExpiresAt: time.Now().Unix() + serverConf.TokenAging,
35 | Issuer: "quick-chat",
36 | },
37 | }
38 |
39 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
40 |
41 | return token.SignedString([]byte(serverConf.JwtKey))
42 | }
43 |
44 | // ParseToken 解析token
45 | func ParseToken(tokenStr string) (*claims, int) {
46 | serverConf := GetConfig().Server
47 |
48 | token, err := jwt.ParseWithClaims(tokenStr, &claims{}, func(token *jwt.Token) (any, error) {
49 | return []byte(serverConf.JwtKey), nil
50 | })
51 |
52 | if err != nil {
53 | if ve, ok := err.(*jwt.ValidationError); ok {
54 | if ve.Errors&jwt.ValidationErrorExpired != 0 {
55 | return nil, status.ERROR_TOKEN_RUNTIME
56 | } else {
57 | return nil, status.ERROR_TOKEN_WRONG
58 | }
59 | }
60 | }
61 |
62 | if token == nil {
63 | return nil, status.ERROR_TOKEN_WRONG
64 | }
65 |
66 | if claims, ok := token.Claims.(*claims); ok && token.Valid {
67 | return claims, status.SUCCESS
68 | } else {
69 | return nil, status.ERROR_TOKEN_PARSE
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/utils/logger.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "io"
5 | "os"
6 |
7 | "github.com/gin-gonic/gin"
8 | "github.com/sirupsen/logrus"
9 | )
10 |
11 | var Logger *logrus.Logger
12 |
13 | func initLogger() {
14 | Logger = logrus.New()
15 |
16 | logFilePath := CreateSafeFilePath([]string{LogDirPath}, "quick_chat_server.log")
17 |
18 | file, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
19 |
20 | if err != nil {
21 | Logger.Fatalln("log文件路径解析错误: ", err)
22 | }
23 |
24 | writers := []io.Writer{file, os.Stdout}
25 |
26 | fileAndStdoutWriter := io.MultiWriter(writers...)
27 |
28 | gin.DefaultErrorWriter = fileAndStdoutWriter
29 | Logger.SetOutput(fileAndStdoutWriter)
30 | Logger.SetReportCaller(true)
31 | Logger.SetLevel(logrus.DebugLevel)
32 | }
33 |
--------------------------------------------------------------------------------
/utils/path.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | )
7 |
8 | var (
9 | ConfigFilePath = ToAbsPath("../config/config.toml")
10 | CertFilePath = ToAbsPath("../config/tls.pem") // create your tls.pem
11 | KeyFilePath = ToAbsPath("../config/tls.key") // create your tls.key
12 | StaticAssetsPath = ToAbsPath("../assets")
13 | LogDirPath = ToAbsPath("../logs")
14 | )
15 |
16 | // GetExecDirPath 获取执行文件外文件夹绝对路径
17 | func GetExecDirPath() string {
18 | ex, err := os.Executable()
19 | if err != nil {
20 | Logger.Fatalln("执行文件路径获取错误: ", err)
21 | }
22 | return filepath.Dir(ex)
23 | }
24 |
25 | // ToAbsPath 相对路径转绝对路径
26 | func ToAbsPath(filePath string) string {
27 | return filepath.Join(GetExecDirPath(), filePath)
28 | }
29 |
--------------------------------------------------------------------------------
/utils/status/status.go:
--------------------------------------------------------------------------------
1 | package status
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | )
6 |
7 | const (
8 | // ws处理类型
9 | // 心跳
10 | WS_PROCESS_HEART = 0
11 | // 单聊
12 | WS_PROCESS_SINGLE_MSG = 1
13 | // 群聊
14 | WS_PROCESS_GROUP_MSG = 2
15 | // 关闭ws连接
16 | WS_PROCESS_CLOSE = 3
17 |
18 | // 通用code
19 | SUCCESS = 200
20 | ERROR = 500
21 |
22 | // code = 5000... 通用错误
23 | ERROR_REQUEST_PARAM = 5000
24 | ERROR_FILE_PARSE = 5001
25 | ERROR_FILE_UPLOAD = 5002
26 |
27 | // code= 1000... 用户模块错误
28 | ERROR_USERNAME_USED = 1000
29 | ERROR_PASSWORD_WRONG = 1001
30 | ERROR_USER_NOT_EXIST = 1002
31 | ERROR_TOKEN_CREATE = 1003
32 | ERROR_TOKEN_RUNTIME = 1004
33 | ERROR_TOKEN_PARSE = 1005
34 | ERROR_TOKEN_WRONG = 1006
35 | ERROR_TOKEN_TYPE_WRONG = 1007
36 | ERROR_USER_NO_RIGHT = 1008
37 | ERROR_USER_UPDATE = 1009
38 | ERROR_USER_DELETE = 1010
39 | ERROR_USER_REGISTER = 1011
40 |
41 | // code = 2000... 关系模块错误
42 | ERROR_RELATION_ADD = 2000
43 | ERROR_RELATION_DELETE = 2001
44 | ERROR_RELATION_SELECT = 2002
45 | ERROR_RELATION_VALIDATE_SELECT = 2003
46 | ERROR_RELATION_VALIDATE_SEND = 2004
47 |
48 | // code = 3000... 聊天模块错误
49 | ERROR_CHAT_WS = 3000
50 |
51 | // code = 4000... 群聊模块错误
52 | ERROR_COMMUNITY_ID_USED = 4000
53 | ERROR_COMMUNITY_CREATE = 4001
54 | ERROR_COMMUNITY_NOT_EXIST = 4002
55 | ERROR_COMMUNITY_UPDATE = 4003
56 | ERROR_COMMUNITY_DELETE = 4004
57 | )
58 |
59 | var statusMsgMap = map[int]string{
60 | SUCCESS: "OK",
61 | ERROR: "ERROR",
62 |
63 | ERROR_REQUEST_PARAM: "请求参数错误",
64 | ERROR_FILE_PARSE: "文件解析失败",
65 | ERROR_FILE_UPLOAD: "文件上传失败",
66 |
67 | ERROR_USERNAME_USED: "用户已存在",
68 | ERROR_PASSWORD_WRONG: "用户名或密码错误",
69 | ERROR_USER_NOT_EXIST: "用户不存在",
70 | ERROR_TOKEN_CREATE: "TOKEN创建失败",
71 | ERROR_TOKEN_PARSE: "TOKEN解析失败",
72 | ERROR_TOKEN_RUNTIME: "TOKEN已过期 请重新登陆",
73 | ERROR_TOKEN_WRONG: "TOKEN不正确 请重新登陆",
74 | ERROR_TOKEN_TYPE_WRONG: "TOKEN格式错误 请重新登陆",
75 | ERROR_USER_NO_RIGHT: "该用户无权限",
76 | ERROR_USER_UPDATE: "用户信息修改失败",
77 | ERROR_USER_DELETE: "用户注销失败",
78 | ERROR_USER_REGISTER: "用户注册失败",
79 |
80 | ERROR_RELATION_ADD: "好友添加失败",
81 | ERROR_RELATION_DELETE: "好友删除失败",
82 | ERROR_RELATION_SELECT: "好友查询失败",
83 | ERROR_RELATION_VALIDATE_SELECT: "验证信息查询失败",
84 | ERROR_RELATION_VALIDATE_SEND: "验证信息发送失败",
85 |
86 | ERROR_CHAT_WS: "创建连接发生错误",
87 |
88 | ERROR_COMMUNITY_ID_USED: "该群已存在",
89 | ERROR_COMMUNITY_CREATE: "新建群失败",
90 | ERROR_COMMUNITY_NOT_EXIST: "该群不存在",
91 | ERROR_COMMUNITY_UPDATE: "群信息修改失败",
92 | ERROR_COMMUNITY_DELETE: "群解散失败",
93 | }
94 |
95 | func GetStatusMsg(status int) string {
96 | return statusMsgMap[status]
97 | }
98 |
99 | func GetResponse(status int, message any, data any) gin.H {
100 | if v, ok := message.(error); ok {
101 | message = GetStatusMsg(status) + "--" + v.Error()
102 | } else if v, ok := message.(string); ok {
103 | message = GetStatusMsg(status) + "--" + v
104 | } else if message == nil {
105 | message = GetStatusMsg(status)
106 | }
107 |
108 | return gin.H{
109 | "status": status,
110 | "message": message,
111 | "data": data,
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os"
7 | "strings"
8 |
9 | "github.com/gin-gonic/gin"
10 | "github.com/google/uuid"
11 | )
12 |
13 | func init() {
14 | initLogger()
15 | initConfig()
16 | initDB()
17 | initCron()
18 | }
19 |
20 | // createApp
21 | func CreateApp() *gin.Engine {
22 | // 设置mode
23 | mode := GetConfig().Server.Mode
24 | gin.SetMode(mode)
25 |
26 | app := gin.New()
27 | app.SetTrustedProxies([]string{"127.0.0.1"})
28 |
29 | return app
30 | }
31 |
32 | // CreateSafeFilePath 创建安全的文件路径
33 | func CreateSafeFilePath(dirNames []string, fileName string) string {
34 | if len(dirNames) < 1 {
35 | return ""
36 | }
37 |
38 | var basePath string
39 |
40 | for index, dir := range dirNames {
41 | if index == 0 {
42 | basePath = dir
43 | } else {
44 | basePath = fmt.Sprintf("%s/%s", basePath, dir)
45 | }
46 |
47 | if _, err := os.Stat(basePath); os.IsNotExist(err) {
48 | os.MkdirAll(basePath, os.ModePerm)
49 | os.Chmod(basePath, os.ModePerm)
50 | }
51 | }
52 |
53 | return strings.Join([]string{basePath, fileName}, "/")
54 | }
55 |
56 | // MergeJson 合并json
57 | func MergeJson(args ...any) (map[string]any, error) {
58 | var jsonMap map[string]any
59 |
60 | for _, item := range args {
61 | itemJson, err := json.Marshal(item)
62 |
63 | if err != nil {
64 | return nil, err
65 | }
66 |
67 | json.Unmarshal(itemJson, &jsonMap)
68 | }
69 |
70 | return jsonMap, nil
71 | }
72 |
73 | // UUID 生成唯一id
74 | func UUID() string {
75 | uuid := uuid.New().String()
76 | return strings.ReplaceAll(uuid, "-", "")
77 | }
78 |
79 | // GetCurrentUserId 获取当前登陆的userId
80 | func GetCurrentUserId(c *gin.Context) (uid string) {
81 | if userInfo, has := c.Get("claims"); has {
82 | if claims, ok := ToClaims(userInfo); ok {
83 | return claims.UserId
84 | } else {
85 | return
86 | }
87 | } else {
88 | return
89 | }
90 | }
91 |
92 | // CheckAuthByUserId 通过userId判断当前有无权限
93 | func CheckAuthByUserId(c *gin.Context, userId string) (isAuth bool) {
94 | currentUid := GetCurrentUserId(c)
95 | return currentUid == userId
96 | }
97 |
98 | // ToHashFileName 转唯一文件名
99 | func ToHashFileName(fileName string) (hashFileName string) {
100 | if filenames := strings.Split(fileName, "."); len(filenames) < 2 {
101 | return fmt.Sprintf("%s__%s", fileName, UUID())
102 | }
103 |
104 | return strings.Join(strings.Split(fileName, "."), fmt.Sprintf("__%s.", UUID()))
105 | }
106 |
107 | // If 模拟三元运算符
108 | func If[T any](is bool, v1 T, v2 T) T {
109 | if is {
110 | return v1
111 | }
112 | return v2
113 | }
114 |
--------------------------------------------------------------------------------
/web/.env.dev:
--------------------------------------------------------------------------------
1 | VITE_MODE_NAME=development
2 | VITE_LOGIN_TEST=true
3 | VITE_HTTP_URL=https://www.lvboda.cn:1001
4 | VITE_WS_URL=wss://101.34.75.4:1001
5 | VITE_APP_TITLE=quick-chat
6 | VITE_EDITOR=vscode
--------------------------------------------------------------------------------
/web/.env.prod:
--------------------------------------------------------------------------------
1 | VITE_MODE_NAME=production
2 | VITE_LOGIN_TEST=true
3 | VITE_HTTP_URL=https://www.lvboda.cn:1001
4 | VITE_WS_URL=wss://www.lvboda.cn:1001
5 | VITE_APP_TITLE=quick-chat
6 | VITE_EDITOR=vscode
--------------------------------------------------------------------------------
/web/.eslintrc-auto-import.json:
--------------------------------------------------------------------------------
1 | {
2 | "globals": {
3 | "EffectScope": true,
4 | "acceptHMRUpdate": true,
5 | "computed": true,
6 | "createApp": true,
7 | "createPinia": true,
8 | "customRef": true,
9 | "defineAsyncComponent": true,
10 | "defineComponent": true,
11 | "defineStore": true,
12 | "effectScope": true,
13 | "getActivePinia": true,
14 | "getCurrentInstance": true,
15 | "getCurrentScope": true,
16 | "h": true,
17 | "inject": true,
18 | "isProxy": true,
19 | "isReactive": true,
20 | "isReadonly": true,
21 | "isRef": true,
22 | "mapActions": true,
23 | "mapGetters": true,
24 | "mapState": true,
25 | "mapStores": true,
26 | "mapWritableState": true,
27 | "markRaw": true,
28 | "nextTick": true,
29 | "onActivated": true,
30 | "onBeforeMount": true,
31 | "onBeforeUnmount": true,
32 | "onBeforeUpdate": true,
33 | "onDeactivated": true,
34 | "onErrorCaptured": true,
35 | "onMounted": true,
36 | "onRenderTracked": true,
37 | "onRenderTriggered": true,
38 | "onScopeDispose": true,
39 | "onServerPrefetch": true,
40 | "onUnmounted": true,
41 | "onUpdated": true,
42 | "provide": true,
43 | "reactive": true,
44 | "readonly": true,
45 | "ref": true,
46 | "resolveComponent": true,
47 | "setActivePinia": true,
48 | "setMapStoreSuffix": true,
49 | "shallowReactive": true,
50 | "shallowReadonly": true,
51 | "shallowRef": true,
52 | "storeToRefs": true,
53 | "toRaw": true,
54 | "toRef": true,
55 | "toRefs": true,
56 | "triggerRef": true,
57 | "unref": true,
58 | "useAttrs": true,
59 | "useCssModule": true,
60 | "useCssVars": true,
61 | "useRoute": true,
62 | "useRouter": true,
63 | "useSlots": true,
64 | "watch": true,
65 | "watchEffect": true,
66 | "watchPostEffect": true,
67 | "watchSyncEffect": true,
68 | "ElNotification": true,
69 | "ElMessage": true
70 | }
71 | }
--------------------------------------------------------------------------------
/web/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | require("@rushstack/eslint-patch/modern-module-resolution");
3 |
4 | module.exports = {
5 | root: true,
6 | extends: [
7 | "plugin:vue/vue3-essential",
8 | "eslint:recommended",
9 | "@vue/eslint-config-typescript",
10 | "@vue/eslint-config-prettier",
11 | "./.eslintrc-auto-import.json",
12 | ],
13 | parserOptions: {
14 | ecmaVersion: "latest",
15 | },
16 | globals: {
17 | $ref: true,
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | .DS_Store
12 | dist
13 | dist-ssr
14 | coverage
15 | *.local
16 |
17 | /cypress/videos/
18 | /cypress/screenshots/
19 |
20 | # Editor directories and files
21 | .vscode/*
22 | !.vscode/extensions.json
23 | .idea
24 | *.suo
25 | *.ntvs*
26 | *.njsproj
27 | *.sln
28 | *.sw?
29 |
--------------------------------------------------------------------------------
/web/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/web/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
3 | }
4 |
--------------------------------------------------------------------------------
/web/auto-imports.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by 'unplugin-auto-import'
2 | export {}
3 | declare global {
4 | const EffectScope: typeof import('vue')['EffectScope']
5 | const ElMessage: typeof import('element-plus/es')['ElMessage']
6 | const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
7 | const computed: typeof import('vue')['computed']
8 | const createApp: typeof import('vue')['createApp']
9 | const createPinia: typeof import('pinia')['createPinia']
10 | const customRef: typeof import('vue')['customRef']
11 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
12 | const defineComponent: typeof import('vue')['defineComponent']
13 | const defineStore: typeof import('pinia')['defineStore']
14 | const effectScope: typeof import('vue')['effectScope']
15 | const getActivePinia: typeof import('pinia')['getActivePinia']
16 | const getCurrentInstance: typeof import('vue')['getCurrentInstance']
17 | const getCurrentScope: typeof import('vue')['getCurrentScope']
18 | const h: typeof import('vue')['h']
19 | const inject: typeof import('vue')['inject']
20 | const isProxy: typeof import('vue')['isProxy']
21 | const isReactive: typeof import('vue')['isReactive']
22 | const isReadonly: typeof import('vue')['isReadonly']
23 | const isRef: typeof import('vue')['isRef']
24 | const mapActions: typeof import('pinia')['mapActions']
25 | const mapGetters: typeof import('pinia')['mapGetters']
26 | const mapState: typeof import('pinia')['mapState']
27 | const mapStores: typeof import('pinia')['mapStores']
28 | const mapWritableState: typeof import('pinia')['mapWritableState']
29 | const markRaw: typeof import('vue')['markRaw']
30 | const nextTick: typeof import('vue')['nextTick']
31 | const onActivated: typeof import('vue')['onActivated']
32 | const onBeforeMount: typeof import('vue')['onBeforeMount']
33 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
34 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
35 | const onDeactivated: typeof import('vue')['onDeactivated']
36 | const onErrorCaptured: typeof import('vue')['onErrorCaptured']
37 | const onMounted: typeof import('vue')['onMounted']
38 | const onRenderTracked: typeof import('vue')['onRenderTracked']
39 | const onRenderTriggered: typeof import('vue')['onRenderTriggered']
40 | const onScopeDispose: typeof import('vue')['onScopeDispose']
41 | const onServerPrefetch: typeof import('vue')['onServerPrefetch']
42 | const onUnmounted: typeof import('vue')['onUnmounted']
43 | const onUpdated: typeof import('vue')['onUpdated']
44 | const provide: typeof import('vue')['provide']
45 | const reactive: typeof import('vue')['reactive']
46 | const readonly: typeof import('vue')['readonly']
47 | const ref: typeof import('vue')['ref']
48 | const resolveComponent: typeof import('vue')['resolveComponent']
49 | const setActivePinia: typeof import('pinia')['setActivePinia']
50 | const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
51 | const shallowReactive: typeof import('vue')['shallowReactive']
52 | const shallowReadonly: typeof import('vue')['shallowReadonly']
53 | const shallowRef: typeof import('vue')['shallowRef']
54 | const storeToRefs: typeof import('pinia')['storeToRefs']
55 | const toRaw: typeof import('vue')['toRaw']
56 | const toRef: typeof import('vue')['toRef']
57 | const toRefs: typeof import('vue')['toRefs']
58 | const triggerRef: typeof import('vue')['triggerRef']
59 | const unref: typeof import('vue')['unref']
60 | const useAttrs: typeof import('vue')['useAttrs']
61 | const useCssModule: typeof import('vue')['useCssModule']
62 | const useCssVars: typeof import('vue')['useCssVars']
63 | const useRoute: typeof import('vue-router')['useRoute']
64 | const useRouter: typeof import('vue-router')['useRouter']
65 | const useSlots: typeof import('vue')['useSlots']
66 | const watch: typeof import('vue')['watch']
67 | const watchEffect: typeof import('vue')['watchEffect']
68 | const watchPostEffect: typeof import('vue')['watchPostEffect']
69 | const watchSyncEffect: typeof import('vue')['watchSyncEffect']
70 | }
71 |
--------------------------------------------------------------------------------
/web/components.d.ts:
--------------------------------------------------------------------------------
1 | // generated by unplugin-vue-components
2 | // We suggest you to commit this file into source control
3 | // Read more: https://github.com/vuejs/core/pull/3399
4 | import '@vue/runtime-core'
5 |
6 | export {}
7 |
8 | declare module '@vue/runtime-core' {
9 | export interface GlobalComponents {
10 | ButtonBox: typeof import('./src/components/ButtonBox/ButtonBox.vue')['default']
11 | ElButton: typeof import('element-plus/es')['ElButton']
12 | ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
13 | ElCollapse: typeof import('element-plus/es')['ElCollapse']
14 | ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
15 | ElDialog: typeof import('element-plus/es')['ElDialog']
16 | ElForm: typeof import('element-plus/es')['ElForm']
17 | ElFormItem: typeof import('element-plus/es')['ElFormItem']
18 | ElInput: typeof import('element-plus/es')['ElInput']
19 | HeadPortrait: typeof import('./src/components/HeadPortrait/HeadPortrait.vue')['default']
20 | RouterLink: typeof import('vue-router')['RouterLink']
21 | RouterView: typeof import('vue-router')['RouterView']
22 | SvgIcon: typeof import('./src/components/SvgIcon/SvgIcon.vue')['default']
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/web/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | interface ImportMetaEnv {
4 | VITE_MODE_NAME: string;
5 | VITE_LOGIN_TEST: string;
6 | VITE_HTTP_URL: string;
7 | VITE_WS_URL: string;
8 | VITE_APP_TITLE: string;
9 | }
10 | interface Window {
11 | SimplePeer: any;
12 | }
13 |
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | QuickChat
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "quick-chat-web",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "build": "run-p type-check build-only",
6 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
7 | "build-only": "vite build",
8 | "dev": "vite --mode dev",
9 | "preview": "vite preview --port 4173",
10 | "type-check": "vue-tsc --noEmit"
11 | },
12 | "dependencies": {
13 | "axios": "^0.27.2",
14 | "element-plus": "^2.2.17",
15 | "lodash": "^4.17.21",
16 | "moment": "^2.29.4",
17 | "pinia": "^2.0.21",
18 | "simple-peer": "^9.11.1",
19 | "uuid": "^9.0.0",
20 | "vue": "^3.2.38",
21 | "vue-router": "^4.1.5",
22 | "webrtc-adapter": "^8.1.2"
23 | },
24 | "devDependencies": {
25 | "@rushstack/eslint-patch": "^1.1.4",
26 | "@types/lodash": "^4.14.185",
27 | "@types/simple-peer": "^9.11.5",
28 | "@types/uuid": "^8.3.4",
29 | "@vitejs/plugin-vue": "^3.0.3",
30 | "@vue/eslint-config-prettier": "^7.0.0",
31 | "@vue/eslint-config-typescript": "^11.0.0",
32 | "@vue/tsconfig": "^0.1.3",
33 | "eslint": "^8.22.0",
34 | "eslint-plugin-vue": "^9.3.0",
35 | "less": "^4.1.3",
36 | "less-loader": "^11.0.0",
37 | "npm-run-all": "^4.1.5",
38 | "prettier": "^2.7.1",
39 | "style-resources-loader": "^1.4.1",
40 | "typescript": "~4.7.4",
41 | "unplugin-auto-import": "^0.11.2",
42 | "unplugin-vue-components": "^0.22.7",
43 | "vite": "^3.0.9",
44 | "vite-plugin-svg-icons": "^2.0.1",
45 | "vue-cli-plugin-style-resources-loader": "~0.1.5",
46 | "vue-tsc": "^0.40.7"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lvboda/quick-chat/8be65b27faae70756b8c708a4d1756f0122d6a9f/web/public/favicon.ico
--------------------------------------------------------------------------------
/web/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/web/src/api/common.ts:
--------------------------------------------------------------------------------
1 | // 性别
2 | export enum GENDER {
3 | male = 1,
4 | female = 2,
5 | }
6 |
7 | // 角色类型
8 | export enum USER_ROLE {
9 | common = 1,
10 | manager = 2,
11 | }
12 |
13 | // 服务端状态码
14 | export enum STATUS {
15 | ok = 200,
16 | error = 500,
17 | }
18 |
19 | // 响应体结构
20 | export type Result = {
21 | status: STATUS;
22 | message: string;
23 | data: T;
24 | };
25 |
26 | export function checkStatus(status: STATUS): boolean {
27 | if (status === STATUS.ok) return true;
28 | return false;
29 | }
30 |
--------------------------------------------------------------------------------
/web/src/api/contact.ts:
--------------------------------------------------------------------------------
1 | import request from "@/utils/http-request";
2 | export { checkStatus } from "@/api/common";
3 |
4 | import type { Result } from "@/api/common";
5 | import type { UserInfo } from "@/api/user";
6 |
7 | export type Relation = {
8 | id: string;
9 | userId: string;
10 | friendId: string;
11 | relationType: 1 | 2 | 3;
12 | roleType: 1 | 2;
13 | memo: string;
14 | friendInfo: UserInfo;
15 | proposerInfo: UserInfo;
16 | };
17 |
18 | async function commonQueryList(
19 | params: Pick
20 | ) {
21 | const { data } = await request.post>(
22 | "/api/v1/relation/list",
23 | params
24 | );
25 |
26 | return data;
27 | }
28 |
29 | // 添加好友
30 | export async function addFriend(userId: string, friendId: string) {
31 | const { data } = await request.post>("/api/v1/relation", {
32 | userId,
33 | friendId,
34 | roleType: 1,
35 | });
36 |
37 | return data;
38 | }
39 |
40 | // 发送添加好友验证
41 | export async function sendFriendValidate(
42 | params: Pick
43 | ) {
44 | const { data } = await request.post>(
45 | "/api/v1/relation/validate",
46 | { ...params, roleType: 1 }
47 | );
48 |
49 | return data;
50 | }
51 |
52 | // 好友验证列表
53 | export async function queryFriendValidateList(userId: string) {
54 | return commonQueryList({
55 | friendId: userId,
56 | relationType: 1,
57 | roleType: 1,
58 | });
59 | }
60 |
61 | // 好友列表
62 | export async function queryFriendList(userId: string) {
63 | return commonQueryList({
64 | friendId: userId,
65 | relationType: 2,
66 | roleType: 1,
67 | });
68 | }
69 |
70 | // 被删除好友列表
71 | export async function queryBeDeletedFriendList(userId: string) {
72 | return commonQueryList({
73 | friendId: userId,
74 | relationType: 3,
75 | roleType: 1,
76 | });
77 | }
78 |
79 | // 发送添加群验证
80 | export async function sendGroupValidate(
81 | params: Pick
82 | ) {
83 | const { data } = await request.post>(
84 | "/api/v1/relation/validate",
85 | { ...params, roleType: 2 }
86 | );
87 |
88 | return data;
89 | }
90 |
91 | // 添加群
92 | export async function addGroup(userId: string, friendId: string) {
93 | const { data } = await request.post>("/api/v1/relation", {
94 | userId,
95 | friendId,
96 | roleType: 1,
97 | });
98 |
99 | return data;
100 | }
101 |
102 | // 群验证列表
103 | export async function queryGroupValidateList(userId: string) {
104 | return commonQueryList({
105 | friendId: userId,
106 | relationType: 1,
107 | roleType: 2,
108 | });
109 | }
110 |
111 | // 群列表
112 | export async function queryGroupList(userId: string) {
113 | return commonQueryList({
114 | friendId: userId,
115 | relationType: 2,
116 | roleType: 2,
117 | });
118 | }
119 |
120 | // 被移除群列表
121 | export async function queryBeDeletedGroupList(userId: string) {
122 | return commonQueryList({
123 | friendId: userId,
124 | relationType: 2,
125 | roleType: 2,
126 | });
127 | }
128 |
129 | export type Contacts = {
130 | validateList: Relation[];
131 | friendList: Relation[];
132 | groupList: Relation[];
133 | };
134 |
135 | // 查所有
136 | export async function queryList(userId: string): Promise {
137 | const { data: friendValidateList } = await queryFriendValidateList(userId);
138 | const { data: groupValidateList } = await queryGroupValidateList(userId);
139 |
140 | const { data: friendList } = await queryFriendList(userId);
141 | const { data: beDeletedFriendList } = await queryBeDeletedFriendList(userId);
142 |
143 | const { data: groupList } = await queryGroupList(userId);
144 | const { data: beDeletedGroupList } = await queryBeDeletedGroupList(userId);
145 |
146 | return {
147 | validateList: [...friendValidateList, ...groupValidateList],
148 | friendList: [...friendList, ...beDeletedFriendList],
149 | groupList: [...groupList, ...beDeletedGroupList],
150 | };
151 | }
152 |
--------------------------------------------------------------------------------
/web/src/api/user.ts:
--------------------------------------------------------------------------------
1 | import request from "@/utils/http-request";
2 | export { checkStatus } from "@/api/common";
3 | import type { GENDER, USER_ROLE, Result } from "@/api/common";
4 |
5 | export type UserInfo = {
6 | id: string;
7 | userId: string;
8 | password: string;
9 | nickName: string;
10 | gender: GENDER;
11 | mobile: number;
12 | userRole: USER_ROLE;
13 | signature: string;
14 | face: string;
15 | token: string;
16 | };
17 |
18 | export type LoginParams = Pick;
19 |
20 | export type RegisterParams = Required<
21 | Pick
22 | >;
23 |
24 | export function getDefaultUserInfo(userInfo?: any): UserInfo {
25 | return {
26 | id: "",
27 | userId: "",
28 | password: "",
29 | nickName: "",
30 | gender: NaN,
31 | mobile: NaN,
32 | userRole: NaN,
33 | signature: "",
34 | face: "",
35 | token: "",
36 | ...userInfo,
37 | };
38 | }
39 |
40 | export async function register(params: RegisterParams) {
41 | const { data } = await request.post>(
42 | "/api/v1/user/register",
43 | params
44 | );
45 |
46 | return data;
47 | }
48 |
49 | export async function login(params: LoginParams) {
50 | const { data } = await request.post>(
51 | "/api/v1/user/login",
52 | params
53 | );
54 |
55 | return data;
56 | }
57 |
58 | export async function queryFriend(friendId: string) {
59 | const { data } = await request.get>(
60 | `/api/v1/user/${friendId}`
61 | );
62 |
63 | return data;
64 | }
65 |
--------------------------------------------------------------------------------
/web/src/components/ButtonBox/ButtonBox.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
29 |
--------------------------------------------------------------------------------
/web/src/components/HeadPortrait/HeadPortrait.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
![]()
16 |
17 |
18 |
19 |
20 |
33 |
--------------------------------------------------------------------------------
/web/src/components/SvgIcon/SvgIcon.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
20 |
21 |
22 |
31 |
--------------------------------------------------------------------------------
/web/src/hooks/use-chat.ts:
--------------------------------------------------------------------------------
1 | import useUserStore from "@/stores/user";
2 | import {
3 | // onReceiveICEOffer,
4 | // onReceiveICECandidate,
5 | // onReceiveICEAnswer,
6 | onSignal,
7 | } from "@/utils/web-rtc";
8 | import WebsocketClient, {
9 | createChatClient,
10 | createMessage,
11 | } from "@/utils/websocket";
12 |
13 | import type { Message } from "@/utils/websocket";
14 |
15 | export let chatClient: WebsocketClient | null = null;
16 |
17 | function useChat() {
18 | const { userInfo } = storeToRefs(useUserStore());
19 |
20 | // 创建ws连接
21 | if (chatClient === null) chatClient = createChatClient(userInfo.value.userId);
22 |
23 | const onMessage = $ref<{ cb: (msg: Message) => void }>({
24 | cb: () => {},
25 | });
26 |
27 | // 存储全局聊天记录 key: friendId, value: 聊天记录list
28 | // 劫持发送和接收函数, 把数据存进来
29 | const globalChatMap = $ref