├── .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 | ![](./images/login.png "登陆") 16 | **登陆** 17 | 18 | ![](./images/massage.png "消息") 19 | **消息** 20 | 21 | ![](./images/contact.png "联系") 22 | **联系** 23 | 24 | ![](./images/chat.png "文字") 25 | **文字** 26 | 27 | ![](./images/video.png "视频") 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 | 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 | 14 | 15 | 29 | -------------------------------------------------------------------------------- /web/src/components/HeadPortrait/HeadPortrait.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | 20 | 33 | -------------------------------------------------------------------------------- /web/src/components/SvgIcon/SvgIcon.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 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>(new Map()); 30 | // get 31 | function getChatBy(uid: string): Message[] { 32 | return globalChatMap.get(uid) || []; 33 | } 34 | // set 35 | function setChat(uid: string, msg: Message) { 36 | if (!globalChatMap.has(uid)) globalChatMap.set(uid, []); 37 | globalChatMap.get(uid)?.push(msg); 38 | } 39 | 40 | chatClient.on("error", () => 41 | ElMessage.error("连接服务端失败, 请尝试切换网络环境") 42 | ); 43 | 44 | chatClient.on("message", (event: any) => { 45 | if (!event || !event.data) return; 46 | 47 | const msg = JSON.parse(event.data) as Message; 48 | 49 | switch (msg.contentType) { 50 | case 1: 51 | onMessage.cb(msg); 52 | setChat(msg.senderId, msg); 53 | break; 54 | case 2: 55 | onMessage.cb(msg); 56 | onSignal(msg); 57 | // onReceiveICEOffer(msg); 58 | break; 59 | case 3: 60 | // onReceiveICECandidate(msg); 61 | break; 62 | case 4: 63 | // onReceiveICEAnswer(msg); 64 | break; 65 | } 66 | }); 67 | 68 | function sendMessage(rid: string, content: string, contentType?: number) { 69 | const msg = createMessage(rid, content, contentType); 70 | setChat(msg.receiverId, msg); 71 | chatClient?.send(msg); 72 | } 73 | 74 | return { chatClient, getChatBy, onMessage, sendMessage }; 75 | } 76 | 77 | export default useChat; 78 | -------------------------------------------------------------------------------- /web/src/hooks/use-common.ts: -------------------------------------------------------------------------------- 1 | import { getFromLocal } from "@/utils/storage"; 2 | import useUserStore from "@/stores/user"; 3 | import useContactStore from "@/stores/contact"; 4 | 5 | import type { UserInfo } from "@/api/user"; 6 | 7 | function useCommon() { 8 | const router = useRouter(); 9 | 10 | const localUserInfo = getFromLocal("userInfo"); 11 | 12 | if (!localUserInfo) { 13 | ElMessage.error("用户信息获取失败, 请重新登录"); 14 | router.push({ path: "/login" }); 15 | return; 16 | } 17 | 18 | const { userInfo } = storeToRefs(useUserStore()); 19 | userInfo.value = localUserInfo; 20 | 21 | const { flushContacts } = useContactStore(); 22 | flushContacts(); 23 | } 24 | 25 | export default useCommon; 26 | -------------------------------------------------------------------------------- /web/src/layouts/NavigationBar/NavigationBar.vue: -------------------------------------------------------------------------------- 1 | 32 | 48 | 49 | 73 | -------------------------------------------------------------------------------- /web/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import Lib from "@/plugins/lib"; 3 | import App from "@/App.vue"; 4 | 5 | import "virtual:svg-icons-register"; 6 | import "@/styles/index.less"; 7 | 8 | const app = createApp(App); 9 | app.use(Lib); 10 | app.mount("#app"); 11 | -------------------------------------------------------------------------------- /web/src/plugins/lib.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue"; 2 | 3 | import { createPinia } from "pinia"; 4 | import router from "@/router"; 5 | 6 | import SvgIcon from "@/components/SvgIcon/SvgIcon.vue"; 7 | 8 | export default { 9 | install(app: App) { 10 | app.use(createPinia()); 11 | app.use(router); 12 | // svg组件 13 | app.component("svg-icon", SvgIcon); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /web/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from "vue-router"; 2 | 3 | import LoginView from "@/views/LoginView/LoginView.vue"; 4 | import HomeView from "@/views/HomeView/HomeView.vue"; 5 | import HomeChatView from "@/views/HomeView/HomeChatView.vue"; 6 | import HomeContactView from "@/views/HomeView/HomeContactView.vue"; 7 | import HomeMomentView from "@/views/HomeView/HomeMomentView.vue"; 8 | import HomeMyView from "@/views/HomeView/HomeMyView.vue"; 9 | 10 | export const routes = [ 11 | { 12 | path: "/", 13 | redirect: "/login", 14 | }, 15 | { 16 | path: "/login", 17 | name: "login", 18 | component: LoginView, 19 | }, 20 | { 21 | path: "/home", 22 | name: "home", 23 | redirect: "/home/chat", 24 | component: HomeView, 25 | children: [ 26 | { path: "/home/chat", name: "chat", component: HomeChatView }, 27 | { path: "/home/contact", name: "contact", component: HomeContactView }, 28 | { path: "/home/moment", name: "moment", component: HomeMomentView }, 29 | { path: "/home/my", name: "my", component: HomeMyView }, 30 | ], 31 | }, 32 | ]; 33 | 34 | const router = createRouter({ 35 | history: createWebHashHistory(import.meta.env.BASE_URL), 36 | routes, 37 | }); 38 | 39 | export default router; 40 | -------------------------------------------------------------------------------- /web/src/stores/contact.ts: -------------------------------------------------------------------------------- 1 | import { queryList } from "@/api/contact"; 2 | 3 | import useUserStore from "@/stores/user"; 4 | 5 | import type { Contacts, Relation } from "@/api/contact"; 6 | 7 | const useContactStore = defineStore("contact", () => { 8 | const { userInfo } = storeToRefs(useUserStore()); 9 | 10 | const validateList = ref([]); 11 | const friendList = ref([]); 12 | const groupList = ref([]); 13 | 14 | function setContacts(data: Contacts) { 15 | validateList.value = data.validateList; 16 | friendList.value = data.friendList; 17 | groupList.value = data.groupList; 18 | } 19 | 20 | function flushContacts() { 21 | queryList(userInfo.value.userId).then(setContacts); 22 | } 23 | 24 | watch(userInfo, flushContacts); 25 | 26 | return { validateList, friendList, groupList, flushContacts }; 27 | }); 28 | 29 | export default useContactStore; 30 | -------------------------------------------------------------------------------- /web/src/stores/user.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultUserInfo } from "@/api/user"; 2 | import type { UserInfo } from "@/api/user"; 3 | 4 | const useUserStore = defineStore("user", () => { 5 | const userInfo = ref(getDefaultUserInfo()); 6 | 7 | return { userInfo }; 8 | }); 9 | 10 | export default useUserStore; 11 | -------------------------------------------------------------------------------- /web/src/styles/index.less: -------------------------------------------------------------------------------- 1 | body { 2 | width: 100vw; 3 | height: 100vh; 4 | margin: 0; 5 | padding: 0; 6 | font-family: tahoma,"microsoft yahei" !important; 7 | } 8 | 9 | #app { 10 | width: 100%; 11 | height: 100%; 12 | } 13 | -------------------------------------------------------------------------------- /web/src/utils/constant.ts: -------------------------------------------------------------------------------- 1 | export const iceServer = { 2 | iceServers: [ 3 | { 4 | urls: "turn:www.lvboda.cn:3478", 5 | username: "root", 6 | credential: "BD1010110", 7 | }, 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /web/src/utils/http-request.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse, AxiosRequestConfig } from "axios"; 2 | 3 | import { getFromLocal } from "@/utils/storage"; 4 | 5 | import type { UserInfo } from "@/api/user"; 6 | 7 | const router = useRouter(); 8 | 9 | const request = axios.create({ 10 | baseURL: `${import.meta.env.BASE_URL}apis`, 11 | }); 12 | 13 | export enum STATUS_CODE { 14 | success = 200, 15 | error = 500, 16 | } 17 | 18 | async function handleRequestSuccess( 19 | request: AxiosRequestConfig 20 | ): Promise { 21 | const userInfo = getFromLocal("userInfo"); 22 | 23 | if (request.headers && userInfo) { 24 | request.headers.Authorization = `Bearer ${userInfo.token}`; 25 | } 26 | 27 | return request; 28 | } 29 | 30 | async function handleResponseSuccess( 31 | response: AxiosResponse 32 | ): Promise { 33 | if (response.status === 401) { 34 | router.replace({ path: "/login" }); 35 | } 36 | 37 | if (response.status !== STATUS_CODE.success) { 38 | console.error("http-request:response.status not the success code."); 39 | } 40 | 41 | return response; 42 | } 43 | 44 | async function handleResponseError(error: unknown): Promise { 45 | console.error("http-request:responseErr: ", error); 46 | return Promise.reject(error); 47 | } 48 | 49 | request.interceptors.request.use(handleRequestSuccess); 50 | request.interceptors.response.use(handleResponseSuccess, handleResponseError); 51 | 52 | export default request; 53 | -------------------------------------------------------------------------------- /web/src/utils/is-dev.ts: -------------------------------------------------------------------------------- 1 | function isDev(): boolean { 2 | return import.meta.env.VITE_MODE_NAME === "development"; 3 | } 4 | 5 | export default isDev; 6 | -------------------------------------------------------------------------------- /web/src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | function genKey(key: string, ns = "[ROOT]"): string { 2 | return `${ns}:${key}`; 3 | } 4 | export function getFromLocal(key: string, ns?: string): T | null { 5 | const str = window.localStorage.getItem(genKey(key, ns)); 6 | return str ? JSON.parse(str) : null; 7 | } 8 | 9 | export function setToLocal(key: string, value: any, ns?: string): void { 10 | window.localStorage.setItem(genKey(key, ns), JSON.stringify(value)); 11 | } 12 | 13 | export function removeFromLocal(key: string, ns?: string): void { 14 | window.localStorage.removeItem(genKey(key, ns)); 15 | } 16 | -------------------------------------------------------------------------------- /web/src/utils/wait.ts: -------------------------------------------------------------------------------- 1 | export async function wait(ms: number): Promise { 2 | return new Promise((res) => setTimeout(res, ms)); 3 | } 4 | 5 | export async function waitCall(fn: () => T, ms: number): Promise { 6 | await new Promise((res) => setTimeout(res, ms)); 7 | return fn(); 8 | } 9 | -------------------------------------------------------------------------------- /web/src/utils/web-rtc.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 2 | import _ from "webrtc-adapter"; 3 | 4 | import { chatClient } from "@/hooks/use-chat"; 5 | import { createMessage } from "@/utils/websocket"; 6 | import { iceServer } from "@/utils/constant"; 7 | 8 | import type { Message } from "@/utils/websocket"; 9 | 10 | let pc: RTCPeerConnection | null = null; 11 | const senderList: RTCRtpSender[] = []; 12 | 13 | let peer: any = null; 14 | let iceRemoteVideo: HTMLVideoElement | null = null; 15 | let iceLocalVideo: HTMLVideoElement | null = null; 16 | export async function createPeer(isSender: boolean, receiverId: string) { 17 | peer = new window.SimplePeer({ initiator: isSender, config: iceServer }); 18 | 19 | peer.on("signal", (signal: any) => { 20 | const msg = createMessage(receiverId, JSON.stringify(signal), 2); 21 | chatClient?.send(msg); 22 | }); 23 | 24 | peer.on("stream", (stream: any) => { 25 | iceRemoteVideo = document.getElementById("iceRemoteVideo") as HTMLVideoElement; 26 | if (iceRemoteVideo) iceRemoteVideo.srcObject = stream; 27 | }); 28 | 29 | const webcamStream = await navigator.mediaDevices.getUserMedia({ 30 | video: true, 31 | audio: true, 32 | }); 33 | 34 | const ms = new MediaStream(); 35 | webcamStream.getVideoTracks().forEach((track) => { 36 | ms.addTrack(track); 37 | }); 38 | 39 | iceLocalVideo = document.getElementById("iceLocalVideo") as HTMLVideoElement; 40 | if (iceLocalVideo) iceLocalVideo.srcObject = ms; 41 | 42 | peer.addStream(webcamStream); 43 | 44 | return () => { 45 | if (iceRemoteVideo) iceRemoteVideo.srcObject = null; 46 | if (iceLocalVideo) iceLocalVideo.srcObject = null; 47 | peer.removeStream(webcamStream); 48 | webcamStream.getTracks().forEach((item) => item.stop()); 49 | ms.getTracks().forEach((item) => item.stop()); 50 | peer.destroy(); 51 | }; 52 | } 53 | 54 | export async function onSignal(msg: Message) { 55 | if (!peer) await createPeer(false, msg.senderId); 56 | peer.signal(JSON.parse(msg.content)); 57 | } 58 | 59 | export async function createRTCPeerConnection( 60 | isSender: boolean, 61 | receiverId: string 62 | ) { 63 | pc = new RTCPeerConnection(); 64 | pc.setConfiguration(iceServer); 65 | 66 | let iceRemoteVideo: HTMLVideoElement | null = null; 67 | let iceLocalVideo: HTMLVideoElement | null = null; 68 | 69 | pc.addEventListener("icecandidate", (event) => { 70 | console.log(111); 71 | if (!event.candidate) return; 72 | console.log(222); 73 | const msg = createMessage(receiverId, JSON.stringify(event.candidate), 3); 74 | chatClient?.send(msg); 75 | }); 76 | 77 | pc.ontrack = (event) => { 78 | iceRemoteVideo = document.getElementById("iceRemoteVideo") as HTMLVideoElement; 79 | if (iceRemoteVideo) iceRemoteVideo.srcObject = event.streams[0]; 80 | }; 81 | 82 | const webcamStream = await navigator.mediaDevices.getUserMedia({ 83 | video: true, 84 | audio: true, 85 | }); 86 | 87 | iceLocalVideo = document.getElementById("iceLocalVideo") as HTMLVideoElement; 88 | if (iceLocalVideo) { 89 | const ms = new MediaStream(); 90 | webcamStream.getVideoTracks().forEach((track) => { 91 | ms.addTrack(track); 92 | }); 93 | iceLocalVideo.srcObject = ms; 94 | } 95 | 96 | webcamStream.getTracks().forEach((track) => { 97 | const sender = pc?.addTrack(track, webcamStream); 98 | if (sender) senderList.push(sender); 99 | }); 100 | 101 | if (isSender) { 102 | const offer = await pc.createOffer(); 103 | await pc.setLocalDescription(offer); 104 | const msg = createMessage( 105 | receiverId, 106 | JSON.stringify(pc?.localDescription), 107 | 2 108 | ); 109 | chatClient?.send(msg); 110 | } 111 | 112 | return () => { 113 | if (iceRemoteVideo) iceRemoteVideo.srcObject = null; 114 | if (iceLocalVideo) iceLocalVideo.srcObject = null; 115 | webcamStream.getTracks().forEach((item) => item.stop()); 116 | senderList.forEach((item) => pc?.removeTrack(item)); 117 | pc?.close(); 118 | }; 119 | } 120 | 121 | export async function onReceiveICEOffer(msg: Message) { 122 | await createRTCPeerConnection(false, msg.senderId); 123 | if (!pc) return; 124 | 125 | await pc.setRemoteDescription( 126 | new RTCSessionDescription(JSON.parse(msg.content)) 127 | ); 128 | const answer = await pc.createAnswer(); 129 | await pc.setLocalDescription(answer); 130 | 131 | chatClient?.send(createMessage(msg.senderId, JSON.stringify(answer), 4)); 132 | } 133 | 134 | export async function onReceiveICECandidate(msg: Message) { 135 | try { 136 | await pc?.addIceCandidate(new RTCIceCandidate(JSON.parse(msg.content))); 137 | } catch (err) { 138 | reportError(err); 139 | } 140 | } 141 | 142 | export async function onReceiveICEAnswer(msg: Message) { 143 | await pc 144 | ?.setRemoteDescription(new RTCSessionDescription(JSON.parse(msg.content))) 145 | .catch(reportError); 146 | } 147 | -------------------------------------------------------------------------------- /web/src/utils/websocket.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import { v4 as UUID } from "uuid"; 3 | 4 | import { getFromLocal } from "@/utils/storage"; 5 | import { wait } from "@/utils/wait"; 6 | 7 | import type { UserInfo } from "@/api/user"; 8 | 9 | const SOCKET_URL = new URL( 10 | `${import.meta.env.BASE_URL}ws`, 11 | `wss://${location.host}` 12 | ); 13 | 14 | export type Message = { 15 | id: string; // id 16 | senderId: string; // 发送者id 17 | receiverId: string; // 接受者id 18 | content: string; // 消息内容 19 | extra: string; // 附加信息 20 | contentType: number; // 消息类型 1文本类型 2ice-offer 3ice-candidate 4ice-answer 21 | processType: number; // 处理类型 1单聊 2群聊 3关闭ws连接 22 | sendTime: string; // 发送时间 23 | resource: any[]; // 源数据 24 | }; 25 | 26 | export function createMessage( 27 | rid: string, 28 | content: string, 29 | contentType?: number, 30 | processType?: number 31 | ): Message { 32 | const userInfo = getFromLocal("userInfo"); 33 | return { 34 | id: UUID(), 35 | senderId: userInfo?.userId || "", 36 | receiverId: rid, 37 | content, 38 | extra: "", 39 | contentType: contentType || 1, 40 | processType: processType || 1, 41 | sendTime: moment().format("YYYY-MM-DD HH:mm:ss"), 42 | resource: [], 43 | }; 44 | } 45 | 46 | class WebsocketClient { 47 | private client: WebSocket | null = null; 48 | 49 | private path = "/"; 50 | 51 | private uid: string | null = null; 52 | 53 | private closeFlag = false; 54 | 55 | private reconnectionMaxTry = 3; 56 | 57 | private waitTime = 3000; 58 | 59 | private listenersMap: Map void>> = new Map(); 60 | 61 | constructor(path: string, uid: string) { 62 | if (!path || !uid) return; 63 | 64 | this.path = path; 65 | this.uid = uid; 66 | 67 | this.setupClient(); 68 | } 69 | 70 | private setupClient(): void { 71 | const client = new WebSocket(`${SOCKET_URL}${this.path}/${this.uid}`); 72 | 73 | client.addEventListener("open", () => { 74 | console.info(`ws:用户${this.uid}开启websocket`); 75 | 76 | this.call("open"); 77 | this.initHeart(); 78 | }); 79 | 80 | client.addEventListener("message", (event) => { 81 | this.call("message", event); 82 | }); 83 | 84 | client.addEventListener("error", (error) => { 85 | console.error( 86 | `ws:用户${this.uid}连接websocket发生错误: ${JSON.stringify(error)}` 87 | ); 88 | this.call("error", error); 89 | }); 90 | 91 | client.addEventListener("close", async () => { 92 | console.info(`ws:用户${this.uid}websocket连接断开`); 93 | 94 | if (this.closeFlag) return; 95 | 96 | if (this.reconnectionMaxTry === 0) { 97 | console.error(`ws:用户${this.uid}websocket重新连接失败`); 98 | this.call("error"); 99 | return; 100 | } 101 | 102 | this.call("close"); 103 | await wait(this.waitTime); 104 | this.reconnectionMaxTry--; 105 | this.setupClient(); 106 | }); 107 | 108 | this.client = client; 109 | } 110 | 111 | private initHeart() { 112 | setInterval(() => this.client?.send("heart"), 1000 * 10); 113 | } 114 | 115 | private call(type: string, event?: Event): void { 116 | const listeners = this.listenersMap.get(type); 117 | if (!listeners) return; 118 | 119 | listeners.forEach((listener) => listener(event)); 120 | } 121 | 122 | on(type: string, listener: (event?: Event | undefined) => void): void { 123 | if (!this.listenersMap.get(type)) { 124 | this.listenersMap.set(type, new Set()); 125 | } 126 | 127 | const listeners = this.listenersMap.get(type); 128 | listeners?.add(listener); 129 | } 130 | 131 | off(type: string, listener: (event?: Event) => void): void { 132 | const listeners = this.listenersMap.get(type); 133 | if (listeners === undefined || listeners === null) return; 134 | listeners.delete(listener); 135 | } 136 | 137 | offAll(): void { 138 | this.listenersMap = new Map(); 139 | } 140 | 141 | close() { 142 | this.closeFlag = true; 143 | this.client?.close(); 144 | } 145 | 146 | send(msg: Message) { 147 | this.client?.send(JSON.stringify(msg)); 148 | } 149 | } 150 | 151 | export function createChatClient(uid: string) { 152 | return new WebsocketClient("/api/v1/chat", uid); 153 | } 154 | 155 | export default WebsocketClient; 156 | -------------------------------------------------------------------------------- /web/src/views/HomeView/HomeChatView.vue: -------------------------------------------------------------------------------- 1 | 105 | 106 | 152 | 153 | 240 | -------------------------------------------------------------------------------- /web/src/views/HomeView/HomeContactView.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 79 | 80 | 150 | -------------------------------------------------------------------------------- /web/src/views/HomeView/HomeMomentView.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/src/views/HomeView/HomeMyView.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/src/views/HomeView/HomeView.vue: -------------------------------------------------------------------------------- 1 | 8 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /web/src/views/HomeView/components/AddModal/AddModal.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 85 | 86 | 119 | -------------------------------------------------------------------------------- /web/src/views/HomeView/components/ChatFrame/ChatFrame.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 127 | 128 | 234 | -------------------------------------------------------------------------------- /web/src/views/HomeView/components/DetailBox/DetailBox.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 84 | 85 | 156 | -------------------------------------------------------------------------------- /web/src/views/HomeView/components/ItemRecord/ItemRecord.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 35 | 36 | 94 | -------------------------------------------------------------------------------- /web/src/views/LoginView/LoginBox.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 87 | 88 | 113 | -------------------------------------------------------------------------------- /web/src/views/LoginView/LoginView.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 29 | 30 | 88 | -------------------------------------------------------------------------------- /web/src/views/LoginView/RegisterBox.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 96 | 97 | 128 | -------------------------------------------------------------------------------- /web/tsconfig.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.node.json", 3 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "types": ["node"] 7 | } 8 | } -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.web.json", 3 | "include": [ 4 | "env.d.ts", 5 | "src/**/*", 6 | "src/**/*.vue", 7 | "auto-imports.d.ts", 8 | "components.d.ts" 9 | ], 10 | "compilerOptions": { 11 | "baseUrl": ".", 12 | "isolatedModules": false, 13 | "paths": { 14 | "@/*": ["./src/*"] 15 | }, 16 | "types": ["vue/ref-macros"] 17 | }, 18 | 19 | "references": [ 20 | { 21 | "path": "./tsconfig.config.json" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from "node:url"; 2 | import path from "path"; 3 | 4 | import { defineConfig } from "vite"; 5 | import vue from "@vitejs/plugin-vue"; 6 | import { createSvgIconsPlugin } from "vite-plugin-svg-icons"; 7 | import AutoImport from "unplugin-auto-import/vite"; 8 | import Components from "unplugin-vue-components/vite"; 9 | import { ElementPlusResolver } from "unplugin-vue-components/resolvers"; 10 | 11 | export default defineConfig(() => { 12 | return { 13 | base: "/quick-chat/", 14 | server: { 15 | port: 5173, 16 | strictPort: true, 17 | }, 18 | plugins: [ 19 | vue({ reactivityTransform: true }), 20 | createSvgIconsPlugin({ 21 | iconDirs: [path.resolve(process.cwd(), "src/assets/icons")], 22 | symbolId: "icon-[dir]-[name]", 23 | }), 24 | AutoImport({ 25 | imports: ["vue", "vue-router", "pinia"], 26 | eslintrc: { 27 | enabled: false, 28 | filepath: "./.eslintrc-auto-import.json", 29 | globalsPropValue: true, 30 | }, 31 | resolvers: [ElementPlusResolver()], 32 | }), 33 | Components({ 34 | resolvers: [ElementPlusResolver()], 35 | }), 36 | ], 37 | resolve: { 38 | alias: { 39 | "@": fileURLToPath(new URL("./src", import.meta.url)), 40 | }, 41 | }, 42 | }; 43 | }); 44 | -------------------------------------------------------------------------------- /web/vue.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('@vue/cli-service') 2 | module.exports = defineConfig({ 3 | pluginOptions: { 4 | 'style-resources-loader': { 5 | preProcessor: 'less', 6 | patterns: [] 7 | } 8 | } 9 | }) 10 | --------------------------------------------------------------------------------