From a5ae680f9450410452de2321cc29c56d55fa7c63 Mon Sep 17 00:00:00 2001 From: Echo <1711788888@qq.com> Date: Wed, 14 May 2025 16:51:31 +0800 Subject: [PATCH] =?UTF-8?q?:art:=20=20=E6=B7=BB=E5=8A=A0=E5=9B=BE=E5=BA=93?= =?UTF-8?q?=E5=92=8C=E5=AE=A2=E6=9C=8D=E6=8F=92=E4=BB=B6(=E6=9C=89?= =?UTF-8?q?=E9=97=AE=E9=A2=98-=E5=BE=85=E4=BF=AE=E6=94=B9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 + go.sum | 4 + initialize/router.go | 5 + model/system/Class.go | 12 - plugin/customerservice/README.md | 70 +++ plugin/customerservice/api/api.go | 407 +++++++++++++++ plugin/customerservice/api/enter.go | 8 + plugin/customerservice/api/service.go | 484 ++++++++++++++++++ plugin/customerservice/config/config.go | 1 + plugin/customerservice/global/global.go | 1 + plugin/customerservice/main.go | 214 ++++++++ plugin/customerservice/middleware/jwt.go | 57 +++ plugin/customerservice/model/model.go | 25 + plugin/customerservice/model/sysService.go | 18 + plugin/customerservice/model/sysServiceMsg.go | 18 + .../customerservice/model/sysServiceRecord.go | 21 + .../customerservice/model/sysServiceReply.go | 16 + .../customerservice/model/sysServiceScript.go | 15 + plugin/customerservice/model/sysTestUser.go | 11 + plugin/customerservice/router/enter.go | 7 + plugin/customerservice/router/router.go | 48 ++ plugin/customerservice/service/enter.go | 7 + plugin/customerservice/service/service.go | 130 +++++ plugin/customerservice/service/ws/ws.go | 271 ++++++++++ plugin/customerservice/tools/jwt.go | 54 ++ plugin/customerservice/tools/limits.go | 77 +++ plugin/customerservice/tools/timeformat.go | 20 + plugin/picturelibrary/README.md | 113 ++++ plugin/picturelibrary/api/api.go | 230 +++++++++ plugin/picturelibrary/api/enter.go | 11 + plugin/picturelibrary/config/config.go | 1 + plugin/picturelibrary/global/global.go | 1 + plugin/picturelibrary/main.go | 77 +++ plugin/picturelibrary/model/model.go | 8 + plugin/picturelibrary/model/sysAttachment.go | 14 + .../model/sysAttachmentCatategory.go | 13 + plugin/picturelibrary/router/enter.go | 7 + plugin/picturelibrary/router/router.go | 22 + plugin/picturelibrary/service/enter.go | 7 + plugin/picturelibrary/service/service.go | 70 +++ test/rain_test.go | 27 + 41 files changed, 2592 insertions(+), 12 deletions(-) delete mode 100644 model/system/Class.go create mode 100644 plugin/customerservice/README.md create mode 100644 plugin/customerservice/api/api.go create mode 100644 plugin/customerservice/api/enter.go create mode 100644 plugin/customerservice/api/service.go create mode 100644 plugin/customerservice/config/config.go create mode 100644 plugin/customerservice/global/global.go create mode 100644 plugin/customerservice/main.go create mode 100644 plugin/customerservice/middleware/jwt.go create mode 100644 plugin/customerservice/model/model.go create mode 100644 plugin/customerservice/model/sysService.go create mode 100644 plugin/customerservice/model/sysServiceMsg.go create mode 100644 plugin/customerservice/model/sysServiceRecord.go create mode 100644 plugin/customerservice/model/sysServiceReply.go create mode 100644 plugin/customerservice/model/sysServiceScript.go create mode 100644 plugin/customerservice/model/sysTestUser.go create mode 100644 plugin/customerservice/router/enter.go create mode 100644 plugin/customerservice/router/router.go create mode 100644 plugin/customerservice/service/enter.go create mode 100644 plugin/customerservice/service/service.go create mode 100644 plugin/customerservice/service/ws/ws.go create mode 100644 plugin/customerservice/tools/jwt.go create mode 100644 plugin/customerservice/tools/limits.go create mode 100644 plugin/customerservice/tools/timeformat.go create mode 100644 plugin/picturelibrary/README.md create mode 100644 plugin/picturelibrary/api/api.go create mode 100644 plugin/picturelibrary/api/enter.go create mode 100644 plugin/picturelibrary/config/config.go create mode 100644 plugin/picturelibrary/global/global.go create mode 100644 plugin/picturelibrary/main.go create mode 100644 plugin/picturelibrary/model/model.go create mode 100644 plugin/picturelibrary/model/sysAttachment.go create mode 100644 plugin/picturelibrary/model/sysAttachmentCatategory.go create mode 100644 plugin/picturelibrary/router/enter.go create mode 100644 plugin/picturelibrary/router/router.go create mode 100644 plugin/picturelibrary/service/enter.go create mode 100644 plugin/picturelibrary/service/service.go create mode 100644 test/rain_test.go diff --git a/go.mod b/go.mod index 1e2c190..81a8446 100644 --- a/go.mod +++ b/go.mod @@ -101,11 +101,13 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.24.0 // indirect github.com/gofrs/flock v0.12.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect diff --git a/go.sum b/go.sum index 21b3906..4d953f8 100644 --- a/go.sum +++ b/go.sum @@ -233,6 +233,8 @@ github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= @@ -301,6 +303,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/initialize/router.go b/initialize/router.go index d0d3ee7..62ed82e 100644 --- a/initialize/router.go +++ b/initialize/router.go @@ -1,6 +1,8 @@ package initialize import ( + "git.echol.cn/loser/lckt/plugin/customerservice" + "git.echol.cn/loser/lckt/plugin/picturelibrary" "net/http" "os" @@ -104,6 +106,9 @@ func Routers() *gin.Engine { // 注册业务路由 initBizRouter(PrivateGroup, PublicGroup) + PluginInit(PublicGroup, customerservice.CreateCustomerServicePlug()) + PluginInit(PrivateGroup, picturelibrary.CreatePictureLibraryPlug()) + global.GVA_ROUTERS = Router.Routes() global.GVA_LOG.Info("router register success") diff --git a/model/system/Class.go b/model/system/Class.go deleted file mode 100644 index 5417f9f..0000000 --- a/model/system/Class.go +++ /dev/null @@ -1,12 +0,0 @@ -package system - -import "git.echol.cn/loser/lckt/global" - -type Class struct { - global.GVA_MODEL - Name string `json:"name" gorm:"comment:分类名"` -} - -func (c *Class) TableName() string { - return "class" -} diff --git a/plugin/customerservice/README.md b/plugin/customerservice/README.md new file mode 100644 index 0000000..74fc68f --- /dev/null +++ b/plugin/customerservice/README.md @@ -0,0 +1,70 @@ +[## GVA 客服聊天功能[前端用户聊天基于gva-shop的uniapp端] + +### 手动安装方法 + + 1.解压zip获得customerservice文件夹 + 2.将 customerservice/web/plugin/customerservice 放置在web/plugin下 + 3.将 customerservice/server/plugin/customerservice 放置在server/plugin下 + 4.将 customerservice/uni 下的文件放到gva-shop商城插件下的uni里 + 5.在gva-shop的uni下的pages.json里新增: + { + "path" : "pages/service/index", + "style" : + { + "navigationBarTitleText" : "客服" + } + } +#### 1. 前往GVA主程序下的initialize/router.go 在Routers 方法最末尾按照你需要的及安全模式添加本插件 + PluginInit(PublicGroup, customerservice.CreateCustomerServicePlug()) + 到gva系统,角色管理,分配角色的api权限即可,插件会自动注册api,需要手动分配。 + 注:会自动生成如下表:sys_service、sys_service_msg、sys_service_record、sys_service_reply、sys_service_script +### 2. 配置说明 + +#### 2-1 后台主要功能 + + 客服管理、客服话术、客服自动回复配置等 + +#### 2-2 使用说明 + + 1、在前端vue部分路由需要手动配置: + web/src/router/index.js下新增如下配置: + { + path: '/kefu/login', + name: 'ServiceLogin', + component: () => import('@/plugin/customerservice/view/login/index.vue'), + meta:{ + client:true + } + }, + { + path: '/kefu/main', + name: 'ServiceMain', + component: () => import('@/plugin/customerservice/view/chat/index.vue'), + meta:{ + client:true + } + }, + { + path: '/kefu/test', + name: 'ServiceUserTest', + component: () => import('@/plugin/customerservice/view/chat/test.vue'), + meta:{ + client:true + } + }, + + 2、后台使用方法: + 启动gva项目,安装后在客服列表添加客服,然后可以从客服列表的进入工作台进入客服聊天页,或者打开客服登录页 + http://localhost:8080/#/kefu/login进行登录 + 3、此插件涉及的图片上传使用了插件管理中《图库》插件,可根据自己喜好进行替换 + 4、后台客服websocket连接的地方在插件view/chat/index.vue,连接地址改成自己项目地址, + 客服:websocket.value = new WebSocket(`ws://localhost:8888/service/serve_ws?token=${token.value}`) + + 5、用户websocket连接的地方在uni/pages/service/index.vue下,连接地址改成自己项目地址,websocket.value = new WebSocket(`ws://localhost:8888/service/ws?token=${token.value}`) + + 6、项目没进行过啥大的测试,仅供参考学习 + +#### 2-3 参数说明 + +### 3. 方法API + 无 \ No newline at end of file diff --git a/plugin/customerservice/api/api.go b/plugin/customerservice/api/api.go new file mode 100644 index 0000000..ac99b6a --- /dev/null +++ b/plugin/customerservice/api/api.go @@ -0,0 +1,407 @@ +package api + +import ( + "crypto/md5" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/user" + sysModel "git.echol.cn/loser/lckt/plugin/customerservice/model" + "git.echol.cn/loser/lckt/plugin/customerservice/service" + "git.echol.cn/loser/lckt/plugin/customerservice/service/ws" + "git.echol.cn/loser/lckt/plugin/customerservice/tools" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "go.uber.org/zap" + "net/http" + "path/filepath" + "sort" + "strconv" + "time" +) + +type CustomerServiceApi struct{} + +func (cus *CustomerServiceApi) ServeWs(ctx *gin.Context) { + ws.WsServe(ctx) +} + +func (cus *CustomerServiceApi) ServeWsForKefu(ctx *gin.Context) { + ws.ServeWsForKefu(ctx) +} + +func (cus *CustomerServiceApi) HandleTransfer(c *gin.Context) { + var transferReq struct { + FromAgent string `json:"from_agent"` + ToAgent string `json:"to_agent"` + UserID string `json:"user_id"` + } + if err := c.ShouldBindJSON(&transferReq); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 更新用户与客服的映射关系 + // 例如:userAgentMap[transferReq.UserID] = transferReq.ToAgent + + c.JSON(http.StatusOK, gin.H{"status": "success"}) +} + +func (cus *CustomerServiceApi) GetKefuInfo(c *gin.Context) { + userID, _ := c.Get("jwt_user_id") + //uidStr := strconv.Itoa(int(userID)) + var serviceId int64 + var recordData sysModel.SysServiceRecord + result := global.GVA_DB.Where("uid = ?", userID).Order("update_time DESC").Limit(1).Find(&recordData) + if result.RowsAffected == 0 || result.Error != nil { + //直接查询service表 + result2 := global.GVA_DB.Model(&sysModel.SysService{}). + Select("id"). + Where("status = ?", 1). + Order("add_time DESC"). + Limit(1).Scan(&serviceId) + fmt.Println("sssssssssssss--->>>") + fmt.Println(serviceId) + if result2.Error != nil { + response.FailWithMessage("获取客服信息失败-1", c) + return + } + } else { + serviceId = recordData.ServiceId + } + var serviceData sysModel.SysService + result3 := global.GVA_DB.Select("id,uid,online,avatar,nickname,add_time,status"). + Where("id = ?", serviceId). + Where("status = ?", 1). + Order("add_time DESC"). + First(&serviceData) + if result3.Error != nil { + response.FailWithMessage("获取客服信息失败-2", c) + return + } + response.OkWithDetailed(serviceData, "获取成功", c) +} + +func (cus *CustomerServiceApi) SendMsg(c *gin.Context) { + var msgJson ws.Message + if jsErr := c.ShouldBindJSON(&msgJson); jsErr != nil { + fmt.Println(jsErr) + response.FailWithMessage("参数有误-1", c) + return + } + fromIdStr := msgJson.Sender + toIdStr := msgJson.Receiver + content := msgJson.Content + isKf := msgJson.IsKf + msgTypeStr := msgJson.MsgType + if content == "" || fromIdStr == "" || toIdStr == "" || msgTypeStr == "" { + response.FailWithMessage("参数有误-2", c) + return + } + toId, err_1 := strconv.ParseInt(toIdStr, 10, 64) + fromId, err_2 := strconv.ParseInt(fromIdStr, 10, 64) + msgType, err_3 := strconv.ParseInt(msgTypeStr, 10, 64) + if err_1 != nil || err_2 != nil || err_3 != nil { + response.FailWithMessage("参数有误", c) + return + } + //限流 + if !tools.LimitFreqSingle("send_message:"+c.ClientIP(), 1, 2) { + response.FailWithMessage("发送频率过快", c) + return + } + var kfInfo sysModel.SysService + //var userInfo sysModel.SysTestUser + var userInfo user.User + var err, err2 error + if isKf == 1 { + err = global.GVA_DB.Where("id = ?", fromId).First(&kfInfo).Error + err2 = global.GVA_DB.Where("id = ?", toId).First(&userInfo).Error + + } else if isKf == 0 { + err = global.GVA_DB.Where("id = ?", toId).First(&kfInfo).Error + err2 = global.GVA_DB.Where("id = ?", fromId).First(&userInfo).Error + } + if err != nil || err2 != nil { + response.FailWithMessage("获取失败-1", c) + return + } + + ser := service.ServiceGroupApp + cErr := ser.CreateMsg(kfInfo, userInfo, msgType, content, strconv.FormatInt(isKf, 10)) + if cErr != nil { + response.FailWithMessage("发送失败", c) + return + } + message := ws.Message{ + Sender: fromIdStr, + Receiver: toIdStr, + Content: content, + MsgType: msgTypeStr, + Role: "kf", + Timestamp: time.Now().Unix(), + } + var key string + if isKf == 1 { + //查找指定用户广播消息 + key = "user" + toIdStr + message.AvatarUrl = kfInfo.Avatar + message.Nickname = kfInfo.Nickname + } else if isKf == 0 { + //查找指定客服广播消息 + key = "kf" + toIdStr + message.Role = "user" + message.AvatarUrl = userInfo.Avatar + message.Nickname = userInfo.NickName + } + conn, ok := ws.Manager.Clients[key] + if conn != nil && ok { + sendMsg := ws.TypeMsg{ + Type: "message", + Data: message, + } + str, _ := json.Marshal(sendMsg) + conn.Send <- str + + if isKf == 0 { + //客服给用户发送自动回复消息 + var autoReply sysModel.SysServiceReply + autoContent := "" + var autoMsgType int64 + aErr := global.GVA_DB.Where("is_complete = ? AND `status` = ? AND keyword = ?", 1, 1, content).First(&autoReply).Error + fmt.Println(aErr) + if aErr == nil { + fmt.Println(autoReply) + autoContent = autoReply.Content + autoMsgType = autoReply.ReplyType + } else { + aErr = global.GVA_DB.Where("is_complete = ? AND `status` = ? AND keyword LIKE ?", 0, 1, "%"+content+"%").First(&autoReply).Error + if aErr == nil { + autoContent = autoReply.Content + autoMsgType = autoReply.ReplyType + } + } + if autoContent != "" { + if autoMsgType == 2 { + autoMsgType = 3 //图片 + } + aErr = ser.CreateMsg(kfInfo, userInfo, autoMsgType, autoContent, "1") + if aErr == nil { + autoUidStr := strconv.FormatUint(uint64(userInfo.ID), 10) + message.Sender = strconv.FormatInt(kfInfo.Id, 10) + message.Receiver = autoUidStr + message.MsgType = strconv.FormatInt(autoMsgType, 10) + message.Content = autoContent + message.IsKf = 1 + message.Role = "kf" + message.AvatarUrl = kfInfo.Avatar + message.Nickname = kfInfo.Nickname + sendMsg.Data = message + autoStr, _ := json.Marshal(sendMsg) + kfConn, isOk := ws.Manager.Clients["user"+autoUidStr] + if kfConn != nil && isOk { + kfConn.Send <- autoStr + } + } + } + } + } + + response.OkWithDetailed(nil, "发送成功", c) +} + +func (cus *CustomerServiceApi) GetMsgList(c *gin.Context) { + + uid, ok := c.Get("jwt_user_id") //jwt里解出的 + jwtServiceId, ok2 := c.Get("service_id") //jwt里解出的 + if !ok2 { + //gva-shop前端用户连接请求消息列表 + jwtServiceId = c.Query("kf_id") + } + if !ok { + //后台客服连接请求消息列表 + uid = c.Query("uid") + } + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + if pageSize > 20 { + pageSize = 20 + } + offset := pageSize * (page - 1) + var total int64 + var list []sysModel.SysServiceMsg + db := global.GVA_DB.Model(&sysModel.SysServiceMsg{}).Where("uid = ?", uid).Where("service_id = ?", jwtServiceId) + db.Count(&total) + err := db.Limit(pageSize).Offset(offset).Order("add_time desc").Find(&list).Error + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + if len(list) > 0 { + sort.Slice(list, func(i, j int) bool { + return list[i].AddTime < list[j].AddTime + }) + for k, v := range list { + decoded, _ := base64.StdEncoding.DecodeString(v.Content) + v.Content = string(decoded) + list[k] = v + } + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: page, + PageSize: pageSize, + }, "获取成功", c) +} + +func (cus *CustomerServiceApi) GetMsgUser(c *gin.Context) { + kfId, _ := c.Get("service_id") + var list []sysModel.SysServiceRecord + err := global.GVA_DB.Where("service_id=?", kfId).Find(&list).Error + if err != nil { + response.FailWithMessage("获取失败", c) + return + } + if len(list) > 0 { + //判断用户在线状况 + for k, v := range list { + userKey := "user" + strconv.FormatInt(v.Uid, 10) + isClent, ok := ws.Manager.Clients[userKey] + if ok && isClent != nil { + v.Online = 1 + } else { + v.Online = 0 + } + decoded, _ := base64.StdEncoding.DecodeString(v.Message) + v.Message = string(decoded) + //查找未读消息数 + var noCount int64 + global.GVA_DB.Model(&sysModel.SysServiceMsg{}). + Where("is_view=?", 0). + Where("is_kf=?", 0). + Where("service_id=?", kfId). + Where("uid=?", v.Uid).Count(&noCount) + v.NoRead = noCount + v.AddTimeStr = tools.FormatTimestamp(v.UpdateTime) + if v.MessageType == 3 { + v.Message = "[图片]" + } + list[k] = v + } + sort.Slice(list, func(i, j int) bool { + if list[i].Online != list[j].Online { + return list[i].Online > list[j].Online + } + return list[i].AddTime > list[j].AddTime + }) + } + response.OkWithDetailed(list, "获取成功", c) +} + +func (cus *CustomerServiceApi) SetMsgView(c *gin.Context) { + kfId, _ := c.Get("service_id") + uid := c.Query("uid") + global.GVA_DB.Model(&sysModel.SysServiceMsg{}). + Where(map[string]interface{}{"is_kf": 0, "service_id": kfId, "is_view": 0, "uid": uid}). + Update("is_view", 1) + response.Ok(c) +} + +func (cus *CustomerServiceApi) UploadFile(c *gin.Context) { + file, err := c.FormFile("file") + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + extension := filepath.Ext(file.Filename) + newUUID := uuid.New().String() + hash := md5.Sum([]byte("gva-service" + newUUID)) + md5Pwd := hex.EncodeToString(hash[:]) + filename := md5Pwd + extension + if err := c.SaveUploadedFile(file, "./uploads/file/"+filename); err != nil { + response.FailWithMessage("上传失败-2", c) + return + + } + //ser := service.ServiceGroupApp + //url := ser.GetUrlHost(c) + response.OkWithDetailed("uploads/file/"+filename, "获取成功", c) + return +} + +func (cus *CustomerServiceApi) GetTestMsgList(c *gin.Context) { + uid := c.Query("uid") + serviceId := c.Query("service_id") + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) + if pageSize > 20 { + pageSize = 20 + } + offset := pageSize * (page - 1) + var total int64 + var list []sysModel.SysServiceMsg + global.GVA_DB.Model(&sysModel.SysServiceMsg{}).Where("uid=?", uid).Where("service_id=?", serviceId).Count(&total) + err := global.GVA_DB.Where("uid=?", uid).Where("service_id=?", serviceId).Limit(pageSize).Offset(offset).Order("add_time desc").Find(&list).Error + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + if len(list) > 0 { + sort.Slice(list, func(i, j int) bool { + return list[i].AddTime < list[j].AddTime + }) + for k, v := range list { + decoded, _ := base64.StdEncoding.DecodeString(v.Content) + v.Content = string(decoded) + list[k] = v + } + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: page, + PageSize: pageSize, + }, "获取成功", c) +} + +func (cus *CustomerServiceApi) GetUserInfo(c *gin.Context) { + //userID := utils.GetUserID(c) + userID, ok := c.Get("jwt_user_id") + if !ok { + //后台客服连接请求 + userID = c.Query("uid") + } + var clientUser user.User + result := global.GVA_DB.Omit("password").Where("id = ?", userID).Where("deleted_at IS NULL").First(&clientUser) + if result.Error != nil { + response.FailWithMessage("获取用户信息失败", c) + return + } + response.OkWithDetailed(clientUser, "获取成功", c) +} + +func (cus *CustomerServiceApi) GetServiceScript(c *gin.Context) { + rType := c.Query("type") + db := global.GVA_DB.Model(&sysModel.SysServiceScript{}) + if rType == "1" { + serviceId, ok := c.Get("service_id") + if serviceId != "" && ok { + db = db.Where("service_id=?", serviceId) + } + } else { + db = db.Where("service_id=?", 0) + } + var list []sysModel.SysServiceScript + err := db.Order("add_time desc").Limit(20).Offset(0).Find(&list).Error + if err != nil { + response.FailWithMessage("查询失败:"+err.Error(), c) + return + } + response.OkWithDetailed(list, "获取成功", c) +} diff --git a/plugin/customerservice/api/enter.go b/plugin/customerservice/api/enter.go new file mode 100644 index 0000000..544d361 --- /dev/null +++ b/plugin/customerservice/api/enter.go @@ -0,0 +1,8 @@ +package api + +type ApiGroup struct { + CustomerServiceApi + AdminServiceApi +} + +var ApiGroupApp = new(ApiGroup) diff --git a/plugin/customerservice/api/service.go b/plugin/customerservice/api/service.go new file mode 100644 index 0000000..1f832ef --- /dev/null +++ b/plugin/customerservice/api/service.go @@ -0,0 +1,484 @@ +package api + +import ( + "crypto/md5" + "encoding/hex" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/plugin/customerservice/model" + "git.echol.cn/loser/lckt/plugin/customerservice/service" + "git.echol.cn/loser/lckt/plugin/customerservice/tools" + "github.com/gin-gonic/gin" + "strconv" + "time" +) + +type AdminServiceApi struct{} + +// GetServiceList +// @Tags sysService +// @Summary 客服列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.PageInfo true "页码, 每页大小" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页客服列表,返回包括列表,总数,页码,每页数量" +// @Router /service/get_service_list [post] +func (ad *AdminServiceApi) GetServiceList(c *gin.Context) { + var pageInfo model.PageInfo + if err := c.ShouldBindQuery(&pageInfo); err != nil { + response.FailWithMessage("参数错误:"+err.Error(), c) + return + } + limit := pageInfo.Limit + offset := pageInfo.Limit * (pageInfo.Page - 1) + db := global.GVA_DB.Model(&model.SysService{}) + var list []model.SysService + var total int64 + db.Count(&total) + err := db.Omit("password").Order("add_time desc").Limit(limit).Offset(offset).Find(&list).Error + if err != nil { + response.FailWithMessage("查询失败:"+err.Error(), c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.Limit, + }, "获取成功", c) +} + +// SaveService +// @Tags sysService +// @Summary 添加/更新客服 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request true "" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "" +// @Router /service/save_service [post] +func (ad *AdminServiceApi) SaveService(c *gin.Context) { + var serviceData model.SysService + if err := c.ShouldBindJSON(&serviceData); err != nil { + response.FailWithMessage("参数错误:"+err.Error(), c) + return + } + //校验数据 + ser := service.ServiceGroupApp + if err := ser.ValidateServiceData(&serviceData); err != nil { + response.FailWithMessage("操作失败:"+err.Error(), c) + return + } + var msg string + if serviceData.Password != "" { + hash := md5.Sum([]byte("gva-service" + serviceData.Password)) + serviceData.Password = hex.EncodeToString(hash[:]) + } + + if serviceData.Id == 0 { + serviceData.AddTime = time.Now().Unix() + if err := global.GVA_DB.Create(&serviceData).Error; err != nil { + response.FailWithMessage("添加失败:"+err.Error(), c) + return + } + msg = "添加成功" + } else { + if err := global.GVA_DB.Model(&model.SysService{}).Where("id = ?", serviceData.Id).Updates(serviceData).Error; err != nil { + response.FailWithMessage("更新失败:"+err.Error(), c) + return + } + msg = "更新成功" + } + response.OkWithMessage(msg, c) +} + +// DeleteService +// @Tags sysService +// @Summary 删除客服 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.id true "id" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "成功消息" +// @Router /service/delete_service?id=xx [delete] +func (ad *AdminServiceApi) DeleteService(c *gin.Context) { + idParam := c.Query("id") + id, err := strconv.Atoi(idParam) + if err != nil { + response.FailWithMessage("参数错误:"+err.Error(), c) + return + } + var ser model.SysService + if err := global.GVA_DB.First(&ser, id).Error; err != nil { + //if errors.Is(err, gorm.ErrRecordNotFound) { + // + //} + response.FailWithMessage("用户不存在:"+err.Error(), c) + return + } + // 删除用户 + if err := global.GVA_DB.Delete(&model.SysService{}, id).Error; err != nil { + response.FailWithMessage("删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// FindService +// @Tags sysService +// @Summary 查找客服 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.id true "id" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "成功消息" +// @Router /service/find_service?id=xx [get] +func (ad *AdminServiceApi) FindService(c *gin.Context) { + idParam := c.Query("id") + id, err := strconv.Atoi(idParam) + if err != nil { + response.FailWithMessage("参数错误:"+err.Error(), c) + return + } + var ser model.SysService + if err := global.GVA_DB.First(&ser, id).Error; err != nil { + response.FailWithMessage("客服不存在:"+err.Error(), c) + return + } + ser.Password = "" + response.OkWithDetailed(ser, "success", c) +} + +// AdminServiceLogin +// @Tags sysService +// @Summary 进入工作台 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.id true "id" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "成功消息" +// @Router /service/delete_reply/:id [delete] +func (ad *AdminServiceApi) AdminServiceLogin(c *gin.Context) { + idParam := c.Query("id") + var ser model.SysService + if err := global.GVA_DB.First(&ser, idParam).Error; err != nil { + response.FailWithMessage("客服不存在:"+err.Error(), c) + return + } + data := map[string]interface{}{} + + expTime, token, err := tools.GenerateToken(ser.Id) + if err != nil { + response.FailWithMessage("登录失败:"+err.Error(), c) + return + } + data["token"] = token + data["exp_time"] = expTime + + response.OkWithDetailed(data, "success", c) +} + +// AccountServiceLogin +// @Tags sysService +// @Summary 账户密码登录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.id true "id" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "成功消息" +// @Router /service/admin_login?id=xx [get] +func (ad *AdminServiceApi) AccountServiceLogin(c *gin.Context) { + var loginInfo model.LoginInfo + if err := c.ShouldBindJSON(&loginInfo); err != nil { + response.FailWithMessage("参数错误:"+err.Error(), c) + return + } + if loginInfo.Account == "" || loginInfo.Password == "" { + response.FailWithMessage("账户或密码为空", c) + return + } + var serviceInfo model.SysService + if err := global.GVA_DB.Limit(1).Where("account=?", loginInfo.Account).Find(&serviceInfo).Error; err != nil { + response.FailWithMessage("客服不存在:"+err.Error(), c) + return + } + hash := md5.Sum([]byte("gva-service" + loginInfo.Password)) + md5Pwd := hex.EncodeToString(hash[:]) + if md5Pwd != serviceInfo.Password { + response.FailWithMessage("密码不正确", c) + return + } + + data := map[string]interface{}{} + expTime, token, err := tools.GenerateToken(serviceInfo.Id) + if err != nil { + response.FailWithMessage("登录失败:"+err.Error(), c) + return + } + data["token"] = token + data["exp_time"] = expTime + response.OkWithDetailed(data, "success", c) +} + +// GetScriptList +// @Tags sysService +// @Summary 客服话术列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.PageInfo true "页码, 每页大小" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页客服列表,返回包括列表,总数,页码,每页数量" +// @Router /service/get_script_list [get] +func (ad *AdminServiceApi) GetScriptList(c *gin.Context) { + var pageInfo model.PageInfo + if err := c.ShouldBindQuery(&pageInfo); err != nil { + response.FailWithMessage("参数错误:"+err.Error(), c) + return + } + limit := pageInfo.Limit + offset := pageInfo.Limit * (pageInfo.Page - 1) + db := global.GVA_DB.Model(&model.SysServiceScript{}) + var list []model.SysServiceScript + var total int64 + db.Count(&total) + err := db.Order("sort desc,add_time desc").Limit(limit).Offset(offset).Find(&list).Error + if err != nil { + response.FailWithMessage("查询失败:"+err.Error(), c) + return + } + for k, v := range list { + t := time.Unix(v.AddTime, 0) + v.AddTimeStr = t.Format("2006-01-02 15:04:05") + list[k] = v + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.Limit, + }, "获取成功", c) +} + +// SaveScript +// @Tags sysService +// @Summary 添加/更新客服话术 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request true "" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "" +// @Router /service/save_script [post] +func (ad *AdminServiceApi) SaveScript(c *gin.Context) { + var scriptData model.SysServiceScript + if err := c.ShouldBindJSON(&scriptData); err != nil { + response.FailWithMessage("参数错误:"+err.Error(), c) + return + } + //校验数据 + ser := service.ServiceGroupApp + if err := ser.ValidateScriptData(&scriptData); err != nil { + response.FailWithMessage("操作失败:"+err.Error(), c) + return + } + var msg string + if scriptData.Id == 0 { + scriptData.AddTime = time.Now().Unix() + if err := global.GVA_DB.Create(&scriptData).Error; err != nil { + response.FailWithMessage("添加失败:"+err.Error(), c) + return + } + msg = "添加成功" + } else { + if err := global.GVA_DB.Model(&model.SysServiceScript{}).Where("id = ?", scriptData.Id).Updates(scriptData).Error; err != nil { + response.FailWithMessage("更新失败:"+err.Error(), c) + return + } + msg = "更新成功" + } + response.OkWithMessage(msg, c) +} + +// DeleteScript +// @Tags sysService +// @Summary 删除客服话术 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.id true "id" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "成功消息" +// @Router /service/delete_script?id=xxx [delete] +func (ad *AdminServiceApi) DeleteScript(c *gin.Context) { + idParam := c.Query("id") + id, err := strconv.Atoi(idParam) + if err != nil { + response.FailWithMessage("参数错误:"+err.Error(), c) + return + } + var ser model.SysServiceScript + if err := global.GVA_DB.First(&ser, id).Error; err != nil { + response.FailWithMessage("话术不存在或已删除:"+err.Error(), c) + return + } + // 删除 + if err := global.GVA_DB.Delete(&model.SysServiceScript{}, id).Error; err != nil { + response.FailWithMessage("删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// FindScript +// @Tags sysService +// @Summary 查找话术 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.id true "id" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "成功消息" +// @Router /service/find_script?id=xx [get] +func (ad *AdminServiceApi) FindScript(c *gin.Context) { + idParam := c.Query("id") + id, err := strconv.Atoi(idParam) + if err != nil { + response.FailWithMessage("参数错误:"+err.Error(), c) + return + } + var ser model.SysServiceScript + if err := global.GVA_DB.First(&ser, id).Error; err != nil { + response.FailWithMessage("话术不存在:"+err.Error(), c) + return + } + response.OkWithDetailed(ser, "success", c) +} + +// AutoReplyList +// @Tags sysService +// @Summary 自动回复列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.PageInfo true "页码, 每页大小" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页客服列表,返回包括列表,总数,页码,每页数量" +// @Router /service/auto_reply_list [get] +func (ad *AdminServiceApi) AutoReplyList(c *gin.Context) { + var pageInfo model.AutoPageInfo + if err := c.ShouldBindQuery(&pageInfo); err != nil { + response.FailWithMessage("参数错误:"+err.Error(), c) + return + } + limit := pageInfo.Limit + offset := pageInfo.Limit * (pageInfo.Page - 1) + db := global.GVA_DB.Model(&model.SysServiceReply{}) + var list []model.SysServiceReply + var total int64 + db.Count(&total) + err := db.Order("add_time desc").Limit(limit).Offset(offset).Find(&list).Error + if err != nil { + response.FailWithMessage("查询失败:"+err.Error(), c) + return + } + for k, v := range list { + t := time.Unix(v.AddTime, 0) + v.AddTimeStr = t.Format("2006-01-02 15:04:05") + list[k] = v + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.Limit, + }, "获取成功", c) +} + +// SaveReply +// @Tags sysService +// @Summary 添加/更新自动回复 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request true "" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "" +// @Router /service/save_reply [post] +func (ad *AdminServiceApi) SaveReply(c *gin.Context) { + var replyData model.SysServiceReply + if err := c.ShouldBindJSON(&replyData); err != nil { + response.FailWithMessage("参数错误:"+err.Error(), c) + return + } + //校验数据 + ser := service.ServiceGroupApp + if err := ser.ValidateReplyData(&replyData); err != nil { + response.FailWithMessage("操作失败:"+err.Error(), c) + return + } + var msg string + if replyData.Id == 0 { + replyData.AddTime = time.Now().Unix() + if err := global.GVA_DB.Create(&replyData).Error; err != nil { + response.FailWithMessage("添加失败:"+err.Error(), c) + return + } + msg = "添加成功" + } else { + if err := global.GVA_DB.Model(&model.SysServiceReply{}).Where("id = ?", replyData.Id).Updates(replyData).Error; err != nil { + response.FailWithMessage("更新失败:"+err.Error(), c) + return + } + msg = "更新成功" + } + response.OkWithMessage(msg, c) +} + +// DeleteReply +// @Tags sysService +// @Summary 删除自动回复 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.id true "id" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "成功消息" +// @Router /service/delete_reply?id=xx [delete] +func (ad *AdminServiceApi) DeleteReply(c *gin.Context) { + idParam := c.Query("id") + id, err := strconv.Atoi(idParam) + if err != nil { + response.FailWithMessage("参数错误:"+err.Error(), c) + return + } + var ser model.SysServiceReply + if err := global.GVA_DB.First(&ser, id).Error; err != nil { + response.FailWithMessage("内容不存在或已删除:"+err.Error(), c) + return + } + // 删除数据 + if err := global.GVA_DB.Delete(&model.SysServiceReply{}, id).Error; err != nil { + response.FailWithMessage("删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// FindReply +// @Tags sysService +// @Summary 查找自动回复详情 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.id true "id" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "成功消息" +// @Router /service/find_reply?id=xxx [get] +func (ad *AdminServiceApi) FindReply(c *gin.Context) { + idParam := c.Query("id") + id, err := strconv.Atoi(idParam) + if err != nil { + response.FailWithMessage("参数错误:"+err.Error(), c) + return + } + var ser model.SysServiceReply + if err := global.GVA_DB.First(&ser, id).Error; err != nil { + response.FailWithMessage("自动回复内容不存在:"+err.Error(), c) + return + } + response.OkWithDetailed(ser, "success", c) +} diff --git a/plugin/customerservice/config/config.go b/plugin/customerservice/config/config.go new file mode 100644 index 0000000..d912156 --- /dev/null +++ b/plugin/customerservice/config/config.go @@ -0,0 +1 @@ +package config diff --git a/plugin/customerservice/global/global.go b/plugin/customerservice/global/global.go new file mode 100644 index 0000000..dfbfa4a --- /dev/null +++ b/plugin/customerservice/global/global.go @@ -0,0 +1 @@ +package global diff --git a/plugin/customerservice/main.go b/plugin/customerservice/main.go new file mode 100644 index 0000000..0aa9ce1 --- /dev/null +++ b/plugin/customerservice/main.go @@ -0,0 +1,214 @@ +package customerservice + +import ( + gvaGlobal "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/plugin/customerservice/model" + "git.echol.cn/loser/lckt/plugin/customerservice/router" + "git.echol.cn/loser/lckt/plugin/customerservice/service/ws" + "git.echol.cn/loser/lckt/plugin/plugin-tool/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type CustomerServicePlugin struct { +} + +func CreateCustomerServicePlug() *CustomerServicePlugin { + go func() { + err := gvaGlobal.GVA_DB.AutoMigrate(model.SysService{}, + model.SysServiceMsg{}, + model.SysServiceRecord{}, + model.SysServiceReply{}, + model.SysServiceScript{}, + model.SysTestUser{}) + if err != nil { + gvaGlobal.GVA_LOG.Error("自动创建表失败", zap.Error(err)) + } else { + gvaGlobal.GVA_LOG.Info("自动创建表成功") + } + }() // 此处可以把插件依赖的数据库结构体自动创建表 需要填写对应的结构体 + // 下方会自动注册菜单 第一个参数为菜单一级路由信息一般为定义好的组名 第二个参数为真实使用的web页面路由信息 + // 具体值请根据实际情况修改 + utils.RegisterMenus( + system.SysBaseMenu{ + Path: "service", + Name: "客服管理", + Hidden: false, + Component: "view/routerHolder.vue", + Sort: 4, + Meta: system.Meta{ + Title: "客服管理", + Icon: "service", + }, + }, + system.SysBaseMenu{ + Path: "index", + Name: "客服列表", + Hidden: false, + Component: "plugin/customerservice/view/service/index.vue", + Sort: 1, + Meta: system.Meta{ + Title: "客服列表", + Icon: "service", + }, + }, + system.SysBaseMenu{ + Path: "script/list", + Name: "客服话术", + Hidden: false, + Component: "plugin/customerservice/view/script/index.vue", + Sort: 2, + Meta: system.Meta{ + Title: "客服话术", + Icon: "document", + }, + }, + system.SysBaseMenu{ + Path: "reply/list", + Name: "自动回复", + Hidden: false, + Component: "plugin/customerservice/view/reply/index.vue", + Sort: 3, + Meta: system.Meta{ + Title: "自动回复", + Icon: "bell-filled", + }, + }, + ) + + // 下方会自动注册api 以下格式为示例格式,请按照实际情况修改 + utils.RegisterApis( + //system.SysApi{ + // Path: "/service/ws", + // Description: "用户连接接口", + // ApiGroup: "客服管理", + // Method: "GET", + //}, + //system.SysApi{ + // Path: "/service/serve_ws", + // Description: "客服连接接口", + // ApiGroup: "客服管理", + // Method: "GET", + //}, + //system.SysApi{ + // Path: "/service/send_msg", + // Description: "发送消息接口", + // ApiGroup: "客服管理", + // Method: "POST", + //}, + //system.SysApi{ + // Path: "/service/get_msg_list", + // Description: "消息列表", + // ApiGroup: "客服管理", + // Method: "GET", + //}, + //system.SysApi{ + // Path: "/service/get_msg_user", + // Description: "客服聊天用户列表", + // ApiGroup: "客服管理", + // Method: "GET", + //}, + //system.SysApi{ + // Path: "/service/get_kf_info", + // Description: "当前客服详情", + // ApiGroup: "客服管理", + // Method: "GET", + //}, + //system.SysApi{ + // Path: "/service/set_msg_view", + // Description: "设置已读", + // ApiGroup: "客服管理", + // Method: "GET", + //}, + system.SysApi{ + Path: "/service/get_service_list", + Description: "后台客服列表", + ApiGroup: "客服管理", + Method: "GET", + }, + system.SysApi{ + Path: "/service/save_service", + Description: "后台客服新增/更新", + ApiGroup: "客服管理", + Method: "POST", + }, + system.SysApi{ + Path: "/service/delete_service", + Description: "删除客服", + ApiGroup: "客服管理", + Method: "DELETE", + }, + system.SysApi{ + Path: "/service/find_service", + Description: "客服详情", + ApiGroup: "客服管理", + Method: "GET", + }, + system.SysApi{ + Path: "/service/get_script_list", + Description: "客服话术列表", + ApiGroup: "客服管理", + Method: "GET", + }, + system.SysApi{ + Path: "/service/save_script", + Description: "客服话术新增/更新", + ApiGroup: "客服管理", + Method: "POST", + }, + system.SysApi{ + Path: "/service/delete_script", + Description: "删除客服话术", + ApiGroup: "客服管理", + Method: "DELETE", + }, + system.SysApi{ + Path: "/service/find_script", + Description: "客服话术详情", + ApiGroup: "客服管理", + Method: "GET", + }, + system.SysApi{ + Path: "/service/auto_reply_list", + Description: "自动回复列表", + ApiGroup: "客服管理", + Method: "GET", + }, + system.SysApi{ + Path: "/service/save_reply", + Description: "自动回复新增/更新", + ApiGroup: "客服管理", + Method: "POST", + }, + system.SysApi{ + Path: "/service/delete_reply", + Description: "删除自动回复", + ApiGroup: "客服管理", + Method: "DELETE", + }, + system.SysApi{ + Path: "/service/find_reply", + Description: "自动回复详情", + ApiGroup: "客服管理", + Method: "GET", + }, + system.SysApi{ + Path: "/service/admin_login", + Description: "进入客服工作台", + ApiGroup: "客服管理", + Method: "GET", + }, + ) + go ws.Manager.Start() + go ws.Manager.CheckClientActivity() + return &CustomerServicePlugin{} +} + +func (*CustomerServicePlugin) Register(group *gin.RouterGroup) { + router.RouterGroupApp.InitCustomerServiceRouter(group) +} + +func (*CustomerServicePlugin) RouterPath() string { + return "" +} diff --git a/plugin/customerservice/middleware/jwt.go b/plugin/customerservice/middleware/jwt.go new file mode 100644 index 0000000..a121ff2 --- /dev/null +++ b/plugin/customerservice/middleware/jwt.go @@ -0,0 +1,57 @@ +package middleware + +import ( + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/plugin/customerservice/tools" + "git.echol.cn/loser/lckt/utils" + "github.com/gin-gonic/gin" + "strings" +) + +func JWTAuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // 从请求头获取 token + authHeader := c.GetHeader("chat-token") + userHeader := c.GetHeader("x-token") + if userHeader == "" && authHeader == "" { + response.FailWithMessage("参数错误:"+"Authorization header is missing", c) + c.Abort() + return + } + + if authHeader != "" { + // 按照格式 "Bearer " 提取 token + tokenString := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer ")) + if tokenString == "" { + response.FailWithMessage("参数错误:"+"Token is missing", c) + c.Abort() + return + } + // 验证 token + claims, err := tools.ValidateToken(tokenString) + if err != nil { + response.FailWithMessage("Invalid token:"+err.Error(), c) + c.Abort() + return + } + + // 将用户信息存储在上下文中,便于后续处理 + c.Set("service_id", claims.ServiceId) + //c.Request.URL.Query().Add("service_id", strconv.FormatInt(claims.ServiceId, 10)) + c.Next() // 继续处理请求 + + } else { + //为了方便客服后台和前端客服聊天界面公用方法,共用同一套jwt,前端的jwt的值取的还是gva-shop的x-token + j := utils.NewJWT() + claims, err := j.ParseToken(userHeader) + if err != nil { + response.FailWithMessage("参数错误:"+"Token is error", c) + c.Abort() + return + } + c.Set("jwt_user_id", claims.BaseClaims.ID) + c.Next() + } + + } +} diff --git a/plugin/customerservice/model/model.go b/plugin/customerservice/model/model.go new file mode 100644 index 0000000..8c22948 --- /dev/null +++ b/plugin/customerservice/model/model.go @@ -0,0 +1,25 @@ +package model + +type PageInfo struct { + Page int `json:"page" form:"page"` + Limit int `json:"limit" form:"limit"` + Keyword string `json:"keyword" form:"keyword"` +} + +type MsgPageInfo struct { + Page int `json:"page" form:"page"` + Limit int `json:"limit" form:"limit"` + FromId int `json:"from_id" form:"from_id"` +} + +type AutoPageInfo struct { + Page int `json:"page" form:"page"` + Limit int `json:"limit" form:"limit"` + Keyword string `json:"keyword" form:"keyword"` + ReplyType int `json:"reply_type" form:"reply_type"` +} + +type LoginInfo struct { + Account string `json:"account" form:"account"` + Password string `json:"password" form:"password"` +} diff --git a/plugin/customerservice/model/sysService.go b/plugin/customerservice/model/sysService.go new file mode 100644 index 0000000..ea550d8 --- /dev/null +++ b/plugin/customerservice/model/sysService.go @@ -0,0 +1,18 @@ +package model + +type SysService struct { + Id int64 `json:"id" form:"id" gorm:"primarykey"` + MerchantId uint `json:"merchant_id" form:"merchant_id" gorm:"default:0;type:int;column:merchant_id;comment:商户id;"` + Uid uint `json:"uid" form:"uid" gorm:"default:0;type:int;column:uid;comment:用户id;"` + Online uint `json:"online" form:"online" gorm:"default:0;type:tinyint;column:online;comment:客服是否在线;"` + Account string `json:"account" form:"account" gorm:"default:'';type:varchar(255);column:account;comment:账户;"` + Password string `json:"password" form:"password" gorm:"default:'';type:varchar(255);column:password;comment:密码;"` + Avatar string `json:"avatar" form:"avatar" gorm:"default:'';type:varchar(255);column:avatar;comment:头像;"` + Nickname string `json:"nickname" form:"nickname" gorm:"default:'';type:varchar(255);column:nickname;comment:客服名称;"` + AddTime int64 `json:"add_time" form:"add_time" gorm:"default:0;type:int;column:add_time;comment:添加时间;"` + Status *uint `json:"status" form:"status" gorm:"default:0;type:tinyint(1);column:status;comment:是否显示;"` +} + +func (SysService) TableName() string { + return "sys_service" +} diff --git a/plugin/customerservice/model/sysServiceMsg.go b/plugin/customerservice/model/sysServiceMsg.go new file mode 100644 index 0000000..c6a4625 --- /dev/null +++ b/plugin/customerservice/model/sysServiceMsg.go @@ -0,0 +1,18 @@ +package model + +type SysServiceMsg struct { + Id uint `gorm:"primarykey" json:"id"` // 主键ID + MerchantId uint `json:"merchant_id" form:"merchant_id" gorm:"default:0;type:int;column:merchant_id;comment:商户id;"` + Content string `json:"content" form:"content" gorm:"type:text;column:content;comment:消息内容;"` + ServiceId int64 `json:"service_id" form:"service_id" gorm:"default:0;type:int;column:service_id;comment:客服id;"` + Uid int64 `json:"uid" form:"uid" gorm:"default:0;type:int;column:uid;comment:用户id;"` + IsTourist uint `json:"is_tourist" form:"is_tourist" gorm:"default:0;type:tinyint;column:is_tourist;comment:是否游客;"` + IsView uint `json:"is_view" form:"is_view" gorm:"default:0;type:tinyint;column:is_view;comment:是否已读;"` + AddTime int `json:"add_time" form:"add_time" gorm:"default:0;type:int;column:add_time;comment:添加时间;"` + MsgType int64 `json:"msg_type" form:"msg_type" gorm:"default:1;type:tinyint;column:msg_type;comment:消息类型 1=文字 2=表情 3=图片 4=语音 5=视频 6=商品;"` + IsKf int64 `json:"is_kf" form:"is_kf" gorm:"default:0;type:tinyint;column:is_kf;comment:是否客服消息;"` +} + +func (SysServiceMsg) TableName() string { + return "sys_service_msg" +} diff --git a/plugin/customerservice/model/sysServiceRecord.go b/plugin/customerservice/model/sysServiceRecord.go new file mode 100644 index 0000000..0e26141 --- /dev/null +++ b/plugin/customerservice/model/sysServiceRecord.go @@ -0,0 +1,21 @@ +package model + +type SysServiceRecord struct { + Id uint `json:"id" form:"id" gorm:"primarykey"` + ServiceId int64 `json:"service_id" form:"service_id" gorm:"default:0;type:int;column:service_id;comment:客服id;"` + Uid int64 `json:"uid" form:"uid" gorm:"default:0;type:int;column:uid;comment:用户id;"` + Avatar string `json:"avatar" form:"avatar" gorm:"default:'';type:varchar(255);column:avatar;comment:用户头像;"` + Nickname string `json:"nickname" form:"nickname" gorm:"default:'';type:varchar(255);column:nickname;comment:用户昵称;"` + Online uint `json:"online" form:"online" gorm:"default:0;type:tinyint;column:online;comment:是否在线;"` + IsTourist uint `json:"is_tourist" form:"is_tourist" gorm:"default:0;type:tinyint;column:is_tourist;comment:是否游客0:否;1:是;"` + Message string `json:"message" form:"message" gorm:"type:text;column:message;comment:最新一条消息;"` + AddTime int64 `json:"add_time" form:"add_time" gorm:"default:0;type:int;column:add_time;comment:添加时间;"` + UpdateTime int64 `json:"update_time" form:"update_time" gorm:"default:0;type:int;column:update_time;comment:更新时间;"` + MessageType int64 `json:"message_type" form:"message_type" gorm:"default:0;type:tinyint(1);column:message_type;comment:消息类型:1=文字 2=表情 3=图片 4=语音 5=视频 6=商品;"` + NoRead int64 `json:"no_read" gorm:"-"` + AddTimeStr string `json:"add_time_str" gorm:"-"` +} + +func (SysServiceRecord) TableName() string { + return "sys_service_record" +} diff --git a/plugin/customerservice/model/sysServiceReply.go b/plugin/customerservice/model/sysServiceReply.go new file mode 100644 index 0000000..a30ea1f --- /dev/null +++ b/plugin/customerservice/model/sysServiceReply.go @@ -0,0 +1,16 @@ +package model + +type SysServiceReply struct { + Id int64 `json:"id" form:"id" gorm:"primarykey"` + ReplyType int64 `json:"reply_type" form:"reply_type" gorm:"default:1;type:int;column:reply_type;comment:回复类型1文本,2图片;"` + IsComplete int64 `json:"is_complete" form:"is_complete" gorm:"default:0;type:int;column:is_complete;comment:是否完全匹配0否1是;"` + Keyword string `json:"keyword" form:"keyword" gorm:"default:'';type:varchar(255);column:keyword;comment:关键字;"` + Content string `json:"content" form:"content" gorm:"type:text;column:content;comment:回复内容;"` + AddTime int64 `json:"add_time" form:"add_time" gorm:"default:0;type:int;column:add_time;comment:添加时间;"` + AddTimeStr string `json:"add_time_str" form:"add_time_str" gorm:"-"` + Status int64 `json:"status" form:"status" gorm:"default:0;type:tinyint(1);column:status;comment:是否显示;"` +} + +func (SysServiceReply) TableName() string { + return "sys_service_reply" +} diff --git a/plugin/customerservice/model/sysServiceScript.go b/plugin/customerservice/model/sysServiceScript.go new file mode 100644 index 0000000..42d3328 --- /dev/null +++ b/plugin/customerservice/model/sysServiceScript.go @@ -0,0 +1,15 @@ +package model + +type SysServiceScript struct { + Id int64 `json:"id" form:"id" gorm:"primarykey"` + ServiceId int64 `json:"service_id" form:"service_id" gorm:"default:0;type:int;column:service_id;comment:客服id为0说明是公共话术;"` + Title string `json:"title" form:"title" gorm:"default:'';type:varchar(255);column:title;comment:话术标题;"` + Content string `json:"content" form:"content" gorm:"type:text;column:content;comment:话术内容;"` + AddTime int64 `json:"add_time" form:"add_time" gorm:"default:0;type:int;column:add_time;comment:添加时间;"` + AddTimeStr string `json:"add_time_str" form:"add_time_str" gorm:"-"` + Sort int64 `json:"sort" form:"sort" gorm:"default:0;type:int;column:sort;comment:排序;"` +} + +func (SysServiceScript) TableName() string { + return "sys_service_script" +} diff --git a/plugin/customerservice/model/sysTestUser.go b/plugin/customerservice/model/sysTestUser.go new file mode 100644 index 0000000..1864e85 --- /dev/null +++ b/plugin/customerservice/model/sysTestUser.go @@ -0,0 +1,11 @@ +package model + +type SysTestUser struct { + Id int64 `json:"id" form:"id" gorm:"primarykey"` + Avatar string `json:"avatar" form:"avatar" gorm:"default:'';type:varchar(255);column:avatar;comment:头像;"` + Nickname string `json:"nickname" form:"nickname" gorm:"default:'';type:varchar(255);column:nickname;comment:昵称;"` +} + +func (SysTestUser) TableName() string { + return "sys_test_user" +} diff --git a/plugin/customerservice/router/enter.go b/plugin/customerservice/router/enter.go new file mode 100644 index 0000000..26b81e3 --- /dev/null +++ b/plugin/customerservice/router/enter.go @@ -0,0 +1,7 @@ +package router + +type RouterGroup struct { + CustomerServiceRouter +} + +var RouterGroupApp = new(RouterGroup) diff --git a/plugin/customerservice/router/router.go b/plugin/customerservice/router/router.go new file mode 100644 index 0000000..0906bac --- /dev/null +++ b/plugin/customerservice/router/router.go @@ -0,0 +1,48 @@ +package router + +import ( + "git.echol.cn/loser/lckt/middleware" + "git.echol.cn/loser/lckt/plugin/customerservice/api" + serMiddleware "git.echol.cn/loser/lckt/plugin/customerservice/middleware" + "github.com/gin-gonic/gin" +) + +type CustomerServiceRouter struct { +} + +func (s *CustomerServiceRouter) InitCustomerServiceRouter(Router *gin.RouterGroup) { + wsRouter := Router.Group("") + plugServiceRouter := Router.Group("").Use(serMiddleware.JWTAuthMiddleware()).Use(middleware.Cors()) + //plugRouter := Router.Group("").Use(middleware.JWTAuth()) + privateRouter := Router.Group("").Use(middleware.JWTAuth()).Use(middleware.CasbinHandler()) + plugAdminApi := api.ApiGroupApp.AdminServiceApi + { + privateRouter.GET("/service/get_service_list", plugAdminApi.GetServiceList) + privateRouter.POST("/service/save_service", plugAdminApi.SaveService) + privateRouter.DELETE("/service/delete_service", plugAdminApi.DeleteService) + privateRouter.GET("/service/find_service", plugAdminApi.FindService) + privateRouter.GET("/service/admin_login", plugAdminApi.AdminServiceLogin) + privateRouter.GET("/service/get_script_list", plugAdminApi.GetScriptList) + privateRouter.POST("/service/save_script", plugAdminApi.SaveScript) + privateRouter.DELETE("/service/delete_script", plugAdminApi.DeleteScript) + privateRouter.GET("/service/find_script", plugAdminApi.FindScript) + privateRouter.GET("/service/auto_reply_list", plugAdminApi.AutoReplyList) + privateRouter.POST("/service/save_reply", plugAdminApi.SaveReply) + privateRouter.DELETE("/service/delete_reply", plugAdminApi.DeleteReply) + privateRouter.GET("/service/find_reply", plugAdminApi.FindReply) + } + plugApi := api.ApiGroupApp.CustomerServiceApi + { + plugServiceRouter.POST("/service/send_msg", plugApi.SendMsg) + plugServiceRouter.GET("/service/get_msg_list", plugApi.GetMsgList) + plugServiceRouter.GET("/service/get_kf_info", plugApi.GetKefuInfo) + plugServiceRouter.POST("/service/upload_file", plugApi.UploadFile) + plugServiceRouter.GET("/service/get_user_info", plugApi.GetUserInfo) + plugServiceRouter.GET("/service/get_msg_user", plugApi.GetMsgUser) + plugServiceRouter.GET("/service/get_service_script", plugApi.GetServiceScript) + plugServiceRouter.GET("/service/set_msg_view", plugApi.SetMsgView) + } + wsRouter.GET("/service/serve_ws", plugApi.ServeWsForKefu) + wsRouter.GET("/service/ws", plugApi.ServeWs) + wsRouter.POST("/service/account_login", plugAdminApi.AccountServiceLogin) +} diff --git a/plugin/customerservice/service/enter.go b/plugin/customerservice/service/enter.go new file mode 100644 index 0000000..cc46ffe --- /dev/null +++ b/plugin/customerservice/service/enter.go @@ -0,0 +1,7 @@ +package service + +type ServiceGroup struct { + CustomerServiceService +} + +var ServiceGroupApp = new(ServiceGroup) diff --git a/plugin/customerservice/service/service.go b/plugin/customerservice/service/service.go new file mode 100644 index 0000000..ab8a967 --- /dev/null +++ b/plugin/customerservice/service/service.go @@ -0,0 +1,130 @@ +package service + +import ( + "encoding/base64" + "errors" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/user" + "git.echol.cn/loser/lckt/plugin/customerservice/model" + "github.com/gin-gonic/gin" + "gorm.io/gorm" + "time" +) + +type CustomerServiceService struct{} + +func (e *CustomerServiceService) PlugService() (err error) { + // 写你的业务逻辑 + return nil +} + +func (e *CustomerServiceService) ValidateServiceData(sys *model.SysService) error { + if sys.Uid == 0 { + return errors.New("客服关联的用户id不能为空") + } else { + db := global.GVA_DB.Model(&model.SysService{}) + if sys.Id > 0 { + db = db.Where("uid=?", sys.Uid).Where("id<>?", sys.Id) + } else { + db = db.Where("uid=?", sys.Uid) + } + var dCount int64 + db.Count(&dCount) + if dCount > 0 { + return errors.New("用户id已关联其他客服,请重新输入") + } + } + db := global.GVA_DB.Model(&model.SysService{}) + if sys.Id == 0 { + if sys.Password == "" { + return errors.New("客服密码必须填写") + } + db = db.Where("account=?", sys.Account) + } else { + db = db.Where("account=?", sys.Account).Where("id<>?", sys.Id) + var dCount int64 + db.Count(&dCount) + if dCount > 0 { + return errors.New("账户已存在,请重新输入") + } + } + if sys.Account == "" { + return errors.New("客服账户必须填写") + } + if sys.Nickname == "" { + return errors.New("客服名称必须填写") + } + if sys.Avatar == "" { + return errors.New("客服头像必须选择") + } + + return nil +} + +func (e *CustomerServiceService) ValidateScriptData(sys *model.SysServiceScript) error { + if sys.Title == "" { + return errors.New("话术标题必须填写") + } + if sys.Content == "" { + return errors.New("话术内容必须填写") + } + return nil +} + +func (e *CustomerServiceService) ValidateReplyData(sys *model.SysServiceReply) error { + if sys.Keyword == "" { + return errors.New("关键字必须填写") + } + if sys.Content == "" { + return errors.New("回复内容必须填写") + } + return nil +} + +func (e *CustomerServiceService) GetUrlHost(c *gin.Context) string { + host := c.Request.Host + scheme := "http" + if c.Request.TLS != nil { + scheme = "https" + } + referer := c.Request.Referer() + if referer != "" { + return referer + } + return scheme + "://" + host + "/" +} + +func (e *CustomerServiceService) CreateMsg(kfInfo model.SysService, userInfo user.User, msgType int64, content string, isKf string) (err error) { + msgRecord := &model.SysServiceRecord{ + ServiceId: kfInfo.Id, + Uid: int64(userInfo.ID), + Message: base64.StdEncoding.EncodeToString([]byte(content)), + MessageType: msgType, + UpdateTime: time.Now().Unix(), + Avatar: userInfo.Avatar, + Nickname: userInfo.NickName, + Online: 1, + } + var record model.SysServiceRecord + + eErr := global.GVA_DB.Where("service_id = ?", kfInfo.Id).Where("uid = ?", userInfo.ID).First(&record).Error + if errors.Is(eErr, gorm.ErrRecordNotFound) { + msgRecord.AddTime = time.Now().Unix() + global.GVA_DB.Create(msgRecord) + } else { + global.GVA_DB.Model(&model.SysServiceRecord{}).Where("id = ?", record.Id).Updates(msgRecord) + } + + //插入消息记录 + msg := map[string]interface{}{ + "service_id": kfInfo.Id, + "uid": userInfo.ID, + "content": base64.StdEncoding.EncodeToString([]byte(content)), + "msg_type": msgType, + "is_view": 0, + "add_time": time.Now().Unix(), + "is_kf": isKf, + } + err = global.GVA_DB.Model(&model.SysServiceMsg{}).Create(msg).Error + return err +} diff --git a/plugin/customerservice/service/ws/ws.go b/plugin/customerservice/service/ws/ws.go new file mode 100644 index 0000000..a7683ba --- /dev/null +++ b/plugin/customerservice/service/ws/ws.go @@ -0,0 +1,271 @@ +package ws + +import ( + "encoding/json" + "errors" + "fmt" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + sysModel "git.echol.cn/loser/lckt/plugin/customerservice/model" + "git.echol.cn/loser/lckt/plugin/customerservice/tools" + "git.echol.cn/loser/lckt/utils" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + "net/http" + "strconv" + "time" +) + +type Message struct { + Sender string `json:"sender"` + Receiver string `json:"receiver"` + Content string `json:"content"` + MsgType string `json:"msg_type"` //对应msg表的msg_type + Role string `json:"role"` + Timestamp int64 `json:"timestamp"` + Nickname string `json:"nickname"` + AvatarUrl string `json:"avatar_url"` + IsKf int64 `json:"is_kf"` +} + +type TypeMsg struct { + Type string `json:"type"` + Data interface{} `json:"data,omitempty"` +} + +type Client struct { + UserID string + Role string + Socket *websocket.Conn + Send chan []byte + LastPingTime time.Time +} + +type ClientManager struct { + Clients map[string]*Client + Broadcast chan TypeMsg + Register chan *Client + Unregister chan *Client +} + +var Manager = ClientManager{ + Clients: make(map[string]*Client), + Broadcast: make(chan TypeMsg), + Register: make(chan *Client), + Unregister: make(chan *Client), +} + +// 定时检查连接的活动状态 +func (manager *ClientManager) CheckClientActivity() { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + <-ticker.C + now := time.Now() + + for ck, client := range manager.Clients { + // 如果超过一定时间没有收到ping,则断开连接 + fmt.Println(ck) + fmt.Println(now.Sub(client.LastPingTime)) + if now.Sub(client.LastPingTime) > 120*time.Second { + client.Socket.Close() + delete(manager.Clients, ck) + //设置离线 + if client.Role == "user" { + setUserOnline("offline", client.UserID) + } + } + } + } +} + +func (manager *ClientManager) Start() { + for { + select { + case conn := <-manager.Register: + key := conn.Role + conn.UserID + if existingConn, ok := manager.Clients[key]; ok { + existingConn.Socket.Close() + delete(manager.Clients, key) + } + fmt.Println(key) + manager.Clients[key] = conn + case conn := <-manager.Unregister: + key := conn.Role + conn.UserID + if existingConn, ok := manager.Clients[key]; ok && existingConn == conn { + delete(manager.Clients, key) + } + case message := <-manager.Broadcast: + data := message.Data.(map[string]interface{}) + receiver := data["receiver"].(string) + receiverKey := "user" + receiver + if data["role"].(string) == "user" { + receiverKey = "kf" + receiver + } + if client, ok := manager.Clients[receiverKey]; ok { + str, _ := json.Marshal(message) + client.Send <- str + } else { + fmt.Println(receiverKey + "链接不存在") + } + } + } +} + +func (c *Client) Read() { + defer func() { + Manager.Unregister <- c + c.Socket.Close() + }() + c.Socket.SetReadLimit(512) + + for { + _, message, err := c.Socket.ReadMessage() + if err != nil { + break + } + var msg TypeMsg + if err := json.Unmarshal(message, &msg); err != nil { + continue + } + switch msg.Type { + case "ping": + // 更新最后一次收到ping消息的时间 + c.LastPingTime = time.Now() + + // 回复pong消息 + pongMsg := TypeMsg{ + Type: "pong", + Data: time.Now().Unix(), + } + pongStr, _ := json.Marshal(pongMsg) + c.Send <- pongStr + + case "message": + //发送消息走的后台接口去触发广播,改成前端发送消息走这里 + Manager.Broadcast <- msg + } + } +} + +func (c *Client) Write() { + defer func() { + c.Socket.Close() + }() + for { + select { + case message, ok := <-c.Send: + c.Socket.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if !ok { + c.Socket.WriteMessage(websocket.CloseMessage, []byte{}) + return + } + if err := c.Socket.WriteMessage(websocket.TextMessage, message); err != nil { + return + } + } + } +} + +var Upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func WsServe(ctx *gin.Context) { + //token := ctx.Query("token") + token := ctx.Query("token") + j := utils.NewJWT() + claims, err := j.ParseToken(token) + if err != nil { + if errors.Is(err, utils.TokenExpired) { + http.NotFound(ctx.Writer, ctx.Request) + return + } + } + conn, err := Upgrader.Upgrade(ctx.Writer, ctx.Request, nil) + if err != nil { + http.NotFound(ctx.Writer, ctx.Request) + return + } + uidStr := strconv.Itoa(int(claims.BaseClaims.ID)) + client := &Client{ + UserID: uidStr, + Role: "user", + Socket: conn, + Send: make(chan []byte), + LastPingTime: time.Now(), + } + + Manager.Register <- client + setUserOnline("online", uidStr) + go client.Read() + go client.Write() +} + +func ServeWsForKefu(ctx *gin.Context) { + token := ctx.Query("token") + claims, err := tools.ValidateToken(token) + if err != nil { + response.FailWithMessage("token已失效", ctx) + return + } + kfId := claims.ServiceId + db := global.GVA_DB.Model(&sysModel.SysService{}) + var info sysModel.SysService + err = db.Find(&info).Error + if err != nil { + response.FailWithMessage("客服不存在", ctx) + return + } + conn, err2 := Upgrader.Upgrade(ctx.Writer, ctx.Request, nil) + if err2 != nil { + http.NotFound(ctx.Writer, ctx.Request) + return + } + client := &Client{ + UserID: fmt.Sprintf("%v", kfId), + Role: "kf", + Socket: conn, + Send: make(chan []byte), + LastPingTime: time.Now(), + } + Manager.Register <- client + + //设置客服在线 + global.GVA_DB.Model(&sysModel.SysService{}).Where("id = ?", kfId).Update("online", 1) + go client.Read() + go client.Write() +} + +func setUserOnline(cType string, Id string) { + //给用户在record表里的客服广播此用户离线 + var list []sysModel.SysServiceRecord + err := global.GVA_DB.Where("uid=?", Id).Find(&list).Error + if err == nil && len(list) > 0 { + for _, rec := range list { + strSerId := strconv.FormatInt(rec.ServiceId, 10) + roleKey := "kf" + strSerId + fmt.Println(roleKey) + serviceClient, ok := Manager.Clients[roleKey] + if serviceClient != nil && ok { + dataMsg := Message{ + MsgType: "1", + Sender: Id, + Receiver: strSerId, + Role: "user", + } + sendMsg := TypeMsg{ + Type: cType, + Data: dataMsg, + } + str, _ := json.Marshal(sendMsg) + serviceClient.Send <- str + } + } + } +} diff --git a/plugin/customerservice/tools/jwt.go b/plugin/customerservice/tools/jwt.go new file mode 100644 index 0000000..5e2a761 --- /dev/null +++ b/plugin/customerservice/tools/jwt.go @@ -0,0 +1,54 @@ +package tools + +import ( + "errors" + "github.com/golang-jwt/jwt/v4" + "time" +) + +var jwtKey = []byte("your-256-bit-secret") + +// CustomClaims 结构体可以根据需要添加自定义的声明 +type CustomClaims struct { + ServiceId int64 `json:"service_id"` + jwt.RegisteredClaims +} + +func GenerateToken(serviceId int64) (int64, string, error) { + // 设置JWT的声明 + claims := CustomClaims{ + ServiceId: serviceId, + RegisteredClaims: jwt.RegisteredClaims{ + Audience: jwt.ClaimStrings{"GVA"}, // 受众 + NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)), // 签名生效时间 + Issuer: "gva", + ExpiresAt: jwt.NewNumericDate(time.Now().Add(72 * time.Hour)), // token 72小时后过期 + }, + } + // 生成JWT token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenStr, err := token.SignedString(jwtKey) + return time.Now().Add(72 * time.Hour).Unix(), tokenStr, err +} + +func ValidateToken(tokenString string) (*CustomClaims, error) { + claims := &CustomClaims{} + + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + // 验证签名方法 + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("unexpected signing method") + } + return jwtKey, nil + }) + + if err != nil { + return nil, err + } + + if !token.Valid { + return nil, errors.New("invalid token") + } + + return claims, nil +} diff --git a/plugin/customerservice/tools/limits.go b/plugin/customerservice/tools/limits.go new file mode 100644 index 0000000..c6676e9 --- /dev/null +++ b/plugin/customerservice/tools/limits.go @@ -0,0 +1,77 @@ +package tools + +import ( + "log" + "sync" + "time" +) + +type LimitQueeMap struct { + sync.RWMutex + LimitQueue map[string][]int64 +} + +func (l *LimitQueeMap) readMap(key string) ([]int64, bool) { + l.RLock() + value, ok := l.LimitQueue[key] + l.RUnlock() + return value, ok +} + +func (l *LimitQueeMap) writeMap(key string, value []int64) { + l.Lock() + l.LimitQueue[key] = value + l.Unlock() +} + +var LimitQueue = &LimitQueeMap{ + LimitQueue: make(map[string][]int64), +} +var ok bool + +func NewLimitQueue() { + cleanLimitQueue() +} +func cleanLimitQueue() { + go func() { + for { + log.Println("cleanLimitQueue start...") + LimitQueue.LimitQueue = nil + now := time.Now() + // 计算下一个零点 + next := now.Add(time.Hour * 24) + next = time.Date(next.Year(), next.Month(), next.Day(), 0, 0, 0, 0, next.Location()) + t := time.NewTimer(next.Sub(now)) + <-t.C + } + }() +} + +//单机时间滑动窗口限流法 +func LimitFreqSingle(queueName string, count uint, timeWindow int64) bool { + currTime := time.Now().Unix() + if LimitQueue.LimitQueue == nil { + LimitQueue.LimitQueue = make(map[string][]int64) + } + if _, ok = LimitQueue.readMap(queueName); !ok { + LimitQueue.writeMap(queueName, make([]int64, 0)) + return true + } + q, _ := LimitQueue.readMap(queueName) + //队列未满 + if uint(len(q)) < count { + LimitQueue.writeMap(queueName, append(q, currTime)) + return true + } + //队列满了,取出最早访问的时间 + earlyTime := q[0] + //说明最早期的时间还在时间窗口内,还没过期,所以不允许通过 + if currTime-earlyTime <= timeWindow { + return false + } else { + //说明最早期的访问应该过期了,去掉最早期的 + q = q[1:] + LimitQueue.writeMap(queueName, append(q, currTime)) + } + return true +} diff --git a/plugin/customerservice/tools/timeformat.go b/plugin/customerservice/tools/timeformat.go new file mode 100644 index 0000000..cd9b940 --- /dev/null +++ b/plugin/customerservice/tools/timeformat.go @@ -0,0 +1,20 @@ +package tools + +import "time" + +func FormatTimestamp(timestamp int64) string { + t := time.Unix(timestamp, 0) + now := time.Now() + + // 格式化时间 + if t.Year() == now.Year() && t.YearDay() == now.YearDay() { + // 当天,返回 24 小时制的时和分 + return t.Format("15:04") + } else if t.Year() == now.Year() && t.YearDay() == now.YearDay()-1 { + // 昨天,返回 "昨天" + return "昨天" + } else { + // 其他时间,返回月和日 + return t.Format("01-02") + } +} diff --git a/plugin/picturelibrary/README.md b/plugin/picturelibrary/README.md new file mode 100644 index 0000000..743606c --- /dev/null +++ b/plugin/picturelibrary/README.md @@ -0,0 +1,113 @@ +## GVA 图库功能 + +### 手动安装方法 + + 1.解压zip获得picturelibrary文件夹 + 2.将 picturelibrary/web/plugin/picturelibrary 放置在web/plugin下 + 3.将 picturelibrary/server/plugin/picturelibrary 放置在server/plugin下 + +#### 执行如下注册方法 + +### 注册(手动自动都需要) + +#### 1. 前往GVA主程序下的initialize/router.go 在Routers 方法最末尾按照你需要的及安全模式添加本插件 + PluginInit(PrivateGroup, picturelibrary.CreatePictureLibraryPlug()) + 到gva系统,角色管理,分配角色的api权限即可,插件会自动注册api,需要手动分配。 + 注:会生成一个表:sys_attachment_category,表exa_file_upload_and_downloads会新增一个cat_id字段 +### 2. 配置说明 + +#### 2-1 全局配置结构体说明 + + 无配置 + +#### 2-2 使用说明 + + 在你需要图片选择的前端页面引入图库组件,如下: +
+
+ + + +
+ +
+ + 图库弹窗: + + + + + js代码: + import {CircleClose, Plus} from '@element-plus/icons-vue' + import ImageLibrary from "@/plugin/picturelibrary/view/components/imageLibrary.vue"; + + const isDialogVisible = ref(false) + const isMultiple = ref(false) // 设置是否允许多选 + const selectedImages = ref([]) + + const openImageLibrary = () => { + isDialogVisible.value = true + } + const fileTypeList = ['png', 'jpg', 'jpeg', 'gif'] + const handleImageSelect = (images) => { + if (isMultiple.value) { + selectedImages.value = [...selectedImages.value, ...images] + } else { + selectedImages.value = Array.isArray(images) ? images : [images] + } + + // 此处是测试项目里上传头像的参数,根据实际情况进行修改 + formData.value.avatar = selectedImages.value[0] + isDialogVisible.value = false + } + + const removeSelectedImage = (image) => { + const index = selectedImages.value.indexOf(image) + if (index !== -1) { + selectedImages.value.splice(index, 1) + } + } + + style代码: + .selected-images { + position: relative; + display: flex; + flex-wrap: wrap; + } + + .selected-image { + position: relative; + margin-right: 10px; + margin-bottom:10px; + width: 100px; + height: 100px; + } + + .selected-image .remove-icon { + position: absolute; + top: 0; /* 微调位置 */ + right: 0; /* 微调位置 */ + color: black; + padding: 5px; + cursor: pointer; + font-size: 22px; + line-height: 22px; + text-align: center; + } + + .avatar-uploader-icon { + font-size: 28px; + color: #8c939d; + width: 100px; + height: 100px; + text-align: center; + border: 1px dashed #d4d9e1; + } + +#### 2-3 参数说明 + isMultiple 是控制能不能选择多张图的参数,false:只能选一张;true:可以选择多张 +### 3. 方法API + 无 + diff --git a/plugin/picturelibrary/api/api.go b/plugin/picturelibrary/api/api.go new file mode 100644 index 0000000..6111316 --- /dev/null +++ b/plugin/picturelibrary/api/api.go @@ -0,0 +1,230 @@ +package api + +import ( + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/example" + picModel "git.echol.cn/loser/lckt/plugin/picturelibrary/model" + "git.echol.cn/loser/lckt/plugin/picturelibrary/service" + "github.com/gin-gonic/gin" + "go.uber.org/zap" + "strconv" +) + +type PictureLibraryApi struct{} + +var picService = service.ServiceGroupApp + +// @Tags PictureLibrary +// @Summary 请手动填写接口功能 +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}" +// @Router /图片库/routerName [post] +func (p *PictureLibraryApi) ApiName(c *gin.Context) { + + if err := picService.PlugService(); err != nil { + global.GVA_LOG.Error("失败!", zap.Error(err)) + response.FailWithMessage("失败", c) + } else { + + response.OkWithData("成功", c) + } +} + +// GetFileList +// @Tags sysAttachment +// @Summary 分页文件列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.PageInfo true "页码, 每页大小" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页文件列表,返回包括列表,总数,页码,每页数量" +// @Router /attachment/getFileList [post] +func (p *PictureLibraryApi) GetFileList(c *gin.Context) { + var pageInfo picModel.PageInfo + _ = c.ShouldBindJSON(&pageInfo) + list, total, err := picService.GetFileRecordInfoList(pageInfo, c) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.Limit, + }, "获取成功", c) +} + +// GetCategoryList +// @Tags sysAttachment +// @Summary 分类列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.PageInfo true "页码, 每页大小" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页文件列表,返回包括列表,总数,页码,每页数量" +// @Router /attachment/getFileList [post] +func (p *PictureLibraryApi) GetCategoryList(c *gin.Context) { + db := global.GVA_DB.Model(&picModel.SysAttachmentCategory{}) + var fileLists []picModel.SysAttachmentCategory + err := db.Find(&fileLists).Error + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + data := picService.BuildTree(fileLists, 0) + response.OkWithDetailed(response.PageResult{ + List: data, + }, "获取成功", c) +} + +// AddCategory +// @Tags sysAttachment +// @Summary 添加分类 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.PageInfo true "页码, 每页大小" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页文件列表,返回包括列表,总数,页码,每页数量" +// @Router /attachment/getFileList [post] +func (p *PictureLibraryApi) AddCategory(c *gin.Context) { + var input struct { + Name string `json:"name" binding:"required"` + Pid uint `json:"pid"` + Id uint `json:"id"` + } + if err := c.ShouldBindJSON(&input); err != nil { + global.GVA_LOG.Error("参数错误!", zap.Error(err)) + response.FailWithMessage("参数错误", c) + return + } + + // 检查是否已存在相同名称的分类 + var existingCategory picModel.SysAttachmentCategory + result := global.GVA_DB.Where("name = ? ", input.Name).First(&existingCategory) + if result.Error == nil && existingCategory.Id != input.Id { + response.FailWithMessage("分类名称已存在", c) + return + } + + // 创建新的分类 + newCategory := picModel.SysAttachmentCategory{Name: input.Name, Pid: input.Pid} + var err error + var title string + if input.Id > 0 { + err = global.GVA_DB.Where("id = ?", input.Id).Updates(newCategory).Error + title = "更新失败" + } else { + err = global.GVA_DB.Create(&newCategory).Error + title = "创建失败" + } + if err != nil { + response.FailWithMessage(title, c) + return + } + + response.OkWithDetailed(response.PageResult{}, "创建成功", c) +} + +// UploadHandler +// @Tags sysAttachment +// @Summary 多文件上传 +// @Security ApiKeyAuth +// @accept multipart/form-data +// @Produce application/json +// @Param file formData [file] true "上传文件示例" +// @Success 200 {object} response.Response{data=exampleRes.ExaFileResponse,msg=string} "上传文件示例,返回包括文件详情" +// @Router /fileUploadAndDownload/upload [post] +func (p *PictureLibraryApi) UploadHandler(c *gin.Context) { + form, _ := c.MultipartForm() + files := form.File["files"] + categoryIDStr := c.PostForm("cat_id") + categoryID, _ := strconv.ParseUint(categoryIDStr, 10, 32) + noSave := c.DefaultQuery("noSave", "0") + for _, file := range files { + classId, _ := strconv.Atoi(c.DefaultPostForm("classId", "0")) + fileData, err := fileUploadAndDownloadService.UploadFile(file, noSave, classId) + if err != nil { + global.GVA_LOG.Error("上传失败!", zap.Error(err)) + response.FailWithMessage("上传失败", c) + return + } + var attachment picModel.SysAttachment + if err := global.GVA_DB.Where("`key` = ? ", fileData.Key).First(&attachment).Error; err != nil { + response.FailWithMessage("上传失败", c) + return + } + // 根据key更新数据 + attachment.CatId = uint(categoryID) + if err := global.GVA_DB.Save(&attachment).Error; err != nil { + response.FailWithMessage("上传文件失败", c) + return + } + } + response.OkWithDetailed(response.PageResult{}, "上传成功", c) +} + +// DeleteFile +// @Tags sysAttachment +// @Summary 删除文件 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body example.ExaFileUploadAndDownload true "传入文件里面id即可" +// @Success 200 {object} response.Response{msg=string} "删除文件" +// @Router /fileUploadAndDownload/deleteFile [post] +func (p *PictureLibraryApi) DeleteFile(c *gin.Context) { + var files []example.ExaFileUploadAndDownload + err := c.ShouldBindJSON(&files) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + for _, file := range files { + if err := fileUploadAndDownloadService.DeleteFile(file); err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + } + response.OkWithMessage("删除成功", c) +} + +// DeleteCategory +// @Tags sysAttachment +// @Summary 删除分类 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body example.ExaFileUploadAndDownload true "传入文件里面id即可" +// @Success 200 {object} response.Response{msg=string} "删除文件" +// @Router /fileUploadAndDownload/deleteFile [post] +func (p *PictureLibraryApi) DeleteCategory(c *gin.Context) { + var input struct { + CatId uint `json:"cat_id"` + } + if err := c.ShouldBindJSON(&input); err != nil { + response.FailWithMessage("参数错误", c) + return + } + if input.CatId == 0 { + response.FailWithMessage("参数错误-1", c) + return + } + var childCount int64 + global.GVA_DB.Model(&picModel.SysAttachmentCategory{}).Where("pid = ?", input.CatId).Count(&childCount) + + if childCount > 0 { + response.FailWithMessage("请先删除子级", c) + return + } + result := global.GVA_DB.Delete(&picModel.SysAttachmentCategory{}, input.CatId) + if result.Error != nil { + response.FailWithMessage("删除失败", c) + return + } + + response.OkWithMessage("删除成功", c) +} diff --git a/plugin/picturelibrary/api/enter.go b/plugin/picturelibrary/api/enter.go new file mode 100644 index 0000000..102a646 --- /dev/null +++ b/plugin/picturelibrary/api/enter.go @@ -0,0 +1,11 @@ +package api + +import "git.echol.cn/loser/lckt/service" + +type ApiGroup struct { + PictureLibraryApi +} + +var ApiGroupApp = new(ApiGroup) + +var fileUploadAndDownloadService = service.ServiceGroupApp.ExampleServiceGroup.FileUploadAndDownloadService diff --git a/plugin/picturelibrary/config/config.go b/plugin/picturelibrary/config/config.go new file mode 100644 index 0000000..d912156 --- /dev/null +++ b/plugin/picturelibrary/config/config.go @@ -0,0 +1 @@ +package config diff --git a/plugin/picturelibrary/global/global.go b/plugin/picturelibrary/global/global.go new file mode 100644 index 0000000..dfbfa4a --- /dev/null +++ b/plugin/picturelibrary/global/global.go @@ -0,0 +1 @@ +package global diff --git a/plugin/picturelibrary/main.go b/plugin/picturelibrary/main.go new file mode 100644 index 0000000..440c3f5 --- /dev/null +++ b/plugin/picturelibrary/main.go @@ -0,0 +1,77 @@ +package picturelibrary + +import ( + gvaGlobal "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/plugin/picturelibrary/model" + "git.echol.cn/loser/lckt/plugin/picturelibrary/router" + "git.echol.cn/loser/lckt/plugin/plugin-tool/utils" + "github.com/gin-gonic/gin" +) + +type PictureLibraryPlugin struct { +} + +func CreatePictureLibraryPlug() *PictureLibraryPlugin { + + gvaGlobal.GVA_DB.AutoMigrate(model.SysAttachment{}, model.SysAttachmentCategory{}) // 此处可以把插件依赖的数据库结构体自动创建表 需要填写对应的结构体 + + // 下方会自动注册菜单 第一个参数为菜单一级路由信息一般为定义好的组名 第二个参数为真实使用的web页面路由信息 + //utils.RegisterMenus( + // system.SysBaseMenu{ + // Path: "picturelibrary", + // Name: "picturelibrary", + // Hidden: false, + // Component: "plugin/picturelibrary/view/index.vue", + // Sort: 0, + // Meta: system.Meta{ + // Title: "图片库", + // Icon: "folder", + // }, + // }, + //) + + // 下方会自动注册api 以下格式为示例格式,请按照实际情况修改 + utils.RegisterApis( + system.SysApi{ + Path: "/pic/pic_library/list", + Description: "图片列表", + ApiGroup: "图片库", + Method: "POST", + }, + system.SysApi{ + Path: "/pic/pic_library/cat_list", + Description: "图片分类列表", + ApiGroup: "图片库", + Method: "POST", + }, + system.SysApi{ + Path: "/pic/pic_library/add_cat", + Description: "添加分类", + ApiGroup: "图片库", + Method: "POST", + }, + system.SysApi{ + Path: "/pic/pic_library/upload_handler", + Description: "上传文件", + ApiGroup: "图片库", + Method: "POST", + }, + system.SysApi{ + Path: "/pic/pic_library/delete_file", + Description: "删除文件", + ApiGroup: "图片库", + Method: "POST", + }, + ) + + return &PictureLibraryPlugin{} +} + +func (*PictureLibraryPlugin) Register(group *gin.RouterGroup) { + router.RouterGroupApp.InitPictureLibraryRouter(group) +} + +func (*PictureLibraryPlugin) RouterPath() string { + return "pic" +} diff --git a/plugin/picturelibrary/model/model.go b/plugin/picturelibrary/model/model.go new file mode 100644 index 0000000..4c3c37b --- /dev/null +++ b/plugin/picturelibrary/model/model.go @@ -0,0 +1,8 @@ +package model + +type PageInfo struct { + Page int `json:"page" form:"page"` + Limit int `json:"limit" form:"limit"` + Cid int `json:"cid" form:"cid"` //分类id + Keyword string `json:"keyword" form:"keyword"` +} diff --git a/plugin/picturelibrary/model/sysAttachment.go b/plugin/picturelibrary/model/sysAttachment.go new file mode 100644 index 0000000..d5f4fec --- /dev/null +++ b/plugin/picturelibrary/model/sysAttachment.go @@ -0,0 +1,14 @@ +package model + +import ( + "git.echol.cn/loser/lckt/model/example" +) + +type SysAttachment struct { + example.ExaFileUploadAndDownload + CatId uint `json:"cat_id" form:"cat_id" gorm:"default:0;type:int;column:cat_id;comment:分类id;"` +} + +func (SysAttachment) TableName() string { + return "exa_file_upload_and_downloads" +} diff --git a/plugin/picturelibrary/model/sysAttachmentCatategory.go b/plugin/picturelibrary/model/sysAttachmentCatategory.go new file mode 100644 index 0000000..545a771 --- /dev/null +++ b/plugin/picturelibrary/model/sysAttachmentCatategory.go @@ -0,0 +1,13 @@ +package model + +type SysAttachmentCategory struct { + Id uint `gorm:"primarykey" json:"id"` // 主键ID + Name string `json:"name" form:"name" gorm:"default:'';type:varchar(255);column:name;comment:分类名称;"` + Pid uint `json:"pid" form:"pid" gorm:"default:0;type:int;column:pid;comment:父节点ID;"` + AddTime int `json:"add_time" form:"add_time" gorm:"default:0;type:int;column:add_time;comment:添加时间;"` + Children []*SysAttachmentCategory `json:"children" gorm:"-"` +} + +func (SysAttachmentCategory) TableName() string { + return "sys_attachment_category" +} diff --git a/plugin/picturelibrary/router/enter.go b/plugin/picturelibrary/router/enter.go new file mode 100644 index 0000000..f3d0e93 --- /dev/null +++ b/plugin/picturelibrary/router/enter.go @@ -0,0 +1,7 @@ +package router + +type RouterGroup struct { + PictureLibraryRouter +} + +var RouterGroupApp = new(RouterGroup) diff --git a/plugin/picturelibrary/router/router.go b/plugin/picturelibrary/router/router.go new file mode 100644 index 0000000..58be69b --- /dev/null +++ b/plugin/picturelibrary/router/router.go @@ -0,0 +1,22 @@ +package router + +import ( + "git.echol.cn/loser/lckt/plugin/picturelibrary/api" + "github.com/gin-gonic/gin" +) + +type PictureLibraryRouter struct { +} + +func (s *PictureLibraryRouter) InitPictureLibraryRouter(Router *gin.RouterGroup) { + plugRouter := Router.Use() + plugApi := api.ApiGroupApp.PictureLibraryApi + { + plugRouter.POST("/pic_library/list", plugApi.GetFileList) + plugRouter.POST("/pic_library/cat_list", plugApi.GetCategoryList) + plugRouter.POST("/pic_library/add_cat", plugApi.AddCategory) + plugRouter.POST("/pic_library/upload_handler", plugApi.UploadHandler) + plugRouter.POST("/pic_library/delete_file", plugApi.DeleteFile) + plugRouter.POST("/pic_library/delete_cat", plugApi.DeleteCategory) + } +} diff --git a/plugin/picturelibrary/service/enter.go b/plugin/picturelibrary/service/enter.go new file mode 100644 index 0000000..6a6bebf --- /dev/null +++ b/plugin/picturelibrary/service/enter.go @@ -0,0 +1,7 @@ +package service + +type ServiceGroup struct { + PictureLibraryService +} + +var ServiceGroupApp = new(ServiceGroup) diff --git a/plugin/picturelibrary/service/service.go b/plugin/picturelibrary/service/service.go new file mode 100644 index 0000000..2d534e5 --- /dev/null +++ b/plugin/picturelibrary/service/service.go @@ -0,0 +1,70 @@ +package service + +import ( + "git.echol.cn/loser/lckt/global" + picModel "git.echol.cn/loser/lckt/plugin/picturelibrary/model" + "github.com/gin-gonic/gin" + "strings" +) + +type PictureLibraryService struct{} + +func (e *PictureLibraryService) PlugService() (err error) { + // 写你的业务逻辑 + return nil +} + +func (e *PictureLibraryService) GetFileRecordInfoList(info picModel.PageInfo, c *gin.Context) (list interface{}, total int64, err error) { + limit := info.Limit + offset := info.Limit * (info.Page - 1) + keyword := info.Keyword + cid := info.Cid + db := global.GVA_DB.Model(&picModel.SysAttachment{}) + var fileLists []picModel.SysAttachment + if len(keyword) > 0 { + db = db.Where("`name` LIKE ?", "%"+keyword+"%") + } + if cid > 0 { + db = db.Where("cat_id = ?", cid) + } + err = db.Count(&total).Error + if err != nil { + return + } + + err = db.Limit(limit).Offset(offset).Order("created_at desc").Find(&fileLists).Error + urlHost := e.GetUrlHost(c) + for k, v := range fileLists { + if !strings.HasPrefix(v.Url, "http://") && !strings.HasPrefix(v.Url, "https://") { + v.Url = urlHost + "api/" + v.Url + fileLists[k] = v + } + } + return fileLists, total, err +} + +// 构建树形结构 +func (e *PictureLibraryService) BuildTree(categories []picModel.SysAttachmentCategory, parentID uint) []*picModel.SysAttachmentCategory { + var tree []*picModel.SysAttachmentCategory + for _, category := range categories { + if category.Pid == parentID { + children := e.BuildTree(categories, category.Id) + category.Children = children + tree = append(tree, &category) + } + } + return tree +} + +func (e *PictureLibraryService) GetUrlHost(c *gin.Context) string { + host := c.Request.Host + scheme := "http" + if c.Request.TLS != nil { + scheme = "https" + } + referer := c.Request.Referer() + if referer != "" { + return referer + } + return scheme + "://" + host + "/" +} diff --git a/test/rain_test.go b/test/rain_test.go new file mode 100644 index 0000000..bd8c773 --- /dev/null +++ b/test/rain_test.go @@ -0,0 +1,27 @@ +package test + +import ( + "fmt" + "golang.org/x/crypto/bcrypt" + "math/rand" + "testing" + "time" +) + +func TestRain(t *testing.T) { + rand.New(rand.NewSource(time.Now().UnixNano())) + verifyCode := fmt.Sprintf("%06v", rand.Int31n(1000000)) + fmt.Println(verifyCode) +} + +func TestPwd(t *testing.T) { + password, _ := bcrypt.GenerateFromPassword([]byte("loser7659"), bcrypt.DefaultCost) + fmt.Println(string(password)) + + err := bcrypt.CompareHashAndPassword(password, []byte("122456")) + if err != nil { + fmt.Println("密码错误") + } else { + fmt.Println("密码正确") + } +}