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 @@ > + +
+
+ 添加客服 +
+ + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/static/templates/setting_bottom.html b/static/templates/setting_bottom.html index 4e5abe7..e229abc 100644 --- a/static/templates/setting_bottom.html +++ b/static/templates/setting_bottom.html @@ -25,6 +25,18 @@ role_name:"", role_id:"", }, + kefuList: [], + resetPassDialog: false, + createKefuDialog: false, + resetPassForm: { + username: "", + password: "" + }, + createKefuForm: { + username: "", + nickname: "", + password: "" + }, avatarUrl:"", chatEndpoint: "", @@ -303,10 +315,73 @@ A: 因二次结算导致,系统将按最终结果扣回或补发。`} if(data.code==200 && data.result!=null){ _this.kefuInfo=data.result; _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(){ @@ -325,4 +400,5 @@ A: 因二次结算导致,系统将按最终结果扣回或补发。`} -{{end}} \ No newline at end of file + +{{end}} diff --git a/tmpl/login.go b/tmpl/login.go index 41693b9..cddd04e 100644 --- a/tmpl/login.go +++ b/tmpl/login.go @@ -1,16 +1,13 @@ package tmpl import ( - "ai-css/tools" - "github.com/gin-gonic/gin" "net/http" + + "github.com/gin-gonic/gin" ) // 登陆界面 func PageLogin(c *gin.Context) { - if noExist, _ := tools.IsFileNotExist("./install.lock"); noExist { - c.Redirect(302, "/install") - } c.HTML(http.StatusOK, "login.html", nil) } diff --git a/ws/user.go b/ws/user.go index fd59782..a349896 100644 --- a/ws/user.go +++ b/ws/user.go @@ -35,6 +35,7 @@ func NewKefuServer(c *gin.Context) { kefu.Avator = kefuInfo.Avator kefu.Conn = conn AddKefuToList(&kefu) + models.UpdateUserIsOnline(kefuInfo.Name, 1) for { //接受消息 @@ -43,6 +44,7 @@ func NewKefuServer(c *gin.Context) { if err != nil { log.Println("ws/user.go ", err) conn.Close() + models.UpdateUserIsOnline(kefuInfo.Name, 0) return } @@ -114,6 +116,7 @@ func SendPingToKefuClient() { if err != nil { log.Println("定时发送ping给客服,失败", err.Error()) delete(KefuList, kefuId) + models.UpdateUserIsOnline(kefuId, 0) } } } diff --git a/ws/ws.go b/ws/ws.go index 3e5fa3a..1c931bc 100644 --- a/ws/ws.go +++ b/ws/ws.go @@ -64,7 +64,7 @@ var message = make(chan *Message, 10) var upgrader = websocket.Upgrader{} var Mux sync.RWMutex -func init() { +func StartUpdateVisitorStatusCron() { upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, @@ -75,6 +75,7 @@ func init() { } go UpdateVisitorStatusCron() } + func SendServerJiang(title string, content string, domain string) string { noticeServerJiang, err := strconv.ParseBool(models.FindConfig("NoticeServerJiang")) serverJiangAPI := models.FindConfig("ServerJiangAPI")