🎨 添加图库和客服插件(有问题-待修改)
This commit is contained in:
parent
5faff4afa0
commit
a5ae680f94
2
go.mod
2
go.mod
@ -101,11 +101,13 @@ require (
|
|||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.24.0 // indirect
|
github.com/go-playground/validator/v10 v10.24.0 // indirect
|
||||||
github.com/gofrs/flock v0.12.1 // 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/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // 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/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
|
4
go.sum
4
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.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 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
||||||
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
|
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.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.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
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/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/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/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.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 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package initialize
|
package initialize
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.echol.cn/loser/lckt/plugin/customerservice"
|
||||||
|
"git.echol.cn/loser/lckt/plugin/picturelibrary"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@ -104,6 +106,9 @@ func Routers() *gin.Engine {
|
|||||||
// 注册业务路由
|
// 注册业务路由
|
||||||
initBizRouter(PrivateGroup, PublicGroup)
|
initBizRouter(PrivateGroup, PublicGroup)
|
||||||
|
|
||||||
|
PluginInit(PublicGroup, customerservice.CreateCustomerServicePlug())
|
||||||
|
PluginInit(PrivateGroup, picturelibrary.CreatePictureLibraryPlug())
|
||||||
|
|
||||||
global.GVA_ROUTERS = Router.Routes()
|
global.GVA_ROUTERS = Router.Routes()
|
||||||
|
|
||||||
global.GVA_LOG.Info("router register success")
|
global.GVA_LOG.Info("router register success")
|
||||||
|
@ -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"
|
|
||||||
}
|
|
70
plugin/customerservice/README.md
Normal file
70
plugin/customerservice/README.md
Normal file
@ -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
|
||||||
|
无
|
407
plugin/customerservice/api/api.go
Normal file
407
plugin/customerservice/api/api.go
Normal file
@ -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)
|
||||||
|
}
|
8
plugin/customerservice/api/enter.go
Normal file
8
plugin/customerservice/api/enter.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
type ApiGroup struct {
|
||||||
|
CustomerServiceApi
|
||||||
|
AdminServiceApi
|
||||||
|
}
|
||||||
|
|
||||||
|
var ApiGroupApp = new(ApiGroup)
|
484
plugin/customerservice/api/service.go
Normal file
484
plugin/customerservice/api/service.go
Normal file
@ -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)
|
||||||
|
}
|
1
plugin/customerservice/config/config.go
Normal file
1
plugin/customerservice/config/config.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package config
|
1
plugin/customerservice/global/global.go
Normal file
1
plugin/customerservice/global/global.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package global
|
214
plugin/customerservice/main.go
Normal file
214
plugin/customerservice/main.go
Normal file
@ -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 ""
|
||||||
|
}
|
57
plugin/customerservice/middleware/jwt.go
Normal file
57
plugin/customerservice/middleware/jwt.go
Normal file
@ -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>" 提取 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
25
plugin/customerservice/model/model.go
Normal file
25
plugin/customerservice/model/model.go
Normal file
@ -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"`
|
||||||
|
}
|
18
plugin/customerservice/model/sysService.go
Normal file
18
plugin/customerservice/model/sysService.go
Normal file
@ -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"
|
||||||
|
}
|
18
plugin/customerservice/model/sysServiceMsg.go
Normal file
18
plugin/customerservice/model/sysServiceMsg.go
Normal file
@ -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"
|
||||||
|
}
|
21
plugin/customerservice/model/sysServiceRecord.go
Normal file
21
plugin/customerservice/model/sysServiceRecord.go
Normal file
@ -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"
|
||||||
|
}
|
16
plugin/customerservice/model/sysServiceReply.go
Normal file
16
plugin/customerservice/model/sysServiceReply.go
Normal file
@ -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"
|
||||||
|
}
|
15
plugin/customerservice/model/sysServiceScript.go
Normal file
15
plugin/customerservice/model/sysServiceScript.go
Normal file
@ -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"
|
||||||
|
}
|
11
plugin/customerservice/model/sysTestUser.go
Normal file
11
plugin/customerservice/model/sysTestUser.go
Normal file
@ -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"
|
||||||
|
}
|
7
plugin/customerservice/router/enter.go
Normal file
7
plugin/customerservice/router/enter.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
type RouterGroup struct {
|
||||||
|
CustomerServiceRouter
|
||||||
|
}
|
||||||
|
|
||||||
|
var RouterGroupApp = new(RouterGroup)
|
48
plugin/customerservice/router/router.go
Normal file
48
plugin/customerservice/router/router.go
Normal file
@ -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)
|
||||||
|
}
|
7
plugin/customerservice/service/enter.go
Normal file
7
plugin/customerservice/service/enter.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
type ServiceGroup struct {
|
||||||
|
CustomerServiceService
|
||||||
|
}
|
||||||
|
|
||||||
|
var ServiceGroupApp = new(ServiceGroup)
|
130
plugin/customerservice/service/service.go
Normal file
130
plugin/customerservice/service/service.go
Normal file
@ -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
|
||||||
|
}
|
271
plugin/customerservice/service/ws/ws.go
Normal file
271
plugin/customerservice/service/ws/ws.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
plugin/customerservice/tools/jwt.go
Normal file
54
plugin/customerservice/tools/jwt.go
Normal file
@ -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
|
||||||
|
}
|
77
plugin/customerservice/tools/limits.go
Normal file
77
plugin/customerservice/tools/limits.go
Normal file
@ -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
|
||||||
|
}
|
20
plugin/customerservice/tools/timeformat.go
Normal file
20
plugin/customerservice/tools/timeformat.go
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
}
|
113
plugin/picturelibrary/README.md
Normal file
113
plugin/picturelibrary/README.md
Normal file
@ -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 使用说明
|
||||||
|
|
||||||
|
在你需要图片选择的前端页面引入图库组件,如下:
|
||||||
|
<div class="selected-images">
|
||||||
|
<div class="selected-image" v-for="image in selectedImages" :key="image">
|
||||||
|
<el-image v-if="fileTypeList.includes(image.tag) === true" :src="image.url" style="width: 100%; height: 100%; object-fit: cover;margin-right: 10px;"></el-image>
|
||||||
|
<video v-else controls style="width: 100%; height: 100%;">
|
||||||
|
<source :src="image.url" />
|
||||||
|
</video>
|
||||||
|
<span class="remove-icon" @click="removeSelectedImage(image)"><el-icon><circle-close></circle-close></el-icon></span>
|
||||||
|
</div>
|
||||||
|
<el-icon v-if="isMultiple || selectedImages.length === 0" class="avatar-uploader-icon" @click="openImageLibrary"><Plus /></el-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
图库弹窗:
|
||||||
|
<el-dialog v-model="isDialogVisible" title="图片库" width="950px" destroy-on-close>
|
||||||
|
<ImageLibrary @select="handleImageSelect" :multiple="isMultiple"/>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
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
|
||||||
|
无
|
||||||
|
|
230
plugin/picturelibrary/api/api.go
Normal file
230
plugin/picturelibrary/api/api.go
Normal file
@ -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)
|
||||||
|
}
|
11
plugin/picturelibrary/api/enter.go
Normal file
11
plugin/picturelibrary/api/enter.go
Normal file
@ -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
|
1
plugin/picturelibrary/config/config.go
Normal file
1
plugin/picturelibrary/config/config.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package config
|
1
plugin/picturelibrary/global/global.go
Normal file
1
plugin/picturelibrary/global/global.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package global
|
77
plugin/picturelibrary/main.go
Normal file
77
plugin/picturelibrary/main.go
Normal file
@ -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"
|
||||||
|
}
|
8
plugin/picturelibrary/model/model.go
Normal file
8
plugin/picturelibrary/model/model.go
Normal file
@ -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"`
|
||||||
|
}
|
14
plugin/picturelibrary/model/sysAttachment.go
Normal file
14
plugin/picturelibrary/model/sysAttachment.go
Normal file
@ -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"
|
||||||
|
}
|
13
plugin/picturelibrary/model/sysAttachmentCatategory.go
Normal file
13
plugin/picturelibrary/model/sysAttachmentCatategory.go
Normal file
@ -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"
|
||||||
|
}
|
7
plugin/picturelibrary/router/enter.go
Normal file
7
plugin/picturelibrary/router/enter.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
type RouterGroup struct {
|
||||||
|
PictureLibraryRouter
|
||||||
|
}
|
||||||
|
|
||||||
|
var RouterGroupApp = new(RouterGroup)
|
22
plugin/picturelibrary/router/router.go
Normal file
22
plugin/picturelibrary/router/router.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
7
plugin/picturelibrary/service/enter.go
Normal file
7
plugin/picturelibrary/service/enter.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
type ServiceGroup struct {
|
||||||
|
PictureLibraryService
|
||||||
|
}
|
||||||
|
|
||||||
|
var ServiceGroupApp = new(ServiceGroup)
|
70
plugin/picturelibrary/service/service.go
Normal file
70
plugin/picturelibrary/service/service.go
Normal file
@ -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 + "/"
|
||||||
|
}
|
27
test/rain_test.go
Normal file
27
test/rain_test.go
Normal file
@ -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("密码正确")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user