feat: add custom list

This commit is contained in:
goder-zhang 2026-02-15 12:54:33 +00:00
parent a203de46f7
commit eeda43d30c
21 changed files with 475 additions and 70 deletions

View File

@ -1,7 +1,10 @@
package cmd package cmd
import ( import (
"ai-css/common"
"ai-css/library/logger" "ai-css/library/logger"
"ai-css/models"
"ai-css/ws"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@ -28,13 +31,16 @@ func args(cmd *cobra.Command, args []string) error {
} }
func Execute() { func Execute() {
logger.InitDefault() logger.InitDefault()
common.LoadConfig()
rootCmd.AddCommand(serverCmd)
rootCmd.AddCommand(installCmd)
rootCmd.AddCommand(stopCmd)
models.Connect()
ws.StartUpdateVisitorStatusCron()
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
} }
func init() {
rootCmd.AddCommand(serverCmd)
rootCmd.AddCommand(installCmd)
rootCmd.AddCommand(stopCmd)
}

View File

@ -1,5 +1,11 @@
package common package common
import (
"fmt"
"os"
"path"
)
var ( var (
PageSize uint = 10 PageSize uint = 10
VisitorPageSize uint = 8 VisitorPageSize uint = 8
@ -10,3 +16,21 @@ var (
MysqlConf string = Dir + "mysql.json" MysqlConf string = Dir + "mysql.json"
IsCompireTemplate bool = false //是否编译静态模板到二进制 IsCompireTemplate bool = false //是否编译静态模板到二进制
) )
const (
ENV_DEV = "dev"
ENV_PROD = "prod"
)
var (
environment = os.Getenv("AICSS_ENV")
)
func getConfigPath() string {
switch environment {
case ENV_DEV, ENV_PROD:
return path.Join(Dir, fmt.Sprintf("config_%s.yaml", environment))
default:
return path.Join(Dir, "config.yaml")
}
}

View File

@ -1,29 +1,59 @@
package common package common
import ( import (
"ai-css/tools" "fmt"
"encoding/json" "sync"
"io/ioutil"
"github.com/spf13/viper"
) )
type Mysql struct { type Config struct {
Server string Service Service `mapstructure:"service" json:"service"` // 服务配置
Port string MysqlService Mysql `mapstructure:"mysql_service" json:"mysql_service"`
Database string
Username string
Password string
} }
func GetMysqlConf() *Mysql { type Service struct {
var mysql = &Mysql{} Sites map[string]SiteConfig `mapstructure:"site" json:"site"`
isExist, _ := tools.IsFileExist(MysqlConf) }
if !isExist {
return mysql type SiteConfig struct {
} UnauthURI string `mapstructure:"unauth_uri" json:"unauth_uri"`
info, err := ioutil.ReadFile(MysqlConf) }
if err != nil {
return mysql type Mysql struct {
} Server string `mapstructure:"server" json:"server"`
err = json.Unmarshal(info, mysql) Port string `mapstructure:"port" json:"port"`
return mysql Database string `mapstructure:"database" json:"database"`
Username string `mapstructure:"username" json:"username"`
Password string `mapstructure:"password" json:"password"`
}
var (
cfg Config
loadOnce sync.Once
)
func LoadConfig() Config {
loadOnce.Do(func() {
var configPath = getConfigPath()
viper.SetConfigType("yaml")
viper.SetConfigFile(configPath)
err := viper.ReadInConfig()
if err != nil {
panic(fmt.Errorf("read file failed err:%v,file:%s", err, configPath))
}
err = viper.Unmarshal(&cfg)
if err != nil {
panic(fmt.Errorf("errpr viper.Unnarshal err:%v,file:%s", err, configPath))
}
})
return cfg
}
func GetMysqlConfig() Mysql {
return cfg.MysqlService
}
func GetServiceConfig() Service {
return cfg.Service
} }

6
config/config.yaml Normal file
View File

@ -0,0 +1,6 @@
mysql_service:
server: xpink-prod-mysql.cjkmm024cf76.ap-east-1.rds.amazonaws.com
port: 3306
database: aicss_db
username: admin
password: o()Vq$NuZdwoEe>VLalG]CBMp4LZ

6
config/config_prod.yaml Normal file
View File

@ -0,0 +1,6 @@
mysql_service:
server: xpink-prod-mysql.cjkmm024cf76.ap-east-1.rds.amazonaws.com
port: 3306
database: aicss_db
username: admin
password: o()Vq$NuZdwoEe>VLalG]CBMp4LZ

View File

@ -5,10 +5,65 @@ import (
"ai-css/tools" "ai-css/tools"
"ai-css/ws" "ai-css/ws"
"net/http" "net/http"
"strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func PostKefuStatus(c *gin.Context) {
v := c.GetString("kefu_name")
userInfo := models.FindUser(v)
if userInfo.Role != 1 {
c.JSON(200, gin.H{
"code": 400,
"msg": "权限不足",
})
return
}
kefuName := c.PostForm("username")
statusStr := c.PostForm("status")
status, _ := strconv.Atoi(statusStr)
models.UpdateUserStatus(kefuName, int32(status))
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": "",
})
}
func PostAdminResetPass(c *gin.Context) {
v := c.GetString("kefu_name")
userInfo := models.FindUser(v)
if userInfo.Role != 1 {
c.JSON(200, gin.H{
"code": 400,
"msg": "权限不足",
})
return
}
kefuName := c.PostForm("username")
newPass := c.PostForm("password")
if kefuName == "" || newPass == "" {
c.JSON(200, gin.H{
"code": 400,
"msg": "Username and password are required",
"result": "",
})
return
}
models.UpdateUserPass(kefuName, tools.Md5(newPass))
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": "",
})
}
func PostKefuAvator(c *gin.Context) { func PostKefuAvator(c *gin.Context) {
avator := c.PostForm("avator") avator := c.PostForm("avator")
@ -78,22 +133,46 @@ func PostKefuClient(c *gin.Context) {
func GetIdleKefu(c *gin.Context) { func GetIdleKefu(c *gin.Context) {
visitorId := c.Query("visitor_id") visitorId := c.Query("visitor_id")
var kefuName string var kefuName, oldKefuname string
var reset bool
var visitorDataId uint
if visitorId != "" { if visitorId != "" {
visitor := models.FindVisitorByVistorId(visitorId) visitor := models.FindVisitorByVistorId(visitorId)
if visitor.ToId != "" { if visitor.ToId != "" {
kefuName = visitor.ToId kefuName = visitor.ToId
visitorDataId = visitor.ID
}
if kefuName != "" {
userInfo := models.FindUser(kefuName)
if userInfo.ID == 0 || userInfo.Status == 0 || userInfo.IsOnline == 0 {
reset = true
oldKefuname = kefuName
kefuName = ""
}
} }
} }
if kefuName == "" { if kefuName == "" {
user := models.FindIdleUser() user := models.FindIdleUser()
kefuName = user.Name kefuName = user.Name
if reset {
if visitorDataId > 0 {
models.UpdatesVisitor(visitorDataId, kefuName)
}
}
}
if kefuName == "" {
c.JSON(200, gin.H{
"code": 400,
"msg": "暂时没有在线客服",
})
} else {
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": kefuName,
"oldResult": oldKefuname,
})
} }
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": kefuName,
})
} }
func GetKefuInfo(c *gin.Context) { func GetKefuInfo(c *gin.Context) {
kefuName, _ := c.Get("kefu_name") kefuName, _ := c.Get("kefu_name")
@ -124,7 +203,7 @@ func GetOtherKefuList(c *gin.Context) {
id := idStr.(float64) id := idStr.(float64)
result := make([]interface{}, 0) result := make([]interface{}, 0)
ws.SendPingToKefuClient() ws.SendPingToKefuClient()
kefus := models.FindUsers() kefus := models.FindUsers("")
for _, kefu := range kefus { for _, kefu := range kefus {
if uint(id) == kefu.ID { if uint(id) == kefu.ID {
continue continue
@ -180,6 +259,15 @@ func GetKefuInfoSetting(c *gin.Context) {
}) })
} }
func PostKefuRegister(c *gin.Context) { func PostKefuRegister(c *gin.Context) {
kefuName := c.GetString("kefu_name")
userInfo := models.FindUser(kefuName)
if userInfo.Role != 1 {
c.JSON(200, gin.H{
"code": 400,
"msg": "权限不足",
})
return
}
name := c.PostForm("username") name := c.PostForm("username")
password := c.PostForm("password") password := c.PostForm("password")
nickname := c.PostForm("nickname") nickname := c.PostForm("nickname")
@ -246,7 +334,16 @@ func PostKefuInfo(c *gin.Context) {
}) })
} }
func GetKefuList(c *gin.Context) { func GetKefuList(c *gin.Context) {
users := models.FindUsers() kefuName := c.GetString("kefu_name")
userInfo := models.FindUser(kefuName)
if userInfo.Role != 1 {
c.JSON(200, gin.H{
"code": 400,
"msg": "权限不足",
})
return
}
users := models.FindUsers(kefuName)
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"code": 200, "code": 200,
"msg": "获取成功", "msg": "获取成功",

View File

@ -3,8 +3,9 @@ package controller
import ( import (
"ai-css/models" "ai-css/models"
"ai-css/tools" "ai-css/tools"
"github.com/gin-gonic/gin"
"time" "time"
"github.com/gin-gonic/gin"
) )
// @Summary User Authentication API // @Summary User Authentication API
@ -33,6 +34,14 @@ func LoginCheckPass(c *gin.Context) {
return return
} }
if info.Status != 1 {
c.JSON(200, gin.H{
"code": 400,
"message": "账号已经停用",
})
return
}
// Prepare user session data // Prepare user session data
userinfo := map[string]interface{}{ userinfo := map[string]interface{}{
"kefu_name": info.Name, "kefu_name": info.Name,

View File

@ -16,6 +16,7 @@ func JwtPageMiddleware(c *gin.Context) {
// c.Abort() // c.Abort()
//} //}
} }
func JwtApiMiddleware(c *gin.Context) { func JwtApiMiddleware(c *gin.Context) {
token := c.GetHeader("aicss-token") token := c.GetHeader("aicss-token")
if token == "" { if token == "" {

View File

@ -0,0 +1,31 @@
package login_direct
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func DomainAuthRedirectMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
host := c.Request.Host
// 只针对 admin 域名
if strings.HasPrefix(host, "admin.example.com") {
// 这里写你的认证逻辑
token := c.GetHeader("Authorization")
if token == "" {
// 未认证跳转
c.Redirect(http.StatusFound, "https://admin.example.com/login")
c.Abort()
return
}
}
c.Next()
}
}

View File

@ -18,25 +18,21 @@ type Model struct {
DeletedAt *time.Time `sql:"index" json:"deleted_at"` DeletedAt *time.Time `sql:"index" json:"deleted_at"`
} }
func init() { func Connect() {
Connect() mysql := common.GetMysqlConfig()
}
func Connect() error {
mysql := common.GetMysqlConf()
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysql.Username, mysql.Password, mysql.Server, mysql.Port, mysql.Database) dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysql.Username, mysql.Password, mysql.Server, mysql.Port, mysql.Database)
var err error var err error
DB, err = gorm.Open("mysql", dsn) DB, err = gorm.Open("mysql", dsn)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
panic("数据库连接失败!") panic("数据库连接失败!")
return err
} }
DB.SingularTable(true) DB.SingularTable(true)
DB.LogMode(true) DB.LogMode(true)
DB.DB().SetMaxIdleConns(10) DB.DB().SetMaxIdleConns(10)
DB.DB().SetMaxOpenConns(100) DB.DB().SetMaxOpenConns(100)
DB.DB().SetConnMaxLifetime(59 * time.Second) DB.DB().SetConnMaxLifetime(59 * time.Second)
return nil DB.AutoMigrate(&User{})
} }
func Execute(sql string) error { func Execute(sql string) error {
return DB.Exec(sql).Error return DB.Exec(sql).Error

View File

@ -16,6 +16,8 @@ type User struct {
Role int32 `json:"role"` Role int32 `json:"role"`
RoleName string `json:"role_name" sql:"-"` RoleName string `json:"role_name" sql:"-"`
RoleId string `json:"role_id" sql:"-"` RoleId string `json:"role_id" sql:"-"`
Status int32 `json:"status" gorm:"default:1"` // 1: active, 0: inactive
IsOnline int32 `json:"is_online" gorm:"default:0"` // 1: online, 0: offline
} }
func CreateUser(name string, password string, avator string, nickname string) uint { func CreateUser(name string, password string, avator string, nickname string) uint {
@ -29,6 +31,15 @@ func CreateUser(name string, password string, avator string, nickname string) ui
DB.Create(user) DB.Create(user)
return user.ID return user.ID
} }
func UpdateUserIsOnline(name string, isOnline int32) {
user := &User{
IsOnline: isOnline,
}
user.UpdatedAt = time.Now()
DB.Model(user).Where("name = ?", name).Update("IsOnline", isOnline)
}
func UpdateUser(name string, password string, avator string, nickname string) { func UpdateUser(name string, password string, avator string, nickname string) {
user := &User{ user := &User{
Avator: avator, Avator: avator,
@ -47,6 +58,13 @@ func UpdateUserPass(name string, pass string) {
user.UpdatedAt = time.Now() user.UpdatedAt = time.Now()
DB.Model(user).Where("name = ?", name).Update("Password", pass) DB.Model(user).Where("name = ?", name).Update("Password", pass)
} }
func UpdateUserStatus(name string, status int32) {
user := &User{
Status: status,
}
user.UpdatedAt = time.Now()
DB.Model(user).Where("name = ?", name).Update("Status", status)
}
func UpdateUserAvator(name string, avator string) { func UpdateUserAvator(name string, avator string) {
user := &User{ user := &User{
Avator: avator, Avator: avator,
@ -70,7 +88,7 @@ func FindIdleUser() User {
defer assignMutex.Unlock() defer assignMutex.Unlock()
var users []User var users []User
DB.Where("name != ?", "admin").Order("id desc").Find(&users) DB.Where("name != ? and `status` = 1 and `is_online` = 1", "admin").Order("id desc").Find(&users)
if len(users) == 0 { if len(users) == 0 {
return User{} return User{}
@ -104,9 +122,13 @@ func FindUserById(id interface{}) User {
func DeleteUserById(id string) { func DeleteUserById(id string) {
DB.Where("id = ?", id).Delete(User{}) DB.Where("id = ?", id).Delete(User{})
} }
func FindUsers() []User { func FindUsers(withoutUsername string) []User {
var users []User var users []User
DB.Select("user.*,role.name role_name").Joins("left join user_role on user.id=user_role.user_id").Joins("left join role on user_role.role_id=role.id").Order("user.id desc").Find(&users) if withoutUsername == "" {
DB.Find(&users)
} else {
DB.Where("name != ?", withoutUsername).Find(&users)
}
return users return users
} }
func FindUserRole(query interface{}, id interface{}) User { func FindUserRole(query interface{}, id interface{}) User {

View File

@ -39,6 +39,11 @@ func FindVisitorByVistorId(visitorId string) Visitor {
DB.Where("visitor_id = ?", visitorId).First(&v) DB.Where("visitor_id = ?", visitorId).First(&v)
return v return v
} }
func UpdatesVisitor(id uint, toId string) {
DB.Model(&Visitor{}).Where("id = ?", id).Update("to_id", toId)
}
func FindVisitors(page uint, pagesize uint) []Visitor { func FindVisitors(page uint, pagesize uint) []Visitor {
offset := (page - 1) * pagesize offset := (page - 1) * pagesize
if offset < 0 { if offset < 0 {
@ -86,21 +91,21 @@ func UpdateVisitorKefu(visitorId string, kefuId string) {
DB.Model(&visitor).Where("visitor_id = ?", visitorId).Update("to_id", kefuId) DB.Model(&visitor).Where("visitor_id = ?", visitorId).Update("to_id", kefuId)
} }
//查询条数 // 查询条数
func CountVisitors() uint { func CountVisitors() uint {
var count uint var count uint
DB.Model(&Visitor{}).Count(&count) DB.Model(&Visitor{}).Count(&count)
return count return count
} }
//查询条数 // 查询条数
func CountVisitorsByKefuId(kefuId string) uint { func CountVisitorsByKefuId(kefuId string) uint {
var count uint var count uint
DB.Model(&Visitor{}).Where("to_id=?", kefuId).Count(&count) DB.Model(&Visitor{}).Where("to_id=?", kefuId).Count(&count)
return count return count
} }
//查询每天条数 // 查询每天条数
type EveryDayNum struct { type EveryDayNum struct {
Day string `json:"day"` Day string `json:"day"`
Num int64 `json:"num"` Num int64 `json:"num"`

Binary file not shown.

View File

@ -25,7 +25,7 @@ func InitApiRouter(engine *gin.RouterGroup) {
engine.POST("/check", controller.LoginCheckPass) engine.POST("/check", controller.LoginCheckPass)
engine.GET("/userinfo", middleware.JwtApiMiddleware, controller.GetKefuInfoAll) engine.GET("/userinfo", middleware.JwtApiMiddleware, controller.GetKefuInfoAll)
engine.POST("/register", middleware.Ipblack, controller.PostKefuRegister) engine.POST("/register", middleware.JwtApiMiddleware, controller.PostKefuRegister)
engine.POST("/install", controller.PostInstall) engine.POST("/install", controller.PostInstall)
//前后聊天 //前后聊天
engine.GET("/ws_kefu", middleware.JwtApiMiddleware, ws.NewKefuServer) engine.GET("/ws_kefu", middleware.JwtApiMiddleware, ws.NewKefuServer)
@ -50,6 +50,8 @@ func InitApiRouter(engine *gin.RouterGroup) {
engine.POST("/kefuinfo", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.PostKefuInfo) engine.POST("/kefuinfo", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.PostKefuInfo)
engine.DELETE("/kefuinfo", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.DeleteKefuInfo) engine.DELETE("/kefuinfo", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.DeleteKefuInfo)
engine.GET("/kefulist", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.GetKefuList) engine.GET("/kefulist", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.GetKefuList)
engine.POST("/kefu_status", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.PostKefuStatus)
engine.POST("/admin_reset_pass", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.PostAdminResetPass)
engine.GET("/other_kefulist", middleware.JwtApiMiddleware, controller.GetOtherKefuList) engine.GET("/other_kefulist", middleware.JwtApiMiddleware, controller.GetOtherKefuList)
engine.GET("/trans_kefu", middleware.JwtApiMiddleware, controller.PostTransKefu) engine.GET("/trans_kefu", middleware.JwtApiMiddleware, controller.PostTransKefu)
engine.POST("/modifypass", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.PostKefuPass) engine.POST("/modifypass", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.PostKefuPass)

View File

@ -726,6 +726,16 @@
success: function(data) { success: function(data) {
if(data.code==200 && data.msg=="ok"){ if(data.code==200 && data.msg=="ok"){
KEFU_ID=data.result; KEFU_ID=data.result;
var oldResult = data.oldResult;
if (oldResult && oldResult != KEFU_ID) {
var oldKey = "visitor_" + oldResult;
var newKey = "visitor_" + KEFU_ID;
var oldCache = localStorage.getItem(oldKey);
if(oldCache) {
localStorage.setItem(newKey, oldCache);
localStorage.removeItem(oldKey);
}
}
}else{ }else{
KEFU_ID="default"; KEFU_ID="default";
} }

View File

@ -121,6 +121,8 @@
] ]
}, },
showRegHtml: false, showRegHtml: false,
kefuInfo:{},
kefuList: [],
}, },
methods: { methods: {
validatePasswordMatch(rule, value, callback) { validatePasswordMatch(rule, value, callback) {
@ -190,31 +192,45 @@
"nickname": this.form.nickname, "nickname": this.form.nickname,
}; };
$.post("/aicss/register", data, (response) => { $.ajax({
if (response.code === 200) { url: "/aicss/register",
type: "POST",
data: data,
headers: {
"aicss-token": localStorage.getItem("aicss-token"),
"X-Client-Type": "web",
"X-Custom-Domain": window.location.host
},
success: (response) => {
if (response.code === 200) {
this.$message({
message: 'Account created successfully!',
type: 'success'
});
this.showRegHtml = false;
} else {
this.$message({
message: response.msg || 'Registration failed',
type: 'error'
});
}
},
error: () => {
this.$message({ this.$message({
message: 'Account created successfully!', message: 'Connection error',
type: 'success'
});
this.showRegHtml = false;
} else {
this.$message({
message: response.msg || 'Registration failed',
type: 'error' type: 'error'
}); });
} }
}).fail(() => {
this.$message({
message: 'Connection error',
type: 'error'
});
}); });
} }
}, },
created: function() { created: function() {
if (top.location != location) { if (top.location != location) {
top.location.href = location.href; top.location.href = location.href;
} }
this.getKefuInfo();
} }
}); });
</script> </script>

View File

@ -115,6 +115,43 @@
</div> </div>
> >
</el-tab-pane> </el-tab-pane>
<el-tab-pane v-if="kefuInfo.role === 1" label="Customer List">
<div class="profile-form">
<div style="margin-bottom: 20px;">
<el-button type="primary" icon="el-icon-plus" @click="openCreateKefu">添加客服</el-button>
</div>
<el-table :data="kefuList" style="width: 100%" stripe>
<el-table-column label="头像" width="80">
<template slot-scope="scope">
<el-avatar :size="40" :src="scope.row.avator"></el-avatar>
</template>
</el-table-column>
<el-table-column prop="name" label="账号"></el-table-column>
<el-table-column prop="nickname" label="昵称"></el-table-column>
<el-table-column label="在线状态" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.is_online === 1" type="success" size="mini">在线</el-tag>
<el-tag v-else type="info" size="mini">离线</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template slot-scope="scope">
<el-switch
v-model="scope.row.status"
:active-value="1"
:inactive-value="0"
@change="handleStatusChange(scope.row)">
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="150">
<template slot-scope="scope">
<el-button size="mini" type="warning" @click="openResetPass(scope.row)">重置密码</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
<el-tab-pane v-if="kefuInfo.role === 1" label="System Configuration"> <el-tab-pane v-if="kefuInfo.role === 1" label="System Configuration">
<div class="profile-form" style="margin-top: 20px"> <div class="profile-form" style="margin-top: 20px">
<el-table <el-table
@ -174,6 +211,36 @@
</div> </div>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<el-dialog title="创建客服" :visible.sync="createKefuDialog" width="400px">
<el-form :model="createKefuForm">
<el-form-item label="账号">
<el-input v-model="createKefuForm.username"></el-input>
</el-form-item>
<el-form-item label="昵称">
<el-input v-model="createKefuForm.nickname"></el-input>
</el-form-item>
<el-form-item label="密码">
<el-input type="password" v-model="createKefuForm.password" show-password></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="createKefuDialog = false">取 消</el-button>
<el-button type="primary" @click="submitCreateKefu">确 定</el-button>
</div>
</el-dialog>
<el-dialog title="重置密码" :visible.sync="resetPassDialog" width="400px">
<el-form :model="resetPassForm">
<el-form-item label="新密码">
<el-input type="password" v-model="resetPassForm.password" show-password></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="resetPassDialog = false">取 消</el-button>
<el-button type="primary" @click="submitResetPass">确 定</el-button>
</div>
</el-dialog>
</template> </template>
</div> </div>
</body> </body>

View File

@ -25,6 +25,18 @@
role_name:"", role_name:"",
role_id:"", role_id:"",
}, },
kefuList: [],
resetPassDialog: false,
createKefuDialog: false,
resetPassForm: {
username: "",
password: ""
},
createKefuForm: {
username: "",
nickname: "",
password: ""
},
avatarUrl:"", avatarUrl:"",
chatEndpoint: "", chatEndpoint: "",
@ -303,10 +315,73 @@ A: 因二次结算导致,系统将按最终结果扣回或补发。`}
if(data.code==200 && data.result!=null){ if(data.code==200 && data.result!=null){
_this.kefuInfo=data.result; _this.kefuInfo=data.result;
_this.chatEndpoint=window.location.origin + '/aicss/livechat?kefu_id='+data.result.username; _this.chatEndpoint=window.location.origin + '/aicss/livechat?kefu_id='+data.result.username;
if (_this.kefuInfo.role === 1) {
_this.getKefuList();
}
} }
} }
}); });
}, },
// Get Kefu List
getKefuList() {
let _this = this;
this.sendAjax("/aicss/kefulist", "get", {}, function(result) {
_this.kefuList = result;
});
},
// Toggle Status
handleStatusChange(row) {
let _this = this;
this.sendAjax("/kefu_status", "post", {
username: row.name,
status: row.status
}, function(result) {
_this.$message({
message: "Status updated",
type: 'success'
});
});
},
// Open Reset Password Dialog
openResetPass(row) {
this.resetPassForm.username = row.name;
this.resetPassForm.password = "";
this.resetPassDialog = true;
},
// Submit Reset Password
submitResetPass() {
let _this = this;
if (this.resetPassForm.password.length < 2) {
this.$message.error("Password too short");
return;
}
this.sendAjax("/admin_reset_pass", "post", this.resetPassForm, function(result) {
_this.$message.success("Password reset successfully");
_this.resetPassDialog = false;
});
},
// Open Create User Dialog
openCreateKefu() {
this.createKefuForm = {
username: "",
nickname: "",
password: ""
};
this.createKefuDialog = true;
},
// Submit Create User
submitCreateKefu() {
let _this = this;
if (!this.createKefuForm.username || !this.createKefuForm.password) {
this.$message.error("Username and password required");
return;
}
this.sendAjax("/register", "post", this.createKefuForm, function(result) {
_this.$message.success("User created successfully");
_this.createKefuDialog = false;
_this.getKefuList();
});
},
}, },
mounted:function(){ mounted:function(){
@ -325,4 +400,5 @@ A: 因二次结算导致,系统将按最终结果扣回或补发。`}
</script> </script>
</html> </html>
{{end}}
{{end}}

View File

@ -1,16 +1,13 @@
package tmpl package tmpl
import ( import (
"ai-css/tools"
"github.com/gin-gonic/gin"
"net/http" "net/http"
"github.com/gin-gonic/gin"
) )
// 登陆界面 // 登陆界面
func PageLogin(c *gin.Context) { func PageLogin(c *gin.Context) {
if noExist, _ := tools.IsFileNotExist("./install.lock"); noExist {
c.Redirect(302, "/install")
}
c.HTML(http.StatusOK, "login.html", nil) c.HTML(http.StatusOK, "login.html", nil)
} }

View File

@ -35,6 +35,7 @@ func NewKefuServer(c *gin.Context) {
kefu.Avator = kefuInfo.Avator kefu.Avator = kefuInfo.Avator
kefu.Conn = conn kefu.Conn = conn
AddKefuToList(&kefu) AddKefuToList(&kefu)
models.UpdateUserIsOnline(kefuInfo.Name, 1)
for { for {
//接受消息 //接受消息
@ -43,6 +44,7 @@ func NewKefuServer(c *gin.Context) {
if err != nil { if err != nil {
log.Println("ws/user.go ", err) log.Println("ws/user.go ", err)
conn.Close() conn.Close()
models.UpdateUserIsOnline(kefuInfo.Name, 0)
return return
} }
@ -114,6 +116,7 @@ func SendPingToKefuClient() {
if err != nil { if err != nil {
log.Println("定时发送ping给客服失败", err.Error()) log.Println("定时发送ping给客服失败", err.Error())
delete(KefuList, kefuId) delete(KefuList, kefuId)
models.UpdateUserIsOnline(kefuId, 0)
} }
} }
} }

View File

@ -64,7 +64,7 @@ var message = make(chan *Message, 10)
var upgrader = websocket.Upgrader{} var upgrader = websocket.Upgrader{}
var Mux sync.RWMutex var Mux sync.RWMutex
func init() { func StartUpdateVisitorStatusCron() {
upgrader = websocket.Upgrader{ upgrader = websocket.Upgrader{
ReadBufferSize: 1024, ReadBufferSize: 1024,
WriteBufferSize: 1024, WriteBufferSize: 1024,
@ -75,6 +75,7 @@ func init() {
} }
go UpdateVisitorStatusCron() go UpdateVisitorStatusCron()
} }
func SendServerJiang(title string, content string, domain string) string { func SendServerJiang(title string, content string, domain string) string {
noticeServerJiang, err := strconv.ParseBool(models.FindConfig("NoticeServerJiang")) noticeServerJiang, err := strconv.ParseBool(models.FindConfig("NoticeServerJiang"))
serverJiangAPI := models.FindConfig("ServerJiangAPI") serverJiangAPI := models.FindConfig("ServerJiangAPI")