diff --git a/cmd/root.go b/cmd/root.go index 45f8e40..49836c3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,7 +1,10 @@ package cmd import ( + "ai-css/common" "ai-css/library/logger" + "ai-css/models" + "ai-css/ws" "errors" "fmt" "os" @@ -28,13 +31,16 @@ func args(cmd *cobra.Command, args []string) error { } func Execute() { logger.InitDefault() + common.LoadConfig() + rootCmd.AddCommand(serverCmd) + rootCmd.AddCommand(installCmd) + rootCmd.AddCommand(stopCmd) + + models.Connect() + ws.StartUpdateVisitorStatusCron() + if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } } -func init() { - rootCmd.AddCommand(serverCmd) - rootCmd.AddCommand(installCmd) - rootCmd.AddCommand(stopCmd) -} diff --git a/common/common.go b/common/common.go index 0b79b39..d3b2363 100644 --- a/common/common.go +++ b/common/common.go @@ -1,5 +1,11 @@ package common +import ( + "fmt" + "os" + "path" +) + var ( PageSize uint = 10 VisitorPageSize uint = 8 @@ -10,3 +16,21 @@ var ( MysqlConf string = Dir + "mysql.json" 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") + } +} diff --git a/common/config.go b/common/config.go index 28ddf8a..816aafc 100644 --- a/common/config.go +++ b/common/config.go @@ -1,29 +1,59 @@ package common import ( - "ai-css/tools" - "encoding/json" - "io/ioutil" + "fmt" + "sync" + + "github.com/spf13/viper" ) -type Mysql struct { - Server string - Port string - Database string - Username string - Password string +type Config struct { + Service Service `mapstructure:"service" json:"service"` // 服务配置 + MysqlService Mysql `mapstructure:"mysql_service" json:"mysql_service"` } -func GetMysqlConf() *Mysql { - var mysql = &Mysql{} - isExist, _ := tools.IsFileExist(MysqlConf) - if !isExist { - return mysql - } - info, err := ioutil.ReadFile(MysqlConf) - if err != nil { - return mysql - } - err = json.Unmarshal(info, mysql) - return mysql +type Service struct { + Sites map[string]SiteConfig `mapstructure:"site" json:"site"` +} + +type SiteConfig struct { + UnauthURI string `mapstructure:"unauth_uri" json:"unauth_uri"` +} + +type Mysql struct { + Server string `mapstructure:"server" json:"server"` + Port string `mapstructure:"port" json:"port"` + 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 } diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..31b76d3 --- /dev/null +++ b/config/config.yaml @@ -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 diff --git a/config/config_prod.yaml b/config/config_prod.yaml new file mode 100644 index 0000000..31b76d3 --- /dev/null +++ b/config/config_prod.yaml @@ -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 diff --git a/controller/kefu.go b/controller/kefu.go index 5e72c97..e3e739c 100644 --- a/controller/kefu.go +++ b/controller/kefu.go @@ -5,10 +5,65 @@ import ( "ai-css/tools" "ai-css/ws" "net/http" + "strconv" "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) { avator := c.PostForm("avator") @@ -78,22 +133,46 @@ func PostKefuClient(c *gin.Context) { func GetIdleKefu(c *gin.Context) { visitorId := c.Query("visitor_id") - var kefuName string + var kefuName, oldKefuname string + var reset bool + var visitorDataId uint if visitorId != "" { visitor := models.FindVisitorByVistorId(visitorId) if 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 == "" { user := models.FindIdleUser() 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) { kefuName, _ := c.Get("kefu_name") @@ -124,7 +203,7 @@ func GetOtherKefuList(c *gin.Context) { id := idStr.(float64) result := make([]interface{}, 0) ws.SendPingToKefuClient() - kefus := models.FindUsers() + kefus := models.FindUsers("") for _, kefu := range kefus { if uint(id) == kefu.ID { continue @@ -180,6 +259,15 @@ func GetKefuInfoSetting(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") password := c.PostForm("password") nickname := c.PostForm("nickname") @@ -246,7 +334,16 @@ func PostKefuInfo(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{ "code": 200, "msg": "获取成功", diff --git a/controller/login.go b/controller/login.go index 06bf108..5d9c8f0 100644 --- a/controller/login.go +++ b/controller/login.go @@ -3,8 +3,9 @@ package controller import ( "ai-css/models" "ai-css/tools" - "github.com/gin-gonic/gin" "time" + + "github.com/gin-gonic/gin" ) // @Summary User Authentication API @@ -33,6 +34,14 @@ func LoginCheckPass(c *gin.Context) { return } + if info.Status != 1 { + c.JSON(200, gin.H{ + "code": 400, + "message": "账号已经停用", + }) + return + } + // Prepare user session data userinfo := map[string]interface{}{ "kefu_name": info.Name, diff --git a/middleware/jwt.go b/middleware/jwt.go index 299fb21..9bf8ca7 100644 --- a/middleware/jwt.go +++ b/middleware/jwt.go @@ -16,6 +16,7 @@ func JwtPageMiddleware(c *gin.Context) { // c.Abort() //} } + func JwtApiMiddleware(c *gin.Context) { token := c.GetHeader("aicss-token") if token == "" { diff --git a/middleware/login_direct/login.go b/middleware/login_direct/login.go new file mode 100644 index 0000000..30eca82 --- /dev/null +++ b/middleware/login_direct/login.go @@ -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() + } +} diff --git a/models/models.go b/models/models.go index 3639372..a05f0e6 100644 --- a/models/models.go +++ b/models/models.go @@ -18,25 +18,21 @@ type Model struct { DeletedAt *time.Time `sql:"index" json:"deleted_at"` } -func init() { - Connect() -} -func Connect() error { - mysql := common.GetMysqlConf() +func Connect() { + mysql := common.GetMysqlConfig() 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 DB, err = gorm.Open("mysql", dsn) if err != nil { log.Println(err) panic("数据库连接失败!") - return err } DB.SingularTable(true) DB.LogMode(true) DB.DB().SetMaxIdleConns(10) DB.DB().SetMaxOpenConns(100) DB.DB().SetConnMaxLifetime(59 * time.Second) - return nil + DB.AutoMigrate(&User{}) } func Execute(sql string) error { return DB.Exec(sql).Error diff --git a/models/users.go b/models/users.go index 9c3872c..6933901 100644 --- a/models/users.go +++ b/models/users.go @@ -16,6 +16,8 @@ type User struct { Role int32 `json:"role"` RoleName string `json:"role_name" 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 { @@ -29,6 +31,15 @@ func CreateUser(name string, password string, avator string, nickname string) ui DB.Create(user) 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) { user := &User{ Avator: avator, @@ -47,6 +58,13 @@ func UpdateUserPass(name string, pass string) { user.UpdatedAt = time.Now() 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) { user := &User{ Avator: avator, @@ -70,7 +88,7 @@ func FindIdleUser() User { defer assignMutex.Unlock() 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 { return User{} @@ -104,9 +122,13 @@ func FindUserById(id interface{}) User { func DeleteUserById(id string) { DB.Where("id = ?", id).Delete(User{}) } -func FindUsers() []User { +func FindUsers(withoutUsername string) []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 } func FindUserRole(query interface{}, id interface{}) User { diff --git a/models/visitors.go b/models/visitors.go index 896a452..600c2d9 100644 --- a/models/visitors.go +++ b/models/visitors.go @@ -39,6 +39,11 @@ func FindVisitorByVistorId(visitorId string) Visitor { DB.Where("visitor_id = ?", visitorId).First(&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 { offset := (page - 1) * pagesize if offset < 0 { @@ -86,21 +91,21 @@ func UpdateVisitorKefu(visitorId string, kefuId string) { DB.Model(&visitor).Where("visitor_id = ?", visitorId).Update("to_id", kefuId) } -//查询条数 +// 查询条数 func CountVisitors() uint { var count uint DB.Model(&Visitor{}).Count(&count) return count } -//查询条数 +// 查询条数 func CountVisitorsByKefuId(kefuId string) uint { var count uint DB.Model(&Visitor{}).Where("to_id=?", kefuId).Count(&count) return count } -//查询每天条数 +// 查询每天条数 type EveryDayNum struct { Day string `json:"day"` Num int64 `json:"num"` diff --git a/output/bin/aicss_service b/output/bin/aicss_service index e1e15f8..b586f60 100755 Binary files a/output/bin/aicss_service and b/output/bin/aicss_service differ diff --git a/router/api.go b/router/api.go index 0d7232b..bfe20d5 100644 --- a/router/api.go +++ b/router/api.go @@ -25,7 +25,7 @@ func InitApiRouter(engine *gin.RouterGroup) { engine.POST("/check", controller.LoginCheckPass) 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.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.DELETE("/kefuinfo", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.DeleteKefuInfo) 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("/trans_kefu", middleware.JwtApiMiddleware, controller.PostTransKefu) engine.POST("/modifypass", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.PostKefuPass) diff --git a/static/templates/chat_page.html b/static/templates/chat_page.html index f0b77b7..2558bd8 100644 --- a/static/templates/chat_page.html +++ b/static/templates/chat_page.html @@ -726,6 +726,16 @@ success: function(data) { if(data.code==200 && data.msg=="ok"){ 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{ KEFU_ID="default"; } diff --git a/static/templates/login.html b/static/templates/login.html index 463318c..cbaa4fe 100644 --- a/static/templates/login.html +++ b/static/templates/login.html @@ -121,6 +121,8 @@ ] }, showRegHtml: false, + kefuInfo:{}, + kefuList: [], }, methods: { validatePasswordMatch(rule, value, callback) { @@ -190,31 +192,45 @@ "nickname": this.form.nickname, }; - $.post("/aicss/register", data, (response) => { - if (response.code === 200) { + $.ajax({ + 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({ - message: 'Account created successfully!', - type: 'success' - }); - this.showRegHtml = false; - } else { - this.$message({ - message: response.msg || 'Registration failed', + message: 'Connection error', type: 'error' }); } - }).fail(() => { - this.$message({ - message: 'Connection error', - type: 'error' - }); }); + } }, created: function() { if (top.location != location) { top.location.href = location.href; } + + this.getKefuInfo(); } }); diff --git a/static/templates/setting.html b/static/templates/setting.html index 0dc247f..6a3241b 100644 --- a/static/templates/setting.html +++ b/static/templates/setting.html @@ -115,6 +115,43 @@ > + + + + 添加客服 + + + + + + + + + + + + 在线 + 离线 + + + + + + + + + + + 重置密码 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +