init project

This commit is contained in:
goder 2026-01-28 22:42:06 +08:00
commit 2a97a6a98b
327 changed files with 14452 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

8
Dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM golang:alpine
WORKDIR /app
COPY . /app
RUN go env -w GO111MODULE=on && go env -w GOPROXY=https://goproxy.cn,direct
VOLUME ["/app/config"]
RUN go build go-fly.go
EXPOSE 8081
CMD ["/app/go-fly","server"]

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

71
cmd/install.go Normal file
View File

@ -0,0 +1,71 @@
package cmd
import (
"github.com/spf13/cobra"
"goflylivechat/models"
"goflylivechat/tools"
"log"
"os"
"strings"
)
var installCmd = &cobra.Command{
Use: "install",
Short: "Initialize database and import data", // More precise description
Run: func(cmd *cobra.Command, args []string) {
install()
},
}
func install() {
// Check if already installed
if ok, _ := tools.IsFileNotExist("./install.lock"); !ok {
log.Println("Please remove ./install.lock file to reinstall")
os.Exit(1)
}
// Verify required files exist
sqlFile := "import.sql"
dataExists, _ := tools.IsFileExist(sqlFile)
if !dataExists {
log.Println("Configuration file config/mysql.json or database import file import.sql not found")
os.Exit(1)
}
// Execute SQL statements
sqls, err := os.ReadFile(sqlFile)
if err != nil {
log.Printf("Failed to read SQL file %s: %v\n", sqlFile, err)
os.Exit(1)
}
sqlArr := strings.Split(string(sqls), ";")
for _, sql := range sqlArr {
sql = strings.TrimSpace(sql)
if sql == "" {
continue
}
err := models.Execute(sql)
if err != nil {
log.Printf("SQL execution failed: %s\nError: %v\n", sql, err)
log.Println("Database initialization failed - please check SQL statements")
os.Exit(1)
}
log.Printf("Executed successfully: %s\n", sql)
}
// Create installation lock file
installFile, err := os.OpenFile("./install.lock", os.O_RDWR|os.O_CREATE, os.ModePerm)
if err != nil {
log.Printf("Failed to create lock file: %v\n", err)
os.Exit(1)
}
defer installFile.Close()
_, err = installFile.WriteString("gofly live chat installation complete")
if err != nil {
log.Printf("Failed to write lock file: %v\n", err)
os.Exit(1)
}
log.Println("Database initialization completed successfully")
}

37
cmd/root.go Normal file
View File

@ -0,0 +1,37 @@
package cmd
import (
"errors"
"fmt"
"github.com/spf13/cobra"
"os"
)
var rootCmd = &cobra.Command{
Use: "go-fly",
Short: "go-fly",
Long: `简洁快速的GO语言WEB在线客服 https://gofly.sopans.com`,
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(serverCmd)
rootCmd.AddCommand(installCmd)
rootCmd.AddCommand(stopCmd)
}

77
cmd/server.go Normal file
View File

@ -0,0 +1,77 @@
package cmd
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/spf13/cobra"
"github.com/zh-five/xdaemon"
"goflylivechat/middleware"
"goflylivechat/router"
"goflylivechat/tools"
"goflylivechat/ws"
"log"
"os"
)
var (
port string
daemon bool
)
var serverCmd = &cobra.Command{
Use: "server",
Short: "启动http服务",
Example: "gofly server -p 8082",
Run: func(cmd *cobra.Command, args []string) {
run()
},
}
func init() {
serverCmd.PersistentFlags().StringVarP(&port, "port", "p", "8081", "监听端口号")
serverCmd.PersistentFlags().BoolVarP(&daemon, "daemon", "d", false, "是否为守护进程模式")
}
func run() {
if daemon == true {
logFilePath := ""
if dir, err := os.Getwd(); err == nil {
logFilePath = dir + "/logs/"
}
_, err := os.Stat(logFilePath)
if os.IsNotExist(err) {
if err := os.MkdirAll(logFilePath, 0777); err != nil {
log.Println(err.Error())
}
}
d := xdaemon.NewDaemon(logFilePath + "go-fly.log")
d.MaxCount = 10
d.Run()
}
baseServer := "0.0.0.0:" + port
log.Println("start server...\r\ngohttp://" + baseServer)
tools.Logger().Println("start server...\r\ngohttp://" + baseServer)
engine := gin.Default()
engine.LoadHTMLGlob("static/templates/*")
engine.Static("/assets", "./static")
engine.Static("/static", "./static")
engine.Use(tools.Session("gofly"))
engine.Use(middleware.CrossSite)
//性能监控
//pprof.Register(engine)
//记录日志
engine.Use(middleware.NewMidLogger())
router.InitViewRouter(engine)
router.InitApiRouter(engine)
//记录pid
os.WriteFile("gofly.sock", []byte(fmt.Sprintf("%d,%d", os.Getppid(), os.Getpid())), 0666)
//限流类
tools.NewLimitQueue()
//清理
ws.CleanVisitorExpire()
//后端websocket
go ws.WsServerBackend()
engine.Run(baseServer)
}

30
cmd/stop.go Normal file
View File

@ -0,0 +1,30 @@
package cmd
import (
"github.com/spf13/cobra"
"io/ioutil"
"os/exec"
"runtime"
"strings"
)
var stopCmd = &cobra.Command{
Use: "stop",
Short: "停止http服务",
Run: func(cmd *cobra.Command, args []string) {
pids, err := ioutil.ReadFile("gofly.sock")
if err != nil {
return
}
pidSlice := strings.Split(string(pids), ",")
var command *exec.Cmd
for _, pid := range pidSlice {
if runtime.GOOS == "windows" {
command = exec.Command("taskkill.exe", "/f", "/pid", pid)
} else {
command = exec.Command("kill", pid)
}
command.Start()
}
},
}

12
common/common.go Normal file
View File

@ -0,0 +1,12 @@
package common
var (
PageSize uint = 10
VisitorPageSize uint = 8
Version string = "0.3.9"
VisitorExpire float64 = 600
Upload string = "static/upload/"
Dir string = "config/"
MysqlConf string = Dir + "mysql.json"
IsCompireTemplate bool = false //是否编译静态模板到二进制
)

29
common/config.go Normal file
View File

@ -0,0 +1,29 @@
package common
import (
"encoding/json"
"goflylivechat/tools"
"io/ioutil"
)
type Mysql struct {
Server string
Port string
Database string
Username string
Password string
}
func GetMysqlConf() *Mysql {
var mysql = &Mysql{}
isExist, _ := tools.IsFileExist(MysqlConf)
if !isExist {
return mysql
}
info, err := ioutil.ReadFile(MysqlConf)
if err != nil {
return mysql
}
err = json.Unmarshal(info, mysql)
return mysql
}

BIN
config/city.free.ipdb Normal file

Binary file not shown.

7
config/mysql.json Normal file
View File

@ -0,0 +1,7 @@
{
"Server":"localhost",
"Port":"3306",
"Database":"goflychat",
"Username":"goflychat",
"Password":"goflychat"
}

52
controller/about.go Normal file
View File

@ -0,0 +1,52 @@
package controller
import (
"github.com/gin-gonic/gin"
"goflylivechat/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 GetAbouts(c *gin.Context) {
about := models.FindAbouts()
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": "",
})
}

44
controller/captcha.go Normal file
View File

@ -0,0 +1,44 @@
package controller
import (
"bytes"
"github.com/dchest/captcha"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
func GetCaptcha(c *gin.Context) {
l := captcha.DefaultLen
w, h := 107, 36
captchaId := captcha.NewLen(l)
session := sessions.Default(c)
session.Set("captcha", captchaId)
_ = session.Save()
_ = Serve(c.Writer, c.Request, captchaId, ".png", "zh", false, w, h)
}
func Serve(w http.ResponseWriter, r *http.Request, id, ext, lang string, download bool, width, height int) error {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
var content bytes.Buffer
switch ext {
case ".png":
w.Header().Set("Content-Type", "image/png")
_ = captcha.WriteImage(&content, id, width, height)
case ".wav":
w.Header().Set("Content-Type", "audio/x-wav")
_ = captcha.WriteAudio(&content, id, lang)
default:
return captcha.ErrNotFound
}
if download {
w.Header().Set("Content-Type", "application/octet-stream")
}
http.ServeContent(w, r, id+ext, time.Time{}, bytes.NewReader(content.Bytes()))
return nil
}

36
controller/chart.go Normal file
View File

@ -0,0 +1,36 @@
package controller
import (
"github.com/gin-gonic/gin"
"goflylivechat/models"
"goflylivechat/tools"
"time"
)
func GetChartStatistic(c *gin.Context) {
kefuName, _ := c.Get("kefu_name")
dayNumMap := make(map[string]string)
result := models.CountVisitorsEveryDay(kefuName.(string))
for _, item := range result {
dayNumMap[item.Day] = tools.Int2Str(item.Num)
}
nowTime := time.Now()
list := make([]map[string]string, 0)
for i := 0; i > -46; i-- {
getTime := nowTime.AddDate(0, 0, i) //年,月,日 获取一天前的时间
resTime := getTime.Format("06-01-02") //获取的时间的格式
tmp := make(map[string]string)
tmp["day"] = resTime
tmp["num"] = dayNumMap[resTime]
list = append(list, tmp)
}
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": list,
})
}

14
controller/index.go Normal file
View File

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

66
controller/ip.go Normal file
View File

@ -0,0 +1,66 @@
package controller
import (
"github.com/gin-gonic/gin"
"goflylivechat/common"
"goflylivechat/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), common.VisitorPageSize)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": gin.H{
"list": list,
"count": count,
"pagesize": common.PageSize,
},
})
}
func GetIpblacksByKefuId(c *gin.Context) {
kefuId, _ := c.Get("kefu_name")
list := models.FindIpsByKefuId(kefuId.(string))
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": list,
})
}

243
controller/kefu.go Normal file
View File

@ -0,0 +1,243 @@
package controller
import (
"github.com/gin-gonic/gin"
"goflylivechat/models"
"goflylivechat/tools"
"goflylivechat/ws"
"net/http"
)
func PostKefuAvator(c *gin.Context) {
avator := c.PostForm("avator")
if avator == "" {
c.JSON(200, gin.H{
"code": 400,
"msg": "不能为空",
"result": "",
})
return
}
kefuName, _ := c.Get("kefu_name")
models.UpdateUserAvator(kefuName.(string), avator)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": "",
})
}
func PostKefuPass(c *gin.Context) {
kefuName, _ := c.Get("kefu_name")
newPass := c.PostForm("new_pass")
confirmNewPass := c.PostForm("confirm_new_pass")
old_pass := c.PostForm("old_pass")
if newPass != confirmNewPass {
c.JSON(200, gin.H{
"code": 400,
"msg": "密码不一致",
"result": "",
})
return
}
user := models.FindUser(kefuName.(string))
if user.Password != tools.Md5(old_pass) {
c.JSON(200, gin.H{
"code": 400,
"msg": "旧密码不正确",
"result": "",
})
return
}
models.UpdateUserPass(kefuName.(string), tools.Md5(newPass))
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": "",
})
}
func PostKefuClient(c *gin.Context) {
kefuName, _ := c.Get("kefu_name")
clientId := c.PostForm("client_id")
if clientId == "" {
c.JSON(200, gin.H{
"code": 400,
"msg": "client_id不能为空",
})
return
}
models.CreateUserClient(kefuName.(string), clientId)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": "",
})
}
func GetKefuInfo(c *gin.Context) {
kefuName, _ := c.Get("kefu_name")
user := models.FindUser(kefuName.(string))
info := make(map[string]interface{})
info["avator"] = user.Avator
info["username"] = user.Name
info["nickname"] = user.Nickname
info["uid"] = user.ID
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 GetOtherKefuList(c *gin.Context) {
idStr, _ := c.Get("kefu_id")
id := idStr.(float64)
result := make([]interface{}, 0)
ws.SendPingToKefuClient()
kefus := models.FindUsers()
for _, kefu := range kefus {
if uint(id) == kefu.ID {
continue
}
item := make(map[string]interface{})
item["name"] = kefu.Name
item["nickname"] = kefu.Nickname
item["avator"] = kefu.Avator
item["status"] = "offline"
kefu, ok := ws.KefuList[kefu.Name]
if ok && kefu != nil {
item["status"] = "online"
}
result = append(result, item)
}
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": result,
})
}
func PostTransKefu(c *gin.Context) {
kefuId := c.Query("kefu_id")
visitorId := c.Query("visitor_id")
curKefuId, _ := c.Get("kefu_name")
user := models.FindUser(kefuId)
visitor := models.FindVisitorByVistorId(visitorId)
if user.Name == "" || visitor.Name == "" {
c.JSON(200, gin.H{
"code": 400,
"msg": "访客或客服不存在",
})
return
}
models.UpdateVisitorKefu(visitorId, kefuId)
ws.UpdateVisitorUser(visitorId, kefuId)
go ws.VisitorOnline(kefuId, visitor)
go ws.VisitorOffline(curKefuId.(string), visitor.VisitorId, visitor.Name)
go ws.VisitorNotice(visitor.VisitorId, "客服转接到"+user.Nickname)
c.JSON(200, gin.H{
"code": 200,
"msg": "转移成功",
})
}
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 PostKefuRegister(c *gin.Context) {
name := c.PostForm("username")
password := c.PostForm("password")
nickname := c.PostForm("nickname")
avatar := "/static/images/4.jpg"
if name == "" || password == "" {
c.JSON(http.StatusOK, gin.H{
"code": 400,
"msg": "All fields are required",
"result": nil,
})
return
}
existingUser := models.FindUser(name)
if existingUser.Name != "" {
c.JSON(http.StatusOK, gin.H{
"code": 409,
"msg": "Username already exists",
"result": nil,
})
return
}
userID := models.CreateUser(name, tools.Md5(password), avatar, nickname)
if userID == 0 {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "Registration Failed",
"result": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": "Registration successful",
"result": gin.H{
"user_id": userID,
},
})
}
func PostKefuInfo(c *gin.Context) {
name, _ := c.Get("kefu_name")
password := c.PostForm("password")
avator := c.PostForm("avator")
nickname := c.PostForm("nickname")
if password != "" {
password = tools.Md5(password)
}
if name == "" {
c.JSON(200, gin.H{
"code": 400,
"msg": "客服账号不能为空",
})
return
}
models.UpdateUser(name.(string), password, avator, nickname)
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 DeleteKefuInfo(c *gin.Context) {
kefuId := c.Query("id")
models.DeleteUserById(kefuId)
models.DeleteRoleByUserId(kefuId)
c.JSON(200, gin.H{
"code": 200,
"msg": "删除成功",
"result": "",
})
}

62
controller/login.go Normal file
View File

@ -0,0 +1,62 @@
package controller
import (
"github.com/gin-gonic/gin"
"goflylivechat/models"
"goflylivechat/tools"
"time"
)
// @Summary User Authentication API
// @Description Validates user credentials and returns access token
// @Tags Authentication
// @Produce json
// @Accept multipart/form-data
// @Param username formData string true "Registered username"
// @Param password formData string true "Account password"
// @Param type formData string true "Auth type (e.g., 'admin' or 'user')"
// @Success 200 {object} Response
// @Failure 401 {object} Response
// @Failure 500 {object} Response
// @Router /check [post]
func LoginCheckPass(c *gin.Context) {
password := c.PostForm("password")
username := c.PostForm("username")
info := models.FindUser(username)
// Authentication failed case
if info.Name == "" || info.Password != tools.Md5(password) {
c.JSON(200, gin.H{
"code": 401,
"message": "Incorrect username or password", // User-friendly message
})
return
}
// Prepare user session data
userinfo := map[string]interface{}{
"kefu_name": info.Name,
"kefu_id": info.ID,
"create_time": time.Now().Unix(),
}
// Token generation
token, err := tools.MakeToken(userinfo)
if err != nil {
c.JSON(200, gin.H{
"code": 500,
"message": "Login temporarily unavailable",
})
return
}
// Successful response
c.JSON(200, gin.H{
"code": 200,
"message": "Login successful",
"result": gin.H{
"token": token,
"created_at": userinfo["create_time"],
},
})
}

113
controller/main.go Normal file
View File

@ -0,0 +1,113 @@
package controller
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
"goflylivechat/common"
"goflylivechat/models"
"goflylivechat/tools"
"goflylivechat/ws"
"io/ioutil"
"log"
"os"
"strings"
)
func PostInstall(c *gin.Context) {
notExist, _ := tools.IsFileNotExist("./install.lock")
if !notExist {
c.JSON(200, gin.H{
"code": 400,
"msg": "系统已经安装过了",
})
return
}
server := c.PostForm("server")
port := c.PostForm("port")
database := c.PostForm("database")
username := c.PostForm("username")
password := c.PostForm("password")
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", username, password, server, port, database)
_, err := gorm.Open("mysql", dsn)
if err != nil {
log.Println(err)
tools.Logger().Println(err)
c.JSON(200, gin.H{
"code": 400,
"msg": "数据库连接失败:" + err.Error(),
})
return
}
isExist, _ := tools.IsFileExist(common.Dir)
if !isExist {
os.Mkdir(common.Dir, os.ModePerm)
}
fileConfig := common.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, server, port, database, username, password)
file.WriteString(data)
models.Connect()
installFile, _ := os.OpenFile("./install.lock", os.O_RDWR|os.O_CREATE, os.ModePerm)
installFile.WriteString("gofly live chat")
ok, err := install()
if !ok {
c.JSON(200, gin.H{
"code": 200,
"msg": err.Error(),
})
return
}
c.JSON(200, gin.H{
"code": 200,
"msg": "安装成功",
})
}
func install() (bool, error) {
sqlFile := common.Dir + "go-fly.sql"
isExit, _ := tools.IsFileExist(common.MysqlConf)
dataExit, _ := tools.IsFileExist(sqlFile)
if !isExit || !dataExit {
return false, errors.New("config/mysql.json 数据库配置文件或者数据库文件go-fly.sql不存在")
}
sqls, _ := ioutil.ReadFile(sqlFile)
sqlArr := strings.Split(string(sqls), "|")
for _, sql := range sqlArr {
if sql == "" {
continue
}
err := models.Execute(sql)
if err == nil {
log.Println(sql, "\t success!")
} else {
log.Println(sql, err, "\t failed!")
}
}
return true, nil
}
func GetStatistics(c *gin.Context) {
visitors := models.CountVisitors()
message := models.CountMessage(nil, nil)
session := len(ws.ClientList)
kefuNum := 0
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": gin.H{
"visitors": visitors,
"message": message,
"session": session + kefuNum,
},
})
}

353
controller/message.go Normal file
View File

@ -0,0 +1,353 @@
package controller
import (
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"goflylivechat/common"
"goflylivechat/models"
"goflylivechat/tools"
"goflylivechat/ws"
"os"
"path"
"strconv"
"strings"
"time"
)
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
}
//限流
if !tools.LimitFreqSingle("sendmessage:"+c.ClientIP(), 1, 2) {
c.JSON(200, gin.H{
"code": 400,
"msg": c.ClientIP() + "发送频率过快",
})
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)
//var msg TypeMessage
if cType == "kefu" {
guest, ok := ws.ClientList[vistorInfo.VisitorId]
if guest != nil && ok {
ws.VisitorMessage(vistorInfo.VisitorId, content, kefuInfo)
}
ws.KefuMessage(vistorInfo.VisitorId, content, kefuInfo)
//msg = TypeMessage{
// Type: "message",
// Data: ws.ClientMessage{
// Name: kefuInfo.Nickname,
// Avator: kefuInfo.Avator,
// Id: vistorInfo.VisitorId,
// Time: time.Now().Format("2006-01-02 15:04:05"),
// ToId: vistorInfo.VisitorId,
// Content: content,
// IsKefu: "yes",
// },
//}
//str2, _ := json.Marshal(msg)
//ws.OneKefuMessage(kefuInfo.Name, str2)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
})
}
if cType == "visitor" {
guest, ok := ws.ClientList[vistorInfo.VisitorId]
if ok && guest != nil {
guest.UpdateTime = time.Now()
}
//kefuConns, ok := ws.KefuList[kefuInfo.Name]
//if kefuConns == nil || !ok {
// c.JSON(200, gin.H{
// "code": 200,
// "msg": "ok",
// })
// return
//}
msg := ws.TypeMessage{
Type: "message",
Data: ws.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"),
IsKefu: "no",
},
}
str, _ := json.Marshal(msg)
ws.OneKefuMessage(kefuInfo.Name, str)
//ws.KefuMessage(vistorInfo.VisitorId, content, kefuInfo)
kefu, ok := ws.KefuList[kefuInfo.Name]
if !ok || kefu == nil {
go SendNoticeEmail(content+"|"+vistorInfo.Name, content)
}
go ws.VisitorAutoReply(vistorInfo, kefuInfo, content)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
})
}
}
func SendKefuMessage(c *gin.Context) {
fromId, _ := c.Get("kefu_name")
toId := c.PostForm("to_id")
content := c.PostForm("content")
cType := c.PostForm("type")
if content == "" {
c.JSON(200, gin.H{
"code": 400,
"msg": "内容不能为空",
})
return
}
//限流
if !tools.LimitFreqSingle("sendmessage:"+c.ClientIP(), 1, 2) {
c.JSON(200, gin.H{
"code": 400,
"msg": c.ClientIP() + "发送频率过快",
})
return
}
var kefuInfo models.User
var vistorInfo models.Visitor
kefuInfo = models.FindUser(fromId.(string))
vistorInfo = models.FindVisitorByVistorId(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)
//var msg TypeMessage
guest, ok := ws.ClientList[vistorInfo.VisitorId]
if guest != nil && ok {
ws.VisitorMessage(vistorInfo.VisitorId, content, kefuInfo)
}
ws.KefuMessage(vistorInfo.VisitorId, content, kefuInfo)
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 := ws.TypeMessage{
Type: "notice",
Data: notice,
}
str, _ := json.Marshal(msg)
for _, visitor := range ws.ClientList {
visitor.Conn.WriteMessage(websocket.TextMessage, str)
}
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
})
}
func SendCloseMessageV2(c *gin.Context) {
visitorId := c.Query("visitor_id")
if visitorId == "" {
c.JSON(200, gin.H{
"code": 400,
"msg": "visitor_id不能为空",
})
return
}
oldUser, ok := ws.ClientList[visitorId]
if oldUser != nil || ok {
msg := ws.TypeMessage{
Type: "force_close",
Data: visitorId,
}
str, _ := json.Marshal(msg)
err := oldUser.Conn.WriteMessage(websocket.TextMessage, str)
oldUser.Conn.Close()
delete(ws.ClientList, visitorId)
tools.Logger().Println("close_message", oldUser, err)
}
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
})
}
func UploadImg(c *gin.Context) {
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
}
isMainUploadExist, _ := tools.IsFileExist(common.Upload)
if !isMainUploadExist {
os.Mkdir(common.Upload, os.ModePerm)
}
fileName := tools.Md5(fmt.Sprintf("%s%s", f.Filename, time.Now().String()))
fildDir := fmt.Sprintf("%s%d%s/", common.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)
c.JSON(200, gin.H{
"code": 200,
"msg": "上传成功!",
"result": gin.H{
"path": filepath,
},
})
}
}
func UploadFile(c *gin.Context) {
f, err := c.FormFile("realfile")
if err != nil {
c.JSON(200, gin.H{
"code": 400,
"msg": "上传失败!",
})
return
} else {
fileExt := strings.ToLower(path.Ext(f.Filename))
if f.Size >= 90*1024*1024 {
c.JSON(200, gin.H{
"code": 400,
"msg": "上传失败!不允许超过90M",
})
return
}
fileName := tools.Md5(fmt.Sprintf("%s%s", f.Filename, time.Now().String()))
fildDir := fmt.Sprintf("%s%d%s/", common.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)
c.JSON(200, gin.H{
"code": 200,
"msg": "上传成功!",
"result": gin.H{
"path": filepath,
"ext": fileExt,
"size": f.Size,
"name": f.Filename,
},
})
}
}
func GetMessagesV2(c *gin.Context) {
visitorId := c.Query("visitor_id")
messages := models.FindMessageByVisitorId(visitorId)
//result := make([]map[string]interface{}, 0)
chatMessages := make([]ChatMessage, 0)
var visitor models.Visitor
var kefu models.User
for _, message := range messages {
//item := make(map[string]interface{})
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,
})
}
func GetMessagespages(c *gin.Context) {
visitorId := c.Query("visitor_id")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pagesize", "10"))
if pageSize > 20 {
pageSize = 20
}
count := models.CountMessage("visitor_id = ?", visitorId)
list := models.FindMessageByPage(uint(page), uint(pageSize), "message.visitor_id = ?", visitorId)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": gin.H{
"count": count,
"page": page,
"list": list,
"pagesize": pageSize,
},
})
}

32
controller/notice.go Normal file
View File

@ -0,0 +1,32 @@
package controller
import (
"github.com/gin-gonic/gin"
"goflylivechat/models"
)
func GetNotice(c *gin.Context) {
kefuId := c.Query("kefu_id")
user := models.FindUser(kefuId)
if user.ID == 0 {
c.JSON(200, gin.H{
"code": 400,
"msg": "user not found",
})
return
}
welcomeMessage := models.FindConfigByUserId(user.Name, "WelcomeMessage")
offlineMessage := models.FindConfigByUserId(user.Name, "OfflineMessage")
allNotice := models.FindConfigByUserId(user.Name, "AllNotice")
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": gin.H{
"welcome": welcomeMessage.ConfValue,
"offline": offlineMessage.ConfValue,
"avatar": user.Avator,
"nickname": user.Nickname,
"allNotice": allNotice.ConfValue,
},
})
}

123
controller/reply.go Normal file
View File

@ -0,0 +1,123 @@
package controller
import (
"github.com/gin-gonic/gin"
"goflylivechat/models"
"log"
)
type ReplyForm struct {
GroupName string `form:"group_name" binding:"required"`
}
type ReplyContentForm struct {
GroupId string `form:"group_id" binding:"required"`
Content string `form:"content" binding:"required"`
ItemName string `form:"item_name" binding:"required"`
}
func GetReplys(c *gin.Context) {
kefuId, _ := c.Get("kefu_name")
log.Println(kefuId)
res := models.FindReplyByUserId(kefuId)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": res,
})
}
func GetAutoReplys(c *gin.Context) {
kefu_id := c.Query("kefu_id")
res := models.FindReplyTitleByUserId(kefu_id)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": res,
})
}
func PostReply(c *gin.Context) {
var replyForm ReplyForm
kefuId, _ := c.Get("kefu_name")
err := c.Bind(&replyForm)
if err != nil {
c.JSON(200, gin.H{
"code": 400,
"msg": "error:" + err.Error(),
})
return
}
models.CreateReplyGroup(replyForm.GroupName, kefuId.(string))
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
})
}
func PostReplyContent(c *gin.Context) {
var replyContentForm ReplyContentForm
kefuId, _ := c.Get("kefu_name")
err := c.Bind(&replyContentForm)
if err != nil {
c.JSON(400, gin.H{
"code": 200,
"msg": "error:" + err.Error(),
})
return
}
models.CreateReplyContent(replyContentForm.GroupId, kefuId.(string), replyContentForm.Content, replyContentForm.ItemName)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
})
}
func PostReplyContentSave(c *gin.Context) {
kefuId, _ := c.Get("kefu_name")
replyId := c.PostForm("reply_id")
replyTitle := c.PostForm("reply_title")
replyContent := c.PostForm("reply_content")
if replyId == "" || replyTitle == "" || replyContent == "" {
c.JSON(400, gin.H{
"code": 200,
"msg": "参数错误!",
})
return
}
models.UpdateReplyContent(replyId, kefuId.(string), replyTitle, replyContent)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
})
}
func DelReplyContent(c *gin.Context) {
kefuId, _ := c.Get("kefu_name")
id := c.Query("id")
models.DeleteReplyContent(id, kefuId.(string))
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
})
}
func DelReplyGroup(c *gin.Context) {
kefuId, _ := c.Get("kefu_name")
id := c.Query("id")
models.DeleteReplyGroup(id, kefuId.(string))
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
})
}
func PostReplySearch(c *gin.Context) {
kefuId, _ := c.Get("kefu_name")
search := c.PostForm("search")
if search == "" {
c.JSON(200, gin.H{
"code": 400,
"msg": "参数错误",
})
return
}
res := models.FindReplyBySearcch(kefuId, search)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": res,
})
}

33
controller/response.go Normal file
View File

@ -0,0 +1,33 @@
package controller
var (
Port string
)
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"`
}
type VisitorOnline struct {
Uid string `json:"uid"`
Username string `json:"username"`
Avator string `json:"avator"`
LastMessage string `json:"last_message"`
}
type GetuiResponse struct {
Code float64 `json:"code"`
Msg string `json:"msg"`
Data map[string]interface{} `json:"data"`
}
type VisitorExtra struct {
VisitorName string `json:"visitorName"`
VisitorAvatar string `json:"visitorAvatar"`
}

33
controller/role.go Normal file
View File

@ -0,0 +1,33 @@
package controller
import (
"github.com/gin-gonic/gin"
"goflylivechat/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": "修改成功",
})
}

44
controller/setting.go Normal file
View File

@ -0,0 +1,44 @@
package controller
import (
"github.com/gin-gonic/gin"
"goflylivechat/models"
)
func GetConfigs(c *gin.Context) {
kefuName, _ := c.Get("kefu_name")
configs := models.FindConfigsByUserId(kefuName)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": configs,
})
}
func GetConfig(c *gin.Context) {
key := c.Query("key")
config := models.FindConfig(key)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": config,
})
}
func PostConfig(c *gin.Context) {
key := c.PostForm("key")
value := c.PostForm("value")
kefuName, _ := c.Get("kefu_name")
if key == "" || value == "" {
c.JSON(200, gin.H{
"code": 400,
"msg": "error",
})
return
}
models.UpdateConfig(kefuName, key, value)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": "",
})
}

155
controller/shout.go Normal file
View File

@ -0,0 +1,155 @@
package controller
import (
"encoding/json"
"fmt"
"goflylivechat/models"
"goflylivechat/tools"
"goflylivechat/ws"
"log"
"strconv"
"time"
)
func SendServerJiang(title string, content string, domain 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%s", title, content)
desp := title + ":" + content + "[登录](http://" + domain + "/main)"
url := serverJiangAPI + "?text=" + sendStr + "&desp=" + desp
//log.Println(url)
res := tools.Get(url)
return res
}
func SendVisitorLoginNotice(kefuName, visitorName, avator, content, visitorId string) {
if !tools.LimitFreqSingle("sendnotice:"+visitorId, 1, 120) {
log.Println("SendVisitorLoginNotice limit")
return
}
userInfo := make(map[string]string)
userInfo["username"] = visitorName
userInfo["avator"] = avator
userInfo["content"] = content
msg := ws.TypeMessage{
Type: "notice",
Data: userInfo,
}
str, _ := json.Marshal(msg)
ws.OneKefuMessage(kefuName, str)
}
func SendNoticeEmail(username, msg string) {
smtp := models.FindConfig("NoticeEmailSmtp")
email := models.FindConfig("NoticeEmailAddress")
password := models.FindConfig("NoticeEmailPassword")
if smtp == "" || email == "" || password == "" {
return
}
err := tools.SendSmtp(smtp, email, password, []string{email}, "[通知]"+username, msg)
if err != nil {
log.Println(err)
}
}
func SendAppGetuiPush(kefu string, title, content string) {
token := models.FindConfig("GetuiToken")
if token == "" {
token = getGetuiToken()
if token == "" {
return
}
}
format := `
{
"request_id":"%s",
"settings":{
"ttl":3600000
},
"audience":{
"cid":[
"%s"
]
},
"push_message":{
"notification":{
"title":"%s",
"body":"%s",
"click_type":"url",
"url":"https//:xxx"
}
}
}
`
clients := models.FindClients(kefu)
if len(clients) == 0 {
return
}
//clientIds := make([]string, 0)
for _, client := range clients {
//clientIds = append(clientIds, client.Client_id)
req := fmt.Sprintf(format, tools.Md5(tools.Uuid()), client.Client_id, title, content)
num := sendPushApi(token, req)
if num == 10001 {
token = getGetuiToken()
sendPushApi(token, req)
}
}
}
func sendPushApi(token string, req string) int {
appid := models.FindConfig("GetuiAppID")
if appid == "" {
return 0
}
url := "https://restapi.getui.com/v2/" + appid + "/push/single/cid"
headers := make(map[string]string)
headers["Content-Type"] = "application/json;charset=utf-8"
headers["token"] = token
res, err := tools.PostHeader(url, []byte(req), headers)
tools.Logger().Infoln(url, req, err, res)
if err == nil && res != "" {
var pushRes GetuiResponse
json.Unmarshal([]byte(res), &pushRes)
if pushRes.Code == 10001 {
return 10001
}
}
return 200
}
func getGetuiToken() string {
appid := models.FindConfig("GetuiAppID")
appkey := models.FindConfig("GetuiAppKey")
//appsecret := models.FindConfig("GetuiAppSecret")
appmastersecret := models.FindConfig("GetuiMasterSecret")
if appid == "" {
return ""
}
type req struct {
Sign string `json:"sign"`
Timestamp string `json:"timestamp"`
Appkey string `json:"appkey"`
}
timestamp := strconv.FormatInt(time.Now().UnixNano()/1e6, 10)
reqJson := req{
Sign: tools.Sha256(appkey + timestamp + appmastersecret),
Timestamp: timestamp,
Appkey: appkey,
}
reqStr, _ := json.Marshal(reqJson)
url := "https://restapi.getui.com/v2/" + appid + "/auth"
res, err := tools.Post(url, "application/json;charset=utf-8", reqStr)
log.Println(url, string(reqStr), err, res)
if err == nil && res != "" {
var pushRes GetuiResponse
json.Unmarshal([]byte(res), &pushRes)
if pushRes.Code == 0 {
token := pushRes.Data["token"].(string)
//models.UpdateConfig("GetuiToken", token)
return token
}
}
return ""
}

58
controller/tcp.go Normal file
View File

@ -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",
})
}

310
controller/visitor.go Normal file
View File

@ -0,0 +1,310 @@
package controller
import (
"encoding/json"
"github.com/gin-gonic/gin"
"goflylivechat/common"
"goflylivechat/models"
"goflylivechat/tools"
"goflylivechat/ws"
"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",
// })
// }
func PostVisitorLogin(c *gin.Context) {
ipcity := tools.ParseIp(c.ClientIP())
avator := ""
userAgent := c.GetHeader("User-Agent")
if tools.IsMobile(userAgent) {
avator = "/static/images/1.png"
} else {
avator = "/static/images/2.png"
}
toId := c.PostForm("to_id")
id := c.PostForm("visitor_id")
if id == "" {
id = tools.Uuid()
}
refer := c.PostForm("refer")
var (
city string
name string
)
if ipcity != nil {
city = ipcity.CountryName + ipcity.RegionName + ipcity.CityName
name = ipcity.CountryName + ipcity.RegionName + ipcity.CityName
if ipcity.CityName == "本机地址" || ipcity.RegionName == "本机地址" || ipcity.CountryName == "本机地址" {
city = "local address"
}
} else {
city = "Unrecognized Region"
name = "visitor"
}
if name == "本机地址本机地址" {
name = "local visitor"
}
client_ip := c.ClientIP()
extra := c.PostForm("extra")
extraJson := tools.Base64Decode(extra)
if extraJson != "" {
var extraObj VisitorExtra
err := json.Unmarshal([]byte(extraJson), &extraObj)
if err == nil {
if extraObj.VisitorName != "" {
name = extraObj.VisitorName
}
if extraObj.VisitorAvatar != "" {
avator = extraObj.VisitorAvatar
}
}
}
//log.Println(name,avator,c.ClientIP(),toId,id,refer,city,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
}
visitor := models.FindVisitorByVistorId(id)
if visitor.Name != "" {
avator = visitor.Avator
//更新状态上线
models.UpdateVisitor(name, visitor.Avator, id, 1, c.ClientIP(), c.ClientIP(), refer, extra)
} else {
models.CreateVisitor(name, avator, c.ClientIP(), toId, id, refer, city, client_ip, extra)
}
visitor.Name = name
visitor.Avator = avator
visitor.ToId = toId
visitor.ClientIp = c.ClientIP()
visitor.VisitorId = id
//各种通知
go SendNoticeEmail(visitor.Name, " incoming!")
//go SendAppGetuiPush(kefuInfo.Name, visitor.Name, visitor.Name+" incoming!")
go SendVisitorLoginNotice(kefuInfo.Name, visitor.Name, visitor.Avator, visitor.Name+" incoming!", visitor.VisitorId)
go ws.VisitorOnline(kefuInfo.Name, visitor)
//go SendServerJiang(visitor.Name, "来了", c.Request.Host)
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"))
pagesize, _ := strconv.Atoi(c.Query("pagesize"))
if pagesize == 0 {
pagesize = int(common.VisitorPageSize)
}
kefuId, _ := c.Get("kefu_name")
vistors := models.FindVisitorsByKefuId(uint(page), uint(pagesize), kefuId.(string))
count := models.CountVisitorsByKefuId(kefuId.(string))
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": gin.H{
"list": vistors,
"count": count,
"pagesize": common.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")
query := "message.visitor_id= ?"
messages := models.FindMessageByWhere(query, visitorId)
result := make([]map[string]interface{}, 0)
for _, message := range messages {
item := make(map[string]interface{})
item["time"] = message.CreatedAt.Format("2006-01-02 15:04:05")
item["content"] = message.Content
item["mes_type"] = message.MesType
item["visitor_name"] = message.VisitorName
item["visitor_avator"] = message.VisitorAvator
item["kefu_name"] = message.KefuName
item["kefu_avator"] = message.KefuAvator
result = append(result, item)
}
go 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 ws.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,
},
})
}
// @Summary 获取客服的在线访客列表接口
// @Produce json
// @Success 200 {object} controller.Response
// @Failure 200 {object} controller.Response
// @Router /visitors_kefu_online [get]
func GetKefusVisitorOnlines(c *gin.Context) {
kefuName, _ := c.Get("kefu_name")
users := make([]*VisitorOnline, 0)
visitorIds := make([]string, 0)
for uid, visitor := range ws.ClientList {
if visitor.To_id != kefuName {
continue
}
userInfo := new(VisitorOnline)
userInfo.Uid = uid
userInfo.Username = 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.LastMessage = temp[user.Uid]
if user.LastMessage == "" {
user.LastMessage = "new visitor"
}
}
tcps := make([]string, 0)
for ip, _ := range clientTcpList {
tcps = append(tcps, ip)
}
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
"result": users,
})
}

35
controller/weixin.go Normal file
View File

@ -0,0 +1,35 @@
package controller
import (
"crypto/sha1"
"encoding/hex"
"github.com/gin-gonic/gin"
"goflylivechat/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验证失败")
}
}

23
go.mod Normal file
View File

@ -0,0 +1,23 @@
module goflylivechat
go 1.16
require (
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-smtp v0.13.0
github.com/gin-contrib/sessions v0.0.3
github.com/gin-gonic/gin v1.7.7
github.com/go-sql-driver/mysql v1.5.0
github.com/gobuffalo/packr/v2 v2.5.1
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/gorilla/websocket v1.4.2
github.com/ipipdotnet/ipdb-go v1.3.0
github.com/jinzhu/gorm v1.9.14
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/satori/go.uuid v1.2.0
github.com/sirupsen/logrus v1.4.2
github.com/spf13/cobra v0.0.5
github.com/zh-five/xdaemon v0.1.1
)

188
go.sum Normal file
View File

@ -0,0 +1,188 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
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/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
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/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M=
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY=
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/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/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/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuLP6fBI=
github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy25mPkXdOgeB9I=
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.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
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.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
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-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
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.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/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
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.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/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
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/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/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
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/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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
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/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/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-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
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/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/zh-five/xdaemon v0.1.1 h1:W5VyJ+5ROjjcb9vNcF/SgWPwTzIRYIsW2yZBAomqMW8=
github.com/zh-five/xdaemon v0.1.1/go.mod h1:i3cluMVOPp/UcX2KDU2qzRv25f8u4y14tHzBPQhD8lI=
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/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
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-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-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/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-20190215142949-d0b11bdaac8a/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-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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
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=

9
gofly.go Normal file
View File

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

112
import.sql Normal file
View File

@ -0,0 +1,112 @@
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '',
`password` varchar(50) NOT NULL DEFAULT '',
`nickname` varchar(50) NOT NULL DEFAULT '',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
`avator` varchar(100) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
TRUNCATE TABLE `user`;
INSERT INTO `user` (`id`, `name`, `password`, `nickname`, `created_at`, `updated_at`, `deleted_at`, `avator`) VALUE
(1, 'agent', 'b33aed8f3134996703dc39f9a7c95783', 'Open Source LiveChat Support', '2020-06-27 19:32:41', '2020-07-04 09:32:20', NULL, '/static/images/4.jpg');
DROP TABLE IF EXISTS `visitor`;
CREATE TABLE `visitor` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '',
`avator` varchar(500) NOT NULL DEFAULT '',
`source_ip` varchar(50) NOT NULL DEFAULT '',
`to_id` varchar(50) NOT NULL DEFAULT '',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
`visitor_id` varchar(100) NOT NULL DEFAULT '',
`status` tinyint(4) NOT NULL DEFAULT '0',
`refer` varchar(500) NOT NULL DEFAULT '',
`city` varchar(100) NOT NULL DEFAULT '',
`client_ip` varchar(100) NOT NULL DEFAULT '',
`extra` varchar(2048) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `visitor_id` (`visitor_id`),
KEY `to_id` (`to_id`),
KEY `idx_update` (`updated_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `message`;
CREATE TABLE `message` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`kefu_id` varchar(100) NOT NULL DEFAULT '',
`visitor_id` varchar(100) NOT NULL DEFAULT '',
`content` varchar(2048) NOT NULL DEFAULT '',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
`mes_type` enum('kefu','visitor') NOT NULL DEFAULT 'visitor',
`status` enum('read','unread') NOT NULL DEFAULT 'unread',
PRIMARY KEY (`id`),
KEY `kefu_id` (`kefu_id`),
KEY `visitor_id` (`visitor_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `ipblack`;
CREATE TABLE `ipblack` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ip` varchar(100) NOT NULL DEFAULT '',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`kefu_id` varchar(100) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `ip` (`ip`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `config`;
CREATE TABLE `config` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`conf_name` varchar(255) NOT NULL DEFAULT '',
`conf_key` varchar(255) NOT NULL DEFAULT '',
`conf_value` varchar(255) NOT NULL DEFAULT '',
`user_id` varchar(500) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `conf_key` (`conf_key`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `config` (`id`, `conf_name`, `conf_key`, `conf_value`, `user_id`) VALUES
(NULL, 'Announcement', 'AllNotice', 'Open source customer support system at your service','agent');
INSERT INTO `config` (`id`, `conf_name`, `conf_key`, `conf_value`, `user_id`) VALUES
(NULL, 'Offline Message', 'OfflineMessage', 'I am currently offline and will reply to you later!','agent');
INSERT INTO `config` (`id`, `conf_name`, `conf_key`, `conf_value`, `user_id`) VALUES
(NULL, 'Welcome Message', 'WelcomeMessage', 'How may I help you?','agent');
INSERT INTO `config` (`id`, `conf_name`, `conf_key`, `conf_value`, `user_id`) VALUES
(NULL, 'Email Address (SMTP)', 'NoticeEmailSmtp', '','agent');
INSERT INTO `config` (`id`, `conf_name`, `conf_key`, `conf_value`, `user_id`) VALUES
(NULL, 'Email Account', 'NoticeEmailAddress', '','agent');
INSERT INTO `config` (`id`, `conf_name`, `conf_key`, `conf_value`, `user_id`) VALUES
(NULL, 'Email Password (SMTP)', 'NoticeEmailPassword', '','agent');
DROP TABLE IF EXISTS `reply_group`;
CREATE TABLE `reply_group` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`group_name` varchar(50) NOT NULL DEFAULT '',
`user_id` varchar(50) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `reply_group` (`id`, `group_name`, `user_id`) VALUES
(NULL, 'Frequently Asked Questions', 'agent');
DROP TABLE IF EXISTS `reply_item`;
CREATE TABLE `reply_item` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`content` varchar(1024) NOT NULL DEFAULT '',
`group_id` int(11) NOT NULL DEFAULT '0',
`user_id` varchar(50) NOT NULL DEFAULT '',
`item_name` varchar(50) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

16
middleware/cross.go Normal file
View File

@ -0,0 +1,16 @@
package middleware
import "github.com/gin-gonic/gin"
func CrossSite(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
//服务器支持的所有跨域请求的方法
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE")
//允许跨域设置可以返回其他子段,可以自定义字段
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session")
// 允许浏览器(客户端)可以解析的头部 (重要)
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
//允许客户端传递校验信息比如 cookie (重要)
c.Header("Access-Control-Allow-Credentials", "true")
c.Next()
}

View File

@ -0,0 +1,12 @@
package middleware
import (
"github.com/gin-gonic/gin"
)
/**
域名中间件
*/
func DomainLimitMiddleware(c *gin.Context) {
}

19
middleware/ipblack.go Normal file
View File

@ -0,0 +1,19 @@
package middleware
import (
"github.com/gin-gonic/gin"
"goflylivechat/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
}
}

45
middleware/jwt.go Normal file
View File

@ -0,0 +1,45 @@
package middleware
import (
"github.com/gin-gonic/gin"
"goflylivechat/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["kefu_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("kefu_id", userinfo["kefu_id"])
c.Set("kefu_name", userinfo["kefu_name"])
}

45
middleware/logger.go Normal file
View File

@ -0,0 +1,45 @@
package middleware
import (
"github.com/gin-gonic/gin"
"goflylivechat/tools"
"time"
)
func NewMidLogger() gin.HandlerFunc {
logger := tools.Logger()
return func(c *gin.Context) {
// 开始时间
startTime := time.Now()
// 处理请求
c.Next()
// 结束时间
endTime := time.Now()
// 执行时间
latencyTime := endTime.Sub(startTime)
// 请求方式
reqMethod := c.Request.Method
// 请求路由
reqUri := c.Request.RequestURI
// 状态码
statusCode := c.Writer.Status()
// 请求IP
clientIP := c.ClientIP()
//日志格式
logger.Infof("| %3d | %13v | %15s | %s | %s |",
statusCode,
latencyTime,
clientIP,
reqMethod,
reqUri,
)
}
}

64
middleware/rbac.go Normal file
View File

@ -0,0 +1,64 @@
package middleware
import (
"github.com/gin-gonic/gin"
)
func RbacAuth(c *gin.Context) {
return
//roleId, _ := c.Get("role_id")
//role := models.FindRole(roleId)
//var flag bool
//rPaths := strings.Split(c.Request.RequestURI, "?")
//uriParam := fmt.Sprintf("%s:%s", c.Request.Method, rPaths[0])
//if role.Method != "*" || role.Path != "*" {
// paths := strings.Split(role.Path, ",")
// for _, p := range paths {
// if uriParam == p {
// flag = true
// break
// }
// }
// if !flag {
// c.JSON(200, gin.H{
// "code": 403,
// "msg": "没有权限:" + uriParam,
// })
// c.Abort()
// return
// }
//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
// }
//}
}

53
models/abouts.go Normal file
View File

@ -0,0 +1,53 @@
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"`
Page string `json:"page"`
}
func FindAbouts() []About {
var a []About
DB.Select("id,title_cn,page").Find(&a)
return a
}
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)
}

53
models/configs.go Normal file
View File

@ -0,0 +1,53 @@
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"`
UserId string `json:"user_id"`
}
func UpdateConfig(userid interface{}, key string, value string) {
config := FindConfigByUserId(userid, key)
if config.ID != 0 {
config.ConfValue = value
DB.Where("user_id = ? and conf_key = ?", userid, key).Update(config)
} else {
newConfig := &Config{
ID: 0,
ConfName: "",
ConfKey: key,
ConfValue: value,
UserId: userid.(string),
}
DB.Create(newConfig)
}
}
func FindConfigs() []Config {
var config []Config
DB.Find(&config)
return config
}
func FindConfigsByUserId(userid interface{}) []Config {
var config []Config
DB.Where("user_id = ?", userid).Find(&config)
return config
}
func FindConfig(key string) string {
for _, config := range CustomConfigs {
if key == config.ConfKey {
return config.ConfValue
}
}
return ""
}
func FindConfigByUserId(userId interface{}, key string) Config {
var config Config
DB.Where("user_id = ? and conf_key = ?", userId, key).Find(&config)
return config
}

57
models/ipblacks.go Normal file
View File

@ -0,0 +1,57 @@
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 FindIpsByKefuId(id string) []Ipblack {
var ipblack []Ipblack
DB.Where("kefu_id = ?", id).Find(&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
}

120
models/messages.go Normal file
View File

@ -0,0 +1,120 @@
package models
import (
"fmt"
"time"
)
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"`
}
type MessageKefu 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"`
VisitorName string `json:"visitor_name"`
VisitorAvator string `json:"visitor_avator"`
KefuName string `json:"kefu_name"`
KefuAvator string `json:"kefu_avator"`
CreateTime string `json:"create_time"`
}
func CreateMessage(kefu_id string, visitor_id string, content string, mes_type string) {
DB.Exec("set names utf8mb4")
v := &Message{
KefuId: kefu_id,
VisitorId: visitor_id,
Content: content,
MesType: mes_type,
Status: "unread",
}
v.UpdatedAt = time.Now()
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 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
if len(visitorIds) <= 0 {
return messages
}
var ids []Message
DB.Select("MAX(id) id").Where(" visitor_id in (? )", visitorIds).Group("visitor_id").Find(&ids)
if len(ids) <= 0 {
return messages
}
var idStr = make([]string, 0, 0)
for _, mes := range ids {
idStr = append(idStr, fmt.Sprintf("%d", mes.ID))
}
DB.Select("visitor_id,id,content").Where(" id in (? )", idStr).Find(&messages)
//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 FindLastMessageByVisitorId(visitorId string) Message {
var m Message
DB.Select("content").Where("visitor_id=?", visitorId).Order("id desc").First(&m)
return m
}
func FindMessageByWhere(query interface{}, args ...interface{}) []MessageKefu {
var messages []MessageKefu
DB.Table("message").Where(query, args...).Select("message.*,visitor.avator visitor_avator,visitor.name visitor_name,user.avator kefu_avator,user.nickname kefu_name").Joins("left join user on message.kefu_id=user.name").Joins("left join visitor on visitor.visitor_id=message.visitor_id").Order("message.id asc").Find(&messages)
return messages
}
//查询条数
func CountMessage(query interface{}, args ...interface{}) uint {
var count uint
DB.Model(&Message{}).Where(query, args...).Count(&count)
return count
}
//分页查询
func FindMessageByPage(page uint, pagesize uint, query interface{}, args ...interface{}) []*MessageKefu {
offset := (page - 1) * pagesize
if offset < 0 {
offset = 0
}
var messages []*MessageKefu
DB.Table("message").Select("message.*,visitor.avator visitor_avator,visitor.name visitor_name,user.avator kefu_avator,user.nickname kefu_name").Offset(offset).Joins("left join user on message.kefu_id=user.name").Joins("left join visitor on visitor.visitor_id=message.visitor_id").Where(query, args...).Limit(pagesize).Order("message.id desc").Find(&messages)
for _, mes := range messages {
mes.CreateTime = mes.CreatedAt.Format("2006-01-02 15:04:05")
}
return messages
}

45
models/models.go Normal file
View File

@ -0,0 +1,45 @@
package models
import (
"fmt"
"github.com/jinzhu/gorm"
"goflylivechat/common"
"log"
"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() {
Connect()
}
func Connect() error {
mysql := common.GetMysqlConf()
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysql.Username, mysql.Password, mysql.Server, mysql.Port, mysql.Database)
var err error
DB, err = gorm.Open("mysql", dsn)
if err != nil {
log.Println(err)
panic("数据库连接失败!")
return err
}
DB.SingularTable(true)
DB.LogMode(true)
DB.DB().SetMaxIdleConns(10)
DB.DB().SetMaxOpenConns(100)
DB.DB().SetConnMaxLifetime(59 * time.Second)
return nil
}
func Execute(sql string) error {
return DB.Exec(sql).Error
}
func CloseDB() {
defer DB.Close()
}

104
models/replys.go Normal file
View File

@ -0,0 +1,104 @@
package models
type ReplyItem struct {
Id string `json:"item_id"`
Content string `json:"item_content"`
GroupId string `json:"group_id"`
ItemName string `json:"item_name"`
UserId string `json:"user_id"`
}
type ReplyGroup struct {
Id string `json:"group_id"`
GroupName string `json:"group_name"`
UserId string `json:"user_id"`
Items []*ReplyItem `json:"items";"`
}
func FindReplyItemByUserIdTitle(userId interface{}, title string) ReplyItem {
var reply ReplyItem
DB.Where("user_id = ? and item_name = ?", userId, title).Find(&reply)
return reply
}
func FindReplyByUserId(userId interface{}) []*ReplyGroup {
var replyGroups []*ReplyGroup
//DB.Raw("select a.*,b.* from reply_group a left join reply_item b on a.id=b.group_id where a.user_id=? ", userId).Scan(&replyGroups)
var replyItems []*ReplyItem
DB.Where("user_id = ?", userId).Find(&replyGroups)
DB.Where("user_id = ?", userId).Find(&replyItems)
temp := make(map[string]*ReplyGroup)
for _, replyGroup := range replyGroups {
replyGroup.Items = make([]*ReplyItem, 0)
temp[replyGroup.Id] = replyGroup
}
for _, replyItem := range replyItems {
temp[replyItem.GroupId].Items = append(temp[replyItem.GroupId].Items, replyItem)
}
return replyGroups
}
func FindReplyTitleByUserId(userId interface{}) []*ReplyGroup {
var replyGroups []*ReplyGroup
//DB.Raw("select a.*,b.* from reply_group a left join reply_item b on a.id=b.group_id where a.user_id=? ", userId).Scan(&replyGroups)
var replyItems []*ReplyItem
DB.Where("user_id = ?", userId).Find(&replyGroups)
DB.Select("item_name,group_id").Where("user_id = ?", userId).Find(&replyItems)
temp := make(map[string]*ReplyGroup)
for _, replyGroup := range replyGroups {
replyGroup.Items = make([]*ReplyItem, 0)
temp[replyGroup.Id] = replyGroup
}
for _, replyItem := range replyItems {
temp[replyItem.GroupId].Items = append(temp[replyItem.GroupId].Items, replyItem)
}
return replyGroups
}
func CreateReplyGroup(groupName string, userId string) {
g := &ReplyGroup{
GroupName: groupName,
UserId: userId,
}
DB.Create(g)
}
func CreateReplyContent(groupId string, userId string, content, itemName string) {
g := &ReplyItem{
GroupId: groupId,
UserId: userId,
Content: content,
ItemName: itemName,
}
DB.Create(g)
}
func UpdateReplyContent(id, userId, title, content string) {
r := &ReplyItem{
ItemName: title,
Content: content,
}
DB.Model(&ReplyItem{}).Where("user_id = ? and id = ?", userId, id).Update(r)
}
func DeleteReplyContent(id string, userId string) {
DB.Where("user_id = ? and id = ?", userId, id).Delete(ReplyItem{})
}
func DeleteReplyGroup(id string, userId string) {
DB.Where("user_id = ? and id = ?", userId, id).Delete(ReplyGroup{})
DB.Where("user_id = ? and group_id = ?", userId, id).Delete(ReplyItem{})
}
func FindReplyBySearcch(userId interface{}, search string) []*ReplyGroup {
var replyGroups []*ReplyGroup
var replyItems []*ReplyItem
DB.Where("user_id = ?", userId).Find(&replyGroups)
DB.Where("user_id = ? and content like ?", userId, "%"+search+"%").Find(&replyItems)
temp := make(map[string]*ReplyGroup)
for _, replyGroup := range replyGroups {
replyGroup.Items = make([]*ReplyItem, 0)
temp[replyGroup.Id] = replyGroup
}
for _, replyItem := range replyItems {
temp[replyItem.GroupId].Items = append(temp[replyItem.GroupId].Items, replyItem)
}
var newReplyGroups []*ReplyGroup = make([]*ReplyGroup, 0)
for _, replyGroup := range replyGroups {
if len(replyGroup.Items) != 0 {
newReplyGroups = append(newReplyGroups, replyGroup)
}
}
return newReplyGroups
}

27
models/roles.go Normal file
View File

@ -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)
}

25
models/user_client.go Normal file
View File

@ -0,0 +1,25 @@
package models
import "time"
type User_client struct {
ID uint `gorm:"primary_key" json:"id"`
Kefu string `json:"kefu"`
Client_id string `json:"client_id"`
Created_at string `json:"created_at"`
}
func CreateUserClient(kefu, clientId string) uint {
u := &User_client{
Kefu: kefu,
Client_id: clientId,
Created_at: time.Now().Format("2006-01-02 15:04:05"),
}
DB.Create(u)
return u.ID
}
func FindClients(kefu string) []User_client {
var arr []User_client
DB.Where("kefu = ?", kefu).Find(&arr)
return arr
}

27
models/user_roles.go Normal file
View File

@ -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{})
}

76
models/users.go Normal file
View File

@ -0,0 +1,76 @@
package models
import (
_ "github.com/jinzhu/gorm/dialects/mysql"
"time"
)
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:"-"`
}
func CreateUser(name string, password string, avator string, nickname string) uint {
user := &User{
Name: name,
Password: password,
Avator: avator,
Nickname: nickname,
}
user.UpdatedAt = time.Now()
DB.Create(user)
return user.ID
}
func UpdateUser(name string, password string, avator string, nickname string) {
user := &User{
Avator: avator,
Nickname: nickname,
}
user.UpdatedAt = time.Now()
if password != "" {
user.Password = password
}
DB.Model(&User{}).Where("name = ?", name).Update(user)
}
func UpdateUserPass(name string, pass string) {
user := &User{
Password: pass,
}
user.UpdatedAt = time.Now()
DB.Model(user).Where("name = ?", name).Update("Password", pass)
}
func UpdateUserAvator(name string, avator string) {
user := &User{
Avator: avator,
}
user.UpdatedAt = time.Now()
DB.Model(user).Where("name = ?", name).Update("Avator", avator)
}
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
}

115
models/visitors.go Normal file
View File

@ -0,0 +1,115 @@
package models
import (
"time"
)
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"`
Extra string `json:"extra"`
}
func CreateVisitor(name, avator, sourceIp, toId, visitorId, refer, city, clientIp, extra string) {
v := &Visitor{
Name: name,
Avator: avator,
SourceIp: sourceIp,
ToId: toId,
VisitorId: visitorId,
Status: 1,
Refer: refer,
City: city,
ClientIp: clientIp,
Extra: extra,
}
v.UpdatedAt = time.Now()
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
//sql := fmt.Sprintf("select * from visitor where id>=(select id from visitor where to_id='%s' order by updated_at desc limit %d,1) and to_id='%s' order by updated_at desc limit %d ", kefuId, offset, kefuId, pagesize)
//DB.Raw(sql).Scan(&visitors)
DB.Where("to_id=?", kefuId).Offset(offset).Limit(pagesize).Order("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(name, avator, visitorId string, status uint, clientIp string, sourceIp string, refer, extra string) {
visitor := &Visitor{
Status: status,
ClientIp: clientIp,
SourceIp: sourceIp,
Refer: refer,
Extra: extra,
Name: name,
Avator: avator,
}
visitor.UpdatedAt = time.Now()
DB.Model(visitor).Where("visitor_id = ?", visitorId).Update(visitor)
}
func UpdateVisitorKefu(visitorId string, kefuId string) {
visitor := Visitor{}
DB.Model(&visitor).Where("visitor_id = ?", visitorId).Update("to_id", kefuId)
}
//查询条数
func CountVisitors() uint {
var count uint
DB.Model(&Visitor{}).Count(&count)
return count
}
//查询条数
func CountVisitorsByKefuId(kefuId string) uint {
var count uint
DB.Model(&Visitor{}).Where("to_id=?", kefuId).Count(&count)
return count
}
//查询每天条数
type EveryDayNum struct {
Day string `json:"day"`
Num int64 `json:"num"`
}
func CountVisitorsEveryDay(toId string) []EveryDayNum {
var results []EveryDayNum
DB.Raw("select DATE_FORMAT(created_at,'%y-%m-%d') as day ,"+
"count(*) as num from visitor where to_id=? group by day order by day desc limit 30",
toId).Scan(&results)
return results
}

54
models/welcomes.go Normal file
View File

@ -0,0 +1,54 @@
package models
import "time"
type Welcome struct {
ID uint `gorm:"primary_key" json:"id"`
UserId string `json:"user_id"`
Keyword string `json:"keyword"`
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(),
Keyword: "welcome",
}
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 FindWelcomeByUserIdKey(userId interface{}, keyword interface{}) Welcome {
var w Welcome
DB.Where("user_id = ? and keyword=?", userId, keyword).First(&w)
return w
}
func FindWelcomesByUserId(userId interface{}) []Welcome {
var w []Welcome
DB.Where("user_id = ?", userId).Find(&w)
return w
}
func FindWelcomesByKeyword(userId interface{}, keyword interface{}) []Welcome {
var w []Welcome
DB.Where("user_id = ? and keyword=?", userId, keyword).Find(&w)
return w
}
func DeleteWelcome(userId interface{}, id string) {
DB.Where("user_id = ? and id = ?", userId, id).Delete(Welcome{})
}

96
router/api.go Normal file
View File

@ -0,0 +1,96 @@
package router
import (
"github.com/gin-gonic/gin"
"goflylivechat/controller"
"goflylivechat/middleware"
"goflylivechat/ws"
)
func InitApiRouter(engine *gin.Engine) {
//路由分组
v2 := engine.Group("/2")
{
//获取消息
v2.GET("/messages", controller.GetMessagesV2)
//发送单条信息
v2.POST("/message", middleware.Ipblack, controller.SendMessageV2)
//关闭连接
v2.GET("/message_close", controller.SendCloseMessageV2)
//分页查询消息
v2.GET("/messagesPages", controller.GetMessagespages)
}
engine.GET("/captcha", controller.GetCaptcha)
engine.POST("/check", controller.LoginCheckPass)
engine.GET("/userinfo", middleware.JwtApiMiddleware, controller.GetKefuInfoAll)
engine.POST("/register", middleware.Ipblack, controller.PostKefuRegister)
engine.POST("/install", controller.PostInstall)
//前后聊天
engine.GET("/ws_kefu", middleware.JwtApiMiddleware, ws.NewKefuServer)
engine.GET("/ws_visitor", middleware.Ipblack, ws.NewVisitorServer)
engine.GET("/messages", controller.GetVisitorMessage)
engine.GET("/message_notice", controller.SendVisitorNotice)
//上传文件
engine.POST("/uploadimg", middleware.Ipblack, controller.UploadImg)
//上传文件
engine.POST("/uploadfile", middleware.Ipblack, controller.UploadFile)
//获取未读消息数
engine.GET("/message_status", controller.GetVisitorMessage)
//设置消息已读
engine.POST("/message_status", controller.GetVisitorMessage)
//获取客服信息
engine.POST("/kefuinfo_client", middleware.JwtApiMiddleware, controller.PostKefuClient)
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", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.GetKefuList)
engine.GET("/other_kefulist", middleware.JwtApiMiddleware, controller.GetOtherKefuList)
engine.GET("/trans_kefu", middleware.JwtApiMiddleware, controller.PostTransKefu)
engine.POST("/modifypass", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.PostKefuPass)
engine.POST("/modifyavator", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.PostKefuAvator)
//角色列表
engine.GET("/roles", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.GetRoleList)
engine.POST("/role", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.PostRole)
engine.GET("/visitors_online", controller.GetVisitorOnlines)
engine.GET("/visitors_kefu_online", middleware.JwtApiMiddleware, controller.GetKefusVisitorOnlines)
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("/aboutpages", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.GetAbouts)
engine.GET("/notice", controller.GetNotice)
engine.POST("/ipblack", middleware.JwtApiMiddleware, middleware.Ipblack, controller.PostIpblack)
engine.DELETE("/ipblack", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.DelIpblack)
engine.GET("/ipblacks_all", middleware.JwtApiMiddleware, controller.GetIpblacks)
engine.GET("/ipblacks", middleware.JwtApiMiddleware, controller.GetIpblacksByKefuId)
engine.GET("/configs", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.GetConfigs)
engine.POST("/config", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.PostConfig)
engine.GET("/config", controller.GetConfig)
engine.GET("/autoreply", controller.GetAutoReplys)
engine.GET("/replys", middleware.JwtApiMiddleware, controller.GetReplys)
engine.POST("/reply", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.PostReply)
engine.POST("/reply_content", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.PostReplyContent)
engine.POST("/reply_content_save", middleware.JwtApiMiddleware, controller.PostReplyContentSave)
engine.DELETE("/reply_content", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.DelReplyContent)
engine.DELETE("/reply", middleware.JwtApiMiddleware, middleware.RbacAuth, controller.DelReplyGroup)
engine.POST("/reply_search", middleware.JwtApiMiddleware, controller.PostReplySearch)
//客服路由分组
kefuGroup := engine.Group("/kefu")
kefuGroup.Use(middleware.JwtApiMiddleware)
{
kefuGroup.GET("/chartStatistics", controller.GetChartStatistic)
kefuGroup.POST("/message", controller.SendKefuMessage)
}
//微信接口
engine.GET("/micro_program", middleware.JwtApiMiddleware, controller.GetCheckWeixinSign)
}

32
router/view.go Normal file
View File

@ -0,0 +1,32 @@
package router
import (
"github.com/gin-gonic/gin"
"goflylivechat/middleware"
"goflylivechat/tmpl"
)
func InitViewRouter(engine *gin.Engine) {
engine.GET("/", tmpl.PageIndex)
engine.GET("/login", tmpl.PageLogin)
engine.GET("/pannel", tmpl.PagePannel)
engine.GET("/chatIndex", tmpl.PageChat)
engine.GET("/livechat", tmpl.PageChat)
engine.GET("/main", middleware.JwtPageMiddleware, tmpl.PageMain)
engine.GET("/chat_main", middleware.JwtPageMiddleware, middleware.DomainLimitMiddleware, tmpl.PageChatMain)
engine.GET("/setting", middleware.DomainLimitMiddleware, tmpl.PageSetting)
engine.GET("/setting_statistics", tmpl.PageSettingStatis)
engine.GET("/setting_indexpage", tmpl.PageSettingIndexPage)
engine.GET("/setting_indexpages", tmpl.PageSettingIndexPages)
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_avator", tmpl.PageAvator)
engine.GET("/setting_modifypass", tmpl.PageModifypass)
engine.GET("/setting_ipblack", tmpl.PageIpblack)
engine.GET("/setting_config", tmpl.PageConfig)
engine.GET("/mail_list", tmpl.PageMailList)
engine.GET("/roles_list", tmpl.PageRoleList)
}

2
start.bat Normal file
View File

@ -0,0 +1,2 @@
go-fly.exe server
pause

BIN
static/.DS_Store vendored Normal file

Binary file not shown.

BIN
static/cdn/.DS_Store vendored Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
static/cdn/jquery/3.6.0/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

28
static/cdn/jquery/jquery.qrcode.min.js vendored Normal file
View File

@ -0,0 +1,28 @@
(function(r){r.fn.qrcode=function(h){var s;function u(a){this.mode=s;this.data=a}function o(a,c){this.typeNumber=a;this.errorCorrectLevel=c;this.modules=null;this.moduleCount=0;this.dataCache=null;this.dataList=[]}function q(a,c){if(void 0==a.length)throw Error(a.length+"/"+c);for(var d=0;d<a.length&&0==a[d];)d++;this.num=Array(a.length-d+c);for(var b=0;b<a.length-d;b++)this.num[b]=a[b+d]}function p(a,c){this.totalCount=a;this.dataCount=c}function t(){this.buffer=[];this.length=0}u.prototype={getLength:function(){return this.data.length},
write:function(a){for(var c=0;c<this.data.length;c++)a.put(this.data.charCodeAt(c),8)}};o.prototype={addData:function(a){this.dataList.push(new u(a));this.dataCache=null},isDark:function(a,c){if(0>a||this.moduleCount<=a||0>c||this.moduleCount<=c)throw Error(a+","+c);return this.modules[a][c]},getModuleCount:function(){return this.moduleCount},make:function(){if(1>this.typeNumber){for(var a=1,a=1;40>a;a++){for(var c=p.getRSBlocks(a,this.errorCorrectLevel),d=new t,b=0,e=0;e<c.length;e++)b+=c[e].dataCount;
for(e=0;e<this.dataList.length;e++)c=this.dataList[e],d.put(c.mode,4),d.put(c.getLength(),j.getLengthInBits(c.mode,a)),c.write(d);if(d.getLengthInBits()<=8*b)break}this.typeNumber=a}this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17;this.modules=Array(this.moduleCount);for(var d=0;d<this.moduleCount;d++){this.modules[d]=Array(this.moduleCount);for(var b=0;b<this.moduleCount;b++)this.modules[d][b]=null}this.setupPositionProbePattern(0,0);this.setupPositionProbePattern(this.moduleCount-
7,0);this.setupPositionProbePattern(0,this.moduleCount-7);this.setupPositionAdjustPattern();this.setupTimingPattern();this.setupTypeInfo(a,c);7<=this.typeNumber&&this.setupTypeNumber(a);null==this.dataCache&&(this.dataCache=o.createData(this.typeNumber,this.errorCorrectLevel,this.dataList));this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,c){for(var d=-1;7>=d;d++)if(!(-1>=a+d||this.moduleCount<=a+d))for(var b=-1;7>=b;b++)-1>=c+b||this.moduleCount<=c+b||(this.modules[a+d][c+b]=
0<=d&&6>=d&&(0==b||6==b)||0<=b&&6>=b&&(0==d||6==d)||2<=d&&4>=d&&2<=b&&4>=b?!0:!1)},getBestMaskPattern:function(){for(var a=0,c=0,d=0;8>d;d++){this.makeImpl(!0,d);var b=j.getLostPoint(this);if(0==d||a>b)a=b,c=d}return c},createMovieClip:function(a,c,d){a=a.createEmptyMovieClip(c,d);this.make();for(c=0;c<this.modules.length;c++)for(var d=1*c,b=0;b<this.modules[c].length;b++){var e=1*b;this.modules[c][b]&&(a.beginFill(0,100),a.moveTo(e,d),a.lineTo(e+1,d),a.lineTo(e+1,d+1),a.lineTo(e,d+1),a.endFill())}return a},
setupTimingPattern:function(){for(var a=8;a<this.moduleCount-8;a++)null==this.modules[a][6]&&(this.modules[a][6]=0==a%2);for(a=8;a<this.moduleCount-8;a++)null==this.modules[6][a]&&(this.modules[6][a]=0==a%2)},setupPositionAdjustPattern:function(){for(var a=j.getPatternPosition(this.typeNumber),c=0;c<a.length;c++)for(var d=0;d<a.length;d++){var b=a[c],e=a[d];if(null==this.modules[b][e])for(var f=-2;2>=f;f++)for(var i=-2;2>=i;i++)this.modules[b+f][e+i]=-2==f||2==f||-2==i||2==i||0==f&&0==i?!0:!1}},setupTypeNumber:function(a){for(var c=
j.getBCHTypeNumber(this.typeNumber),d=0;18>d;d++){var b=!a&&1==(c>>d&1);this.modules[Math.floor(d/3)][d%3+this.moduleCount-8-3]=b}for(d=0;18>d;d++)b=!a&&1==(c>>d&1),this.modules[d%3+this.moduleCount-8-3][Math.floor(d/3)]=b},setupTypeInfo:function(a,c){for(var d=j.getBCHTypeInfo(this.errorCorrectLevel<<3|c),b=0;15>b;b++){var e=!a&&1==(d>>b&1);6>b?this.modules[b][8]=e:8>b?this.modules[b+1][8]=e:this.modules[this.moduleCount-15+b][8]=e}for(b=0;15>b;b++)e=!a&&1==(d>>b&1),8>b?this.modules[8][this.moduleCount-
b-1]=e:9>b?this.modules[8][15-b-1+1]=e:this.modules[8][15-b-1]=e;this.modules[this.moduleCount-8][8]=!a},mapData:function(a,c){for(var d=-1,b=this.moduleCount-1,e=7,f=0,i=this.moduleCount-1;0<i;i-=2)for(6==i&&i--;;){for(var g=0;2>g;g++)if(null==this.modules[b][i-g]){var n=!1;f<a.length&&(n=1==(a[f]>>>e&1));j.getMask(c,b,i-g)&&(n=!n);this.modules[b][i-g]=n;e--; -1==e&&(f++,e=7)}b+=d;if(0>b||this.moduleCount<=b){b-=d;d=-d;break}}}};o.PAD0=236;o.PAD1=17;o.createData=function(a,c,d){for(var c=p.getRSBlocks(a,
c),b=new t,e=0;e<d.length;e++){var f=d[e];b.put(f.mode,4);b.put(f.getLength(),j.getLengthInBits(f.mode,a));f.write(b)}for(e=a=0;e<c.length;e++)a+=c[e].dataCount;if(b.getLengthInBits()>8*a)throw Error("code length overflow. ("+b.getLengthInBits()+">"+8*a+")");for(b.getLengthInBits()+4<=8*a&&b.put(0,4);0!=b.getLengthInBits()%8;)b.putBit(!1);for(;!(b.getLengthInBits()>=8*a);){b.put(o.PAD0,8);if(b.getLengthInBits()>=8*a)break;b.put(o.PAD1,8)}return o.createBytes(b,c)};o.createBytes=function(a,c){for(var d=
0,b=0,e=0,f=Array(c.length),i=Array(c.length),g=0;g<c.length;g++){var n=c[g].dataCount,h=c[g].totalCount-n,b=Math.max(b,n),e=Math.max(e,h);f[g]=Array(n);for(var k=0;k<f[g].length;k++)f[g][k]=255&a.buffer[k+d];d+=n;k=j.getErrorCorrectPolynomial(h);n=(new q(f[g],k.getLength()-1)).mod(k);i[g]=Array(k.getLength()-1);for(k=0;k<i[g].length;k++)h=k+n.getLength()-i[g].length,i[g][k]=0<=h?n.get(h):0}for(k=g=0;k<c.length;k++)g+=c[k].totalCount;d=Array(g);for(k=n=0;k<b;k++)for(g=0;g<c.length;g++)k<f[g].length&&
(d[n++]=f[g][k]);for(k=0;k<e;k++)for(g=0;g<c.length;g++)k<i[g].length&&(d[n++]=i[g][k]);return d};s=4;for(var j={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,
78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:1335,G18:7973,G15_MASK:21522,getBCHTypeInfo:function(a){for(var c=a<<10;0<=j.getBCHDigit(c)-j.getBCHDigit(j.G15);)c^=j.G15<<j.getBCHDigit(c)-j.getBCHDigit(j.G15);return(a<<10|c)^j.G15_MASK},getBCHTypeNumber:function(a){for(var c=a<<12;0<=j.getBCHDigit(c)-
j.getBCHDigit(j.G18);)c^=j.G18<<j.getBCHDigit(c)-j.getBCHDigit(j.G18);return a<<12|c},getBCHDigit:function(a){for(var c=0;0!=a;)c++,a>>>=1;return c},getPatternPosition:function(a){return j.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,c,d){switch(a){case 0:return 0==(c+d)%2;case 1:return 0==c%2;case 2:return 0==d%3;case 3:return 0==(c+d)%3;case 4:return 0==(Math.floor(c/2)+Math.floor(d/3))%2;case 5:return 0==c*d%2+c*d%3;case 6:return 0==(c*d%2+c*d%3)%2;case 7:return 0==(c*d%3+(c+d)%2)%2;default:throw Error("bad maskPattern:"+
a);}},getErrorCorrectPolynomial:function(a){for(var c=new q([1],0),d=0;d<a;d++)c=c.multiply(new q([1,l.gexp(d)],0));return c},getLengthInBits:function(a,c){if(1<=c&&10>c)switch(a){case 1:return 10;case 2:return 9;case s:return 8;case 8:return 8;default:throw Error("mode:"+a);}else if(27>c)switch(a){case 1:return 12;case 2:return 11;case s:return 16;case 8:return 10;default:throw Error("mode:"+a);}else if(41>c)switch(a){case 1:return 14;case 2:return 13;case s:return 16;case 8:return 12;default:throw Error("mode:"+
a);}else throw Error("type:"+c);},getLostPoint:function(a){for(var c=a.getModuleCount(),d=0,b=0;b<c;b++)for(var e=0;e<c;e++){for(var f=0,i=a.isDark(b,e),g=-1;1>=g;g++)if(!(0>b+g||c<=b+g))for(var h=-1;1>=h;h++)0>e+h||c<=e+h||0==g&&0==h||i==a.isDark(b+g,e+h)&&f++;5<f&&(d+=3+f-5)}for(b=0;b<c-1;b++)for(e=0;e<c-1;e++)if(f=0,a.isDark(b,e)&&f++,a.isDark(b+1,e)&&f++,a.isDark(b,e+1)&&f++,a.isDark(b+1,e+1)&&f++,0==f||4==f)d+=3;for(b=0;b<c;b++)for(e=0;e<c-6;e++)a.isDark(b,e)&&!a.isDark(b,e+1)&&a.isDark(b,e+
2)&&a.isDark(b,e+3)&&a.isDark(b,e+4)&&!a.isDark(b,e+5)&&a.isDark(b,e+6)&&(d+=40);for(e=0;e<c;e++)for(b=0;b<c-6;b++)a.isDark(b,e)&&!a.isDark(b+1,e)&&a.isDark(b+2,e)&&a.isDark(b+3,e)&&a.isDark(b+4,e)&&!a.isDark(b+5,e)&&a.isDark(b+6,e)&&(d+=40);for(e=f=0;e<c;e++)for(b=0;b<c;b++)a.isDark(b,e)&&f++;a=Math.abs(100*f/c/c-50)/5;return d+10*a}},l={glog:function(a){if(1>a)throw Error("glog("+a+")");return l.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;256<=a;)a-=255;return l.EXP_TABLE[a]},EXP_TABLE:Array(256),
LOG_TABLE:Array(256)},m=0;8>m;m++)l.EXP_TABLE[m]=1<<m;for(m=8;256>m;m++)l.EXP_TABLE[m]=l.EXP_TABLE[m-4]^l.EXP_TABLE[m-5]^l.EXP_TABLE[m-6]^l.EXP_TABLE[m-8];for(m=0;255>m;m++)l.LOG_TABLE[l.EXP_TABLE[m]]=m;q.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var c=Array(this.getLength()+a.getLength()-1),d=0;d<this.getLength();d++)for(var b=0;b<a.getLength();b++)c[d+b]^=l.gexp(l.glog(this.get(d))+l.glog(a.get(b)));return new q(c,0)},mod:function(a){if(0>
this.getLength()-a.getLength())return this;for(var c=l.glog(this.get(0))-l.glog(a.get(0)),d=Array(this.getLength()),b=0;b<this.getLength();b++)d[b]=this.get(b);for(b=0;b<a.getLength();b++)d[b]^=l.gexp(l.glog(a.get(b))+c);return(new q(d,0)).mod(a)}};p.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],
[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,
116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,
43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,
3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,
55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,
45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]];p.getRSBlocks=function(a,c){var d=p.getRsBlockTable(a,c);if(void 0==d)throw Error("bad rs block @ typeNumber:"+a+"/errorCorrectLevel:"+c);for(var b=d.length/3,e=[],f=0;f<b;f++)for(var h=d[3*f+0],g=d[3*f+1],j=d[3*f+2],l=0;l<h;l++)e.push(new p(g,j));return e};p.getRsBlockTable=function(a,c){switch(c){case 1:return p.RS_BLOCK_TABLE[4*(a-1)+0];case 0:return p.RS_BLOCK_TABLE[4*(a-1)+1];case 3:return p.RS_BLOCK_TABLE[4*
(a-1)+2];case 2:return p.RS_BLOCK_TABLE[4*(a-1)+3]}};t.prototype={get:function(a){return 1==(this.buffer[Math.floor(a/8)]>>>7-a%8&1)},put:function(a,c){for(var d=0;d<c;d++)this.putBit(1==(a>>>c-d-1&1))},getLengthInBits:function(){return this.length},putBit:function(a){var c=Math.floor(this.length/8);this.buffer.length<=c&&this.buffer.push(0);a&&(this.buffer[c]|=128>>>this.length%8);this.length++}};"string"===typeof h&&(h={text:h});h=r.extend({},{render:"canvas",width:256,height:256,typeNumber:-1,
correctLevel:2,background:"#ffffff",foreground:"#000000"},h);return this.each(function(){var a;if("canvas"==h.render){a=new o(h.typeNumber,h.correctLevel);a.addData(h.text);a.make();var c=document.createElement("canvas");c.width=h.width;c.height=h.height;for(var d=c.getContext("2d"),b=h.width/a.getModuleCount(),e=h.height/a.getModuleCount(),f=0;f<a.getModuleCount();f++)for(var i=0;i<a.getModuleCount();i++){d.fillStyle=a.isDark(f,i)?h.foreground:h.background;var g=Math.ceil((i+1)*b)-Math.floor(i*b),
j=Math.ceil((f+1)*b)-Math.floor(f*b);d.fillRect(Math.round(i*b),Math.round(f*e),g,j)}}else{a=new o(h.typeNumber,h.correctLevel);a.addData(h.text);a.make();c=r("<table></table>").css("width",h.width+"px").css("height",h.height+"px").css("border","0px").css("border-collapse","collapse").css("background-color",h.background);d=h.width/a.getModuleCount();b=h.height/a.getModuleCount();for(e=0;e<a.getModuleCount();e++){f=r("<tr></tr>").css("height",b+"px").appendTo(c);for(i=0;i<a.getModuleCount();i++)r("<td></td>").css("width",
d+"px").css("background-color",a.isDark(e,i)?h.foreground:h.background).appendTo(f)}}a=c;jQuery(a).appendTo(this)})}})(jQuery);

6
static/cdn/vue/2.6.11/vue.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
static/css/.DS_Store vendored Normal file

Binary file not shown.

7
static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

586
static/css/common.css Normal file
View File

@ -0,0 +1,586 @@
*{padding:0;margin:0}
.floatRight{float: right;}
.clear{clear: both;}
.visitorBody{
background-color: #4c4c4c;
}
.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;
}
#app {
width: 100%;
height: 100%;
}
.chatBg .el-tabs__header{margin: 0;}
.visitorFaceBtn{
float: left;
margin-left: 5px;
}
.visitorFaceBox{
position: absolute;
bottom: 85px;
}
.visitorIconBtns{
margin-right: 8px;
transform: scale(0.8);
}
.visitorIconBtns:hover{
color:#484848;
}
.kefuFaceBox{
position: absolute;
bottom: 0px;
z-index: 999;
}
.faceBox{
width: 100%;
background: #fff;
z-index: 99999999;
padding: 2px;
}
.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-container{
height: 100%;
}
.el-aside {
height: 100%;
background: #fff;
}
.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;
}
/*客服聊天主板*/
.chatBg{background: #fff;border: solid 1px #e6e6e6;overflow: hidden;}
.chatLeft{height:100%;overflow:auto;}
.chatLeft .el-tabs__nav,.chatRight .el-tabs__nav {
margin-left: 20px;
}
.chatRight{
height: 100%;
background: #fff;
overflow-x: hidden;
}
.onlineUsers {
padding: 10px 4px;
height: 40px;
font-size: 14px;
border-bottom: solid 1px #f1f1f1;
border-left: 4px solid #fff;
display: flex;
cursor: pointer;
}
.onlineUsers a{
color: #333;
}
.onlineUsers:hover, .onlineUsers.cur {
background-color: rgb(238 247 255);
border-left: 4px solid #4299e2;
}
.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: 840px;
margin: 0 auto;
box-shadow: 2px 2px 6px rgba(0,0,0,.3);
border-top: none;
overflow: hidden;
}
.chatContext{
width: 100%;
text-align: left;
position: relative;
background: rgb(245,245,245);
}
.chatBox{
/*overflow-y: auto;*/
overflow-x: hidden;
background: rgb(245,245,245);
/*margin-bottom: 80px;*/
}
.chatVisitorPage{
height: calc(100% - 86px);
overflow-y: auto;
}
.chatVisitorPage .chatBox{
padding: 5px 4px;
}
.chatBox .el-col{margin:10px 0;}
.chatUser{
font-size: 12px;
white-space: nowrap;
color: #999;
text-align: left;
margin: -5px 0px 3px 0px;
}
.chatMainPage{
margin-top: 1px;
}
.chatContent {
background-color: rgb(255,255,255);
color: #000;
border: 1px solid rgb(237,237,237);
min-height: 20px;
padding: 8px 15px;
word-break: break-all;
position: relative;
border-radius: 5px;
display: inline-block;
line-height: 21px;
font-size: 14px;
}
.chatContent2 {
border-radius: 8px 8px 8px 0px;
padding: 10px 15px;
}
.chatBoxMe{
margin-top: 10px;
}
.chatBoxMe .chatContent2 {
border-radius: 8px 8px 0px 8px;
background-color: #cde0ff;
color: #000;
}
a{color: #07a9fe;text-decoration: none;}
.chatBoxMe .chatContent{float: right;}
.chatBoxMe .chatContent:after{border-left-color: rgba(152,225,101,1);border-right:none;left:auto;right: -5px;}
.chatBoxMe .chatContent:before{border-left-color: rgb(152,225,101);border-right:none;right: -5px;left: auto;}
.chatBoxMe .el-col-3{float: right;text-align: right;}
.chatBoxMe .chatUser{text-align: right}
.btnArea{width: 10%;float: right;}
.chatTitle{height: 30px;line-height: 30px;color: #1989fa}
.chatBoxSend{
position: relative;
padding-top: 5px;
background: #fff;
width: 100%;
height: 80px;
z-index: 99;
border-top: 1px solid #e4e4e4;
}
.chatBoxSendBtn{float: right;margin: 12px 4px 0 0;}
.footContact{text-align: center;
position: absolute;
bottom: 2px;
left: 0;
right: 0;
}
.footContact a{font-size: 12px;color: #999;text-decoration: none;}
.chatTime{text-align: center;color: #bbb;margin: 12px 0;font-size: 12px;}
.chatTime span{display: inline-block;padding: 2px 5px;background: rgb(218,218,218);color: #fff;}
.chatTimeHide{display: none;}
.visitorInfo .el-menu-item{
font-size: 12px;
}
.chatRightTitle{
color: #303133;
height: 40px;
line-height: 40px;
padding:0 10px;
font-size: 14px;
border-bottom:1px solid #E4E7ED;
}
.chatRightTitle a{
float: right;
text-decoration: none;
color: #519eff;
}
.replyBox{
font-size: 12px;
min-height: 300px;
background: #fff;
}
.replyItem:hover{
background-color: #f0f9eb;
color: #67C23A;
}
.replyContent{
padding: 0 10px;
}
.replySearch{
margin: 5px 7px;
width: 96% !important;
}
.iconBtnsBox{
display: flex;
align-items: center;
color: #7e7e7e;
height: 35px;
}
.iconBtnsBox .iconBtn{
margin-right: 10px;
cursor: pointer;
}
.kefuMainBg{background: #f5f5f5;border-right: solid 1px #e6e6e6;boder-top:none;}
.kefuFuncBtns{background:#fff;margin: 2px 0px;color: #7f7f7f;border-bottom: 1px solid #e6e6e6;font-size: 12px;padding: 5px 0px;}
.kefuFuncBox{
position: absolute;
height: 135px;
bottom: 0px;
width: 100%;
background: #fff;
border-top: 1px solid #e5e5e5;
}
.kefuFuncBox .faceBox{
position: absolute;
bottom:100px;
}
.kefuFolderBtn{vertical-align: middle;}
.visitorReply{
font-size: 14px;
line-height: 24px;
}
.visitorReplyTitle{
}
.visitorReplyContent{
color:#007aff;
cursor: pointer;
}
.kefuSendBtn{
position: absolute;
right: 10px;
bottom: 10px;
z-index: 2;
}
.clear{clear:both;}
.chatEntTitle {
display: none;
width: 100%;
z-index: 9;
margin: 0 auto;
height: 56px;
overflow: hidden;
font-size: 16px;
color: #fff;
overflow: hidden;
background-color: rgb(11 113 236);
background-image: url(../images/visitor_title_bg.png);
background-size: auto 72px;
background-position: center 0;
align-items: center;
}
.chatEntTitle span {
margin-right: 10px;
}
.chatEntTitle .el-badge__content.is-fixed.is-dot {
right: 17px;
bottom: -5px;
top: unset;
}
.chatEntBox{
height: 100%;
position: relative;
}
.chatArticle{
display: none;
}
.visitorIconBox{
display: flex;
align-items: center;
color:#9c9c9c;
}
.visitorIconBox .iconBtn{
margin-right: 10px;
cursor: pointer;
}
.productCard{
cursor: pointer;
padding: 5px;
border-radius: 2px;
width: auto;
max-width: 730px;
display: flex;
border-radius: 5px;
}
.productCard img{
width: 100px;
height: 100px;
margin-right: 15px;
}
.productCard .productCardPrice{
color: #ff7736;
font-weight: bold;
font-size: 16px;
margin-top: 10px;
}
.productCard .productCardTitle{
color: #333;
}
@media screen and (min-width: 900px) {
.chatCenter {
max-height: 650px;
box-shadow: 0 2px 8px rgba(0,0,0,.15);
border-radius: 8px;
}
.chatVisitorPage {
height: calc(100% - 156px);
}
.chatEntTitle{display: flex;}
.chatEntTitleLogo {
margin-left: 15px;
}
.visitorBody {
display: flex;
-ms-flex-pack: center;
justify-content: center;
-ms-flex-align: center;
align-items: center;
}
.chatEntBox {
width: calc(100% - 265px);
float: left;
border-right: 1px solid #e6e6e6;
}
.chatArticle {
display: block;
width: 260px;
float: right;
overflow-y: auto;
}
.hotQuestionTitle {
padding: 10px 10px 10px 10px;
border-bottom: 1px solid rgba(0,0,0,.09);
font-size: 16px;
display: flex;
}
.hotQuestionTitle .fire {
width: 20px;
margin-right: 10px;
color: rgb(250, 84, 28);
}
}
.visitorEditorArea{
}
.visitorEditorArea textarea {
padding: 7px 0 7px 8px;
font-size:16px;
line-height: 21px;
border: none;
}
.visitorEditorBtn{
position: absolute;
right: 10px;
bottom: 2px;
}
.mainLeftMenu {
width: 70px;
float: left;
height: 100%;
text-align: center;
background-color: #0a2f5a;
position: relative;
}
.menuLeftItem {
height: 55px;
text-align: center;
color: #fff;
cursor: pointer;
padding-top: 12px;
width: 100%;
}
.menuLeftItem i, .menuLeftItem .el-badge {
display: block;
margin-bottom: 2px;
font-size: 20px;
}
.menuLeftItem span {
font-size: 12px;
}
.menuLeftItemLogout {
position: absolute;
bottom: 0px;
}
.mainRight {
width: calc(100% - 70px);
height: 100%;
float: left;
}
.mainIframe {
width: 100%;
height: 100%;
}
.menuLeftItem:active, .menuLeftItem.active {
background: #2b5a96;
}
.chatNotice {
text-align: center;
margin: 12px 0px;
}
.chatNoticeContent {
display: inline-block;
word-break: break-all;
color: rgba(0,0,0,.45);
margin: 0 24px;
max-width: calc(100% - 48px);
background-color: #fff;
border-radius: 16px;
font-size: 12px;
padding: 4px 16px;
}
.chatRow {
display: flex;
}
.chatRowAvator {
margin-right: 10px;
flex-shrink: 0;
}
.chatBoxMe .chatRow {
float: right;
}
.chatBoxMe .chatRowAvator {
margin-left: 10px;
}
.allNotice{
font-size: 12px;
margin: 10px 0px;
line-height: 23px;
color: #666;
}
.chatArea .el-textarea__inner{
border: none;
}
.tongji{
display: flex;
}
.tongji .tongjiItem{
background: #fff;
margin: 10px;
border-radius: 5px;
width: 100%;
}
.tongji .tongjiHeader{
font-size: 14px;
font-weight: bold;
padding: 10px 15px;
border-bottom: 1px solid #e8eaec;
color:#17233d;
}
.tongji .tongjiBody{
font-size: 30px;
margin: 0px 10px;
padding: 10px 0px;
border-bottom: 1px solid #e8eaec;
color: #515a6e;
}
.tongji .tongjiFooter{
font-size: 14px;
margin: 10px 10px;
color: #515a6e;
}
.tongji .tongjiFooter span{
color: red;
padding: 0 3px;
}
/* 定义滚动条的宽度、高度和背景色 */
::-webkit-scrollbar {
width: 8px;
height: 10px;
background-color: #f5f5f5;
}
/* 定义滚动条滑块的样式 */
::-webkit-scrollbar-thumb {
background-color: #c5c5c5;
}
/* 定义滚动条滑块在 hover 状态下的样式 */
::-webkit-scrollbar-thumb:hover {
background-color: #999;
}

37
static/css/front.css Normal file
View File

@ -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;/*背景颜色*/
}

229
static/css/gofly-front.css Normal file
View File

@ -0,0 +1,229 @@
.launchButtonBox{
position: fixed!important;
bottom: 2px;
right: 20px;
left: auto;
z-index: 999999;
}
.launchButtonNotice{
width: 270px;
padding: 10px;
margin: 0 auto;
display: block;
clear: both;
border-radius: 4px;
background-color: #fff;
box-shadow: 0 3px 15px 0 rgba(0,0,0,.25)!important;
position: absolute;
bottom: 60px;
right: 0;
z-index: 999999;
color: #222;
line-height: 1.5;
font-size: 14px;
display: none;
}
.launchButtonNotice:after{
content: "";
display: block;
width: 0;
height: 0;
border: 6px solid transparent;
border-top-color: rgba(255,255,255,1);
z-index: 1;
position: absolute;
bottom: -12px;
right: 40px;
}
.launchButtonNotice a{
color: #07a9fe;!important;
text-decoration: none;
}
.launchIcon{
background: #ff305f;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
border-radius: 50%;
position: absolute;
top: -4px;
left: 0px;
color: #fff;
display: none;
}
.launchButton{
height: 42px;
width: auto;
z-index: 10000000000000!important;
border-radius: 2px;
border: 0!important;
background: rgb(18, 122, 202);
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 6px, rgba(0, 0, 0, 0.16) 0px 2px 32px;
box-sizing: border-box!important;
padding: 0 25px;
cursor: pointer!important;
outline: 0!important;
display: inline-block;
margin: 0!important;
-webkit-font-smoothing: antialiased!important;
-webkit-tap-highlight-color: transparent!important;
color: #ffffff;
position: relative;
}
.launchButton:hover {
box-shadow: 0 3px 20px 0 rgba(0,0,0,.5)!important;
}
.launchButton svg{
width: 28px;
height: 48px;
}
.launchButtonText {
color: #fff;
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;
line-height: 42px;
font-weight: 700!important;
overflow: hidden!important;
text-overflow: ellipsis!important;
vertical-align: top!important;
white-space: nowrap;
transition: .6s ease-in-out!important;
}
.launchButtonNotice .flyAvatar{
width: 30px;
height: 30px;
border-radius: 50%;
display: inline-block;
border:1px solid #cccccc;
float: left;
}
.flyAvatar{
width: 30px;
height: 30px;
border-radius: 50%;
display: inline-block;
border:2px solid #fff;
float: left;
margin-right: 5px;
}
.layui-layer-title .flyAvatar{
margin-top: 5px;
}
.launchButtonNotice .flyUsername{
font-weight: bold;
float: left;
margin-left: 4px;
}
.launchButtonNotice .flyUser{
height: 32px;
overflow: hidden;
line-height: 32px;
margin-bottom: 4px;
}
.launchButtonNotice .flyClose{
float: right;
display: inline-block;
text-align: center;
width: 30px;
height: 30px;
line-height: 30px;
cursor: pointer;
}
#layui-layer19911116{
display: none;
}
.launchPointer{
background: #3cc51f;
width: 10px;
height: 10px;
display: inline-block;
border-radius: 50%;
position: absolute;
top: 25px;
left: 42px;
border:2px solid #fff;
}
.launchPointer.offline{
background: #ce3c39;
}
.folderBtn {
display: inline-block;
background-color: transparent;
overflow: hidden;
font-size: 1px;
}
.folderBtn:before {
content: '';
float: left;
background-color: #9da0a0;
width: 15px;
height: 3px;
margin-left: 2px;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
box-shadow: 2px 2px 0 0 #9da0a0;
}
.folderBtn:after {
content: '';
float: left;
clear: left;
background-color: #d4d6d6;
width: 33px;
height: 22px;
border-radius: 1px;
}
@-webkit-keyframes bounce-up {
25% {-webkit-transform: translateY(4px);}
50%, 100% {-webkit-transform: translateY(0);}
75% {-webkit-transform: translateY(-4px);}
}
@keyframes bounce-up {
25% {transform: translateY(4px);}
50%, 100% {transform: translateY(0);}
75% {transform: translateY(-4px);}
}
.animateUpDown{
-webkit-animation: bounce-up 0.5s linear infinite;
animation: bounce-up 0.5s linear infinite;
}
@media screen and (max-width: 500px) {
.launchButtonBox{
width: 100%;
text-align: center;
right: 0px;
}
.launchButton{
width: 43px;
height: auto;
position: absolute;
right: 6px;
bottom: 140px;
padding: 10px 0px;
}
.launchButtonNotice{
width: 90%;
bottom: 270px;
right: 2%;
text-align: left;
}
.launchButtonText{
width: 40px;
font-size: 8px;
line-height: 21px;
white-space: normal;
word-wrap: break-word;
max-height: 145px;
}
.launchButtonText .flyAvatar{
display: block;
margin-right: 0px;
float: none;
}
.launchButtonNotice:after{
right: 4%;
}
}

View File

@ -0,0 +1,51 @@
@font-face {
font-family: "iconfont"; /* Project id 3864484 */
src: url('iconfont.woff2?t=1678338039263') format('woff2'),
url('iconfont.woff?t=1678338039263') format('woff'),
url('iconfont.ttf?t=1678338039263') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-more:before {
content: "\e867";
}
.icon-zengjiatianjiajiahao:before {
content: "\e62a";
}
.icon-jianshaominimize3:before {
content: "\e68b";
}
.icon-folder-fill:before {
content: "\e7c4";
}
.icon-jietu:before {
content: "\e611";
}
.icon-duoyuyan:before {
content: "\e606";
}
.icon-jiahao:before {
content: "\eaf3";
}
.icon-xiaolian:before {
content: "\ec80";
}
.icon-fasong:before {
content: "\e604";
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,72 @@
{
"id": "3864484",
"name": "我的客服",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "18991683",
"name": "more",
"font_class": "more",
"unicode": "e867",
"unicode_decimal": 59495
},
{
"icon_id": "8817897",
"name": "增加添加加号",
"font_class": "zengjiatianjiajiahao",
"unicode": "e62a",
"unicode_decimal": 58922
},
{
"icon_id": "608294",
"name": "减少",
"font_class": "jianshaominimize3",
"unicode": "e68b",
"unicode_decimal": 59019
},
{
"icon_id": "6151130",
"name": "folder-fill",
"font_class": "folder-fill",
"unicode": "e7c4",
"unicode_decimal": 59332
},
{
"icon_id": "13397103",
"name": "截图",
"font_class": "jietu",
"unicode": "e611",
"unicode_decimal": 58897
},
{
"icon_id": "10598085",
"name": "language,多语言",
"font_class": "duoyuyan",
"unicode": "e606",
"unicode_decimal": 58886
},
{
"icon_id": "5387527",
"name": "加号",
"font_class": "jiahao",
"unicode": "eaf3",
"unicode_decimal": 60147
},
{
"icon_id": "6337465",
"name": "笑脸",
"font_class": "xiaolian",
"unicode": "ec80",
"unicode_decimal": 60544
},
{
"icon_id": "1418205",
"name": "发送",
"font_class": "fasong",
"unicode": "e604",
"unicode_decimal": 58884
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

1
static/css/icono.min.css vendored Normal file

File diff suppressed because one or more lines are too long

1
static/css/index.css Normal file
View File

@ -0,0 +1 @@
.footer{background: #30313a;color: #acacac;padding: 30px 0;min-width: 1190px;}

743
static/css/kefu-front.css Normal file
View File

@ -0,0 +1,743 @@
.launchButtonBox{
position: fixed!important;
bottom: 25px;
right: 30px;
left: auto;
z-index: 999999;
}
.launchButtonNotice{
width: 270px;
padding: 10px;
margin: 0 auto;
clear: both;
position: absolute;
bottom: 60px;
right: 0;
z-index: 999999;
color: #545454;
line-height: 1.5;
font-size: 14px;
display: none;
}
.launchNoticeContent{
border-radius: 8px;
background-color: #fff;
border: 1px solid #f7f7f9;
box-shadow: 0 2px 8px rgb(0 0 0 / 10%);
padding: 10px;
}
.flexBox{
display: flex;
align-items: flex-end;
margin-top: 10px;
}
.launchButtonNotice a{
color: #07a9fe;!important;
text-decoration: none;
}
.launchIcon{
background: #ff305f;
width: 18px;
height: 18px;
font-size: 12px;
line-height: 18px;
text-align: center;
border-radius: 50%;
position: absolute;
top: -2px;
left: 14px;
color: #fff;
display: none;
}
.launchButton{
border-radius: 30px;
z-index: 10000000000000!important;
background: #2d8cf0;
box-shadow: rgb(0 0 0 / 6%) 0px 1px 6px, rgb(0 0 0 / 16%) 0px 2px 32px;
box-sizing: border-box!important;
padding: 15px 20px;
cursor: pointer!important;
outline: 0!important;
display: flex;
justify-content: center;
align-items: center;
margin: 0!important;
-webkit-font-smoothing: antialiased!important;
-webkit-tap-highlight-color: transparent!important;
color: #ffffff;
position: relative;
}
.launchButton:hover {
box-shadow: 0 8px 32px rgb(0 0 0 / 40%) !important;
}
.launchButton svg{
width: 28px;
height: 48px;
}
.launchButtonText {
display: inline-block;
/*font-size: 14px;*/
letter-spacing:1px;
text-overflow: ellipsis!important;
vertical-align: top!important;
white-space: nowrap;
}
.launchButtonText img{
width: 26px;
display: inline-block;
}
.launchButtonNotice .flyAvatar{
width: 36px;
height: 36px;
border-radius: 50%;
border: 1px solid #fff;
}
.flyAvatar{
width: 30px;
height: 30px;
border-radius: 50%;
display: inline-block;
border:2px solid #fff;
margin-right: 5px;
}
.layui-layer-title .flyAvatar{
margin-top: 5px;
}
.launchButtonNotice .flyUsername{
font-weight: bold;
float: left;
margin-left: 4px;
}
.launchButtonNotice .flyUser{
width: 36px;
height: 36px;
overflow: hidden;
flex-shrink: 0;
margin-right: 4px;
box-shadow: 0 2px 8px rgb(0 0 0 / 10%);
border-radius: 50%;
}
.launchButtonNotice .flyClose {
text-align: center;
border-radius: 50%;
width: 30px;
height: 30px;
line-height: 30px;
color: #464646;
cursor: pointer;
position: absolute;
top: -25px;
right: 0px;
background-color: #fff;
border: 1px solid #f7f7f9;
box-shadow: 0 2px 8px rgb(0 0 0 / 10%);
}
#layui-layer19911116{
/*display: none;*/
box-shadow: 0 5px 40px rgb(0 0 0 / 16%);
border-radius: 12px;
border: none;
animation: up 0.5s ease-in-out both
}
#layui-layer-iframe19911116{
border-radius: 0 0 12px 12px;
}
.launchPointer{
background: #3cc51f;
width: 10px;
height: 10px;
display: inline-block;
border-radius: 50%;
position: absolute;
top: 25px;
left: 42px;
border:2px solid #fff;
}
.launchPointer.offline{
background: #ce3c39;
}
.folderBtn {
display: inline-block;
background-color: transparent;
overflow: hidden;
font-size: 1px;
}
.folderBtn:before {
content: '';
float: left;
background-color: #9da0a0;
width: 15px;
height: 3px;
margin-left: 2px;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
box-shadow: 2px 2px 0 0 #9da0a0;
}
.folderBtn:after {
content: '';
float: left;
clear: left;
background-color: #d4d6d6;
width: 33px;
height: 22px;
border-radius: 1px;
}
@-webkit-keyframes bounce-up {
25% {-webkit-transform: translateY(4px);}
50%, 100% {-webkit-transform: translateY(0);}
75% {-webkit-transform: translateY(-4px);}
}
@keyframes bounce-up {
25% {transform: translateY(4px);}
50%, 100% {transform: translateY(0);}
75% {transform: translateY(-4px);}
}
.animateUpDown{
-webkit-animation: bounce-up 0.5s linear infinite;
animation: bounce-up 0.5s linear infinite;
}
@keyframes up {
0% {
-webkit-transform: translateY(20px);
transform: translateY(20px);
opacity: 0
}
to {
-webkit-transform: translateY(-20px);
transform: translateY(-20px);
opacity: 1
}
}
@media screen and (max-width: 500px) {
.launchButtonBox{
width: 100%;
right: 0px;
}
.launchButton{
width: 43px;
height: auto;
position: absolute;
right: 6px;
bottom: 140px;
padding: 10px 0px;
}
.launchButtonNotice{
bottom: 120px;
right: 42px;
}
.launchButtonText{
white-space: normal;
word-wrap: break-word;
text-align: center;
writing-mode: vertical-lr;
text-orientation: mixed;
text-align: center;
justify-content: center;
display: flex;
align-items: center;
letter-spacing: 2px;
}
.launchButtonText img{margin-bottom: 5px;}
.launchButtonText .flyAvatar{
display: block;
margin-right: 0px;
float: none;
}
.launchButtonNotice:after{
right: 4%;
}
}
/*看板娘*/
.waifu-toggle {
background-color: #fa0;
border-radius: 5px;
bottom: 66px;
color: #fff;
cursor: pointer;
font-size: 12px;
left: 0;
margin-left: -100px;
padding: 5px 2px 5px 5px;
position: fixed;
transition: margin-left 1s;
width: 60px;
writing-mode: vertical-rl;
}
.waifu-toggle.waifu-toggle-active {
margin-left: -50px;
}
.waifu-toggle.waifu-toggle-active:hover {
margin-left: -30px;
}
.waifu {
bottom: -1000px;
left: 0;
line-height: 0;
margin-bottom: -10px;
position: fixed;
transform: translateY(3px);
transition: transform .3s ease-in-out, bottom 3s ease-in-out;
z-index: 1;
}
.waifu:hover {
transform: translateY(0);
}
.waifu-tips {
top:-50px;
animation: shake 50s ease-in-out 5s infinite;
background-color: rgba(236, 217, 188, .5);
border: 1px solid rgba(224, 186, 140, .62);
border-radius: 12px;
box-shadow: 0 3px 15px 2px rgba(191, 158, 118, .2);
font-size: 14px;
line-height: 24px;
margin: 0px 20px;
min-height: 70px;
opacity: 0;
overflow: hidden;
padding: 5px 10px;
position: absolute;
text-overflow: ellipsis;
transition: opacity 1s;
width: 250px;
word-break: break-all;
}
.waifu-tips.waifu-tips-active {
opacity: 1;
transition: opacity .2s;
background-color: rgba(236, 217, 188, 0.8);
}
.waifu-tips span {
color: #0099cc;
}
.waifu #live2d {
cursor: grab;
height: 300px;
position: relative;
width: 300px;
}
.waifu #live2d:active {
cursor: grabbing;
}
.waifu-tool {
color: #aaa;
opacity: 0;
position: absolute;
right: -10px;
top: 70px;
transition: opacity 1s;
}
.waifu:hover #waifu-tool {
opacity: 1;
}
.waifu-tool span {
color: #7b8c9d;
cursor: pointer;
display: block;
line-height: 30px;
text-align: center;
transition: color .3s;
}
.waifu-tool span:hover {
color: #0684bd; /* #34495e */
}
.waifu-input{
background-color: rgba(236, 217, 188, .9);
border: 1px solid rgba(224, 186, 140, .8);
border-radius: 12px;
box-shadow:0 3px 15px 2px rgba(191, 158, 118, .4);
left: 20px;
bottom: 30px;
width: 250px;
position: absolute;
padding: 6px;
z-index: 2;
}
@keyframes shake {
2% {
transform: translate(.5px, -1.5px) rotate(-.5deg);
}
4% {
transform: translate(.5px, 1.5px) rotate(1.5deg);
}
6% {
transform: translate(1.5px, 1.5px) rotate(1.5deg);
}
8% {
transform: translate(2.5px, 1.5px) rotate(.5deg);
}
10% {
transform: translate(.5px, 2.5px) rotate(.5deg);
}
12% {
transform: translate(1.5px, 1.5px) rotate(.5deg);
}
14% {
transform: translate(.5px, .5px) rotate(.5deg);
}
16% {
transform: translate(-1.5px, -.5px) rotate(1.5deg);
}
18% {
transform: translate(.5px, .5px) rotate(1.5deg);
}
20% {
transform: translate(2.5px, 2.5px) rotate(1.5deg);
}
22% {
transform: translate(.5px, -1.5px) rotate(1.5deg);
}
24% {
transform: translate(-1.5px, 1.5px) rotate(-.5deg);
}
26% {
transform: translate(1.5px, .5px) rotate(1.5deg);
}
28% {
transform: translate(-.5px, -.5px) rotate(-.5deg);
}
30% {
transform: translate(1.5px, -.5px) rotate(-.5deg);
}
32% {
transform: translate(2.5px, -1.5px) rotate(1.5deg);
}
34% {
transform: translate(2.5px, 2.5px) rotate(-.5deg);
}
36% {
transform: translate(.5px, -1.5px) rotate(.5deg);
}
38% {
transform: translate(2.5px, -.5px) rotate(-.5deg);
}
40% {
transform: translate(-.5px, 2.5px) rotate(.5deg);
}
42% {
transform: translate(-1.5px, 2.5px) rotate(.5deg);
}
44% {
transform: translate(-1.5px, 1.5px) rotate(.5deg);
}
46% {
transform: translate(1.5px, -.5px) rotate(-.5deg);
}
48% {
transform: translate(2.5px, -.5px) rotate(.5deg);
}
50% {
transform: translate(-1.5px, 1.5px) rotate(.5deg);
}
52% {
transform: translate(-.5px, 1.5px) rotate(.5deg);
}
54% {
transform: translate(-1.5px, 1.5px) rotate(.5deg);
}
56% {
transform: translate(.5px, 2.5px) rotate(1.5deg);
}
58% {
transform: translate(2.5px, 2.5px) rotate(.5deg);
}
60% {
transform: translate(2.5px, -1.5px) rotate(1.5deg);
}
62% {
transform: translate(-1.5px, .5px) rotate(1.5deg);
}
64% {
transform: translate(-1.5px, 1.5px) rotate(1.5deg);
}
66% {
transform: translate(.5px, 2.5px) rotate(1.5deg);
}
68% {
transform: translate(2.5px, -1.5px) rotate(1.5deg);
}
70% {
transform: translate(2.5px, 2.5px) rotate(.5deg);
}
72% {
transform: translate(-.5px, -1.5px) rotate(1.5deg);
}
74% {
transform: translate(-1.5px, 2.5px) rotate(1.5deg);
}
76% {
transform: translate(-1.5px, 2.5px) rotate(1.5deg);
}
78% {
transform: translate(-1.5px, 2.5px) rotate(.5deg);
}
80% {
transform: translate(-1.5px, .5px) rotate(-.5deg);
}
82% {
transform: translate(-1.5px, .5px) rotate(-.5deg);
}
84% {
transform: translate(-.5px, .5px) rotate(1.5deg);
}
86% {
transform: translate(2.5px, 1.5px) rotate(.5deg);
}
88% {
transform: translate(-1.5px, .5px) rotate(1.5deg);
}
90% {
transform: translate(-1.5px, -.5px) rotate(-.5deg);
}
92% {
transform: translate(-1.5px, -1.5px) rotate(1.5deg);
}
94% {
transform: translate(.5px, .5px) rotate(-.5deg);
}
96% {
transform: translate(2.5px, -.5px) rotate(-.5deg);
}
98% {
transform: translate(-1.5px, -1.5px) rotate(-.5deg);
}
0%, 100% {
transform: translate(0, 0) rotate(0);
}
}
.flySimpleIconBox{
position: fixed;
bottom: 150px;
right: 10px;
z-index: 999999;
}
.flySimpleIcon{
width: 60px;
height: 60px;
text-align: center;
box-shadow: rgba(0, 0, 0, 0.16) 0px 5px 14px;
border-radius: 50%;
cursor: pointer;
}
.flySimpleIcon .flySimpleDefaultImg{
display: inline-block;
margin-top: 12px;
width: 35px;
}
.flySimpleIcon .flySimpleUserImg{
width: 60px;
height: 60px;
border-radius: 50%;
}
.flySimpleIconTip{
display: none;
width: 250px;
max-height: 200px;
overflow-y: auto;
background: #fff;
position: absolute;
top:5px;
right: 70px;
border-radius: 4px;
box-shadow: 0 3px 15px 0 rgba(0,0,0,.25)!important;
padding: 14px;
font-size: 14px;
}
.flySimpleIconBox .flyClose{
text-align: center;
width: 30px;
height: 30px;
line-height: 30px;
cursor: pointer;
position: absolute;
right: 0px;
top: 2px;
}
.chatImagePic{
max-width: 100%;
}
.lineBox{
position: fixed!important;
bottom: 30%;
right: 0px;
left: auto;
z-index: 999999;
}
.lineBox .lineItem{
cursor: pointer;
width: 50px;
height: 55px;
background: #2d8cf0;
margin-bottom: 1px;
color: #fff;
line-height: 55px;
text-align: center;
position: relative;
}
.lineItem .layui-icon{
font-size: 26px;
}
.lineItem:hover{
opacity: 0.8;
}
.lineTop{
margin-top: 4px;
}
.lineTip{
border-radius: 2px;
box-shadow: 1px 1px 3px rgba(0,0,0,.2);
position: absolute;
top:0px;
right: 59px;
color: #000;
padding: 0 10px;
background: #fff;
display: none;
}
.lineTip:before, .lineTip:after {
content: "";
display: block;
position: absolute;
width: 0;
height: 0;
border: 10px solid transparent;
border-left-color: rgba(255,255,255,1);
right: -16px;
top: 10px;
z-index: 1;
}
.lineTip:after{
right: -18px;
border-left-color: rgb(237,237,237);
z-index: 0;
}
.lineWechat{
width: 100px;
height: 100px;
padding: 0px;
}
.kfLayer .layui-layer-title{
background-color: #3369FF;
background-image: linear-gradient(to right, #0d6efd, #2aadeb);
height: 60px;
border: 0px;
color: #fff;
padding: 0px;
line-height: normal;
border-radius: 12px 12px 0 0;
}
.kfLayer .kfBar{
display: flex;
align-items: center;
height: 60px;
}
.kfLayer .kfBarLogo{
width: 40px;
height: 40px;
border-radius: 50%;
}
.kfLayer .kfBarText{
float: left;
margin:0px 0px 0px 10px;
max-width: 220px;
}
.kfLayer .kfBarBtn{
margin-right: 40px;
margin-left: auto;
margin-bottom: 7px;
}
.kfLayer .kfBarBtn .cursor{
cursor: pointer;
}
.kfLayer .kfDesc{
font-size: 12px;
margin-top: 5px;
}
.kfLayer .layui-layer-setwin .layui-layer-close1{
background: url("../images/zoom_out.png") no-repeat 0;
width: 22px;
}
.kfBarAvator{
width: 40px;
height: 40px;
position: relative;
margin: 0 0px 0 15px;
}
.kfBarStatus{
width: 8px;
height: 8px;
border-radius: 50%;
--bg-opacity: 1;
background-color: #00c41d;
background-color: rgba(0,196,29,var(--bg-opacity));
border:1px solid #fff;
display: inline-block;
}
.kfBarAvator i.offline{
background: #f56c6c;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
html #layuicss-skincodecss{display:none;position:absolute;width:1989px}.layui-code-h3,.layui-code-view{position:relative;font-size:12px}.layui-code-view{display:block;margin:10px 0;padding:0;border:1px solid #eee;border-left-width:6px;background-color:#FAFAFA;color:#333;font-family:Courier New}.layui-code-h3{padding:0 10px;height:40px;line-height:40px;border-bottom:1px solid #eee}.layui-code-h3 a{position:absolute;right:10px;top:0;color:#999}.layui-code-view .layui-code-ol{position:relative;overflow:auto}.layui-code-view .layui-code-ol li{position:relative;margin-left:45px;line-height:20px;padding:0 10px;border-left:1px solid #e2e2e2;list-style-type:decimal-leading-zero;*list-style-type:decimal;background-color:#fff}.layui-code-view .layui-code-ol li:first-child{padding-top:10px}.layui-code-view .layui-code-ol li:last-child{padding-bottom:10px}.layui-code-view pre{margin:0}.layui-code-notepad{border:1px solid #0C0C0C;border-left-color:#3F3F3F;background-color:#0C0C0C;color:#C2BE9E}.layui-code-notepad .layui-code-h3{border-bottom:none}.layui-code-notepad .layui-code-ol li{background-color:#3F3F3F;border-left:none}.layui-code-demo .layui-code{visibility:visible!important;margin:-15px;border-top:none;border-right:none;border-bottom:none}.layui-code-demo .layui-tab-content{padding:15px;border-top:none}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

188
static/css/style.css Normal file
View File

@ -0,0 +1,188 @@
*{
margin: 0;padding: 0;
}
a {
text-decoration: none;
color: #3973ac;
}
a:hover {
text-decoration: underline;
}
pre{
background:#f9f9f9;
color: #d73a49;
line-height: 21px;
border: 1px solid #e8e8e8;
padding: 5px;
}
.header{
background-color: #fff;
color: #fff;
width: 100%;
z-index: 100;
}
.container{
width: 1140px;
padding: 0;
margin: 0 auto;
}
.header .logo{
margin: 0;
float: left;
font-size: 32px;
font-weight: bold;
margin-top: 10px;
}
.header a{
color: #828282;
font-weight: bold;
font-family: "Microsoft JhengHei";
text-decoration: none;
}
.header .logo a{
font-size: 30px;
text-decoration: none;
}
.header .logo img{
width: 120px;
}
.header .navBtn{
float: right;
margin:30px 0 30px 20px;
display: inline-block;
}
.banner{
text-align: center;
color: #fff;
background-color: #3385ff;
padding: 20px 10px;
font-size: 18px;
line-height: 28px;
}
.banner h1 {
font-size: 34px;
margin: 20px 0px;
line-height: 45px;
font-weight: 500;
font-family: Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,SimSun,sans-serif;
}
.banner p{
max-width: 1200px;
margin: 0 auto;
}
.banner .downloadBtn{margin-top: 20px;}
.downloadBtn{display:inline-block;text-decoration:none;color:#fff;border:1px solid #fff;padding:10px 15px;border-radius:5px;}
.downloadBtn:hover{background:#fff;color:#20b2bb;text-decoration: none;}
.jumbotron{
width: 1000px;
margin: 10px auto;
box-shadow: 2px 2px 15px rgba(0,0,0,.3);
display: block;
}
.copyright{
font-size: 14px;
color: #333;
text-align: center;
padding: 10px;
}
.links{
margin-top: 20px;
}
.links a{
color: #20b2bb;
line-height: 23px;
margin: 5px;
}
.main{
background: #ededed;
overflow: hidden;
}
.mainIntro{
background: #fff;
box-shadow: 0 1px 5px 0 rgba(0,0,0,.05);
margin: 20px auto;
}
.product{
padding: 10px;
}
.product h3{
color: #333;
font-size: 16px;
line-height: 36px;
border-bottom: 1px solid #e6e6e6;
}
.product h4{
padding: 10px;
font-size: 14px;
font-weight: normal;
border-left: 2px #3385ff solid;
background: #e6e6e6;
margin: 10px 0px;
}
.productContent{
color: #444;
font-size: 14px;
line-height: 30px;
margin-top: 10px;
}
.productContent ol{
margin: 0px 15px;
}
.productContent a{
color: #3973ac;
text-decoration: none;
}
.productContent a:hover{
text-decoration: underline;
}
.productPost{
overflow: hidden;
}
.productPost li:first-child {
border-top: 0;
}
.productPost li {
float: left;
margin-right: 2%;
width: 48%;
line-height: 36px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
border-top: 1px dashed #e6e6e6;
}
@media screen and (max-width: 800px) {
.container{
width: 100%;
}
.header .logo{
float: none;
text-align: center;
margin: 20px 0px;
}
.header .navBtn{
margin:5px 0 10px 10px;
float: none;
}
.banner p{
text-align: left;
}
.jumbotron{
width: 100%;
height: auto;
}
}
.clear{clear:both}
.links{
margin-top: 20px;
}
.links a{
color: #20b2bb;
line-height: 23px;
margin: 5px;
}

View File

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
<script>
// 设置重连时间间隔(单位:毫秒)
const RECONNECT_INTERVAL = 1000;
// 设置最大重连次数
const MAX_RECONNECT_TIMES = 3;
let reconnectTimes = 0;
let ws;
// 尝试连接 WebSocket
function connect() {
ws = new WebSocket('wss://gofly.v1kf.com/ws_visitor?visitor_id=5|a780d122-daa3-4315-a413-f93b29b026d0&to_id=taoshihan');
ws.onopen = function () {
console.log('WebSocket 连接已打开');
reconnectTimes = 0;
};
ws.onclose = function () {
console.log('WebSocket 连接已关闭');
// 尝试重连
reconnect();
};
ws.onmessage = function (event) {
console.log(`收到服务器的消息:${event.data}`);
// // 解析消息
// const message = JSON.parse(event.data);
// if (message.type === 'message') {
// console.log(`收到消息:${message.data}`);
// }
};
}
// 尝试重连
function reconnect() {
if (reconnectTimes >= MAX_RECONNECT_TIMES) {
console.log('重连失败');
return;
}
reconnectTimes++;
console.log(`正在尝试重连(第 ${reconnectTimes} 次)`);
setTimeout(function () {
connect();
}, RECONNECT_INTERVAL);
}
connect();
</script>
</html>

BIN
static/images/.DS_Store vendored Normal file

Binary file not shown.

BIN
static/images/0.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
static/images/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
static/images/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

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