joe 4 years ago
commit
678e456da6
100 changed files with 7048 additions and 0 deletions
  1. 7 0
      .gitignore
  2. 21 0
      build
  3. 38 0
      cmd/install.go
  4. 37 0
      cmd/root.go
  5. 86 0
      cmd/server.go
  6. 15 0
      cmd/version.go
  7. BIN
      config/city.free.ipdb
  8. 175 0
      config/config.go
  9. 4 0
      config/config.json
  10. 132 0
      config/go-fly.sql
  11. 47 0
      config/language.go
  12. 20 0
      config/language.json
  13. 11 0
      config/model.conf
  14. 7 0
      config/mysql.json
  15. 23 0
      config/nginx.conf
  16. 4 0
      config/policy.csv
  17. 7 0
      config/qiniu.json
  18. 7 0
      config/twong.json
  19. 44 0
      controller/about.go
  20. 63 0
      controller/auth.go
  21. 345 0
      controller/chat.go
  22. 262 0
      controller/folder.go
  23. 14 0
      controller/index.go
  24. 57 0
      controller/ip.go
  25. 132 0
      controller/kefu.go
  26. 54 0
      controller/login.go
  27. 47 0
      controller/main.go
  28. 337 0
      controller/message.go
  29. 60 0
      controller/mysql.go
  30. 121 0
      controller/notice.go
  31. 14 0
      controller/response.go
  32. 33 0
      controller/role.go
  33. 33 0
      controller/setting.go
  34. 24 0
      controller/shout.go
  35. 58 0
      controller/tcp.go
  36. 238 0
      controller/visitor.go
  37. 35 0
      controller/weixin.go
  38. 27 0
      database/mysql.go
  39. 270 0
      docs/docs.go
  40. 202 0
      docs/swagger.json
  41. 132 0
      docs/swagger.yaml
  42. 9 0
      go-fly.go
  43. 31 0
      go.mod
  44. 260 0
      go.sum
  45. 32 0
      middleware/casbin.go
  46. 19 0
      middleware/ipblack.go
  47. 49 0
      middleware/jwt.go
  48. 16 0
      middleware/language.go
  49. 49 0
      middleware/rbac.go
  50. 47 0
      models/abouts.go
  51. 34 0
      models/configs.go
  52. 52 0
      models/ipblacks.go
  53. 69 0
      models/messages.go
  54. 40 0
      models/models.go
  55. 27 0
      models/roles.go
  56. 27 0
      models/user_roles.go
  57. 63 0
      models/users.go
  58. 90 0
      models/visitors.go
  59. 47 0
      models/welcomes.go
  60. 126 0
      readme.md
  61. 75 0
      router/api.go
  62. 30 0
      router/view.go
  63. 317 0
      static/css/common.css
  64. 0 0
      static/css/emojione.min.css
  65. 37 0
      static/css/front.css
  66. 62 0
      static/css/gofly-front.css
  67. 82 0
      static/html/chat_kf_page.html
  68. 206 0
      static/html/chat_main.html
  69. 83 0
      static/html/chat_page.html
  70. 5 0
      static/html/chat_web.css
  71. 54 0
      static/html/chat_web.js
  72. 44 0
      static/html/header.html
  73. 108 0
      static/html/index.html
  74. 100 0
      static/html/list.html
  75. 174 0
      static/html/login.html
  76. 108 0
      static/html/mail_detail.html
  77. 11 0
      static/html/mail_left.html
  78. 162 0
      static/html/main.html
  79. 24 0
      static/html/nav.html
  80. 14 0
      static/html/setting.html
  81. 428 0
      static/html/setting_bottom.html
  82. 35 0
      static/html/setting_config.html
  83. 29 0
      static/html/setting_deploy.html
  84. 34 0
      static/html/setting_ipblack.html
  85. 104 0
      static/html/setting_kefu_list.html
  86. 47 0
      static/html/setting_left.html
  87. 35 0
      static/html/setting_mysql.html
  88. 47 0
      static/html/setting_pageindex.html
  89. 60 0
      static/html/setting_role_list.html
  90. 34 0
      static/html/setting_statistics.html
  91. 53 0
      static/html/setting_welcome.html
  92. 116 0
      static/html/write.html
  93. BIN
      static/images/0.jpg
  94. BIN
      static/images/1.jpg
  95. BIN
      static/images/1.png
  96. BIN
      static/images/10.jpg
  97. BIN
      static/images/11.jpg
  98. BIN
      static/images/12.jpg
  99. BIN
      static/images/13.jpg
  100. BIN
      static/images/14.jpg

+ 7 - 0
.gitignore

@@ -0,0 +1,7 @@
+.idea
+imaptool.exe
+gofly
+logs/
+static/upload/
+*.tar.gz
+bin/

+ 21 - 0
build

@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+FILENAME="update"
+rm -rf bin > /dev/null 2>&1
+mkdir -p bin/{logs,static,docs}
+if [ $1x = "releasex" ]; then
+	FILENAME="release"
+	mkdir -p bin/config
+	cp config/*.ipdb bin/config/
+	cp config/*.json bin/config/
+	cp config/*.sql bin/config/
+	cp config/*.conf bin/config/
+	cp config/*.csv bin/config/
+fi
+rm -rf static/upload/* > /dev/null 2>&1
+cp -r static bin/
+cp -r docs bin/
+cp readme.* bin/
+go build -o gofly .
+cp gofly bin
+rm ${FILENAME}.tar.gz > /dev/null 2>&1
+tar czf ${FILENAME}.tar.gz bin/

+ 38 - 0
cmd/install.go

@@ -0,0 +1,38 @@
+package cmd
+
+import (
+	"fmt"
+	"github.com/spf13/cobra"
+	"github.com/wenstudio/gofly/config"
+	"github.com/wenstudio/gofly/models"
+	"github.com/wenstudio/gofly/tools"
+	"io/ioutil"
+	"os"
+	"strings"
+)
+
+var installCmd = &cobra.Command{
+	Use:   "install",
+	Short: "example:gofly install",
+	Run: func(cmd *cobra.Command, args []string) {
+		install()
+	},
+}
+
+func install() {
+	sqlFile := config.Dir + "go-fly.sql"
+	isExit, _ := tools.IsFileExist(config.MysqlConf)
+	dataExit, _ := tools.IsFileExist(sqlFile)
+	if !isExit || !dataExit {
+		fmt.Println("config/mysql.json 数据库配置文件或者数据库文件go-fly.sql不存在")
+		os.Exit(1)
+	}
+	sqls, _ := ioutil.ReadFile(sqlFile)
+	sqlArr := strings.Split(string(sqls), "|")
+	for _, sql := range sqlArr {
+		if sql == "" {
+			continue
+		}
+		models.Execute(sql)
+	}
+}

+ 37 - 0
cmd/root.go

@@ -0,0 +1,37 @@
+package cmd
+
+import (
+	"errors"
+	"fmt"
+	"github.com/spf13/cobra"
+	"os"
+)
+
+var rootCmd = &cobra.Command{
+	Use:   "gofly",
+	Short: "gofly",
+	Long:  `简洁快速的GO语言WEB在线客服`,
+	Args:  args,
+	Run: func(cmd *cobra.Command, args []string) {
+
+	},
+}
+
+func args(cmd *cobra.Command, args []string) error {
+	if len(args) < 1 {
+
+		return errors.New("至少需要一个参数!")
+	}
+	return nil
+}
+func Execute() {
+	if err := rootCmd.Execute(); err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+}
+func init() {
+	rootCmd.AddCommand(versionCmd)
+	rootCmd.AddCommand(serverCmd)
+	rootCmd.AddCommand(installCmd)
+}

+ 86 - 0
cmd/server.go

@@ -0,0 +1,86 @@
+package cmd
+
+import (
+	"github.com/gin-contrib/cors"
+	"github.com/gin-gonic/gin"
+	"github.com/spf13/cobra"
+	ginSwagger "github.com/swaggo/gin-swagger"
+	"github.com/swaggo/gin-swagger/swaggerFiles"
+	"github.com/wenstudio/gofly/config"
+	"github.com/wenstudio/gofly/docs"
+	"github.com/wenstudio/gofly/router"
+	"github.com/wenstudio/gofly/tools"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+)
+
+var (
+	addr        string
+	port        string
+	tcpport     string
+	daemon      bool
+	GoflyConfig *config.Config
+)
+var serverCmd = &cobra.Command{
+	Use:     "server",
+	Short:   "example:gofly server port 8081",
+	Example: "gofly server -c config/",
+	Run: func(cmd *cobra.Command, args []string) {
+		run()
+	},
+}
+
+func init() {
+	serverCmd.PersistentFlags().StringVarP(&addr, "addr", "a", "127.0.0.1", "监听地址")
+	serverCmd.PersistentFlags().StringVarP(&port, "port", "p", "8081", "监听端口号")
+	serverCmd.PersistentFlags().StringVarP(&tcpport, "tcpport", "t", "8082", "监听tcp端口号")
+	serverCmd.PersistentFlags().BoolVarP(&daemon, "daemon", "d", false, "是否为守护进程模式")
+}
+func run() {
+	if daemon == true {
+		if os.Getppid() != 1 {
+			// 将命令行参数中执行文件路径转换成可用路径
+			filePath, _ := filepath.Abs(os.Args[0])
+			cmd := exec.Command(filePath, os.Args[1:]...)
+			// 将其他命令传入生成出的进程
+			cmd.Stdin = os.Stdin // 给新进程设置文件描述符,可以重定向到文件中
+			cmd.Stdout = os.Stdout
+			cmd.Stderr = os.Stderr
+			cmd.Start() // 开始执行新进程,不等待新进程退出
+			os.Exit(0)
+		}
+	}
+
+	baseServer := addr + ":" + port
+	//tcpBaseServer := "0.0.0.0:"+tcpport
+	log.Println("start server...\r\ngo:http://" + baseServer)
+	gin.SetMode(gin.ReleaseMode)
+	engine := gin.Default()
+	engine.Use(cors.Default())
+	engine.LoadHTMLGlob("static/html/*")
+	engine.Static("/static", "./static")
+
+	//记录日志
+	engine.Use(tools.LoggerToFile())
+	router.InitViewRouter(engine)
+	router.InitApiRouter(engine)
+
+	//文档服务
+	docs.SwaggerInfo.Title = "接口文档"
+	docs.SwaggerInfo.Description = "go-fly即时通讯web客服管理系统 , 测试账户:kefu2 测试密码:123 类型:kefu"
+	docs.SwaggerInfo.Version = "0.0.7"
+	//docs.SwaggerInfo.Host = "127.0.0.1:"+port
+	docs.SwaggerInfo.Host = "gofly.sopans.com"
+	docs.SwaggerInfo.BasePath = "/"
+	docs.SwaggerInfo.Schemes = []string{"https"}
+	engine.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
+
+	//logFile, _ := os.OpenFile("./fatal.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0660)
+	//tools.RedirectStderr(logFile)
+
+	//tcp服务
+	//go controller.NewTcpServer(tcpBaseServer)
+	engine.Run(baseServer)
+}

+ 15 - 0
cmd/version.go

@@ -0,0 +1,15 @@
+package cmd
+
+import (
+	"fmt"
+	"github.com/spf13/cobra"
+	"github.com/wenstudio/gofly/config"
+)
+
+var versionCmd = &cobra.Command{
+	Use:   "version",
+	Short: "example:gofly version",
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Println("gofly " + config.Version)
+	},
+}

BIN
config/city.free.ipdb


+ 175 - 0
config/config.go

@@ -0,0 +1,175 @@
+package config
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/wenstudio/gofly/tools"
+	"io/ioutil"
+	"os"
+)
+
+var (
+	PageSize        uint = 10
+	VisitorPageSize uint = 8
+	Version              = "0.1.2"
+	GoflyConfig     *Config
+	QiniuConfig     *Qiniu
+)
+
+const Dir = "config/"
+const AccountConf = Dir + "account.json"
+const MysqlConf = Dir + "mysql.json"
+const QiniuFile = Dir + "qiniu.json"
+const MailConf = Dir + "mail.json"
+const MainConf = Dir + "config.json"
+
+func init() {
+	//配置文件
+	GoflyConfig = CreateConfig()
+	QiniuConfig = CreateQiniu(QiniuFile)
+}
+
+type Mysql struct {
+	Server   string
+	Port     string
+	Database string
+	Username string
+	Password string
+}
+
+type Qiniu struct {
+	Access string
+	Secret string
+	Bucket string
+	Zone   string
+	Domain string
+}
+
+type MailServer struct {
+	Server, Email, Password string
+}
+type Config struct {
+	Upload            string
+	NoticeServerJiang bool
+}
+
+func CreateConfig() *Config {
+	var configObj Config
+	c := &Config{
+		Upload:            "static/upload/",
+		NoticeServerJiang: false,
+	}
+	isExist, _ := tools.IsFileExist(MainConf)
+	if !isExist {
+		return c
+	}
+	info, err := ioutil.ReadFile(MainConf)
+	if err != nil {
+		return c
+	}
+	err = json.Unmarshal(info, &configObj)
+	return &configObj
+}
+func CreateMailServer() *MailServer {
+	var imap MailServer
+	isExist, _ := tools.IsFileExist(MailConf)
+	if !isExist {
+		return &imap
+	}
+	info, err := ioutil.ReadFile(MailConf)
+	if err != nil {
+		return &imap
+	}
+
+	err = json.Unmarshal(info, &imap)
+	return &imap
+}
+func CreateMysql(f string) *Mysql {
+	var mysql Mysql
+	isExist, _ := tools.IsFileExist(f)
+	if !isExist {
+		return &mysql
+	}
+	info, err := ioutil.ReadFile(f)
+	if err != nil {
+		return &mysql
+	}
+
+	err = json.Unmarshal(info, &mysql)
+	return &mysql
+}
+
+func CreateQiniu(f string) *Qiniu {
+	var q Qiniu
+	isExist, _ := tools.IsFileExist(f)
+	if !isExist {
+		fmt.Println(f)
+		return &q
+	}
+	info, err := ioutil.ReadFile(f)
+	if err != nil {
+		fmt.Println("bb")
+		return &q
+	}
+
+	_ = json.Unmarshal(info, &q)
+	return &q
+}
+
+func GetMysql() map[string]string {
+	var mysql map[string]string
+	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
+}
+func GetAccount() map[string]string {
+	var account map[string]string
+	isExist, _ := tools.IsFileExist(AccountConf)
+	if !isExist {
+		return account
+	}
+	info, err := ioutil.ReadFile(AccountConf)
+	if err != nil {
+		return account
+	}
+
+	err = json.Unmarshal(info, &account)
+	return account
+}
+func GetUserInfo(uid string) map[string]string {
+	var userInfo map[string]string
+	userFile := Dir + "sess_" + uid + ".json"
+	isExist, _ := tools.IsFileExist(userFile)
+	if !isExist {
+		return userInfo
+	}
+	info, err := ioutil.ReadFile(userFile)
+	if err != nil {
+		return userInfo
+	}
+
+	err = json.Unmarshal(info, &userInfo)
+	return userInfo
+}
+func SetUserInfo(uid string, info map[string]string) {
+	userFile := Dir + "sess_" + uid + ".json"
+	isExist, _ := tools.IsFileExist(Dir)
+	if !isExist {
+		os.Mkdir(Dir, os.ModePerm)
+	}
+	file, _ := os.OpenFile(userFile, os.O_RDWR|os.O_CREATE, os.ModePerm)
+	str := "{\r\n"
+	for k, v := range info {
+		str += fmt.Sprintf(`"%s":"%s",`, k, v)
+	}
+	str += fmt.Sprintf(`"session_id":"%s"%s}`, uid, "\r\n")
+	file.WriteString(str)
+}

+ 4 - 0
config/config.json

@@ -0,0 +1,4 @@
+{
+  "Upload":"static/upload/",
+  "NoticeServerJiang": false
+}

File diff suppressed because it is too large
+ 132 - 0
config/go-fly.sql


+ 47 - 0
config/language.go

@@ -0,0 +1,47 @@
+package config
+
+type Language struct {
+	WebCopyRight                                                             string
+	MainIntro                                                                string
+	Send                                                                     string
+	Notice, Maintech, NowAsk, LaterAsk                                       string
+	IndexSubIntro, IndexVisitors, IndexAgent, IndexDocument, IndexOnlineChat string
+}
+
+func CreateLanguage(lang string) *Language {
+	var language *Language
+
+	if lang == "en" {
+		language = &Language{
+			WebCopyRight:    "twong",
+			MainIntro:       "twong online customer chat system",
+			IndexSubIntro:   "for effectiveness, for customers",
+			IndexDocument:   "API Documents",
+			IndexVisitors:   "Visitors Here",
+			IndexAgent:      "Agents Here",
+			IndexOnlineChat: "Let’s chat. - We're online",
+			Send:            "Send",
+			Notice:          "Hello and welcome to twong - how can we help?",
+			Maintech:        "Main technical architecture",
+			NowAsk:          "Start Chat",
+			LaterAsk:        "Chat Later",
+		}
+	}
+	if lang == "cn" {
+		language = &Language{
+			WebCopyRight:    "美天旺版权所有",
+			MainIntro:       "美天旺在线客服系统",
+			IndexSubIntro:   "专注效率,服务客服",
+			IndexVisitors:   "访客入口",
+			IndexAgent:      "客服入口",
+			IndexDocument:   "文档",
+			IndexOnlineChat: "点击交流",
+			Send:            "发送",
+			Notice:          "欢迎您使用客服系统!点击测试网页版客服",
+			Maintech:        "主要技术架构",
+			NowAsk:          "现在咨询",
+			LaterAsk:        "稍后再说",
+		}
+	}
+	return language
+}

+ 20 - 0
config/language.json

@@ -0,0 +1,20 @@
+{
+  "En":{
+    "WebCopyRight": "TaoShihan",
+    "MainIntro": "Simple and Powerful Go language online customer chat system",
+    "IndexSubIntro": "GO-FLY, a Vue 2.0-based online customer service instant messaging system for PHP engineers and Golang engineers",
+    "IndexDocument":"API Documents",
+    "IndexVisitors":"Visitors Here",
+    "IndexAgent":"Agents Here",
+    "IndexOnlineChat":"Let’s chat. - We're online"
+  },
+  "Cn":{
+    "WebCopyRight": "陶士涵的菜地版权所有",
+    "MainIntro":"极简强大的Go语言在线客服系统",
+    "IndexSubIntro":"GO-FLY,一套为PHP工程师、Golang工程师准备的基于 Vue 2.0的在线客服即时通讯系统",
+    "IndexVisitors":"访客入口",
+    "IndexAgent":"客服入口",
+    "IndexDocument":"接口文档",
+    "IndexOnlineChat":"在线咨询"
+  }
+}

+ 11 - 0
config/model.conf

@@ -0,0 +1,11 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

+ 7 - 0
config/mysql.json

@@ -0,0 +1,7 @@
+{
+	"Server":"192.168.3.187",
+	"Port":"58888",
+	"Database":"gofly",
+	"Username":"gofly",
+	"Password":"gofly"
+}

+ 23 - 0
config/nginx.conf

@@ -0,0 +1,23 @@
+
+upstream twong_cs{
+    server 127.0.0.1:30001;
+}
+
+server {
+    listen 443;
+    ssl on;
+    ssl_certificate /etc/letsencrypt/live/twongkefu.shotshock.shop/fullchain.pem;
+    ssl_certificate_key /etc/letsencrypt/live/twongkefu.shotshock.shop/privkey.pem;
+    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+    server_tokens off;
+    server_name twongkefu.shotshock.shop;
+
+    location / {
+        proxy_pass http://twong_cs;
+        proxy_http_version 1.1;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+        #proxy_set_header Origin "";
+    }
+}

+ 4 - 0
config/policy.csv

@@ -0,0 +1,4 @@
+p, role_1, /mysql, GET
+p, role_1, /mysql, POST
+p, role_1, /kefuinfo,POST
+p, role_1, /kefuinfo,DELETE

+ 7 - 0
config/qiniu.json

@@ -0,0 +1,7 @@
+{
+  "access": "SneSBtnWLdStBhCx0O_QogNkXoRlKNOiv1--XMBB",
+  "secret": "GXMg-ENcp2UKYQWdeaf43tk_06NnMoA4OVFxdkYw",
+  "bucket": "twongd",
+  "zone": "huanan",
+  "domain": "http://twongpicd.shotshock.shop/"
+}

+ 7 - 0
config/twong.json

@@ -0,0 +1,7 @@
+{
+	"Server":"192.168.3.187",
+	"Port":"58888",
+	"Database":"twong",
+	"Username":"twong",
+	"Password":"twong"
+}

+ 44 - 0
controller/about.go

@@ -0,0 +1,44 @@
+package controller
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/wenstudio/gofly/models"
+)
+
+func GetAbout(c *gin.Context) {
+	page := c.Query("page")
+	if page == "" {
+		page = "index"
+	}
+	about := models.FindAboutByPage(page)
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": about,
+	})
+}
+func PostAbout(c *gin.Context) {
+	title_cn := c.PostForm("title_cn")
+	title_en := c.PostForm("title_en")
+	keywords_cn := c.PostForm("keywords_cn")
+	keywords_en := c.PostForm("keywords_en")
+	desc_cn := c.PostForm("desc_cn")
+	desc_en := c.PostForm("desc_en")
+	css_js := c.PostForm("css_js")
+	html_cn := c.PostForm("html_cn")
+	html_en := c.PostForm("html_en")
+	if title_cn == "" || title_en == "" || html_cn == "" || html_en == "" {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "error",
+		})
+		return
+	}
+	models.UpdateAbout("index", title_cn, title_en, keywords_cn, keywords_en, desc_cn, desc_en, css_js, html_cn, html_en)
+
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": "",
+	})
+}

+ 63 - 0
controller/auth.go

@@ -0,0 +1,63 @@
+package controller
+
+import (
+	"github.com/wenstudio/gofly/config"
+	"github.com/wenstudio/gofly/models"
+	"github.com/wenstudio/gofly/tools"
+)
+
+func CheckPass(username string, password string) string {
+	account := config.GetAccount()
+	if account == nil {
+		account = make(map[string]string)
+	}
+	if account["Username"] == "" && account["Password"] == "" {
+		account["Username"] = "admin"
+		account["Password"] = "admin123"
+	}
+	if username == account["Username"] && password == account["Password"] {
+
+		sessionId := tools.Md5(username)
+		info := make(map[string]string)
+		info["username"] = username
+		config.SetUserInfo(sessionId, info)
+		return sessionId
+	}
+	return ""
+}
+func CheckKefuPass(username string, password string) (models.User, models.User_role, bool) {
+	info := models.FindUser(username)
+	var uRole models.User_role
+	if info.Name == "" || info.Password != tools.Md5(password) {
+		return info, uRole, false
+	}
+	uRole = models.FindRoleByUserId(info.ID)
+
+	return info, uRole, true
+}
+func AuthLocal(username string, password string) string {
+	account := config.GetAccount()
+	if account == nil {
+		account = make(map[string]string)
+	}
+	if account["Username"] == "" && account["Password"] == "" {
+		account["Username"] = "admin"
+		account["Password"] = "admin123"
+	}
+	if username == account["Username"] && password == account["Password"] {
+
+		sessionId := tools.Md5(username)
+		info := make(map[string]string)
+		info["username"] = username
+		config.SetUserInfo(sessionId, info)
+		return sessionId
+	}
+	return ""
+}
+
+//验证是否已经登录
+func AuthCheck(uid string) map[string]string {
+	info := config.GetUserInfo(uid)
+
+	return info
+}

+ 345 - 0
controller/chat.go

@@ -0,0 +1,345 @@
+package controller
+
+import (
+	"encoding/json"
+	"github.com/gin-gonic/gin"
+	"github.com/gorilla/websocket"
+	"github.com/wenstudio/gofly/models"
+	"github.com/wenstudio/gofly/ws"
+	"log"
+	"net/http"
+	"sort"
+	"time"
+)
+
+type vistor struct {
+	conn   *websocket.Conn
+	name   string
+	id     string
+	avator string
+	to_id  string
+}
+type Message struct {
+	conn        *websocket.Conn
+	c           *gin.Context
+	content     []byte
+	messageType int
+}
+
+var clientList = make(map[string]*vistor)
+var kefuList = make(map[string][]*websocket.Conn)
+var message = make(chan *Message)
+
+type TypeMessage struct {
+	Type interface{} `json:"type"`
+	Data interface{} `json:"data"`
+}
+type ClientMessage struct {
+	Name      string `json:"name"`
+	Avator    string `json:"avator"`
+	Id        string `json:"id"`
+	VisitorId string `json:"visitor_id"`
+	Group     string `json:"group"`
+	Time      string `json:"time"`
+	ToId      string `json:"to_id"`
+	Content   string `json:"content"`
+	City      string `json:"city"`
+	ClientIp  string `json:"client_ip"`
+	Refer     string `json:"refer"`
+}
+
+//定时检测客户端是否在线
+func init() {
+	upgrader = websocket.Upgrader{
+		ReadBufferSize:  1024,
+		WriteBufferSize: 1024,
+		CheckOrigin: func(r *http.Request) bool {
+			return true
+		},
+	}
+	go UpdateVisitorStatusCron()
+	go singleBroadcaster()
+	//go sendPingOnlineUsers()
+	//sendPingToClient()
+}
+
+func NewChatServer(c *gin.Context) {
+	conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
+	if err != nil {
+		log.Print("upgrade:", err)
+		return
+	}
+	for {
+		//接受消息
+		var receive []byte
+		var recevString string
+		messageType, receive, err := conn.ReadMessage()
+		if err != nil {
+			for uid, visitor := range clientList {
+				if visitor.conn == conn {
+					log.Println("删除用户", uid)
+					delete(clientList, uid)
+					models.UpdateVisitorStatus(uid, 0)
+					userInfo := make(map[string]string)
+					userInfo["uid"] = uid
+					userInfo["name"] = visitor.name
+					msg := TypeMessage{
+						Type: "userOffline",
+						Data: userInfo,
+					}
+					str, _ := json.Marshal(msg)
+					kefuConns := kefuList[visitor.to_id]
+					if kefuConns != nil {
+						for _, kefuConn := range kefuConns {
+							kefuConn.WriteMessage(websocket.TextMessage, str)
+						}
+					}
+					//新版
+					mKefuConns := ws.KefuList[visitor.to_id]
+					if mKefuConns != nil {
+						for _, kefu := range mKefuConns {
+							kefu.Conn.WriteMessage(websocket.TextMessage, str)
+						}
+					}
+					sendPingOnlineUsers()
+				}
+			}
+			log.Println(err)
+			return
+		}
+		recevString = string(receive)
+		log.Println("客户端:", recevString)
+		message <- &Message{
+			conn:        conn,
+			content:     receive,
+			c:           c,
+			messageType: messageType,
+		}
+	}
+}
+
+//发送给客户客服上线
+func SendKefuOnline(clientMsg ClientMessage, conn *websocket.Conn) {
+	sendMsg := TypeMessage{
+		Type: "kfOnline",
+		Data: ClientMessage{
+			Name:    clientMsg.Name,
+			Avator:  clientMsg.Avator,
+			Id:      clientMsg.Id,
+			Group:   clientMsg.Group,
+			Time:    time.Now().Format("2006-01-02 15:04:05"),
+			Content: "客服上线",
+		},
+	}
+	jsonStrByte, _ := json.Marshal(sendMsg)
+	conn.WriteMessage(websocket.TextMessage, jsonStrByte)
+}
+
+//发送通知
+func SendNotice(msg string, conn *websocket.Conn) {
+	sendMsg := TypeMessage{
+		Type: "notice",
+		Data: msg,
+	}
+	jsonStrByte, _ := json.Marshal(sendMsg)
+	conn.WriteMessage(websocket.TextMessage, jsonStrByte)
+}
+
+//定时给客户端发送消息判断客户端是否在线
+func sendPingToClient() {
+	msg := TypeMessage{
+		Type: "ping",
+	}
+	go func() {
+		for {
+			str, _ := json.Marshal(msg)
+			for uid, user := range clientList {
+				err := user.conn.WriteMessage(websocket.TextMessage, str)
+				if err != nil {
+					delete(clientList, uid)
+					models.UpdateVisitorStatus(uid, 0)
+				}
+			}
+			for kefuId, kfConns := range kefuList {
+
+				var newkfConns = make([]*websocket.Conn, 0)
+				for _, kefuConn := range kfConns {
+					if kefuConn == nil {
+						continue
+					}
+					err := kefuConn.WriteMessage(websocket.TextMessage, str)
+					if err == nil {
+						newkfConns = append(newkfConns, kefuConn)
+					}
+				}
+				if newkfConns == nil {
+					delete(kefuList, kefuId)
+				} else {
+					kefuList[kefuId] = newkfConns
+				}
+			}
+			time.Sleep(15 * time.Second)
+		}
+
+	}()
+}
+
+//定时给更新数据库状态
+func UpdateVisitorStatusCron() {
+	for {
+		visitors := models.FindVisitorsOnline()
+		for _, visitor := range visitors {
+			if visitor.VisitorId == "" {
+				continue
+			}
+			_, ok := clientList[visitor.VisitorId]
+			if !ok {
+				models.UpdateVisitorStatus(visitor.VisitorId, 0)
+			}
+		}
+		time.Sleep(60 * time.Second)
+	}
+}
+
+//定时推送当前在线用户
+func sendPingOnlineUsers() {
+	var visitorIds []string
+	for visitorId, _ := range clientList {
+		visitorIds = append(visitorIds, visitorId)
+	}
+	sort.Strings(visitorIds)
+
+	for kefuId, kfConns := range kefuList {
+
+		result := make([]map[string]string, 0)
+		for _, visitorId := range visitorIds {
+			user := clientList[visitorId]
+			userInfo := make(map[string]string)
+			userInfo["uid"] = user.id
+			userInfo["username"] = user.name
+			userInfo["avator"] = user.avator
+			if user.to_id == kefuId {
+				result = append(result, userInfo)
+			}
+		}
+		msg := TypeMessage{
+			Type: "allUsers",
+			Data: result,
+		}
+		str, _ := json.Marshal(msg)
+		var newkfConns = make([]*websocket.Conn, 0)
+		for _, kefuConn := range kfConns {
+			err := kefuConn.WriteMessage(websocket.TextMessage, str)
+			if err == nil {
+				newkfConns = append(newkfConns, kefuConn)
+			}
+		}
+		if len(newkfConns) == 0 {
+			delete(kefuList, kefuId)
+		} else {
+			kefuList[kefuId] = newkfConns
+		}
+	}
+}
+
+//后端广播发送消息
+func singleBroadcaster() {
+	for {
+		message := <-message
+		//log.Println("debug:",message)
+
+		var typeMsg TypeMessage
+		var clientMsg ClientMessage
+		json.Unmarshal(message.content, &typeMsg)
+		conn := message.conn
+		if typeMsg.Type == nil || typeMsg.Data == nil {
+			continue
+		}
+		msgType := typeMsg.Type.(string)
+		msgData, _ := json.Marshal(typeMsg.Data)
+		switch msgType {
+		//用户上线
+		case "userInit":
+			json.Unmarshal(msgData, &clientMsg)
+			vistorInfo := models.FindVisitorByVistorId(clientMsg.VisitorId)
+			if vistorInfo.VisitorId == "" {
+				SendNotice("访客数据不存在", conn)
+				continue
+			}
+			//用户id对应的连接
+			user := &vistor{
+				conn:   conn,
+				name:   clientMsg.Name,
+				avator: clientMsg.Avator,
+				id:     clientMsg.VisitorId,
+				to_id:  clientMsg.ToId,
+			}
+			clientList[clientMsg.VisitorId] = user
+			//插入数据表
+			models.UpdateVisitor(clientMsg.VisitorId, 1, clientMsg.ClientIp, message.c.ClientIP(), clientMsg.Refer)
+			//models.CreateVisitor(clientMsg.Name,clientMsg.Avator,message.c.ClientIP(),clientMsg.ToId,clientMsg.VisitorId,clientMsg.Refer,clientMsg.City,clientMsg.ClientIp)
+			userInfo := make(map[string]string)
+			userInfo["uid"] = user.id
+			userInfo["username"] = user.name
+			userInfo["avator"] = user.avator
+			msg := TypeMessage{
+				Type: "userOnline",
+				Data: userInfo,
+			}
+			str, _ := json.Marshal(msg)
+
+			//新版
+			mKefuConns := ws.KefuList[user.to_id]
+			if mKefuConns != nil {
+				for _, kefu := range mKefuConns {
+					kefu.Conn.WriteMessage(websocket.TextMessage, str)
+				}
+			}
+
+			//兼容旧版
+			kefuConns := kefuList[user.to_id]
+			if kefuConns != nil {
+				for k, kefuConn := range kefuConns {
+					log.Println(k, "xxxxxxxx")
+					kefuConn.WriteMessage(websocket.TextMessage, str)
+				}
+			}
+
+			//客户上线发微信通知
+			go SendServerJiang(userInfo["username"])
+			sendPingOnlineUsers()
+		//客服上线
+		case "kfOnline":
+			json.Unmarshal(msgData, &clientMsg)
+			//客服id对应的连接
+			var newKefuConns = []*websocket.Conn{conn}
+			kefuConns := kefuList[clientMsg.Id]
+			if kefuConns != nil {
+				newKefuConns = append(newKefuConns, kefuConns...)
+			}
+			log.Println(newKefuConns)
+			kefuList[clientMsg.Id] = newKefuConns
+			//发送给客户
+			if len(clientList) == 0 {
+				continue
+			}
+			sendPingOnlineUsers()
+		//客服接手
+		case "kfConnect":
+			json.Unmarshal(msgData, &clientMsg)
+			visitor, ok := clientList[clientMsg.ToId]
+			if visitor == nil || !ok {
+				continue
+			}
+			SendKefuOnline(clientMsg, visitor.conn)
+		//心跳
+		case "ping":
+			msg := TypeMessage{
+				Type: "pong",
+			}
+			str, _ := json.Marshal(msg)
+			conn.WriteMessage(websocket.TextMessage, str)
+		}
+
+	}
+}

+ 262 - 0
controller/folder.go

@@ -0,0 +1,262 @@
+package controller
+
+import (
+	"encoding/json"
+	"github.com/gin-gonic/gin"
+	"github.com/wenstudio/gofly/config"
+	"github.com/wenstudio/gofly/tmpl"
+	"github.com/wenstudio/gofly/tools"
+	"io/ioutil"
+	"net/http"
+	"strconv"
+	"sync"
+)
+
+const PageSize = 20
+
+func GetFolders(c *gin.Context) {
+	fid := c.Query("fid")
+	currentPage, _ := strconv.Atoi(c.Query("page"))
+	if fid == "" {
+		fid = "INBOX"
+	}
+	if currentPage == 0 {
+		currentPage = 1
+	}
+
+	mailServer := config.CreateMailServer()
+
+	var wg sync.WaitGroup
+	wg.Add(2)
+	result := make(map[string]interface{})
+	go func() {
+		defer wg.Done()
+		folders := tools.GetFolders(mailServer.Server, mailServer.Email, mailServer.Password, fid)
+		result["folders"] = folders
+		result["total"] = folders[fid]
+	}()
+	go func() {
+		defer wg.Done()
+		mails := tools.GetFolderMail(mailServer.Server, mailServer.Email, mailServer.Password, fid, currentPage, PageSize)
+		result["mails"] = mails
+	}()
+	wg.Wait()
+	result["pagesize"] = PageSize
+	result["fid"] = fid
+
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": result,
+	})
+}
+func GetFolderList(c *gin.Context) {
+	fid := c.Query("fid")
+	if fid == "" {
+		fid = "INBOX"
+	}
+
+	mailServer := config.CreateMailServer()
+
+	result := make(map[string]interface{})
+	folders := tools.GetFolders(mailServer.Server, mailServer.Email, mailServer.Password, fid)
+	result["folders"] = folders
+	result["total"] = folders[fid]
+	result["fid"] = fid
+
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": result,
+	})
+}
+
+//输出列表
+func ActionFolder(w http.ResponseWriter, r *http.Request) {
+	fid := tools.GetUrlArg(r, "fid")
+	currentPage, _ := strconv.Atoi(tools.GetUrlArg(r, "page"))
+	if fid == "" {
+		fid = "INBOX"
+	}
+	if currentPage == 0 {
+		currentPage = 1
+	}
+	render := tmpl.NewFolderHtml(w)
+	render.CurrentPage = currentPage
+	render.Fid = fid
+	render.Display("list", render)
+}
+
+//写信界面
+func ActionWrite(w http.ResponseWriter, r *http.Request) {
+	render := tmpl.NewRender(w)
+	render.SetLeft("mail_left")
+	render.Display("write", nil)
+}
+
+//读信界面
+func ActionDetail(w http.ResponseWriter, r *http.Request) {
+	fid := tools.GetUrlArg(r, "fid")
+	id, _ := strconv.Atoi(tools.GetUrlArg(r, "id"))
+
+	render := tmpl.NewDetailHtml(w)
+	render.SetLeft("mail_left")
+	render.Fid = fid
+	render.Id = uint32(id)
+	render.Display("mail_detail", render)
+}
+
+//获取邮件夹接口
+func FolderDir(w http.ResponseWriter, r *http.Request) {
+	fid := tools.GetUrlArg(r, "fid")
+
+	if fid == "" {
+		fid = "INBOX"
+	}
+
+	mailServer := tools.GetMailServerFromCookie(r)
+	w.Header().Set("content-type", "text/json;charset=utf-8;")
+
+	if mailServer == nil {
+		msg, _ := json.Marshal(tools.JsonResult{Code: 400, Msg: "验证失败"})
+		w.Write(msg)
+		return
+	}
+	result := make(map[string]interface{})
+	folders := tools.GetFolders(mailServer.Server, mailServer.Email, mailServer.Password, fid)
+	result["folders"] = folders
+	result["total"] = folders[fid]
+	result["fid"] = fid
+	msg, _ := json.Marshal(tools.JsonListResult{
+		JsonResult: tools.JsonResult{Code: 200, Msg: "获取成功"},
+		Result:     result,
+	})
+	w.Write(msg)
+}
+
+//邮件夹接口
+func FoldersList(w http.ResponseWriter, r *http.Request) {
+	fid := tools.GetUrlArg(r, "fid")
+	currentPage, _ := strconv.Atoi(tools.GetUrlArg(r, "page"))
+
+	if fid == "" {
+		fid = "INBOX"
+	}
+	if currentPage == 0 {
+		currentPage = 1
+	}
+
+	mailServer := tools.GetMailServerFromCookie(r)
+	w.Header().Set("content-type", "text/json;charset=utf-8;")
+
+	if mailServer == nil {
+		msg, _ := json.Marshal(tools.JsonResult{Code: 400, Msg: "验证失败"})
+		w.Write(msg)
+		return
+	}
+
+	var wg sync.WaitGroup
+	wg.Add(2)
+	result := make(map[string]interface{})
+	go func() {
+		defer wg.Done()
+		folders := tools.GetFolders(mailServer.Server, mailServer.Email, mailServer.Password, fid)
+		result["folders"] = folders
+		result["total"] = folders[fid]
+	}()
+	go func() {
+		defer wg.Done()
+		mails := tools.GetFolderMail(mailServer.Server, mailServer.Email, mailServer.Password, fid, currentPage, PageSize)
+		result["mails"] = mails
+	}()
+	wg.Wait()
+	result["pagesize"] = PageSize
+	result["fid"] = fid
+
+	msg, _ := json.Marshal(tools.JsonListResult{
+		JsonResult: tools.JsonResult{Code: 200, Msg: "获取成功"},
+		Result:     result,
+	})
+	w.Write(msg)
+}
+
+//邮件接口
+func FolderMail(w http.ResponseWriter, r *http.Request) {
+	fid := tools.GetUrlArg(r, "fid")
+	id, _ := strconv.Atoi(tools.GetUrlArg(r, "id"))
+	mailServer := tools.GetMailServerFromCookie(r)
+	w.Header().Set("content-type", "text/json;charset=utf-8;")
+
+	if mailServer == nil {
+		msg, _ := json.Marshal(tools.JsonResult{Code: 400, Msg: "验证失败"})
+		w.Write(msg)
+		return
+	}
+	var wg sync.WaitGroup
+	result := make(map[string]interface{})
+	wg.Add(2)
+	go func() {
+		defer wg.Done()
+		folders := tools.GetFolders(mailServer.Server, mailServer.Email, mailServer.Password, fid)
+		result["folders"] = folders
+		result["total"] = folders[fid]
+	}()
+	go func() {
+		defer wg.Done()
+		mail := tools.GetMessage(mailServer.Server, mailServer.Email, mailServer.Password, fid, uint32(id))
+		result["from"] = mail.From
+		result["to"] = mail.To
+		result["subject"] = mail.Subject
+		result["date"] = mail.Date
+		result["html"] = mail.Body
+	}()
+	wg.Wait()
+	result["fid"] = fid
+
+	msg, _ := json.Marshal(tools.JsonListResult{
+		JsonResult: tools.JsonResult{Code: 200, Msg: "获取成功"},
+		Result:     result,
+	})
+	w.Write(msg)
+}
+
+//发送邮件接口
+func FolderSend(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("content-type", "text/json;charset=utf-8;")
+	mailServer := tools.GetMailServerFromCookie(r)
+
+	if mailServer == nil {
+		msg, _ := json.Marshal(tools.JsonResult{Code: 400, Msg: "验证失败"})
+		w.Write(msg)
+		return
+	}
+
+	bodyBytes, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		msg, _ := json.Marshal(tools.JsonResult{Code: 400, Msg: "操作失败," + err.Error()})
+		w.Write(msg)
+		return
+	}
+	var sendData tools.SmtpBody
+	err = json.Unmarshal(bodyBytes, &sendData)
+	if err != nil {
+		msg, _ := json.Marshal(tools.JsonResult{Code: 400, Msg: "操作失败," + err.Error()})
+		w.Write(msg)
+		return
+	}
+
+	smtpServer := sendData.Smtp
+	smtpFrom := mailServer.Email
+	smtpTo := sendData.To
+	smtpBody := sendData.Body
+	smtpPass := mailServer.Password
+	smtpSubject := sendData.Subject
+	err = tools.Send(smtpServer, smtpFrom, smtpPass, smtpTo, smtpSubject, smtpBody)
+	if err != nil {
+		msg, _ := json.Marshal(tools.JsonResult{Code: 400, Msg: err.Error()})
+		w.Write(msg)
+		return
+	}
+	msg, _ := json.Marshal(tools.JsonResult{Code: 200, Msg: "发送成功!"})
+	w.Write(msg)
+}

+ 14 - 0
controller/index.go

@@ -0,0 +1,14 @@
+package controller
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/wenstudio/gofly/models"
+)
+
+func Index(c *gin.Context) {
+	jump := models.FindConfig("JumpLang")
+	if jump != "cn" {
+		jump = "en"
+	}
+	c.Redirect(302, "/index_"+jump)
+}

+ 57 - 0
controller/ip.go

@@ -0,0 +1,57 @@
+package controller
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/wenstudio/gofly/config"
+	"github.com/wenstudio/gofly/models"
+	"strconv"
+)
+
+func PostIpblack(c *gin.Context) {
+	ip := c.PostForm("ip")
+	if ip == "" {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "请输入IP!",
+		})
+		return
+	}
+	kefuId, _ := c.Get("kefu_name")
+	models.CreateIpblack(ip, kefuId.(string))
+	c.JSON(200, gin.H{
+		"code": 200,
+		"msg":  "添加黑名单成功!",
+	})
+}
+func DelIpblack(c *gin.Context) {
+	ip := c.Query("ip")
+	if ip == "" {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "请输入IP!",
+		})
+		return
+	}
+	models.DeleteIpblackByIp(ip)
+	c.JSON(200, gin.H{
+		"code": 200,
+		"msg":  "删除黑名单成功!",
+	})
+}
+func GetIpblacks(c *gin.Context) {
+	page, _ := strconv.Atoi(c.Query("page"))
+	if page == 0 {
+		page = 1
+	}
+	count := models.CountIps(nil, nil)
+	list := models.FindIps(nil, nil, uint(page), config.VisitorPageSize)
+	c.JSON(200, gin.H{
+		"code": 200,
+		"msg":  "ok",
+		"result": gin.H{
+			"list":     list,
+			"count":    count,
+			"pagesize": config.PageSize,
+		},
+	})
+}

+ 132 - 0
controller/kefu.go

@@ -0,0 +1,132 @@
+package controller
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/wenstudio/gofly/models"
+	"github.com/wenstudio/gofly/tools"
+	"strconv"
+)
+
+func GetKefuInfo(c *gin.Context) {
+	kefuId, _ := c.Get("kefu_id")
+	user := models.FindUserById(kefuId)
+	info := make(map[string]interface{})
+	info["name"] = user.Nickname
+	info["id"] = user.Name
+	info["avator"] = user.Avator
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": info,
+	})
+}
+func GetKefuInfoAll(c *gin.Context) {
+	id, _ := c.Get("kefu_id")
+	userinfo := models.FindUserRole("user.avator,user.name,user.id, role.name role_name", id)
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "验证成功",
+		"result": userinfo,
+	})
+}
+func GetKefuInfoSetting(c *gin.Context) {
+	kefuId := c.Query("kefu_id")
+	user := models.FindUserById(kefuId)
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": user,
+	})
+}
+func PostKefuInfo(c *gin.Context) {
+	id := c.PostForm("id")
+	name := c.PostForm("name")
+	password := c.PostForm("password")
+	avator := c.PostForm("avator")
+	nickname := c.PostForm("nickname")
+	roleId := c.PostForm("role_id")
+	senabled := c.PostForm("enabled")
+
+	enabled64, err := strconv.ParseUint(senabled, 10, 32)
+	if err != nil {
+		enabled64 = 0
+	}
+	enabled := uint(enabled64)
+
+	if roleId == "" {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "请选择角色!",
+		})
+		return
+	}
+	if len(password) <= 0 {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "请设置密码",
+		})
+		return
+	}
+	//插入新用户
+	if id == "" {
+		uid := models.CreateUser(name, tools.Md5(password), avator, nickname, enabled)
+		if uid == 0 {
+			c.JSON(200, gin.H{
+				"code":   400,
+				"msg":    "增加用户失败",
+				"result": "",
+			})
+			return
+		}
+		roleIdInt, _ := strconv.Atoi(roleId)
+		models.CreateUserRole(uid, uint(roleIdInt))
+	} else {
+		//更新用户
+		if password != "" {
+			password = tools.Md5(password)
+		}
+		models.UpdateUser(id, name, password, avator, nickname, enabled)
+		roleIdInt, _ := strconv.Atoi(roleId)
+		uid, _ := strconv.Atoi(id)
+		models.DeleteRoleByUserId(uid)
+		models.CreateUserRole(uint(uid), uint(roleIdInt))
+	}
+
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": "",
+	})
+}
+func GetKefuList(c *gin.Context) {
+	users := models.FindUsers()
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "获取成功",
+		"result": users,
+	})
+}
+func GetKefuListEnabled(c *gin.Context) {
+	users := models.FindUsers()
+	enabledUsers := []models.User{}
+	for _, user := range users {
+		if user.Enabled == 1 {
+			enabledUsers = append(enabledUsers, user)
+		}
+	}
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "获取成功",
+		"result": enabledUsers,
+	})
+}
+func DeleteKefuInfo(c *gin.Context) {
+	kefuId := c.Query("id")
+	models.DeleteUserById(kefuId)
+	models.DeleteRoleByUserId(kefuId)
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "删除成功",
+		"result": "",
+	})
+}

+ 54 - 0
controller/login.go

@@ -0,0 +1,54 @@
+package controller
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/wenstudio/gofly/tools"
+	"time"
+)
+
+// @Summary 登陆验证接口
+// @Produce  json
+// @Accept multipart/form-data
+// @Param username formData   string true "用户名"
+// @Param password formData   string true "密码"
+// @Param type formData   string true "类型"
+// @Success 200 {object} controller.Response
+// @Failure 200 {object} controller.Response
+// @Router /check [post]
+//验证接口
+func LoginCheckPass(c *gin.Context) {
+	password := c.PostForm("password")
+	username := c.PostForm("username")
+
+	info, uRole, ok := CheckKefuPass(username, password)
+	userinfo := make(map[string]interface{})
+	if !ok {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "验证失败",
+		})
+		return
+	}
+	userinfo["name"] = info.Name
+	userinfo["kefu_id"] = info.ID
+	userinfo["type"] = "kefu"
+	if uRole.RoleId != 0 {
+		userinfo["role_id"] = uRole.RoleId
+	} else {
+		userinfo["role_id"] = 2
+	}
+	userinfo["create_time"] = time.Now().Unix()
+
+	token, _ := tools.MakeToken(userinfo)
+	userinfo["ref_token"] = true
+	refToken, _ := tools.MakeToken(userinfo)
+	c.JSON(200, gin.H{
+		"code": 200,
+		"msg":  "验证成功,正在跳转",
+		"result": gin.H{
+			"token":       token,
+			"ref_token":   refToken,
+			"create_time": userinfo["create_time"],
+		},
+	})
+}

+ 47 - 0
controller/main.go

@@ -0,0 +1,47 @@
+package controller
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/wenstudio/gofly/models"
+	"github.com/wenstudio/gofly/tmpl"
+	"github.com/wenstudio/gofly/tools"
+	"net/http"
+)
+
+func ActionMain(w http.ResponseWriter, r *http.Request) {
+	sessionId := tools.GetCookie(r, "session_id")
+	info := AuthCheck(sessionId)
+	if len(info) == 0 {
+		http.Redirect(w, r, "/login", 302)
+		return
+	}
+	render := tmpl.NewRender(w)
+	render.Display("main", render)
+}
+func MainCheckAuth(c *gin.Context) {
+	id, _ := c.Get("kefu_id")
+	userinfo := models.FindUserRole("user.avator,user.name,user.id, role.name role_name", id)
+	c.JSON(200, gin.H{
+		"code": 200,
+		"msg":  "验证成功",
+		"result": gin.H{
+			"avator":    userinfo.Avator,
+			"name":      userinfo.Name,
+			"role_name": userinfo.RoleName,
+		},
+	})
+}
+func GetStatistics(c *gin.Context) {
+	visitors := models.CountVisitors()
+	message := models.CountMessage()
+	session := len(clientList)
+	c.JSON(200, gin.H{
+		"code": 200,
+		"msg":  "ok",
+		"result": gin.H{
+			"visitors": visitors,
+			"message":  message,
+			"session":  session,
+		},
+	})
+}

+ 337 - 0
controller/message.go

@@ -0,0 +1,337 @@
+package controller
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"github.com/gorilla/websocket"
+	"github.com/wenstudio/gofly/config"
+	"github.com/wenstudio/gofly/models"
+	"github.com/wenstudio/gofly/tools"
+	"github.com/wenstudio/gofly/tools/store"
+	"github.com/wenstudio/gofly/ws"
+	"os"
+	"path"
+	"strings"
+	"time"
+)
+
+// @Summary 发送消息接口
+// @Produce  json
+// @Accept multipart/form-data
+// @Param from_id formData   string true "来源uid"
+// @Param to_id formData   string true "目标uid"
+// @Param content formData   string true "内容"
+// @Param type formData   string true "类型|kefu,visitor"
+// @Success 200 {object} controller.Response
+// @Failure 200 {object} controller.Response
+// @Router /message [post]
+func SendMessage(c *gin.Context) {
+	fromId := c.PostForm("from_id")
+	toId := c.PostForm("to_id")
+	content := c.PostForm("content")
+	cType := c.PostForm("type")
+	if content == "" {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "内容不能为空",
+		})
+		return
+	}
+
+	var kefuInfo models.User
+	var vistorInfo models.Visitor
+	if cType == "kefu" {
+		kefuInfo = models.FindUser(fromId)
+		vistorInfo = models.FindVisitorByVistorId(toId)
+	} else if cType == "visitor" {
+		vistorInfo = models.FindVisitorByVistorId(fromId)
+		kefuInfo = models.FindUser(toId)
+	}
+
+	if kefuInfo.ID == 0 || vistorInfo.ID == 0 {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "用户不存在",
+		})
+		return
+	}
+	models.CreateMessage(kefuInfo.Name, vistorInfo.VisitorId, content, cType)
+
+	if cType == "kefu" {
+		guest, ok := clientList[vistorInfo.VisitorId]
+		if guest == nil || !ok {
+			c.JSON(200, gin.H{
+				"code": 200,
+				"msg":  "ok",
+			})
+			return
+		}
+		conn := guest.conn
+
+		msg := TypeMessage{
+			Type: "message",
+			Data: ClientMessage{
+				Name:    kefuInfo.Nickname,
+				Avator:  kefuInfo.Avator,
+				Id:      kefuInfo.Name,
+				Time:    time.Now().Format("2006-01-02 15:04:05"),
+				ToId:    vistorInfo.VisitorId,
+				Content: content,
+			},
+		}
+		str, _ := json.Marshal(msg)
+		PushServerTcp(str)
+		conn.WriteMessage(websocket.TextMessage, str)
+	}
+	if cType == "visitor" {
+		kefuConns, ok := kefuList[kefuInfo.Name]
+		if kefuConns == nil || !ok {
+			c.JSON(200, gin.H{
+				"code":   200,
+				"msg":    "ok",
+				"result": content,
+			})
+			return
+		}
+		msg := TypeMessage{
+			Type: "message",
+			Data: ClientMessage{
+				Avator:  vistorInfo.Avator,
+				Id:      vistorInfo.VisitorId,
+				Name:    vistorInfo.Name,
+				ToId:    kefuInfo.Name,
+				Content: content,
+				Time:    time.Now().Format("2006-01-02 15:04:05"),
+			},
+		}
+		str, _ := json.Marshal(msg)
+		PushServerTcp(str)
+		for _, kefuConn := range kefuConns {
+			kefuConn.WriteMessage(websocket.TextMessage, str)
+		}
+	}
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": content,
+	})
+}
+func SendMessageV2(c *gin.Context) {
+	fromId := c.PostForm("from_id")
+	toId := c.PostForm("to_id")
+	content := c.PostForm("content")
+	cType := c.PostForm("type")
+	if content == "" {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "内容不能为空",
+		})
+		return
+	}
+
+	var kefuInfo models.User
+	var vistorInfo models.Visitor
+	if cType == "kefu" {
+		kefuInfo = models.FindUser(fromId)
+		vistorInfo = models.FindVisitorByVistorId(toId)
+	} else if cType == "visitor" {
+		vistorInfo = models.FindVisitorByVistorId(fromId)
+		kefuInfo = models.FindUser(toId)
+	}
+
+	if kefuInfo.ID == 0 || vistorInfo.ID == 0 {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "用户不存在",
+		})
+		return
+	}
+	models.CreateMessage(kefuInfo.Name, vistorInfo.VisitorId, content, cType)
+
+	if cType == "kefu" {
+		guest, ok := clientList[vistorInfo.VisitorId]
+		if guest == nil || !ok {
+			c.JSON(200, gin.H{
+				"code": 200,
+				"msg":  "ok",
+			})
+			return
+		}
+		conn := guest.conn
+
+		msg := TypeMessage{
+			Type: "message",
+			Data: ClientMessage{
+				Name:    kefuInfo.Nickname,
+				Avator:  kefuInfo.Avator,
+				Id:      kefuInfo.Name,
+				Time:    time.Now().Format("2006-01-02 15:04:05"),
+				ToId:    vistorInfo.VisitorId,
+				Content: content,
+			},
+		}
+		str, _ := json.Marshal(msg)
+		conn.WriteMessage(websocket.TextMessage, str)
+	}
+	if cType == "visitor" {
+		kefuConns, ok := ws.KefuList[kefuInfo.Name]
+		if kefuConns == nil || !ok {
+			c.JSON(200, gin.H{
+				"code": 200,
+				"msg":  "ok",
+			})
+			return
+		}
+		msg := TypeMessage{
+			Type: "message",
+			Data: ClientMessage{
+				Avator:  vistorInfo.Avator,
+				Id:      vistorInfo.VisitorId,
+				Name:    vistorInfo.Name,
+				ToId:    kefuInfo.Name,
+				Content: content,
+				Time:    time.Now().Format("2006-01-02 15:04:05"),
+			},
+		}
+		str, _ := json.Marshal(msg)
+		for _, kefuConn := range kefuConns {
+			kefuConn.Conn.WriteMessage(websocket.TextMessage, str)
+		}
+	}
+	c.JSON(200, gin.H{
+		"code": 200,
+		"msg":  "ok",
+	})
+}
+func SendVisitorNotice(c *gin.Context) {
+	notice := c.Query("msg")
+	if notice == "" {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "msg不能为空",
+		})
+		return
+	}
+	msg := TypeMessage{
+		Type: "notice",
+		Data: notice,
+	}
+	str, _ := json.Marshal(msg)
+	for _, visitor := range clientList {
+		visitor.conn.WriteMessage(websocket.TextMessage, str)
+	}
+	c.JSON(200, gin.H{
+		"code": 200,
+		"msg":  "ok",
+	})
+}
+func SendCloseMessage(c *gin.Context) {
+	visitorId := c.Query("visitor_id")
+	if visitorId == "" {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "visitor_id不能为空",
+		})
+		return
+	}
+	msg := TypeMessage{
+		Type: "close",
+		Data: visitorId,
+	}
+	str, _ := json.Marshal(msg)
+	for _, visitor := range clientList {
+		if visitorId == visitor.id {
+			visitor.conn.WriteMessage(websocket.TextMessage, str)
+		}
+	}
+	c.JSON(200, gin.H{
+		"code": 200,
+		"msg":  "ok",
+	})
+}
+func UploadImg(c *gin.Context) {
+	cfg := config.CreateConfig()
+	f, err := c.FormFile("imgfile")
+	if err != nil {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "上传失败!",
+		})
+		return
+	} else {
+
+		fileExt := strings.ToLower(path.Ext(f.Filename))
+		if fileExt != ".png" && fileExt != ".jpg" && fileExt != ".gif" && fileExt != ".jpeg" {
+			c.JSON(200, gin.H{
+				"code": 400,
+				"msg":  "上传失败!只允许png,jpg,gif,jpeg文件",
+			})
+			return
+		}
+		fileName := tools.Md5(fmt.Sprintf("%s%s", f.Filename, time.Now().String()))
+		fildDir := fmt.Sprintf("%s%d%s/", cfg.Upload, time.Now().Year(), time.Now().Month().String())
+		isExist, _ := tools.IsFileExist(fildDir)
+		if !isExist {
+			os.Mkdir(fildDir, os.ModePerm)
+		}
+		filepath := fmt.Sprintf("%s%s%s", fildDir, fileName, fileExt)
+		c.SaveUploadedFile(f, filepath)
+		// 上传到七牛
+		qn := store.NewQn(config.QiniuConfig.Access,
+			config.QiniuConfig.Secret, config.QiniuConfig.Bucket,
+			config.QiniuConfig.Zone)
+		key, err := qn.Upload(filepath)
+		if err != nil {
+			c.JSON(200, gin.H{
+				"code": 400,
+				"msg":  "上传失败",
+			})
+			return
+		}
+		os.Remove(filepath)
+		dest := config.QiniuConfig.Domain + key
+		c.JSON(200, gin.H{
+			"code": 200,
+			"msg":  "上传成功!",
+			"result": gin.H{
+				"path": dest,
+			},
+		})
+	}
+}
+
+func GetMessagesV2(c *gin.Context) {
+	visitorId := c.Query("visitor_id")
+	kefuId := c.Query("kefu_id")
+	messages := models.FindMessagesByVisitorAndKefuId(visitorId, kefuId)
+	//result := make([]map[string]interface{}, 0)
+	chatMessages := make([]ChatMessage, 0)
+	for _, message := range messages {
+		//item := make(map[string]interface{})
+		var visitor models.Visitor
+		var kefu models.User
+		if visitor.Name == "" || kefu.Name == "" {
+			kefu = models.FindUser(message.KefuId)
+			visitor = models.FindVisitorByVistorId(message.VisitorId)
+		}
+		var chatMessage ChatMessage
+		chatMessage.Time = message.CreatedAt.Format("2006-01-02 15:04:05")
+		chatMessage.Content = message.Content
+		chatMessage.MesType = message.MesType
+		if message.MesType == "kefu" {
+			chatMessage.Name = kefu.Nickname
+			chatMessage.Avator = kefu.Avator
+		} else {
+			chatMessage.Name = visitor.Name
+			chatMessage.Avator = visitor.Avator
+		}
+		chatMessages = append(chatMessages, chatMessage)
+	}
+	models.ReadMessageByVisitorId(visitorId)
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": chatMessages,
+	})
+}

+ 60 - 0
controller/mysql.go

@@ -0,0 +1,60 @@
+package controller
+
+import (
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"github.com/wenstudio/gofly/config"
+	"github.com/wenstudio/gofly/database"
+	"github.com/wenstudio/gofly/tools"
+	"os"
+)
+
+func MysqlGetConf(c *gin.Context) {
+	mysqlInfo := config.GetMysql()
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "验证成功",
+		"result": mysqlInfo,
+	})
+}
+func MysqlSetConf(c *gin.Context) {
+
+	mysqlServer := c.PostForm("server")
+	mysqlPort := c.PostForm("port")
+	mysqlDb := c.PostForm("database")
+	mysqlUsername := c.PostForm("username")
+	mysqlPassword := c.PostForm("password")
+	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8", mysqlUsername, mysqlPassword, mysqlServer, mysqlPort, mysqlDb)
+	mysql := database.NewMysql()
+	mysql.Dsn = dsn
+	err := mysql.Ping()
+	if err != nil {
+		c.JSON(200, gin.H{
+			"code": 403,
+			"msg":  "数据库连接失败:" + err.Error(),
+		})
+		return
+	}
+	isExist, _ := tools.IsFileExist(config.Dir)
+	if !isExist {
+		os.Mkdir(config.Dir, os.ModePerm)
+	}
+	fileConfig := config.MysqlConf
+	file, _ := os.OpenFile(fileConfig, os.O_RDWR|os.O_CREATE, os.ModePerm)
+
+	format := `{
+	"Server":"%s",
+	"Port":"%s",
+	"Database":"%s",
+	"Username":"%s",
+	"Password":"%s"
+}
+`
+	data := fmt.Sprintf(format, mysqlServer, mysqlPort, mysqlDb, mysqlUsername, mysqlPassword)
+	file.WriteString(data)
+
+	c.JSON(200, gin.H{
+		"code": 200,
+		"msg":  "操作成功",
+	})
+}

+ 121 - 0
controller/notice.go

@@ -0,0 +1,121 @@
+package controller
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"github.com/gorilla/websocket"
+	"github.com/wenstudio/gofly/models"
+	"github.com/wenstudio/gofly/tools"
+	"log"
+	"net/http"
+	"time"
+)
+
+func GetNotice(c *gin.Context) {
+	kefuId := c.Query("kefu_id")
+	welcomes := models.FindWelcomesByUserId(kefuId)
+	user := models.FindUser(kefuId)
+	result := make([]gin.H, 0)
+	for _, welcome := range welcomes {
+		h := gin.H{
+			"name":    user.Nickname,
+			"avator":  user.Avator,
+			"is_kefu": false,
+			"content": welcome.Content,
+			"time":    time.Now().Format("2006-01-02 15:04:05"),
+		}
+		result = append(result, h)
+	}
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": result,
+	})
+}
+func GetNotices(c *gin.Context) {
+	kefuId, _ := c.Get("kefu_name")
+	welcomes := models.FindWelcomesByUserId(kefuId)
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": welcomes,
+	})
+}
+func PostNotice(c *gin.Context) {
+	kefuId, _ := c.Get("kefu_name")
+	content := c.PostForm("content")
+	models.CreateWelcome(fmt.Sprintf("%s", kefuId), content)
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": "",
+	})
+}
+func PostNoticeSave(c *gin.Context) {
+	kefuId, _ := c.Get("kefu_name")
+	content := c.PostForm("content")
+	id := c.PostForm("id")
+	models.UpdateWelcome(fmt.Sprintf("%s", kefuId), id, content)
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": "",
+	})
+}
+func DelNotice(c *gin.Context) {
+	kefuId, _ := c.Get("kefu_name")
+	id := c.Query("id")
+	models.DeleteWelcome(kefuId, id)
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": "",
+	})
+}
+
+var upgrader = websocket.Upgrader{}
+var oldFolders map[string]int
+
+//推送新邮件到达
+func PushMailServer(w http.ResponseWriter, r *http.Request) {
+	c, err := upgrader.Upgrade(w, r, nil)
+	if err != nil {
+		log.Print("upgrade:", err)
+		return
+	}
+	defer c.Close()
+	for {
+		mt, message, err := c.ReadMessage()
+		if err != nil {
+			log.Println("read:", err)
+			break
+		}
+		log.Printf("recv: %s", message)
+		mailServer := tools.GetMailServerFromCookie(r)
+		var msg []byte
+		if mailServer == nil {
+			msg, _ = json.Marshal(tools.JsonResult{Code: 400, Msg: "验证失败"})
+			err = c.WriteMessage(mt, msg)
+			if err != nil {
+				log.Println("write:", err)
+				break
+			}
+		} else {
+			folders := tools.GetMailNum(mailServer.Server, mailServer.Email, mailServer.Password)
+			for name, num := range folders {
+				if oldFolders[name] != num {
+					result := make(map[string]interface{})
+					result["folder_name"] = name
+					result["new_num"] = num - oldFolders[name]
+					msg, _ := json.Marshal(tools.JsonListResult{
+						JsonResult: tools.JsonResult{Code: 200, Msg: "获取成功"},
+						Result:     result,
+					})
+					c.WriteMessage(mt, msg)
+				}
+			}
+			oldFolders = folders
+		}
+	}
+}

+ 14 - 0
controller/response.go

@@ -0,0 +1,14 @@
+package controller
+
+type Response struct {
+	Code   int         `json:"code"`
+	Msg    string      `json:"msg"`
+	result interface{} `json:"result"`
+}
+type ChatMessage struct {
+	Time    string `json:"time"`
+	Content string `json:"content"`
+	MesType string `json:"mes_type"`
+	Name    string `json:"name"`
+	Avator  string `json:"avator"`
+}

+ 33 - 0
controller/role.go

@@ -0,0 +1,33 @@
+package controller
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/wenstudio/gofly/models"
+)
+
+func GetRoleList(c *gin.Context) {
+	roles := models.FindRoles()
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "获取成功",
+		"result": roles,
+	})
+}
+func PostRole(c *gin.Context) {
+	roleId := c.PostForm("id")
+	method := c.PostForm("method")
+	name := c.PostForm("name")
+	path := c.PostForm("path")
+	if roleId == "" || method == "" || name == "" || path == "" {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "参数不能为空",
+		})
+		return
+	}
+	models.SaveRole(roleId, name, method, path)
+	c.JSON(200, gin.H{
+		"code": 200,
+		"msg":  "修改成功",
+	})
+}

+ 33 - 0
controller/setting.go

@@ -0,0 +1,33 @@
+package controller
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/wenstudio/gofly/models"
+)
+
+func GetConfigs(c *gin.Context) {
+	configs := models.FindConfigs()
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": configs,
+	})
+}
+func PostConfig(c *gin.Context) {
+	key := c.PostForm("key")
+	value := c.PostForm("value")
+	if key == "" || value == "" {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "error",
+		})
+		return
+	}
+	models.UpdateConfig(key, value)
+
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": "",
+	})
+}

+ 24 - 0
controller/shout.go

@@ -0,0 +1,24 @@
+package controller
+
+import (
+	"fmt"
+	"github.com/wenstudio/gofly/models"
+	"github.com/wenstudio/gofly/tools"
+	"log"
+	"strconv"
+)
+
+func SendServerJiang(content string) string {
+	noticeServerJiang, err := strconv.ParseBool(models.FindConfig("NoticeServerJiang"))
+	serverJiangAPI := models.FindConfig("ServerJiangAPI")
+	if err != nil || !noticeServerJiang || serverJiangAPI == "" {
+		log.Println("do not notice serverjiang:", serverJiangAPI, noticeServerJiang)
+		return ""
+	}
+	sendStr := fmt.Sprintf("%s,访客来了", content)
+	desp := "[登录](https://gofly.sopans.com/main)"
+	url := serverJiangAPI + "?text=" + sendStr + "&desp=" + desp
+	//log.Println(url)
+	res := tools.Get(url)
+	return res
+}

+ 58 - 0
controller/tcp.go

@@ -0,0 +1,58 @@
+package controller
+
+import (
+	"github.com/gin-gonic/gin"
+	"log"
+	"net"
+)
+
+var clientTcpList = make(map[string]net.Conn)
+
+func NewTcpServer(tcpBaseServer string) {
+	listener, err := net.Listen("tcp", tcpBaseServer)
+	if err != nil {
+		log.Println("Error listening", err.Error())
+		return //终止程序
+	}
+	// 监听并接受来自客户端的连接
+	for {
+		conn, err := listener.Accept()
+		if err != nil {
+			log.Println("Error accepting", err.Error())
+			return // 终止程序
+		}
+		var remoteIpAddress = conn.RemoteAddr()
+		clientTcpList[remoteIpAddress.String()] = conn
+		log.Println(remoteIpAddress, clientTcpList)
+		//clientTcpList=append(clientTcpList,conn)
+	}
+}
+func PushServerTcp(str []byte) {
+	for ip, conn := range clientTcpList {
+		line := append(str, []byte("\r\n")...)
+		_, err := conn.Write(line)
+		log.Println(ip, err)
+		if err != nil {
+			conn.Close()
+			delete(clientTcpList, ip)
+			//clientTcpList=append(clientTcpList[:index],clientTcpList[index+1:]...)
+		}
+	}
+}
+func DeleteOnlineTcp(c *gin.Context) {
+	ip := c.Query("ip")
+	for ipkey, conn := range clientTcpList {
+		if ip == ipkey {
+			conn.Close()
+			delete(clientTcpList, ip)
+		}
+		if ip == "all" {
+			conn.Close()
+			delete(clientTcpList, ipkey)
+		}
+	}
+	c.JSON(200, gin.H{
+		"code": 200,
+		"msg":  "ok",
+	})
+}

+ 238 - 0
controller/visitor.go

@@ -0,0 +1,238 @@
+package controller
+
+import (
+	"encoding/json"
+	"github.com/gin-gonic/gin"
+	"github.com/gorilla/websocket"
+	"github.com/wenstudio/gofly/config"
+	"github.com/wenstudio/gofly/models"
+	"github.com/wenstudio/gofly/tools"
+	"github.com/wenstudio/gofly/user/provider/twong"
+	"log"
+	"strconv"
+)
+
+func PostVisitor(c *gin.Context) {
+	name := c.PostForm("name")
+	avator := c.PostForm("avator")
+	toId := c.PostForm("to_id")
+	id := c.PostForm("id")
+	refer := c.PostForm("refer")
+	city := c.PostForm("city")
+	client_ip := c.PostForm("client_ip")
+	if name == "" || avator == "" || toId == "" || id == "" || refer == "" || city == "" || client_ip == "" {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "error",
+		})
+		return
+	}
+	kefuInfo := models.FindUser(toId)
+	if kefuInfo.ID == 0 {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "用户不存在",
+		})
+		return
+	}
+	models.CreateVisitor(name, avator, c.ClientIP(), toId, id, refer, city, client_ip)
+
+	userInfo := make(map[string]string)
+	userInfo["uid"] = id
+	userInfo["username"] = name
+	userInfo["avator"] = avator
+	msg := TypeMessage{
+		Type: "userOnline",
+		Data: userInfo,
+	}
+	str, _ := json.Marshal(msg)
+	kefuConns := kefuList[toId]
+	if kefuConns != nil {
+		for k, kefuConn := range kefuConns {
+			log.Println(k, "xxxxxxxx")
+			kefuConn.WriteMessage(websocket.TextMessage, str)
+		}
+	}
+	c.JSON(200, gin.H{
+		"code": 200,
+		"msg":  "ok",
+	})
+}
+
+// post /visitor_login
+// 访客第一次连接时请求
+func PostVisitorLogin(c *gin.Context) {
+	suid := c.PostForm("uid")
+	uid, err := strconv.ParseUint(suid, 10, 64)
+	if err != nil || uid <= 0 {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "参数错误",
+		})
+		return
+	}
+	ipcity := tools.ParseIp(c.ClientIP())
+	toId := c.PostForm("to_id")
+	refer := c.PostForm("refer")
+
+	var city string
+	if ipcity != nil {
+		city = ipcity.CountryName + ipcity.RegionName + ipcity.CityName
+	} else {
+		city = "未识别地区"
+	}
+	client_ip := c.ClientIP() // c.PostForm("client_ip")
+
+	ut := twong.NewTwongUser()
+	v, err := ut.GetVisitorInfo(uid)
+	if err != nil {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  err.Error(),
+		})
+	}
+	//log.Println(name,avator,c.ClientIP(),toId,id,refer,city,client_ip)
+	if v.Name == "" || v.Avator == "" || toId == "" || v.VisitorId == "" || city == "" || client_ip == "" {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "error",
+		})
+		return
+	}
+	if refer == "" {
+		refer = "app"
+	}
+	// 找到客服信息
+	kefuInfo := models.FindUser(toId)
+	if kefuInfo.ID == 0 {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "客服不存在",
+		})
+		return
+	}
+	// 插入或更新 访客信息
+	models.CreateVisitor(v.Name, v.Avator,
+		client_ip, toId, v.VisitorId, refer, city, client_ip)
+
+	visitor := models.FindVisitorByVistorId(v.VisitorId)
+	visitor.ToId = toId
+
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": visitor,
+	})
+}
+func GetVisitor(c *gin.Context) {
+	visitorId := c.Query("visitorId")
+	vistor := models.FindVisitorByVistorId(visitorId)
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": vistor,
+	})
+}
+
+// @Summary 获取访客列表接口
+// @Produce  json
+// @Accept multipart/form-data
+// @Param page query   string true "分页"
+// @Param token header string true "认证token"
+// @Success 200 {object} controller.Response
+// @Failure 200 {object} controller.Response
+// @Router /visitors [get]
+func GetVisitors(c *gin.Context) {
+	page, _ := strconv.Atoi(c.Query("page"))
+	kefuId, _ := c.Get("kefu_name")
+	vistors := models.FindVisitorsByKefuId(uint(page), config.VisitorPageSize, kefuId.(string))
+	count := models.CountVisitorsByKefuId(kefuId.(string))
+	c.JSON(200, gin.H{
+		"code": 200,
+		"msg":  "ok",
+		"result": gin.H{
+			"list":     vistors,
+			"count":    count,
+			"pagesize": config.PageSize,
+		},
+	})
+}
+
+// @Summary 获取访客聊天信息接口
+// @Produce  json
+// @Accept multipart/form-data
+// @Param visitorId query   string true "访客ID"
+// @Param token header string true "认证token"
+// @Success 200 {object} controller.Response
+// @Failure 200 {object} controller.Response
+// @Router /messages [get]
+func GetVisitorMessage(c *gin.Context) {
+	visitorId := c.Query("visitorId")
+	kefuId := c.Query("kefuId")
+	messages := models.FindMessagesByVisitorAndKefuId(visitorId, kefuId)
+	result := make([]map[string]interface{}, 0)
+	for _, message := range messages {
+		item := make(map[string]interface{})
+		var visitor models.Visitor
+		var kefu models.User
+		if visitor.Name == "" || kefu.Name == "" {
+			kefu = models.FindUser(message.KefuId)
+			visitor = models.FindVisitorByVistorId(message.VisitorId)
+		}
+		item["time"] = message.CreatedAt.Format("2006-01-02 15:04:05")
+		item["content"] = message.Content
+		item["mes_type"] = message.MesType
+		item["visitor_name"] = visitor.Name
+		item["visitor_avator"] = visitor.Avator
+		item["kefu_name"] = kefu.Nickname
+		item["kefu_avator"] = kefu.Avator
+		result = append(result, item)
+	}
+	models.ReadMessageByVisitorId(visitorId)
+	c.JSON(200, gin.H{
+		"code":   200,
+		"msg":    "ok",
+		"result": result,
+	})
+}
+
+// @Summary 获取在线访客列表接口
+// @Produce  json
+// @Success 200 {object} controller.Response
+// @Failure 200 {object} controller.Response
+// @Router /visitors_online [get]
+func GetVisitorOnlines(c *gin.Context) {
+	users := make([]map[string]string, 0)
+	visitorIds := make([]string, 0)
+	for uid, visitor := range clientList {
+		userInfo := make(map[string]string)
+		userInfo["uid"] = uid
+		userInfo["name"] = visitor.name
+		userInfo["avator"] = visitor.avator
+		users = append(users, userInfo)
+		visitorIds = append(visitorIds, visitor.id)
+	}
+
+	//查询最新消息
+	messages := models.FindLastMessage(visitorIds)
+	temp := make(map[string]string, 0)
+	for _, mes := range messages {
+		temp[mes.VisitorId] = mes.Content
+	}
+	for _, user := range users {
+		user["last_message"] = temp[user["uid"]]
+	}
+
+	tcps := make([]string, 0)
+	for ip, _ := range clientTcpList {
+		tcps = append(tcps, ip)
+	}
+	c.JSON(200, gin.H{
+		"code": 200,
+		"msg":  "ok",
+		"result": gin.H{
+			"ws":  users,
+			"tcp": tcps,
+		},
+	})
+}

+ 35 - 0
controller/weixin.go

@@ -0,0 +1,35 @@
+package controller
+
+import (
+	"crypto/sha1"
+	"encoding/hex"
+	"github.com/gin-gonic/gin"
+	"github.com/wenstudio/gofly/models"
+	"log"
+	"sort"
+)
+
+func GetCheckWeixinSign(c *gin.Context) {
+	token := models.FindConfig("WeixinToken")
+	signature := c.Query("signature")
+	timestamp := c.Query("timestamp")
+	nonce := c.Query("nonce")
+	echostr := c.Query("echostr")
+	//将token、timestamp、nonce三个参数进行字典序排序
+	var tempArray = []string{token, timestamp, nonce}
+	sort.Strings(tempArray)
+	//将三个参数字符串拼接成一个字符串进行sha1加密
+	var sha1String string = ""
+	for _, v := range tempArray {
+		sha1String += v
+	}
+	h := sha1.New()
+	h.Write([]byte(sha1String))
+	sha1String = hex.EncodeToString(h.Sum([]byte("")))
+	//获得加密后的字符串可与signature对比
+	if sha1String == signature {
+		c.Writer.Write([]byte(echostr))
+	} else {
+		log.Println("微信API验证失败")
+	}
+}

+ 27 - 0
database/mysql.go

@@ -0,0 +1,27 @@
+package database
+
+import (
+	"database/sql"
+	"fmt"
+	_ "github.com/go-sql-driver/mysql"
+	"github.com/wenstudio/gofly/config"
+)
+
+type Mysql struct {
+	SqlDB *sql.DB
+	Dsn   string
+}
+
+func NewMysql() *Mysql {
+	mysql := config.CreateMysql(config.MysqlConf)
+	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", mysql.Username, mysql.Password, mysql.Server, mysql.Port, mysql.Database)
+	return &Mysql{
+		Dsn: dsn,
+	}
+}
+
+func (db *Mysql) Ping() error {
+	sqlDb, _ := sql.Open("mysql", db.Dsn)
+	db.SqlDB = sqlDb
+	return db.SqlDB.Ping()
+}

+ 270 - 0
docs/docs.go

@@ -0,0 +1,270 @@
+// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
+// This file was generated by swaggo/swag at
+// 2020-10-19 23:32:55.3852666 +0800 CST m=+0.208956401
+
+package docs
+
+import (
+	"bytes"
+	"encoding/json"
+	"strings"
+
+	"github.com/alecthomas/template"
+	"github.com/swaggo/swag"
+)
+
+var doc = `{
+    "schemes": {{ marshal .Schemes }},
+    "swagger": "2.0",
+    "info": {
+        "description": "{{.Description}}",
+        "title": "{{.Title}}",
+        "contact": {},
+        "license": {},
+        "version": "{{.Version}}"
+    },
+    "host": "{{.Host}}",
+    "basePath": "{{.BasePath}}",
+    "paths": {
+        "/check": {
+            "post": {
+                "consumes": [
+                    "multipart/form-data"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "summary": "登陆验证接口",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "用户名",
+                        "name": "username",
+                        "in": "formData",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "密码",
+                        "name": "password",
+                        "in": "formData",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "类型",
+                        "name": "type",
+                        "in": "formData",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/controller.Response"
+                        }
+                    }
+                }
+            }
+        },
+        "/message": {
+            "post": {
+                "consumes": [
+                    "multipart/form-data"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "summary": "发送消息接口",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "来源uid",
+                        "name": "from_id",
+                        "in": "formData",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "目标uid",
+                        "name": "to_id",
+                        "in": "formData",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "内容",
+                        "name": "content",
+                        "in": "formData",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "类型|kefu,visitor",
+                        "name": "type",
+                        "in": "formData",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/controller.Response"
+                        }
+                    }
+                }
+            }
+        },
+        "/messages": {
+            "get": {
+                "consumes": [
+                    "multipart/form-data"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "summary": "获取访客聊天信息接口",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "访客ID",
+                        "name": "visitorId",
+                        "in": "query",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "认证token",
+                        "name": "token",
+                        "in": "header",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/controller.Response"
+                        }
+                    }
+                }
+            }
+        },
+        "/visitors": {
+            "get": {
+                "consumes": [
+                    "multipart/form-data"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "summary": "获取访客列表接口",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "分页",
+                        "name": "page",
+                        "in": "query",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "认证token",
+                        "name": "token",
+                        "in": "header",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/controller.Response"
+                        }
+                    }
+                }
+            }
+        },
+        "/visitors_online": {
+            "get": {
+                "produces": [
+                    "application/json"
+                ],
+                "summary": "获取在线访客列表接口",
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/controller.Response"
+                        }
+                    }
+                }
+            }
+        }
+    },
+    "definitions": {
+        "controller.Response": {
+            "type": "object",
+            "properties": {
+                "code": {
+                    "type": "integer"
+                },
+                "msg": {
+                    "type": "string"
+                },
+                "result": {
+                    "type": "object"
+                }
+            }
+        }
+    }
+}`
+
+type swaggerInfo struct {
+	Version     string
+	Host        string
+	BasePath    string
+	Schemes     []string
+	Title       string
+	Description string
+}
+
+// SwaggerInfo holds exported Swagger Info so clients can modify it
+var SwaggerInfo = swaggerInfo{
+	Version:     "",
+	Host:        "",
+	BasePath:    "",
+	Schemes:     []string{},
+	Title:       "",
+	Description: "",
+}
+
+type s struct{}
+
+func (s *s) ReadDoc() string {
+	sInfo := SwaggerInfo
+	sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1)
+
+	t, err := template.New("swagger_info").Funcs(template.FuncMap{
+		"marshal": func(v interface{}) string {
+			a, _ := json.Marshal(v)
+			return string(a)
+		},
+	}).Parse(doc)
+	if err != nil {
+		return doc
+	}
+
+	var tpl bytes.Buffer
+	if err := t.Execute(&tpl, sInfo); err != nil {
+		return doc
+	}
+
+	return tpl.String()
+}
+
+func init() {
+	swag.Register(swag.Name, &s{})
+}

+ 202 - 0
docs/swagger.json

@@ -0,0 +1,202 @@
+{
+    "swagger": "2.0",
+    "info": {
+        "contact": {},
+        "license": {}
+    },
+    "paths": {
+        "/check": {
+            "post": {
+                "consumes": [
+                    "multipart/form-data"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "summary": "登陆验证接口",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "用户名",
+                        "name": "username",
+                        "in": "formData",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "密码",
+                        "name": "password",
+                        "in": "formData",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "类型",
+                        "name": "type",
+                        "in": "formData",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/controller.Response"
+                        }
+                    }
+                }
+            }
+        },
+        "/message": {
+            "post": {
+                "consumes": [
+                    "multipart/form-data"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "summary": "发送消息接口",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "来源uid",
+                        "name": "from_id",
+                        "in": "formData",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "目标uid",
+                        "name": "to_id",
+                        "in": "formData",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "内容",
+                        "name": "content",
+                        "in": "formData",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "类型|kefu,visitor",
+                        "name": "type",
+                        "in": "formData",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/controller.Response"
+                        }
+                    }
+                }
+            }
+        },
+        "/messages": {
+            "get": {
+                "consumes": [
+                    "multipart/form-data"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "summary": "获取访客聊天信息接口",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "访客ID",
+                        "name": "visitorId",
+                        "in": "query",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "认证token",
+                        "name": "token",
+                        "in": "header",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/controller.Response"
+                        }
+                    }
+                }
+            }
+        },
+        "/visitors": {
+            "get": {
+                "consumes": [
+                    "multipart/form-data"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "summary": "获取访客列表接口",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "分页",
+                        "name": "page",
+                        "in": "query",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "认证token",
+                        "name": "token",
+                        "in": "header",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/controller.Response"
+                        }
+                    }
+                }
+            }
+        },
+        "/visitors_online": {
+            "get": {
+                "produces": [
+                    "application/json"
+                ],
+                "summary": "获取在线访客列表接口",
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/controller.Response"
+                        }
+                    }
+                }
+            }
+        }
+    },
+    "definitions": {
+        "controller.Response": {
+            "type": "object",
+            "properties": {
+                "code": {
+                    "type": "integer"
+                },
+                "msg": {
+                    "type": "string"
+                },
+                "result": {
+                    "type": "object"
+                }
+            }
+        }
+    }
+}

+ 132 - 0
docs/swagger.yaml

@@ -0,0 +1,132 @@
+definitions:
+  controller.Response:
+    properties:
+      code:
+        type: integer
+      msg:
+        type: string
+      result:
+        type: object
+    type: object
+info:
+  contact: {}
+  license: {}
+paths:
+  /check:
+    post:
+      consumes:
+      - multipart/form-data
+      parameters:
+      - description: 用户名
+        in: formData
+        name: username
+        required: true
+        type: string
+      - description: 密码
+        in: formData
+        name: password
+        required: true
+        type: string
+      - description: 类型
+        in: formData
+        name: type
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/controller.Response'
+      summary: 登陆验证接口
+  /message:
+    post:
+      consumes:
+      - multipart/form-data
+      parameters:
+      - description: 来源uid
+        in: formData
+        name: from_id
+        required: true
+        type: string
+      - description: 目标uid
+        in: formData
+        name: to_id
+        required: true
+        type: string
+      - description: 内容
+        in: formData
+        name: content
+        required: true
+        type: string
+      - description: 类型|kefu,visitor
+        in: formData
+        name: type
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/controller.Response'
+      summary: 发送消息接口
+  /messages:
+    get:
+      consumes:
+      - multipart/form-data
+      parameters:
+      - description: 访客ID
+        in: query
+        name: visitorId
+        required: true
+        type: string
+      - description: 认证token
+        in: header
+        name: token
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/controller.Response'
+      summary: 获取访客聊天信息接口
+  /visitors:
+    get:
+      consumes:
+      - multipart/form-data
+      parameters:
+      - description: 分页
+        in: query
+        name: page
+        required: true
+        type: string
+      - description: 认证token
+        in: header
+        name: token
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/controller.Response'
+      summary: 获取访客列表接口
+  /visitors_online:
+    get:
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/controller.Response'
+      summary: 获取在线访客列表接口
+swagger: "2.0"

+ 9 - 0
go-fly.go

@@ -0,0 +1,9 @@
+package main
+
+import (
+	"github.com/wenstudio/gofly/cmd"
+)
+
+func main() {
+	cmd.Execute()
+}

+ 31 - 0
go.mod

@@ -0,0 +1,31 @@
+module github.com/wenstudio/gofly
+
+go 1.14
+
+require github.com/emersion/go-imap v1.0.4
+
+require (
+	github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
+	github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394
+	github.com/casbin/casbin/v2 v2.7.2
+	github.com/dgrijalva/jwt-go v3.2.0+incompatible
+	github.com/emersion/go-message v0.11.2
+	github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
+	github.com/emersion/go-smtp v0.13.0
+	github.com/gin-contrib/cors v1.3.1
+	github.com/gin-gonic/gin v1.6.3
+	github.com/go-sql-driver/mysql v1.5.0
+	github.com/gobuffalo/packr/v2 v2.5.1
+	github.com/gorilla/websocket v1.4.2
+	github.com/ipipdotnet/ipdb-go v1.3.0
+	github.com/jinzhu/gorm v1.9.14
+	github.com/qiniu/api.v7 v7.2.5+incompatible
+	github.com/qiniu/api.v7/v7 v7.6.0
+	github.com/satori/go.uuid v1.2.0
+	github.com/sirupsen/logrus v1.4.2
+	github.com/spf13/cobra v0.0.5
+	github.com/swaggo/gin-swagger v1.2.0
+	github.com/swaggo/swag v1.5.1
+	golang.org/x/net v0.0.0-20201024042810-be3efd7ff127
+	golang.org/x/text v0.3.3
+)

+ 260 - 0
go.sum

@@ -0,0 +1,260 @@
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
+github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
+github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
+github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 h1:OYA+5W64v3OgClL+IrOD63t4i/RW7RqrAVl9LTZ9UqQ=
+github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg=
+github.com/casbin/casbin/v2 v2.7.2 h1:PM/u9RGCZmlN4/cpS3FbVqCXG+H5806faG7QGwEy+lE=
+github.com/casbin/casbin/v2 v2.7.2/go.mod h1:XXtYGrs/0zlOsJMeRteEdVi/FsB0ph7KgNfjoCoJUD8=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
+github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/emersion/go-imap v1.0.4 h1:uiCAIHM6Z5Jwkma1zdNDWWXxSCqb+/xHBkHflD7XBro=
+github.com/emersion/go-imap v1.0.4/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
+github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
+github.com/emersion/go-message v0.11.2 h1:oxO9SQ+3wgBAQRdk07eqfkCJ26Tl8ZHF7CcpGVoE00o=
+github.com/emersion/go-message v0.11.2/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
+github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
+github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
+github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
+github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
+github.com/emersion/go-smtp v0.13.0 h1:aC3Kc21TdfvXnuJXCQXuhnDXUldhc12qME/S7Y3Y94g=
+github.com/emersion/go-smtp v0.13.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
+github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
+github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
+github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
+github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA=
+github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk=
+github.com/gin-contrib/gzip v0.0.1 h1:ezvKOL6jH+jlzdHNE4h9h8q8uMpDQjyl0NN0Jd7jozc=
+github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w=
+github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
+github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
+github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
+github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
+github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
+github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
+github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0=
+github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
+github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
+github.com/go-openapi/jsonreference v0.19.0 h1:BqWKpV1dFd+AuiKlgtddwVIFQsuMpxfBDBHGfM2yNpk=
+github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
+github.com/go-openapi/spec v0.19.0 h1:A4SZ6IWh3lnjH0rG0Z5lkxazMGBECtrZcbyYQi+64k4=
+github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
+github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880=
+github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
+github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
+github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
+github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU=
+github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
+github.com/gobuffalo/logger v1.0.0 h1:xw9Ko9EcC5iAFprrjJ6oZco9UpzS5MQ4jAwghsLHdy4=
+github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
+github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
+github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
+github.com/gobuffalo/packr/v2 v2.5.1 h1:TFOeY2VoGamPjQLiNDT3mn//ytzk236VMO2j7iHxJR4=
+github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/ipipdotnet/ipdb-go v1.3.0 h1:FfkSkAI1do3bZ7F35ueGuF7Phur64jmikQ1C4IPl/gc=
+github.com/ipipdotnet/ipdb-go v1.3.0/go.mod h1:yZ+8puwe3R37a/3qRftXo40nZVQbxYDLqls9o5foexs=
+github.com/jinzhu/gorm v1.9.14 h1:Kg3ShyTPcM6nzVo148fRrcMO6MNKuqtOUwnzqMgVniM=
+github.com/jinzhu/gorm v1.9.14/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
+github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
+github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/karrick/godirwalk v1.10.12 h1:BqUm+LuJcXjGv1d2mj3gBiQyrQ57a0rYoAmhvJQ7RDU=
+github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
+github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
+github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A=
+github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
+github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/qiniu/api.v7 v7.2.5+incompatible h1:6KKaGt7MbFzVGSniwzv7qsM/Qv0or4SkRJfmak8LqZE=
+github.com/qiniu/api.v7 v7.2.5+incompatible/go.mod h1:V8/EzlTgLN6q0s0CJmg/I81ytsvldSF22F7h6MI02+c=
+github.com/qiniu/api.v7/v7 v7.6.0 h1:396UGG+AWLh80pIhpPNCgEzb04t4S8CGKxqvLkiQeZI=
+github.com/qiniu/api.v7/v7 v7.6.0/go.mod h1:zg3DaqU8mVnoQSQmtC/Mr2wXTJIE7fvcup+7sMt58Q0=
+github.com/qiniu/x v1.11.5 h1:TYr5cl4g2yoHAZeDK4MTjKF6CMoG+IHlCDvvM5qym6U=
+github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
+github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/swaggo/gin-swagger v1.2.0 h1:YskZXEiv51fjOMTsXrOetAjrMDfFaXD79PEoQBOe2W0=
+github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI=
+github.com/swaggo/swag v1.5.1 h1:2Agm8I4K5qb00620mHq0VJ05/KT4FtmALPIcQR9lEZM=
+github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0=
+github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI=
+github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A=
+golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM=
+golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201024042810-be3efd7ff127 h1:pZPp9+iYUqwYKLjht0SDBbRCRK/9gAXDy7pz5fRDpjo=
+golang.org/x/net v0.0.0-20201024042810-be3efd7ff127/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c h1:KfpJVdWhuRqNk4XVXzjXf2KAV4TBEP77SYdFGjeGuIE=
+golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
+gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
+gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

+ 32 - 0
middleware/casbin.go

@@ -0,0 +1,32 @@
+package middleware
+
+import (
+	"fmt"
+	"github.com/casbin/casbin/v2"
+	"github.com/gin-gonic/gin"
+	"log"
+)
+
+func CasbinACL(c *gin.Context) {
+	roleId, _ := c.Get("role_id")
+	sub := fmt.Sprintf("%s_%d", "role", int(roleId.(float64)))
+	obj := c.Request.RequestURI
+	act := c.Request.Method
+	e, err := casbin.NewEnforcer("config/model.conf", "config/policy.csv")
+	log.Println(sub, obj, act, err)
+	ok, err := e.Enforce(sub, obj, act)
+	if err != nil {
+		c.JSON(200, gin.H{
+			"code": 403,
+			"msg":  "没有权限:" + err.Error(),
+		})
+		c.Abort()
+	}
+	if !ok {
+		c.JSON(200, gin.H{
+			"code": 403,
+			"msg":  fmt.Sprintf("没有权限:%s,%s,%s", sub, obj, act),
+		})
+		c.Abort()
+	}
+}

+ 19 - 0
middleware/ipblack.go

@@ -0,0 +1,19 @@
+package middleware
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/wenstudio/gofly/models"
+)
+
+func Ipblack(c *gin.Context) {
+	ip := c.ClientIP()
+	ipblack := models.FindIp(ip)
+	if ipblack.IP != "" {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "IP已被加入黑名单",
+		})
+		c.Abort()
+		return
+	}
+}

+ 49 - 0
middleware/jwt.go

@@ -0,0 +1,49 @@
+package middleware
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/wenstudio/gofly/tools"
+	"time"
+)
+
+func JwtPageMiddleware(c *gin.Context) {
+	//暂时不处理
+	//token := c.Query("token")
+	//userinfo := tools.ParseToken(token)
+	//if userinfo == nil {
+	//	c.Redirect(302,"/login")
+	//	c.Abort()
+	//}
+}
+func JwtApiMiddleware(c *gin.Context) {
+	token := c.GetHeader("token")
+	if token == "" {
+		token = c.Query("token")
+	}
+	userinfo := tools.ParseToken(token)
+	if userinfo == nil || userinfo["name"] == nil || userinfo["create_time"] == nil {
+		c.JSON(200, gin.H{
+			"code": 400,
+			"msg":  "验证失败",
+		})
+		c.Abort()
+		return
+	}
+	createTime := int64(userinfo["create_time"].(float64))
+	var expire int64 = 24 * 60 * 60
+	nowTime := time.Now().Unix()
+	if (nowTime - createTime) >= expire {
+		c.JSON(200, gin.H{
+			"code": 401,
+			"msg":  "token失效",
+		})
+		c.Abort()
+	}
+	c.Set("user", userinfo["name"])
+	//log.Println(userinfo)
+	//if userinfo["type"]=="kefu"{
+	c.Set("kefu_id", userinfo["kefu_id"])
+	c.Set("kefu_name", userinfo["name"])
+	c.Set("role_id", userinfo["role_id"])
+	//}
+}

+ 16 - 0
middleware/language.go

@@ -0,0 +1,16 @@
+package middleware
+
+import (
+	"github.com/gin-gonic/gin"
+)
+
+func SetLanguage(c *gin.Context) {
+	var lang string
+	if lang = c.Param("lang"); lang == "" {
+		lang = c.Query("lang")
+	}
+	if lang == "" || lang != "cn" {
+		lang = "en"
+	}
+	c.Set("lang", lang)
+}

+ 49 - 0
middleware/rbac.go

@@ -0,0 +1,49 @@
+package middleware
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/wenstudio/gofly/models"
+	"strings"
+)
+
+func RbacAuth(c *gin.Context) {
+	roleId, _ := c.Get("role_id")
+	role := models.FindRole(roleId)
+	var methodFlag bool
+	rPaths := strings.Split(c.Request.RequestURI, "?")
+	if role.Method != "*" {
+		methods := strings.Split(role.Method, ",")
+		for _, m := range methods {
+			if c.Request.Method == m {
+				methodFlag = true
+				break
+			}
+		}
+		if !methodFlag {
+			c.JSON(200, gin.H{
+				"code": 403,
+				"msg":  "没有权限:" + c.Request.Method + "," + rPaths[0],
+			})
+			c.Abort()
+			return
+		}
+	}
+	var flag bool
+	if role.Path != "*" {
+		paths := strings.Split(role.Path, ",")
+		for _, p := range paths {
+			if rPaths[0] == p {
+				flag = true
+				break
+			}
+		}
+		if !flag {
+			c.JSON(200, gin.H{
+				"code": 403,
+				"msg":  "没有权限:" + rPaths[0],
+			})
+			c.Abort()
+			return
+		}
+	}
+}

+ 47 - 0
models/abouts.go

@@ -0,0 +1,47 @@
+package models
+
+type About struct {
+	ID         uint   `gorm:"primary_key" json:"id"`
+	TitleCn    string `json:"title_cn"`
+	TitleEn    string `json:"title_en"`
+	KeywordsCn string `json:"keywords_cn"`
+	KeywordsEn string `json:"keywords_en"`
+	DescCn     string `json:"desc_cn"`
+	DescEn     string `json:"desc_en"`
+	CssJs      string `json:"css_js"`
+	HtmlCn     string `json:"html_cn"`
+	HtmlEn     string `json:"html_en"`
+}
+
+func FindAboutByPage(page interface{}) About {
+	var a About
+	DB.Where("page = ?", page).First(&a)
+	return a
+}
+func FindAboutByPageLanguage(page interface{}, lang string) About {
+	var a About
+	if lang == "" {
+		lang = "cn"
+	}
+	if lang == "en" {
+		DB.Select("css_js,title_en,keywords_en,desc_en,html_en").Where("page = ?", page).First(&a)
+	} else {
+		DB.Select("css_js,title_cn,keywords_cn,desc_cn,html_cn").Where("page = ?", page).First(&a)
+	}
+	return a
+}
+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) {
+	c := &About{
+		TitleCn:    title_cn,
+		TitleEn:    title_en,
+		KeywordsCn: keywords_cn,
+		KeywordsEn: keywords_en,
+		DescCn:     desc_cn,
+		DescEn:     desc_en,
+		CssJs:      css_js,
+		HtmlCn:     html_cn,
+		HtmlEn:     html_en,
+	}
+	DB.Model(c).Where("page = ?", page).Update(c)
+	InitConfig()
+}

+ 34 - 0
models/configs.go

@@ -0,0 +1,34 @@
+package models
+
+var CustomConfigs []Config
+
+type Config struct {
+	ID        uint   `gorm:"primary_key" json:"id"`
+	ConfName  string `json:"conf_name"`
+	ConfKey   string `json:"conf_key"`
+	ConfValue string `json:"conf_value"`
+}
+
+func UpdateConfig(key string, value string) {
+	c := &Config{
+		ConfValue: value,
+	}
+	DB.Model(c).Where("conf_key = ?", key).Update(c)
+	InitConfig()
+}
+func FindConfigs() []Config {
+	var config []Config
+	DB.Find(&config)
+	return config
+}
+func InitConfig() {
+	CustomConfigs = FindConfigs()
+}
+func FindConfig(key string) string {
+	for _, config := range CustomConfigs {
+		if key == config.ConfKey {
+			return config.ConfValue
+		}
+	}
+	return ""
+}

+ 52 - 0
models/ipblacks.go

@@ -0,0 +1,52 @@
+package models
+
+import "time"
+
+type Ipblack struct {
+	ID       uint      `gorm:"primary_key" json:"id"`
+	IP       string    `json:"ip"`
+	KefuId   string    `json:"kefu_id"`
+	CreateAt time.Time `json:"create_at"`
+}
+
+func CreateIpblack(ip string, kefuId string) uint {
+	black := &Ipblack{
+		IP:       ip,
+		KefuId:   kefuId,
+		CreateAt: time.Now(),
+	}
+	DB.Create(black)
+	return black.ID
+}
+func DeleteIpblackByIp(ip string) {
+	DB.Where("ip = ?", ip).Delete(Ipblack{})
+}
+func FindIp(ip string) Ipblack {
+	var ipblack Ipblack
+	DB.Where("ip = ?", ip).First(&ipblack)
+	return ipblack
+}
+func FindIps(query interface{}, args []interface{}, page uint, pagesize uint) []Ipblack {
+	offset := (page - 1) * pagesize
+	if offset < 0 {
+		offset = 0
+	}
+	var ipblacks []Ipblack
+	if query != nil {
+		DB.Where(query, args...).Offset(offset).Limit(pagesize).Find(&ipblacks)
+	} else {
+		DB.Offset(offset).Limit(pagesize).Find(&ipblacks)
+	}
+	return ipblacks
+}
+
+//查询条数
+func CountIps(query interface{}, args []interface{}) uint {
+	var count uint
+	if query != nil {
+		DB.Model(&Visitor{}).Where(query, args...).Count(&count)
+	} else {
+		DB.Model(&Visitor{}).Count(&count)
+	}
+	return count
+}

+ 69 - 0
models/messages.go

@@ -0,0 +1,69 @@
+package models
+
+type Message struct {
+	Model
+	KefuId    string `json:"kefu_id"`
+	VisitorId string `json:"visitor_id"`
+	Content   string `json:"content"`
+	MesType   string `json:"mes_type"`
+	Status    string `json:"status"`
+}
+
+func CreateMessage(kefu_id string, visitor_id string, content string, mes_type string) {
+	v := &Message{
+		KefuId:    kefu_id,
+		VisitorId: visitor_id,
+		Content:   content,
+		MesType:   mes_type,
+		Status:    "unread",
+	}
+	DB.Create(v)
+}
+
+//func FindMessageByVisitorId(visitor_id string) []Message {
+//	var messages []Message
+//	DB.Where("visitor_id=?", visitor_id).Order("id asc").Find(&messages)
+//	return messages
+//}
+
+func FindMessagesByVisitorAndKefuId(visitorId, kefuId string) []Message {
+	var messages []Message
+	DB.Where("visitor_id=? AND kefu_id=?", visitorId, kefuId).Order("id asc").Find(&messages)
+	return messages
+}
+
+//修改消息状态
+func ReadMessageByVisitorId(visitor_id string) {
+	message := &Message{
+		Status: "read",
+	}
+	DB.Model(&message).Where("visitor_id=?", visitor_id).Update(message)
+}
+
+//获取未读数
+func FindUnreadMessageNumByVisitorId(visitor_id string) uint {
+	var count uint
+	DB.Where("visitor_id=? and status=?", visitor_id, "unread").Count(&count)
+	return count
+}
+
+//查询最后一条消息
+func FindLastMessage(visitorIds []string) []Message {
+	var messages []Message
+	subQuery := DB.
+		Table("message").
+		Where(" visitor_id in (? )", visitorIds).
+		Order("id desc").
+		Limit(1024).
+		SubQuery()
+	DB.Raw("SELECT ANY_VALUE(visitor_id) visitor_id,ANY_VALUE(id) id,ANY_VALUE(content) content FROM ? message_alia GROUP BY visitor_id", subQuery).Scan(&messages)
+	//DB.Select("ANY_VALUE(visitor_id) visitor_id,MAX(ANY_VALUE(id)) id,ANY_VALUE(content) content").Group("visitor_id").Find(&messages)
+	return messages
+}
+
+//查询条数
+func CountMessage() uint {
+	var count uint
+	DB.Model(&Message{}).Count(&count)
+	return count
+}

+ 40 - 0
models/models.go

@@ -0,0 +1,40 @@
+package models
+
+import (
+	"fmt"
+	"github.com/jinzhu/gorm"
+	"github.com/wenstudio/gofly/config"
+	"time"
+)
+
+var DB *gorm.DB
+
+type Model struct {
+	ID        uint       `gorm:"primary_key" json:"id"`
+	CreatedAt time.Time  `json:"created_at"`
+	UpdatedAt time.Time  `json:"updated_at"`
+	DeletedAt *time.Time `sql:"index" json:"deleted_at"`
+}
+
+func init() {
+	mysql := config.CreateMysql(config.MysqlConf)
+	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&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 {
+		panic("数据库连接失败!")
+	}
+	DB.SingularTable(true)
+	DB.LogMode(false)
+	//DB.SetLogger(tools.Logger())
+	DB.DB().SetMaxIdleConns(10)
+	DB.DB().SetMaxOpenConns(100)
+
+	InitConfig()
+}
+func Execute(sql string) {
+	DB.Exec(sql)
+}
+func CloseDB() {
+	defer DB.Close()
+}

+ 27 - 0
models/roles.go

@@ -0,0 +1,27 @@
+package models
+
+type Role struct {
+	Id     string `json:"role_id"`
+	Name   string `json:"role_name"`
+	Method string `json:"method"`
+	Path   string `json:"path"`
+}
+
+func FindRoles() []Role {
+	var roles []Role
+	DB.Order("id desc").Find(&roles)
+	return roles
+}
+func FindRole(id interface{}) Role {
+	var role Role
+	DB.Where("id = ?", id).First(&role)
+	return role
+}
+func SaveRole(id string, name string, method string, path string) {
+	role := &Role{
+		Method: method,
+		Name:   name,
+		Path:   path,
+	}
+	DB.Model(role).Where("id=?", id).Update(role)
+}

+ 27 - 0
models/user_roles.go

@@ -0,0 +1,27 @@
+package models
+
+import (
+	"strconv"
+)
+
+type User_role struct {
+	ID     uint   `gorm:"primary_key" json:"id"`
+	UserId string `json:"user_id"`
+	RoleId uint   `json:"role_id"`
+}
+
+func FindRoleByUserId(userId interface{}) User_role {
+	var uRole User_role
+	DB.Where("user_id = ?", userId).First(&uRole)
+	return uRole
+}
+func CreateUserRole(userId uint, roleId uint) {
+	uRole := &User_role{
+		UserId: strconv.Itoa(int(userId)),
+		RoleId: roleId,
+	}
+	DB.Create(uRole)
+}
+func DeleteRoleByUserId(userId interface{}) {
+	DB.Where("user_id = ?", userId).Delete(User_role{})
+}

+ 63 - 0
models/users.go

@@ -0,0 +1,63 @@
+package models
+
+import (
+	_ "github.com/jinzhu/gorm/dialects/mysql"
+)
+
+type User struct {
+	Model
+	Name     string `json:"name"`
+	Password string `json:"password"`
+	Nickname string `json:"nickname"`
+	Avator   string `json:"avator"`
+	RoleName string `json:"role_name" sql:"-"`
+	RoleId   string `json:"role_id" sql:"-"`
+	Enabled  uint   `json:"enabled"`
+}
+
+func CreateUser(name string, password string, avator string, nickname string, enabled uint) uint {
+	user := &User{
+		Name:     name,
+		Password: password,
+		Avator:   avator,
+		Nickname: nickname,
+		Enabled:  enabled,
+	}
+	DB.Create(user)
+	return user.ID
+}
+func UpdateUser(id string, name string, password string, avator string, nickname string, enabled uint) {
+	user := &User{
+		Name:     name,
+		Avator:   avator,
+		Nickname: nickname,
+		Enabled:  enabled,
+	}
+	if password != "" {
+		user.Password = password
+	}
+	DB.Model(&User{}).Where("id = ?", id).Update(user)
+}
+func FindUser(username string) User {
+	var user User
+	DB.Where("name = ?", username).First(&user)
+	return user
+}
+func FindUserById(id interface{}) User {
+	var user User
+	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)
+	return user
+}
+func DeleteUserById(id string) {
+	DB.Where("id = ?", id).Delete(User{})
+}
+func FindUsers() []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)
+	return users
+}
+func FindUserRole(query interface{}, id interface{}) User {
+	var user User
+	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)
+	return user
+}

+ 90 - 0
models/visitors.go

@@ -0,0 +1,90 @@
+package models
+
+type Visitor struct {
+	Model
+	Name      string `json:"name"`
+	Avator    string `json:"avator"`
+	SourceIp  string `json:"source_ip"`
+	ToId      string `json:"to_id"`
+	VisitorId string `json:"visitor_id"`
+	Status    uint   `json:"status"`
+	Refer     string `json:"refer"`
+	City      string `json:"city"`
+	ClientIp  string `json:"client_ip"`
+}
+
+func CreateVisitor(name string, avator string, sourceIp string, toId string, visitorId string, refer string, city string, clientIp string) {
+	old := FindVisitorByVistorId(visitorId)
+	if old.Name != "" {
+		//更新状态上线
+		UpdateVisitor(visitorId, 1, clientIp, sourceIp, refer)
+		return
+	}
+	v := &Visitor{
+		Name:      name,
+		Avator:    avator,
+		SourceIp:  sourceIp,
+		ToId:      toId,
+		VisitorId: visitorId,
+		Status:    1,
+		Refer:     refer,
+		City:      city,
+		ClientIp:  clientIp,
+	}
+	DB.Create(v)
+}
+func FindVisitorByVistorId(visitorId string) Visitor {
+	var v Visitor
+	DB.Where("visitor_id = ?", visitorId).First(&v)
+	return v
+}
+func FindVisitors(page uint, pagesize uint) []Visitor {
+	offset := (page - 1) * pagesize
+	if offset < 0 {
+		offset = 0
+	}
+	var visitors []Visitor
+	DB.Offset(offset).Limit(pagesize).Order("status desc, updated_at desc").Find(&visitors)
+	return visitors
+}
+func FindVisitorsByKefuId(page uint, pagesize uint, kefuId string) []Visitor {
+	offset := (page - 1) * pagesize
+	if offset < 0 {
+		offset = 0
+	}
+	var visitors []Visitor
+	DB.Where("to_id=?", kefuId).Offset(offset).Limit(pagesize).Order("status desc, updated_at desc").Find(&visitors)
+	return visitors
+}
+func FindVisitorsOnline() []Visitor {
+	var visitors []Visitor
+	DB.Where("status = ?", 1).Find(&visitors)
+	return visitors
+}
+func UpdateVisitorStatus(visitorId string, status uint) {
+	visitor := Visitor{}
+	DB.Model(&visitor).Where("visitor_id = ?", visitorId).Update("status", status)
+}
+func UpdateVisitor(visitorId string, status uint, clientIp string, sourceIp string, refer string) {
+	visitor := &Visitor{
+		Status:   status,
+		ClientIp: clientIp,
+		SourceIp: sourceIp,
+		Refer:    refer,
+	}
+	DB.Model(visitor).Where("visitor_id = ?", visitorId).Update(visitor)
+}
+
+//查询条数
+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
+}

+ 47 - 0
models/welcomes.go

@@ -0,0 +1,47 @@
+package models
+
+import "time"
+
+type Welcome struct {
+	ID        uint      `gorm:"primary_key" json:"id"`
+	UserId    string    `json:"user_id"`
+	Content   string    `json:"content"`
+	IsDefault uint      `json:"is_default"`
+	Ctime     time.Time `json:"ctime"`
+}
+
+func CreateWelcome(userId string, content string) uint {
+	if userId == "" || content == "" {
+		return 0
+	}
+	w := &Welcome{
+		UserId:  userId,
+		Content: content,
+		Ctime:   time.Now(),
+	}
+	DB.Create(w)
+	return w.ID
+}
+func UpdateWelcome(userId string, id string, content string) uint {
+	if userId == "" || content == "" {
+		return 0
+	}
+	w := &Welcome{
+		Content: content,
+	}
+	DB.Model(w).Where("user_id = ? and id = ?", userId, id).Update(w)
+	return w.ID
+}
+func FindWelcomeByUserId(userId interface{}) Welcome {
+	var w Welcome
+	DB.Where("user_id = ? and is_default=?", userId, 1).First(&w)
+	return w
+}
+func FindWelcomesByUserId(userId interface{}) []Welcome {
+	var w []Welcome
+	DB.Where("user_id = ?", userId).Find(&w)
+	return w
+}
+func DeleteWelcome(userId interface{}, id string) {
+	DB.Where("user_id = ? and id = ?", userId, id).Delete(Welcome{})
+}

+ 126 - 0
readme.md

@@ -0,0 +1,126 @@
+# go-fly
+基于GO语言实现的web客服即时通讯与客服管理系统。
+
+1.使用gin http框架实现restful风格的API和template包的模板语法进行展示界面
+
+2.使用jwt-go配合gin中间件实现无状态的jwt登陆认证
+
+3.数据库实现的rbac权限配合gin中间件实现权限控制
+
+4.通过cobra进行命令行参数解析和执行对应的功能
+
+5.使用go modoule解决依赖问题
+
+6.使用swagger实现文档展示
+
+7.使用go-imap实现邮件的列表展示和读取
+
+8.使用go-smtp实现发送邮件
+
+9.使用github.com/gorilla/websocket实现即时通讯
+
+10.使用gorm配合mysql实现数据存储
+
+11.前端使用elementUI和Vue展示界面
+
+11.充分实践了struct,interface,map,slice,for range,groutine和channel管道等基础知识
+
+### 项目预览
+
+![Image text](https://img2020.cnblogs.com/blog/726254/202009/726254-20200902141655838-534372058.jpg)
+
+![Image text](https://img2020.cnblogs.com/blog/726254/202009/726254-20200902141707515-1201702349.jpg)
+
+![Image text](https://img2020.cnblogs.com/blog/726254/202009/726254-20200902141723679-927777888.png)
+
+![Image text](https://img2020.cnblogs.com/blog/726254/202009/726254-20200902141736713-1155907367.jpg)
+
+![Image text](https://img2020.cnblogs.com/blog/726254/202009/726254-20200902141745935-1312775469.jpg)
+
+
+### 安装使用
+
+
+1. 先安装和运行mysql , 创建go-fly数据库,并导入*.sql创建表结构与数据.
+
+2. 基于go module使用
+
+   go env -w GO111MODULE=on
+   
+   go env -w GOPROXY=https://goproxy.cn,direct
+   
+   在任意目录 git clone https://github.com/taoshihan1991/go-fly.git
+   
+   进入go-fly 目录
+   
+   在config目录mysql.json中配置数据库
+```php
+{
+	"Server":"127.0.0.1",
+	"Port":"3306",
+	"Database":"go-fly",
+	"Username":"go-fly",
+	"Password":"go-fly"
+}
+```
+
+
+3. 源码运行 go run go-fly.go server port 8081
+
+4. 源码打包 go build go-fly.go 会生成go-fly可以执行文件
+
+5. 导入数据库(会删除表清空数据) ./go-fly install
+
+6. 二进制文件运行
+ 
+   linux:   ./go-fly server port 8081
+   
+   windows: go-fly.exe server port 8081 
+
+### nginx部署
+
+访问:https://gofly.sopans.com
+
+参考支持https的部署示例 , 注意反向代理的端口号和证书地址
+
+```php
+server {
+       listen 443 ssl http2;
+        ssl on;
+        ssl_certificate   conf.d/cert/4263285_gofly.sopans.com.pem;
+        ssl_certificate_key  conf.d/cert/4263285_gofly.sopans.com.key;
+        ssl_session_timeout 5m;
+        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
+        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+        ssl_prefer_server_ciphers on;
+        #listen          80; 
+        server_name  gofly.sopans.com;
+        access_log  /var/log/nginx/gofly.sopans.com.access.log  main;
+        location / {
+                proxy_pass http://127.0.0.1:8081;
+                    proxy_http_version 1.1;
+                    proxy_set_header X-Real-IP $remote_addr;
+                    proxy_set_header Upgrade $http_upgrade;
+                    proxy_set_header Connection "upgrade";
+                    proxy_set_header Origin "";
+        }
+}
+server{
+       listen 80;
+        server_name  gofly.sopans.com;
+        access_log  /var/log/nginx/gofly.sopans.com.access.log  main;
+        location / {
+                proxy_pass http://127.0.0.1:8081;
+                    proxy_http_version 1.1;
+                    proxy_set_header X-Real-IP $remote_addr;
+                    proxy_set_header Upgrade $http_upgrade;
+                    proxy_set_header Connection "upgrade";
+                    proxy_set_header Origin "";
+        }
+}
+```
+
+### 生成文档
+
+1. 需要先安装swag
+2. 在根目录swag init -g go-fly.go

+ 75 - 0
router/api.go

@@ -0,0 +1,75 @@
+package router
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/wenstudio/gofly/controller"
+	"github.com/wenstudio/gofly/middleware"
+	"github.com/wenstudio/gofly/ws"
+)
+
+func InitApiRouter(engine *gin.Engine) {
+	//首页
+	engine.GET("/", controller.Index)
+	engine.POST("/check", controller.LoginCheckPass)
+	engine.POST("/check_auth", middleware.JwtApiMiddleware, controller.MainCheckAuth)
+	engine.GET("/userinfo", middleware.JwtApiMiddleware, controller.GetKefuInfoAll)
+
+	//前后聊天
+	engine.GET("/chat_server", middleware.Ipblack, controller.NewChatServer)
+	engine.GET("/ws_kefu", middleware.JwtApiMiddleware, ws.NewKefuServer)
+	engine.GET("/ws_visitor", ws.NewVisitorServer)
+
+	//获取消息
+	engine.GET("/messages", controller.GetVisitorMessage)
+	engine.GET("/2/messages", controller.GetMessagesV2)
+	engine.GET("/message_notice", controller.SendVisitorNotice)
+	//发送单条消息
+	engine.POST("/message", middleware.Ipblack, controller.SendMessage)
+	engine.POST("/2/message", middleware.Ipblack, controller.SendMessageV2)
+	//发送关闭消息
+	engine.GET("/message_close", controller.SendCloseMessage)
+	//上传文件
+	engine.POST("/uploadimg", middleware.Ipblack, controller.UploadImg)
+	//获取未读消息数
+	engine.GET("/message_status", controller.GetVisitorMessage)
+	//设置消息已读
+	engine.POST("/message_status", controller.GetVisitorMessage)
+
+	//获取客服信息
+	engine.GET("/kefuinfo", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.GetKefuInfo)
+	engine.GET("/kefuinfo_setting", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.GetKefuInfoSetting)
+	engine.POST("/kefuinfo", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.PostKefuInfo)
+	engine.DELETE("/kefuinfo", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.DeleteKefuInfo)
+	engine.GET("/kefulist", controller.GetKefuList)
+	engine.GET("/kefulist_enabled", controller.GetKefuListEnabled)
+	//角色列表
+	engine.GET("/roles", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.GetRoleList)
+	engine.POST("/role", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.PostRole)
+	//邮件夹列表
+	engine.GET("/folders", controller.GetFolders)
+
+	engine.GET("/mysql", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.MysqlGetConf)
+	engine.POST("/mysql", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.MysqlSetConf)
+	engine.GET("/visitors_online", controller.GetVisitorOnlines)
+	engine.GET("/clear_online_tcp", controller.DeleteOnlineTcp)
+	engine.POST("/visitor_login", middleware.Ipblack, controller.PostVisitorLogin)
+	engine.POST("/visitor", controller.PostVisitor)
+	engine.GET("/visitor", middleware.JwtApiMiddleware, controller.GetVisitor)
+	engine.GET("/visitors", middleware.JwtApiMiddleware, controller.GetVisitors)
+	engine.GET("/statistics", middleware.JwtApiMiddleware, controller.GetStatistics)
+	//前台接口
+	engine.GET("/about", controller.GetAbout)
+	engine.POST("/about", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.PostAbout)
+	engine.GET("/notice", middleware.SetLanguage, controller.GetNotice)
+	engine.POST("/notice", middleware.JwtApiMiddleware, controller.PostNotice)
+	engine.DELETE("/notice", middleware.JwtApiMiddleware, controller.DelNotice)
+	engine.POST("/notice_save", middleware.JwtApiMiddleware, controller.PostNoticeSave)
+	engine.GET("/notices", middleware.JwtApiMiddleware, controller.GetNotices)
+	engine.POST("/ipblack", middleware.JwtApiMiddleware, controller.PostIpblack)
+	engine.DELETE("/ipblack", middleware.JwtApiMiddleware, controller.DelIpblack)
+	engine.GET("/ipblacks_all", middleware.JwtApiMiddleware, controller.GetIpblacks)
+	engine.GET("/configs", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.GetConfigs)
+	engine.POST("/config", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.PostConfig)
+	//微信接口
+	engine.GET("/micro_program", controller.GetCheckWeixinSign)
+}

+ 30 - 0
router/view.go

@@ -0,0 +1,30 @@
+package router
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/wenstudio/gofly/middleware"
+	"github.com/wenstudio/gofly/tmpl"
+)
+
+func InitViewRouter(engine *gin.Engine) {
+	engine.GET("/index_:lang", middleware.SetLanguage, tmpl.PageIndex)
+	engine.GET("/login", tmpl.PageLogin)
+	engine.GET("/chat_page", middleware.SetLanguage, tmpl.PageChat)
+	engine.GET("/chatIndex", middleware.SetLanguage, tmpl.PageChat)
+	engine.GET("/chatKfIndex", tmpl.PageKfChat)
+	engine.GET("/main", middleware.JwtPageMiddleware, tmpl.PageMain)
+	engine.GET("/chat_main", middleware.JwtPageMiddleware, tmpl.PageChatMain)
+	engine.GET("/setting", tmpl.PageSetting)
+	engine.GET("/setting_statistics", tmpl.PageSettingStatis)
+	engine.GET("/setting_indexpage", tmpl.PageSettingIndexPage)
+	engine.GET("/setting_mysql", tmpl.PageSettingMysql)
+	engine.GET("/setting_welcome", tmpl.PageSettingWelcome)
+	engine.GET("/setting_deploy", tmpl.PageSettingDeploy)
+	engine.GET("/setting_kefu_list", tmpl.PageKefuList)
+	engine.GET("/setting_ipblack", tmpl.PageIpblack)
+	engine.GET("/setting_config", tmpl.PageConfig)
+	engine.GET("/mail_list", tmpl.PageMailList)
+	engine.GET("/roles_list", tmpl.PageRoleList)
+	engine.GET("/webjs", tmpl.PageWebJs)
+	engine.GET("/webcss", tmpl.PageWebCss)
+}

+ 317 - 0
static/css/common.css

@@ -0,0 +1,317 @@
+*{padding:0;margin:0}
+.floatRight{float: right;}
+.clear{clear: both;}
+
+.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
+    font-family: inherit;
+    font-weight: 500;
+    line-height: 1.2;
+    color: inherit;
+}
+.el-menu.el-menu--horizontal{
+    border-bottom: none;
+    padding-bottom: 4px;
+}
+.el-menu--horizontal>.el-menu-item.is-active{
+    border-bottom: 3px solid #409EFF;
+}
+.chatBg .el-tabs__header{margin: 0;}
+.faceBtn, .faceBtn:after, .faceBtn {
+    border: 1px solid;
+}
+.iconBtns{
+    border-top:1px solid #e4e4e4;
+    border-bottom:1px solid #e4e4e4;
+    padding: 2px 0;
+}
+.visitorFaceBtn{
+    float: left;
+    margin-left: 5px;
+}
+.visitorFaceBox{
+    position: absolute;
+    bottom: 105px;
+}
+.kefuFaceBox{
+    position: absolute;
+    bottom: 0px;
+    z-index: 999;
+}
+.faceBtn {
+    -webkit-border-radius: 50%;
+    -moz-border-radius: 50%;
+    -o-border-radius: 50%;
+    border-radius: 50%;
+    height: 28px;
+    width: 28px;
+    display: inline-block;
+    vertical-align: middle;
+    font-style: normal;
+    color: #9da0a0;
+    text-align: left;
+    text-indent: -9999px;
+    direction: ltr;
+    position: relative;
+    cursor: pointer;
+}
+.faceBtn:before {
+    content: '';
+    pointer-events: none;
+    -webkit-border-radius: 50%;
+    -moz-border-radius: 50%;
+    -o-border-radius: 50%;
+    border-radius: 50%;
+    box-shadow: 8px 0 0 0, 0 0 0 2px inset;
+    height: 4px;
+    width: 4px;
+    left: 7px;
+    position: absolute;
+    top: 29%;
+}
+.faceBtn:after {
+    content: '';
+    pointer-events: none;
+    -webkit-border-radius: 50%;
+    -moz-border-radius: 50%;
+    -o-border-radius: 50%;
+    border-radius: 50%;
+    -webkit-transform: translateX(-50%);
+    -moz-transform: translateX(-50%);
+    -ms-transform: translateX(-50%);
+    -o-transform: translateX(-50%);
+    transform: translateX(-50%);
+    border-top-color: transparent;
+    border-left-color: transparent;
+    border-right-color: transparent;
+    height: 15px;
+    left: 50%;
+    position: absolute;
+    top: 10%;
+    width: 15px;
+}
+.imageBtn {
+    width: 32px;
+    height: 23px;
+    overflow: hidden;
+    display: inline-block;
+    vertical-align: middle;
+    position: relative;
+    font-style: normal;
+    color: #9da0a0;
+    text-align: left;
+    text-indent: -9999px;
+    direction: ltr;
+    border: 1px solid;
+}
+.imageBtn:before {
+    content: '';
+    position: absolute;
+    width: 17px;
+    height: 16px;
+    left: -2px;
+    top: 10px;
+    -webkit-transform: rotate(45deg);
+    -moz-transform: rotate(45deg);
+    -ms-transform: rotate(45deg);
+    -o-transform: rotate(45deg);
+    transform: rotate(45deg);
+    box-shadow: inset 0 0 0 32px, 10px -6px 0 0;
+}
+.imageBtn:after {
+    content: '';
+    -webkit-border-radius: 50%;
+    -moz-border-radius: 50%;
+    -o-border-radius: 50%;
+    border-radius: 50%;
+    position: absolute;
+    width: 3px;
+    height: 3px;
+    box-shadow: inset 0 0 0 32px;
+    top: 5px;
+    right: 5px
+}
+.visitorImageBtn{
+    float: left;
+    margin-left: 20px;
+    margin-top: 2px;
+}
+.faceBox{
+    width: 100%;
+    background: #fff;
+    z-index: 99999999;
+    padding: 2px;
+    display: none;
+}
+.faceBoxList{
+    list-style: none;
+    padding: 0;
+    margin: 0;
+}
+.faceBoxList li{
+    cursor: pointer;
+    float: left;
+    border: 1px solid #e8e8e8;
+    width: 28px;
+    overflow: hidden;
+    margin: -1px 0 0 -1px;
+    padding: 4px 2px;
+    text-align: center;
+}
+
+@-webkit-keyframes bounce-up {
+    25% {-webkit-transform: translateY(10px);}
+    50%, 100% {-webkit-transform: translateY(0);}
+    75% {-webkit-transform: translateY(-10px);}
+}
+
+@keyframes bounce-up {
+    25% {transform: translateY(10px);}
+    50%, 100% {transform: translateY(0);}
+    75% {transform: translateY(-10px);}
+}
+.animate-bounce-up{ -webkit-animation: bounce-up 1.4s linear infinite;animation: bounce-up 1.4s linear infinite;}
+.mainLogo{
+    font-size: 20px;
+    font-weight: bold;
+    color: #fff;
+}
+.mainVersion{
+    margin-left: 5px;
+    font-size: 12px;
+}
+.el-submenu__title i{
+    color: #fff;
+}
+.el-container{
+    height: 100%;
+}
+.el-aside{
+    height: 100%;
+    background: #222d32;
+}
+.textDark {color: #343a40;}
+.bgInfo {background-color: #17a2b8}
+.bgSuccess {background-color: #28a745}
+.bgDanger {background-color: #dc3545}
+.bgInfo {background-color: #17a2b8}
+.smallBox {
+    border-radius: .25rem;
+    box-shadow: 0 0 1px rgba(0,0,0,.125), 0 1px 3px rgba(0,0,0,.2);
+    display: block;
+    margin-bottom: 20px;
+    position: relative;
+    padding: 10px;
+    color: #fff;
+}
+.settingMain h2{
+    margin-bottom: 20px;
+}
+.settingMain h3{
+    font-size: 24px;
+    margin-bottom: 10px;
+}
+.bigPic{
+    background: #ccc;
+    width: 100%;
+    height: 100%;
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: 999;
+    display: none;
+    text-align: center;
+}
+/*客服聊天主板*/
+.chatLeft .el-tabs__nav,.chatRight .el-tabs__nav {
+    margin-left: 20px;
+}
+.onlineUsers {
+    padding: 5px;
+    height: 40px;
+    line-height: 40px;
+    font-size: 14px;
+    border-bottom: solid 1px #e6e6e6;
+}
+.onlineUsers a{
+    color: #333;
+}
+.onlineUsers:hover,.onlineUsers.cur{background-color: #f0f9eb;color: #67C23A;}
+.imgGray {-webkit-filter: grayscale(100%);-ms-filter: grayscale(100%);filter: grayscale(100%);filter: gray;color:#888;}
+.hasLastMsg{line-height: normal;}
+.lastNewMsg{font-size: 12px;color: #7f7f7f;margin-top: 4px;overflow: hidden;height: 16px;}
+/*客服页*/
+.chatKfPageApp{
+    max-width: 800px;
+    margin:0 auto;
+}
+.chatCenter{background: #fff;max-width: 800px;margin: 0 auto;}
+.chatContext{
+    width: 100%;
+    text-align: left;
+    position: relative;
+    margin-bottom: 105px;
+}
+.chatBox{
+    /*overflow-y: auto;*/
+    overflow-x: hidden;
+    /*margin-bottom: 80px;*/
+}
+.chatVisitorPage .chatBox{
+    min-height: 540px;
+    padding: 0 4px;
+}
+.chatBox .el-col{margin:10px 0;}
+.chatUser{
+    line-height: 24px;
+    font-size: 12px;
+    white-space: nowrap;
+    color: #999;
+    text-align: left;
+}
+.chatContent{
+    background-color: rgb(166,212,242);
+    color: #000;
+    border: 1px solid rgb(152, 199, 230);
+    padding: 8px 15px;
+    word-break: break-all;
+    position: relative;
+    border-radius: 5px;
+    display: inline-block;
+    margin-left: 6px;
+}
+.chatContent:after {
+    content: '';
+    position: absolute;
+    left: -10px;
+    top: 13px;
+    width: 0;
+    height: 0;
+    border-style: dashed;
+    border-color: transparent;
+    overflow: hidden;
+    border-width: 10px;
+    border-top-style: solid;
+    border-top-color: rgb(166,212,242);
+}
+.chatBoxMe .chatContent{float: right;background-color: rgb(152,225,101);border: 1px solid rgb(145, 215, 96);}
+.chatBoxMe .chatContent:after{border-top-color: rgb(152,225,101);}
+.chatBoxMe .el-col-3{float: right;text-align: right;}
+.chatBoxMe .chatUser{text-align: right}
+.chatBoxMe .chatContent:after{left:auto;right: -10px;}
+.chatArea{float: left;width: 85%;margin: 4px 0 0 4px;}
+.chatArea .el-textarea__inner{padding: 1px 5px}
+.btnArea{width: 10%;float: right;}
+@media screen and (max-width: 500px) {
+    body{background: #fff}
+    .chatArea {width: 70%;}
+    .btnArea{width: 20%;}
+}
+
+.chatTitle{height: 30px;line-height: 30px;color: #1989fa}
+.chatBoxSend{background: #f5f5f5;position: fixed;bottom: 0px;width: 100%;height: 105px;max-width: 800px;}
+.chatBoxSendBtn{float: right;margin: 12px 4px 0 0;}
+.footContact{text-align: center;}
+.footContact a{font-size: 12px;color: #999;text-decoration: none;}
+.chatTime{text-align: center;color: #bbb;margin: 5px 0;font-size: 12px;}
+.chatTimeHide{display: none;}
+.clear{clear:both;}

File diff suppressed because it is too large
+ 0 - 0
static/css/emojione.min.css


+ 37 - 0
static/css/front.css

@@ -0,0 +1,37 @@
+::-webkit-scrollbar
+{
+    width: 5px;
+    height: 110px;
+    background-color: #F5F5F5;
+}
+/*定义滚动条轨道 内阴影+圆角*/
+::-webkit-scrollbar-track
+{
+    -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
+    border-radius: 10px;
+    background-color: #F5F5F5;
+}
+/*定义滑块 内阴影+圆角*/
+::-webkit-scrollbar-thumb
+{
+    border-radius: 10px;
+    -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
+    background-color: #bdbdbd;
+}
+/*滑块效果*/
+::-webkit-scrollbar-thumb:hover
+{
+    border-radius: 5px;
+    -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
+    background: rgba(0,0,0,0.4);
+}
+/*IE滚动条颜色*/
+html {
+    scrollbar-face-color:#bfbfbf;/*滚动条颜色*/
+    scrollbar-highlight-color:#000;
+    scrollbar-3dlight-color:#000;
+    scrollbar-darkshadow-color:#000;
+    scrollbar-Shadow-color:#adadad;/*滑块边色*/
+    scrollbar-arrow-color:rgba(0,0,0,0.4);/*箭头颜色*/
+    scrollbar-track-color:#eeeeee;/*背景颜色*/
+}

+ 62 - 0
static/css/gofly-front.css

@@ -0,0 +1,62 @@
+.launchButton{
+    position: fixed!important;
+    bottom: 55px!important;
+    right: 20px!important;
+    left: auto!important;
+    height: 48px!important;
+    width: auto!important;
+    z-index: 10000000000000!important;
+    background: #20B2BB!important;
+    border: 0!important;
+    border-radius: 100px!important;
+    box-shadow: 0 3px 15px 0 rgba(0,0,0,.25)!important;
+    box-sizing: border-box!important;
+    padding: 0 20px!important;
+    transition: all .4s,bottom .8s ease-in-out!important;
+    cursor: pointer!important;
+    outline: 0!important;
+    display: inline-block;
+    margin: 0!important;
+    -webkit-font-smoothing: antialiased!important;
+    -webkit-tap-highlight-color: transparent!important;
+    animation-name: loadBubble;
+    animation-iteration-count: 1;
+    animation-timing-function: ease-in-out;
+    animation-duration: .2s;
+    color: #ffffff !important;
+}
+.launchButton:hover {
+    box-shadow: 0 3px 20px 0 rgba(0,0,0,.5)!important;
+}
+.launchButton svg{
+    width: 28px;
+    height: 48px;
+}
+.launchButtonText {
+    color: #fff!important;
+    display: inline-block!important;
+    font-family: -apple-system,BlinkMacSystemFont,segoe ui,Roboto,Oxygen,Ubuntu,Cantarell,fira sans,droid sans,helvetica neue,sans-serif!important;
+    font-size: 1em!important;
+    line-height: 48px!important;
+    font-weight: 700!important;
+    margin: 0 0 0 12px!important;
+    overflow: hidden!important;
+    text-overflow: ellipsis!important;
+    vertical-align: top!important;
+    white-space: nowrap!important;
+    padding: 0!important;
+    transition: .6s ease-in-out!important;
+}
+
+@-webkit-keyframes bounce-up {
+    25% {-webkit-transform: translateY(6px);}
+    50%, 100% {-webkit-transform: translateY(0);}
+    75% {-webkit-transform: translateY(-6px);}
+}
+
+@keyframes bounce-up {
+    25% {transform: translateY(6px);}
+    50%, 100% {transform: translateY(0);}
+    75% {transform: translateY(-6px);}
+}
+.animateUpDown{ -webkit-animation: bounce-up 1.4s linear infinite;animation: bounce-up 1.4s linear infinite;}

+ 82 - 0
static/html/chat_kf_page.html

@@ -0,0 +1,82 @@
+<html lang="cn">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <meta name="description" content="">
+    <meta name="author" content="陶士涵">
+    <title>美天旺客服页</title>
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-ui@2.13.1/lib/theme-chalk/index.css">
+    <script src="/static/js/functions.js?v=0.1.1"></script>
+    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/vue-router/dist/vue-router.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/element-ui@2.13.1/lib/index.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
+    <link rel="stylesheet" href="/static/css/common.css?v=0.1.1" />
+    <style>
+        html,
+        body {
+            height: 100%;
+            background: #fff;
+        }
+    </style>
+</head>
+<body>
+<div id="app" class="chatKfPageApp">
+    <router-view></router-view>
+</div>
+<template id="chatKfIndex">
+    <el-row>
+        <div style="cursor:pointer" v-for="item in visitors" :key="item.uid" class="onlineUsers">
+            <router-link :to="'/chatKfBox/'+item.uid">
+            <el-col :span="6">
+                <el-avatar :size="40" :src="item.avator"></el-avatar>
+            </el-col>
+            <el-col :span="18">
+                <{item.username}>
+            </el-col>
+            </router-link>
+        </div>
+    </el-row>
+</template>
+<template id="chatBox">
+    <div>
+        <div class="chatContext">
+            <div class="chatBox">
+                <el-row :gutter="2" v-for="v in msgList" v-bind:class="{'chatBoxMe': v.is_kefu==true}">
+                    <div class="chatTime" v-bind:class="{'chatTimeHide': v.show_time==false}"><{v.time}></div>
+                    <el-col :span="3"><el-avatar :src="v.avator"></el-avatar></el-col>
+                    <el-col :span="21">
+                        <div class="chatUser"><{v.name}></div>
+                        <div class="chatContent" v-html="v.content"></div>
+                    </el-col>
+                    <div class="clear"></div>
+                </el-row>
+            </div>
+        </div>
+        <div class="chatBoxSend">
+            <div class="iconBtns">
+                <div class="faceBtn visitorFaceBtn"></div>
+                <div class="imageBtn visitorImageBtn" id="uploadImg" v-on:click="uploadImg('/uploadimg')"></div>
+                <div class="clear"></div>
+            </div>
+            <el-input type="textarea" class="chatArea" v-model="messageContent" v-on:keyup.enter.native="chatToUser"></el-input>
+            <div class="faceBox visitorFaceBox">
+                <ul class="faceBoxList">
+                    <li v-on:click="faceIconClick(i)" class="faceIcon" v-for="(v,i) in face"  :title="v.name"><img :src=v.path></li>
+                </ul>
+                <div class="clear"></div>
+            </div>
+            <div class="btnArea">
+                <el-button type="primary" class="chatBoxSendBtn" size="small" v-on:click="chatToUser">发送</el-button>
+            </div>
+            <div class="clear"></div>
+        </div>
+    </div>
+</template>
+</body>
+<script src="https://cdn.bootcss.com/reconnecting-websocket/1.0.0/reconnecting-websocket.min.js"></script>
+<script>
+    var TOKEN='{{.Token}}';
+</script>
+<script src="/static/js/chat-kf-page.js?v=0.1.1"></script>
+</html>

+ 206 - 0
static/html/chat_main.html

@@ -0,0 +1,206 @@
+<html lang="cn">
+<head>
+    <meta charset="utf-8">
+    <meta name="description" content="">
+    <meta name="author" content="陶士涵">
+    <title>聊天界面</title>
+    <link rel="stylesheet" href="/static/css/common.css">
+    <link rel="stylesheet" href="/static/css/emojione.min.css">
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-ui@2.13.1/lib/theme-chalk/index.css">
+    <script src="/static/js/functions.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/element-ui@2.13.1/lib/index.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
+    <script src="https://cdn.bootcss.com/reconnecting-websocket/1.0.0/reconnecting-websocket.min.js"></script>
+    <style>
+        html, body {height: 100%;padding: 0;margin: 0;background-color: #f5f5f5;}
+        .el-row{width:100%}#app{margin-top: 10px;}
+        .chatBg{min-height: 100%;background: #fff;border: solid 1px #e6e6e6;overflow: hidden;}
+        .chatLeft{    margin-left: 4px;}
+        .sw-bg{background: #fff;border: solid 1px #e6e6e6;boder-top:none;padding:5px 10px;}
+        .chatBgContext .el-row{margin-bottom: 5px;}
+        .chatBgContext{position: relative;}
+        .chatUser{
+            line-height: 24px;
+            font-size: 12px;
+            white-space: nowrap;
+            color: #999;
+        }
+        .chatContent{
+            text-align: left;
+            background-color: rgb(166,212,242);
+            color: #000;
+            border: 1px solid rgb(152, 199, 230);
+            padding: 8px 15px;
+            min-height: 26px;
+            word-break: break-all;
+            position: relative;
+            border-radius: 5px;
+            display: inline-block;
+        }
+        .chatContent:after {
+            content: '';
+            position: absolute;
+            left: -10px;
+            top: 13px;
+            width: 0;
+            height: 0;
+            border-style: dashed;
+            border-color: transparent;
+            overflow: hidden;
+            border-width: 10px;
+            border-top-style: solid;
+            border-top-color: rgb(166,212,242);
+        }
+        .chatBoxMe .chatContent{float: right;background-color: rgb(152,225,101);border: 1px solid rgb(145, 215, 96);}
+        .chatBoxMe .chatContent:after{border-top-color: rgb(152,225,101);}
+        .chatBoxMe .el-col-3{float: right;text-align: right;}
+        .chatBoxMe .chatUser{text-align: right}
+        .chatBoxMe .chatContent:after{left:auto;right: -10px;}
+        .chatArea{margin: 10px 0;}
+        .chatBox{max-height: 350px;overflow-y: auto;overflow-x: hidden;}
+        .chatTime{text-align: center;color: #bbb;margin: 5px 0;font-size: 12px;}
+    </style>
+</head>
+<body>
+<div id="app">
+    <template>
+            <el-row :gutter="2">
+                <el-col :span="6">
+                    <div class="chatBg chatLeft">
+                        <el-tabs v-model="leftTabActive" @tab-click="handleTabClick">
+                            <el-tab-pane label="在线用户" name="first">
+                                <el-row  v-for="item in users" :key="item.uid" class="">
+                                    <div :title="item.last_message" style="cursor:pointer" class="onlineUsers hasLastMsg" v-bind:class="{'cur': item.uid==currentGuest }" v-on:click="talkTo(item.uid,item.username)">
+                                        <el-col :span="4">
+                                            <el-avatar :size="40" :src="item.avator"></el-avatar>
+                                        </el-col>
+                                        <el-col :span="16">
+                                            <div><{item.username}></div>
+                                            <div class="lastNewMsg"><{item.last_message}></div>
+                                        </el-col>
+                                    </div>
+                                </el-row>
+                            </el-tab-pane>
+                            <el-tab-pane label="已接访客" name="second">
+                                <el-row  v-for="item in visitors" :key="item.uid" class="">
+                                    <div style="cursor:pointer" class="onlineUsers" v-bind:class="{'cur': item.visitor_id==currentGuest }" v-on:click="talkTo(item.visitor_id,item.name)">
+                                        <el-col :span="4">
+                                            <el-avatar v-bind:class="{'imgGray': item.status==0 }" :size="40" :src="item.avator"></el-avatar>
+                                        </el-col>
+                                        <el-col :span="16" v-bind:class="{'imgGray': item.status==0 }">
+                                            <{item.name}>
+                                        </el-col>
+                                    </div>
+                                </el-row>
+                                <el-pagination
+                                        background
+                                        @current-change="visitorPage"
+                                        :current-page="visitorCurrentPage"
+                                        layout="prev,pager, next"
+                                        :page-size="visitorPageSize"
+                                        :total="visitorCount">
+                                </el-pagination>
+                            </el-tab-pane>
+                        </el-tabs>
+                    </div>
+                </el-col>
+                <el-col :span="12">
+                    <div class="sw-bg chatBgContext">
+                        <el-alert
+                                :title="chatTitle"
+                                type="success">
+                        </el-alert>
+                        <div class="chatBox">
+                            <el-row :gutter="2" v-for="v in msgList" v-bind:class="{'chatBoxMe': v.is_kefu==true}">
+                                <div class="chatTime"><{v.time}></div>
+                                <el-col :span="3"><el-avatar :size="60" :src="v.avator"></el-avatar></el-col>
+                                <el-col :span="21">
+                                    <div class="chatUser"><{v.name}></div>
+                                    <div class="chatContent" v-html="v.content"></div>
+                                </el-col>
+                            </el-row>
+                        </div>
+                        <div class="faceBox kefuFaceBox">
+                            <ul class="faceBoxList">
+                                <li v-on:click="faceIconClick(i)" class="faceIcon" v-for="(v,i) in face"  :title="v">
+<!--                                    <img :src=v.path>-->
+                                    <span class="em" :class="v"></span>
+                                </li>
+                            </ul>
+                            <div class="clear"></div>
+                        </div>
+                        <el-input type="textarea" class="chatArea" v-model="messageContent"  v-on:keyup.enter.native="chatToUser"></el-input>
+                        <div class="faceBtn"></div>
+                        <div class="imageBtn" id="uploadImg" v-on:click="uploadImg('/uploadimg')"></div>
+                        <el-button class="floatRight" type="primary" v-on:click="chatToUser">发送</el-button>
+                        <div class="clear"></div>
+                    </div>
+                </el-col>
+                <el-col :span="6">
+                    <div   class="chatBg chatRight">
+                        <el-tabs v-model="rightTabActive" @tab-click="handleTabClick">
+                            <el-tab-pane label="访客信息" name="visitorInfo">
+                                <el-menu>
+                                    <el-tooltip content="点击加入黑名单" placement="left">
+                                        <el-menu-item v-on:click="addIpblack(visitor.source_ip)" title="点击加入黑名单" style="padding-left:2px;">
+                                            <i class="el-icon-s-tools"></i>
+                                            <span slot="title">ClientIP:<{visitor.source_ip}></span>
+                                        </el-menu-item>
+                                    </el-tooltip>
+                                    <el-tooltip content="点击加入黑名单" placement="left">
+                                        <el-menu-item v-on:click="addIpblack(visitor.client_ip)" title="点击加入黑名单"  style="padding-left:2px;">
+                                            <i class="el-icon-s-tools"></i>
+                                            <span slot="title">IP:<{visitor.client_ip}></span>
+                                        </el-menu-item>
+                                    </el-tooltip>
+                                    <el-menu-item v-on:click="openUrl('https://www.baidu.com/s?wd='+visitor.client_ip)" style="padding-left:2px;">
+                                        <i class="el-icon-s-tools"></i>
+                                        <span slot="title">城市:<{visitor.city}></span>
+                                    </el-menu-item>
+                                    <el-popover
+                                            ref="popover"
+                                            placement="top"
+                                            title="来源"
+                                            width="300"
+                                            trigger="hover"
+                                            :content="visitor.refer">
+                                    </el-popover>
+                                    <el-menu-item v-popover:popover style="padding-left:2px;">
+                                        <i class="el-icon-s-tools"></i>
+                                        <span slot="title" >来源:<{visitor.refer}></span>
+                                    </el-menu-item>
+                                    <el-menu-item style="padding-left:2px;">
+                                        <i class="el-icon-s-tools"></i>
+                                        <span slot="title">时间:<{visitor.created_at}></span>
+                                    </el-menu-item>
+                                    <el-tooltip content="点击关闭连接" placement="left">
+                                        <el-menu-item v-on:click="closeVisitor(visitor.visitor_id)" style="padding-left:2px;">
+                                            <i class="el-icon-s-tools"></i>
+                                            <span slot="title">状态:<{visitor.status}></span>
+                                        </el-menu-item>
+                                    </el-tooltip>
+                                </el-menu>
+                            </el-tab-pane>
+                            <el-tab-pane label="黑名单" name="blackList">
+                                <el-row  v-for="item in visitors" :key="item.uid" class="">
+                                    <div style="cursor:pointer" class="onlineUsers imgGray">
+                                            待开发
+                                    </div>
+                                </el-row>
+                            </el-tab-pane>
+                        </el-tabs>
+
+                    </div>
+                </el-col>
+            </el-row>
+        <!--图片放大-->
+        <div id="bigPic" class="bigPic">
+            <img src="/static/images/3.jpg"/>
+        </div>
+        <!--//图片放大-->
+    </template>
+</div>
+</body>
+<script src="/static/js/chat-main.js?v=0.1.1"></script>
+</html>

+ 83 - 0
static/html/chat_page.html

@@ -0,0 +1,83 @@
+<html lang="cn">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <meta name="description" content="">
+    <meta name="author" content="美天旺">
+    <title>美天旺在线咨询</title>
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-ui@2.13.1/lib/theme-chalk/index.css">
+    <script src="/static/js/functions.js?v=0.1.1"></script>
+    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/element-ui@2.13.1/lib/index.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
+    <!-- Bootstrap core CSS -->
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.4.1/dist/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
+    <link rel="stylesheet" href="/static/css/front.css?v=0.2.3" />
+    <link rel="stylesheet" href="/static/css/common.css?v=0.2.3" />
+    <link rel="stylesheet" href="/static/css/emojione.min.css">
+</head>
+<body>
+<div id="app"  class="chatCenter">
+    <template>
+        <!--客服代码-->
+        <div class="chatContext chatVisitorPage">
+            <div class="chatBox">
+                <el-alert
+                        style="margin-bottom: 10px;font-size: 12px;"
+                        title="公告 : 访客您好,欢迎使用客服系統,感谢您的支持。"
+                        type="success">
+                </el-alert>
+                <el-alert
+                        style="margin-bottom: 10px;"
+                        :title="chatTitle"
+                        :closable="false"
+                        type="success">
+                </el-alert>
+                <el-row :gutter="2" v-for="v in msgList" v-bind:class="{'chatBoxMe': v.is_kefu==true}">
+                    <div class="chatTime" v-bind:class="{'chatTimeHide': v.show_time==false}"><{v.time}></div>
+                    <el-col :span="3"><el-avatar :src="v.avator"></el-avatar></el-col>
+                    <el-col :span="21">
+                        <div class="chatUser"><{v.name}></div>
+                        <div class="chatContent" v-html="v.content"></div>
+                    </el-col>
+                    <div class="clear"></div>
+                </el-row>
+            </div>
+        </div>
+        <div class="chatBoxSend">
+            <div class="iconBtns">
+                <div class="faceBtn visitorFaceBtn"></div>
+                <div class="imageBtn visitorImageBtn" id="uploadImg" v-on:click="uploadImg('/uploadimg')"></div>
+                <div class="clear"></div>
+            </div>
+            <el-input type="textarea" class="chatArea" v-model="messageContent" v-on:keyup.enter.native="chatToUser"></el-input>
+            <div class="faceBox visitorFaceBox">
+                <ul class="faceBoxList">
+                    <li v-on:click="faceIconClick(i)" class="faceIcon" v-for="(v,i) in face"  :title="v">
+                        <span class="em" :class="v"></span>
+                    </li>
+                </ul>
+                <div class="clear"></div>
+            </div>
+            <div class="btnArea">
+                <el-button type="primary" class="chatBoxSendBtn" size="small" v-on:click="chatToUser">{{.SendBtn}}</el-button>
+            </div>
+            <div class="footContact clear">
+                <a href="https://github.com/taoshihan1991/go-fly" target="_blank">美天旺客服 美天旺提供技术支持</a>
+            </div>
+        </div>
+        <!--//客服代码-->
+        <audio id="chatMessageAudio">
+            <source id="chatMessageAudioSource" src="/static/images/alert.mp3" type="audio/mpeg" />
+        </audio>
+    </template>
+</div>
+</body>
+<script src="//pv.sohu.com/cityjson?ie=utf-8"></script>
+<script src="https://cdn.bootcss.com/reconnecting-websocket/1.0.0/reconnecting-websocket.min.js"></script>
+<script>
+    var KEFU_ID='{{.KEFU_ID}}';
+    var REFER='{{.Refer}}';
+</script>
+<script src="/static/js/chat-page.js?v=0.2.5"></script>
+</html>

+ 5 - 0
static/html/chat_web.css

@@ -0,0 +1,5 @@
+.chatBtn{
+    position: fixed;
+    right: 10px;
+    bottom: 10px;
+}

+ 54 - 0
static/html/chat_web.js

@@ -0,0 +1,54 @@
+var loadJs=function(url,callback){
+    var script = document.createElement('script'), fn = callback || function(){};
+    script.type = 'text/javascript';
+    if(script.readyState){
+        script.onreadystatechange = function(){
+            if( script.readyState == 'loaded' || script.readyState == 'complete' ){
+                script.onreadystatechange = null;
+                fn();
+            }
+        };
+    }else{
+        script.onload = function(){
+            fn();
+        };
+    }
+    script.src = url;
+    document.getElementsByTagName('head')[0].appendChild(script);
+};
+loadJs("https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js",function(){
+    loadJs("https://cdn.bootcdn.net/ajax/libs/layer/3.1.1/layer.min.js" ,function () {
+        $(function () {
+            var goflyKefuId="";
+            if(typeof GOFLY_KEFU_ID!="undefined"){
+                var goflyKefuId=GOFLY_KEFU_ID;
+            }
+
+            var div =document.createElement('div');
+            div.id ='goflyKefu';
+            div.className +='goflyKefu';
+            document.body.appendChild(div);
+            var w =document.getElementById('goflyKefu');
+            w.innerHTML='<div style="border-radius:5px;position: fixed;right: 10px;bottom: 10px;background: #66b1ff;padding: 10px 30px;color: #fff;cursor: pointer;">在线咨询</div>';
+
+            $("#goflyKefu").click(function () {
+                $("#goflyKefu").hide();
+                layer.open({
+                    type: 2,
+                    title: '在线咨询',
+                    shadeClose: true,
+                    shade: false,
+                    maxmin: true,
+                    area: ['660px', '600px'],
+                    content: ['http://gofly.sopans.com/chat_page?kefu_id='+goflyKefuId,'no'],
+                    end: function(){
+                        $("#goflyKefu").show();
+                    }
+                });
+            });
+            //END
+        })
+    });
+});
+
+

+ 44 - 0
static/html/header.html

@@ -0,0 +1,44 @@
+{{define "header"}}
+<html lang="cn">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <meta name="description" content="">
+    <meta name="author" content="美天旺">
+    <title>美天旺客服管理系统</title>
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-ui@2.13.1/lib/theme-chalk/index.css">
+    <link rel="stylesheet" href="/static/css/common.css">
+    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/element-ui@2.13.1/lib/index.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
+    <style>
+        html,
+        body {
+            height: 100%;
+            padding: 0;
+            margin: 0;
+        }
+        body {
+            overflow: hidden;
+            background-color: #f5f5f5;
+        }
+        .el-aside .el-menu{
+            border-right: none;
+        }
+        .mainMain{
+            background: #fff;
+            /*margin-left: 10px;*/
+            margin-bottom: 60px;
+        }
+        .mainIframe{
+            width: 100%;
+            height: 100%;
+        }
+        .el-card__body{
+            cursor: pointer;
+        }
+    </style>
+
+</head>
+<body class="text-center">
+{{end}}

+ 108 - 0
static/html/index.html

@@ -0,0 +1,108 @@
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+    <title>{{.Title}}</title>
+    <meta name="keywords" content="{{.Keywords}}" />
+    <meta name="description" content="{{.Desc}}" />
+
+    {{.CssJs}}
+</head>
+<body>
+
+
+{{.Content}}
+
+
+
+
+<!--对接客服代码-->
+<script>
+    var GOFLY_URL="";
+    var GOFLY_KEFU_ID="kefu2";
+    var GOFLY_BTN_TEXT="{{.OnlineChat}}";
+</script>
+<script src="/static/js/gofly-front.js"></script>
+<!--//对接客服代码-->
+
+<!--自动弹代码-->
+<style>
+    /*自动弹出*/
+    .autoInvite{
+        width: 400px;
+        height: 162px;
+        position: fixed;
+        top:50%;
+        left: 50%;
+        margin-top: -81px;
+        margin-left: -200px;
+        background: url("/static/images/inviteColorBack1.png");
+        display: none;
+    }
+    .autoInvite .autoInviteNotice{
+        margin-top: 4px;
+    }
+    .autoInvite .autoInviteContent{
+        width: 220px;
+        height: 90px;
+        position: absolute;
+        top:55px;
+        right: 20px;
+        font-size: 13pt;
+        color: #fff;
+        word-break: break-all;
+        line-height: 25px;
+    }
+    .autoInvite .autoInviteBtns a{
+        display: inline-block;
+        width: 80px;
+        height: 32px;
+        line-height: 32px;
+        color: #fff;
+        text-align: center;
+        border: 1px solid #fff;
+        border-radius: 5px;
+        cursor: pointer;
+        font-size: 13px;
+        margin-right: 8px;
+        text-decoration: none;
+    }
+    .autoInvite .autoInviteBtns a.nowAsk{
+        color: #0085DA;
+        background-color: #fff;
+    }
+
+</style>
+<div class="autoInvite">
+    <div class="autoInviteContent">
+        <div class="autoInviteNotice">{{.Notice}}</div>
+        <div class="autoInviteBtns">
+            <a href="javascript:void(0)" id="noAsk">{{.LaterAsk}}</a>
+            <a href="javascript:void(0)" class="nowAsk">{{.NowAsk}}</a>
+        </div>
+    </div>
+</div>
+<script>
+    var invite=false;
+
+setTimeout(function(){
+    if (invite) return;
+    $(".autoInvite").show();
+    invite=true;
+
+    $("#noAsk").click(function(){
+        $(".autoInvite").hide();
+        invite=true;
+    });
+    $(".nowAsk").click(function(){
+        $(".autoInvite").hide();
+        showKefu();
+        invite=true;
+    });
+},8000);
+</script>
+<!--自动弹代码-->
+</body>
+</html>

+ 100 - 0
static/html/list.html

@@ -0,0 +1,100 @@
+{{template "header" }}
+<div id="app" style="width:100%">
+    <template>
+        <el-container  v-loading.fullscreen.lock="fullscreenLoading">
+                <el-aside>
+                    <el-menu
+                    :default-active="fid">
+                        <el-menu-item  v-on:click="openUrl('/write')">
+                            <i class="el-icon-edit"></i>
+                            <span slot="title">写信</span>
+                        </el-menu-item>
+                        <el-menu-item :index="v" v-for="(f,v) in folders" v-bind:key="v"  v-on:click="getFolders(1,v,true)">
+                            <i class="el-icon-message"></i>
+                            <span slot="title"><{v}></span>
+                        </el-menu-item>
+                    </el-menu>
+                </el-aside>
+
+                <el-main class="mainMain">
+                    <div class="block">
+                        <el-timeline :reverse="true">
+                            <el-timeline-item  v-for="item in mails" v-bind:key="item.Id" :timestamp="item.Date" placement="top">
+                                <el-card  v-on:click.native="openUrl('/view?fid='+fid+'&id='+item.Id)">
+                                    <h4><{item.Subject}></h4>
+                                    <p><{item.From}> 发送于 <{item.Date}></p>
+                                </el-card>
+                            </el-timeline-item>
+                        </el-timeline>
+                    </div>
+                    <el-pagination
+                            background
+                            :current-page="page"
+                            :page-size="pagesize"
+                            @current-change="getFolders"
+                            layout="prev, pager, next"
+                            :total="mailTotal">
+                    </el-pagination>
+                </el-main>
+
+        </el-container>
+</template>
+
+</div>
+</body>
+<script>
+	new Vue({
+		el: '#app',
+        delimiters:["<{","}>"],
+		data: {
+            fullscreenLoading:true,
+            folders:[],
+            mails:[],
+            mailTotal:0,
+            page:1,
+            pagesize:10,
+            fid:"",
+		},
+		methods: {
+            //获取邮件夹
+            getFolders: function (page,fid,isLeft) {
+                this.fullscreenLoading=true;
+                if(typeof(page)=="undefined" || page==""){
+                    page=1;
+                }else{
+                    this.page=page;
+                }
+                let data={};
+                data.page=page;
+                if(typeof(fid)!="undefined" && fid!=""){
+                    data.fid=fid;
+                    this.fid=fid;
+                }else if(this.fid!=""){
+                    data.fid=this.fid;
+                }
+                let _this = this;
+                $.get('/folders',data, function (rs) {
+                    if(!isLeft){
+                        _this.folders=rs.result.folders;
+                    }
+                    _this.mails=rs.result.mails
+                    _this.mailTotal=rs.result.total;
+                    _this.pagesize=rs.result.pagesize;
+                    _this.fid=rs.result.fid;
+                    _this.fullscreenLoading=false;
+                }).then(()=>{
+                    _this.fullscreenLoading=false;
+                });
+            },
+            //跳转
+            openUrl(url){
+                window.location.href=url;
+            },
+		},
+        created: function () {
+            this.getFolders(1,"INBOX");
+        }
+	})
+
+</script>
+</html>

+ 174 - 0
static/html/login.html

@@ -0,0 +1,174 @@
+<html lang="cn">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+    <meta name="description" content="">
+    <meta name="author" content="美天旺">
+    <title>美天旺客服系统登录页</title>
+	<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-ui@2.13.1/lib/theme-chalk/index.css">
+    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
+	<script src="https://cdn.jsdelivr.net/npm/element-ui@2.13.1/lib/index.js"></script>
+	<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
+    <script src="https://cdn.bootcdn.net/ajax/libs/layer/3.1.1/layer.min.js"></script>
+    <style>
+        body {
+            background-color: #f5f5f5;
+            margin: 0;
+            padding: 0;
+        }
+        .signin {
+            width: 350px;
+            padding: 20px;
+            margin:100px auto;
+            background: #fff;
+            -webkit-box-shadow: 0 1px 2px 0 rgba(101,129,156,.08);
+            box-shadow: 0 1px 2px 0 rgba(101,129,156,.08);
+        }
+        .signin h1,.signin h2,.signin .copyright{
+            font-weight: normal;
+            color: #4d627b;
+            text-align: center;
+        }
+        .signin .loginTitle{
+            font-size: 24px;
+        }
+        .signin .loginDesc{
+            font-size: 14px;
+            margin-bottom: 15px;
+        }
+        .signin .loginDesc a{
+            color: #409EFF;
+            text-decoration: none;
+        }
+        .signin .copyright{
+            font-size: 12px;
+        }
+        @media (max-width: 768px) {
+            .signin{
+                width: 90%;
+                margin:40px auto;
+                background-color: #f5f5f5;
+                box-shadow:none;
+            }
+        }
+    </style>
+
+</head>
+<body>
+<div id="app" class="signin">
+    <template>
+        <h1 class="loginTitle">美天旺客服登录</h1>
+        <h2 class="loginDesc">请联系管理员获取登录账号</h2>
+        <el-form :model="kefuForm"  :rules="rules" ref="kefuForm">
+            <el-form-item  prop="username">
+                <el-input v-model="kefuForm.username" placeholder="用户名"></el-input>
+            </el-form-item>
+            <el-form-item  prop="password">
+                <el-input :type="pass_attr" v-model="kefuForm.password" placeholder="密码"></el-input>
+                <i slot="suffix" :class="icon" @click="showHidePassword"></i>
+            </el-form-item>
+            <el-form-item>
+                <el-button style="width: 100%" :loading="loading" type="primary" @click="kefuLogin('kefuForm')">登录</el-button>
+            </el-form-item>
+        </el-form>
+        <p class="copyright">美天旺版权所有 &copy; 2020 </p>
+</template>
+</div>
+</body>
+<script>
+	new Vue({
+		el: '#app',
+        delimiters:["<{","}>"],
+		data: {
+            window:window,
+            activeName:"second",
+			loading:false,
+            pass_attr: "password",    // 密码输入框属性
+            icon: "el-input__icon el-icon-view",    // 显示/隐藏密码图标
+            localAuth:{
+                username:'',
+                password:'',
+            },
+            ruleForm:{
+                server:'',
+                email:'',
+                password:'',
+            },
+            kefuForm:{
+                username:'kefu2',
+                password:'123',
+            },
+            rules: {
+                server: [
+                    { required: true, message: 'IMAP服务器如"imap.sina.net:143"包含端口号', trigger: 'blur' },
+                ],
+                email: [
+                    { required: true, message: '邮箱地址', trigger: 'blur' },
+                ],
+                username: [
+                    { required: true, message: '用户名不能为空', trigger: 'blur' },
+                ],
+                password: [
+                    { required: true, message: '密码不能为空', trigger: 'blur' },
+                ],
+            },
+		},
+		methods: {
+		    showHidePassword(){
+                if(this.pass_attr=='text'){
+                    this.pass_attr="password";
+                    this.icon="el-input__icon el-icon-view";
+                }else{
+                    this.pass_attr="text";
+                    this.icon="el-input__icon el-icon-loading";
+                }
+            },
+            //提交表单
+            kefuLogin(formName){
+                let _this=this;
+                this.$refs[formName].validate((valid) => {
+                    if (!valid) {
+                        return false;
+                    } else {
+                        let data = {};
+                        data.type="kefu";
+                        data.username = _this.kefuForm.username;
+                        data.password = _this.kefuForm.password;
+                        _this.loading = true;
+                        $.post("/check", data, function (data) {
+                            if (data.code == 200) {
+                                _this.$message({
+                                    message: data.msg,
+                                    type: 'success'
+                                });
+                                localStorage.setItem("token",data.result.token);
+                                localStorage.setItem("ref_token",data.result.ref_token);
+                                localStorage.setItem("create_time",data.result.create_time);
+                                window.location.href="/main";
+                            } else {
+                                _this.$message({
+                                    message: data.msg,
+                                    type: 'error'
+                                });
+                            }
+                            _this.loading = false;
+                        });
+                    }
+                });
+			},
+            //重置表单
+            resetForm(formName) {
+                this.loading=false;
+                this.$refs[formName].resetFields();
+            },
+		},
+        created: function () {
+            if (top.location != location){
+                top.location.href = location.href;
+            }
+        }
+	})
+
+</script>
+
+</html>

+ 108 - 0
static/html/mail_detail.html

@@ -0,0 +1,108 @@
+{{.Header}}
+<div id="app" style="width:100%">
+    <template>
+        <el-container  v-loading.fullscreen.lock="fullscreenLoading">
+            <el-aside>
+            {{.Left}}
+            </el-aside>
+
+            <el-main class="mainMain">
+                <el-row :gutter="10">
+                    <el-col :span="2">
+                        发件人:
+                    </el-col>
+                    <el-col :span="22">
+                        <{from}>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="10">
+                    <el-col :span="2">
+                        收件人:
+                    </el-col>
+                    <el-col :span="22">
+                        <{to}>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="10">
+                    <el-col :span="2">
+                        时间:
+                    </el-col>
+                    <el-col :span="22">
+                        <{date}>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="10">
+                    <el-col :span="2">
+                        标题:
+                    </el-col>
+                    <el-col :span="22">
+                        <{subject}>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="10">
+                    <el-col :span="2">
+                        内容:
+                    </el-col>
+                    <el-col :span="22" v-html="html">
+                    </el-col>
+                </el-row>
+            </el-main>
+
+        </el-container>
+    </template>
+</div>
+</body>
+<script>
+    new Vue({
+        el: '#app',
+        delimiters:["<{","}>"],
+        data: {
+            fullscreenLoading:true,
+            folders:[],
+            mailTotal:0,
+            fid:"INBOX",
+            from:"",
+            to:"",
+            date:"",
+            subject:"",
+            html:"",
+        },
+        methods: {
+            //获取邮件夹
+            getMail: function (fid,id) {
+                this.fullscreenLoading=true;
+                var data={};
+                if(fid!=""){
+                    data.fid=fid;
+                    this.fid=fid;
+                }
+                if(id!=""){
+                    data.id=id;
+                }
+                let _this = this;
+                $.get('/mail',data, function (rs) {
+                    _this.folders=rs.result.folders;
+                    _this.fid=rs.result.fid;
+                    _this.mailTotal=rs.result.total;
+                    _this.from=rs.result.from;
+                    _this.to=rs.result.to;
+                    _this.date=rs.result.date;
+                    _this.subject=rs.result.subject;
+                    _this.html=rs.result.html;
+                    _this.fullscreenLoading=false;
+                }).then(()=>{
+                    _this.fullscreenLoading=false;
+                });
+            },
+            //跳转
+            openUrl(url){
+                window.location.href=url;
+            },
+        },
+        created: function () {
+            this.getMail({{.Fid}},{{.Id}});
+        }
+    })
+
+</script>
+</html>

+ 11 - 0
static/html/mail_left.html

@@ -0,0 +1,11 @@
+<el-menu
+        :default-active="fid">
+    <el-menu-item  v-on:click="openUrl('/write')">
+        <i class="el-icon-edit"></i>
+        <span slot="title">写信</span>
+    </el-menu-item>
+    <el-menu-item :index="v" v-for="(f,v) in folders" v-bind:key="v"  v-on:click="openUrl('/list?fid='+v)">
+        <i class="el-icon-menu"></i>
+        <span slot="title"><{v}></span>
+    </el-menu-item>
+</el-menu>

+ 162 - 0
static/html/main.html

@@ -0,0 +1,162 @@
+<html lang="cn">
+<head>
+    <meta charset="utf-8">
+    <meta name="description" content="">
+    <meta name="author" content="陶士涵">
+    <title>美天旺即时通讯工具集</title>
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-ui@2.13.1/lib/theme-chalk/index.css">
+    <link rel="stylesheet" href="/static/css/common.css">
+    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/element-ui@2.13.1/lib/index.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
+    <style>
+        html,
+        body {
+            height: 100%;
+            padding: 0;
+            margin: 0;
+        }
+        body {
+            overflow: hidden;
+            background-color: #f5f5f5;
+        }
+
+        .el-aside{
+            height: 100%;
+            background: #fff;
+            border: solid 1px #e6e6e6;
+        }
+        .el-aside .el-menu{
+            border-right: none;
+        }
+        .mainLogo{
+            font-size: 20px;
+            font-weight: bold;
+        }
+        .mainMain{
+            background: #fff;
+            margin-left: 10px;
+            margin-bottom: 60px;
+        }
+        .mainIframe{
+            width: 100%;
+            height: 100%;
+        }
+        .el-card__body{
+            cursor: pointer;
+        }
+
+    </style>
+
+</head>
+<body class="text-center">
+<div id="app">
+    <template>
+        {{template "nav" }}
+        <iframe  class="mainIframe"  v-bind:src="iframeUrl" frameborder="0"></iframe>
+    </template>
+</div>
+</body>
+<script>
+    new Vue({
+        el: '#app',
+        delimiters:["<{","}>"],
+        data: {
+            iframeUrl:"",
+            mailTotal:0,
+            adminAvator:"",
+            adminRole:"",
+        },
+        methods: {
+            openIframeUrl(url){
+                this.iframeUrl=url;
+            },
+            //跳转
+            openUrl(url){
+                window.location.href=url;
+            },
+            GetQueryString(name){
+                var reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)");
+                var r = window.location.search.substr(1).match(reg);
+                if(r!=null)return  unescape(r[2]); return null;
+            },
+            checkAuth(){
+                let _this=this;
+                $.ajax({
+                    type:"post",
+                    url:"/check_auth",
+                    headers:{
+                        "token":localStorage.getItem("token")
+                    },
+                    success: function(data) {
+                        if (data.code != 200) {
+                            window.location.href="/login";
+                        } else {
+                            _this.adminAvator=data.result.avator;
+                            _this.adminRole=data.result.role_name;
+                            _this.iframeUrl = "/chat_main";
+                        }
+                    }
+                    });
+            }
+        },
+        created: function () {
+            this.checkAuth();
+        }
+    })
+
+</script>
+<script>
+    new Vue({
+        delimiters:["<{","}>"],
+        data: {
+            websock: null,
+        },
+        created() {
+            //this.initWebSocket();
+        },
+        destroyed() {
+            this.websock.close() //离开路由之后断开websocket连接
+        },
+        methods: {
+            initWebSocket(){ //初始化weosocket
+                const wsuri = "ws://127.0.0.1:8080/push_mail";
+                this.websock = new WebSocket(wsuri);
+                this.websock.onmessage = this.websocketonmessage;
+                this.websock.onopen = this.websocketonopen;
+                this.websock.onerror = this.websocketonerror;
+                this.websock.onclose = this.websocketclose;
+            },
+            websocketonopen(){ //连接建立之后执行send方法发送数据
+                // let actions = "ping";
+                // let _this=this;
+                // setInterval(function(){
+                //     _this.websocketsend(JSON.stringify(actions));
+                // },10000);
+            },
+            websocketonerror(){//连接建立失败重连
+                this.initWebSocket();
+            },
+            websocketonmessage(e){ //数据接收
+                const redata = JSON.parse(e.data);
+                if (redata.code==200){
+                    this.$notify({
+                        title: redata.result.folder_name,
+                        message: "新邮件:"+redata.result.new_num,
+                        type: 'success',
+                        duration: 0,
+                    });
+                }
+            },
+            websocketsend(Data){//数据发送
+                this.websock.send(Data);
+            },
+            websocketclose(e){  //关闭
+                console.log('断开连接',e);
+            },
+        },
+    });
+</script>
+
+
+</html>

+ 24 - 0
static/html/nav.html

@@ -0,0 +1,24 @@
+{{define "nav"}}
+<el-menu
+        class="el-menu-demo"
+        background-color="#3c8dbc"
+        text-color="#fff"
+        active-text-color="#fff"
+        default-active="3"
+        mode="horizontal">
+    <el-menu-item  class="mainLogo" v-on:click="openUrl('/main')">美天旺<span class="mainVersion">V0.2.2</span></el-menu-item>
+    <el-menu-item style="display:none" index="2" v-on:click="openIframeUrl('/list')">邮箱<el-badge class="mark" :value="mailTotal" style="margin-bottom: 20px;"/>
+    </el-menu-item>
+    <el-menu-item index="3" v-on:click="openIframeUrl('/chat_main')">聊天</el-menu-item>
+    <el-menu-item index="4" v-on:click="openIframeUrl('/setting')">设置</el-menu-item>
+    <el-submenu style="float: right" index="10">
+        <template slot="title">
+            <el-avatar :size="30" :src="adminAvator"></el-avatar>
+            <span v-html="adminRole"></span>
+        </template>
+        <el-menu-item  v-on:click="openIframeUrl('/login')">
+            退出
+        </el-menu-item>
+    </el-submenu>
+</el-menu>
+{{end}}

+ 14 - 0
static/html/setting.html

@@ -0,0 +1,14 @@
+{{template "header" }}
+<div id="app" style="width:100%">
+    <template>
+        <el-container v-loading.fullscreen.lock="fullscreenLoading">
+
+            {{template "setting_left" .}}
+            <iframe  class="mainIframe"  v-bind:src="iframeUrl" frameborder="0"></iframe>
+        </el-container>
+    </template>
+
+</div>
+</body>
+
+{{template "setting_bottom" .}}

+ 428 - 0
static/html/setting_bottom.html

@@ -0,0 +1,428 @@
+{{define "setting_bottom"}}
+<script>
+    var ACTION="{{.action}}";
+    var QINIU_DOMAIN="{{.qiniu_domain}}";
+</script>
+<script>
+    var app=new Vue({
+        el: '#app',
+        delimiters:["<{","}>"],
+        data: {
+            iframeUrl:"/setting_statistics",
+            fullscreenLoading:false,
+            openIndex:[1],
+            account: {
+                username: "",
+                password: "",
+            },
+            mysql: {
+                server: "",
+                port: "",
+                database: "",
+                username: "",
+                password: "",
+            },
+            rules: {
+                server: [
+                    { required: true, message: '请输入服务地址', trigger: 'blur' },
+                ],
+                port: [
+                    { required: true, message: '请输入端口号', trigger: 'blur' },
+                ],
+                database: [
+                    { required: true, message: '请输入数据库名', trigger: 'blur' },
+                ],
+                username: [
+                    { required: true, message: '请输入用户名', trigger: 'blur' },
+                ],
+                name: [
+                    { required: true, message: '请输入用户名', trigger: 'blur' },
+                ],
+                avator: [
+                    { required: true, message: '请选择头像', trigger: 'blur' },
+                ],
+                role_id: [
+                    { required: true, message: '请选择角色', trigger: 'blur' },
+                ],
+                password: [
+                    { required: true, message: '请输入密码', trigger: 'blur' },
+                ],
+                nickname: [
+                    { required: true, message: '请输入昵称', trigger: 'blur' },
+                ],
+                method: [
+                    { required: true, message: '请输入允许的方法', trigger: 'blur' },
+                ],
+                path: [
+                    { required: true, message: '请输入允许的路径', trigger: 'blur' },
+                ],
+            },
+            kefuList:[],
+            kefuDialog:false,
+            kefuForm:{
+                id:"",
+                name:"",
+                password:"",
+                avator:"",
+                nickname:"",
+                role_name:"",
+                role_id:"",
+                enabled:0,
+            },
+            enabledItems: [
+                {id:2, text: "禁用"},
+                {id:1, text: "启用"}
+            ],
+            domain: QINIU_DOMAIN,
+            roleList:[],
+            configList:[],
+            roleDialog:false,
+            noticeList:[],
+            welcomeDialog:false,
+            ipblackList:[],
+            welcomeForm: {
+                content: "",
+            },
+            roleForm:{
+                id:"",
+                name:"",
+                method:"",
+                path:"",
+            },
+            statistics:{},
+            pageindex: {
+                title_cn: "",
+                title_en: "",
+                keywords_cn: "",
+                keywords_en: "",
+                desc_cn: "",
+                desc_en: "",
+                css_js: "",
+                html_cn: "",
+                html_en: "",
+            },
+        },
+        methods: {
+            //提交表单
+            setAccount(formName){
+                let _this=this;
+                this.$refs[formName].validate((valid) => {
+                    if (valid) {
+                        $.post("/setting_account",_this.account,function(data){
+                            if(data.code==200){
+                                _this.$message({
+                                    message: data.msg,
+                                    type: 'success'
+                                });
+                            }else{
+                                _this.$message({
+                                    message: data.msg,
+                                    type: 'error'
+                                });
+                            }
+                        });
+                    } else {
+                        return false;
+                    }
+                });
+            },
+            //设置mysql
+            setMysql(formName){
+                let _this=this;
+                this.$refs[formName].validate((valid) => {
+                    if (valid) {
+                        $.ajax({
+                            type:"POST",
+                            url:"/mysql",
+                            data:_this.mysql,
+                            headers:{
+                                "token":localStorage.getItem("token")
+                            },
+                            success: function(data) {
+                                if(data.code==200){
+                                    _this.$message({
+                                        message: data.msg,
+                                        type: 'success'
+                                    });
+                                }else{
+                                    _this.$message({
+                                        message: data.msg,
+                                        type: 'error'
+                                    });
+                                }
+                            }
+                        });
+                    } else {
+                        return false;
+                    }
+                });
+            },
+            //重置表单
+            resetForm(formName) {
+                this.loading=false;
+                this.$refs[formName].resetFields();
+            },
+            //跳转
+            openUrl(url){
+                //window.location.href=url;
+                this.iframeUrl=url;
+            },
+            //展示提示
+            showNotice(){
+                this.fullscreenLoading=false;
+                this.$message({
+                    message: '配置信息写入同级config目录,目录不存在会自动创建!',
+                    type: 'warning',
+                    duration:'8000',
+                    showClose:true,
+                });
+            },
+            addWelcome(){
+                this.welcomeForm.content="";
+                this.welcomeDialog=true;
+            },
+            //初始化数据
+            initInfo(){
+                let _this=this;
+                if(ACTION=="setting_mysql"){
+                    this.sendAjax("/mysql","get",{},function(result){
+                        _this.mysql.username=result.Username;
+                        _this.mysql.password=result.Password;
+                        _this.mysql.database=result.Database;
+                        _this.mysql.server=result.Server;
+                        _this.mysql.port=result.Port;
+                    });
+                }
+                if(ACTION=="setting_kefu_list"){
+                    this.sendAjax("/kefulist","get",{},function(result){
+                        _this.kefuList=result;
+                    });
+                    this.sendAjax("/roles","get",{},function(result){
+                        _this.roleList=result;
+                    });
+                }
+                if(ACTION=="roles_list"){
+                    this.sendAjax("/roles","get",{},function(result){
+                        _this.roleList=result;
+                    });
+                }
+                if(ACTION=="setting_statistics"){
+                    this.sendAjax("/statistics","get",{},function(result) {
+                        _this.statistics = result;
+                    });
+                }
+                if(ACTION=="setting_welcome"){
+                    this.sendAjax("/notices","get",{},function(result){
+                        _this.noticeList=result;
+                    });
+                }
+                if(ACTION=="setting_ipblack"){
+                    this.sendAjax("/ipblacks_all","get",{},function(result){
+                        _this.ipblackList=result.list;
+                    });
+                }
+                if(ACTION=="setting_config"){
+                    this.sendAjax("/configs","get",{},function(result){
+                        _this.configList=result;
+                    });
+                }
+                if(ACTION=="setting_pageindex"){
+                    this.sendAjax("/about","get",{},function(result){
+                        _this.pageindex=result;
+                    });
+                }
+            },
+            sendAjax(url,method,params,callback){
+                let _this=this;
+                $.ajax({
+                    type: method,
+                    url: url,
+                    data:params,
+                    headers: {
+                        "token": localStorage.getItem("token")
+                    },
+                    success: function(data) {
+                        if(data.code!=200){
+                            _this.$message({
+                                message: data.msg,
+                                type: 'error'
+                            });
+                        }else if(data.result!=null){
+                            callback(data.result);
+                        }else{
+                            callback(data);
+                        }
+                        _this.fullscreenLoading=false
+
+                    }
+                });
+            },
+            //添加客服的dialog
+            addKefu(){
+                this.kefuForm={
+                    id:"",
+                    name:"",
+                    password:"",
+                    avator:"",
+                };
+                this.kefuDialog=true;
+            },
+            //提交客服表单
+            submitKefuForm(formName){
+                let _this=this;
+                this.$refs[formName].validate((valid) => {
+                    if (valid) {
+                        this.sendAjax("/kefuinfo","POST",_this.kefuForm,function(result){
+                            _this.kefuDialog=false;
+                            _this.sendAjax("/kefulist","get",{},function(result){
+                                _this.kefuList=result;
+                            });
+                        });
+                    } else {
+                        return false;
+                    }
+                });
+            },
+            //提交欢迎表单
+            submitWelcomeForm(formName){
+                let _this=this;
+                this.$refs[formName].validate((valid) => {
+                    if (valid) {
+                        this.sendAjax("/notice","POST",_this.welcomeForm,function(result){
+                            _this.welcomeDialog=false;
+                            _this.sendAjax("/notices","get",{},function(result){
+                                _this.noticeList=result;
+                            });
+                        });
+                    } else {
+                        return false;
+                    }
+                });
+            },
+            //编辑客服表单
+            editKefuForm(formName){
+                let _this=this;
+                this.$refs[formName].validate((valid) => {
+                    if (valid) {
+                            _this.sendAjax("/kefulist","PUT",_this.kefuForm,function(result){
+                                _this.kefuList=result;
+                            });
+                    } else {
+                        return false;
+                    }
+                });
+            },
+            //获取客服
+            getKefu(kefuId){
+                let _this=this;
+                this.sendAjax("/kefuinfo_setting","GET",{kefu_id:kefuId},function(result){
+                    _this.kefuDialog=true;
+                    _this.kefuForm=result;
+                    _this.kefuForm.password="";
+                });
+            },
+            //删除客服
+            deleteKefu(kefuId){
+                let _this=this;
+                this.sendAjax("/kefuinfo?id="+kefuId,"DELETE",{id:kefuId},function(result){
+                    _this.kefuDialog=false;
+                    _this.sendAjax("/kefulist","get",{},function(result){
+                        _this.kefuList=result;
+                    });
+                });
+            },
+            //删除欢迎
+            deleteWelcome(id){
+                let _this=this;
+                this.sendAjax("/notice?id="+id,"DELETE",{id:id},function(result){
+                    _this.kefuDialog=false;
+                    _this.sendAjax("/notices","get",{},function(result){
+                        _this.noticeList=result;
+                    });
+                });
+            },
+            //删除ip
+            deleteIpblack(ip){
+                let _this=this;
+                this.sendAjax("/ipblack?ip="+ip,"DELETE",{ip:ip},function(result){
+                    _this.sendAjax("/ipblacks_all","get",{},function(result){
+                        _this.ipblackList=result.list;
+                    });
+                });
+            },
+            //配置角色权限
+            showAuthDialog(id,name,method,path){
+                this.roleForm.id=id
+                this.roleForm.name=name
+                this.roleForm.method=method
+                this.roleForm.path=path
+                this.roleDialog=true;
+            },
+            //设置配置项
+            setConfigItem(key,value){
+                let _this=this;
+                this.sendAjax("/config","POST",{key:key,value:value},function(result){
+                    _this.sendAjax("/configs","get",{},function(result){
+                        _this.configList=result;
+                    });
+                });
+            },
+            //设置配置项
+            setWelcomeItem(id,content){
+                let _this=this;
+                this.sendAjax("/notice_save","POST",{id:id,content:content},function(result){
+                    _this.sendAjax("/notices","get",{},function(result){
+                        _this.noticeList=result;
+                    });
+                });
+            },
+            //提交角色表单
+            submitRoleForm(formName){
+                let _this=this;
+                this.$refs[formName].validate((valid) => {
+                    if (valid) {
+                        this.sendAjax("/role","POST",_this.roleForm,function(result){
+                            _this.roleDialog=false;
+                            _this.sendAjax("/roles","get",{},function(result){
+                                _this.roleList=result;
+                            });
+                            _this.$message({
+                                message: result.msg,
+                                type: 'success'
+                            });
+                        });
+                    } else {
+                        return false;
+                    }
+                });
+            },
+            //提交首页表单
+            setPageIndex(){
+                let _this=this;
+                this.sendAjax("/about","POST",this.pageindex,function(result){
+                    _this.$message({
+                        message: "编辑成功",
+                        type: 'success'
+                    });
+                });
+            },
+            //生成部署js
+            createDeployJs(){
+                let domain=window.location.host;
+                this.$alert('    <script type="text/javascript">\n' +
+                    '    var GOFLY_KEFU_ID="'+this.kefuForm.name+'";\n' +
+                    '    <\/script>\n'+
+                    ' <script type="text/javascript" src="http://'+domain+'/webjs"><\/script>', '网页部署');
+            }
+        },
+        created: function () {
+            // if(ACTION=="setting"){
+            //     this.showNotice();
+            // }
+            this.initInfo();
+        }
+    })
+
+</script>
+</html>
+{{end}}

+ 35 - 0
static/html/setting_config.html

@@ -0,0 +1,35 @@
+{{template "header" }}
+<div id="app" style="width:100%">
+    <template>
+        <el-container v-loading.fullscreen.lock="fullscreenLoading">
+
+            <el-main class="mainMain">
+
+                <el-table
+                        :data="configList"
+                        border
+                        style="width: 100%">
+                    <el-table-column
+                            prop="conf_name"
+                            label="配置参数">
+                    </el-table-column>
+                    <el-table-column
+                            prop="conf_key"
+                            label="配置key">
+                    </el-table-column>
+                    <el-table-column
+                            prop="id"
+                            label="操作">
+                        <template slot-scope="scope">
+                            <el-input @change="setConfigItem(scope.row.conf_key,scope.row.conf_value)" v-model="scope.row.conf_value"></el-input>
+                        </template>
+                    </el-table-column>
+                </el-table>
+            </el-main>
+
+        </el-container>
+    </template>
+
+</div>
+</body>
+{{template "setting_bottom" .}}

+ 29 - 0
static/html/setting_deploy.html

@@ -0,0 +1,29 @@
+{{template "header" }}
+<div id="app" style="width:100%">
+    <template>
+        <el-container>
+
+            <el-main class="mainMain">
+                <el-tabs>
+                    <el-tab-pane label="默认JS模式">
+                       <textarea style="width:100%;color:green;font-size:12px;height:290px;">
+<script>
+    var GOFLY_URL="";
+    var GOFLY_KEFU_ID="kefu2";
+    var GOFLY_BTN_TEXT="Chat with me";
+</script>
+<script src="/static/js/gofly-front.js"></script>
+                       </textarea>
+                    </el-tab-pane>
+                    <el-tab-pane label="超链接模式">
+                        <el-input value="https://gofly.sopans.com/chatIndex?kefu_id=[客服用户名]"></el-input>
+                    </el-tab-pane>
+                </el-tabs>
+            </el-main>
+
+        </el-container>
+    </template>
+
+</div>
+</body>
+{{template "setting_bottom" .}}

+ 34 - 0
static/html/setting_ipblack.html

@@ -0,0 +1,34 @@
+{{template "header" }}
+<div id="app" style="width:100%">
+    <template>
+        <el-container v-loading.fullscreen.lock="fullscreenLoading">
+
+            <el-main class="mainMain">
+                <el-table
+                        :data="ipblackList"
+                        border
+                        style="width: 100%">
+                    <el-table-column
+                            prop="ip"
+                            label="黑IP">
+                    </el-table-column>
+                    <el-table-column
+                            prop="create_at"
+                            label="添加时间">
+                    </el-table-column>
+                    <el-table-column
+                            prop="id"
+                            label="操作">
+                        <template slot-scope="scope">
+                            <el-button @click="deleteIpblack(scope.row.ip)" type="danger" size="small" plain>删除</el-button>
+                        </template>
+                    </el-table-column>
+                </el-table>
+            </el-main>
+
+        </el-container>
+    </template>
+
+</div>
+</body>
+{{template "setting_bottom" .}}

+ 104 - 0
static/html/setting_kefu_list.html

@@ -0,0 +1,104 @@
+{{template "header" }}
+<div id="app" style="width:100%">
+    <template>
+        <el-container v-loading.fullscreen.lock="fullscreenLoading">
+
+            <el-main class="mainMain">
+                <el-button style="margin-bottom: 10px;" @click="addKefu" type="primary" size="small">添加客服</el-button>
+                <el-table
+                        :data="kefuList"
+                        border
+                        style="width: 100%">
+                    <el-table-column
+                            prop="img"
+                            label="客服头像">
+                        <template slot-scope="scope">
+                            <el-avatar :size="50"><img :src="scope.row.avator"/></el-avatar>
+                        </template>
+                    </el-table-column>
+                    <el-table-column
+                            prop="name"
+                            label="客服账号">
+                    </el-table-column>
+                    <el-table-column
+                            prop="nickname"
+                            label="客服昵称">
+                    </el-table-column>
+                    <el-table-column
+                            prop="role_name"
+                            label="角色">
+                    </el-table-column>
+                    <el-table-column
+                            prop="enabled"
+                            label="是否启用">
+                        <template slot-scope="scope">
+                            <div v-if="scope.row.enabled==2">
+                                <el-tag type="warning">禁用</el-tag>
+                            </div>
+                            <div v-else>
+                                <el-tag type="success">启用</el-tag>
+                            </div>
+                        </template>
+                    </el-table-column>
+                    <el-table-column
+                            prop="created_at"
+                            label="添加时间">
+                    </el-table-column>
+                    <el-table-column
+                            prop="id"
+                            label="操作">
+                        <template slot-scope="scope">
+                            <el-button @click="getKefu(scope.row.id)" type="primary" size="small" plain>编辑</el-button>
+                            <el-button @click="deleteKefu(scope.row.id)" type="danger" size="small" plain>删除</el-button>
+                        </template>
+                    </el-table-column>
+                </el-table>
+            </el-main>
+
+        </el-container>
+        <el-dialog
+                title="客服"
+                :visible.sync="kefuDialog"
+                width="30%"
+                top="0"
+                >
+            <el-form ref="kefuForm" :model="kefuForm" :rules="rules" label-width="70px">
+                <el-form-item label="用户名"  prop="name">
+                    <el-input v-model="kefuForm.name"></el-input>
+                </el-form-item>
+                <el-form-item label="密码"  prop="password">
+                    <el-input v-model="kefuForm.password"></el-input>
+                </el-form-item>
+                <el-form-item label="昵称"  prop="nickname">
+                    <el-input v-model="kefuForm.nickname"></el-input>
+                </el-form-item>
+                <el-form-item label="头像"  prop="avator">
+                    <el-select v-model="kefuForm.avator" placeholder="请选择头像">
+                        <el-option :label="'头像'+item" :value="domain+item+'.jpg'" v-for="item in [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14]" v-bind:key="item">
+                            <el-avatar :size="30" :src="domain+item+'.jpg'"></el-avatar>
+                        </el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="角色"  prop="role_id">
+                    <el-select v-model="kefuForm.role_id" placeholder="请选择角色">
+                        <el-option :label="item.role_name" :value="item.role_id" v-for="item in roleList" v-bind:key="item.role_id">
+                        </el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="启用"  prop="enabled">
+                    <el-select v-model="kefuForm.enabled" placeholder="是否启用">
+                        <el-option :label="item.text" :value="item.id" v-for="item in enabledItems" v-bind:key="item.id">
+                        </el-option>
+                    </el-select>
+                </el-form-item>
+            </el-form>
+            <span slot="footer" class="dialog-footer">
+                <el-button @click="kefuDialog = false">取 消</el-button>
+                <el-button type="primary" @click="submitKefuForm('kefuForm')">确 定</el-button>
+              </span>
+        </el-dialog>
+    </template>
+
+</div>
+</body>
+{{template "setting_bottom" .}}

+ 47 - 0
static/html/setting_left.html

@@ -0,0 +1,47 @@
+{{define "setting_left"}}
+<el-aside width="200px">
+<el-menu
+        background-color="#222d32"
+        text-color="#b8c7ce"
+        active-text-color="#fff"
+        default-active="{{.tab_index}}" :default-openeds="openIndex"
+        :unique-opened=true>
+    <el-submenu index="1">
+        <template slot="title">
+            <i class="el-icon-s-custom"></i>
+            <span>账户设置</span>
+        </template>
+        <el-menu-item-group>
+            <el-menu-item index="1-1" v-on:click="openUrl('/setting_statistics')">统计信息</el-menu-item>
+            <el-menu-item index="1-2" v-on:click="openUrl('/setting_welcome')">自动欢迎</el-menu-item>
+        </el-menu-item-group>
+    </el-submenu>
+    <el-submenu index="3">
+        <template slot="title">
+            <i class="el-icon-s-cooperation"></i>
+            <span>权限设置</span>
+        </template>
+        <el-menu-item-group>
+            <el-menu-item index="3-2"  v-on:click="openUrl('/setting_kefu_list')">用户管理</el-menu-item>
+            <el-menu-item index="3-1"  v-on:click="openUrl('/roles_list')">角色管理</el-menu-item>
+        </el-menu-item-group>
+    </el-submenu>
+    <el-submenu index="2">
+        <template slot="title">
+            <i class="el-icon-s-tools"></i>
+            <span>系统设置</span>
+        </template>
+        <el-menu-item-group>
+            <el-menu-item style="display:none" index="2-1">设置smtp</el-menu-item>
+            <el-menu-item style="display:none" index="2-2">设置imap</el-menu-item>
+            <el-menu-item style="display:none" index="2-3" v-on:click="openUrl('/setting')">设置登陆账号</el-menu-item>
+            <el-menu-item style="display:none" index="2-4"  v-on:click="openUrl('/setting_mysql')">设置mysql</el-menu-item>
+            <el-menu-item index="4-7"  v-on:click="openUrl('/setting_indexpage')">编辑首页</el-menu-item>
+            <el-menu-item index="4-6"  v-on:click="openUrl('/setting_config')">配置参数</el-menu-item>
+            <el-menu-item index="4-5"  v-on:click="openUrl('/setting_ipblack')">IP黑名单</el-menu-item>
+            <el-menu-item index="2-5"  v-on:click="openUrl('/setting_deploy')">网页部署</el-menu-item>
+        </el-menu-item-group>
+    </el-submenu>
+</el-menu>
+</el-aside>
+{{end}}

+ 35 - 0
static/html/setting_mysql.html

@@ -0,0 +1,35 @@
+{{template "header" }}
+<div id="app" style="width:100%">
+    <template>
+        <el-container v-loading.fullscreen.lock="fullscreenLoading">
+
+            <el-main class="mainMain">
+                <el-form :model="mysql" :rules="rules" ref="mysql"  label-width="120px">
+                    <el-form-item label="服务地址" prop="server">
+                        <el-input v-model="mysql.server"></el-input>
+                    </el-form-item>
+                    <el-form-item label="端口" prop="port">
+                        <el-input v-model="mysql.port"></el-input>
+                    </el-form-item>
+                    <el-form-item label="数据库名" prop="database">
+                        <el-input v-model="mysql.database"></el-input>
+                    </el-form-item>
+                    <el-form-item label="用户名" prop="username">
+                        <el-input v-model="mysql.username"></el-input>
+                    </el-form-item>
+                    <el-form-item label="密码" prop="password">
+                        <el-input v-model="mysql.password"></el-input>
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button type="primary" @click="setMysql('mysql')">立即创建</el-button>
+                        <el-button @click="resetForm('mysql')">取消</el-button>
+                    </el-form-item>
+                </el-form>
+            </el-main>
+
+        </el-container>
+    </template>
+
+</div>
+</body>
+{{template "setting_bottom" .}}

+ 47 - 0
static/html/setting_pageindex.html

@@ -0,0 +1,47 @@
+{{template "header" }}
+<div id="app" style="width:100%">
+    <template>
+        <el-container>
+
+            <el-main class="mainMain">
+                <el-form ref="form" :model="pageindex" label-width="100px">
+                    <el-form-item label="中文标题">
+                        <el-input v-model="pageindex.title_cn"></el-input>
+                    </el-form-item>
+                    <el-form-item label="英文标题">
+                        <el-input v-model="pageindex.title_en"></el-input>
+                    </el-form-item>
+                    <el-form-item label="中文关键词">
+                        <el-input v-model="pageindex.keywords_cn"></el-input>
+                    </el-form-item>
+                    <el-form-item label="英文关键词">
+                        <el-input v-model="pageindex.keywords_en"></el-input>
+                    </el-form-item>
+                    <el-form-item label="中文描述">
+                        <el-input v-model="pageindex.desc_cn"></el-input>
+                    </el-form-item>
+                    <el-form-item label="英文描述">
+                        <el-input v-model="pageindex.desc_en"></el-input>
+                    </el-form-item>
+                    <el-form-item label="JS&CSS">
+                        <el-input :autosize="{ minRows: 2, maxRows: 6}" type="textarea" v-model="pageindex.css_js"></el-input>
+                    </el-form-item>
+                    <el-form-item label="中文内容">
+                        <el-input :autosize="{ minRows: 2, maxRows: 8}" type="textarea" v-model="pageindex.html_cn"></el-input>
+                    </el-form-item>
+                    <el-form-item label="英文内容">
+                        <el-input :autosize="{ minRows: 2, maxRows: 8}" type="textarea" v-model="pageindex.html_en"></el-input>
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button type="primary" @click="setPageIndex()">保存</el-button>
+                        <el-button>取消</el-button>
+                    </el-form-item>
+                </el-form>
+            </el-main>
+
+        </el-container>
+    </template>
+
+</div>
+</body>
+{{template "setting_bottom" .}}

+ 60 - 0
static/html/setting_role_list.html

@@ -0,0 +1,60 @@
+{{template "header" }}
+<div id="app" style="width:100%">
+    <template>
+        <el-container v-loading.fullscreen.lock="fullscreenLoading">
+
+            <el-main class="mainMain">
+                <el-button style="margin-bottom: 10px;" @click="" type="primary" size="small">添加角色</el-button>
+                <el-table
+                        :data="roleList"
+                        border
+                        style="width: 100%">
+                    <el-table-column
+                            prop="role_name"
+                            label="角色名称">
+                    </el-table-column>
+                    <el-table-column
+                            prop="method"
+                            label="允许方法">
+                    </el-table-column>
+                    <el-table-column
+                            prop="path"
+                            label="路径">
+                    </el-table-column>
+                    <el-table-column
+                            prop="id"
+                            label="操作">
+                        <template slot-scope="scope">
+                            <el-button @click="showAuthDialog(scope.row.role_id,scope.row.role_name,scope.row.method,scope.row.path)" type="primary" size="small" plain>配置权限</el-button>
+                        </template>
+                    </el-table-column>
+                </el-table>
+            </el-main>
+
+        </el-container>
+        <el-dialog
+                    title="配置权限"
+                :visible.sync="roleDialog"
+                width="30%"
+        >
+            <el-form ref="roleForm" :model="roleForm" :rules="rules" label-width="70px">
+                <el-form-item label="角色名"  prop="name">
+                    <el-input v-model="roleForm.name"></el-input>
+                </el-form-item>
+                <el-form-item label="方法"  prop="method">
+                    <el-input v-model="roleForm.method"></el-input>
+                </el-form-item>
+                <el-form-item label="路径"  prop="path">
+                    <el-input type="textarea" v-model="roleForm.path"></el-input>
+                </el-form-item>
+            </el-form>
+            <span slot="footer" class="dialog-footer">
+                <el-button @click="roleDialog = false">取 消</el-button>
+                <el-button type="primary" @click="submitRoleForm('roleForm')">确 定</el-button>
+              </span>
+        </el-dialog>
+    </template>
+
+</div>
+</body>
+{{template "setting_bottom" .}}

+ 34 - 0
static/html/setting_statistics.html

@@ -0,0 +1,34 @@
+{{template "header" }}
+<div id="app" style="width:100%">
+    <template>
+        <el-container v-loading.fullscreen.lock="fullscreenLoading">
+            <el-main class="mainMain settingMain">
+                <h2 class="textDark">数据总览</h2>
+                <el-row :gutter="10">
+                    <el-col :span="8">
+                        <div class="smallBox bgInfo">
+                            <h3  v-html="statistics.visitors"></h3>
+                            <p>总访客数</p>
+                        </div>
+                    </el-col>
+                    <el-col :span="8">
+                        <div class="smallBox bgSuccess">
+                            <h3  v-html="statistics.message"></h3>
+                            <p>总消息数</p>
+                        </div>
+                    </el-col>
+                    <el-col :span="8">
+                        <div class="smallBox bgDanger">
+                            <h3 v-html="statistics.session"></h3>
+                            <p>当前会话数</p>
+                        </div>
+                    </el-col>
+                </el-row>
+            </el-main>
+        </el-container>
+    </template>
+</div>
+</body>
+{{template "setting_bottom" .}}
+
+

+ 53 - 0
static/html/setting_welcome.html

@@ -0,0 +1,53 @@
+{{template "header" }}
+<div id="app" style="width:100%">
+    <template>
+        <el-container v-loading.fullscreen.lock="fullscreenLoading">
+
+            <el-main class="mainMain">
+                <el-button style="margin-bottom: 10px;" @click="addWelcome" type="primary" size="small">添加欢迎</el-button>
+                <el-table
+                        :data="noticeList"
+                        border
+                        style="width: 100%">
+                    <el-table-column
+                            prop="content"
+                            label="回复内容">
+                        <template slot-scope="scope">
+                            <el-input @change="setWelcomeItem(scope.row.id,scope.row.content)" v-model="scope.row.content"></el-input>
+                        </template>
+                    </el-table-column>
+                    <el-table-column
+                            prop="ctime"
+                            label="添加时间">
+                    </el-table-column>
+                    <el-table-column
+                            prop="id"
+                            label="操作">
+                        <template slot-scope="scope">
+                            <el-button @click="deleteWelcome(scope.row.id)" type="danger" size="small" plain>删除</el-button>
+                        </template>
+                    </el-table-column>
+                </el-table>
+            </el-main>
+
+        </el-container>
+        <el-dialog
+                title="欢迎"
+                :visible.sync="welcomeDialog"
+                width="30%"
+        >
+            <el-form ref="welcomeForm" :model="welcomeForm" :rules="rules" label-width="70px">
+                <el-form-item label="内容"  prop="content">
+                    <el-input v-model="welcomeForm.content"></el-input>
+                </el-form-item>
+            </el-form>
+            <span slot="footer" class="dialog-footer">
+                <el-button @click="welcomeDialog = false">取 消</el-button>
+                <el-button type="primary" @click="submitWelcomeForm('welcomeForm')">确 定</el-button>
+              </span>
+        </el-dialog>
+    </template>
+
+</div>
+</body>
+{{template "setting_bottom" .}}

+ 116 - 0
static/html/write.html

@@ -0,0 +1,116 @@
+{{.Header}}
+<div id="app" style="width:100%">
+    <template>
+        <el-container  v-loading.fullscreen.lock="fullscreenLoading">
+            <el-aside>
+                {{.Left}}
+            </el-aside>
+
+            <el-main class="mainMain">
+                <el-form :model="ruleForm" :rules="rules" ref="ruleForm"  label-width="120px">
+                    <el-form-item label="smtp地址" prop="smtp">
+                        <el-input v-model="ruleForm.smtp" placeholder="SMTP服务器如smtp.sina.net:25包含端口号"></el-input>
+                    </el-form-item>
+                    <el-form-item label="主题" prop="subject">
+                        <el-input v-model="ruleForm.subject"></el-input>
+                    </el-form-item>
+                    <el-form-item label="收件人" prop="to">
+                        <el-input v-model="ruleForm.to"></el-input>
+                    </el-form-item>
+                    <el-form-item label="内容" prop="content">
+                        <el-input type="textarea" v-model="ruleForm.content"></el-input>
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button type="primary" @click="submitForm('ruleForm')">立即发送</el-button>
+                        <el-button @click="resetForm('ruleForm')">取消</el-button>
+                    </el-form-item>
+                </el-form>
+            </el-main>
+
+        </el-container>
+    </template>
+
+</div>
+</body>
+<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
+<script>
+    new Vue({
+        el: '#app',
+        delimiters:["<{","}>"],
+        data: {
+            fullscreenLoading:true,
+            folders:[],
+            fid:"",
+            mailTotal:0,
+            ruleForm: {
+                smtp: '',
+                to: '',
+                subject: '',
+                content: '',
+            },
+            rules: {
+                smtp: [
+                    { required: true, message: 'SMTP服务器如"smtp.sina.net:25"包含端口号', trigger: 'blur' },
+                ],
+                to: [
+                    { required: true, message: '邮箱地址', trigger: 'blur' },
+                ],
+                subject: [
+                    { required: true, message: '主题', trigger: 'blur' },
+                ],
+                content: [
+                    { required: true, message: '内容', trigger: 'blur' },
+                ],
+            },
+        },
+        methods: {
+            //获取邮件夹
+            getFolders: function () {
+                this.fullscreenLoading=true;
+                let _this = this;
+                $.get('/folder_dirs', function (rs) {
+                    _this.folders=rs.result.folders;
+                    _this.mailTotal=rs.result.total;
+                    _this.fid=rs.result.fid;
+                    _this.fullscreenLoading=false;
+                }).then(()=>{
+                    _this.fullscreenLoading=false;
+                });
+            },
+            //跳转
+            openUrl(url){
+                window.location.href=url;
+            },
+            //提交表单
+            submitForm(formName){
+                let _this=this;
+                this.$refs[formName].validate((valid) => {
+                    console.log(valid,formName,this.$refs[formName]);
+                    if (valid) {
+                        let data=this.ruleForm;
+                        let to=[];
+                        to.push(this.ruleForm.to);
+                        data.to=to;
+                        axios.post('/send', data).then(function (response) {
+                                console.log(response);
+                        }).catch(function (error) {
+                                console.log(error);
+                        });
+                    } else {
+                        return false;
+                    }
+                });
+            },
+            //重置表单
+            resetForm(formName) {
+                this.loading=false;
+                this.$refs[formName].resetFields();
+            },
+        },
+        created: function () {
+            this.getFolders();
+        }
+    })
+
+</script>
+</html>

BIN
static/images/0.jpg


BIN
static/images/1.jpg


BIN
static/images/1.png


BIN
static/images/10.jpg


BIN
static/images/11.jpg


BIN
static/images/12.jpg


BIN
static/images/13.jpg


BIN
static/images/14.jpg


Some files were not shown because too many files changed in this diff