├── .gitignore ├── Dockerfile ├── LICENSE ├── cmd ├── install.go ├── root.go ├── server.go └── stop.go ├── common ├── common.go └── config.go ├── config ├── city.free.ipdb ├── go-fly.sql └── mysql.json ├── controller ├── about.go ├── auth.go ├── captcha.go ├── chart.go ├── index.go ├── ip.go ├── kefu.go ├── login.go ├── main.go ├── message.go ├── notice.go ├── reply.go ├── response.go ├── role.go ├── setting.go ├── shout.go ├── tcp.go ├── visitor.go └── weixin.go ├── go-fly.go ├── go.mod ├── go.sum ├── import.sql ├── middleware ├── cross.go ├── domain_limit.go ├── ipblack.go ├── jwt.go ├── logger.go └── rbac.go ├── models ├── abouts.go ├── configs.go ├── ipblacks.go ├── messages.go ├── models.go ├── replys.go ├── roles.go ├── user_client.go ├── user_roles.go ├── users.go ├── visitors.go └── welcomes.go ├── readme.md ├── readme_en.md ├── router ├── api.go └── view.go ├── start.bat ├── static ├── cdn │ ├── element-ui │ │ ├── 2.15.1 │ │ │ ├── index.js │ │ │ └── theme-chalk │ │ │ │ ├── fonts │ │ │ │ └── element-icons.woff │ │ │ │ └── index.min.css │ │ └── 2.15.7 │ │ │ ├── index.js │ │ │ └── theme-chalk │ │ │ ├── fonts │ │ │ └── element-icons.woff │ │ │ └── index.min.css │ ├── jquery │ │ ├── 3.6.0 │ │ │ └── jquery.min.js │ │ └── jquery.qrcode.min.js │ └── vue │ │ └── 2.6.11 │ │ └── vue.min.js ├── css │ ├── bootstrap.min.css │ ├── common.css │ ├── front.css │ ├── gofly-front.css │ ├── icon │ │ ├── iconfont.css │ │ ├── iconfont.js │ │ ├── iconfont.json │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ └── iconfont.woff2 │ ├── icono.min.css │ ├── index.css │ ├── kefu-front.css │ ├── layui │ │ ├── css │ │ │ ├── layui.css │ │ │ └── modules │ │ │ │ ├── code.css │ │ │ │ ├── laydate │ │ │ │ └── default │ │ │ │ │ └── laydate.css │ │ │ │ └── layer │ │ │ │ └── default │ │ │ │ ├── icon-ext.png │ │ │ │ ├── icon.png │ │ │ │ ├── layer.css │ │ │ │ ├── loading-0.gif │ │ │ │ ├── loading-1.gif │ │ │ │ └── loading-2.gif │ │ ├── font │ │ │ ├── iconfont.eot │ │ │ ├── iconfont.svg │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ └── iconfont.woff2 │ │ └── layui.js │ └── style.css ├── demos │ └── websocket.html ├── images │ ├── 0.jpg │ ├── 1.jpg │ ├── 1.png │ ├── 10.jpg │ ├── 11.jpg │ ├── 12.jpg │ ├── 13.jpg │ ├── 14.jpg │ ├── 2.jpg │ ├── 2.png │ ├── 3.jpg │ ├── 3.png │ ├── 4.jpg │ ├── 5.jpg │ ├── 6.jpg │ ├── 7.jpg │ ├── 8.jpg │ ├── 9.jpg │ ├── admin.jpg │ ├── admin.png │ ├── alert.mp3 │ ├── alert2.ogg │ ├── attachent.png │ ├── avator.jpg │ ├── computer.png │ ├── ext │ │ ├── 7z.png │ │ ├── BAT.png │ │ ├── BMP.png │ │ ├── DOC.png │ │ ├── DOCX.png │ │ ├── JPEG.png │ │ ├── JPG.png │ │ ├── MP3.png │ │ ├── MP4.png │ │ ├── MPGE.png │ │ ├── PDF.png │ │ ├── PNG.png │ │ ├── PPT.png │ │ ├── RAR.png │ │ ├── SVG.png │ │ ├── TAR.png │ │ ├── TXT.png │ │ ├── XLSX.png │ │ ├── ZIP.png │ │ ├── default.png │ │ └── voice.png │ ├── face │ │ ├── 0.gif │ │ ├── 1.gif │ │ ├── 10.gif │ │ ├── 11.gif │ │ ├── 12.gif │ │ ├── 13.gif │ │ ├── 14.gif │ │ ├── 15.gif │ │ ├── 16.gif │ │ ├── 17.gif │ │ ├── 18.gif │ │ ├── 19.gif │ │ ├── 2.gif │ │ ├── 20.gif │ │ ├── 21.gif │ │ ├── 22.gif │ │ ├── 23.gif │ │ ├── 24.gif │ │ ├── 25.gif │ │ ├── 26.gif │ │ ├── 27.gif │ │ ├── 28.gif │ │ ├── 29.gif │ │ ├── 3.gif │ │ ├── 30.gif │ │ ├── 31.gif │ │ ├── 32.gif │ │ ├── 33.gif │ │ ├── 34.gif │ │ ├── 35.gif │ │ ├── 36.gif │ │ ├── 37.gif │ │ ├── 38.gif │ │ ├── 39.gif │ │ ├── 4.gif │ │ ├── 40.gif │ │ ├── 41.gif │ │ ├── 42.gif │ │ ├── 43.gif │ │ ├── 44.gif │ │ ├── 45.gif │ │ ├── 46.gif │ │ ├── 47.gif │ │ ├── 48.gif │ │ ├── 49.gif │ │ ├── 5.gif │ │ ├── 50.gif │ │ ├── 51.gif │ │ ├── 52.gif │ │ ├── 53.gif │ │ ├── 54.gif │ │ ├── 55.gif │ │ ├── 56.gif │ │ ├── 57.gif │ │ ├── 58.gif │ │ ├── 59.gif │ │ ├── 6.gif │ │ ├── 60.gif │ │ ├── 61.gif │ │ ├── 62.gif │ │ ├── 63.gif │ │ ├── 64.gif │ │ ├── 65.gif │ │ ├── 66.gif │ │ ├── 67.gif │ │ ├── 68.gif │ │ ├── 69.gif │ │ ├── 7.gif │ │ ├── 70.gif │ │ ├── 71.gif │ │ ├── 8.gif │ │ └── 9.gif │ ├── fire.svg │ ├── image-text.png │ ├── intro1.jpg │ ├── intro2.jpg │ ├── intro3.png │ ├── inviteColorBack1.png │ ├── logo.png │ ├── newintro1.jpg │ ├── newintro2.jpg │ ├── newintro3.jpg │ ├── phone.png │ ├── sent.ogg │ ├── smile.png │ ├── visitor_title_bg.png │ ├── wechatLogo.png │ └── zoom_out.png ├── js │ ├── chat-lang.js │ ├── chat-main.js │ ├── chat-page.js │ ├── echarts.min.js │ ├── functions.js │ ├── gofly-front.js │ ├── index.js │ ├── kefu-front.js │ ├── layer │ │ ├── layer.js │ │ ├── mobile │ │ │ ├── layer.js │ │ │ └── need │ │ │ │ └── layer.css │ │ └── theme │ │ │ └── default │ │ │ ├── icon-ext.png │ │ │ ├── icon.png │ │ │ ├── layer.css │ │ │ ├── loading-0.gif │ │ │ ├── loading-1.gif │ │ │ └── loading-2.gif │ └── reconnecting-websocket.min.js ├── static.go └── templates │ ├── bind.html │ ├── chat_main.html │ ├── chat_page.html │ ├── chat_video.html │ ├── detail.html │ ├── header.html │ ├── index.html │ ├── index_demo.html │ ├── install.html │ ├── list.html │ ├── login.html │ ├── mail_detail.html │ ├── mail_left.html │ ├── main.html │ ├── nav.html │ ├── pannel.html │ ├── setting.html │ ├── setting_avator.html │ ├── setting_bottom.html │ ├── setting_config.html │ ├── setting_deploy.html │ ├── setting_indexpages.html │ ├── setting_ipblack.html │ ├── setting_kefu_list.html │ ├── setting_left.html │ ├── setting_modifypass.html │ ├── setting_mysql.html │ ├── setting_pageindex.html │ ├── setting_role_list.html │ ├── setting_statistics.html │ ├── setting_welcome.html │ └── write.html ├── stop.bat ├── stop.sh ├── tmpl ├── chat.go ├── common.go ├── detail.go ├── folder.go ├── login.go ├── mail.go └── setting.go ├── tools ├── binsearch.go ├── binsearch_test.go ├── cookie.go ├── file.go ├── hash.go ├── http.go ├── http_tool.go ├── import_sql.go ├── import_sql_test.go ├── ip.go ├── ip_test.go ├── jwt.go ├── limits.go ├── logger.go ├── mytest.go ├── mytest_test.go ├── paniclog_linux.go ├── paniclog_windows.go ├── reverse_test.go ├── rpc_test.go ├── session.go ├── singlelist.go ├── smtp.go ├── smtp_test.go ├── snowflake.go ├── sorts.go ├── sorts_test.go ├── stringutil.go ├── stringutil_test.go ├── test.go ├── test_test.go ├── types.go └── uuid.go └── ws ├── user.go ├── visitor.go └── ws.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | imaptool.exe -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine 2 | WORKDIR /app 3 | COPY . /app 4 | RUN go env -w GO111MODULE=on && go env -w GOPROXY=https://goproxy.cn,direct 5 | VOLUME ["/app/config"] 6 | RUN go build go-fly.go 7 | EXPOSE 8081 8 | CMD ["/app/go-fly","server"] -------------------------------------------------------------------------------- /cmd/install.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/taoshihan1991/imaptool/common" 6 | "github.com/taoshihan1991/imaptool/models" 7 | "github.com/taoshihan1991/imaptool/tools" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "strings" 12 | ) 13 | 14 | var installCmd = &cobra.Command{ 15 | Use: "install", 16 | Short: "安装导入数据", 17 | Run: func(cmd *cobra.Command, args []string) { 18 | install() 19 | }, 20 | } 21 | 22 | func install() { 23 | if ok, _ := tools.IsFileNotExist("./install.lock"); !ok { 24 | log.Println("请先删除./install.lock") 25 | os.Exit(1) 26 | } 27 | sqlFile := "import.sql" 28 | isExit, _ := tools.IsFileExist(common.MysqlConf) 29 | dataExit, _ := tools.IsFileExist(sqlFile) 30 | if !isExit || !dataExit { 31 | log.Println("config/mysql.json 数据库配置文件或者数据库文件go-fly.sql不存在") 32 | os.Exit(1) 33 | } 34 | sqls, _ := ioutil.ReadFile(sqlFile) 35 | sqlArr := strings.Split(string(sqls), ";") 36 | for _, sql := range sqlArr { 37 | sql = strings.TrimSpace(sql) 38 | if sql == "" { 39 | continue 40 | } 41 | err := models.Execute(sql) 42 | if err == nil { 43 | log.Println(sql, "\t success!") 44 | } else { 45 | log.Println(sql, err, "\t failed!", "数据库导入失败") 46 | os.Exit(1) 47 | } 48 | } 49 | installFile, _ := os.OpenFile("./install.lock", os.O_RDWR|os.O_CREATE, os.ModePerm) 50 | installFile.WriteString("gofly live chat") 51 | } 52 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/spf13/cobra" 7 | "os" 8 | ) 9 | 10 | var rootCmd = &cobra.Command{ 11 | Use: "go-fly", 12 | Short: "go-fly", 13 | Long: `简洁快速的GO语言WEB在线客服 https://gofly.sopans.com`, 14 | Args: args, 15 | Run: func(cmd *cobra.Command, args []string) { 16 | 17 | }, 18 | } 19 | 20 | func args(cmd *cobra.Command, args []string) error { 21 | if len(args) < 1 { 22 | 23 | return errors.New("至少需要一个参数!") 24 | } 25 | return nil 26 | } 27 | func Execute() { 28 | if err := rootCmd.Execute(); err != nil { 29 | fmt.Println(err) 30 | os.Exit(1) 31 | } 32 | } 33 | func init() { 34 | rootCmd.AddCommand(serverCmd) 35 | rootCmd.AddCommand(installCmd) 36 | rootCmd.AddCommand(stopCmd) 37 | } 38 | -------------------------------------------------------------------------------- /cmd/server.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-contrib/pprof" 6 | "github.com/gin-gonic/gin" 7 | "github.com/spf13/cobra" 8 | "github.com/taoshihan1991/imaptool/common" 9 | "github.com/taoshihan1991/imaptool/middleware" 10 | "github.com/taoshihan1991/imaptool/router" 11 | "github.com/taoshihan1991/imaptool/static" 12 | "github.com/taoshihan1991/imaptool/tools" 13 | "github.com/taoshihan1991/imaptool/ws" 14 | "github.com/zh-five/xdaemon" 15 | "html/template" 16 | "io/ioutil" 17 | "log" 18 | "net/http" 19 | "os" 20 | ) 21 | 22 | var ( 23 | port string 24 | daemon bool 25 | ) 26 | var serverCmd = &cobra.Command{ 27 | Use: "server", 28 | Short: "启动http服务", 29 | Example: "go-fly server -c config/", 30 | Run: func(cmd *cobra.Command, args []string) { 31 | run() 32 | }, 33 | } 34 | 35 | func init() { 36 | serverCmd.PersistentFlags().StringVarP(&port, "port", "p", "8081", "监听端口号") 37 | serverCmd.PersistentFlags().BoolVarP(&daemon, "daemon", "d", false, "是否为守护进程模式") 38 | } 39 | func run() { 40 | if daemon == true { 41 | logFilePath := "" 42 | if dir, err := os.Getwd(); err == nil { 43 | logFilePath = dir + "/logs/" 44 | } 45 | _, err := os.Stat(logFilePath) 46 | if os.IsNotExist(err) { 47 | if err := os.MkdirAll(logFilePath, 0777); err != nil { 48 | log.Println(err.Error()) 49 | } 50 | } 51 | d := xdaemon.NewDaemon(logFilePath + "go-fly.log") 52 | d.MaxCount = 10 53 | d.Run() 54 | } 55 | 56 | baseServer := "0.0.0.0:" + port 57 | log.Println("start server...\r\ngo:http://" + baseServer) 58 | tools.Logger().Println("start server...\r\ngo:http://" + baseServer) 59 | 60 | engine := gin.Default() 61 | if common.IsCompireTemplate { 62 | templ := template.Must(template.New("").ParseFS(static.TemplatesEmbed, "templates/*.html")) 63 | engine.SetHTMLTemplate(templ) 64 | engine.StaticFS("/assets", http.FS(static.JsEmbed)) 65 | } else { 66 | engine.LoadHTMLGlob("static/templates/*") 67 | engine.Static("/assets", "./static") 68 | } 69 | 70 | engine.Static("/static", "./static") 71 | engine.Use(tools.Session("gofly")) 72 | engine.Use(middleware.CrossSite) 73 | //性能监控 74 | pprof.Register(engine) 75 | 76 | //记录日志 77 | engine.Use(middleware.NewMidLogger()) 78 | router.InitViewRouter(engine) 79 | router.InitApiRouter(engine) 80 | //记录pid 81 | ioutil.WriteFile("gofly.sock", []byte(fmt.Sprintf("%d,%d", os.Getppid(), os.Getpid())), 0666) 82 | //限流类 83 | tools.NewLimitQueue() 84 | //清理 85 | ws.CleanVisitorExpire() 86 | //后端websocket 87 | go ws.WsServerBackend() 88 | 89 | engine.Run(baseServer) 90 | } 91 | -------------------------------------------------------------------------------- /cmd/stop.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "io/ioutil" 6 | "os/exec" 7 | "runtime" 8 | "strings" 9 | ) 10 | 11 | var stopCmd = &cobra.Command{ 12 | Use: "stop", 13 | Short: "停止http服务", 14 | Run: func(cmd *cobra.Command, args []string) { 15 | pids, err := ioutil.ReadFile("gofly.sock") 16 | if err != nil { 17 | return 18 | } 19 | pidSlice := strings.Split(string(pids), ",") 20 | var command *exec.Cmd 21 | for _, pid := range pidSlice { 22 | if runtime.GOOS == "windows" { 23 | command = exec.Command("taskkill.exe", "/f", "/pid", pid) 24 | } else { 25 | command = exec.Command("kill", pid) 26 | } 27 | command.Start() 28 | } 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | var ( 4 | PageSize uint = 10 5 | VisitorPageSize uint = 8 6 | Version string = "0.3.9" 7 | VisitorExpire float64 = 600 8 | Upload string = "static/upload/" 9 | Dir string = "config/" 10 | MysqlConf string = Dir + "mysql.json" 11 | IsCompireTemplate bool = false //是否编译静态模板到二进制 12 | ) 13 | -------------------------------------------------------------------------------- /common/config.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/taoshihan1991/imaptool/tools" 6 | "io/ioutil" 7 | ) 8 | 9 | type Mysql struct { 10 | Server string 11 | Port string 12 | Database string 13 | Username string 14 | Password string 15 | } 16 | 17 | func GetMysqlConf() *Mysql { 18 | var mysql = &Mysql{} 19 | isExist, _ := tools.IsFileExist(MysqlConf) 20 | if !isExist { 21 | return mysql 22 | } 23 | info, err := ioutil.ReadFile(MysqlConf) 24 | if err != nil { 25 | return mysql 26 | } 27 | err = json.Unmarshal(info, mysql) 28 | return mysql 29 | } 30 | -------------------------------------------------------------------------------- /config/city.free.ipdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoshihan1991/go-fly/9988222db083df397604281124311c87c2ff9967/config/city.free.ipdb -------------------------------------------------------------------------------- /config/mysql.json: -------------------------------------------------------------------------------- 1 | { 2 | "Server":"localhost", 3 | "Port":"3306", 4 | "Database":"go-fly", 5 | "Username":"go-fly", 6 | "Password":"go-fly" 7 | } 8 | -------------------------------------------------------------------------------- /controller/about.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/taoshihan1991/imaptool/models" 6 | ) 7 | 8 | func GetAbout(c *gin.Context) { 9 | page := c.Query("page") 10 | if page == "" { 11 | page = "index" 12 | } 13 | about := models.FindAboutByPage(page) 14 | c.JSON(200, gin.H{ 15 | "code": 200, 16 | "msg": "ok", 17 | "result": about, 18 | }) 19 | } 20 | func GetAbouts(c *gin.Context) { 21 | about := models.FindAbouts() 22 | c.JSON(200, gin.H{ 23 | "code": 200, 24 | "msg": "ok", 25 | "result": about, 26 | }) 27 | } 28 | func PostAbout(c *gin.Context) { 29 | title_cn := c.PostForm("title_cn") 30 | title_en := c.PostForm("title_en") 31 | keywords_cn := c.PostForm("keywords_cn") 32 | keywords_en := c.PostForm("keywords_en") 33 | desc_cn := c.PostForm("desc_cn") 34 | desc_en := c.PostForm("desc_en") 35 | css_js := c.PostForm("css_js") 36 | html_cn := c.PostForm("html_cn") 37 | html_en := c.PostForm("html_en") 38 | if title_cn == "" || title_en == "" || html_cn == "" || html_en == "" { 39 | c.JSON(200, gin.H{ 40 | "code": 400, 41 | "msg": "error", 42 | }) 43 | return 44 | } 45 | models.UpdateAbout("index", title_cn, title_en, keywords_cn, keywords_en, desc_cn, desc_en, css_js, html_cn, html_en) 46 | 47 | c.JSON(200, gin.H{ 48 | "code": 200, 49 | "msg": "ok", 50 | "result": "", 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /controller/auth.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/taoshihan1991/imaptool/models" 5 | "github.com/taoshihan1991/imaptool/tools" 6 | ) 7 | 8 | func CheckKefuPass(username string, password string) (models.User, models.User_role, bool) { 9 | info := models.FindUser(username) 10 | var uRole models.User_role 11 | if info.Name == "" || info.Password != tools.Md5(password) { 12 | return info, uRole, false 13 | } 14 | uRole = models.FindRoleByUserId(info.ID) 15 | 16 | return info, uRole, true 17 | } 18 | -------------------------------------------------------------------------------- /controller/captcha.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "bytes" 5 | "github.com/dchest/captcha" 6 | "github.com/gin-contrib/sessions" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | func GetCaptcha(c *gin.Context) { 13 | l := captcha.DefaultLen 14 | w, h := 107, 36 15 | captchaId := captcha.NewLen(l) 16 | session := sessions.Default(c) 17 | session.Set("captcha", captchaId) 18 | _ = session.Save() 19 | _ = Serve(c.Writer, c.Request, captchaId, ".png", "zh", false, w, h) 20 | } 21 | 22 | func Serve(w http.ResponseWriter, r *http.Request, id, ext, lang string, download bool, width, height int) error { 23 | w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") 24 | w.Header().Set("Pragma", "no-cache") 25 | w.Header().Set("Expires", "0") 26 | 27 | var content bytes.Buffer 28 | switch ext { 29 | case ".png": 30 | w.Header().Set("Content-Type", "image/png") 31 | _ = captcha.WriteImage(&content, id, width, height) 32 | case ".wav": 33 | w.Header().Set("Content-Type", "audio/x-wav") 34 | _ = captcha.WriteAudio(&content, id, lang) 35 | default: 36 | return captcha.ErrNotFound 37 | } 38 | 39 | if download { 40 | w.Header().Set("Content-Type", "application/octet-stream") 41 | } 42 | http.ServeContent(w, r, id+ext, time.Time{}, bytes.NewReader(content.Bytes())) 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /controller/chart.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/taoshihan1991/imaptool/models" 6 | "github.com/taoshihan1991/imaptool/tools" 7 | "time" 8 | ) 9 | 10 | func GetChartStatistic(c *gin.Context) { 11 | kefuName, _ := c.Get("kefu_name") 12 | 13 | dayNumMap := make(map[string]string) 14 | result := models.CountVisitorsEveryDay(kefuName.(string)) 15 | for _, item := range result { 16 | dayNumMap[item.Day] = tools.Int2Str(item.Num) 17 | } 18 | 19 | nowTime := time.Now() 20 | list := make([]map[string]string, 0) 21 | for i := 0; i > -46; i-- { 22 | getTime := nowTime.AddDate(0, 0, i) //年,月,日 获取一天前的时间 23 | resTime := getTime.Format("06-01-02") //获取的时间的格式 24 | tmp := make(map[string]string) 25 | tmp["day"] = resTime 26 | tmp["num"] = dayNumMap[resTime] 27 | list = append(list, tmp) 28 | } 29 | 30 | c.JSON(200, gin.H{ 31 | "code": 200, 32 | "msg": "ok", 33 | "result": list, 34 | }) 35 | 36 | } 37 | -------------------------------------------------------------------------------- /controller/index.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/taoshihan1991/imaptool/models" 6 | ) 7 | 8 | func Index(c *gin.Context) { 9 | jump := models.FindConfig("JumpLang") 10 | if jump != "cn" { 11 | jump = "en" 12 | } 13 | c.Redirect(302, "/index_"+jump) 14 | } 15 | -------------------------------------------------------------------------------- /controller/ip.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/taoshihan1991/imaptool/common" 6 | "github.com/taoshihan1991/imaptool/models" 7 | "strconv" 8 | ) 9 | 10 | func PostIpblack(c *gin.Context) { 11 | ip := c.PostForm("ip") 12 | if ip == "" { 13 | c.JSON(200, gin.H{ 14 | "code": 400, 15 | "msg": "请输入IP!", 16 | }) 17 | return 18 | } 19 | kefuId, _ := c.Get("kefu_name") 20 | models.CreateIpblack(ip, kefuId.(string)) 21 | c.JSON(200, gin.H{ 22 | "code": 200, 23 | "msg": "添加黑名单成功!", 24 | }) 25 | } 26 | func DelIpblack(c *gin.Context) { 27 | ip := c.Query("ip") 28 | if ip == "" { 29 | c.JSON(200, gin.H{ 30 | "code": 400, 31 | "msg": "请输入IP!", 32 | }) 33 | return 34 | } 35 | models.DeleteIpblackByIp(ip) 36 | c.JSON(200, gin.H{ 37 | "code": 200, 38 | "msg": "删除黑名单成功!", 39 | }) 40 | } 41 | func GetIpblacks(c *gin.Context) { 42 | page, _ := strconv.Atoi(c.Query("page")) 43 | if page == 0 { 44 | page = 1 45 | } 46 | count := models.CountIps(nil, nil) 47 | list := models.FindIps(nil, nil, uint(page), common.VisitorPageSize) 48 | c.JSON(200, gin.H{ 49 | "code": 200, 50 | "msg": "ok", 51 | "result": gin.H{ 52 | "list": list, 53 | "count": count, 54 | "pagesize": common.PageSize, 55 | }, 56 | }) 57 | } 58 | func GetIpblacksByKefuId(c *gin.Context) { 59 | kefuId, _ := c.Get("kefu_name") 60 | list := models.FindIpsByKefuId(kefuId.(string)) 61 | c.JSON(200, gin.H{ 62 | "code": 200, 63 | "msg": "ok", 64 | "result": list, 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /controller/login.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/taoshihan1991/imaptool/tools" 6 | "log" 7 | "net/url" 8 | "time" 9 | ) 10 | 11 | // @Summary 登陆验证接口 12 | // @Produce json 13 | // @Accept multipart/form-data 14 | // @Param username formData string true "用户名" 15 | // @Param password formData string true "密码" 16 | // @Param type formData string true "类型" 17 | // @Success 200 {object} controller.Response 18 | // @Failure 200 {object} controller.Response 19 | // @Router /check [post] 20 | //验证接口 21 | func LoginCheckPass(c *gin.Context) { 22 | password := c.PostForm("password") 23 | username := c.PostForm("username") 24 | 25 | info, uRole, ok := CheckKefuPass(username, password) 26 | userinfo := make(map[string]interface{}) 27 | if !ok { 28 | c.JSON(200, gin.H{ 29 | "code": 400, 30 | "msg": "验证失败", 31 | }) 32 | return 33 | } 34 | 35 | userinfo["name"] = info.Name 36 | userinfo["kefu_id"] = info.ID 37 | userinfo["type"] = "kefu" 38 | if uRole.RoleId != 0 { 39 | userinfo["role_id"] = uRole.RoleId 40 | } else { 41 | userinfo["role_id"] = 2 42 | } 43 | userinfo["create_time"] = time.Now().Unix() 44 | 45 | token, _ := tools.MakeToken(userinfo) 46 | userinfo["ref_token"] = true 47 | refToken, _ := tools.MakeToken(userinfo) 48 | c.JSON(200, gin.H{ 49 | "code": 200, 50 | "msg": "验证成功,正在跳转", 51 | "result": gin.H{ 52 | "token": token, 53 | "ref_token": refToken, 54 | "create_time": userinfo["create_time"], 55 | }, 56 | }) 57 | } 58 | //远程请求 59 | func PostBindOfficial(c *gin.Context) { 60 | api := "https://gofly.v1kf.com/2/officialBindIp" 61 | 62 | phone := c.PostForm("phone") 63 | password := c.PostForm("password") 64 | host := c.Request.Host 65 | data := url.Values{} 66 | data.Set("phone", phone) 67 | data.Set("password", password) 68 | data.Set("host", host) 69 | res, err := tools.PostForm(api, data) 70 | if err != nil { 71 | log.Println("绑定官网账户发送认证连接错误") 72 | } 73 | c.Writer.Write([]byte(res)) 74 | } -------------------------------------------------------------------------------- /controller/main.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "github.com/jinzhu/gorm" 8 | "github.com/taoshihan1991/imaptool/common" 9 | "github.com/taoshihan1991/imaptool/models" 10 | "github.com/taoshihan1991/imaptool/tools" 11 | "github.com/taoshihan1991/imaptool/ws" 12 | "io/ioutil" 13 | "log" 14 | "os" 15 | "strings" 16 | ) 17 | 18 | func PostInstall(c *gin.Context) { 19 | notExist, _ := tools.IsFileNotExist("./install.lock") 20 | if !notExist { 21 | c.JSON(200, gin.H{ 22 | "code": 400, 23 | "msg": "系统已经安装过了", 24 | }) 25 | return 26 | } 27 | server := c.PostForm("server") 28 | port := c.PostForm("port") 29 | database := c.PostForm("database") 30 | username := c.PostForm("username") 31 | password := c.PostForm("password") 32 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", username, password, server, port, database) 33 | _, err := gorm.Open("mysql", dsn) 34 | if err != nil { 35 | log.Println(err) 36 | tools.Logger().Println(err) 37 | c.JSON(200, gin.H{ 38 | "code": 400, 39 | "msg": "数据库连接失败:" + err.Error(), 40 | }) 41 | return 42 | } 43 | isExist, _ := tools.IsFileExist(common.Dir) 44 | if !isExist { 45 | os.Mkdir(common.Dir, os.ModePerm) 46 | } 47 | fileConfig := common.MysqlConf 48 | file, _ := os.OpenFile(fileConfig, os.O_RDWR|os.O_CREATE, os.ModePerm) 49 | 50 | format := `{ 51 | "Server":"%s", 52 | "Port":"%s", 53 | "Database":"%s", 54 | "Username":"%s", 55 | "Password":"%s" 56 | } 57 | ` 58 | data := fmt.Sprintf(format, server, port, database, username, password) 59 | file.WriteString(data) 60 | models.Connect() 61 | installFile, _ := os.OpenFile("./install.lock", os.O_RDWR|os.O_CREATE, os.ModePerm) 62 | installFile.WriteString("gofly live chat") 63 | ok, err := install() 64 | if !ok { 65 | c.JSON(200, gin.H{ 66 | "code": 200, 67 | "msg": err.Error(), 68 | }) 69 | return 70 | } 71 | c.JSON(200, gin.H{ 72 | "code": 200, 73 | "msg": "安装成功", 74 | }) 75 | } 76 | func install() (bool, error) { 77 | sqlFile := common.Dir + "go-fly.sql" 78 | isExit, _ := tools.IsFileExist(common.MysqlConf) 79 | dataExit, _ := tools.IsFileExist(sqlFile) 80 | if !isExit || !dataExit { 81 | return false, errors.New("config/mysql.json 数据库配置文件或者数据库文件go-fly.sql不存在") 82 | } 83 | sqls, _ := ioutil.ReadFile(sqlFile) 84 | sqlArr := strings.Split(string(sqls), "|") 85 | for _, sql := range sqlArr { 86 | if sql == "" { 87 | continue 88 | } 89 | err := models.Execute(sql) 90 | if err == nil { 91 | log.Println(sql, "\t success!") 92 | } else { 93 | log.Println(sql, err, "\t failed!") 94 | } 95 | } 96 | return true, nil 97 | } 98 | func MainCheckAuth(c *gin.Context) { 99 | id, _ := c.Get("kefu_id") 100 | userinfo := models.FindUserRole("user.avator,user.name,user.id, role.name role_name", id) 101 | c.JSON(200, gin.H{ 102 | "code": 200, 103 | "msg": "验证成功", 104 | "result": gin.H{ 105 | "avator": userinfo.Avator, 106 | "name": userinfo.Name, 107 | "role_name": userinfo.RoleName, 108 | }, 109 | }) 110 | } 111 | func GetStatistics(c *gin.Context) { 112 | visitors := models.CountVisitors() 113 | message := models.CountMessage(nil,nil) 114 | session := len(ws.ClientList) 115 | kefuNum := 0 116 | c.JSON(200, gin.H{ 117 | "code": 200, 118 | "msg": "ok", 119 | "result": gin.H{ 120 | "visitors": visitors, 121 | "message": message, 122 | "session": session + kefuNum, 123 | }, 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /controller/notice.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/taoshihan1991/imaptool/models" 6 | ) 7 | 8 | func GetNotice(c *gin.Context) { 9 | kefuId := c.Query("kefu_id") 10 | user := models.FindUser(kefuId) 11 | if user.ID==0{ 12 | c.JSON(200, gin.H{ 13 | "code": 400, 14 | "msg": "user not found", 15 | }) 16 | return 17 | } 18 | welcomeMessage:=models.FindConfig("WelcomeMessage") 19 | offlineMessage:=models.FindConfig("OfflineMessage") 20 | allNotice:=models.FindConfig("AllNotice") 21 | c.JSON(200, gin.H{ 22 | "code": 200, 23 | "msg": "ok", 24 | "result": gin.H{ 25 | "welcome":welcomeMessage, 26 | "offline":offlineMessage, 27 | "avatar":user.Avator, 28 | "nickname":user.Nickname, 29 | "allNotice":allNotice, 30 | }, 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /controller/reply.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/taoshihan1991/imaptool/models" 6 | "log" 7 | ) 8 | 9 | type ReplyForm struct { 10 | GroupName string `form:"group_name" binding:"required"` 11 | } 12 | type ReplyContentForm struct { 13 | GroupId string `form:"group_id" binding:"required"` 14 | Content string `form:"content" binding:"required"` 15 | ItemName string `form:"item_name" binding:"required"` 16 | } 17 | 18 | func GetReplys(c *gin.Context) { 19 | kefuId, _ := c.Get("kefu_name") 20 | log.Println(kefuId) 21 | res := models.FindReplyByUserId(kefuId) 22 | c.JSON(200, gin.H{ 23 | "code": 200, 24 | "msg": "ok", 25 | "result": res, 26 | }) 27 | } 28 | func GetAutoReplys(c *gin.Context) { 29 | kefu_id := c.Query("kefu_id") 30 | res := models.FindReplyTitleByUserId(kefu_id) 31 | c.JSON(200, gin.H{ 32 | "code": 200, 33 | "msg": "ok", 34 | "result": res, 35 | }) 36 | } 37 | func PostReply(c *gin.Context) { 38 | var replyForm ReplyForm 39 | kefuId, _ := c.Get("kefu_name") 40 | err := c.Bind(&replyForm) 41 | if err != nil { 42 | c.JSON(200, gin.H{ 43 | "code": 400, 44 | "msg": "error:" + err.Error(), 45 | }) 46 | return 47 | } 48 | models.CreateReplyGroup(replyForm.GroupName, kefuId.(string)) 49 | c.JSON(200, gin.H{ 50 | "code": 200, 51 | "msg": "ok", 52 | }) 53 | } 54 | func PostReplyContent(c *gin.Context) { 55 | var replyContentForm ReplyContentForm 56 | kefuId, _ := c.Get("kefu_name") 57 | err := c.Bind(&replyContentForm) 58 | if err != nil { 59 | c.JSON(400, gin.H{ 60 | "code": 200, 61 | "msg": "error:" + err.Error(), 62 | }) 63 | return 64 | } 65 | models.CreateReplyContent(replyContentForm.GroupId, kefuId.(string), replyContentForm.Content, replyContentForm.ItemName) 66 | c.JSON(200, gin.H{ 67 | "code": 200, 68 | "msg": "ok", 69 | }) 70 | } 71 | func PostReplyContentSave(c *gin.Context) { 72 | kefuId, _ := c.Get("kefu_name") 73 | replyId := c.PostForm("reply_id") 74 | replyTitle := c.PostForm("reply_title") 75 | replyContent := c.PostForm("reply_content") 76 | if replyId == "" || replyTitle == "" || replyContent == "" { 77 | c.JSON(400, gin.H{ 78 | "code": 200, 79 | "msg": "参数错误!", 80 | }) 81 | return 82 | } 83 | models.UpdateReplyContent(replyId, kefuId.(string), replyTitle, replyContent) 84 | c.JSON(200, gin.H{ 85 | "code": 200, 86 | "msg": "ok", 87 | }) 88 | } 89 | func DelReplyContent(c *gin.Context) { 90 | kefuId, _ := c.Get("kefu_name") 91 | id := c.Query("id") 92 | models.DeleteReplyContent(id, kefuId.(string)) 93 | c.JSON(200, gin.H{ 94 | "code": 200, 95 | "msg": "ok", 96 | }) 97 | } 98 | func DelReplyGroup(c *gin.Context) { 99 | kefuId, _ := c.Get("kefu_name") 100 | id := c.Query("id") 101 | models.DeleteReplyGroup(id, kefuId.(string)) 102 | c.JSON(200, gin.H{ 103 | "code": 200, 104 | "msg": "ok", 105 | }) 106 | } 107 | func PostReplySearch(c *gin.Context) { 108 | kefuId, _ := c.Get("kefu_name") 109 | search := c.PostForm("search") 110 | if search == "" { 111 | c.JSON(200, gin.H{ 112 | "code": 400, 113 | "msg": "参数错误", 114 | }) 115 | return 116 | } 117 | res := models.FindReplyBySearcch(kefuId, search) 118 | c.JSON(200, gin.H{ 119 | "code": 200, 120 | "msg": "ok", 121 | "result": res, 122 | }) 123 | } 124 | -------------------------------------------------------------------------------- /controller/response.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | var ( 4 | Port string 5 | ) 6 | 7 | type Response struct { 8 | Code int `json:"code"` 9 | Msg string `json:"msg"` 10 | result interface{} `json:"result"` 11 | } 12 | type ChatMessage struct { 13 | Time string `json:"time"` 14 | Content string `json:"content"` 15 | MesType string `json:"mes_type"` 16 | Name string `json:"name"` 17 | Avator string `json:"avator"` 18 | } 19 | type VisitorOnline struct { 20 | Uid string `json:"uid"` 21 | Username string `json:"username"` 22 | Avator string `json:"avator"` 23 | LastMessage string `json:"last_message"` 24 | } 25 | type GetuiResponse struct { 26 | Code float64 `json:"code"` 27 | Msg string `json:"msg"` 28 | Data map[string]interface{} `json:"data"` 29 | } 30 | type VisitorExtra struct { 31 | VisitorName string `json:"visitorName"` 32 | VisitorAvatar string `json:"visitorAvatar"` 33 | } 34 | -------------------------------------------------------------------------------- /controller/role.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/taoshihan1991/imaptool/models" 6 | ) 7 | 8 | func GetRoleList(c *gin.Context) { 9 | roles := models.FindRoles() 10 | c.JSON(200, gin.H{ 11 | "code": 200, 12 | "msg": "获取成功", 13 | "result": roles, 14 | }) 15 | } 16 | func PostRole(c *gin.Context) { 17 | roleId := c.PostForm("id") 18 | method := c.PostForm("method") 19 | name := c.PostForm("name") 20 | path := c.PostForm("path") 21 | if roleId == "" || method == "" || name == "" || path == "" { 22 | c.JSON(200, gin.H{ 23 | "code": 400, 24 | "msg": "参数不能为空", 25 | }) 26 | return 27 | } 28 | models.SaveRole(roleId, name, method, path) 29 | c.JSON(200, gin.H{ 30 | "code": 200, 31 | "msg": "修改成功", 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /controller/setting.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/taoshihan1991/imaptool/models" 6 | ) 7 | 8 | func GetConfigs(c *gin.Context) { 9 | configs := models.FindConfigs() 10 | c.JSON(200, gin.H{ 11 | "code": 200, 12 | "msg": "ok", 13 | "result": configs, 14 | }) 15 | } 16 | func GetConfig(c *gin.Context) { 17 | key := c.Query("key") 18 | config := models.FindConfig(key) 19 | c.JSON(200, gin.H{ 20 | "code": 200, 21 | "msg": "ok", 22 | "result": config, 23 | }) 24 | } 25 | func PostConfig(c *gin.Context) { 26 | key := c.PostForm("key") 27 | value := c.PostForm("value") 28 | if key == "" || value == "" { 29 | c.JSON(200, gin.H{ 30 | "code": 400, 31 | "msg": "error", 32 | }) 33 | return 34 | } 35 | models.UpdateConfig(key, value) 36 | 37 | c.JSON(200, gin.H{ 38 | "code": 200, 39 | "msg": "ok", 40 | "result": "", 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /controller/tcp.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "log" 6 | "net" 7 | ) 8 | 9 | var clientTcpList = make(map[string]net.Conn) 10 | 11 | func NewTcpServer(tcpBaseServer string) { 12 | listener, err := net.Listen("tcp", tcpBaseServer) 13 | if err != nil { 14 | log.Println("Error listening", err.Error()) 15 | return //终止程序 16 | } 17 | // 监听并接受来自客户端的连接 18 | for { 19 | conn, err := listener.Accept() 20 | if err != nil { 21 | log.Println("Error accepting", err.Error()) 22 | return // 终止程序 23 | } 24 | var remoteIpAddress = conn.RemoteAddr() 25 | clientTcpList[remoteIpAddress.String()] = conn 26 | log.Println(remoteIpAddress, clientTcpList) 27 | //clientTcpList=append(clientTcpList,conn) 28 | } 29 | } 30 | func PushServerTcp(str []byte) { 31 | for ip, conn := range clientTcpList { 32 | line := append(str, []byte("\r\n")...) 33 | _, err := conn.Write(line) 34 | log.Println(ip, err) 35 | if err != nil { 36 | conn.Close() 37 | delete(clientTcpList, ip) 38 | //clientTcpList=append(clientTcpList[:index],clientTcpList[index+1:]...) 39 | } 40 | } 41 | } 42 | func DeleteOnlineTcp(c *gin.Context) { 43 | ip := c.Query("ip") 44 | for ipkey, conn := range clientTcpList { 45 | if ip == ipkey { 46 | conn.Close() 47 | delete(clientTcpList, ip) 48 | } 49 | if ip == "all" { 50 | conn.Close() 51 | delete(clientTcpList, ipkey) 52 | } 53 | } 54 | c.JSON(200, gin.H{ 55 | "code": 200, 56 | "msg": "ok", 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /controller/weixin.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "github.com/gin-gonic/gin" 7 | "github.com/taoshihan1991/imaptool/models" 8 | "log" 9 | "sort" 10 | ) 11 | 12 | func GetCheckWeixinSign(c *gin.Context) { 13 | token := models.FindConfig("WeixinToken") 14 | signature := c.Query("signature") 15 | timestamp := c.Query("timestamp") 16 | nonce := c.Query("nonce") 17 | echostr := c.Query("echostr") 18 | //将token、timestamp、nonce三个参数进行字典序排序 19 | var tempArray = []string{token, timestamp, nonce} 20 | sort.Strings(tempArray) 21 | //将三个参数字符串拼接成一个字符串进行sha1加密 22 | var sha1String string = "" 23 | for _, v := range tempArray { 24 | sha1String += v 25 | } 26 | h := sha1.New() 27 | h.Write([]byte(sha1String)) 28 | sha1String = hex.EncodeToString(h.Sum([]byte(""))) 29 | //获得加密后的字符串可与signature对比 30 | if sha1String == signature { 31 | c.Writer.Write([]byte(echostr)) 32 | } else { 33 | log.Println("微信API验证失败") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /go-fly.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/taoshihan1991/imaptool/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/taoshihan1991/imaptool 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f 7 | github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 8 | github.com/emersion/go-smtp v0.13.0 9 | github.com/gin-contrib/pprof v1.3.0 10 | github.com/gin-contrib/sessions v0.0.3 11 | github.com/gin-gonic/gin v1.7.7 12 | github.com/go-sql-driver/mysql v1.5.0 13 | github.com/gobuffalo/packr/v2 v2.5.1 14 | github.com/golang-jwt/jwt v3.2.2+incompatible 15 | github.com/gorilla/websocket v1.4.2 16 | github.com/ipipdotnet/ipdb-go v1.3.0 17 | github.com/jinzhu/gorm v1.9.14 18 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 19 | github.com/modern-go/reflect2 v1.0.1 // indirect 20 | github.com/satori/go.uuid v1.2.0 21 | github.com/sirupsen/logrus v1.4.2 22 | github.com/spf13/cobra v0.0.5 23 | github.com/zh-five/xdaemon v0.1.1 24 | ) 25 | -------------------------------------------------------------------------------- /middleware/cross.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func CrossSite(c *gin.Context) { 6 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") 7 | //服务器支持的所有跨域请求的方法 8 | c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE") 9 | //允许跨域设置可以返回其他子段,可以自定义字段 10 | c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session") 11 | // 允许浏览器(客户端)可以解析的头部 (重要) 12 | c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers") 13 | //允许客户端传递校验信息比如 cookie (重要) 14 | c.Header("Access-Control-Allow-Credentials", "true") 15 | c.Next() 16 | } 17 | -------------------------------------------------------------------------------- /middleware/domain_limit.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | /** 8 | 域名中间件 9 | */ 10 | func DomainLimitMiddleware(c *gin.Context) { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /middleware/ipblack.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/taoshihan1991/imaptool/models" 6 | ) 7 | 8 | func Ipblack(c *gin.Context) { 9 | ip := c.ClientIP() 10 | ipblack := models.FindIp(ip) 11 | if ipblack.IP != "" { 12 | c.JSON(200, gin.H{ 13 | "code": 400, 14 | "msg": "IP已被加入黑名单", 15 | }) 16 | c.Abort() 17 | return 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /middleware/jwt.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/taoshihan1991/imaptool/tools" 6 | "time" 7 | ) 8 | 9 | func JwtPageMiddleware(c *gin.Context) { 10 | //暂时不处理 11 | //token := c.Query("token") 12 | //userinfo := tools.ParseToken(token) 13 | //if userinfo == nil { 14 | // c.Redirect(302,"/login") 15 | // c.Abort() 16 | //} 17 | } 18 | func JwtApiMiddleware(c *gin.Context) { 19 | token := c.GetHeader("token") 20 | if token == "" { 21 | token = c.Query("token") 22 | } 23 | userinfo := tools.ParseToken(token) 24 | if userinfo == nil || userinfo["name"] == nil || userinfo["create_time"] == nil { 25 | c.JSON(200, gin.H{ 26 | "code": 400, 27 | "msg": "验证失败", 28 | }) 29 | c.Abort() 30 | return 31 | } 32 | createTime := int64(userinfo["create_time"].(float64)) 33 | var expire int64 = 24 * 60 * 60 34 | nowTime := time.Now().Unix() 35 | if (nowTime - createTime) >= expire { 36 | c.JSON(200, gin.H{ 37 | "code": 401, 38 | "msg": "token失效", 39 | }) 40 | c.Abort() 41 | } 42 | c.Set("user", userinfo["name"]) 43 | //log.Println(userinfo) 44 | //if userinfo["type"]=="kefu"{ 45 | c.Set("kefu_id", userinfo["kefu_id"]) 46 | c.Set("kefu_name", userinfo["name"]) 47 | c.Set("role_id", userinfo["role_id"]) 48 | //} 49 | } 50 | -------------------------------------------------------------------------------- /middleware/logger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/taoshihan1991/imaptool/tools" 6 | "time" 7 | ) 8 | 9 | func NewMidLogger() gin.HandlerFunc { 10 | logger := tools.Logger() 11 | return func(c *gin.Context) { 12 | // 开始时间 13 | startTime := time.Now() 14 | 15 | // 处理请求 16 | c.Next() 17 | 18 | // 结束时间 19 | endTime := time.Now() 20 | 21 | // 执行时间 22 | latencyTime := endTime.Sub(startTime) 23 | 24 | // 请求方式 25 | reqMethod := c.Request.Method 26 | 27 | // 请求路由 28 | reqUri := c.Request.RequestURI 29 | 30 | // 状态码 31 | statusCode := c.Writer.Status() 32 | 33 | // 请求IP 34 | clientIP := c.ClientIP() 35 | 36 | //日志格式 37 | logger.Infof("| %3d | %13v | %15s | %s | %s |", 38 | statusCode, 39 | latencyTime, 40 | clientIP, 41 | reqMethod, 42 | reqUri, 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /middleware/rbac.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "github.com/taoshihan1991/imaptool/models" 7 | "strings" 8 | ) 9 | 10 | func RbacAuth(c *gin.Context) { 11 | roleId, _ := c.Get("role_id") 12 | role := models.FindRole(roleId) 13 | var flag bool 14 | rPaths := strings.Split(c.Request.RequestURI, "?") 15 | uriParam := fmt.Sprintf("%s:%s", c.Request.Method, rPaths[0]) 16 | if role.Method != "*" || role.Path != "*" { 17 | paths := strings.Split(role.Path, ",") 18 | for _, p := range paths { 19 | if uriParam == p { 20 | flag = true 21 | break 22 | } 23 | } 24 | if !flag { 25 | c.JSON(200, gin.H{ 26 | "code": 403, 27 | "msg": "没有权限:" + uriParam, 28 | }) 29 | c.Abort() 30 | return 31 | } 32 | //methods := strings.Split(role.Method, ",") 33 | //for _, m := range methods { 34 | // if c.Request.Method == m { 35 | // methodFlag = true 36 | // break 37 | // } 38 | //} 39 | //if !methodFlag { 40 | // c.JSON(200, gin.H{ 41 | // "code": 403, 42 | // "msg": "没有权限:" + c.Request.Method + "," + rPaths[0], 43 | // }) 44 | // c.Abort() 45 | // return 46 | //} 47 | } 48 | //var flag bool 49 | //if role.Path != "*" { 50 | // paths := strings.Split(role.Path, ",") 51 | // for _, p := range paths { 52 | // if rPaths[0] == p { 53 | // flag = true 54 | // break 55 | // } 56 | // } 57 | // if !flag { 58 | // c.JSON(200, gin.H{ 59 | // "code": 403, 60 | // "msg": "没有权限:" + rPaths[0], 61 | // }) 62 | // c.Abort() 63 | // return 64 | // } 65 | //} 66 | } 67 | -------------------------------------------------------------------------------- /models/abouts.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type About struct { 4 | ID uint `gorm:"primary_key" json:"id"` 5 | TitleCn string `json:"title_cn"` 6 | TitleEn string `json:"title_en"` 7 | KeywordsCn string `json:"keywords_cn"` 8 | KeywordsEn string `json:"keywords_en"` 9 | DescCn string `json:"desc_cn"` 10 | DescEn string `json:"desc_en"` 11 | CssJs string `json:"css_js"` 12 | HtmlCn string `json:"html_cn"` 13 | HtmlEn string `json:"html_en"` 14 | Page string `json:"page"` 15 | } 16 | 17 | func FindAbouts() []About { 18 | var a []About 19 | DB.Select("id,title_cn,page").Find(&a) 20 | return a 21 | } 22 | 23 | func FindAboutByPage(page interface{}) About { 24 | var a About 25 | DB.Where("page = ?", page).First(&a) 26 | return a 27 | } 28 | func FindAboutByPageLanguage(page interface{}, lang string) About { 29 | var a About 30 | if lang == "" { 31 | lang = "cn" 32 | } 33 | if lang == "en" { 34 | DB.Select("css_js,title_en,keywords_en,desc_en,html_en").Where("page = ?", page).First(&a) 35 | } else { 36 | DB.Select("css_js,title_cn,keywords_cn,desc_cn,html_cn").Where("page = ?", page).First(&a) 37 | } 38 | return a 39 | } 40 | func UpdateAbout(page string, title_cn string, title_en string, keywords_cn string, keywords_en string, desc_cn string, desc_en string, css_js string, html_cn string, html_en string) { 41 | c := &About{ 42 | TitleCn: title_cn, 43 | TitleEn: title_en, 44 | KeywordsCn: keywords_cn, 45 | KeywordsEn: keywords_en, 46 | DescCn: desc_cn, 47 | DescEn: desc_en, 48 | CssJs: css_js, 49 | HtmlCn: html_cn, 50 | HtmlEn: html_en, 51 | } 52 | DB.Model(c).Where("page = ?", page).Update(c) 53 | InitConfig() 54 | } 55 | -------------------------------------------------------------------------------- /models/configs.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | var CustomConfigs []Config 4 | 5 | type Config struct { 6 | ID uint `gorm:"primary_key" json:"id"` 7 | ConfName string `json:"conf_name"` 8 | ConfKey string `json:"conf_key"` 9 | ConfValue string `json:"conf_value"` 10 | } 11 | 12 | func UpdateConfig(key string, value string) { 13 | c := &Config{ 14 | ConfValue: value, 15 | } 16 | DB.Model(c).Where("conf_key = ?", key).Update(c) 17 | InitConfig() 18 | } 19 | func FindConfigs() []Config { 20 | var config []Config 21 | DB.Find(&config) 22 | return config 23 | } 24 | func InitConfig() { 25 | CustomConfigs = FindConfigs() 26 | } 27 | func FindConfig(key string) string { 28 | for _, config := range CustomConfigs { 29 | if key == config.ConfKey { 30 | return config.ConfValue 31 | } 32 | } 33 | return "" 34 | } 35 | -------------------------------------------------------------------------------- /models/ipblacks.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type Ipblack struct { 6 | ID uint `gorm:"primary_key" json:"id"` 7 | IP string `json:"ip"` 8 | KefuId string `json:"kefu_id"` 9 | CreateAt time.Time `json:"create_at"` 10 | } 11 | 12 | func CreateIpblack(ip string, kefuId string) uint { 13 | black := &Ipblack{ 14 | IP: ip, 15 | KefuId: kefuId, 16 | CreateAt: time.Now(), 17 | } 18 | DB.Create(black) 19 | return black.ID 20 | } 21 | func DeleteIpblackByIp(ip string) { 22 | DB.Where("ip = ?", ip).Delete(Ipblack{}) 23 | } 24 | func FindIp(ip string) Ipblack { 25 | var ipblack Ipblack 26 | DB.Where("ip = ?", ip).First(&ipblack) 27 | return ipblack 28 | } 29 | func FindIpsByKefuId(id string) []Ipblack { 30 | var ipblack []Ipblack 31 | DB.Where("kefu_id = ?", id).Find(&ipblack) 32 | return ipblack 33 | } 34 | func FindIps(query interface{}, args []interface{}, page uint, pagesize uint) []Ipblack { 35 | offset := (page - 1) * pagesize 36 | if offset < 0 { 37 | offset = 0 38 | } 39 | var ipblacks []Ipblack 40 | if query != nil { 41 | DB.Where(query, args...).Offset(offset).Limit(pagesize).Find(&ipblacks) 42 | } else { 43 | DB.Offset(offset).Limit(pagesize).Find(&ipblacks) 44 | } 45 | return ipblacks 46 | } 47 | 48 | //查询条数 49 | func CountIps(query interface{}, args []interface{}) uint { 50 | var count uint 51 | if query != nil { 52 | DB.Model(&Visitor{}).Where(query, args...).Count(&count) 53 | } else { 54 | DB.Model(&Visitor{}).Count(&count) 55 | } 56 | return count 57 | } 58 | -------------------------------------------------------------------------------- /models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jinzhu/gorm" 6 | "github.com/taoshihan1991/imaptool/common" 7 | "log" 8 | "time" 9 | ) 10 | 11 | var DB *gorm.DB 12 | 13 | type Model struct { 14 | ID uint `gorm:"primary_key" json:"id"` 15 | CreatedAt time.Time `json:"created_at"` 16 | UpdatedAt time.Time `json:"updated_at"` 17 | DeletedAt *time.Time `sql:"index" json:"deleted_at"` 18 | } 19 | 20 | func init() { 21 | Connect() 22 | } 23 | func Connect() error { 24 | mysql := common.GetMysqlConf() 25 | 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) 26 | var err error 27 | DB, err = gorm.Open("mysql", dsn) 28 | if err != nil { 29 | log.Println(err) 30 | panic("数据库连接失败!") 31 | return err 32 | } 33 | DB.SingularTable(true) 34 | DB.LogMode(true) 35 | DB.DB().SetMaxIdleConns(10) 36 | DB.DB().SetMaxOpenConns(100) 37 | DB.DB().SetConnMaxLifetime(59 * time.Second) 38 | InitConfig() 39 | return nil 40 | } 41 | func Execute(sql string) error { 42 | return DB.Exec(sql).Error 43 | } 44 | func CloseDB() { 45 | defer DB.Close() 46 | } 47 | -------------------------------------------------------------------------------- /models/replys.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ReplyItem struct { 4 | Id string `json:"item_id"` 5 | Content string `json:"item_content"` 6 | GroupId string `json:"group_id"` 7 | ItemName string `json:"item_name"` 8 | UserId string `json:"user_id"` 9 | } 10 | type ReplyGroup struct { 11 | Id string `json:"group_id"` 12 | GroupName string `json:"group_name"` 13 | UserId string `json:"user_id"` 14 | Items []*ReplyItem `json:"items";"` 15 | } 16 | 17 | func FindReplyItemByUserIdTitle(userId interface{}, title string) ReplyItem { 18 | var reply ReplyItem 19 | DB.Where("user_id = ? and item_name = ?", userId, title).Find(&reply) 20 | return reply 21 | } 22 | func FindReplyByUserId(userId interface{}) []*ReplyGroup { 23 | var replyGroups []*ReplyGroup 24 | //DB.Raw("select a.*,b.* from reply_group a left join reply_item b on a.id=b.group_id where a.user_id=? ", userId).Scan(&replyGroups) 25 | var replyItems []*ReplyItem 26 | DB.Where("user_id = ?", userId).Find(&replyGroups) 27 | DB.Where("user_id = ?", userId).Find(&replyItems) 28 | temp := make(map[string]*ReplyGroup) 29 | for _, replyGroup := range replyGroups { 30 | replyGroup.Items = make([]*ReplyItem, 0) 31 | temp[replyGroup.Id] = replyGroup 32 | } 33 | for _, replyItem := range replyItems { 34 | temp[replyItem.GroupId].Items = append(temp[replyItem.GroupId].Items, replyItem) 35 | } 36 | return replyGroups 37 | } 38 | func FindReplyTitleByUserId(userId interface{}) []*ReplyGroup { 39 | var replyGroups []*ReplyGroup 40 | //DB.Raw("select a.*,b.* from reply_group a left join reply_item b on a.id=b.group_id where a.user_id=? ", userId).Scan(&replyGroups) 41 | var replyItems []*ReplyItem 42 | DB.Where("user_id = ?", userId).Find(&replyGroups) 43 | DB.Select("item_name,group_id").Where("user_id = ?", userId).Find(&replyItems) 44 | temp := make(map[string]*ReplyGroup) 45 | for _, replyGroup := range replyGroups { 46 | replyGroup.Items = make([]*ReplyItem, 0) 47 | temp[replyGroup.Id] = replyGroup 48 | } 49 | for _, replyItem := range replyItems { 50 | temp[replyItem.GroupId].Items = append(temp[replyItem.GroupId].Items, replyItem) 51 | } 52 | return replyGroups 53 | } 54 | func CreateReplyGroup(groupName string, userId string) { 55 | g := &ReplyGroup{ 56 | GroupName: groupName, 57 | UserId: userId, 58 | } 59 | DB.Create(g) 60 | } 61 | func CreateReplyContent(groupId string, userId string, content, itemName string) { 62 | g := &ReplyItem{ 63 | GroupId: groupId, 64 | UserId: userId, 65 | Content: content, 66 | ItemName: itemName, 67 | } 68 | DB.Create(g) 69 | } 70 | func UpdateReplyContent(id, userId, title, content string) { 71 | r := &ReplyItem{ 72 | ItemName: title, 73 | Content: content, 74 | } 75 | DB.Model(&ReplyItem{}).Where("user_id = ? and id = ?", userId, id).Update(r) 76 | } 77 | func DeleteReplyContent(id string, userId string) { 78 | DB.Where("user_id = ? and id = ?", userId, id).Delete(ReplyItem{}) 79 | } 80 | func DeleteReplyGroup(id string, userId string) { 81 | DB.Where("user_id = ? and id = ?", userId, id).Delete(ReplyGroup{}) 82 | DB.Where("user_id = ? and group_id = ?", userId, id).Delete(ReplyItem{}) 83 | } 84 | func FindReplyBySearcch(userId interface{}, search string) []*ReplyGroup { 85 | var replyGroups []*ReplyGroup 86 | var replyItems []*ReplyItem 87 | DB.Where("user_id = ?", userId).Find(&replyGroups) 88 | DB.Where("user_id = ? and content like ?", userId, "%"+search+"%").Find(&replyItems) 89 | temp := make(map[string]*ReplyGroup) 90 | for _, replyGroup := range replyGroups { 91 | replyGroup.Items = make([]*ReplyItem, 0) 92 | temp[replyGroup.Id] = replyGroup 93 | } 94 | for _, replyItem := range replyItems { 95 | temp[replyItem.GroupId].Items = append(temp[replyItem.GroupId].Items, replyItem) 96 | } 97 | var newReplyGroups []*ReplyGroup = make([]*ReplyGroup, 0) 98 | for _, replyGroup := range replyGroups { 99 | if len(replyGroup.Items) != 0 { 100 | newReplyGroups = append(newReplyGroups, replyGroup) 101 | } 102 | } 103 | return newReplyGroups 104 | } 105 | -------------------------------------------------------------------------------- /models/roles.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Role struct { 4 | Id string `json:"role_id"` 5 | Name string `json:"role_name"` 6 | Method string `json:"method"` 7 | Path string `json:"path"` 8 | } 9 | 10 | func FindRoles() []Role { 11 | var roles []Role 12 | DB.Order("id desc").Find(&roles) 13 | return roles 14 | } 15 | func FindRole(id interface{}) Role { 16 | var role Role 17 | DB.Where("id = ?", id).First(&role) 18 | return role 19 | } 20 | func SaveRole(id string, name string, method string, path string) { 21 | role := &Role{ 22 | Method: method, 23 | Name: name, 24 | Path: path, 25 | } 26 | DB.Model(role).Where("id=?", id).Update(role) 27 | } 28 | -------------------------------------------------------------------------------- /models/user_client.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type User_client struct { 6 | ID uint `gorm:"primary_key" json:"id"` 7 | Kefu string `json:"kefu"` 8 | Client_id string `json:"client_id"` 9 | Created_at string `json:"created_at"` 10 | } 11 | 12 | func CreateUserClient(kefu, clientId string) uint { 13 | u := &User_client{ 14 | Kefu: kefu, 15 | Client_id: clientId, 16 | Created_at: time.Now().Format("2006-01-02 15:04:05"), 17 | } 18 | DB.Create(u) 19 | return u.ID 20 | } 21 | func FindClients(kefu string) []User_client { 22 | var arr []User_client 23 | DB.Where("kefu = ?", kefu).Find(&arr) 24 | return arr 25 | } 26 | -------------------------------------------------------------------------------- /models/user_roles.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type User_role struct { 8 | ID uint `gorm:"primary_key" json:"id"` 9 | UserId string `json:"user_id"` 10 | RoleId uint `json:"role_id"` 11 | } 12 | 13 | func FindRoleByUserId(userId interface{}) User_role { 14 | var uRole User_role 15 | DB.Where("user_id = ?", userId).First(&uRole) 16 | return uRole 17 | } 18 | func CreateUserRole(userId uint, roleId uint) { 19 | uRole := &User_role{ 20 | UserId: strconv.Itoa(int(userId)), 21 | RoleId: roleId, 22 | } 23 | DB.Create(uRole) 24 | } 25 | func DeleteRoleByUserId(userId interface{}) { 26 | DB.Where("user_id = ?", userId).Delete(User_role{}) 27 | } 28 | -------------------------------------------------------------------------------- /models/users.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | _ "github.com/jinzhu/gorm/dialects/mysql" 5 | "time" 6 | ) 7 | 8 | type User struct { 9 | Model 10 | Name string `json:"name"` 11 | Password string `json:"password"` 12 | Nickname string `json:"nickname"` 13 | Avator string `json:"avator"` 14 | RoleName string `json:"role_name" sql:"-"` 15 | RoleId string `json:"role_id" sql:"-"` 16 | } 17 | 18 | func CreateUser(name string, password string, avator string, nickname string) uint { 19 | user := &User{ 20 | Name: name, 21 | Password: password, 22 | Avator: avator, 23 | Nickname: nickname, 24 | } 25 | user.UpdatedAt = time.Now() 26 | DB.Create(user) 27 | return user.ID 28 | } 29 | func UpdateUser(id string, name string, password string, avator string, nickname string) { 30 | user := &User{ 31 | Name: name, 32 | Avator: avator, 33 | Nickname: nickname, 34 | } 35 | user.UpdatedAt = time.Now() 36 | if password != "" { 37 | user.Password = password 38 | } 39 | DB.Model(&User{}).Where("id = ?", id).Update(user) 40 | } 41 | func UpdateUserPass(name string, pass string) { 42 | user := &User{ 43 | Password: pass, 44 | } 45 | user.UpdatedAt = time.Now() 46 | DB.Model(user).Where("name = ?", name).Update("Password", pass) 47 | } 48 | func UpdateUserAvator(name string, avator string) { 49 | user := &User{ 50 | Avator: avator, 51 | } 52 | user.UpdatedAt = time.Now() 53 | DB.Model(user).Where("name = ?", name).Update("Avator", avator) 54 | } 55 | func FindUser(username string) User { 56 | var user User 57 | DB.Where("name = ?", username).First(&user) 58 | return user 59 | } 60 | func FindUserById(id interface{}) User { 61 | var user User 62 | DB.Select("user.*,role.name role_name,role.id role_id").Joins("join user_role on user.id=user_role.user_id").Joins("join role on user_role.role_id=role.id").Where("user.id = ?", id).First(&user) 63 | return user 64 | } 65 | func DeleteUserById(id string) { 66 | DB.Where("id = ?", id).Delete(User{}) 67 | } 68 | func FindUsers() []User { 69 | var users []User 70 | 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) 71 | return users 72 | } 73 | func FindUserRole(query interface{}, id interface{}) User { 74 | var user User 75 | DB.Select(query).Where("user.id = ?", id).Joins("join user_role on user.id=user_role.user_id").Joins("join role on user_role.role_id=role.id").First(&user) 76 | return user 77 | } 78 | -------------------------------------------------------------------------------- /models/visitors.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Visitor struct { 8 | Model 9 | Name string `json:"name"` 10 | Avator string `json:"avator"` 11 | SourceIp string `json:"source_ip"` 12 | ToId string `json:"to_id"` 13 | VisitorId string `json:"visitor_id"` 14 | Status uint `json:"status"` 15 | Refer string `json:"refer"` 16 | City string `json:"city"` 17 | ClientIp string `json:"client_ip"` 18 | Extra string `json:"extra"` 19 | } 20 | 21 | func CreateVisitor(name, avator, sourceIp, toId, visitorId, refer, city, clientIp, extra string) { 22 | v := &Visitor{ 23 | Name: name, 24 | Avator: avator, 25 | SourceIp: sourceIp, 26 | ToId: toId, 27 | VisitorId: visitorId, 28 | Status: 1, 29 | Refer: refer, 30 | City: city, 31 | ClientIp: clientIp, 32 | Extra: extra, 33 | } 34 | v.UpdatedAt = time.Now() 35 | DB.Create(v) 36 | } 37 | func FindVisitorByVistorId(visitorId string) Visitor { 38 | var v Visitor 39 | DB.Where("visitor_id = ?", visitorId).First(&v) 40 | return v 41 | } 42 | func FindVisitors(page uint, pagesize uint) []Visitor { 43 | offset := (page - 1) * pagesize 44 | if offset < 0 { 45 | offset = 0 46 | } 47 | var visitors []Visitor 48 | DB.Offset(offset).Limit(pagesize).Order("status desc, updated_at desc").Find(&visitors) 49 | return visitors 50 | } 51 | func FindVisitorsByKefuId(page uint, pagesize uint, kefuId string) []Visitor { 52 | offset := (page - 1) * pagesize 53 | if offset <= 0 { 54 | offset = 0 55 | } 56 | var visitors []Visitor 57 | //sql := fmt.Sprintf("select * from visitor where id>=(select id from visitor where to_id='%s' order by updated_at desc limit %d,1) and to_id='%s' order by updated_at desc limit %d ", kefuId, offset, kefuId, pagesize) 58 | //DB.Raw(sql).Scan(&visitors) 59 | DB.Where("to_id=?", kefuId).Offset(offset).Limit(pagesize).Order("updated_at desc").Find(&visitors) 60 | return visitors 61 | } 62 | func FindVisitorsOnline() []Visitor { 63 | var visitors []Visitor 64 | DB.Where("status = ?", 1).Find(&visitors) 65 | return visitors 66 | } 67 | func UpdateVisitorStatus(visitorId string, status uint) { 68 | visitor := Visitor{} 69 | DB.Model(&visitor).Where("visitor_id = ?", visitorId).Update("status", status) 70 | } 71 | func UpdateVisitor(name, avator, visitorId string, status uint, clientIp string, sourceIp string, refer, extra string) { 72 | visitor := &Visitor{ 73 | Status: status, 74 | ClientIp: clientIp, 75 | SourceIp: sourceIp, 76 | Refer: refer, 77 | Extra: extra, 78 | Name: name, 79 | Avator: avator, 80 | } 81 | visitor.UpdatedAt = time.Now() 82 | DB.Model(visitor).Where("visitor_id = ?", visitorId).Update(visitor) 83 | } 84 | func UpdateVisitorKefu(visitorId string, kefuId string) { 85 | visitor := Visitor{} 86 | DB.Model(&visitor).Where("visitor_id = ?", visitorId).Update("to_id", kefuId) 87 | } 88 | 89 | //查询条数 90 | func CountVisitors() uint { 91 | var count uint 92 | DB.Model(&Visitor{}).Count(&count) 93 | return count 94 | } 95 | 96 | //查询条数 97 | func CountVisitorsByKefuId(kefuId string) uint { 98 | var count uint 99 | DB.Model(&Visitor{}).Where("to_id=?", kefuId).Count(&count) 100 | return count 101 | } 102 | 103 | //查询每天条数 104 | type EveryDayNum struct { 105 | Day string `json:"day"` 106 | Num int64 `json:"num"` 107 | } 108 | 109 | func CountVisitorsEveryDay(toId string) []EveryDayNum { 110 | var results []EveryDayNum 111 | DB.Raw("select DATE_FORMAT(created_at,'%y-%m-%d') as day ,"+ 112 | "count(*) as num from visitor where to_id=? group by day order by day desc limit 30", 113 | toId).Scan(&results) 114 | return results 115 | } 116 | -------------------------------------------------------------------------------- /models/welcomes.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type Welcome struct { 6 | ID uint `gorm:"primary_key" json:"id"` 7 | UserId string `json:"user_id"` 8 | Keyword string `json:"keyword"` 9 | Content string `json:"content"` 10 | IsDefault uint `json:"is_default"` 11 | Ctime time.Time `json:"ctime"` 12 | } 13 | 14 | func CreateWelcome(userId string, content string) uint { 15 | if userId == "" || content == "" { 16 | return 0 17 | } 18 | w := &Welcome{ 19 | UserId: userId, 20 | Content: content, 21 | Ctime: time.Now(), 22 | Keyword: "welcome", 23 | } 24 | DB.Create(w) 25 | return w.ID 26 | } 27 | func UpdateWelcome(userId string, id string, content string) uint { 28 | if userId == "" || content == "" { 29 | return 0 30 | } 31 | w := &Welcome{ 32 | Content: content, 33 | } 34 | DB.Model(w).Where("user_id = ? and id = ?", userId, id).Update(w) 35 | return w.ID 36 | } 37 | func FindWelcomeByUserIdKey(userId interface{}, keyword interface{}) Welcome { 38 | var w Welcome 39 | DB.Where("user_id = ? and keyword=?", userId, keyword).First(&w) 40 | return w 41 | } 42 | func FindWelcomesByUserId(userId interface{}) []Welcome { 43 | var w []Welcome 44 | DB.Where("user_id = ?", userId).Find(&w) 45 | return w 46 | } 47 | func FindWelcomesByKeyword(userId interface{}, keyword interface{}) []Welcome { 48 | var w []Welcome 49 | DB.Where("user_id = ? and keyword=?", userId, keyword).Find(&w) 50 | return w 51 | } 52 | func DeleteWelcome(userId interface{}, id string) { 53 | DB.Where("user_id = ? and id = ?", userId, id).Delete(Welcome{}) 54 | } 55 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ### 郑重提示 2 | 禁止将本项目用于含病毒、木马、色情、赌博、诈骗、违禁用品、假冒产品、虚假信息、数字货币、金融等违法违规业务 3 | 4 | 当前项目仅供个人学习测试,禁止一切线上商用行为,禁止一切违法使用!!! 5 | 6 | 7 | 8 | 9 | ### 项目简介 10 | 11 | Golang语言开源客服系统,主要使用了gin + jwt-go + websocket + go.uuid + gorm + cobra + VueJS + ElementUI + MySQL等技术 12 | 13 | 14 | ### 安装使用 15 | 16 | 17 | * 先安装和运行mysql数据库 ,版本>=5.5 ,创建数据库 18 | 19 | ``` 20 | create database gofly charset utf8mb4; 21 | ``` 22 | 23 | * 配置数据库链接信息,在config目录mysql.json中 24 | ```php 25 | { 26 | "Server":"127.0.0.1", 27 | "Port":"3306", 28 | "Database":"gofly", 29 | "Username":"go-fly", 30 | "Password":"go-fly" 31 | } 32 | ``` 33 | * 安装配置Golang运行环境,请参照下面的命令去执行 34 | ```php 35 | wget https://studygolang.com/dl/golang/go1.20.2.linux-amd64.tar.gz 36 | tar -C /usr/local -xvf go1.20.2.linux-amd64.tar.gz 37 | mv go1.20.2.linux-amd64.tar.gz /tmp 38 | echo "PATH=\$PATH:/usr/local/go/bin" >> /etc/profile 39 | echo "PATH=\$PATH:/usr/local/go/bin" >> ~/.bashrc 40 | source /etc/profile 41 | go version 42 | go env -w GO111MODULE=on 43 | go env -w GOPROXY=https://goproxy.cn,direct 44 | ``` 45 | * 下载代码 46 | 47 | 在任意目录 git clone https://github.com/taoshihan1991/go-fly.git 48 | 49 | 进入go-fly 目录 50 | 51 | * 导入数据库 go run go-fly.go install 52 | 53 | * 源码运行 go run go-fly.go server 54 | 55 | * 源码打包 go build -o kefu 会生成kefu可以执行文件 56 | 57 | * 二进制文件运行 58 | 59 | linux: ./kefu server [可选 -p 8082 -d] 60 | 61 | windows: kefu.exe server [可选 -p 8082 -d] 62 | 63 | * 关闭程序 64 | 65 | killall kefu 66 | 67 | 68 | 程序正常运行后,监听端口8081,可以直接ip+端口8081访问 69 | 70 | 也可以配置域名访问,反向代理到8081端口,就能隐藏端口号 71 | ### 客服对接 72 | 聊天链接 73 | 74 | http://127.0.0.1:8081/chatIndex?kefu_id=kefu2 75 | 76 | 弹窗使用 77 | 78 | ``` 79 | (function(a, b, c, d) { 80 | let h = b.getElementsByTagName('head')[0];let s = b.createElement('script'); 81 | s.type = 'text/javascript';s.src = c+"/static/js/kefu-front.js";s.onload = s.onreadystatechange = function () { 82 | if (!this.readyState || this.readyState === "loaded" || this.readyState === "complete") d(c); 83 | };h.appendChild(s); 84 | })(window, document,"http://127.0.0.1:8081",function(u){ 85 | KEFU.init({ 86 | KEFU_URL:u, 87 | KEFU_KEFU_ID: "kefu2", 88 | }) 89 | }); 90 | 91 | ``` 92 | ### 版权声明 93 | 94 | 当前项目是完整功能代码 , 但是仍然仅支持个人演示测试 , 不包含线上使用 ,禁止一切商用行为。 95 | 使用本软件时,请遵守当地法律法规,任何违法用途一切后果请自行承担. -------------------------------------------------------------------------------- /router/view.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/taoshihan1991/imaptool/middleware" 6 | "github.com/taoshihan1991/imaptool/tmpl" 7 | ) 8 | 9 | func InitViewRouter(engine *gin.Engine) { 10 | engine.GET("/", tmpl.PageIndex) 11 | 12 | engine.GET("/login", tmpl.PageLogin) 13 | engine.GET("/pannel", tmpl.PagePannel) 14 | engine.GET("/chatIndex", tmpl.PageChat) 15 | engine.GET("/main", middleware.JwtPageMiddleware, tmpl.PageMain) 16 | engine.GET("/chat_main", middleware.JwtPageMiddleware, middleware.DomainLimitMiddleware, tmpl.PageChatMain) 17 | engine.GET("/setting", middleware.DomainLimitMiddleware, tmpl.PageSetting) 18 | engine.GET("/setting_statistics", tmpl.PageSettingStatis) 19 | engine.GET("/setting_indexpage", tmpl.PageSettingIndexPage) 20 | engine.GET("/setting_indexpages", tmpl.PageSettingIndexPages) 21 | engine.GET("/setting_mysql", tmpl.PageSettingMysql) 22 | engine.GET("/setting_welcome", tmpl.PageSettingWelcome) 23 | engine.GET("/setting_deploy", tmpl.PageSettingDeploy) 24 | engine.GET("/setting_kefu_list", tmpl.PageKefuList) 25 | engine.GET("/setting_avator", tmpl.PageAvator) 26 | engine.GET("/setting_modifypass", tmpl.PageModifypass) 27 | engine.GET("/setting_ipblack", tmpl.PageIpblack) 28 | engine.GET("/setting_config", tmpl.PageConfig) 29 | engine.GET("/mail_list", tmpl.PageMailList) 30 | engine.GET("/roles_list", tmpl.PageRoleList) 31 | } 32 | -------------------------------------------------------------------------------- /start.bat: -------------------------------------------------------------------------------- 1 | go-fly.exe server 2 | pause -------------------------------------------------------------------------------- /static/cdn/element-ui/2.15.1/theme-chalk/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoshihan1991/go-fly/9988222db083df397604281124311c87c2ff9967/static/cdn/element-ui/2.15.1/theme-chalk/fonts/element-icons.woff -------------------------------------------------------------------------------- /static/cdn/element-ui/2.15.7/theme-chalk/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoshihan1991/go-fly/9988222db083df397604281124311c87c2ff9967/static/cdn/element-ui/2.15.7/theme-chalk/fonts/element-icons.woff -------------------------------------------------------------------------------- /static/css/front.css: -------------------------------------------------------------------------------- 1 | ::-webkit-scrollbar 2 | { 3 | width: 5px; 4 | height: 110px; 5 | background-color: #F5F5F5; 6 | } 7 | /*定义滚动条轨道 内阴影+圆角*/ 8 | ::-webkit-scrollbar-track 9 | { 10 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); 11 | border-radius: 10px; 12 | background-color: #F5F5F5; 13 | } 14 | /*定义滑块 内阴影+圆角*/ 15 | ::-webkit-scrollbar-thumb 16 | { 17 | border-radius: 10px; 18 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); 19 | background-color: #bdbdbd; 20 | } 21 | /*滑块效果*/ 22 | ::-webkit-scrollbar-thumb:hover 23 | { 24 | border-radius: 5px; 25 | -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.2); 26 | background: rgba(0,0,0,0.4); 27 | } 28 | /*IE滚动条颜色*/ 29 | html { 30 | scrollbar-face-color:#bfbfbf;/*滚动条颜色*/ 31 | scrollbar-highlight-color:#000; 32 | scrollbar-3dlight-color:#000; 33 | scrollbar-darkshadow-color:#000; 34 | scrollbar-Shadow-color:#adadad;/*滑块边色*/ 35 | scrollbar-arrow-color:rgba(0,0,0,0.4);/*箭头颜色*/ 36 | scrollbar-track-color:#eeeeee;/*背景颜色*/ 37 | } -------------------------------------------------------------------------------- /static/css/icon/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 3864484 */ 3 | src: url('iconfont.woff2?t=1678338039263') format('woff2'), 4 | url('iconfont.woff?t=1678338039263') format('woff'), 5 | url('iconfont.ttf?t=1678338039263') format('truetype'); 6 | } 7 | 8 | .iconfont { 9 | font-family: "iconfont" !important; 10 | font-size: 16px; 11 | font-style: normal; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .icon-more:before { 17 | content: "\e867"; 18 | } 19 | 20 | .icon-zengjiatianjiajiahao:before { 21 | content: "\e62a"; 22 | } 23 | 24 | .icon-jianshaominimize3:before { 25 | content: "\e68b"; 26 | } 27 | 28 | .icon-folder-fill:before { 29 | content: "\e7c4"; 30 | } 31 | 32 | .icon-jietu:before { 33 | content: "\e611"; 34 | } 35 | 36 | .icon-duoyuyan:before { 37 | content: "\e606"; 38 | } 39 | 40 | .icon-jiahao:before { 41 | content: "\eaf3"; 42 | } 43 | 44 | .icon-xiaolian:before { 45 | content: "\ec80"; 46 | } 47 | 48 | .icon-fasong:before { 49 | content: "\e604"; 50 | } 51 | 52 | -------------------------------------------------------------------------------- /static/css/icon/iconfont.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "3864484", 3 | "name": "我的客服", 4 | "font_family": "iconfont", 5 | "css_prefix_text": "icon-", 6 | "description": "", 7 | "glyphs": [ 8 | { 9 | "icon_id": "18991683", 10 | "name": "more", 11 | "font_class": "more", 12 | "unicode": "e867", 13 | "unicode_decimal": 59495 14 | }, 15 | { 16 | "icon_id": "8817897", 17 | "name": "增加添加加号", 18 | "font_class": "zengjiatianjiajiahao", 19 | "unicode": "e62a", 20 | "unicode_decimal": 58922 21 | }, 22 | { 23 | "icon_id": "608294", 24 | "name": "减少", 25 | "font_class": "jianshaominimize3", 26 | "unicode": "e68b", 27 | "unicode_decimal": 59019 28 | }, 29 | { 30 | "icon_id": "6151130", 31 | "name": "folder-fill", 32 | "font_class": "folder-fill", 33 | "unicode": "e7c4", 34 | "unicode_decimal": 59332 35 | }, 36 | { 37 | "icon_id": "13397103", 38 | "name": "截图", 39 | "font_class": "jietu", 40 | "unicode": "e611", 41 | "unicode_decimal": 58897 42 | }, 43 | { 44 | "icon_id": "10598085", 45 | "name": "language,多语言", 46 | "font_class": "duoyuyan", 47 | "unicode": "e606", 48 | "unicode_decimal": 58886 49 | }, 50 | { 51 | "icon_id": "5387527", 52 | "name": "加号", 53 | "font_class": "jiahao", 54 | "unicode": "eaf3", 55 | "unicode_decimal": 60147 56 | }, 57 | { 58 | "icon_id": "6337465", 59 | "name": "笑脸", 60 | "font_class": "xiaolian", 61 | "unicode": "ec80", 62 | "unicode_decimal": 60544 63 | }, 64 | { 65 | "icon_id": "1418205", 66 | "name": "发送", 67 | "font_class": "fasong", 68 | "unicode": "e604", 69 | "unicode_decimal": 58884 70 | } 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /static/css/icon/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoshihan1991/go-fly/9988222db083df397604281124311c87c2ff9967/static/css/icon/iconfont.ttf -------------------------------------------------------------------------------- /static/css/icon/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoshihan1991/go-fly/9988222db083df397604281124311c87c2ff9967/static/css/icon/iconfont.woff -------------------------------------------------------------------------------- /static/css/icon/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoshihan1991/go-fly/9988222db083df397604281124311c87c2ff9967/static/css/icon/iconfont.woff2 -------------------------------------------------------------------------------- /static/css/index.css: -------------------------------------------------------------------------------- 1 | .footer{background: #30313a;color: #acacac;padding: 30px 0;min-width: 1190px;} -------------------------------------------------------------------------------- /static/css/layui/css/modules/code.css: -------------------------------------------------------------------------------- 1 | html #layuicss-skincodecss{display:none;position:absolute;width:1989px}.layui-code-h3,.layui-code-view{position:relative;font-size:12px}.layui-code-view{display:block;margin:10px 0;padding:0;border:1px solid #eee;border-left-width:6px;background-color:#FAFAFA;color:#333;font-family:Courier New}.layui-code-h3{padding:0 10px;height:40px;line-height:40px;border-bottom:1px solid #eee}.layui-code-h3 a{position:absolute;right:10px;top:0;color:#999}.layui-code-view .layui-code-ol{position:relative;overflow:auto}.layui-code-view .layui-code-ol li{position:relative;margin-left:45px;line-height:20px;padding:0 10px;border-left:1px solid #e2e2e2;list-style-type:decimal-leading-zero;*list-style-type:decimal;background-color:#fff}.layui-code-view .layui-code-ol li:first-child{padding-top:10px}.layui-code-view .layui-code-ol li:last-child{padding-bottom:10px}.layui-code-view pre{margin:0}.layui-code-notepad{border:1px solid #0C0C0C;border-left-color:#3F3F3F;background-color:#0C0C0C;color:#C2BE9E}.layui-code-notepad .layui-code-h3{border-bottom:none}.layui-code-notepad .layui-code-ol li{background-color:#3F3F3F;border-left:none}.layui-code-demo .layui-code{visibility:visible!important;margin:-15px;border-top:none;border-right:none;border-bottom:none}.layui-code-demo .layui-tab-content{padding:15px;border-top:none} -------------------------------------------------------------------------------- /static/css/layui/css/modules/layer/default/icon-ext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoshihan1991/go-fly/9988222db083df397604281124311c87c2ff9967/static/css/layui/css/modules/layer/default/icon-ext.png -------------------------------------------------------------------------------- /static/css/layui/css/modules/layer/default/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoshihan1991/go-fly/9988222db083df397604281124311c87c2ff9967/static/css/layui/css/modules/layer/default/icon.png -------------------------------------------------------------------------------- /static/css/layui/css/modules/layer/default/loading-0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoshihan1991/go-fly/9988222db083df397604281124311c87c2ff9967/static/css/layui/css/modules/layer/default/loading-0.gif -------------------------------------------------------------------------------- /static/css/layui/css/modules/layer/default/loading-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoshihan1991/go-fly/9988222db083df397604281124311c87c2ff9967/static/css/layui/css/modules/layer/default/loading-1.gif -------------------------------------------------------------------------------- /static/css/layui/css/modules/layer/default/loading-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoshihan1991/go-fly/9988222db083df397604281124311c87c2ff9967/static/css/layui/css/modules/layer/default/loading-2.gif -------------------------------------------------------------------------------- /static/css/layui/font/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoshihan1991/go-fly/9988222db083df397604281124311c87c2ff9967/static/css/layui/font/iconfont.eot -------------------------------------------------------------------------------- /static/css/layui/font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoshihan1991/go-fly/9988222db083df397604281124311c87c2ff9967/static/css/layui/font/iconfont.ttf -------------------------------------------------------------------------------- /static/css/layui/font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoshihan1991/go-fly/9988222db083df397604281124311c87c2ff9967/static/css/layui/font/iconfont.woff -------------------------------------------------------------------------------- /static/css/layui/font/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoshihan1991/go-fly/9988222db083df397604281124311c87c2ff9967/static/css/layui/font/iconfont.woff2 -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | *{ 2 | margin: 0;padding: 0; 3 | } 4 | a { 5 | text-decoration: none; 6 | color: #3973ac; 7 | } 8 | a:hover { 9 | text-decoration: underline; 10 | } 11 | pre{ 12 | background:#f9f9f9; 13 | color: #d73a49; 14 | line-height: 21px; 15 | border: 1px solid #e8e8e8; 16 | padding: 5px; 17 | } 18 | .header{ 19 | background-color: #fff; 20 | color: #fff; 21 | width: 100%; 22 | z-index: 100; 23 | } 24 | .container{ 25 | width: 1140px; 26 | padding: 0; 27 | margin: 0 auto; 28 | } 29 | .header .logo{ 30 | margin: 0; 31 | float: left; 32 | font-size: 32px; 33 | font-weight: bold; 34 | margin-top: 10px; 35 | } 36 | .header a{ 37 | color: #828282; 38 | font-weight: bold; 39 | font-family: "Microsoft JhengHei"; 40 | text-decoration: none; 41 | } 42 | .header .logo a{ 43 | font-size: 30px; 44 | text-decoration: none; 45 | } 46 | .header .logo img{ 47 | width: 120px; 48 | } 49 | .header .navBtn{ 50 | float: right; 51 | margin:30px 0 30px 20px; 52 | display: inline-block; 53 | } 54 | .banner{ 55 | text-align: center; 56 | color: #fff; 57 | background-color: #3385ff; 58 | padding: 20px 10px; 59 | font-size: 18px; 60 | line-height: 28px; 61 | } 62 | .banner h1 { 63 | font-size: 34px; 64 | margin: 20px 0px; 65 | line-height: 45px; 66 | font-weight: 500; 67 | font-family: Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,SimSun,sans-serif; 68 | } 69 | .banner p{ 70 | max-width: 1200px; 71 | margin: 0 auto; 72 | } 73 | .banner .downloadBtn{margin-top: 20px;} 74 | .downloadBtn{display:inline-block;text-decoration:none;color:#fff;border:1px solid #fff;padding:10px 15px;border-radius:5px;} 75 | .downloadBtn:hover{background:#fff;color:#20b2bb;text-decoration: none;} 76 | .jumbotron{ 77 | width: 1000px; 78 | margin: 10px auto; 79 | box-shadow: 2px 2px 15px rgba(0,0,0,.3); 80 | display: block; 81 | } 82 | 83 | 84 | .copyright{ 85 | font-size: 14px; 86 | color: #333; 87 | text-align: center; 88 | padding: 10px; 89 | } 90 | .links{ 91 | margin-top: 20px; 92 | } 93 | .links a{ 94 | color: #20b2bb; 95 | line-height: 23px; 96 | margin: 5px; 97 | } 98 | .main{ 99 | background: #ededed; 100 | overflow: hidden; 101 | } 102 | .mainIntro{ 103 | background: #fff; 104 | box-shadow: 0 1px 5px 0 rgba(0,0,0,.05); 105 | margin: 20px auto; 106 | } 107 | .product{ 108 | padding: 10px; 109 | } 110 | .product h3{ 111 | color: #333; 112 | font-size: 16px; 113 | line-height: 36px; 114 | border-bottom: 1px solid #e6e6e6; 115 | } 116 | .product h4{ 117 | padding: 10px; 118 | font-size: 14px; 119 | font-weight: normal; 120 | border-left: 2px #3385ff solid; 121 | background: #e6e6e6; 122 | margin: 10px 0px; 123 | } 124 | .productContent{ 125 | color: #444; 126 | font-size: 14px; 127 | line-height: 30px; 128 | margin-top: 10px; 129 | } 130 | .productContent ol{ 131 | margin: 0px 15px; 132 | } 133 | .productContent a{ 134 | color: #3973ac; 135 | text-decoration: none; 136 | } 137 | .productContent a:hover{ 138 | text-decoration: underline; 139 | } 140 | .productPost{ 141 | overflow: hidden; 142 | } 143 | .productPost li:first-child { 144 | border-top: 0; 145 | } 146 | .productPost li { 147 | float: left; 148 | margin-right: 2%; 149 | width: 48%; 150 | line-height: 36px; 151 | text-overflow: ellipsis; 152 | white-space: nowrap; 153 | overflow: hidden; 154 | border-top: 1px dashed #e6e6e6; 155 | } 156 | 157 | @media screen and (max-width: 800px) { 158 | .container{ 159 | width: 100%; 160 | } 161 | .header .logo{ 162 | float: none; 163 | text-align: center; 164 | margin: 20px 0px; 165 | } 166 | .header .navBtn{ 167 | margin:5px 0 10px 10px; 168 | float: none; 169 | } 170 | .banner p{ 171 | text-align: left; 172 | } 173 | .jumbotron{ 174 | width: 100%; 175 | height: auto; 176 | } 177 | 178 | 179 | } 180 | .clear{clear:both} 181 | .links{ 182 | margin-top: 20px; 183 | } 184 | .links a{ 185 | color: #20b2bb; 186 | line-height: 23px; 187 | margin: 5px; 188 | } -------------------------------------------------------------------------------- /static/demos/websocket.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |'+(n.content||"")+"
"),n.skin&&(n.anim="up"),"msg"===n.skin&&(n.shade=!1),s.innerHTML=(n.shade?"':"")+'<{item.From}> 发送于 <{item.Date}>
26 |