🎨 添加图库和客服插件(有问题-待修改)

This commit is contained in:
loser 2025-05-14 16:51:31 +08:00
parent 5faff4afa0
commit a5ae680f94
41 changed files with 2592 additions and 12 deletions

2
go.mod
View File

@ -101,11 +101,13 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.24.0 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect

4
go.sum
View File

@ -233,6 +233,8 @@ github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
@ -301,6 +303,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=

View File

@ -1,6 +1,8 @@
package initialize
import (
"git.echol.cn/loser/lckt/plugin/customerservice"
"git.echol.cn/loser/lckt/plugin/picturelibrary"
"net/http"
"os"
@ -104,6 +106,9 @@ func Routers() *gin.Engine {
// 注册业务路由
initBizRouter(PrivateGroup, PublicGroup)
PluginInit(PublicGroup, customerservice.CreateCustomerServicePlug())
PluginInit(PrivateGroup, picturelibrary.CreatePictureLibraryPlug())
global.GVA_ROUTERS = Router.Routes()
global.GVA_LOG.Info("router register success")

View File

@ -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"
}

View 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

View 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)
}

View File

@ -0,0 +1,8 @@
package api
type ApiGroup struct {
CustomerServiceApi
AdminServiceApi
}
var ApiGroupApp = new(ApiGroup)

View 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)
}

View File

@ -0,0 +1 @@
package config

View File

@ -0,0 +1 @@
package global

View 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 ""
}

View 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()
}
}
}

View 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"`
}

View 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"
}

View 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"
}

View 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:是否游客01是;"`
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"
}

View 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"
}

View 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"
}

View 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"
}

View File

@ -0,0 +1,7 @@
package router
type RouterGroup struct {
CustomerServiceRouter
}
var RouterGroupApp = new(RouterGroup)

View 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)
}

View File

@ -0,0 +1,7 @@
package service
type ServiceGroup struct {
CustomerServiceService
}
var ServiceGroupApp = new(ServiceGroup)

View 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
}

View 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
}
}
}
}

View 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
}

View 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
}

View 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")
}
}

View 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

View 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)
}

View 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

View File

@ -0,0 +1 @@
package config

View File

@ -0,0 +1 @@
package global

View 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"
}

View 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"`
}

View 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"
}

View 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"
}

View File

@ -0,0 +1,7 @@
package router
type RouterGroup struct {
PictureLibraryRouter
}
var RouterGroupApp = new(RouterGroup)

View 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)
}
}

View File

@ -0,0 +1,7 @@
package service
type ServiceGroup struct {
PictureLibraryService
}
var ServiceGroupApp = new(ServiceGroup)

View 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
View 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("密码正确")
}
}