dujunchen 6 mesiacov pred
commit
e0582ad3e1
79 zmenil súbory, kde vykonal 6018 pridanie a 0 odobranie
  1. 34 0
      Makefile
  2. 1 0
      README
  3. 95 0
      api/admin/adminModel/firewall.go
  4. 247 0
      api/admin/auth/account.go
  5. 23 0
      api/admin/router.go
  6. 152 0
      api/admin/vtiger/info.go
  7. 8 0
      api/admin/vtiger/model.go
  8. 10 0
      api/admin/vtiger/router.go
  9. 138 0
      api/admin/zoho/info.go
  10. 11 0
      api/admin/zoho/router.go
  11. 1 0
      api/index.go
  12. 4 0
      api/plugins/api/README.md
  13. 97 0
      api/plugins/api/index.go
  14. 12 0
      api/plugins/api/router.go
  15. 56 0
      api/response.go
  16. 96 0
      cmd/commands/service.go
  17. 114 0
      cmd/commands/web.go
  18. 70 0
      cmd/main.go
  19. 45 0
      configs/config.demo.yaml
  20. 28 0
      configs/config.yaml
  21. 1 0
      configs/push.json
  22. BIN
      deployments/pbx-api-gin
  23. BIN
      deployments/pms-api-arm
  24. BIN
      deployments/pms-api-arm_20241121bak
  25. 37 0
      go.mod
  26. 470 0
      go.sum
  27. 203 0
      internal/app/ami/action/call.go
  28. 117 0
      internal/app/ami/action/channel.go
  29. 77 0
      internal/app/ami/action/database.go
  30. 51 0
      internal/app/ami/action/extension.go
  31. 73 0
      internal/app/ami/action/sip.go
  32. 99 0
      internal/app/ami/bridge.go
  33. 379 0
      internal/app/ami/index.go
  34. 40 0
      internal/app/ami/model/dial.go
  35. 100 0
      internal/app/ami/model/event.go
  36. 80 0
      internal/app/ami/model/meetme.go
  37. 210 0
      internal/app/ami/model/park.go
  38. 90 0
      internal/app/ami/model/queue.go
  39. 119 0
      internal/app/ami/model/sip.go
  40. 15 0
      internal/app/ami/model/userevent.go
  41. 132 0
      internal/app/ami/vtiger.go
  42. 273 0
      internal/app/ami/zoho.go
  43. 12 0
      internal/app/http_server/api/index_disable.go
  44. 71 0
      internal/app/http_server/index.go
  45. 13 0
      internal/app/http_server/pbx/pbx.go
  46. 11 0
      internal/app/http_server/pbx/pbx_disable.go
  47. 19 0
      internal/app/http_server/socketio_client_tool/socketio_client_tool.go
  48. 15 0
      internal/app/http_server/socketio_client_tool/socketio_client_tool_disable.go
  49. 90 0
      internal/app/index.go
  50. 109 0
      internal/app/mysql/extension.go
  51. 53 0
      internal/app/mysql/index.go
  52. 1 0
      internal/app/mysql/voicefile.go
  53. 98 0
      internal/app/redis/extension.go
  54. 30 0
      internal/app/redis/index.go
  55. 44 0
      internal/app/socket_io/index.go
  56. 88 0
      internal/app/status/index.go
  57. 27 0
      pkg/commonService/check.go
  58. 15 0
      pkg/commonService/error.go
  59. 101 0
      pkg/configs/decode.go
  60. 31 0
      pkg/configs/interface.go
  61. 9 0
      pkg/configs/path.go
  62. 41 0
      pkg/configs/push.go
  63. 6 0
      pkg/configs/shell.go
  64. 204 0
      pkg/httpclient/index.go
  65. 64 0
      pkg/i18n/index.go
  66. 218 0
      pkg/lfshook/log.go
  67. 91 0
      pkg/utils/cmd.go
  68. 17 0
      pkg/utils/common.go
  69. 22 0
      pkg/utils/exit.go
  70. 42 0
      pkg/utils/file.go
  71. 18 0
      pkg/utils/time.go
  72. 149 0
      pkg/utils/utils.go
  73. 63 0
      pkg/voicemail/parse.go
  74. 23 0
      pkg/weblog/index.go
  75. 12 0
      web/embed.go
  76. 179 0
      web/resources/en_US.json
  77. BIN
      web/resources/no_logo.png
  78. 179 0
      web/resources/zh_CN.json
  79. 45 0
      web/www/index.html

+ 34 - 0
Makefile

@@ -0,0 +1,34 @@
+buildDateTime = $(shell date '+%Y-%m-%d %H:%M:%S')
+gitCommitCode = $(shell git rev-list --full-history --all --abbrev-commit --max-count 1)
+goVersion = $(shell go version)
+
+run3100: build
+	./deployments/pbx-api-gin --level 2 --reportCaller web -c ./configs/_config.3100.yaml
+
+run: build
+	#./deployments/pbx-api-gin --level 2  --reportCaller  web -c ./configs/config.yaml
+	./deployments/pbx-api-gin --level 5  --reportCaller  web -c ./configs/config.yaml
+# build: swagger
+# 	go build -tags "webuser pbx pprof socketio_client_tool swagger api" -o ./deployments/pbx-api-gin ./cmd/main.go
+# 	go build -tags "pbx" -o ./deployments/pbx-api-gin ./cmd/main.go
+build: 
+	go build -tags "pbx" -o ./deployments/pbx-api-gin ./cmd/main.go
+swagger:
+# 	swag init -d ./api/plugins/webuser,./api/commonModel,./api/admin/adminModel  -g swagger/swagger.go  -o ./web/swagger/webuser --instanceName webuser
+# 	swag init -d ./api/plugins/api  -g swagger/swagger.go  -o ./web/swagger/api --instanceName api
+# 	swag init -d ./api/admin,./api/commonModel  -g swagger/swagger.go  -o ./web/swagger/pbx --instanceName pbx
+# 	swag init -d ./api/plugins/webuser,./api/admin/adminModel  -g swagger/swagger.go  -o ./web/swagger/webuser --instanceName webuser
+# 	swag init -d ./api/plugins/api  -g swagger/swagger.go  -o ./web/swagger/api --instanceName api
+	swag init -d ./api/admin  -g swagger/swagger.go  -o ./web/swagger/pbx --instanceName pbx
+
+release:
+	#cd web/www && find ./ui_pbx -type f  -name '*.js' | xargs gzip -k
+	GOOS=linux GOARCH=amd64 go build -tags "webuser pbx pprof api" -ldflags "-X 'main.buildDateTime=$(buildDateTime)' -X 'main.gitCommitCode=$(gitCommitCode)' -X 'main.goVersion=${goVersion}' -s -w" -o ./deployments/pms-api-arm ./cmd/main.go
+release-arm:
+	#cd web/www && find ./ui_pbx -type f  -name '*.js' | xargs gzip -k
+	GOOS=linux GOARCH=arm go build -tags "webuser pbx pprof api" -ldflags "-X 'main.buildDateTime=$(buildDateTime)' -X 'main.gitCommitCode=$(gitCommitCode)' -X 'main.goVersion=${goVersion}' -s -w" -o ./deployments/pms-api-arm ./cmd/main.go
+
+upx:
+	upx -9 --lzma ./deployments/pbx-api-gin
+scp:
+	scp deployments/pbx-api-gin root@192.168.18.252:/usr/local/pbx-webserver/deployments

+ 1 - 0
README

@@ -0,0 +1 @@
+IPPBX 对接CRM

+ 95 - 0
api/admin/adminModel/firewall.go

@@ -0,0 +1,95 @@
+package adminModel
+
+import "gopkg.in/guregu/null.v4"
+
+type FirewallGlobalInfo struct {
+	ID     int64  `xorm:"id pk autoincr" json:"id"`
+	Name   string `xorm:"name"`
+	Enable string `xorm:"enable"`
+}
+
+func (*FirewallGlobalInfo) TableName() string {
+	return "t_firewall_global"
+}
+
+type FirewallAutoDefenceRuleVO struct {
+	Name        string `xorm:"name" json:"name"`
+	Port        int64  `xorm:"port" json:"port"`
+	Protocol    string `xorm:"protocol" json:"protocol"`
+	PacketCount int64  `xorm:"packet_count" json:"packetCount"`
+	Interval    int64  `xorm:"time_interval" json:"interval"`
+}
+
+type FirewallAutoDefenceRule struct {
+	ID                        int64 `xorm:"id pk autoincr" json:"id"`
+	FirewallAutoDefenceRuleVO `xorm:"extends"`
+}
+
+func (*FirewallAutoDefenceRule) TableName() string {
+	return "t_firewall_auto_defence"
+}
+
+type FirewallCommonRule struct {
+	ID        int64       `xorm:"id pk autoincr" json:"id"`
+	Name      null.String `xorm:"name" json:"name"`
+	StartPort string      `xorm:"start_port" json:"startPort"`
+	EndPort   string      `xorm:"end_port" json:"endPort"`
+	Protocol  null.String `xorm:"protocol" json:"protocol"`
+	Ip        null.String `xorm:"ip" json:"ip"`
+	Netmask   null.String `xorm:"netmask" json:"netmask"`
+	Mac       null.String `xorm:"mac" json:"mac"`
+	Action    null.String `xorm:"rule_action" json:"action"`
+	Priority  int64       `xorm:"priority" json:"priority"`
+}
+
+func (*FirewallCommonRule) TableName() string {
+	return " t_firewall_common"
+}
+
+type GeoipRule struct {
+	ID          int64  `xorm:"id pk autoincr" json:"id"`
+	CountryName string `xorm:"country_name" json:"countryName"`
+}
+
+func (*GeoipRule) TableName() string {
+	return " t_firewall_geoip"
+}
+
+type GeoIPNames struct {
+	Names []GeoipRule `json:"names"`
+}
+
+type FailToBan struct {
+	ID       int64  `xorm:"id pk autoincr" json:"id"`
+	Name     string `xorm:"name" json:"name"`
+	Enable   bool   `xorm:"enable" json:"enable"`
+	MaxRetry int64  `xorm:"max_retry" json:"maxRetry"`
+	FindTime int64  `xorm:"find_time" json:"findTime"`
+	BanTime  int64  `xorm:"ban_time" json:"banTime"`
+}
+
+func (*FailToBan) TableName() string {
+	return " t_fail2ban_basic"
+}
+
+type FailToBanIgnored struct {
+	ID            int64  `xorm:"id pk autoincr" json:"id"`
+	Name          string `xorm:"name" json:"name"`
+	Enable        int    `xorm:"enable" json:"enable"`
+	Https         int    `xorm:"protocol_https" json:"https"`
+	Iax           int    `xorm:"protocol_iax" json:"iax"`
+	Sip           int    `xorm:"protocol_sip" json:"sip"`
+	Ssh           int    `xorm:"protocol_ssh" json:"ssh"`
+	Ip            string `xorm:"ip" json:"ip"`
+	Netmask       string `xorm:"netmask" json:"netmask"`
+	NetmaskLength int    `xorm:"netmask_length"`
+}
+
+func (*FailToBanIgnored) TableName() string {
+	return " t_fail2ban_ignored"
+}
+
+type UpdatePriority struct {
+	Action string `json:"action"`
+	ID     int64  `json:"id"`
+}

+ 247 - 0
api/admin/auth/account.go

@@ -0,0 +1,247 @@
+package auth
+
+import (
+	"pms-api-go/pkg/lfshook"
+
+	"gopkg.in/ini.v1"
+)
+
+var AuthAccounts = make(map[string]string)
+
+/* 20230419 pms 删除 =======================================================================================================
+// Login 登录认证
+// @tags PBX-Auth
+// @Summary 登录认证
+// @Description 获取登录参数(账号,密码,角色) 与数据库数据对比,认证通过返回 jwt token
+// @Accept  json
+// @Produce  json
+// @Param   data body  commonModel.UserInfoReqVO  true "登录信息"
+// @Router /pbx/auth/login [post]
+func Login(ctx *gin.Context) {
+	var userReqInfo commonModel.UserInfoReqVO
+	if err := ctx.ShouldBindJSON(&userReqInfo); err != nil {
+		api.Error(ctx, http.StatusBadRequest, err.Error())
+		return
+	}
+	lfshook.NewLogger().Infof("user input info: %+v", userReqInfo)
+
+	var info *commonModel.UserInfoVO
+
+	// 用户名转换成分机号(数字)
+	var err error
+	result := utils.IsDigit(userReqInfo.UserName)
+	if !result {
+		// 用户登录
+		lfshook.NewLogger().Info("login by user")
+		info, err = services.AuthUser(userReqInfo.UserName, userReqInfo.Password)
+	} else {
+		// 分机登录
+		lfshook.NewLogger().Info("login by digital")
+		info, err = services.AuthDigital(userReqInfo.UserName, userReqInfo.Password)
+	}
+
+	if err != nil {
+		weblog.AuthError(ctx.ClientIP(), err.Error())
+		api.Error(ctx, http.StatusBadRequest, err.Error())
+		return
+	}
+
+	exp := time.Now().Add(time.Duration(time.Hour * 24 * 31)).Unix()
+	claim := commonModel.JWTCustomClaims{
+		ID:        info.ID,
+		UserName:  info.UserName,
+		Role:      info.Role,
+		Extension: info.Exten,
+		StandardClaims: jwt.StandardClaims{
+			ExpiresAt: exp,
+		},
+	}
+	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim)
+	tokenString, err := token.SignedString([]byte(configs.ConfigGlobal.IdentityKey))
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		api.Error(ctx, http.StatusBadRequest, "signed failure")
+		return
+	}
+	api.Success(ctx, fmt.Sprintf("Bearer %s", tokenString))
+}
+* ========================================================================================================================= */
+
+// AddAuth pms用户认证
+// @tags PBX-Auth
+// @Summary pms用户认证
+// @Description 从t_user表中抽出数据,添加到用户认证
+// @Security ApiKeyAuth
+
+func AddAuth() {
+	// 取表 t_user 中用户
+	/*var dbUser []commonModel.User
+	if err := mysql.DBOrmInstance.Find(&dbUser); err != nil {
+		lfshook.NewLogger().Error(err)
+	}
+	for _, item := range dbUser {
+		AuthAccounts[item.UserName] = item.PassWord
+	}
+	fmt.Printf("extenList=%s\n", AuthAccounts)*/
+
+	// 设计变更
+	// 读取pms配置文件中的用户密码
+	confPath := "/etc/asterisk/pms_api.conf"
+	cfg, err := ini.Load(confPath)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return
+	}
+	UserName := cfg.Section("general").Key("username").String()
+	PassWord := cfg.Section("general").Key("password").String()
+	if UserName == "" || PassWord == "" {
+		lfshook.NewLogger().Error("/etc/asterisk/pms_api.conf not set username or password")
+		return
+	}
+	AuthAccounts[UserName] = PassWord
+	// fmt.Printf("extenList=%s\n", AuthAccounts)
+}
+
+// VtigerAddAuth vtiger用户认证
+// @tags PBX-Auth
+// @Summary vtiger用户认证
+// @Description 从配置文件中取出数据,添加到用户认证
+// @Security ApiKeyAuth
+
+func VtigerAddAuth() {
+	// 读取vtiger配置文件中的用户密码
+	// confPath := "/etc/asterisk/vtiger_api.conf"
+	confPath := "/etc/asterisk/pms_api.conf"
+	cfg, err := ini.Load(confPath)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return
+	}
+	BasicAuthUser := cfg.Section("general").Key("vtigerBasicAuthUser").String()
+	BasicAuthPWD := cfg.Section("general").Key("vtigerBasicAuthPWD").String()
+	ApiKey := cfg.Section("general").Key("vtigerApiKey").String()
+	ApiKeyValue := cfg.Section("general").Key("vtigerApiKeyValue").String()
+	if (BasicAuthUser == "" || BasicAuthPWD == "") && (ApiKey == "" || ApiKeyValue == "") {
+		lfshook.NewLogger().Error("/etc/asterisk/pms_api.conf not set vtigerBasicAuth or vtigerApiKey")
+		return
+	}
+	AuthAccounts[BasicAuthUser] = BasicAuthPWD
+	AuthAccounts[ApiKey] = ApiKeyValue
+	// fmt.Printf("AuthAccounts=%s\n", AuthAccounts)
+}
+
+// ZohoAddAuth zoho用户认证
+// @tags PBX-Auth
+// @Summary zoho用户认证
+// @Description 从配置文件中取出数据,添加到用户认证
+// @Security ApiKeyAuth
+
+func ZohoAddAuth() {
+	// 读取vtiger配置文件中的用户密码
+	// confPath := "/etc/asterisk/vtiger_api.conf"
+	confPath := "/etc/asterisk/pms_api.conf"
+	cfg, err := ini.Load(confPath)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return
+	}
+	BasicAuthUser := cfg.Section("general").Key("zohoBasicAuthUser").String()
+	BasicAuthPWD := cfg.Section("general").Key("zohoBasicAuthPWD").String()
+	if BasicAuthUser == "" || BasicAuthPWD == "" {
+		lfshook.NewLogger().Error("/etc/asterisk/pms_api.conf not set zohoBasicAuthUser or zohoBasicAuthPWD")
+		return
+	}
+	AuthAccounts[BasicAuthUser] = BasicAuthPWD
+	// fmt.Printf("AuthAccounts=%s\n", AuthAccounts)
+}
+
+/* 20230419 pms 删除 =======================================================================================================
+// Logout 注销登录
+// @tags PBX-Auth
+// @Summary 注销登录
+// @Description 将 token 写入 redis, 标记注销。此 token 认证失效。
+// @Security ApiKeyAuth
+// @Accept  json
+// @Produce  json
+// @Router /pbx/auth/logout [get]
+func Logout(ctx *gin.Context) {
+	ID, _ := ctx.Get("ID")
+	UserName, _ := ctx.Get("UserName")
+	authorization := ctx.GetHeader("Authorization")
+	auth.Logout(authorization)
+	api.Success(ctx, map[string]string{"ID": strconv.FormatInt(ID.(int64), 10), "UserName": UserName.(string)})
+}
+
+// CurrentUser 当前用户
+// @tags PBX-Auth
+// @Summary 当前用户
+// @Description 获取当前用户信息ID UserName Password Role UserExtension(关联的分机号) PanelQueueuNumber(关联的随机一个队列号)
+// @Security ApiKeyAuth
+// @Accept  json
+// @Produce  json
+// @Router /pbx/auth/currentuser [get]
+func CurrentUser(ctx *gin.Context) {
+	ID, _ := ctx.Get("ID")
+	if ID == nil {
+		lfshook.NewLogger().Error("currentUser get no not found")
+		api.Error(ctx, http.StatusInternalServerError, "not found id")
+		return
+	}
+	idInt64 := ID.(int64)
+	lfshook.NewLogger().Infof("currentUser id: %d", idInt64)
+	dbUser := mysql.GetUserInfoByID(idInt64)
+
+	// 根据分机号查询关联队列
+	data, err := mysql.DBOrmInstance.SQL("select queue_name from t_queue_agent where exten = ? and agent_type='static'", dbUser.UserExtension).QueryString()
+	if err != nil {
+		lfshook.NewLogger().Errorf("currentUser get queue %+v", err)
+	}
+	if len(data) > 0 {
+		dbUser.PanelQueueNumber = data[0]["queue_name"]
+	}
+	api.Success(ctx, dbUser)
+}
+
+// @tags PBX-Auth
+// @Summary 更新密码
+// @Description 更新当前用户密码
+// @Security ApiKeyAuth
+// @Accept  json
+// @Produce  json
+// @Router /pbx/auth/update-password [post]
+func UpdatePassword(ctx *gin.Context) {
+	var info commonModel.UserPasswordVO
+	if err := ctx.ShouldBind(&info); err != nil {
+		api.Error(ctx, http.StatusBadRequest, err.Error())
+		return
+	}
+	ID, _ := ctx.Get("ID")
+	if ID == nil {
+		lfshook.NewLogger().Error("currentUser get no not found")
+		api.Error(ctx, http.StatusInternalServerError, "not found id")
+		return
+	}
+	idInt64 := ID.(int64)
+	user := commonModel.User{ID: idInt64}
+	exist, err := mysql.DBOrmInstance.Get(&user)
+	if err != nil {
+		api.Error(ctx, http.StatusInternalServerError, err.Error())
+		return
+	}
+	if !exist {
+		api.Error(ctx, http.StatusInternalServerError, "not found user by id "+strconv.FormatInt(idInt64, 10))
+		return
+	}
+	if user.PassWord != info.OldPassWord {
+		api.Error(ctx, http.StatusInternalServerError, "old password error")
+		return
+	}
+	user.PassWord = info.NewPassWord
+	_, err = mysql.DBOrmInstance.ID(idInt64).Cols("password").Update(&user)
+	if err != nil {
+		api.Error(ctx, http.StatusInternalServerError, err.Error())
+		return
+	}
+	api.Success(ctx, "success")
+}
+* ========================================================================================================================= */

+ 23 - 0
api/admin/router.go

@@ -0,0 +1,23 @@
+package admin
+
+import (
+	"pms-api-go/api/admin/auth"
+	"pms-api-go/api/admin/vtiger"
+	"pms-api-go/api/admin/zoho"
+
+	"github.com/gin-gonic/gin"
+)
+
+func AddRouter(router *gin.Engine) {
+	pbxGroup := router.Group("/pbx")
+	// swagger.Enable(pbxGroup) // 20230419 pms 注释
+
+	//pbxGroup.GET("/the42/password", getPassword)
+
+	// pbxGroup.Use(middleware.JWTAuth(configs.ConfigGlobal.IdentityKey))
+	pbxGroup.Use(gin.BasicAuth(auth.AuthAccounts))
+	// auth.AddRouter(pbxGroup) // 20230419 pms 注释
+
+	vtiger.AddRouter(pbxGroup)
+	zoho.AddRouter(pbxGroup)
+}

+ 152 - 0
api/admin/vtiger/info.go

@@ -0,0 +1,152 @@
+package vtiger
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"pms-api-go/api"
+	"pms-api-go/pkg/lfshook"
+
+	"github.com/gin-gonic/gin"
+	"gopkg.in/ini.v1"
+)
+
+// const VTIGER_URL = "https://zycoo1.od2.vtiger.com"
+
+// // Params 队列参数
+// type CallInitiatedParams struct {
+// 	From      string `form:"from"`
+// 	To        string `form:"to"`
+// 	Event     string `form:"event"`
+// 	CallId    string `form:"call_id"`
+// 	Direction string `form:"direction"`
+// }
+
+// @tags PBX-vtiger
+// @Summary 查询contact
+// @Description 查询contact
+// @Security ApiKeyAuth
+// @Accept  json
+// @Produce  json
+// @Router /api/vtiger/lookup [get]
+func contactsInfo(ctx *gin.Context) {
+	fmt.Printf("contactsInfo ............\n")
+
+	// 获取 vtigerUrl
+	// confPath := "/etc/asterisk/vtiger_api.conf"
+	confPath := "/etc/asterisk/pms_api.conf"
+	cfg, err := ini.Load(confPath)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return
+	}
+	VtigerUrl := cfg.Section("general").Key("vtigerUrl").String()
+	BasicAuthUser := cfg.Section("general").Key("vtigerBasicAuthUser").String()
+	BasicAuthPWD := cfg.Section("general").Key("vtigerBasicAuthPWD").String()
+	if VtigerUrl == "" || BasicAuthUser == "" || BasicAuthPWD == "" {
+		lfshook.NewLogger().Error("/etc/asterisk/pms_api.conf not set vtigerUrl or vtigerBasicAuthUser or vtigerBasicAuthPWD")
+		return
+	}
+
+	// 获取 phone_number
+	var reqVO CheckExistReqVO
+	if err := ctx.ShouldBind(&reqVO); err != nil {
+		api.Error(ctx, http.StatusBadRequest, err.Error())
+		return
+	}
+
+	// 创建HTTP客户端
+	client := &http.Client{}
+
+	// 创建请求
+	fmt.Printf("phone_number = %s\n", reqVO.PhoneNumber)
+	// getURL := fmt.Sprintf("%s/modules/PhoneCalls/callbacks/Search.php?phone_number=%s", VTIGER_URL, reqVO.PhoneNumber)
+	// getURL := fmt.Sprintf("%s/restapi/v1/vtiger/default/lookup?type=phone&value=%s", VTIGER_URL, reqVO.PhoneNumber)
+	getURL := fmt.Sprintf("%s/restapi/v1/vtiger/default/lookup?type=phone&value=%s", VtigerUrl, reqVO.PhoneNumber)
+	fmt.Printf("getURL = %s\n", getURL)
+	req, err := http.NewRequest("GET", getURL, nil)
+	if err != nil {
+		fmt.Println("创建请求时发生错误:", err)
+		return
+	}
+	// req.SetBasicAuth("juncheng.du@zycoo.com", "8DJ3O28MCZ4sPAk5")
+	req.SetBasicAuth(BasicAuthUser, BasicAuthPWD)
+
+	// 发送请求
+	resp, err := client.Do(req)
+	if err != nil {
+		fmt.Println("发送请求时发生错误:", err)
+		return
+	}
+	defer resp.Body.Close()
+
+	// 读取请求后的响应
+	data, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		fmt.Println("读取请求后的响应时发生错误:", err)
+		return
+	}
+
+	// 打印请求后的响应
+	fmt.Printf("data = %+v\n", string(data))
+	api.Success(ctx, string(data))
+}
+
+/* ===移到ami中了====================================================================================================
+// @tags PBX-vtiger
+// @Summary 呼叫发起事件
+// @Description 呼叫发起事件
+// @Security ApiKeyAuth
+// @Accept  json
+// @Produce  json
+// @Router /api/vtiger/call-initiated [get]
+func callInitiated(ctx *gin.Context) {
+	fmt.Printf("callInitiated ............\n")
+	var callInitiated CallInitiatedParams
+	if err := ctx.ShouldBind(&callInitiated); err != nil {
+		api.Error(ctx, http.StatusBadRequest, err.Error())
+		return
+	}
+
+	fmt.Printf("From = %s\n", callInitiated.From)
+	fmt.Printf("To = %s\n", callInitiated.To)
+	fmt.Printf("Event = %s\n", callInitiated.Event)
+	fmt.Printf("CallId = %s\n", callInitiated.CallId)
+	fmt.Printf("Direction = %s\n", callInitiated.Direction)
+
+	apiKeyValue := "174781790673da476e25cf"
+
+	// 创建HTTP客户端
+	client := &http.Client{}
+
+	// 创建请求
+	// https://zycoo1.od2.vtiger.com/modules/PhoneCalls/callbacks/Generic.php?from=10000002&to=12300001&event=call_initiated&call_id=12345678&direction=inbound
+	getURL := fmt.Sprintf("%s/modules/PhoneCalls/callbacks/Generic.php?from=%s&to=%s&event=%s&call_id=%s&direction=%s", VTIGER_URL, callInitiated.From, callInitiated.To, callInitiated.Event, callInitiated.CallId, callInitiated.Direction)
+	fmt.Printf("getURL = %s\n", getURL)
+	req, err := http.NewRequest("GET", getURL, nil)
+	if err != nil {
+		fmt.Println("创建请求时发生错误:", err)
+		return
+	}
+	req.Header.Set("X-VTIGER-SECRET", apiKeyValue)
+
+	// 发送请求
+	resp, err := client.Do(req)
+	if err != nil {
+		fmt.Println("发送请求时发生错误:", err)
+		return
+	}
+	defer resp.Body.Close()
+
+	// 读取请求后的响应
+	data, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		fmt.Println("读取请求后的响应后的响应时发生错误:", err)
+		return
+	}
+
+	// 打印请求后的响应
+	fmt.Printf("data = %+v\n", string(data))
+	api.Success(ctx, string(data))
+}
+// ======================================================================================================= */

+ 8 - 0
api/admin/vtiger/model.go

@@ -0,0 +1,8 @@
+package vtiger
+
+// CheckExistReqVO
+// @Description 检查是否存在
+type CheckExistReqVO struct {
+	ID          int64  `json:"id" form:"id"`                     //ID 为0时表示不比较ID存在
+	PhoneNumber string `json:"phone_number" form:"phone_number"` // 需要检查的值
+}

+ 10 - 0
api/admin/vtiger/router.go

@@ -0,0 +1,10 @@
+package vtiger
+
+import (
+	"github.com/gin-gonic/gin"
+)
+
+func AddRouter(group *gin.RouterGroup) {
+	group.GET("/vtiger/lookup", contactsInfo) // 按编号搜索
+	// group.GET("/vtiger/call-initiated", callInitiated)
+}

+ 138 - 0
api/admin/zoho/info.go

@@ -0,0 +1,138 @@
+package zoho
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"pms-api-go/api"
+	"pms-api-go/pkg/lfshook"
+
+	"github.com/gin-gonic/gin"
+	"gopkg.in/ini.v1"
+)
+
+// @tags PBX-zoho
+// @Summary 获取token
+// @Description 获取token
+// @Security ZohoToken
+// @Accept  json
+// @Produce  json
+// @Router /api/zoho/gettoken [post]
+func getToken(ctx *gin.Context) {
+	fmt.Printf("getToken ............\n")
+
+	// 获取配置文件信息
+	// confPath := "/etc/asterisk/vtiger_api.conf"
+	confPath := "/etc/asterisk/pms_api.conf"
+	cfg, err := ini.Load(confPath)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return
+	}
+	ZohoAuthUrl := cfg.Section("general").Key("zohoAuthUrl").String()
+	ZohoCode := cfg.Section("general").Key("zohoCode").String()
+	ZohoClientId := cfg.Section("general").Key("zohoClientId").String()
+	ZohoClientSecret := cfg.Section("general").Key("zohoClientSecret").String()
+	if ZohoAuthUrl == "" || ZohoCode == "" || ZohoClientId == "" || ZohoClientSecret == "" {
+		lfshook.NewLogger().Error("/etc/asterisk/pms_api.conf not set zohoAuthUrl or zohoCode or zohoClientId or zohoClientSecret")
+		return
+	}
+
+	// 创建HTTP客户端
+	client := &http.Client{}
+
+	// 创建请求
+	// https://accounts.zoho.com/oauth/v3/device/token?code=1004.d4c145db9ec33e64f955290f0905ff1e.eebc39b33f228c3bee806d6f8200c50f
+	// &client_id=1004.LWJCJZD5O9DB6SZLL5YJEWHT7LH0BV&client_secret=fc3aef43dc58af8a49d3ed597710924200b03f74d0&grant_type=device_token
+	getURL := fmt.Sprintf("%s/oauth/v3/device/token?code=%s&client_id=%s&client_secret=%s&grant_type=device_token", ZohoAuthUrl, ZohoCode, ZohoClientId, ZohoClientSecret)
+	fmt.Printf("getURL = %s\n", getURL)
+	req, err := http.NewRequest("POST", getURL, nil)
+	if err != nil {
+		fmt.Println("创建请求时发生错误:", err)
+		return
+	}
+
+	// req.Header.Set("Authorization", "Bearer "+accessToken)  // 获取token时不需要
+
+	// 发送请求
+	resp, err := client.Do(req)
+	if err != nil {
+		fmt.Println("发送请求时发生错误:", err)
+		return
+	}
+	defer resp.Body.Close()
+
+	// 读取请求后的响应
+	data, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		fmt.Println("读取请求后的响应时发生错误:", err)
+		return
+	}
+
+	// 打印请求后的响应
+	fmt.Printf("data = %+v\n", string(data))
+	api.Success(ctx, string(data))
+}
+
+// @tags PBX-zoho
+// @Summary 刷新token
+// @Description 刷新token
+// @Security ZohoToken
+// @Accept  json
+// @Produce  json
+// @Router /api/zoho/refresh-token [post]
+func refreshToken(ctx *gin.Context) {
+	fmt.Printf("refreshToken ............\n")
+
+	// 获取配置文件信息
+	// confPath := "/etc/asterisk/vtiger_api.conf"
+	confPath := "/etc/asterisk/pms_api.conf"
+	cfg, err := ini.Load(confPath)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return
+	}
+	ZohoAuthUrl := cfg.Section("general").Key("zohoAuthUrl").String()
+	ZohoRefreshToken := cfg.Section("general").Key("zohoRefreshToken").String()
+	ZohoClientId := cfg.Section("general").Key("zohoClientId").String()
+	ZohoClientSecret := cfg.Section("general").Key("zohoClientSecret").String()
+	if ZohoAuthUrl == "" || ZohoRefreshToken == "" || ZohoClientId == "" || ZohoClientSecret == "" {
+		lfshook.NewLogger().Error("/etc/asterisk/pms_api.conf not set zohoAuthUrl or zohoRefreshToken or zohoClientId or zohoClientSecret")
+		return
+	}
+
+	// 创建HTTP客户端
+	client := &http.Client{}
+
+	// 创建请求
+	// https://accounts.zoho.com/oauth/v2/token?refresh_token=1004.86c8c0e3db7bfe9133598825bef28eb9.17a82a3bf3e675c504f478c1b0b5c456
+	// &client_id=1004.LWJCJZD5O9DB6SZLL5YJEWHT7LH0BV&client_secret=fc3aef43dc58af8a49d3ed597710924200b03f74d0&grant_type=refresh_token
+	getURL := fmt.Sprintf("%s/oauth/v2/token?refresh_token=%s&client_id=%s&client_secret=%s&grant_type=refresh_token", ZohoAuthUrl, ZohoRefreshToken, ZohoClientId, ZohoClientSecret)
+	fmt.Printf("getURL = %s\n", getURL)
+	req, err := http.NewRequest("POST", getURL, nil)
+	if err != nil {
+		fmt.Println("创建请求时发生错误:", err)
+		return
+	}
+
+	// req.Header.Set("Authorization", "Bearer "+accessToken) // 刷新token时不需要
+
+	// 发送请求
+	resp, err := client.Do(req)
+	if err != nil {
+		fmt.Println("发送请求时发生错误:", err)
+		return
+	}
+	defer resp.Body.Close()
+
+	// 读取请求后的响应
+	data, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		fmt.Println("读取请求后的响应时发生错误:", err)
+		return
+	}
+
+	// 打印请求后的响应
+	fmt.Printf("data = %+v\n", string(data))
+	api.Success(ctx, string(data))
+}

+ 11 - 0
api/admin/zoho/router.go

@@ -0,0 +1,11 @@
+package zoho
+
+import (
+	"github.com/gin-gonic/gin"
+)
+
+func AddRouter(group *gin.RouterGroup) {
+	group.POST("/zoho/gettoken", getToken)          // 获取token
+	group.POST("/zoho/refresh-token", refreshToken) // 刷新token
+
+}

+ 1 - 0
api/index.go

@@ -0,0 +1 @@
+package api

+ 4 - 0
api/plugins/api/README.md

@@ -0,0 +1,4 @@
+# 一些非管理界面用到的 API 接口
+
+* 拨号
+* 挂断

+ 97 - 0
api/plugins/api/index.go

@@ -0,0 +1,97 @@
+package api
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"pms-api-go/api"
+	"pms-api-go/internal/app/ami/action"
+	"pms-api-go/internal/app/mysql"
+	"pms-api-go/pkg/lfshook"
+	"strings"
+
+	"github.com/gin-gonic/gin"
+)
+
+type DialParam struct {
+	Dest string `json:"dest" binding:"required"`
+	Src  string `json:"src" binding:"required"`
+}
+
+type HangupParam struct {
+	Extensions []string `json:"extensions" binding:"required"`
+}
+
+// @tags API
+// @Summary 点击拨号
+// @Description 点击拨号
+// @Security ApiKeyAuth
+// @Accept  json
+// @Produce  json
+// @Param   data body  DialParam true  "拨号参数"
+// @Router /api/call-dial/dial [post]
+func dialExtension(ctx *gin.Context) {
+	var param DialParam
+	err := ctx.ShouldBindJSON(&param)
+	if err != nil {
+		lfshook.NewLogger().Infof("Error : %+v", err)
+		api.Error(ctx, http.StatusBadRequest, err.Error())
+		return
+	}
+	dialrule := mysql.GetDialPlanByExtension(param.Src)
+	action.Dial(param.Src, param.Dest, dialrule, param.Dest, param.Dest)
+	api.Success(ctx, gin.H{"msg": "ami dial"})
+}
+
+// @tags API
+// @Summary zoho点击拨号
+// @Description zoho点击拨号
+// @Security ApiKeyAuth
+// @Accept  json
+// @Produce  json
+// @Param   data body  DialParam true  "拨号参数"
+// @Router /api/call-dial/zoho-dial [post]
+func zohoDialExtension(ctx *gin.Context) {
+	// Zoho CRM 拨号对应 POST /pbx/api/call-dial/dial tonumber=12300000001&dest=302
+	// requestURI := ctx.Request.RequestURI
+	// fmt.Println("requestURI = ", requestURI) // requestURI =  /pbx/api/call-dial/dial
+	// requestBody := ctx.Request.Body
+	// fmt.Println("requestBody = ", requestBody) // requestBody =  {tonumber=12300000001&dest=302}
+
+	requestBody, _ := ioutil.ReadAll(ctx.Request.Body)
+	// fmt.Println("requestBody = ", requestBody)                 // requestBody =  [116 111 110 117 109 98 101 114 61 49 50 51 48 48 48 48 48 48 48 49 38 100 101 115 116 61 51 48 50]
+	// fmt.Println("string(requestBody) = ", string(requestBody)) // string(requestBody) =  tonumber=12300000001&src=302
+	if strings.Contains(string(requestBody), "tonumber") && strings.Contains(string(requestBody), "src") {
+		srcNum := strings.Split(string(requestBody), "=")[2]
+		fmt.Println("srcNum = ", srcNum) // srcNum =  302
+
+		destNum := strings.Split(strings.Split(string(requestBody), "=")[1], "&")[0]
+		fmt.Println("destNum = ", destNum) //  destNum =  12300000001
+
+		dialrule := mysql.GetDialPlanByExtension(srcNum)
+		fmt.Println("dialrule = ", dialrule)
+		action.Dial(srcNum, destNum, dialrule, destNum, destNum)
+	}
+	api.Success(ctx, gin.H{"msg": "ami dial"})
+}
+
+// @tags API
+// @Summary 挂断指定分机
+// @Description 挂断指定分机
+// @Security ApiKeyAuth
+// @Accept  json
+// @Produce  json
+// @Param   data body  HangupParam true  "挂断参数"
+// @Router /api/call-dial/hangup [post]
+func dialHangup(ctx *gin.Context) {
+	data := &HangupParam{}
+	err := ctx.ShouldBindJSON(data)
+	if err != nil {
+		api.Error(ctx, http.StatusInternalServerError, "no data")
+		return
+	}
+	for _, extension := range data.Extensions {
+		action.Hangup(extension)
+	}
+	api.Success(ctx, gin.H{"msg": "ami hangup"})
+}

+ 12 - 0
api/plugins/api/router.go

@@ -0,0 +1,12 @@
+package api
+
+import (
+	"github.com/gin-gonic/gin"
+)
+
+func AddRouter(group *gin.RouterGroup) {
+	// group.Use(middleware.JWTAuth(configs.ConfigGlobal.IdentityKey))
+	group.POST("/call-dial/dial", dialExtension)          // pbx 或 vtiger 点击拨号
+	group.POST("/call-dial/zoho-dial", zohoDialExtension) // zoho 点击拨号
+	group.POST("/call-dial/hangup", dialHangup)
+}

+ 56 - 0
api/response.go

@@ -0,0 +1,56 @@
+package api
+
+import (
+	"fmt"
+	"net/http"
+	"pms-api-go/pkg/lfshook"
+	"runtime"
+
+	"github.com/gin-gonic/gin"
+)
+
+var resp *Response
+
+func init() {
+	resp = NewResponse()
+}
+
+type Response struct {
+	Code    int         `json:"code"`
+	Message string      `json:"message"`
+	Data    interface{} `json:"data"`
+}
+
+func NewResponse() *Response {
+	return &Response{}
+}
+
+func Success(ctx *gin.Context, data interface{}) { resp.Success(ctx, data) }
+func (r *Response) Success(ctx *gin.Context, data interface{}) {
+	if data == nil {
+		data = gin.H{}
+	}
+
+	ctx.JSON(http.StatusOK, Response{
+		Code:    0,
+		Message: "OK",
+		Data:    data,
+	})
+}
+
+func Error(ctx *gin.Context, code int, message string) { resp.Error(ctx, code, message) }
+func (r *Response) Error(ctx *gin.Context, code int, message string) {
+	// 调用信息
+	msg := fmt.Sprintf("error msg: %s", message)
+	pc, fileName, lineNumber, ok := runtime.Caller(2)
+	if ok {
+		msg = fmt.Sprintf("%s\n%s %s:%d", message, runtime.FuncForPC(pc).Name(), fileName, lineNumber)
+	}
+
+	lfshook.NewLogger().Error(msg)
+	ctx.JSON(http.StatusOK, Response{
+		Code:    code,
+		Message: message,
+		Data:    nil,
+	})
+}

+ 96 - 0
cmd/commands/service.go

@@ -0,0 +1,96 @@
+package commands
+
+import (
+	"fmt"
+	"pms-api-go/pkg/lfshook"
+
+	"github.com/kardianos/service"
+	"github.com/urfave/cli"
+)
+
+var logger service.Logger
+
+type program struct {
+	ctx *cli.Context
+}
+
+func (p *program) Start(s service.Service) error {
+	go p.run()
+	return nil
+}
+func (p *program) run() {
+	runWeb(p.ctx)
+}
+func (p *program) Stop(s service.Service) error {
+	// Stop should not block. Return with a few seconds.
+	return nil
+}
+
+// CmdService cli 命令
+var CmdService = cli.Command{
+	Name:        "service",
+	Usage:       "Run the program as a service",
+	Description: "PBX API install/uninstall",
+	Action:      runService,
+	Flags: []cli.Flag{
+		cli.StringFlag{Name: "action, a", Usage: "install/uninstall/start/stop"},
+		cli.StringFlag{Name: "config, c", Usage: "配置文件路径"},
+		cli.Int64Flag{Name: "port, p", Usage: "默认绑定端口 8080", Value: 8080},
+		cli.StringFlag{Name: "host", Usage: "默认绑定地址 0.0.0.0", Value: "0.0.0.0"},
+	},
+}
+
+func runService(ctx *cli.Context) {
+	svcConfig := &service.Config{
+		Name:        "PBX-API",
+		DisplayName: "PBX-API",
+		Description: "PBX-API as service.",
+		Arguments:   []string{"web", "--config", ctx.String("config")},
+	}
+
+	prg := &program{
+		ctx: ctx,
+	}
+	s, err := service.New(prg, svcConfig)
+	if err != nil {
+		lfshook.NewLogger().Fatal(err)
+	}
+	logger, err = s.Logger(nil)
+	if err != nil {
+		lfshook.NewLogger().Fatal(err)
+	}
+
+	switch ctx.String("action") {
+	case "install":
+		if !ctx.IsSet("config") {
+			fmt.Print("install must set config path, please by  -config set")
+			return
+		}
+		err = s.Install()
+		if err != nil {
+			fmt.Println("install err", err)
+		} else {
+			fmt.Println("install success")
+		}
+		return
+	case "uninstall":
+		err = s.Uninstall()
+		if err != nil {
+			fmt.Println("Uninstall err", err)
+		} else {
+			fmt.Println("Uninstall success")
+		}
+		return
+	case "start":
+		s.Start()
+		return
+	case "stop":
+		s.Stop()
+		return
+	}
+
+	err = s.Run()
+	if err != nil {
+		logger.Error(err)
+	}
+}

+ 114 - 0
cmd/commands/web.go

@@ -0,0 +1,114 @@
+package commands
+
+import (
+	"fmt"
+	"io/ioutil"
+	"pms-api-go/internal/app"
+	"pms-api-go/pkg/configs"
+	"pms-api-go/pkg/lfshook"
+	"pms-api-go/pkg/utils"
+
+	// _ "pms-api-go/api/admin/swagger"  // 20230419 pms 注释
+
+	"github.com/gin-gonic/gin"
+	"github.com/sirupsen/logrus"
+	"github.com/urfave/cli"
+	"gopkg.in/natefinch/lumberjack.v2"
+)
+
+// CmdWeb cli 命令
+var CmdWeb = cli.Command{
+	Name:        "web",
+	Usage:       "./pbx-api-gin web",
+	Description: "Run PBX Web Server",
+	Action:      runWeb,
+	Flags: []cli.Flag{
+		cli.StringFlag{Name: "config, c", Usage: "config file path"},
+	},
+}
+
+func runWeb(ctx *cli.Context) error {
+	if !ctx.IsSet("config") {
+		fmt.Print("not set config file path, add -config param")
+		return fmt.Errorf("not set config")
+	}
+
+	// 解析配置文件
+	configs.ConfigPath = ctx.String("config")
+	configs.DecodeConfig()
+	configs.GetPushConfig()
+
+	if ctx.GlobalIsSet("level") {
+		configs.ConfigGlobal.LogLevel = logrus.Level(ctx.GlobalInt("level"))
+	} else {
+		configs.ConfigGlobal.LogLevel = logrus.InfoLevel
+	}
+
+	if ctx.GlobalBool("release") {
+		gin.SetMode(gin.ReleaseMode)
+	} else {
+		gin.SetMode(gin.DebugMode)
+	}
+
+	if ctx.GlobalBool("reportCaller") {
+		lfshook.NewLogger().Logger.SetReportCaller(true)
+	}
+
+	if ctx.GlobalBool("disableConsoleLog") {
+		lfshook.NewLogger().Logger.Out = ioutil.Discard
+	}
+
+	lfshook.NewLogger().Logger.SetFormatter(&logrus.TextFormatter{
+		ForceQuote:      false,
+		FullTimestamp:   true,
+		TimestampFormat: "01-02 15:04:05",
+	})
+
+	pathMap := lfshook.LoggerMap{
+		logrus.InfoLevel: &lumberjack.Logger{
+			Filename:   configs.ConfigGlobal.LogInfoPath,
+			MaxSize:    10, // maxSize M
+			MaxBackups: 5,  // keep 5 file
+			MaxAge:     7,  //  7 day
+		},
+
+		logrus.WarnLevel: &lumberjack.Logger{
+			Filename:   configs.ConfigGlobal.LogErrorPath,
+			MaxSize:    10, // maxSize M
+			MaxBackups: 5,  // keep 5 file
+			MaxAge:     7,  //  7 day
+		},
+
+		logrus.ErrorLevel: &lumberjack.Logger{
+			Filename:   configs.ConfigGlobal.LogErrorPath,
+			MaxSize:    10, // maxSize M
+			MaxBackups: 5,  // keep 5 file
+			MaxAge:     7,  //  7 day
+		},
+		logrus.FatalLevel: &lumberjack.Logger{
+			Filename:   configs.ConfigGlobal.LogErrorPath,
+			MaxSize:    10, // maxSize M
+			MaxBackups: 5,  // keep 5 file
+			MaxAge:     7,  //  7 day
+		},
+		logrus.PanicLevel: &lumberjack.Logger{
+			Filename:   configs.ConfigGlobal.LogErrorPath,
+			MaxSize:    10, // maxSize M
+			MaxBackups: 5,  // keep 5 file
+			MaxAge:     7,  //  7 day
+		},
+	}
+
+	lfshook.NewLogger().Logger.Hooks.Add(lfshook.NewHook(
+		pathMap,
+		&logrus.TextFormatter{
+			DisableColors:   true,
+			ForceQuote:      true,
+			TimestampFormat: "01-02 15:04:05",
+		},
+	))
+
+	app.StartApp()
+	utils.Exit()
+	return nil
+}

+ 70 - 0
cmd/main.go

@@ -0,0 +1,70 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"pms-api-go/cmd/commands"
+	"pms-api-go/pkg/utils"
+	"runtime"
+	"time"
+
+	"github.com/urfave/cli"
+)
+
+var (
+	gitCommitCode string
+	buildDateTime string
+	goVersion     string
+)
+
+func init() {
+	locationName := utils.GetLocationName()
+	if locationName != "" {
+		time.Local, _ = time.LoadLocation(locationName)
+	}
+	runtime.GOMAXPROCS(runtime.NumCPU())
+}
+
+func main() {
+	initVersion()
+	app := cli.NewApp()
+	app.Name = "pbx-api-gin"
+	app.Usage = "A PBX Manager Server"
+	app.Version = fmt.Sprintf("%s-%s-%s", buildDateTime, gitCommitCode, goVersion)
+	app.Commands = []cli.Command{
+		commands.CmdWeb,
+		commands.CmdService,
+	}
+	app.Flags = []cli.Flag{
+		cli.IntFlag{
+			Name:  "level",
+			Usage: "log level 0(panic)-6(trace), default 4(info)",
+			Value: 4,
+		},
+		cli.BoolFlag{
+			Name:  "reportCaller",
+			Usage: "if true report logger caller",
+		},
+		cli.BoolFlag{
+			Name:  "disableConsoleLog",
+			Usage: "if true cancel console log",
+		},
+		cli.BoolFlag{
+			Name:  "release",
+			Usage: "if true gin set release",
+		},
+		cli.BoolFlag{
+			Name:  "prompt",
+			Usage: "if ture, prompt will show",
+		},
+	}
+	_ = app.Run(os.Args)
+}
+
+func initVersion() {
+	if gitCommitCode != "" {
+		utils.VersionInstance.BuildDate = buildDateTime
+		utils.VersionInstance.GoVersion = goVersion
+		utils.VersionInstance.GitCommitCode = gitCommitCode
+	}
+}

+ 45 - 0
configs/config.demo.yaml

@@ -0,0 +1,45 @@
+identityKey: '' # jwt secret
+
+pbxnumber: '67129100'
+
+# AMI
+asteriskAMIHost: '127.0.0.1'
+asteriskAMIPort: '5038'
+asteriskAMIUser: 'admin'
+asteriskAMISecret: 'admin'
+
+# 数据库配置
+mysqlDBHost: '127.0.0.1'
+mysqlDBUser: 'ippbx'
+mysqlDBName: 'ippbx_db'
+mysqlDBSecret: '123456'
+
+# Redis
+redisDBHost: '127.0.0.1'
+redisDBPort: '6379'
+redisDBSecret: ''
+
+# log
+logErrorPath: '/var/log/weblog/error.log'
+logInfoPath: '/var/log/weblog/info.log'
+
+# music upload
+storagePath: '/tmp/'
+# ssl folder
+sslPath: '/usr/local/defaults/etc/asterisk/keys'
+# logo folder
+logoPath: '/usr/local/defaults/etc/asterisk/logo'
+
+webtype: http
+webhost: 0.0.0.0
+webport: 8080
+
+# push url
+pushConfig: ./configs/push.json
+
+#cors
+allowOrigin: http://localhost:8080
+
+#优先国际化目录, 不存在加载 resources
+i18nPath: './configs'
+loglevel: panic

+ 28 - 0
configs/config.yaml

@@ -0,0 +1,28 @@
+identityKey: bbbe0c7a-6877-4b27-9ad3-d086616e7c03
+pbxnumber: "67129100"
+asteriskAMIHost: 127.0.0.1
+asteriskAMIPort: "5038"
+asteriskAMIUser: admin
+asteriskAMISecret: admin
+mysqlDBHost: 127.0.0.1
+mysqlDBUser: ippbx
+mysqlDBSecret: "123456"
+mysqlDBName: ippbx_db
+redisDBHost: 127.0.0.1
+redisDBPort: "6379"
+redisDBSecret: ""
+logInfoPath: /var/log/weblog/info.log
+logErrorPath: /var/log/weblog/error.log
+storagePath: /tmp/
+logoPath: /usr/local/defaults/etc/asterisk/logo
+webtype: http
+webhost: 0.0.0.0
+webport: 8080
+allowOrigin: http://localhost:8080
+pushConfig: ./configs/push.json
+i18nPath: ./configs
+loglevel: panic
+# basicAuthUser: juncheng.du@zycoo.com
+# basicAuthPWD: 8DJ3O28MCZ4sPAk5
+# apiKey: X-VTIGER-SECRET
+# apiKeyValue: 174781790673da476e25cf

+ 1 - 0
configs/push.json

@@ -0,0 +1 @@
+{"events":["AgentLogin","AgentLogoff"],"allEventPushUrl":""}

BIN
deployments/pbx-api-gin


BIN
deployments/pms-api-arm


BIN
deployments/pms-api-arm_20241121bak


+ 37 - 0
go.mod

@@ -0,0 +1,37 @@
+module pms-api-go
+
+go 1.16
+
+require (
+	github.com/dgrijalva/jwt-go v3.2.0+incompatible
+	github.com/gin-contrib/gzip v0.0.5
+	github.com/gin-contrib/pprof v1.3.0
+	github.com/gin-gonic/gin v1.9.0
+	github.com/go-ldap/ldap/v3 v3.4.4
+	github.com/go-ping/ping v1.1.0
+	github.com/go-redis/redis/v8 v8.11.5
+	github.com/go-sql-driver/mysql v1.6.0
+	github.com/gofrs/uuid v4.0.0+incompatible
+	github.com/google/go-cmp v0.5.7 // indirect
+	github.com/googollee/go-socket.io v1.4.5
+	github.com/kardianos/service v1.2.0
+	github.com/mitchellh/mapstructure v1.4.1
+	github.com/nicksnyder/go-i18n/v2 v2.1.2
+	github.com/shirou/gopsutil/v3 v3.21.7
+	github.com/sirupsen/logrus v1.9.3
+	github.com/smartystreets/goconvey v1.6.4 // indirect
+	github.com/spf13/cast v1.5.0
+	github.com/swaggo/gin-swagger v1.4.3
+	github.com/swaggo/swag v1.8.2
+	github.com/tqcenglish/amigo-go v1.1.15
+	github.com/ugorji/go v1.2.7 // indirect
+	github.com/unrolled/secure v1.13.0
+	github.com/urfave/cli v1.20.0
+	github.com/xuri/excelize/v2 v2.4.1
+	golang.org/x/text v0.7.0
+	gopkg.in/guregu/null.v4 v4.0.0
+	gopkg.in/ini.v1 v1.61.0
+	gopkg.in/natefinch/lumberjack.v2 v2.0.0
+	gopkg.in/yaml.v2 v2.4.0
+	xorm.io/xorm v1.0.7
+)

+ 470 - 0
go.sum

@@ -0,0 +1,470 @@
+gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
+gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
+github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e h1:ZU22z/2YRFLyf/P4ZwUYSdNCWsMEI0VeyrFoI2rAhJQ=
+github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
+github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU=
+github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
+github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
+github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
+github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
+github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
+github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
+github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
+github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
+github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
+github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
+github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
+github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
+github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gin-contrib/gzip v0.0.5 h1:mhnVU32YnnBh2LPH2iqRqsA/eR7SAqRaD388jL2s/j0=
+github.com/gin-contrib/gzip v0.0.5/go.mod h1:OPIK6HR0Um2vNmBUTlayD7qle4yVVRZT0PyhdUigrKk=
+github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
+github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=
+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.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
+github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
+github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
+github.com/gin-gonic/gin v1.8.0 h1:4WFH5yycBMA3za5Hnl425yd9ymdw1XPm4666oab+hv4=
+github.com/gin-gonic/gin v1.8.0/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
+github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
+github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
+github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
+github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
+github.com/go-ldap/ldap/v3 v3.4.3 h1:JCKUtJPIcyOuG7ctGabLKMgIlKnGumD/iGjuWeEruDI=
+github.com/go-ldap/ldap/v3 v3.4.3/go.mod h1:7LdHfVt6iIOESVEe3Bs4Jp2sHEKgDeduAhgM1/f9qmo=
+github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs=
+github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI=
+github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY=
+github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
+github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
+github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
+github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
+github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
+github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
+github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw=
+github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
+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/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
+github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
+github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
+github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
+github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
+github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
+github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
+github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
+github.com/go-redis/redis/v8 v8.8.2 h1:O/NcHqobw7SEptA0yA6up6spZVFtwE06SXM8rgLtsP8=
+github.com/go-redis/redis/v8 v8.8.2/go.mod h1:F7resOH5Kdug49Otu24RjHWwgK7u9AmtqWMnCV1iP5Y=
+github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
+github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
+github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
+github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
+github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
+github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/gomodule/redigo v1.8.4 h1:Z5JUg94HMTR1XpwBaSH4vq3+PNSIykBLxMdglbw10gg=
+github.com/gomodule/redigo v1.8.4/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
+github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
+github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googollee/go-socket.io v1.4.5 h1:9uq4RvfxTH7wvfvnPS9ff2NJ9xuBvAwI1ujcN9e6LhA=
+github.com/googollee/go-socket.io v1.4.5/go.mod h1:oxHyzWprXBI9NRq9MtScgQsb5x4f72GZ92ZSJLLFh6w=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/kardianos/service v1.2.0 h1:bGuZ/epo3vrt8IPC7mnKQolqFeYJb7Cs8Rk4PSOBB/g=
+github.com/kardianos/service v1.2.0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
+github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
+github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
+github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
+github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
+github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
+github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
+github.com/nicksnyder/go-i18n/v2 v2.1.2 h1:QHYxcUJnGHBaq7XbvgunmZ2Pn0focXFqTD61CkH146c=
+github.com/nicksnyder/go-i18n/v2 v2.1.2/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4=
+github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
+github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
+github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
+github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ=
+github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
+github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
+github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
+github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
+github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
+github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
+github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
+github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
+github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
+github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
+github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
+github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+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/richardlehane/mscfb v1.0.3 h1:rD8TBkYWkObWO0oLDFCbwMeZ4KoalxQy+QgniCj3nKI=
+github.com/richardlehane/mscfb v1.0.3/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
+github.com/richardlehane/msoleps v1.0.1 h1:RfrALnSNXzmXLbGct/P2b4xkFz4e8Gmj/0Vj9M9xC1o=
+github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shirou/gopsutil/v3 v3.21.7 h1:PnTqQamUjwEDSgn+nBGu0qSDV/CfvyiR/gwTH3i7HTU=
+github.com/shirou/gopsutil/v3 v3.21.7/go.mod h1:RGl11Y7XMTQPmHh8F0ayC6haKNBgH4PXMJuTAcMOlz4=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
+github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+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/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
+github.com/swaggo/gin-swagger v1.4.3 h1:mHJz+yzJne0udgYnC5qlDf4e7KuxUbVNX2dhD/cw2rU=
+github.com/swaggo/gin-swagger v1.4.3/go.mod h1:hBg6tGeKJsUu/P79BH+WGUR8nq2LuGE0O160+s4iefo=
+github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
+github.com/swaggo/swag v1.8.2 h1:D4aBiVS2a65zhyk3WFqOUz7Rz0sOaUcgeErcid5uGL4=
+github.com/swaggo/swag v1.8.2/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg=
+github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
+github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
+github.com/tklauser/go-sysconf v0.3.7 h1:HT7h4+536gjqeq1ZIJPgOl1rg1XFatQGVZWp7Py53eg=
+github.com/tklauser/go-sysconf v0.3.7/go.mod h1:JZIdXh4RmBvZDBZ41ld2bGxRV3n4daiiqA3skYhAoQ4=
+github.com/tklauser/numcpus v0.2.3 h1:nQ0QYpiritP6ViFhrKYsiv6VVxOpum2Gks5GhnJbS/8=
+github.com/tklauser/numcpus v0.2.3/go.mod h1:vpEPS/JC+oZGGQ/My/vJnNsvMDQL6PwOqt8dsCw5j+E=
+github.com/tqcenglish/amigo-go v1.1.12 h1:Lti9DcuLjASy7MOocGdFsI2unoK4XMEnJPJbue4O6fA=
+github.com/tqcenglish/amigo-go v1.1.12/go.mod h1:4EgGuajmQawlqIPtDtRPETIj/0eNkzr5kq2P6moMJAU=
+github.com/tqcenglish/amigo-go v1.1.15 h1:lVVkEXZQSCR8EAX5tgySVTTa9ot9rSX2+RnKaviME6Q=
+github.com/tqcenglish/amigo-go v1.1.15/go.mod h1:R4D2J0kumfy0aJU5n4262rgXgqsRgeSr2NgDD4yWoFQ=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
+github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
+github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
+github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
+github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/unrolled/secure v1.0.9 h1:BWRuEb1vDrBFFDdbCnKkof3gZ35I/bnHGyt0LB0TNyQ=
+github.com/unrolled/secure v1.0.9/go.mod h1:fO+mEan+FLB0CdEnHf6Q4ZZVNqG+5fuLFnP8p0BXDPI=
+github.com/unrolled/secure v1.13.0 h1:sdr3Phw2+f8Px8HE5sd1EHdj1aV3yUwed/uZXChLFsk=
+github.com/unrolled/secure v1.13.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
+github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
+github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
+github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
+github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
+github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 h1:EpI0bqf/eX9SdZDwlMmahKM+CDBgNbsXMhsN28XrM8o=
+github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
+github.com/xuri/excelize/v2 v2.4.1 h1:veeeFLAJwsNEBPBlDepzPIYS1eLyBVcXNZUW79exZ1E=
+github.com/xuri/excelize/v2 v2.4.1/go.mod h1:rSu0C3papjzxQA3sdK8cU544TebhrPUoTOaGPIh0Q1A=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
+go.opentelemetry.io/otel v0.19.0 h1:Lenfy7QHRXPZVsw/12CWpxX6d/JkrX8wrx2vO8G80Ng=
+go.opentelemetry.io/otel v0.19.0/go.mod h1:j9bF567N9EfomkSidSfmMwIwIBuP37AMAIzVW85OxSg=
+go.opentelemetry.io/otel/metric v0.19.0 h1:dtZ1Ju44gkJkYvo+3qGqVXmf88tc+a42edOywypengg=
+go.opentelemetry.io/otel/metric v0.19.0/go.mod h1:8f9fglJPRnXuskQmKpnad31lcLJ2VmNNqIsx/uIwBSc=
+go.opentelemetry.io/otel/oteltest v0.19.0 h1:YVfA0ByROYqTwOxqHVZYZExzEpfZor+MU1rU+ip2v9Q=
+go.opentelemetry.io/otel/oteltest v0.19.0/go.mod h1:tI4yxwh8U21v7JD6R3BcA/2+RBoTKFexE/PJ/nSO7IA=
+go.opentelemetry.io/otel/trace v0.19.0 h1:1ucYlenXIDA1OlHVLDZKX0ObXV5RLaq06DtUKz5e5zc=
+go.opentelemetry.io/otel/trace v0.19.0/go.mod h1:4IXiNextNOpPnRlI4ryK69mn5iC84bjBWZQA5DXz/qg=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o=
+golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
+golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
+golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk=
+golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
+golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
+golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
+golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
+golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
+golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+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/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
+golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
+golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
+golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg=
+gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI=
+gopkg.in/ini.v1 v1.61.0 h1:LBCdW4FmFYL4s/vDZD1RQYX7oAR6IjujCYgMdbHBR10=
+gopkg.in/ini.v1 v1.61.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+xorm.io/builder v0.3.7 h1:2pETdKRK+2QG4mLX4oODHEhn5Z8j1m8sXa7jfu+/SZI=
+xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
+xorm.io/xorm v1.0.7 h1:26yBTDVI+CfQpVz2Y88fISh+aiJXIPP4eNoTJlwzsC4=
+xorm.io/xorm v1.0.7/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=

+ 203 - 0
internal/app/ami/action/call.go

@@ -0,0 +1,203 @@
+package action
+
+import (
+	"fmt"
+	"pms-api-go/internal/app/ami"
+	"pms-api-go/pkg/lfshook"
+	"pms-api-go/pkg/utils"
+)
+
+// Hangup 挂断指定分机或通道
+func Hangup(channel string) {
+	lfshook.NewLogger().Infof("hangup extensions/channel %s", channel)
+	if !utils.IsChannel(channel) {
+		channel = fmt.Sprintf(`/^(PJ)?SIP\/%s-.*$/`, channel)
+	}
+
+	action := map[string]string{
+		"Action":  "hangup",
+		"Channel": channel,
+	}
+	lfshook.NewLogger().Infof("hangup action %+v", action)
+	if _, _, err := ami.AminInstance.Send(action); err != nil {
+		lfshook.NewLogger().Errorf("Hangup %+v", err)
+	}
+}
+
+// Dial 拨打号码
+func Dial(src, dst, dialrule, callerID, callerName string) {
+	chanel := fmt.Sprintf("Local/%s", src)
+	ChanVar := fmt.Sprintf("DIAL_DEST=%s", dst)
+
+	action := map[string]string{
+		"Action":   "Originate",
+		"Channel":  chanel,
+		"Exten":    dst,
+		"Context":  dialrule,
+		"CallerID": fmt.Sprintf("%s<%s>", callerName, callerID),
+		"Priority": "1",
+		"async":    "true",
+		"Variable": ChanVar,
+	}
+	lfshook.NewLogger().Infof("dial action %+v", action)
+	res, _, err := ami.AminInstance.Send(action)
+	if err != nil {
+		lfshook.NewLogger().Errorf("%+v", err)
+	}
+	lfshook.NewLogger().Info(res)
+}
+
+/* *****************************************************************************************
+// ChanSpy
+func ChanSpy(src, dst string, whisper, bargein bool) {
+	lfshook.NewLogger().Infof("chan spy src:%s dst:%s", src, dst)
+
+	channel := fmt.Sprintf("%s/%s", utils.DialPrefix, dst)
+	data := fmt.Sprintf("%s/%s,q,E", utils.DialPrefix, src)
+
+	if whisper {
+		data = fmt.Sprintf("%s,w", data)
+	}
+
+	if bargein {
+		data = fmt.Sprintf("%s,B", data)
+	}
+
+	action := map[string]string{
+		"Action":      "Originate",
+		"Channel":     channel, // 不存在的通话
+		"Application": "ChanSpy",
+		"Data":        data, // 存在的通话
+		"CallerID":    dst,
+		"Async":       "true",
+	}
+	lfshook.NewLogger().Infof("ChanSpy action %+v", action)
+	_, _, err := ami.AminInstance.Send(action)
+	if err != nil {
+		lfshook.NewLogger().Errorf("%+v", err)
+	}
+}
+
+// Page
+func Page(src string, extensions []string, duplex bool) {
+	channel := fmt.Sprintf("%s/%s", utils.DialPrefix, src)
+	data := make([]string, 0)
+	for _, exten := range extensions {
+		data = append(data, fmt.Sprintf("%s/%s", utils.DialPrefix, exten))
+	}
+	appData := strings.Join(data, "&")
+	if duplex {
+		appData = fmt.Sprintf("%s,d", appData)
+	}
+	//timeout
+	appData = fmt.Sprintf("%s,30", appData)
+	action := map[string]string{
+		"Action":      "Originate",
+		"Channel":     channel,
+		"Application": "page",
+		"Data":        appData,
+		"CallerID":    src,
+		"CallerSrc":   src,
+		"async":       "true",
+		"Variable":    "CDR(group_type)=rg",
+	}
+	lfshook.NewLogger().Infof("page action %+v", action)
+	res, _, err := ami.AminInstance.Send(action)
+	if err != nil {
+		lfshook.NewLogger().Errorf("%+v", err)
+	}
+	lfshook.NewLogger().Infof("%+v", res)
+}
+
+// Play 转 AGI 播放
+func Play(name, UUID string, extensions []string, hangupAll bool) {
+	if hangupAll {
+		lfshook.NewLogger().Infof("hangup all before play to %+v", extensions)
+		for _, extension := range extensions {
+			Hangup(extension)
+		}
+		time.Sleep(3 * time.Second)
+	}
+
+	channel := "Local/broadcast@broadcast"
+	data := make([]string, 0)
+	for _, exten := range extensions {
+		data = append(data, fmt.Sprintf("%s/%s", utils.DialPrefix, exten))
+	}
+	appData := strings.Join(data, "&")
+	//timeout
+	appData = fmt.Sprintf("%s,,%d", appData, 3000)
+	variable := fmt.Sprintf("task_UUID=%s", UUID)
+	action := map[string]string{
+		"Action":      "Originate",
+		"Channel":     channel,
+		"Application": "page",
+		"Data":        appData,
+		"Variable":    variable,
+		//"CallerID":    fmt.Sprintf("%s<%s>", configs.ConfigGlobal.AsteriskBroadcastName, configs.ConfigGlobal.AsteriskBroadcastID),
+		"CallerID": fmt.Sprintf("%s<%s>", name, name),
+		"CallSrc":  name,
+		"async":    "true",
+	}
+	lfshook.NewLogger().Infof("play action %+v", action)
+	_, _, err := ami.AminInstance.Send(action)
+	if err != nil {
+		lfshook.NewLogger().Errorf("%+v", err)
+	}
+}
+
+// Redirect 转接
+func Redirect(channel, dst, dialrule string) (err error) {
+	callerID := "redirect"
+	lfshook.NewLogger().Infof("redirect src %s to dst %s", channel, dst)
+	if !utils.IsChannel(channel) {
+		callerID = channel
+		if channel, err = GetChannelByExten(channel); err != nil {
+			return err
+		}
+	}
+
+	action := map[string]string{
+		"Action":    "Redirect",
+		"Channel":   channel,
+		"Extension": dst,
+		"Context":   dialrule,
+		"CallerID":  callerID,
+		"Priority":  "1",
+		"async":     "true",
+	}
+	lfshook.NewLogger().Infof("redirect %+v", action)
+	res, _, err := ami.AminInstance.Send(action)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+	}
+	lfshook.NewLogger().Info(res)
+	return err
+}
+
+// Atxfer
+func Atxfer(channel, dst, dialrule string) (err error) {
+	// 获取通道
+	if !utils.IsChannel(channel) {
+		if channel, err = GetChannelByExten(channel); err != nil {
+			return err
+		}
+	}
+
+	action := map[string]string{
+		"Action":    "Atxfer",
+		"Channel":   channel,
+		"Extension": dst,
+		"Context":   dialrule,
+	}
+	lfshook.NewLogger().Infof("atxfer action %+v", action)
+	res, _, err := ami.AminInstance.Send(action)
+	if err != nil {
+		lfshook.NewLogger().Errorf("%+v", err)
+		return err
+	}
+	lfshook.NewLogger().Debug("atxfer res %+v", res)
+	return err
+}
+
+// ***************************************************************************************** */

+ 117 - 0
internal/app/ami/action/channel.go

@@ -0,0 +1,117 @@
+package action
+
+import (
+	"errors"
+	"pms-api-go/internal/app/ami"
+	"pms-api-go/pkg/lfshook"
+	"pms-api-go/pkg/utils"
+	"strconv"
+	"strings"
+)
+
+type CoreShowChannelResVO struct {
+	CallerIDName      string `json:"callerIDName"`
+	CallerIDNum       string `json:"callerIDNumber"`
+	Channel           string `json:"channel"`
+	ConnectedLineName string `json:"connectedLineName"`
+	ConnectedLineNum  string `json:"connectedLineNumber"`
+	Duration          string `json:"duration"`
+	DurationSecond    int    `json:"durationSecond"`
+}
+
+// CoreShowChannels 获取通话通道
+func CoreShowChannels() (result []CoreShowChannelResVO, err error) {
+	// 通过 src 查询对应通道
+	_, events, err := ami.AminInstance.Send(map[string]string{
+		"Action": "CoreShowChannels",
+	})
+	if err != nil {
+		lfshook.NewLogger().Errorf("core show channels error %+v", err)
+		return nil, err
+	}
+
+	lfshook.NewLogger().Tracef("events %+v", events)
+	result = make([]CoreShowChannelResVO, 0)
+	for _, event := range events {
+		if event.Data["Event"] == "CoreShowChannel" {
+			channel := CoreShowChannelResVO{
+				CallerIDName:      event.Data["CallerIDName"],
+				CallerIDNum:       event.Data["CallerIDNum"],
+				ConnectedLineName: event.Data["ConnectedLineName"],
+				ConnectedLineNum:  event.Data["ConnectedLineNum"],
+				Channel:           event.Data["Channel"],
+				Duration:          event.Data["Duration"],
+				DurationSecond:    utils.TimeStringToSecond(event.Data["Duration"]),
+			}
+			result = append(result, channel)
+		}
+	}
+	lfshook.NewLogger().Tracef("channels %+v", result)
+	return result, nil
+}
+
+type CoreShowCallsResVO struct {
+	Active    int `json:"active"`
+	Processed int `json:"processed"`
+}
+
+// CoreShowCalls 获取当前通话数量
+// Response: Success
+// Message: Command output follows
+// Output: 66 active calls
+// Output: 6090 calls processed
+func CoreShowCalls() (result CoreShowCallsResVO, err error) {
+	result = CoreShowCallsResVO{}
+	data, _, err := ami.AminInstance.Send(map[string]string{
+		"Action":  "Command",
+		"Command": "core show calls",
+	})
+	if err != nil {
+		lfshook.NewLogger().Errorf("core show calls error %+v", err)
+		return result, err
+	}
+	// lfshook.NewLogger().Infof("data %+v", data)
+	lines := strings.Split(data["Output"], "\n")
+	for index, line := range lines {
+		data := strings.Fields(line)
+		if len(data) == 0 {
+			continue
+		}
+		switch index {
+		case 0:
+			//:0 active calls
+			result.Active, _ = strconv.Atoi(data[0])
+		case 1:
+			//:1 call processed
+			result.Processed, _ = strconv.Atoi(data[0])
+		}
+	}
+	return
+}
+
+// GetChannelByExten  通过 exten 查询对应通道
+func GetChannelByExten(exten string) (channel string, err error) {
+	lfshook.NewLogger().Infof("GetChannelByExten %s", exten)
+	_, events, err := ami.AminInstance.Send(map[string]string{
+		"Action": "CoreShowChannels",
+	})
+	if err != nil {
+		lfshook.NewLogger().Errorf("core show channels error %+v", err)
+		return "", err
+	}
+
+	for _, event := range events {
+		lfshook.NewLogger().Infof("CoreShowChannels event Data %+v", event.Data)
+		if event.Data["Event"] == "CoreShowChannel" && event.Data["ConnectedLineNum"] == exten && event.Data["CallerIDNum"] != exten {
+			channel = event.Data["Channel"]
+			lfshook.NewLogger().Infof("GetChannelByExten get channel %s", channel)
+			break
+		}
+	}
+	if channel == "" {
+		lfshook.NewLogger().Errorf("not found channel %s", exten)
+		return "", errors.New("not found channel")
+	}
+
+	return channel, nil
+}

+ 77 - 0
internal/app/ami/action/database.go

@@ -0,0 +1,77 @@
+package action
+
+import (
+	"errors"
+	"fmt"
+	"pms-api-go/internal/app/ami"
+	"pms-api-go/internal/app/redis"
+	"pms-api-go/pkg/utils"
+)
+
+func DBPut(family, key, value string) (err error) {
+	action := map[string]string{
+		"Action": "DBPut",
+		"Family": family,
+		"Key":    key,
+		"Val":    value,
+	}
+
+	res, _, err := ami.AminInstance.Send(action)
+	if err != nil {
+		return err
+	}
+	if res["Response"] != "Success" {
+		return errors.New(res["Message"])
+	}
+	if family == "DND" {
+		redis.ExtensionDNDSet(key, value)
+		_, _, _ = utils.ExecCmdAsync("asterisk", "-rx", fmt.Sprintf("presencestate change CustomPresence:%s dnd", key))
+		_, _, _ = utils.ExecCmdAsync("asterisk", "-rx", fmt.Sprintf("pjsip send notify dndon endpoint %s", key))
+
+	}
+	return nil
+}
+
+func DBDel(family, key string) error {
+	action := map[string]string{
+		"Action": "DBDel",
+		"Family": family,
+		"Key":    key,
+	}
+
+	res, _, err := ami.AminInstance.Send(action)
+	if err != nil {
+		return err
+	}
+	if res["Response"] != "Success" {
+		return errors.New(res["Message"])
+	}
+	if family == "DND" {
+		redis.ExtensionDNDDel(key)
+		_, _, _ = utils.ExecCmdAsync("asterisk", "-rx", fmt.Sprintf("presencestate change CustomPresence:%s XA", key))
+		_, _, _ = utils.ExecCmdAsync("asterisk", "-rx", fmt.Sprintf("pjsip send notify dndoff endpoint %s", key))
+	}
+	return nil
+}
+
+func DBGet(family, key string) (value string, err error) {
+	action := map[string]string{
+		"Action": "DBGet",
+		"Family": family,
+		"Key":    key,
+	}
+
+	res, events, err := ami.AminInstance.Send(action)
+	if err != nil {
+		return "", err
+	}
+	if res["Response"] != "Success" {
+		return "", errors.New(res["Message"])
+	}
+	for _, v := range events {
+		if v.Data["Event"] == "DBGetResponse" {
+			return v.Data["Val"], nil
+		}
+	}
+	return "", nil
+}

+ 51 - 0
internal/app/ami/action/extension.go

@@ -0,0 +1,51 @@
+package action
+
+import (
+	"errors"
+	"pms-api-go/internal/app/ami"
+	"pms-api-go/internal/app/ami/model"
+	"strings"
+)
+
+func ExtensionStateList() (result []*model.ExtensionStatus, err error) {
+	action := map[string]string{
+		"Action": "ExtensionStateList",
+	}
+
+	res, events, err := ami.AminInstance.Send(action)
+
+	if err != nil {
+		return nil, err
+	}
+	if res["Response"] != "Success" {
+		return nil, errors.New(res["Message"])
+	}
+
+	for _, event := range events {
+		if event.Data["Event"] == "ExtensionStatus" {
+			// pjsip web app 三个类型
+			// PJSIP/842&PJSIP/wb_842,CustomPresence:842
+			hint := event.Data["Hint"]
+			end := strings.Index(hint, ",")
+			if end > 1 {
+				extens := strings.Split(hint[:end], "&")
+				//lfshook.NewLogger().Infof("hint %s, extens: %+v", hint, extens)
+				for _, exten := range extens {
+					if !strings.HasPrefix(exten, "PJSIP") {
+						continue
+					}
+					point := &model.ExtensionStatus{
+						Event:      event.Data["Event"],
+						Exten:      exten[6:],
+						Context:    event.Data["Context"],
+						Hint:       hint,
+						Status:     event.Data["Status"],
+						StatusText: event.Data["StatusText"],
+					}
+					result = append(result, point)
+				}
+			}
+		}
+	}
+	return result, nil
+}

+ 73 - 0
internal/app/ami/action/sip.go

@@ -0,0 +1,73 @@
+package action
+
+import (
+	"encoding/json"
+	"pms-api-go/internal/app/ami"
+	"pms-api-go/internal/app/ami/model"
+)
+
+func PJSIPShowEndpoints() (points []*model.EndpointList, err error) {
+	var action = map[string]string{
+		"Action": "PJSIPShowEndpoints",
+	}
+	_, events, err := ami.AminInstance.Send(action)
+
+	if err != nil {
+		return nil, err
+	}
+
+	points = make([]*model.EndpointList, 0)
+	for _, event := range events {
+		if event.Data["Event"] == "EndpointList" {
+			data, _ := json.Marshal(event.Data)
+			point := &model.EndpointList{}
+			json.Unmarshal(data, point)
+			points = append(points, point)
+		}
+	}
+	return points, nil
+}
+
+func PJSIPShowContacts() (points []*model.ContactList, err error) {
+	points = make([]*model.ContactList, 0)
+	var action = map[string]string{
+		"Action": "PJSIPShowContacts",
+	}
+	_, events, err := ami.AminInstance.Send(action)
+
+	if err != nil {
+		return points, err
+	}
+
+	for _, event := range events {
+		if event.Data["Event"] == "ContactList" {
+			data, _ := json.Marshal(event.Data)
+			point := &model.ContactList{}
+			json.Unmarshal(data, point)
+			points = append(points, point)
+		}
+	}
+	return points, nil
+}
+
+func SIPPeers() (points []*model.PeerEntry, err error) {
+	var action = map[string]string{
+		"Action": "SIPPeers",
+	}
+	_, events, err := ami.AminInstance.Send(action)
+
+	if err != nil {
+		return nil, err
+	}
+
+	points = make([]*model.PeerEntry, 0)
+	for _, event := range events {
+		if event.Data["Event"] == "PeerEntry" {
+			data, _ := json.Marshal(event.Data)
+			point := &model.PeerEntry{}
+			json.Unmarshal(data, point)
+			points = append(points, point)
+		}
+	}
+	return points, nil
+}

+ 99 - 0
internal/app/ami/bridge.go

@@ -0,0 +1,99 @@
+package ami
+
+import (
+	"fmt"
+	socketio "pms-api-go/internal/app/socket_io"
+	"pms-api-go/pkg/lfshook"
+	"sync"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+var bridgeMap sync.Map
+
+const bridgeEventName = "CustomBridgeEvent"
+
+type BridgeEvent struct {
+	Event string `json:"event"`
+	// Timestamp         string `json:"timestamp"`
+	BridgeUniqueid          string `json:"-"`
+	Channel                 string `json:"channel"`
+	ChannelState            string `json:"channelState"`
+	ChannelStateDesc        string `json:"channelStateDesc"`
+	CallerIDNum             string `json:"callerIDNumber"`
+	CallerIDName            string `json:"callerIDName"`
+	CallerConnectedLineNum  string `json:"callerConnectedLineNumber"`
+	CallerConnectedLineName string `json:"callerConnectedLineName"`
+	BridgeType              string `json:"bridgeType"`
+	BridgeNumChannels       string `json:"bridgeNumberChannels"`
+	Uniqueid                string `json:"uniqueid"`
+}
+
+func (event BridgeEvent) String() string {
+	return fmt.Sprintf("channel: %s, uniqueid: %s\n", event.Channel, event.Uniqueid)
+}
+
+func handleAMIBridge(event map[string]string) {
+	if event["BridgeType"] != "basic" {
+		return
+	}
+
+	if event["CallerIDName"] == "conference" {
+		return
+	}
+
+	switch event["Event"] {
+	case "BridgeCreate":
+		bridgeMap.Store(event["BridgeUniqueid"], []*BridgeEvent{})
+	case "BridgeEnter":
+		status := &BridgeEvent{}
+		mapstructure.Decode(event, status)
+		if events, ok := bridgeMap.Load(status.BridgeUniqueid); ok {
+			bridgeMap.Store(status.BridgeUniqueid, append(events.([]*BridgeEvent), status))
+		} else {
+			lfshook.NewLogger().Warnf("BridgeEnter add failure id: %s on load", status.BridgeUniqueid)
+		}
+		socketio.SocketIOServer.BroadcastToNamespace("", bridgeEventName, GetBridgeMapValue())
+	case "BridgeLeave":
+		status := &BridgeEvent{}
+		mapstructure.Decode(event, status)
+		if events, ok := bridgeMap.Load(status.BridgeUniqueid); ok {
+			events := events.([]*BridgeEvent)
+			for index, event := range events {
+				if event.Channel == status.Channel {
+					if index == len(events)-1 {
+						bridgeMap.Store(status.BridgeUniqueid, events[:index])
+					} else {
+						bridgeMap.Store(status.BridgeUniqueid, append(events[:index], events[index+1]))
+					}
+					break
+				}
+			}
+		} else {
+			lfshook.NewLogger().Warnf("BridgeLeave remove failure id: %s on load", status.BridgeUniqueid)
+		}
+		socketio.SocketIOServer.BroadcastToNamespace("", bridgeEventName, GetBridgeMapValue())
+	case "BridgeDestroy":
+		bridgeMap.Delete(event["BridgeUniqueid"])
+		socketio.SocketIOServer.BroadcastToNamespace("", bridgeEventName, GetBridgeMapValue())
+	}
+
+}
+
+func GetBridgeMapValue() (result map[string][]*BridgeEvent) {
+	result = make(map[string][]*BridgeEvent)
+	bridgeMap.Range(func(key, value interface{}) bool {
+		events := value.([]*BridgeEvent)
+		result[key.(string)] = events
+		return true
+	})
+	return
+}
+
+// ClearBridge Asterisk 重启,内部通话清空, AMI 重连成功也需求清空 Bridge
+func ClearBridge() {
+	bridgeMap.Range(func(key, value interface{}) bool {
+		bridgeMap.Delete(key)
+		return true
+	})
+}

+ 379 - 0
internal/app/ami/index.go

@@ -0,0 +1,379 @@
+package ami
+
+import (
+	"pms-api-go/internal/app/ami/model"
+	"pms-api-go/pkg/configs"
+	"pms-api-go/pkg/lfshook"
+	"strings"
+
+	"pms-api-go/internal/app/redis"
+	socketio "pms-api-go/internal/app/socket_io"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/sirupsen/logrus"
+	"github.com/tqcenglish/amigo-go"
+	"github.com/tqcenglish/amigo-go/pkg"
+)
+
+var AminInstance *amigo.Amigo
+
+// const VTIGER_URL = "https://zycoo1.od2.vtiger.com"
+
+// 认证信息添加到 ./config.yaml
+// const BasicAuthUser = "juncheng.du@zycoo.com"
+// const BasicAuthPWD = "8DJ3O28MCZ4sPAk5"
+// const ApiKey = "X-VTIGER-SECRET"
+// const ApiKeyValue = "174781790673da476e25cf"
+
+func StartAMI(connectOKCallBack func(), handleEvents []func(event map[string]string), apiType string) {
+	lfshook.NewLogger().Info("Start AMI")
+	settings := &amigo.Settings{
+		Host:     configs.ConfigGlobal.AsteriskAMIHost,
+		Port:     configs.ConfigGlobal.AsteriskAMIPort,
+		Username: configs.ConfigGlobal.AsteriskAMIUser,
+		Password: configs.ConfigGlobal.AsteriskAMISecret,
+		LogLevel: logrus.InfoLevel}
+	lfshook.NewLogger().Infof("ami setting: %+v", settings)
+	AminInstance = amigo.New(settings, lfshook.NewLogger())
+	AminInstance.EventOn(func(payload ...interface{}) {
+		// lfshook.NewLogger().Infof("ami event on %+v", payload[0])
+		event := payload[0].(map[string]string)
+
+		handleAMIBridge(event)
+		handleAMI(event)
+		handleSocketIO(event)
+
+		// /* 20241128 vtiger crm 对应 ============================================================
+		if apiType == "CRM_vtiger" {
+			phoneCalls(event)
+		} else if apiType == "CRM_zoho" {
+			callNotify(event)
+		}
+		// * ===================================================================================== */
+		/* 20241128 vtiger crm 对应 ============================================================
+		fmt.Println("=========================================== ")
+		fmt.Println("event[] = ", event)
+		fmt.Println("event = ", event["Event"])
+		fmt.Println("AMIPushUrl = ", configs.PushConfigValue.AMIPushUrl)
+		fmt.Println("Channel = ", event["Channel"])
+
+		var callId string
+		if event["Channel"] != "" {
+			callId = strings.Split(strings.Split(event["Channel"], "/")[1], "-")[0]
+			fmt.Println("callId = ", callId)
+		}
+
+		// if configs.PushConfigValue.AMIPushUrl != "" {
+		// 	for _, eventName := range configs.PushConfigValue.AMIEvents {
+		// 		if eventName == event["Event"] {
+		// 			go httpclient.Post(event, configs.PushConfigValue.AMIPushUrl)
+		// 			break
+		// 		}
+		// 	}
+		// }
+		callId = "10000007"                                                                                           // 测试用,每次测试 +1
+		recordingUrl := "https%3A%2F%2Fs3.amazonaws.com%2Frecordings_2013%2F8e522852-72aa-11e5-ab5f-842b2b021118.mp3" // 测试用
+		event["Exten"] = "123456789"                                                                                  // 测试用
+		fmt.Println("Exten = ", event["Exten"])
+
+		// 读取vtiger配置文件
+		confPath := "/etc/asterisk/vtiger_api.conf"
+		cfg, err := ini.Load(confPath)
+		if err != nil {
+			lfshook.NewLogger().Error(err)
+			return
+		}
+		VTIGER_URL := cfg.Section("general").Key("vtigerUrl").String()
+		if VTIGER_URL == "" {
+			lfshook.NewLogger().Error("/etc/asterisk/vtiger_api.conf not set vtigerUrl")
+			return
+		}
+
+		// 呼叫发起事件
+		if event["Event"] == "DialBegin" { // "DialBegin" "Newchannel"
+			// https://zycoo1.od2.vtiger.com/modules/PhoneCalls/callbacks/Generic.php?from=147258369&to=12300001&event=call_initiated&call_id=12345678&direction=inbound
+			// https://zycoo1.od2.vtiger.com/modules/PhoneCalls/callbacks/Generic.php?from=86147258369&event=call_initiated&call_id=12345678  // 可以省去部分参数
+			getURL := fmt.Sprintf("%s/modules/PhoneCalls/callbacks/Generic.php?from=%s&event=call_initiated&call_id=%s", VTIGER_URL, event["Exten"], callId)
+			// fmt.Println("getURL = ", getURL)
+			// go httpclient.Get(getURL)
+			go httpclient.ApiKeyGet(getURL) // 后面要不要把用户密码给传过去
+
+			// resp := httpclient.ApiKeyGet(getURL)
+			// // 读取请求后的响应
+			// data, err := ioutil.ReadAll(resp.Body)
+			// if err != nil {
+			// 	fmt.Println("读取请求后的响应时发生错误:", err)
+			// 	return
+			// }
+			// // 打印请求后的响应
+			// fmt.Printf("data = %+v\n", string(data))
+
+			// 呼叫已连接事件
+		} else if event["Event"] == "BridgeEnter" { // "Newstate" "BridgeEnter"
+			// https://zycoo1.od2.vtiger.com/modules/PhoneCalls/callbacks/Generic.php?from=86147258369&to=12300001&event=call_connected&call_id=12345678
+			// https://zycoo1.od2.vtiger.com/modules/PhoneCalls/callbacks/Generic.php?event=call_connected&call_id=12345678  // 可以省去部分参数
+			getURL := fmt.Sprintf("%s/modules/PhoneCalls/callbacks/Generic.php?event=call_connected&call_id=%s", VTIGER_URL, callId)
+			// fmt.Println("getURL = ", getURL)
+			go httpclient.ApiKeyGet(getURL)
+
+			// 通话录音事件
+		} else if event["Event"] == "Cdr" {
+			// https://zycoo1.od2.vtiger.com/modules/PhoneCalls/callbacks/Generic.php?call_id=12345678&event=call_recording&recordingurl=https%3A%2F%2Fs3.amazonaws.com%2Frecordings_2013%2F8e522852-72aa-11e5-ab5f-842b2b021118.mp3
+			getURL := fmt.Sprintf("%s/modules/PhoneCalls/callbacks/Generic.php?call_id=%s&event=call_recording&recordingurl=%s", VTIGER_URL, callId, recordingUrl)
+			// fmt.Println("getURL = ", getURL)
+			go httpclient.ApiKeyGet(getURL)
+
+			// 呼叫转移事件
+			// Command:SET VARIABLE FORWARD_TYPE "busy"   Command:SET VARIABLE FORWARD_DEST "136"
+			// Command:SET VARIABLE FORWARD_TYPE "always"  Command:SET VARIABLE FORWARD_DEST "136"
+			// Command:SET VARIABLE FORWARD_TYPE "noan_busy"  Command:SET VARIABLE FORWARD_DEST "136"
+			// Command:SET VARIABLE FORWARD_TYPE "noan_unav"   Command:SET VARIABLE FORWARD_DEST "136"
+			// Command:SET VARIABLE FORWARD_TYPE "unavailable"  Command:SET VARIABLE FORWARD_DEST "136"
+		} else if strings.Contains(event["Command"], "FORWARD_DEST") {
+			// transferredNumber := strings.Split(event["Command"], "\"")[1] // 正式用
+			transferredNumber := "147258369" // 测试用
+			// fmt.Println("Command = ", event["Command"])
+			// fmt.Println("transferredNumber = ", transferredNumber)
+
+			// https://zycoo1.od2.vtiger.com/modules/PhoneCalls/callbacks/Generic.php?call_id=12345678&event=call_transfer&transferred_number=147258369
+			getURL := fmt.Sprintf("%s/modules/PhoneCalls/callbacks/Generic.php?call_id=%s&event=call_transfer&transferred_number=%s", VTIGER_URL, callId, transferredNumber)
+			// fmt.Println("getURL = ", getURL)
+			go httpclient.ApiKeyGet(getURL)
+
+			// 呼叫挂断事件
+		} else if event["Event"] == "HangupRequest" { // "HangupRequest" "Hangup"
+			// https://zycoo1.od2.vtiger.com/modules/PhoneCalls/callbacks/Generic.php?call_id=12345678&event=call_hangup
+			getURL := fmt.Sprintf("%s/modules/PhoneCalls/callbacks/Generic.php?call_id=%s&event=call_hangup", VTIGER_URL, callId)
+			fmt.Println("getURL = ", getURL)
+			go httpclient.ApiKeyGet(getURL)
+		}
+		// 按编号搜索API 暂时用的以下接口
+		// group.GET("/vtiger/lookup", contactsInfo) // 按编号搜索
+
+		// * ===================================================================================== */
+
+		for _, handle := range handleEvents {
+			go handle(event)
+		}
+
+	})
+	AminInstance.ConnectOn(func(payload ...interface{}) {
+		if payload[0] == pkg.Connect_OK {
+			lfshook.NewLogger().Info("ami connect ok")
+			ClearBridge()
+			connectOKCallBack()
+		} else {
+			lfshook.NewLogger().Errorf("ami connect failure %+v", payload)
+		}
+	})
+	AminInstance.Connect()
+}
+
+func handleAMI(event map[string]string) {
+	switch event["Event"] {
+	case "ContactStatus":
+		endpointName := event["EndpointName"]
+		contactStatus := event["ContactStatus"]
+
+		// NonQualified
+		round := event["RoundtripUsec"]
+		if round == "" {
+			round = "1000"
+		}
+		uri := event["URI"]
+		data := strings.Split(uri, "@")
+		if len(data) < 2 {
+			lfshook.NewLogger().Debugf("split URI by @ error URI: %s event:%+v", uri, event)
+			return
+		}
+		first := data[1]
+
+		// 中继变化也会触发此事件, 需要忽略
+		switch contactStatus {
+		case "Removed":
+			//remove contact
+			redis.ExtensionDel(endpointName, event["URI"], strings.Split(first, ":")[0], round)
+		case "Reachable":
+			fallthrough
+		default:
+			redis.ExtensionSet(endpointName, event["URI"], strings.Split(first, ":")[0], round)
+		}
+	case "ExtensionStatus":
+		status := &model.ExtensionStatus{
+			Event:      event["Event"],
+			Exten:      event["Exten"],
+			Context:    event["Context"],
+			Hint:       event["Hint"],
+			Status:     event["Status"],
+			StatusText: event["StatusText"],
+		}
+		redis.ExtensionSetStatus(status)
+	}
+}
+
+func handleSocketIO(event map[string]string) {
+	var data interface{}
+	switch event["Event"] {
+	case "ExtensionStatus":
+		lfshook.NewLogger().Tracef("ExtensionStatus %+v", event)
+		status := &model.ExtensionStatus{}
+		mapstructure.Decode(event, status)
+		data = status
+	case "QueueMemberStatus":
+		lfshook.NewLogger().Tracef("QueueMemberStatus %+v", event)
+		status := &model.QueueMember{}
+		mapstructure.Decode(event, status)
+		data = status
+	case "QueueCallerJoin":
+		status := &model.QueueCallerJoin{}
+		lfshook.NewLogger().Infof("status %+v", event)
+		mapstructure.Decode(event, status)
+		data = status
+	case "QueueCallerLeave":
+		status := &model.QueueCallerLeave{}
+		mapstructure.Decode(event, status)
+		data = status
+	case "QueueCallerAbandon":
+		status := &model.QueueCallerAbandon{}
+		mapstructure.Decode(event, status)
+		data = status
+	case "ParkedCall":
+		status := &model.ParkedCall{}
+		mapstructure.Decode(event, status)
+		data = status
+	case "ParkedCallTimeOut":
+		status := &model.ParkedCallTimeOut{}
+		mapstructure.Decode(event, status)
+		data = status
+	case "ParkedCallGiveUp":
+		status := &model.ParkedCallGiveUp{}
+		mapstructure.Decode(event, status)
+		data = status
+	case "UnParkedCall":
+		status := &model.UnParkedCall{}
+		mapstructure.Decode(event, status)
+		data = status
+
+	case "MeetmeEnd":
+		status := &model.MeetmeEnd{}
+		mapstructure.Decode(event, status)
+		data = status
+
+	case "MeetmeJoin":
+		fallthrough
+	case "MeetmeLeave":
+		fallthrough
+	case "MeetmeMute":
+		fallthrough
+	case "MeetmeTalking":
+		fallthrough
+	case "MeetmeTalkRequest":
+		status := &model.Meetme{}
+		mapstructure.Decode(event, status)
+		data = status
+
+	case "PresenceStateChange":
+		status := &model.PresenceStateChange{}
+		mapstructure.Decode(event, status)
+		if status.Presentity != "" {
+			data := strings.Split(status.Presentity, ":")
+			if len(data) == 2 {
+				status.Extension = data[1]
+			}
+		}
+
+		if status.Status == "dnd" {
+			status.DndStatus = "yes"
+		} else {
+			status.DndStatus = ""
+		}
+		data = status
+
+	case "DialBegin":
+		fallthrough
+	case "DialEnd":
+		fallthrough
+	case "DialState":
+		status := &model.Dial{}
+		mapstructure.Decode(event, status)
+		data = status
+
+	case "Newstate":
+		status := &model.Newstate{}
+		mapstructure.Decode(event, status)
+		data = status
+
+	case "Hangup":
+		status := &model.Hangup{}
+		mapstructure.Decode(event, status)
+		data = status
+
+	case "UserEvent":
+		switch event["UserEvent"] {
+		case "SetDND":
+			status := &model.SetDND{}
+			mapstructure.Decode(event, status)
+			data = status
+			if status.DNDStatus == "1" {
+				redis.ExtensionDNDSet(status.Exten, "yes")
+			} else {
+				redis.ExtensionDNDDel(status.Exten)
+			}
+			event["Event"] = event["UserEvent"]
+		case "WakeUpStatus":
+			status := &model.WakeUpStatus{}
+			mapstructure.Decode(event, status)
+			data = status
+			event["Event"] = event["UserEvent"]
+		default:
+			data = event
+		}
+	case "SuccessfulAuth":
+		// 获取上线 IP 地址
+		// !TODO 通过代理服务器地址不正确
+		status := &model.SuccessfulAuth{}
+		mapstructure.Decode(event, status)
+		status.GetAddress()
+		data = status
+
+	case "ContactStatus":
+		status := &model.ContactStatus{}
+		mapstructure.Decode(event, status)
+		status.GetAddress()
+		data = status
+
+	case "MessageWaiting":
+		status := &model.MessageWaiting{}
+		mapstructure.Decode(event, status)
+		status.GetExtension()
+		data = status
+		if status.Extension == "" {
+			lfshook.NewLogger().Warnf("MessageWaiting error %+v", event)
+			return
+		}
+	default:
+		// if !strings.Contains(event["Event"], "RTP") &&
+		// 	!strings.Contains(event["Event"], "RTCP") &&
+		// 	!strings.Contains(event["Event"], "VarSet") &&
+		// 	!strings.Contains(event["Event"], "Bridge") &&
+		// 	!strings.Contains(event["Event"], "New") &&
+		// 	!strings.Contains(event["Event"], "SuccessfulAuth") &&
+		// 	!strings.Contains(event["Event"], "ChallengeSent") {
+		// 	lfshook.NewLogger().Info(event)
+		// }
+	}
+
+	if data != nil {
+		// if configs.ConfigGlobal.FilterEventPushUrl != "" {
+		// 	httpclient.Post(data, configs.ConfigGlobal.FilterEventPushUrl)
+		// }
+		socketio.SocketIOServer.BroadcastToNamespace("", event["Event"], data)
+	}
+}
+
+func Connected() bool {
+	if AminInstance != nil {
+		return AminInstance.Connected()
+	}
+	return false
+}

+ 40 - 0
internal/app/ami/model/dial.go

@@ -0,0 +1,40 @@
+package model
+
+type Dial struct {
+	Event                 string `json:"event"`
+	Channel               string `json:"channel"`
+	ChannelState          string `json:"channelState"`
+	ChannelStateDesc      string `json:"channelStateDesc"`
+	CallerIDNum           string `json:"callerIDNum"`
+	CallerIDName          string `json:"callerIDName"`
+	ConnectedLineNum      string `json:"connectedLineNum"`
+	ConnectedLineName     string `json:"connectedLineName"`
+	AccountCode           string `json:"accountCode"`
+	Context               string `json:"context"`
+	Exten                 string `json:"exten"`
+	Priority              string `json:"priority"`
+	Uniqueid              string `json:"uniqueid"`
+	Linkedid              string `json:"linkedid"`
+	DestChannel           string `json:"destChannel"`
+	DestChannelState      string `json:"destChannelState"`
+	DestChannelStateDesc  string `json:"destChannelStateDesc"`
+	DestCallerIDNum       string `json:"destCallerIDNum"`
+	DestCallerIDName      string `json:"destCallerIDName"`
+	DestConnectedLineNum  string `json:"destConnectedLineNum"`
+	DestConnectedLineName string `json:"destConnectedLineName"`
+	DestLanguage          string `json:"destLanguage"`
+	DestAccountCode       string `json:"destAccountCode"`
+	DestContext           string `json:"destContext"`
+	DestExten             string `json:"destExten"`
+	DestPriority          string `json:"destPriority"`
+	DestUniqueid          string `json:"destUniqueid"`
+	DestLinkedid          string `json:"destLinkedid"`
+	DialString            string `json:"dialString"`
+	Forward               string `json:"forward"`
+}
+
+type DialBeign Dial
+
+type DialEnd Dial
+
+type DialState Dial

+ 100 - 0
internal/app/ami/model/event.go

@@ -0,0 +1,100 @@
+package model
+
+import "strings"
+
+type PresenceStateChange struct {
+	Event      string `json:"event"`
+	Status     string `json:"status"`
+	Message    string `json:"message"`
+	Presentity string `json:"presentity"`
+	Extension  string `json:"extension"`
+	DndStatus  string `json:"dndStatus"`
+}
+
+type Hangup struct {
+	AccountCode       string `json:"accountCode"`
+	CallerIDName      string `json:"callerIDName"`
+	CallerIDNum       string `json:"callerIDNumber"`
+	Cause             string `json:"cause"`
+	Causetxt          string `json:"causetxt"`
+	Channel           string `json:"channel"`
+	ChannelState      string `json:"channelState"`
+	ChannelStateDesc  string `json:"channelStateDesc"`
+	ConnectedLineName string `json:"connectedLineName"`
+	ConnectedLineNum  string `json:"connectedLineNumber"`
+	Context           string `json:"context"`
+	Event             string `json:"event"`
+	Exten             string `json:"exten"`
+	Linkedid          string `json:"linkedid"`
+	Priority          string `json:"priority"`
+	Privilege         string `json:"privilege"`
+	Timestamp         string `json:"timestamp"`
+	Uniqueid          string `json:"uniqueid"`
+}
+
+type Newstate struct {
+	AccountCode       string `json:"accountCode"`
+	CallerIDName      string `json:"callerIDName"`
+	CallerIDNum       string `json:"callerIDNumber"`
+	Channel           string `json:"channel"`
+	ChannelState      string `json:"channelState"`
+	ChannelStateDesc  string `json:"channelStateDesc"`
+	ConnectedLineName string `json:"connectedLineName"`
+	ConnectedLineNum  string `json:"connectedLineNumber"`
+	Context           string `json:"context"`
+	Event             string `json:"event"`
+	Exten             string `json:"exten"`
+	Language          string `json:"language"`
+	Linkedid          string `json:"linkedid"`
+	Priority          string `json:"priority"`
+	Privilege         string `json:"privilege"`
+	Timestamp         string `json:"timestamp"`
+	Uniqueid          string `json:"uniqueid"`
+}
+
+type SuccessfulAuth struct {
+	Event         string `json:"event"`
+	AccountID     string `json:"accountID"`
+	RemoteAddress string `json:"remoteAddress"`
+	Address       string `json:"address"`
+}
+
+func (event *SuccessfulAuth) GetAddress() {
+	data := strings.Split(event.RemoteAddress, "/")
+	if len(data) == 4 {
+		event.Address = data[2]
+	} else {
+		event.Address = event.RemoteAddress
+	}
+}
+
+type ContactStatus struct {
+	Event         string `json:"event"`
+	URI           string `json:"url"`
+	ContactStatus string `json:"contactStatus"`
+	Address       string `json:"address"`
+	AOR           string `json:"extension"`
+	RoundtripUsec string `json:"roundtripUsec"`
+}
+
+func (event *ContactStatus) GetAddress() {
+	data := strings.Split(event.URI, "@")
+	if len(data) == 2 {
+		event.Address = strings.Split(data[1], ":")[0]
+	}
+}
+
+type MessageWaiting struct {
+	Event     string `json:"event"`
+	Extension string `json:"extension"`
+	New       string `json:"new"`
+	Old       string `json:"old"`
+	Mailbox   string `json:"mainbox"`
+}
+
+func (event *MessageWaiting) GetExtension() {
+	data := strings.Split(event.Mailbox, "@")
+	if len(data) == 2 {
+		event.Extension = data[0]
+	}
+}

+ 80 - 0
internal/app/ami/model/meetme.go

@@ -0,0 +1,80 @@
+package model
+
+import (
+	"encoding/json"
+	"pms-api-go/pkg/utils"
+)
+
+type MeetMeListRooms struct {
+	Activity   string `json:"activity"`
+	Conference string `json:"conference"`
+	Creation   string `json:"creation"`
+	Event      string `json:"event"`
+	Locked     string `json:"locked"`
+	Marked     string `json:"marked"`
+	Parties    string `json:"parties"`
+}
+
+type MeetmeList struct {
+	Admin             string `json:"admin"`
+	CallerIDName      string `json:"callerIDName"`
+	CallerIDNum       string `json:"callerIDNumber"`
+	Channel           string `json:"channel"`
+	Conference        string `json:"conference"`
+	ConnectedLineName string `json:"connectedLineName"`
+	ConnectedLineNum  string `json:"connectedLineNumber"`
+	Event             string `json:"event"`
+	MarkedUser        string `json:"markedUser"`
+	Muted             string `json:"muted"`
+	Role              string `json:"role"`
+	Talking           string `json:"talking"`
+	UserNumber        string `json:"user"`
+}
+
+func (data *MeetmeList) MarshalJSON() ([]byte, error) {
+	type Alias MeetmeList
+
+	muted := utils.YesToOn(data.Muted)
+	talking := utils.YesToOn(data.Talking)
+
+	return json.Marshal(&struct {
+		Talking string `json:"talking"`
+		Muted   string `json:"muted"`
+		*Alias
+	}{
+		Muted:   muted,
+		Talking: talking,
+		Alias:   (*Alias)(data),
+	})
+}
+
+type MeetmeEnd struct {
+	Event     string `json:"event"`
+	Meetme    string `json:"meetme"`
+	Timestamp string `json:"timestamp"`
+}
+
+type Meetme struct {
+	CallerIDName      string `json:"callerIDName"`
+	CallerIDNum       string `json:"callerIDNumber"`
+	Channel           string `json:"channel"`
+	ChannelState      string `json:"channelState"`
+	ConnectedLineName string `json:"connectedLineName"`
+	ConnectedLineNum  string `json:"connectedLineNumber"`
+	Duration          string `json:"duration"`
+	Context           string `json:"context"`
+	Event             string `json:"event"`
+	Exten             string `json:"exten"`
+	Meetme            string `json:"meetme"`
+	Priority          string `json:"priority"`
+	Privilege         string `json:"privilege"`
+	Timestamp         string `json:"timestamp"`
+	User              string `json:"user"`
+	Status            string `json:"status"`
+}
+
+type MeetmeJoin Meetme
+type MeetmeLeave Meetme
+type MeetmeMute Meetme
+type MeetmeTalkRequest Meetme
+type MeetmeTalking Meetme

+ 210 - 0
internal/app/ami/model/park.go

@@ -0,0 +1,210 @@
+package model
+
+type Parkinglot struct {
+	Name       string `json:"name"`
+	StartSpace string `json:"startSpace"`
+	StopSpace  string `json:"stopSpace"`
+	Timeout    string `json:"timeout"`
+}
+
+// Event:ParkedCall
+// ParkeeAccountCode:
+// ParkeeCallerIDName:106
+// ParkeeCallerIDNum:106
+// ParkeeChannel:PJSIP/106-00000171
+// ParkeeChannelState:6
+// ParkeeChannelStateDesc:Up
+// ParkeeConnectedLineName:120
+// ParkeeConnectedLineNum:120
+// ParkeeContext:macro-stdexten
+// ParkeeExten:s
+// ParkeeLanguage:en
+// ParkeeLinkedid:1621821838.695
+// ParkeePriority:26
+// ParkeeUniqueid:1621821838.695
+// ParkerDialString:PJSIP/106
+// ParkingDuration:113
+// ParkingSpace:41
+// ParkingTimeout:487
+// Parkinglot:default
+
+type ParkedCall struct {
+	Event                   string `json:"event"`
+	ParkeeCallerIDName      string `json:"parkeeCallerIDName"`
+	ParkeeCallerIDNum       string `json:"parkeeCallerIDNumber"`
+	ParkeeChannel           string `json:"parkeeChannel"`
+	ParkeeChannelState      string `json:"parkeeChannelState"`
+	ParkeeChannelStateDesc  string `json:"parkeeChannelStateDesc"`
+	ParkeeConnectedLineName string `json:"parkeeConnectedLineName"`
+	ParkeeConnectedLineNum  string `json:"parkeeConnectedLineNumber"`
+	ParkeeContext           string `json:"parkeeContext"`
+	ParkeeExten             string `json:"parkeeExten"`
+	ParkeeLanguage          string `json:"parkeeLanguage"`
+	ParkeeLinkedid          string `json:"parkeeLinkedid"`
+	ParkeePriority          string `json:"parkeePriority"`
+	ParkeeUniqueid          string `json:"parkeeUniqueid"`
+	ParkerDialString        string `json:"parkerDialString"`
+	ParkingDuration         string `json:"parkingDuration"`
+	ParkingSpace            string `json:"parkingSpace"`
+	ParkingTimeout          string `json:"parkingTimeout"`
+	Parkinglot              string `json:"parkinglot"`
+}
+
+// Event:UnParkedCall
+// ParkeeAccountCode: ParkeeCallerIDName:106
+// ParkeeCallerIDNum:106
+// ParkeeChannel:PJSIP/106-000001a0
+// ParkeeChannelState:6
+// ParkeeChannelStateDesc:Up
+// ParkeeConnectedLineName:120
+// ParkeeConnectedLineNum:120
+// ParkeeContext:macro-stdexten
+// ParkeeExten:s
+// ParkeeLanguage:en
+// ParkeeLinkedid:1621835298.828
+// ParkeePriority:26
+// ParkeeUniqueid:1621835298.828
+// ParkerDialString:PJSIP/106
+// ParkingDuration:12
+// ParkingSpace:41
+// ParkingTimeout:588
+// Parkinglot:default
+// Privilege:call,all
+// RetrieverAccountCode:
+// RetrieverCallerIDName:119
+// RetrieverCallerIDNum:119
+// RetrieverChannel:PJSIP/119-000001a2
+// RetrieverChannelState:6
+// RetrieverChannelStateDesc:Up
+// RetrieverConnectedLineName:<unknown>
+// RetrieverConnectedLineNum:<unknown>
+// RetrieverContext:DialPlan1
+// RetrieverExten:41
+// RetrieverLanguage:en
+// RetrieverLinkedid:1621835340.830
+// RetrieverPriority:1
+// RetrieverUniqueid:1621835340.830
+// Timestamp:1621835340.214037
+
+type UnParkedCall struct {
+	Event                   string `json:"event"`
+	ParkeeCallerIDName      string `json:"parkeeCallerIDName"`
+	ParkeeCallerIDNum       string `json:"parkeeCallerIDNumber"`
+	ParkeeChannel           string `json:"parkeeChannel"`
+	ParkeeChannelState      string `json:"parkeeChannelState"`
+	ParkeeChannelStateDesc  string `json:"parkeeChannelStateDesc"`
+	ParkeeConnectedLineName string `json:"parkeeConnectedLineName"`
+	ParkeeConnectedLineNum  string `json:"parkeeConnectedLineNumber"`
+	ParkeeContext           string `json:"parkeeContext"`
+	ParkeeExten             string `json:"parkeeExten"`
+	ParkeeLanguage          string `json:"parkeeLanguage"`
+	ParkeeLinkedid          string `json:"parkeeLinkedid"`
+	ParkeePriority          string `json:"parkeePriority"`
+	ParkeeUniqueid          string `json:"parkeeUniqueid"`
+	ParkerDialString        string `json:"parkerDialString"`
+	ParkingDuration         string `json:"parkingDuration"`
+	ParkingSpace            string `json:"parkingSpace"`
+	ParkingTimeout          string `json:"parkingTimeout"`
+	Parkinglot              string `json:"parkinglot"`
+}
+
+// Event: ParkedCallTimeOut
+// ParkeeAccountCode: <value>
+// ParkeeCallerIDName: <value>
+// ParkeeCallerIDNum: <value>
+// ParkeeChannel: <value>
+// ParkeeChannelState: <value>
+// ParkeeChannelStateDesc: <value>
+// ParkeeConnectedLineName: <value>
+// ParkeeConnectedLineNum: <value>
+// ParkeeContext: <value>
+// ParkeeExten: <value>
+// ParkeeLanguage: <value>
+// ParkeeLinkedid: <value>
+// ParkeePriority: <value>
+// ParkeeUniqueid: <value>
+// ParkerAccountCode: <value>
+// ParkerCallerIDName: <value>
+// ParkerCallerIDNum: <value>
+// ParkerChannel: <value>
+// ParkerChannelState: <value>
+// ParkerChannelStateDesc: <value>
+// ParkerConnectedLineName: <value>
+// ParkerConnectedLineNum: <value>
+// ParkerContext: <value>
+// ParkerDialString: <value>
+// ParkerExten: <value>
+// ParkerLanguage: <value>
+// ParkerLinkedid: <value>
+// ParkerPriority: <value>
+// ParkerUniqueid: <value>
+// ParkingDuration: <value>
+// ParkingSpace: <value>
+// ParkingTimeout: <value>
+// Parkinglot: <value>
+type ParkedCallTimeOut struct {
+	Event                   string `json:"event"`
+	ParkeeCallerIDName      string `json:"parkeeCallerIDName"`
+	ParkeeCallerIDNum       string `json:"parkeeCallerIDNumber"`
+	ParkeeChannel           string `json:"parkeeChannel"`
+	ParkeeChannelState      string `json:"parkeeChannelState"`
+	ParkeeChannelStateDesc  string `json:"parkeeChannelStateDesc"`
+	ParkeeConnectedLineName string `json:"parkeeConnectedLineName"`
+	ParkeeConnectedLineNum  string `json:"parkeeConnectedLineNumber"`
+	ParkeeContext           string `json:"parkeeContext"`
+	ParkeeExten             string `json:"parkeeExten"`
+	ParkeeLanguage          string `json:"parkeeLanguage"`
+	ParkeeLinkedid          string `json:"parkeeLinkedid"`
+	ParkeePriority          string `json:"parkeePriority"`
+	ParkeeUniqueid          string `json:"parkeeUniqueid"`
+	ParkerDialString        string `json:"parkerDialString"`
+	ParkingDuration         string `json:"parkingDuration"`
+	ParkingSpace            string `json:"parkingSpace"`
+	ParkingTimeout          string `json:"parkingTimeout"`
+	Parkinglot              string `json:"parkinglot"`
+}
+
+// Event: ParkedCallGiveUp
+// ParkeeAccountCode:
+// ParkeeCallerIDName: 106
+// ParkeeCallerIDNum: 106
+// ParkeeChannel: PJSIP/106-00000191
+// ParkeeChannelState: 6
+// ParkeeChannelStateDesc: Up
+// ParkeeConnectedLineName: 120
+// ParkeeConnectedLineNum: 120
+// ParkeeContext: macro-stdexten
+// ParkeeExten: s
+// ParkeeLanguage: en
+// ParkeeLinkedid: 1621827325.781
+// ParkeePriority: 26
+// ParkeeUniqueid: 1621827325.781
+// ParkerDialString: PJSIP/106
+// ParkingDuration: 48
+// ParkingSpace: 41
+// ParkingTimeout: 552
+// Parkinglot: default
+// Privilege: call,all
+// Timestamp: 1621827421.460917
+
+type ParkedCallGiveUp struct {
+	Event                   string `json:"event"`
+	ParkeeCallerIDName      string `json:"parkeeCallerIDName"`
+	ParkeeCallerIDNum       string `json:"parkeeCallerIDNumber"`
+	ParkeeChannel           string `json:"parkeeChannel"`
+	ParkeeChannelState      string `json:"parkeeChannelState"`
+	ParkeeChannelStateDesc  string `json:"parkeeChannelStateDesc"`
+	ParkeeConnectedLineName string `json:"parkeeConnectedLineName"`
+	ParkeeConnectedLineNum  string `json:"parkeeConnectedLineNumber"`
+	ParkeeContext           string `json:"parkeeContext"`
+	ParkeeExten             string `json:"parkeeExten"`
+	ParkeeLanguage          string `json:"parkeeLanguage"`
+	ParkeeLinkedid          string `json:"parkeeLinkedid"`
+	ParkeePriority          string `json:"parkeePriority"`
+	ParkeeUniqueid          string `json:"parkeeUniqueid"`
+	ParkerDialString        string `json:"parkerDialString"`
+	ParkingDuration         string `json:"parkingDuration"`
+	ParkingSpace            string `json:"parkingSpace"`
+	ParkingTimeout          string `json:"parkingTimeout"`
+	Parkinglot              string `json:"parkinglot"`
+}

+ 90 - 0
internal/app/ami/model/queue.go

@@ -0,0 +1,90 @@
+package model
+
+//QueueParams 队列参数
+type QueueParams struct {
+	Queue            string         `json:"queue"`
+	Max              string         `json:"max"`
+	Strategy         string         `json:"strategy"`
+	Calls            string         `json:"calls"`
+	Holdtime         string         `json:"holdtime"`
+	TalkTime         string         `json:"talkTime"`
+	Completed        string         `json:"completed"`
+	Abandoned        string         `json:"abandoned"`
+	ServiceLevel     string         `json:"serviceLevel"`
+	ServicelevelPerf string         `json:"servicelevelPerf"`
+	Weight           string         `json:"weight"`
+	Members          []*QueueMember `json:"members"`
+	Entrys           []*QueueEntry  `json:"entrys"`
+}
+
+//QueueMember 队列成员
+type QueueMember struct {
+	Queue          string `json:"queue"`
+	Name           string `json:"name"`
+	Location       string `json:"location"`
+	StateInterface string `json:"stateInterface"`
+	Membership     string `json:"membership"`
+	Penalty        string `json:"penalty"`
+	CallsTaken     string `json:"callsTaken"`
+	LastCall       string `json:"lastCall"`
+	InCall         string `json:"inCall"`
+	Status         string `json:"status"`
+	Paused         string `json:"paused"`
+	PausedReason   string `json:"pausedReason"`
+}
+
+type QueueEntry struct {
+	CallerIDName      string `json:"callerIDName"`
+	CallerIDNum       string `json:"callerIDNumber"`
+	Channel           string `json:"channel"`
+	ConnectedLineName string `json:"connectedLineName"`
+	ConnectedLineNum  string `json:"connectedLineNumber"`
+	Event             string `json:"event"`
+	Position          string `json:"position"`
+	Priority          string `json:"priority"`
+	Queue             string `json:"queue"`
+	Uniqueid          string `json:"uniqueid"`
+	Wait              string `json:"wait"`
+}
+
+type QueueCallerJoin struct {
+	CallerIDName      string `json:"callerIDName"`
+	CallerIDNum       string `json:"callerIDNumber"`
+	Channel           string `json:"channel"`
+	ConnectedLineName string `json:"connectedLineName"`
+	ConnectedLineNum  string `json:"connectedLineNumber"`
+	Event             string `json:"event"`
+	Position          string `json:"position"`
+	Priority          string `json:"priority"`
+	Queue             string `json:"queue"`
+	Uniqueid          string `json:"uniqueid"`
+	Count             string `json:"count"`
+}
+
+type QueueCallerAbandon struct {
+	CallerIDName      string `json:"callerIDName"`
+	CallerIDNum       string `json:"callerIDNumber"`
+	Channel           string `json:"channel"`
+	ConnectedLineName string `json:"connectedLineName"`
+	ConnectedLineNum  string `json:"connectedLineNumber"`
+	Event             string `json:"event"`
+	Position          string `json:"position"`
+	Priority          string `json:"priority"`
+	Queue             string `json:"queue"`
+	Uniqueid          string `json:"uniqueid"`
+	HoldTime          string `json:"holdTime"`
+}
+
+type QueueCallerLeave struct {
+	CallerIDName      string `json:"callerIDName"`
+	CallerIDNum       string `json:"callerIDNumber"`
+	Channel           string `json:"channel"`
+	ConnectedLineName string `json:"connectedLineName"`
+	ConnectedLineNum  string `json:"connectedLineNumber"`
+	Event             string `json:"event"`
+	Position          string `json:"position"`
+	Priority          string `json:"priority"`
+	Queue             string `json:"queue"`
+	Uniqueid          string `json:"uniqueid"`
+	Count             string `json:"count"`
+}

+ 119 - 0
internal/app/ami/model/sip.go

@@ -0,0 +1,119 @@
+package model
+
+import "encoding/json"
+
+// Event: EndpointList
+// ObjectType: endpoint
+// ObjectName: www
+// Transport: UDP
+// Aor: www
+// Auths: www
+// OutboundAuths: www
+// Contacts:
+// DeviceState: Unavailable
+// ActiveChannels:
+
+// Event: EndpointList
+// ObjectType: endpoint
+// ObjectName: 899
+// Transport:
+// Aor: 899
+// Auths: 899
+// OutboundAuths:
+// Contacts:
+// DeviceState: Unavailable
+// ActiveChannels:
+
+type EndpointList struct {
+	Event          string `json:"Event"`
+	ObjectType     string `json:"ObjectType"`
+	ObjectName     string `json:"ObjectName"`
+	Transport      string `json:"Transport"`
+	Aor            string `json:"Aor"`
+	Auths          string `json:"Auths"`
+	OutboundAuths  string `json:"OutboundAuths"`
+	Contacts       string `json:"Contacts"`
+	DeviceState    string `json:"DeviceState"`
+	ActiveChannels string `json:"ActiveChannels"`
+}
+
+// Event: PeerEntry
+// Channeltype: SIP
+// ObjectName: 875
+// ChanObjectType: peer
+// IPaddress: -none-
+// IPport: 0
+// Dynamic: yes
+// AutoForcerport: no
+// Forcerport: yes
+// AutoComedia: no
+// Comedia: yes
+// VideoSupport: no
+// TextSupport: no
+// ACL: no
+// Status: UNKNOWN
+// RealtimeDevice: no
+// Description:
+// Accountcode:
+type PeerEntry struct {
+	Event          string `json:"Event"`
+	Channeltype    string `json:"Channeltype"`
+	ObjectName     string `json:"ObjectName"`
+	ChanObjectType string `json:"ChanObjectType"`
+	IPaddress      string `json:"IPaddress"`
+	IPport         string `json:"IPport"`
+	Dynamic        string `json:"Dynamic"`
+	AutoForcerport string `json:"AutoForcerport"`
+	Forcerport     string `json:"Forcerport"`
+	AutoComedia    string `json:"AutoComedia"`
+	Status         string `json:"Status"`
+	RealtimeDevice string `json:"RealtimeDevice"`
+}
+
+// Event: ContactList
+// ObjectType: contact
+// ObjectName: 120;@199f77999ab9abe1ec7fbe48e0b51053
+// ViaAddr: 192.168.17.183
+// QualifyTimeout: 30.000000
+// CallId: DBnHD24ea-7MTKkYj9ewfafR0KAmWv4z
+// RegServer:
+// PruneOnBoot: no
+// Path:
+// Endpoint: 120
+// ViaPort: 60442
+// AuthenticateQualify: no
+// Uri: sip:120@192.168.17.183:60442;ob
+// QualifyFrequency: 300
+// UserAgent: Telephone 1.2.6
+// ExpirationTime: 1620818122
+// OutboundProxy:
+// Status: Reachable
+// RoundtripUsec: 85040
+type ContactList struct {
+	Event         string `json:"Event"`
+	ObjectName    string `json:"ObjectName"`
+	ViaAddr       string `json:"ViaAddr"`
+	Uri           string `json:"Uri"`
+	RoundtripUsec string `json:"RoundtripUsec"`
+}
+
+func (list ContactList) String() string {
+	data, _ := json.Marshal(list)
+	return string(data)
+}
+
+// 分机状态
+// Event: ExtensionStatus
+// Exten: 118
+// Context: channelhints_exten
+// Hint: PJSIP/118,CustomPresence:118
+// Status: 4
+// StatusText: Unavailable
+type ExtensionStatus struct {
+	Event      string `json:"event"`
+	Exten      string `json:"extension"`
+	Context    string `json:"context"`
+	Hint       string `json:"hint"`
+	Status     string `json:"status"`
+	StatusText string `json:"statusText"`
+}

+ 15 - 0
internal/app/ami/model/userevent.go

@@ -0,0 +1,15 @@
+package model
+
+type SetDND struct {
+	Context   string `json:"context"`
+	Exten     string `json:"exten"`
+	UserEvent string `json:"event"`
+	DNDStatus string `json:"dndStatus"`
+}
+
+type WakeUpStatus struct {
+	Status            string `json:"status"`
+	ResTime           string `json:"resTime"`
+	ConnectedLineName string `json:"connectedLineName"`
+	ConnectedLineNum  string `json:"connectedLineNum"`
+}

+ 132 - 0
internal/app/ami/vtiger.go

@@ -0,0 +1,132 @@
+package ami
+
+import (
+	"fmt"
+	"pms-api-go/pkg/configs"
+	"pms-api-go/pkg/httpclient"
+	"pms-api-go/pkg/lfshook"
+	"strings"
+
+	"gopkg.in/ini.v1"
+)
+
+func phoneCalls(event map[string]string) {
+	fmt.Println("=========================================== ")
+	fmt.Println("event[] = ", event)
+	fmt.Println("event = ", event["Event"])
+	fmt.Println("AMIPushUrl = ", configs.PushConfigValue.AMIPushUrl)
+	fmt.Println("Channel = ", event["Channel"])
+
+	var callDest string
+	if event["Channel"] != "" {
+		callDest = strings.Split(strings.Split(event["Channel"], "/")[1], "-")[0]
+		fmt.Println("callDest = ", callDest)
+	}
+	// /* 20241128 vtiger crm 对应 ============================================================
+	// if configs.PushConfigValue.AMIPushUrl != "" {
+	// 	for _, eventName := range configs.PushConfigValue.AMIEvents {
+	// 		if eventName == event["Event"] {
+	// 			go httpclient.Post(event, configs.PushConfigValue.AMIPushUrl)
+	// 			break
+	// 		}
+	// 	}
+	// }
+
+	// var callId string
+	var callId = "10000007"                                                                                       // 测试用,每次测试 +1
+	recordingUrl := "https%3A%2F%2Fs3.amazonaws.com%2Frecordings_2013%2F8e522852-72aa-11e5-ab5f-842b2b021118.mp3" // 测试用
+	event["Exten"] = "123456789"                                                                                  // 测试用
+	fmt.Println("Exten = ", event["Exten"])
+
+	// 读取vtiger配置文件
+	confPath := "/etc/asterisk/pms_api.conf"
+	cfg, err := ini.Load(confPath)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return
+	}
+	VTIGER_URL := cfg.Section("general").Key("vtigerUrl").String()
+	if VTIGER_URL == "" {
+		lfshook.NewLogger().Error("/etc/asterisk/pms_api.conf not set vtigerUrl")
+		return
+	}
+
+	var getURL string
+	// 呼叫发起事件
+	if event["Event"] == "DialBegin" { // "DialBegin" "Newchannel"
+		// https://zycoo1.od2.vtiger.com/modules/PhoneCalls/callbacks/Generic.php?from=147258369&to=12300001&event=call_initiated&call_id=12345678&direction=inbound
+		// https://zycoo1.od2.vtiger.com/modules/PhoneCalls/callbacks/Generic.php?from=86147258369&event=call_initiated&call_id=12345678  // 可以省去部分参数
+		// getURL := fmt.Sprintf("%s/modules/PhoneCalls/callbacks/Generic.php?from=%s&event=call_initiated&call_id=%s", VTIGER_URL, event["Exten"], callId)
+		if callDest == "" {
+			getURL = fmt.Sprintf("%s/modules/PhoneCalls/callbacks/Generic.php?from=%s&event=call_initiated&call_id=%s&direction=inbound", VTIGER_URL, event["Exten"], callId) // 20241212 省去参数 to
+		} else {
+			getURL = fmt.Sprintf("%s/modules/PhoneCalls/callbacks/Generic.php?from=%s&to=%s&event=call_initiated&call_id=%s&direction=inbound", VTIGER_URL, event["Exten"], callDest, callId) // 20241212 不省参数
+		}
+		// fmt.Println("getURL = ", getURL)
+		// go httpclient.Get(getURL)
+		go httpclient.ApiKeyGet(getURL) // 后面要不要把用户密码给传过去
+		/*
+			resp := httpclient.ApiKeyGet(getURL)
+			// 读取请求后的响应
+			data, err := ioutil.ReadAll(resp.Body)
+			if err != nil {
+				fmt.Println("读取请求后的响应时发生错误:", err)
+				return
+			}
+			// 打印请求后的响应
+			fmt.Printf("data = %+v\n", string(data))
+		*/
+
+		// 呼叫已连接事件
+	} else if event["Event"] == "BridgeEnter" { // "Newstate" "BridgeEnter"
+		// https://zycoo1.od2.vtiger.com/modules/PhoneCalls/callbacks/Generic.php?from=86147258369&to=12300001&event=call_connected&call_id=12345678
+		// https://zycoo1.od2.vtiger.com/modules/PhoneCalls/callbacks/Generic.php?event=call_connected&call_id=12345678  // 可以省去部分参数
+		getURL = fmt.Sprintf("%s/modules/PhoneCalls/callbacks/Generic.php?event=call_connected&call_id=%s", VTIGER_URL, callId)
+		// getURL = fmt.Sprintf("%s/modules/PhoneCalls/callbacks/Generic.php?", VTIGER_URL)
+		if event["Exten"] != "" {
+			getURL = fmt.Sprintf("%sfrom=%s", getURL, event["Exten"])
+		}
+		if callDest != "" {
+			getURL = fmt.Sprintf("%s&to=%s", getURL, callDest)
+		}
+		getURL = fmt.Sprintf("%s&event=call_connected&call_id=%s", getURL, callId)
+		// fmt.Println("getURL = ", getURL)
+		go httpclient.ApiKeyGet(getURL)
+
+		// 通话录音事件
+	} else if event["Event"] == "Cdr" {
+		// https://zycoo1.od2.vtiger.com/modules/PhoneCalls/callbacks/Generic.php?call_id=12345678&event=call_recording&recordingurl=https%3A%2F%2Fs3.amazonaws.com%2Frecordings_2013%2F8e522852-72aa-11e5-ab5f-842b2b021118.mp3
+		getURL = fmt.Sprintf("%s/modules/PhoneCalls/callbacks/Generic.php?call_id=%s&event=call_recording&recordingurl=%s", VTIGER_URL, callId, recordingUrl)
+		// fmt.Println("getURL = ", getURL)
+		go httpclient.ApiKeyGet(getURL)
+
+		// 呼叫转移事件
+		// Command:SET VARIABLE FORWARD_TYPE "busy"   Command:SET VARIABLE FORWARD_DEST "136"
+		// Command:SET VARIABLE FORWARD_TYPE "always"  Command:SET VARIABLE FORWARD_DEST "136"
+		// Command:SET VARIABLE FORWARD_TYPE "noan_busy"  Command:SET VARIABLE FORWARD_DEST "136"
+		// Command:SET VARIABLE FORWARD_TYPE "noan_unav"   Command:SET VARIABLE FORWARD_DEST "136"
+		// Command:SET VARIABLE FORWARD_TYPE "unavailable"  Command:SET VARIABLE FORWARD_DEST "136"
+	} else if strings.Contains(event["Command"], "FORWARD_DEST") {
+		// transferredNumber := strings.Split(event["Command"], "\"")[1] // 正式用
+		transferredNumber := "147258369" // 测试用
+		// fmt.Println("Command = ", event["Command"])
+		// fmt.Println("transferredNumber = ", transferredNumber)
+
+		// https://zycoo1.od2.vtiger.com/modules/PhoneCalls/callbacks/Generic.php?call_id=12345678&event=call_transfer&transferred_number=147258369
+		getURL = fmt.Sprintf("%s/modules/PhoneCalls/callbacks/Generic.php?call_id=%s&event=call_transfer&transferred_number=%s", VTIGER_URL, callId, transferredNumber)
+		// fmt.Println("getURL = ", getURL)
+		go httpclient.ApiKeyGet(getURL)
+
+		// 呼叫挂断事件
+	} else if event["Event"] == "HangupRequest" { // "HangupRequest" "Hangup"
+		// https://zycoo1.od2.vtiger.com/modules/PhoneCalls/callbacks/Generic.php?call_id=12345678&event=call_hangup
+		getURL = fmt.Sprintf("%s/modules/PhoneCalls/callbacks/Generic.php?call_id=%s&event=call_hangup", VTIGER_URL, callId)
+		// fmt.Println("getURL = ", getURL)
+		go httpclient.ApiKeyGet(getURL)
+	}
+	fmt.Println("getURL = ", getURL)
+
+	// 按编号搜索API 暂时用的以下接口
+	// group.GET("/vtiger/lookup", contactsInfo) // 按编号搜索
+
+}

+ 273 - 0
internal/app/ami/zoho.go

@@ -0,0 +1,273 @@
+package ami
+
+import (
+	"fmt"
+	"pms-api-go/pkg/configs"
+	"pms-api-go/pkg/httpclient"
+	"pms-api-go/pkg/lfshook"
+	"strconv"
+	"strings"
+	"time"
+
+	"gopkg.in/ini.v1"
+)
+
+var TimestampList = make(map[string]string)
+var StartTimeList = make(map[string]time.Time) // 事件中 Timestamp 转 StartTime 有问题,重新记录时间
+var CallTypeList = make(map[string]string)     // 临时记录 incoming 和 outgoing ,为incoming时: HangupRequest 中如果CallerIDNum为被叫号码,则未接听
+// var DialStatusList = make(map[string]string)     // 临时记录 DialStatus:ANSWER NOANSWER
+
+func callNotify(event map[string]string) {
+	fmt.Println("=========================================== ")
+	fmt.Println("event[] = ", event)
+	fmt.Println("event = ", event["Event"])
+	fmt.Println("AMIPushUrl = ", configs.PushConfigValue.AMIPushUrl)
+	fmt.Println("Channel = ", event["Channel"])
+
+	var callDest string
+	if event["Channel"] != "" {
+		callDest = strings.Split(strings.Split(event["Channel"], "/")[1], "-")[0]
+		fmt.Println("callDest = ", callDest)
+	}
+
+	// var callId string
+	var callId = "10000021" // 测试用,每次测试 +1
+	// event["Exten"] = "123456789" // 测试用
+	// fmt.Println("Exten = ", event["Exten"])
+
+	// 读取vtiger配置文件
+	confPath := "/etc/asterisk/pms_api.conf"
+	cfg, err := ini.Load(confPath)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return
+	}
+	ZOHO_URL := cfg.Section("general").Key("zohoUrl").String()
+	if ZOHO_URL == "" {
+		lfshook.NewLogger().Error("/etc/asterisk/pms_api.conf not set zohoUrl")
+		return
+	}
+
+	// var getURL string
+	fmt.Println("Context = ", event["Context"])
+	// 呼叫发起事件
+	// if event["Event"] == "DialBegin" && event["Context"] == "macro-stdexten" {
+	// if event["Event"] == "DialBegin" && strings.Compare(event["Context"], "macro-stdexten") == 0 {
+	if event["Event"] == "DialBegin" {
+
+		/* ===================先放这里测试用 ============================================================
+		// 记录开始时间
+		TimestampList[event["Uniqueid"]] = event["Timestamp"]
+		// StartTimeList[event["Uniqueid"]] = time.Now().Format("2006-01-02 15:04:09")
+		StartTimeList[event["Uniqueid"]] = time.Now()
+		/* ===================先放这里测试用 ============================================================
+		fmt.Printf("Uniqueid=%s\n", event["Uniqueid"])
+		fmt.Printf("Timestamp=%s\n", event["Timestamp"])
+
+		TimestampTmp, err := strconv.Atoi(event["Timestamp"]) // 不要
+		if err != nil {
+			fmt.Printf("转换出错: %v\n", err)
+			return
+		}
+		fmt.Printf("TimestampAtoi=%d\n", TimestampTmp)
+
+		TimestampTmp002, err := strconv.ParseFloat(event["Timestamp"], 64) // OK
+		if err != nil {
+			fmt.Printf("转换出错: %v\n", err)
+			return
+		}
+		fmt.Printf("TimestampParseFloat=%f\n", TimestampTmp002)
+
+		// * ===================================================================================== */
+
+		// Incoming Call - Ringing
+		if event["Context"] == "macro-stdexten" {
+			// if strings.Compare(event["Context"], "macro-stdexten") == 0 {  // OK
+			// https://www.zohoapis.com/phonebridge/v3/callnotify?type=received&state=ringing&id=10033&from=12300000001&to=123456789
+			// getURL := fmt.Sprintf("%s/phonebridge/v3/callnotify?type=received&state=ringing&id=%s&from=%s&to=%s", ZOHO_URL, callId, event["Exten"], callDest) // 取得from to 可能不准
+			getURL := fmt.Sprintf("%s/phonebridge/v3/callnotify?type=received&state=ringing&id=%s&from=%s&to=%s", ZOHO_URL, callId, event["DestConnectedLineNum"], event["DestCallerIDNum"])
+
+			fmt.Println("getURL = ", getURL)
+			go httpclient.ZohoGet(getURL)
+
+			// Outgoing Call - Ringing
+		} else if event["Context"] == "macro-trunkdial-failover" {
+			// https://www.zohoapis.com/phonebridge/v3/callnotify?type=dialed&state=ringing&id=10031&from=123456789&to=12300000001
+			getURL := fmt.Sprintf("%s/phonebridge/v3/callnotify?type=dialed&state=ringing&id=%s&from=%s&to=%s", ZOHO_URL, callId, event["DestConnectedLineNum"], event["DestCallerIDNum"])
+			fmt.Println("getURL = ", getURL)
+			go httpclient.ZohoGet(getURL)
+		}
+
+		// 呼叫已连接事件
+	} else if event["Event"] == "BridgeEnter" {
+		// Incoming Call - Answered
+		if event["Context"] == "macro-stdexten" {
+			// 记录开始时间
+			// TimestampList[event["Uniqueid"]] = event["Timestamp"] // Uniqueid 有时通话也不合适  NG  // 都改为 Linkedid
+			// StartTimeList[event["Uniqueid"]] = time.Now()         // Uniqueid 有时通话也不合适  NG  // 都改为 Linkedid
+			TimestampList[event["Linkedid"]] = event["Timestamp"] // 呼入时设置 Uniqueid 不是 Linkedid
+			StartTimeList[event["Linkedid"]] = time.Now()         // 呼入时设置 Uniqueid 不是 Linkedid
+
+			// https://www.zohoapis.com/phonebridge/v3/callnotify?type=received&state=answered&id=10023&from=12300000001&to=123456789
+			getURL := fmt.Sprintf("%s/phonebridge/v3/callnotify?type=received&state=answered&id=%s&from=%s&to=%s", ZOHO_URL, callId, event["CallerIDNum"], event["ConnectedLineNum"])
+			fmt.Println("getURL = ", getURL)
+			go httpclient.ZohoGet(getURL)
+
+			// Outgoing Call - Answered
+		} else if event["Context"] == "macro-trunkdial-failover" && event["Priority"] == "1" { // 注意这里选择 Priority:1 的,便于确认 CallerIDNum,ConnectedLineNum
+			// 记录开始时间
+			// TimestampList[event["Uniqueid"]] = event["Timestamp"] // error 需要把 Uniqueid 改为 Linkedid
+			// StartTimeList[event["Uniqueid"]] = time.Now()         // error 需要把 Uniqueid 改为 Linkedid
+			TimestampList[event["Linkedid"]] = event["Timestamp"]
+			StartTimeList[event["Linkedid"]] = time.Now()
+
+			// https: //www.zohoapis.com/phonebridge/v3/callnotify?type=dialed&state=answered&id=10003&from=123456789&to=12300000001
+			// getURL := fmt.Sprintf("%s/phonebridge/v3/callnotify?type=dialed&state=answered&id=%s&from=%s&to=%s", ZOHO_URL, callId, event["CallerIDNum"], event["ConnectedLineNum"])
+			getURL := fmt.Sprintf("%s/phonebridge/v3/callnotify?type=dialed&state=answered&id=%s&from=%s&to=%s", ZOHO_URL, callId, event["ConnectedLineNum"], event["CallerIDNum"])
+			fmt.Println("getURL = ", getURL)
+			go httpclient.ZohoGet(getURL)
+		}
+
+		// 呼叫挂断事件
+	} else if event["Event"] == "HangupRequest" {
+		/* ===================先放这里测试用 ============================================================
+		// 记录结束时间
+		// TimestampList[event["Uniqueid"]] = event["Timestamp"]
+		// startTimeStamp := int64(TimestampList[event["Uniqueid"]])
+
+		fmt.Println("startTimeStamp Str: ", TimestampList[event["Uniqueid"]])
+		// keyTmp := event["Uniqueid"]
+		// fmt.Println("keyTmp: ", keyTmp)
+		// fmt.Println("startTimeStamp Str: ", TimestampList[keyTmp])
+
+		startTimeStamp, err := strconv.ParseFloat(TimestampList[event["Uniqueid"]], 64) // OK
+		if err != nil {
+			fmt.Printf("startTimeStamp转换出错: %v\n", err)
+			return
+		}
+		fmt.Printf("startTimeStamp=%f\n", startTimeStamp)
+
+		endTimeStamp, err := strconv.ParseFloat(event["Timestamp"], 64) // OK
+		if err != nil {
+			fmt.Printf("endTimeStamp转换出错: %v\n", err)
+			return
+		}
+		fmt.Printf("endTimeStamp=%f\n", endTimeStamp)
+
+		durationTime := time.Duration(endTimeStamp-startTimeStamp) * time.Second
+		fmt.Println("durationTime=", durationTime)
+		// 删除记录时间
+		// delete(TimestampList, event["Uniqueid"])
+		// * ===================================================================================== */
+		// Incoming Call - Ended
+		// if event["Context"] == "macro-stdexten" {  // Context:DialPlan1
+		if event["Context"] != "macro-trunkdial-failover" {
+			// 获取时间
+			// durationTime := getDuration(event["Uniqueid"], event["Timestamp"])
+			// startTime := StartTimeList[event["Uniqueid"]].Format("2006-01-02 15:04:09") // 存储从string 改为 time.Time
+			durationTime := getDuration(event["Linkedid"], event["Timestamp"])
+			startTime := StartTimeList[event["Linkedid"]].Format("2006-01-02 15:04:09") // 存储从string 改为 time.Time
+			fmt.Println("startTime: ", startTime)
+
+			// https://www.zohoapis.com/phonebridge/v3/callnotify?type=received&state=ended&id=100&from=12300000001&to=123456789&start_time=2024-12-04 10:32:10&duration=223
+			// getURL := fmt.Sprintf("%s/phonebridge/v3/callnotify?type=received&state=ended&id=%s&from=%s&to=%s&start_time=%s&duration=%d", ZOHO_URL, callId, event["CallerIDNum"], event["ConnectedLineNum"], startTime, durationTime)
+			getURL := fmt.Sprintf("%s/phonebridge/v3/callnotify?type=received&state=ended&id=%s&from=%s&to=%s&start_time=%s&duration=%d", ZOHO_URL, callId, event["ConnectedLineNum"], event["CallerIDName"], startTime, durationTime)
+
+			fmt.Println("getURL = ", getURL)
+			go httpclient.ZohoGet(getURL)
+
+			// Outgoing Call - Ended
+		} else if event["Context"] == "macro-trunkdial-failover" {
+			// 获取时间
+			// durationTime := getDuration(event["Uniqueid"], event["Timestamp"])
+			durationTime := getDuration(event["Linkedid"], event["Timestamp"])
+			// /* ===================测试用 ============================================================
+			// startTime, _ := time.Parse("2006-01-02 15:04:05", TimestampList[event["Uniqueid"]]) // error   1734328213.774410  =>  0001-01-01 00:00:00 +0000 UTC
+			// startTime := StartTimeList[event["Uniqueid"]]
+			// startTime := StartTimeList[event["Uniqueid"]].Format("2006-01-02 15:04:09") // 存储从string 改为 time.Time
+			startTime := StartTimeList[event["Linkedid"]].Format("2006-01-02 15:04:09") // 存储从string 改为 time.Time
+			fmt.Println("startTime: ", startTime)
+			// durationTime002 := getDuration002(startTime)
+			// durationTime002 := time.Now().Sub(StartTimeList[event["Uniqueid"]]) // ok    durationTime002:  7.78408849s   // should use time.Since instead of time.Now().Sub (S1012)
+			// durationTime002 := time.Since(StartTimeList[event["Uniqueid"]]) // ok    durationTime002:  11.611233458s
+			// fmt.Println("durationTime002: ", durationTime002)
+			// * ===================================================================================== */
+
+			// 删除记录时间
+			delete(StartTimeList, event["Linkedid"])
+
+			// https://www.zohoapis.com/phonebridge/v3/callnotify?type=dailed&state=ended&id=10030&from=123456789&to=12300000001&start_time=2024-12-04 11:32:15&duration=325
+			getURL := fmt.Sprintf("%s/phonebridge/v3/callnotify?type=dailed&state=ended&id=%s&from=%s&to=%s&start_time=%s&duration=%d", ZOHO_URL, callId, event["CallerIDNum"], event["ConnectedLineNum"], startTime, durationTime)
+			// getURL := fmt.Sprintf("%s/phonebridge/v3/callnotify?type=dailed&state=ended&id=%s&from=%s&to=%s&start_time=%s&duration=%d", ZOHO_URL, callId, event["ConnectedLineNum"], event["CallerIDNum"], startTime, durationTime)
+			fmt.Println("getURL = ", getURL)
+			go httpclient.ZohoGet(getURL)
+		}
+	}
+	// fmt.Println("getURL = ", getURL)
+
+	// 测试验证下上面有没有删除,以防TimestampList越来越大
+	for k, v := range TimestampList {
+		fmt.Printf("===Linkedid:%s   Timestamp:%s===\n", k, v)
+	}
+
+	// 按编号搜索API 暂时用的以下接口
+	// group.GET("/vtiger/lookup", contactsInfo) // 按编号搜索
+
+}
+
+// 使用事件中event["Timestamp"]的值计算时间间隔
+// func getDuration(uniqueid string, timeStamp string) int {
+func getDuration(linkedid string, timeStamp string) int { // 都改为 Linkedid
+
+	fmt.Println("startTimeStamp Str: ", TimestampList[linkedid])
+	if TimestampList[linkedid] == "" || timeStamp == "" {
+		fmt.Printf("linkedid 或 timeStamp 为空\n")
+		return 9999
+	}
+
+	startTimeStamp, err := strconv.ParseFloat(TimestampList[linkedid], 64)
+	if err != nil {
+		fmt.Printf("startTimeStamp转换出错: %v\n", err)
+		return 9999
+	}
+	fmt.Printf("startTimeStamp=%f\n", startTimeStamp)
+
+	endTimeStamp, err := strconv.ParseFloat(timeStamp, 64)
+	if err != nil {
+		fmt.Printf("endTimeStamp转换出错: %v\n", err)
+		return 9999
+	}
+	fmt.Printf("endTimeStamp=%f\n", endTimeStamp)
+
+	durationTime := time.Duration(endTimeStamp-startTimeStamp) * time.Second
+	// fmt.Println("durationTime=", durationTime)                   // durationTime= 35s
+	// fmt.Printf("durationTime=%d\n", durationTime)                // durationTime=35000000000
+	// fmt.Printf("durationTime=%d\n", durationTime.Seconds())      // durationTime=%!d(float64=35)
+	// fmt.Printf("durationTime=%f\n", durationTime.Seconds())      // durationTime=35.000000
+	fmt.Printf("durationTime=%d\n", int(durationTime.Seconds())) // durationTime=35
+
+	// durationTime001 := time.Duration(endTimeStamp - startTimeStamp)
+	// fmt.Println("durationTime001=", durationTime001) // durationTime001= 35ns
+
+	// durationTime003 := time.Duration(endTimeStamp - startTimeStamp).Seconds()
+	// fmt.Println("durationTime003=", durationTime003) // durationTime003= 3.5e-08
+
+	// 删除记录时间
+	delete(TimestampList, linkedid)
+
+	// return durationTime
+	return int(durationTime.Seconds())
+}
+
+// // 程序中取时间计算间隔多少秒
+// func getDuration002(startTime string) time.Duration {
+// 	// durationTime := endTime.Sub(startTime)
+// 	startTime002, err := time.Parse("2006-01-02 15:04:05", startTime)
+// 	if err != nil {
+// 		fmt.Println("Error parsing time:", err)
+// 		return 0
+// 	}
+// 	durationTime := time.Now().Sub(startTime002)
+// 	return durationTime
+
+// }

+ 12 - 0
internal/app/http_server/api/index_disable.go

@@ -0,0 +1,12 @@
+//go:build !api
+// +build !api
+
+package api
+
+import (
+	"github.com/gin-gonic/gin"
+)
+
+func Enable(router *gin.Engine) {
+
+}

+ 71 - 0
internal/app/http_server/index.go

@@ -0,0 +1,71 @@
+package http_server
+
+import (
+	"fmt"
+	"pms-api-go/api/admin/auth"
+
+	// "pms-api-go/internal/app/http_server/bill" // 20230411 pms 删除
+	"pms-api-go/internal/app/http_server/pbx"
+	"pms-api-go/pkg/configs"
+	"pms-api-go/pkg/lfshook"
+	"syscall"
+
+	"github.com/gin-contrib/gzip"
+	"github.com/gin-gonic/gin"
+)
+
+func StartRoute() {
+	// 创建路由
+	router := gin.New()
+
+	// pms 对应 =======================
+	auth.AddAuth()
+
+	// vtiger 对应 =======================
+	auth.VtigerAddAuth()
+
+	// zoho 对应 =======================
+	auth.ZohoAddAuth()
+
+	// 配置中间件
+	// 使用 logrus 自定义 logger, 可以统一写入日志文件
+	// router.Use(gin.Logger())
+	// router.Use(log.LogrusLog)
+
+	router.Use(gzip.Gzip(gzip.DefaultCompression))
+	router.Use(gin.Recovery())
+
+	// pprof_info.Enable(router) // 20230419 pms 注释
+	// router.Use(weblog.WebLogMiddleware()) // 20230419 pms 注释
+
+	// 设置 static
+	/* 20230419 pms 删除 =======================================================================================================
+	socketio_client_tool.Enable(router)
+
+		if configs.ConfigGlobal.AllowOrigin != "" {
+			// web 页面需要
+			router.Use(middleware.CORSMiddleware(configs.ConfigGlobal.AllowOrigin))
+		} else {
+			// panel 需要
+			router.Use(middleware.CORSMiddleware("*"))
+		}
+
+		//API 路由
+		home.AddRouter(router)
+	* ========================================================================================================================= */
+
+	//pbx 功能模块
+	// static.Enable(router) // 20230411 pms 删除
+	pbx.Enable(router)
+	// webuser.Enable(router) // 20230419 pms 注释
+	// bill.Enable(router) // 20230411 pms 删除
+	// api.Enable(router) // 20230419 pms 注释
+
+	addr := fmt.Sprintf("%s:%d", configs.ConfigGlobal.WebHost, configs.ConfigGlobal.WebPort)
+	lfshook.NewLogger().Infof("http addr: %s", addr)
+	err := router.Run(addr)
+	if err != nil {
+		lfshook.NewLogger().Error(err.Error())
+		syscall.Kill(syscall.Getpid(), syscall.SIGINT)
+	}
+}

+ 13 - 0
internal/app/http_server/pbx/pbx.go

@@ -0,0 +1,13 @@
+//go:build pbx
+// +build pbx
+
+package pbx
+
+import (
+	"github.com/gin-gonic/gin"
+	"pms-api-go/api/admin"
+)
+
+func Enable(router *gin.Engine) {
+	admin.AddRouter(router)
+}

+ 11 - 0
internal/app/http_server/pbx/pbx_disable.go

@@ -0,0 +1,11 @@
+//+build !pbx
+
+package pbx
+
+import (
+	"github.com/gin-gonic/gin"
+)
+
+func Enable(router *gin.Engine) {
+
+}

+ 19 - 0
internal/app/http_server/socketio_client_tool/socketio_client_tool.go

@@ -0,0 +1,19 @@
+//go:build socketio_client_tool
+// +build socketio_client_tool
+
+package socketio_client_tool
+
+import (
+	"github.com/gin-gonic/gin"
+	"io/fs"
+	"net/http"
+	"pms-api-go/web"
+)
+
+/* 20230419 pms 删除 =======================================================================================================
+
+func Enable(router *gin.Engine) {
+	socketIOToolRoot, _ := fs.Sub(web.SocketIOClientTool, "www/socketio-client-tool")
+	router.StaticFS("/socketio-client-tool", http.FS(socketIOToolRoot))
+}
+* ========================================================================================================================= */

+ 15 - 0
internal/app/http_server/socketio_client_tool/socketio_client_tool_disable.go

@@ -0,0 +1,15 @@
+//go:build !socketio_client_tool
+// +build !socketio_client_tool
+
+package socketio_client_tool
+
+/* 20230419 pms 删除 =======================================================================================================
+import (
+	"github.com/gin-gonic/gin"
+)
+
+
+func Enable(router *gin.Engine) {
+
+}
+* ========================================================================================================================= */

+ 90 - 0
internal/app/index.go

@@ -0,0 +1,90 @@
+package app
+
+import (
+	"fmt"
+	"pms-api-go/internal/app/ami"
+	httpServer "pms-api-go/internal/app/http_server"
+	"pms-api-go/internal/app/mysql"
+	"pms-api-go/internal/app/redis"
+	socketio "pms-api-go/internal/app/socket_io"
+	"pms-api-go/internal/app/status"
+	"sync"
+
+	"pms-api-go/pkg/i18n"
+	"pms-api-go/pkg/lfshook"
+
+	"gopkg.in/ini.v1"
+)
+
+// var once sync.Once
+var once sync.Once // 20241128 vtiger crm 对应
+
+func StartApp() {
+	mysql.CreateDBInstance()
+	redis.CreateRedisInstance()
+	socketio.StartSocketIO()
+	i18n.InitBundle()
+	// initCronTask()
+
+	go httpServer.StartRoute()
+	/* 20230411 pms 删除 =======================================================================================================
+	go ami.StartAMI(func() {
+		lfshook.NewLogger().Info("ami callback function")
+		// 首次连接才进行初始化
+		once.Do(func() {
+			status.InitAsterisk()
+			// agi.InitPageingPlan(configs.ConfigGlobal.AsteriskPagingPath)
+		})
+	}, []func(event map[string]string){})
+	* ========================================================================================================================= */
+
+	// go agi.StartAGI()
+	// go systeminfo.SendMessage() // 20230411 pms 删除
+
+	// 启动插件程序
+	// go plugin.StartUP() // 20230411 pms 删除
+
+	// /* 20241128 vtiger crm 对应 =======================================================================================================
+	PmsConfPath := "/etc/asterisk/pms_api.conf"
+	pmsCfg, pmsErr := ini.Load(PmsConfPath)
+	if pmsErr != nil {
+		lfshook.NewLogger().Error(pmsErr)
+		return
+	}
+	PmsEnabled := pmsCfg.Section("general").Key("enable").String()
+	ApiType := pmsCfg.Section("general").Key("apitype").String()
+	if PmsEnabled == "" || ApiType == "" {
+		lfshook.NewLogger().Error("/etc/asterisk/pms_api.conf not set enabled or apitype")
+		return
+	}
+
+	/* **************************************************************************
+	// 先暂时都使用一个配置文件 pms_api.conf
+	VtigerConfPath := "/etc/asterisk/vtiger_api.conf"
+	vtigerCfg, vtigerErr := ini.Load(VtigerConfPath)
+	if vtigerErr != nil {
+		lfshook.NewLogger().Error(vtigerErr)
+		return
+	}
+	VtigerEnabled := vtigerCfg.Section("general").Key("enabled").String()
+	if VtigerEnabled == "" {
+		lfshook.NewLogger().Error("/etc/asterisk/vtiger_api.conf not set enabled")
+		return
+	}
+
+	if VtigerEnabled == "yes" && PmsEnabled == "no" { // pms基本是数据库操作,但是有个叫醒,会产生事件,所以当 pms 开启时,不能开启 vtiger
+	// ************************************************************************** */
+	fmt.Println("ApiType = ", ApiType)
+	if ApiType == "CRM_vtiger" || ApiType == "CRM_zoho" {
+		go ami.StartAMI(func() {
+			lfshook.NewLogger().Info("ami callback function")
+			// 首次连接才进行初始化
+			once.Do(func() {
+				status.InitAsterisk()
+				// agi.InitPageingPlan(configs.ConfigGlobal.AsteriskPagingPath)
+			})
+		}, []func(event map[string]string){}, ApiType)
+	}
+
+	// * ========================================================================================================================= */
+}

+ 109 - 0
internal/app/mysql/extension.go

@@ -0,0 +1,109 @@
+package mysql
+
+import (
+	"pms-api-go/pkg/lfshook"
+)
+
+/* *****************************************************************************************
+// GetExtensionRangeInfoByName
+// 根据名字获取号码取值范围
+func GetExtensionRangeInfoByName(name string) map[string]string {
+	data, err := DBOrmInstance.SQL("select exten_start,exten_end from t_extension_range where name = ?", name).QueryString()
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return nil
+	} else if data == nil {
+		return nil
+	}
+	return data[0]
+}
+// ***************************************************************************************** */
+
+// GetDialPlanByUserID 通过用户 ID 获取主控话机的拨号规则名
+func GetDialPlanByUserID(id int64) (plan string) {
+	data, err := DBOrmInstance.SQL("select name from t_dial_plan where id = (select dial_plan_id from t_extension_digital where exten = (select user_exten from t_user where id = ?))", id).QueryString()
+	if err != nil {
+		lfshook.NewLogger().Errorf("middleware GetExtraInfo %+v", err)
+		return "default"
+	}
+	lfshook.NewLogger().Infof("GetDialPlanByUserID %+v", data)
+	if len(data) == 0 {
+		return "default"
+	}
+	return data[0]["name"]
+}
+
+// GetDialPlanByExtension 通过分机号 获取主控话机的拨号规则名
+func GetDialPlanByExtension(extension string) (plan string) {
+	data, err := DBOrmInstance.SQL("select name from t_dial_plan where id = (select dial_plan_id from t_extension_digital where exten = ?)", extension).QueryString()
+	if err != nil {
+		lfshook.NewLogger().Errorf("middleware GetExtraInfo %+v", err)
+		return "default"
+	}
+	lfshook.NewLogger().Infof("GetDialPlanByUserID %+v", data)
+	if len(data) == 0 {
+		return "default"
+	}
+	return data[0]["name"]
+}
+
+/* *****************************************************************************************
+// GetExtensionByUserID 通过用户 ID 获取主控话机的分机号
+func GetExtensionByUserID(id int64) (extension string) {
+	user := commonModel.User{
+		ID: id,
+	}
+	_, err := DBOrmInstance.Get(&user)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+	}
+	return user.UserExtension
+}
+
+// GetUserInfoByID 通过 ID 获取用户信息
+func GetUserInfoByID(id int64) commonModel.User {
+	user := commonModel.User{}
+	_, err := DBOrmInstance.Where("id = ?", id).Get(&user)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+	}
+	if user.ID == 0 {
+		user.Role = "extension"
+	}
+	return user
+}
+
+// GetExtensionNameByNumber 通过号码获取名称
+func GetExtensionNameByNumber(number string) (name string) {
+	data := adminModel.TabExtensionDigital{
+		Exten: number,
+	}
+	ok, err := DBOrmInstance.Get(&data)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return number
+	}
+	if ok {
+		return data.Username
+	}
+	return number
+}
+
+func GetAllAnalogExtension() (data []*adminModel.TabExtensionAnalog, err error) {
+	err = DBOrmInstance.Find(&data)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return data, err
+	}
+	return data, nil
+}
+
+func GetAllDigitalExtension() (data []*adminModel.TabExtensionDigital, err error) {
+	err = DBOrmInstance.Find(&data)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return data, err
+	}
+	return data, nil
+}
+// ***************************************************************************************** */

+ 53 - 0
internal/app/mysql/index.go

@@ -0,0 +1,53 @@
+package mysql
+
+import (
+	"fmt"
+	"os"
+	"pms-api-go/pkg/configs"
+	"pms-api-go/pkg/lfshook"
+	"syscall"
+	"time"
+
+	_ "github.com/go-sql-driver/mysql"
+	"github.com/sirupsen/logrus"
+	"xorm.io/xorm"
+	"xorm.io/xorm/log"
+)
+
+var DBOrmInstance *xorm.Engine
+
+func CreateDBInstance() {
+	var err error
+	// DBOrmInstance, err = xorm.NewEngine("sqlite3", "playcall.db")
+	url := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?charset=utf8",
+		configs.ConfigGlobal.MysqlDBUser,
+		// "coovox_admin",
+		configs.ConfigGlobal.MysqlDBSecret,
+		// "ZycooCoovoxDba42",
+		configs.ConfigGlobal.MysqlDBHost,
+		configs.ConfigGlobal.MysqlDBName,
+	)
+	lfshook.NewLogger().Infof("mysql url %s", url)
+	DBOrmInstance, err = xorm.NewEngine("mysql", url)
+	if err != nil {
+		lfshook.NewLogger().Panic(err)
+		return
+	}
+	err = DBOrmInstance.Ping()
+	if err != nil {
+		lfshook.NewLogger().Error("mysql ping error: ", err)
+		syscall.Kill(syscall.Getpid(), syscall.SIGINT)
+		return
+	}
+	if configs.ConfigGlobal.LogLevel >= logrus.DebugLevel {
+		DBOrmInstance.ShowSQL(true)
+	} else {
+		info, err := os.Open(configs.ConfigGlobal.LogInfoPath)
+		if err != nil {
+			lfshook.NewLogger().Error(err.Error())
+			return
+		}
+		DBOrmInstance.SetLogger(log.NewSimpleLogger(info))
+	}
+	DBOrmInstance.Dialect().URI().Timeout = 30 * time.Second
+}

+ 1 - 0
internal/app/mysql/voicefile.go

@@ -0,0 +1 @@
+package mysql

+ 98 - 0
internal/app/redis/extension.go

@@ -0,0 +1,98 @@
+package redis
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"pms-api-go/internal/app/ami/model"
+	"pms-api-go/pkg/lfshook"
+	"strings"
+)
+
+func ExtensionClean() {
+	ctx := context.Background()
+	keys, err := RedisInstance.Keys(ctx, "extension-*").Result()
+	if err != nil {
+		lfshook.NewLogger().Error("get redis keys error", err)
+		return
+	}
+	// lfshook.NewLogger().Info("ready clean keys", keys)
+	RedisInstance.Del(ctx, keys...)
+}
+func ExtensionDNDSet(endpointName, value string) {
+	ctx := context.Background()
+	RedisInstance.Set(ctx, fmt.Sprintf("extension-dnd-%s", endpointName), value, 0)
+}
+
+func ExtensionDNDGet(endpointName string) (value string) {
+	ctx := context.Background()
+	return RedisInstance.Get(ctx, fmt.Sprintf("extension-dnd-%s", endpointName)).Val()
+}
+
+func ExtensionDNDDel(endpointName string) {
+	ctx := context.Background()
+	RedisInstance.Del(ctx, fmt.Sprintf("extension-dnd-%s", endpointName))
+}
+
+func ExtensionSet(endpointName, uri, addr, roundtripusec string) {
+	ctx := context.Background()
+	RedisInstance.Set(ctx, fmt.Sprintf("extension-contact-uri-%s", endpointName), uri, 0)
+	RedisInstance.Set(ctx, fmt.Sprintf("extension-contact-roundtripusec-%s", endpointName), roundtripusec, 0)
+	addrKey := fmt.Sprintf("extension-contact-addr_delay-%s", endpointName)
+	oldValue := RedisInstance.Get(ctx, addrKey).Val()
+	if !strings.Contains(oldValue, addr) {
+		newValue := fmt.Sprintf("%s%s[%s]\n", oldValue, addr, roundtripusec)
+		//lfshook.NewLogger().Infof("new %+v, old %+v addr %+v", newValue, oldValue, addr)
+		RedisInstance.Set(ctx, addrKey, newValue, 0)
+	}
+}
+
+func ExtensionSetStatus(status *model.ExtensionStatus) {
+	ctx := context.Background()
+	statusStr, _ := json.Marshal(status)
+	RedisInstance.Set(ctx, fmt.Sprintf("extension-status-%s", status.Exten), statusStr, 0)
+}
+
+func ExtensionGetStatus(extension string) (status *model.ExtensionStatus) {
+	ctx := context.Background()
+	status = &model.ExtensionStatus{
+		StatusText: "Unavailable",
+	}
+	statusStr := RedisInstance.Get(ctx, fmt.Sprintf("extension-status-%s", extension)).Val()
+	if len(statusStr) == 0 {
+		//lfshook.NewLogger().Warnf("extension-status-%s redis value empty", extension)
+		return
+	}
+	if err := json.Unmarshal([]byte(statusStr), status); err != nil {
+		lfshook.NewLogger().Errorf("json unmarshall %s failure %+v", statusStr, err)
+	}
+	return
+}
+
+func ExtensionDel(endpointName, uri, addr, roundtripusec string) {
+	ctx := context.Background()
+	RedisInstance.Del(ctx, fmt.Sprintf("extension-contact-uri-%s", endpointName))
+	RedisInstance.Del(ctx, fmt.Sprintf("extension-contact-roundtripusec-%s", endpointName))
+	addrKey := fmt.Sprintf("extension-contact-addr_delay-%s", endpointName)
+	oldValue := RedisInstance.Get(ctx, addrKey).Val()
+	if oldValue != "" {
+		addrs := strings.Split(oldValue, "\n")
+		newAddrs := make([]string, 0)
+		for _, item := range addrs {
+			if !strings.Contains(item, addr) {
+				newAddrs = append(newAddrs, item)
+			}
+		}
+		//lfshook.NewLogger().Infof("new %+v, old %+v addr %+v", strings.Join(newAddrs, "\n"), oldValue, addr)
+		RedisInstance.Set(ctx, addrKey, strings.Join(newAddrs, "\n"), 0)
+	}
+}
+
+func ExtensionGet(endpointName string) (uri, addrDelay, roundtripusec, hookStatus string) {
+	ctx := context.Background()
+	uri = RedisInstance.Get(ctx, fmt.Sprintf("extension-contact-uri-%s", endpointName)).Val()
+	addrDelay = RedisInstance.Get(ctx, fmt.Sprintf("extension-contact-addr_delay-%s", endpointName)).Val()
+	roundtripusec = RedisInstance.Get(ctx, fmt.Sprintf("extension-contact-roundtripusec-%s", endpointName)).Val()
+	hookStatus = RedisInstance.Get(ctx, fmt.Sprintf("hook-%s", endpointName)).Val()
+	return
+}

+ 30 - 0
internal/app/redis/index.go

@@ -0,0 +1,30 @@
+package redis
+
+import (
+	"context"
+	"fmt"
+	"pms-api-go/pkg/configs"
+	"pms-api-go/pkg/lfshook"
+	"syscall"
+
+	"github.com/go-redis/redis/v8"
+)
+
+var RedisInstance *redis.Client
+
+func CreateRedisInstance() {
+	RedisInstance = redis.NewClient(&redis.Options{
+		Addr: fmt.Sprintf("%s:%s",
+			configs.ConfigGlobal.RedisDBHost,
+			configs.ConfigGlobal.RedisDBPort,
+		),
+		Password: configs.ConfigGlobal.RedisDBSecret,
+		DB:       0, // use default DB
+	})
+
+	err := RedisInstance.Ping(context.Background()).Err()
+	if err != nil {
+		lfshook.NewLogger().Error("redis ping error", err)
+		syscall.Kill(syscall.Getpid(), syscall.SIGINT)
+	}
+}

+ 44 - 0
internal/app/socket_io/index.go

@@ -0,0 +1,44 @@
+package socket_io
+
+import (
+	"pms-api-go/pkg/lfshook"
+
+	socketio "github.com/googollee/go-socket.io"
+)
+
+var SocketIOServer *socketio.Server
+
+func StartSocketIO() {
+	lfshook.NewLogger().Info("Start SocketIO")
+	SocketIOServer = socketio.NewServer(nil)
+
+	SocketIOServer.OnConnect("/", func(s socketio.Conn) error {
+		s.SetContext("")
+		//!TODO check token
+		// lfshook.NewLogger().Infof("socketio query %s", s.URL().RawQuery)
+		// lfshook.NewLogger().Infof("socket.io connected id: %s", s.ID())
+		return nil
+	})
+
+	SocketIOServer.OnEvent("/", "chat", func(s socketio.Conn, msg string) {
+		lfshook.NewLogger().Infof("chat: %s", msg)
+		s.Emit("reply", "have "+msg)
+	})
+
+	SocketIOServer.OnEvent("/", "bye", func(s socketio.Conn) string {
+		last := s.Context().(string)
+		s.Emit("bye", last)
+		s.Close()
+		return last
+	})
+
+	SocketIOServer.OnError("/", func(s socketio.Conn, e error) {
+		lfshook.NewLogger().Infof("meet error: %+v", e)
+	})
+
+	SocketIOServer.OnDisconnect("/", func(s socketio.Conn, reason string) {
+		lfshook.NewLogger().Infof("closed %+v", reason)
+	})
+
+	go SocketIOServer.Serve()
+}

+ 88 - 0
internal/app/status/index.go

@@ -0,0 +1,88 @@
+package status
+
+import (
+	"pms-api-go/api/admin/adminModel"
+	"pms-api-go/internal/app/ami/action"
+	"pms-api-go/internal/app/mysql"
+	"pms-api-go/internal/app/redis"
+	socketio "pms-api-go/internal/app/socket_io"
+	"pms-api-go/pkg/lfshook"
+	"strings"
+	"time"
+)
+
+// InitAsterisk 初始化状态
+func InitAsterisk() {
+	var extensions []adminModel.GeoipRule
+	if err := mysql.DBOrmInstance.Find(&extensions); err != nil {
+		lfshook.NewLogger().Errorf("db find failure %+v", err)
+		return
+	}
+
+	// 清空存在的状态
+	redis.ExtensionClean()
+
+	// PJSIPShowContacts 查询注册状态
+	data, err := action.PJSIPShowContacts()
+	if err != nil {
+		lfshook.NewLogger().Errorf("PJSIPShowContacts error %+v", err)
+		return
+	}
+	extens := make([]string, 0)
+
+	// core show contacts
+	for _, point := range data {
+		exten := strings.Split(point.ObjectName, ";")[0]
+		exten = strings.TrimSpace(exten)
+		addr := point.ViaAddr
+		// 远程注册 addr 不能取 ViaAddr, 通过 uri 获取
+		info := strings.Split(point.Uri, "@")
+		if len(info) == 2 {
+			addr = strings.Split(info[1], ":")[0]
+		}
+
+		redis.ExtensionSet(exten, point.Uri, addr, point.RoundtripUsec)
+		extens = append(extens, exten)
+	}
+
+	initDNDStatus(&extens)
+	initExtensionStatus()
+
+	// 触发任务可能修改了文件
+	// go action.Command("dialplan reload")
+}
+
+func initDNDStatus(extensions *[]string) {
+	for _, extension := range *extensions {
+		value, _ := action.DBGet("DND", extension)
+		if value != "" {
+			redis.ExtensionDNDSet(extension, value)
+		}
+	}
+}
+
+func initExtensionStatus() {
+	events, err := action.ExtensionStateList()
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return
+	}
+	for _, event := range events {
+		//lfshook.NewLogger().Infof("init event %+v", event)
+		redis.ExtensionSetStatus(event)
+	}
+}
+
+func initScheduleChannel() {
+	ticker := time.NewTicker(1500 * time.Millisecond)
+
+	for {
+		<-ticker.C
+		data, err := action.CoreShowChannels()
+		if err != nil {
+			lfshook.NewLogger().Errorf("initScheduleChannel %+v", err)
+			continue
+		}
+		socketio.SocketIOServer.BroadcastToNamespace("", "CustomCoreShowChannel", data)
+	}
+}

+ 27 - 0
pkg/commonService/check.go

@@ -0,0 +1,27 @@
+package commonService
+
+import (
+	"net/http"
+	"pms-api-go/api"
+	"pms-api-go/internal/app/mysql"
+
+	"github.com/gin-gonic/gin"
+)
+
+// 检查是否存在一条记录
+func CheckHasOne(bean interface{}, ctx *gin.Context, id int64) {
+	session := mysql.DBOrmInstance.NewSession()
+	if id != 0 {
+		session = session.Where("id != ?", id)
+	}
+	has, err := session.Get(bean)
+	if err != nil {
+		api.Error(ctx, http.StatusInternalServerError, err.Error())
+		return
+	}
+	if has {
+		api.Success(ctx, false)
+		return
+	}
+	api.Success(ctx, true)
+}

+ 15 - 0
pkg/commonService/error.go

@@ -0,0 +1,15 @@
+package commonService
+
+import "pms-api-go/pkg/lfshook"
+
+//HasError 统一处理错误
+func HasError(err error, msg ...string) {
+	if err == nil {
+		return
+	}
+	if len(msg) == 0 {
+		lfshook.NewLogger().Errorf("has error %+v", err)
+	} else {
+		lfshook.NewLogger().Errorf("has error %+v, msg %+v", err, msg)
+	}
+}

+ 101 - 0
pkg/configs/decode.go

@@ -0,0 +1,101 @@
+package configs
+
+import (
+	"io/ioutil"
+	"os"
+	"pms-api-go/pkg/lfshook"
+
+	"github.com/gofrs/uuid"
+	log "github.com/sirupsen/logrus"
+
+	"gopkg.in/yaml.v2"
+)
+
+// Config https://godoc.org/gopkg.in/yaml.v2
+// Config 存储配置
+type Config struct {
+	IdentityKey string `yaml:"identityKey"`
+	PbxNumber   string `yaml:"pbxnumber"`
+
+	AsteriskAMIHost   string `yaml:"asteriskAMIHost"`   // Host
+	AsteriskAMIPort   string `yaml:"asteriskAMIPort"`   // Port
+	AsteriskAMIUser   string `yaml:"asteriskAMIUser"`   // User
+	AsteriskAMISecret string `yaml:"asteriskAMISecret"` // Secret
+
+	MysqlDBHost   string `yaml:"mysqlDBHost"`   // Host
+	MysqlDBUser   string `yaml:"mysqlDBUser"`   // User
+	MysqlDBSecret string `yaml:"mysqlDBSecret"` // Secret
+	MysqlDBName   string `yaml:"mysqlDBName"`   // Name
+
+	RedisDBHost   string `yaml:"redisDBHost"`   // Host
+	RedisDBPort   string `yaml:"redisDBPort"`   // User
+	RedisDBSecret string `yaml:"redisDBSecret"` // Secret
+
+	LogInfoPath  string `yaml:"logInfoPath"`  //logInfoPath
+	LogErrorPath string `yaml:"logErrorPath"` //logErrorPath
+
+	StoragePath string `yaml:"storagePath"` // 存储目录
+	LogoPath    string `yaml:"logoPath"`    // 存储目录
+
+	WebType string `yaml:"webtype"` //https http
+	WebHost string `yaml:"webhost"` //Host
+	WebPort int    `yaml:"webport"` //port
+
+	AllowOrigin string `yaml:"allowOrigin"` // allowOrigin
+
+	PushConfig string `yaml:"pushConfig"` // 推送配置目录
+	I18nPath   string `yaml:"i18nPath"`   //i18n
+
+	LogLevel log.Level //logLevel
+
+	// BasicAuthUser string `yaml:"basicAuthUser"` //BasicAuth Username
+	// BasicAuthPWD  string `yaml:"basicAuthPWD"`  //BasicAuth Password
+	// ApiKey        string `yaml:"apiKey"`        //ApiKey
+	// ApiKeyValue   string `yaml:"apiKeyValue"`   //ApiKeyValue
+
+}
+
+// ConfigPath 配置文件路径
+var ConfigPath = "./config.yaml"
+
+// ConfigGlobal 全局配置变量
+var ConfigGlobal *Config
+
+// DecodeConfig 解析配置
+func DecodeConfig() {
+	_, err := os.Stat(ConfigPath)
+	if err != nil {
+		lfshook.NewLogger().Errorf("config file not exist %+v", err)
+		return
+	}
+
+	fileByte, err := ioutil.ReadFile(ConfigPath)
+	if err != nil {
+		lfshook.NewLogger().Errorf("read config file %+v", err)
+		return
+	}
+
+	ConfigGlobal = &Config{}
+	err = yaml.Unmarshal(fileByte, ConfigGlobal)
+	if err != nil {
+		lfshook.NewLogger().Errorf("Unmarshal config file %+v", err)
+		return
+	}
+
+	// jwt secret 生成
+	if ConfigGlobal.IdentityKey == "" {
+		uuid, _ := uuid.NewV4()
+		ConfigGlobal.IdentityKey = uuid.String()
+		EncodeConfig()
+	}
+}
+
+// EncodeConfig 保存配置
+func EncodeConfig() error {
+	out, _ := yaml.Marshal(ConfigGlobal)
+	err := ioutil.WriteFile(ConfigPath, out, 0777)
+	if err != nil {
+		lfshook.NewLogger().Errorf("save config file %+v", err)
+	}
+	return err
+}

+ 31 - 0
pkg/configs/interface.go

@@ -0,0 +1,31 @@
+package configs
+
+import (
+	"gopkg.in/ini.v1"
+)
+
+const ipconffile = "/etc/asterisk/dev_ip.conf"
+
+type NetworkInterface struct {
+	Key   string `json:"key"`
+	Value string `json:"value"`
+	Name  string `json:"name"`
+}
+
+func GetNetworkInterface() []NetworkInterface {
+	interfaces := make([]NetworkInterface, 0)
+	cfg, err := ini.Load(ipconffile)
+	if err != nil {
+		return interfaces
+	}
+
+	keys := cfg.Section("interface").Keys()
+	for _, key := range keys {
+		if key.Name() == "eth0" {
+			interfaces = append(interfaces, NetworkInterface{Key: key.Name(), Value: key.Value(), Name: "WAN"})
+		} else {
+			interfaces = append(interfaces, NetworkInterface{Key: key.Name(), Value: key.Value(), Name: "LAN"})
+		}
+	}
+	return interfaces
+}

+ 9 - 0
pkg/configs/path.go

@@ -0,0 +1,9 @@
+package configs
+
+import "fmt"
+
+func SSLFilePath() (key string, pem string) {
+	key = fmt.Sprintf("%s/%s.%s", "/tmp", "httpserver", "key")
+	pem = fmt.Sprintf("%s/%s.%s", "/tmp", "httpserver", "pem")
+	return
+}

+ 41 - 0
pkg/configs/push.go

@@ -0,0 +1,41 @@
+package configs
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"pms-api-go/pkg/lfshook"
+)
+
+type PushConfig struct {
+	AMIEvents  []string `json:"events"`
+	AMIPushUrl string   `json:"allEventPushUrl"`
+}
+
+var PushConfigValue PushConfig
+
+func GetPushConfig() {
+	data, err := ioutil.ReadFile(ConfigGlobal.PushConfig)
+	if err != nil {
+		PushConfigValue = PushConfig{
+			AMIEvents:  []string{},
+			AMIPushUrl: "",
+		}
+		return
+	}
+	json.Unmarshal(data, &PushConfigValue)
+}
+
+func UpdatePushConfig(config PushConfig) error {
+	event, err := json.Marshal(config)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return err
+	}
+	err = ioutil.WriteFile(ConfigGlobal.PushConfig, event, 666)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return err
+	}
+	PushConfigValue = config
+	return nil
+}

+ 6 - 0
pkg/configs/shell.go

@@ -0,0 +1,6 @@
+package configs
+
+const GetVoiceMailCmdPath = "/etc/scripts/getvm.sh"
+const WakeupCallCmdPath = "/etc/scripts/set_wakeup_call.sh"
+const DisaRule = "/etc/scripts/gen_dial_rule disa"
+const SystemInfoShell = "/etc/scripts/sysinfo.sh"

+ 204 - 0
pkg/httpclient/index.go

@@ -0,0 +1,204 @@
+package httpclient
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"pms-api-go/pkg/lfshook"
+	"time"
+
+	"gopkg.in/ini.v1"
+)
+
+func Get(url string) *http.Response {
+	if url == "" {
+		lfshook.NewLogger().Errorf("http get url is empty")
+		return nil
+	}
+	// 若认证存在,添加到 get url
+	// if configs.ConfigGlobal.Key != "" {
+	// 	url = fmt.Sprintf("%s&key=%s", url, configs.ConfigGlobal.Key)
+	// }
+
+	lfshook.NewLogger().Infof("get %s", url)
+	client := http.Client{
+		Timeout: 10 * time.Second,
+	}
+	resp, err := client.Get(url)
+	if err != nil {
+		lfshook.NewLogger().Errorf("Error: unable to connect to - %s\n", err.Error())
+	} else if resp.Status != "200 OK" {
+		lfshook.NewLogger().Errorf("Error: non 200 status from - %s\n", resp.Status)
+	}
+	return resp
+}
+
+func BasicAuthGet(url string) *http.Response {
+	if url == "" {
+		lfshook.NewLogger().Errorf("http get url is empty")
+		return nil
+	}
+
+	lfshook.NewLogger().Infof("get %s", url)
+	client := http.Client{
+		Timeout: 10 * time.Second,
+	}
+	// resp, err := client.Get(url)
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return nil
+	}
+
+	// 读取vtiger配置文件中的用户密码
+	// confPath := "/etc/asterisk/vtiger_api.conf"
+	confPath := "/etc/asterisk/pms_api.conf"
+	cfg, err := ini.Load(confPath)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return nil
+	}
+	BasicAuthUser := cfg.Section("general").Key("vtigerBasicAuthUser").String()
+	BasicAuthPWD := cfg.Section("general").Key("vtigerBasicAuthPWD").String()
+	if BasicAuthUser == "" || BasicAuthPWD == "" {
+		lfshook.NewLogger().Error("/etc/asterisk/pms_api.conf not set BasicAuthUser or BasicAuthPWD")
+		return nil
+	}
+	// 认证
+	// res.SetBasicAuth("juncheng.du@zycoo.com", "8DJ3O28MCZ4sPAk5")
+	// req.SetBasicAuth(configs.ConfigGlobal.BasicAuthUser, configs.ConfigGlobal.BasicAuthPWD)
+	req.SetBasicAuth(BasicAuthUser, BasicAuthPWD)
+
+	resp, err := client.Do(req)
+	if err != nil {
+		lfshook.NewLogger().Errorf("Error: unable to connect to - %s\n", err.Error())
+	} else if resp.Status != "200 OK" {
+		lfshook.NewLogger().Errorf("Error: non 200 status from - %s\n", resp.Status)
+	}
+
+	return resp
+}
+
+func ApiKeyGet(url string) *http.Response {
+	if url == "" {
+		lfshook.NewLogger().Errorf("http get url is empty")
+		return nil
+	}
+
+	lfshook.NewLogger().Infof("get %s", url)
+	client := http.Client{
+		Timeout: 10 * time.Second,
+	}
+	// resp, err := client.Get(url)
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return nil
+	}
+
+	// 读取vtiger配置文件中的用户密码
+	// confPath := "/etc/asterisk/vtiger_api.conf"
+	confPath := "/etc/asterisk/pms_api.conf"
+	cfg, err := ini.Load(confPath)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return nil
+	}
+	ApiKey := cfg.Section("general").Key("vtigerApiKey").String()
+	ApiKeyValue := cfg.Section("general").Key("vtigerApiKeyValue").String()
+	if ApiKey == "" || ApiKeyValue == "" {
+		lfshook.NewLogger().Error("/etc/asterisk/pms_api.conf not set ApiKey or ApiKeyValue")
+		return nil
+	}
+	// 认证
+	// req.Header.Set(configs.ConfigGlobal.ApiKey, configs.ConfigGlobal.ApiKeyValue)
+	req.Header.Set(ApiKey, ApiKeyValue)
+
+	resp, err := client.Do(req)
+	if err != nil {
+		lfshook.NewLogger().Errorf("Error: unable to connect to - %s\n", err.Error())
+	} else if resp.Status != "200 OK" {
+		lfshook.NewLogger().Errorf("Error: non 200 status from - %s\n", resp.Status)
+	}
+
+	// 读取请求后的响应
+	data, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		fmt.Println("读取请求后的响应时发生错误:", err)
+		return nil
+	}
+	// 打印请求后的响应
+	fmt.Printf("data = %+v\n", string(data))
+
+	return resp
+}
+
+func ZohoGet(url string) *http.Response {
+	if url == "" {
+		lfshook.NewLogger().Errorf("http get url is empty")
+		return nil
+	}
+
+	lfshook.NewLogger().Infof("get %s", url)
+	client := http.Client{
+		Timeout: 10 * time.Second,
+	}
+	// resp, err := client.Get(url)
+	req, err := http.NewRequest("POST", url, nil)
+	if err != nil {
+		return nil
+	}
+
+	// 读取vtiger配置文件中的用户密码
+	// confPath := "/etc/asterisk/vtiger_api.conf"
+	confPath := "/etc/asterisk/pms_api.conf"
+	cfg, err := ini.Load(confPath)
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return nil
+	}
+	AccessToken := cfg.Section("general").Key("zohoAccessToken").String()
+	if AccessToken == "" {
+		lfshook.NewLogger().Error("/etc/asterisk/pms_api.conf not set zohoAccessToken")
+		return nil
+	}
+	// 认证
+	req.Header.Set("Authorization", "Bearer "+AccessToken)
+
+	resp, err := client.Do(req)
+	if err != nil {
+		lfshook.NewLogger().Errorf("Error: unable to connect to - %s\n", err.Error())
+	} else if resp.Status != "200 OK" {
+		lfshook.NewLogger().Errorf("Error: non 200 status from - %s\n", resp.Status)
+	}
+
+	// 读取请求后的响应
+	data, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		fmt.Println("读取请求后的响应时发生错误:", err)
+		return nil
+	}
+	// 打印请求后的响应
+	fmt.Printf("data = %+v\n", string(data))
+
+	return resp
+}
+
+func Post(event interface{}, url string) *http.Response {
+	eventByte, err := json.Marshal(event)
+	if err != nil {
+		lfshook.NewLogger().Errorf("event marshall error %+v", err)
+		return nil
+	}
+	data := bytes.NewBuffer([]byte(eventByte))
+	client := http.Client{
+		Timeout: 20 * time.Second,
+	}
+	resp, err := client.Post(url, "application/json", data)
+	if err != nil {
+		lfshook.NewLogger().Errorf("Error: unable to connect to - %s\n", err.Error())
+	} else if resp.Status != "200 OK" {
+		lfshook.NewLogger().Errorf("Error: non 200 status from - %s\n, event: %+v", resp.Status, event)
+	}
+	return resp
+}

+ 64 - 0
pkg/i18n/index.go

@@ -0,0 +1,64 @@
+package i18n
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/fs"
+	"pms-api-go/pkg/configs"
+	"pms-api-go/pkg/utils"
+	"pms-api-go/web"
+
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+	"github.com/sirupsen/logrus"
+	"golang.org/x/text/language"
+)
+
+var i18nBundle *i18n.Bundle
+
+func InitBundle() {
+	i18nBundle = i18n.NewBundle(language.English)
+	i18nBundle.RegisterUnmarshalFunc("json", json.Unmarshal)
+	files := []string{"zh_CN", "en_US"}
+	for _, file := range files {
+		filePath := fmt.Sprintf("%s/%s.json", configs.ConfigGlobal.I18nPath, file)
+		if utils.PathExists(filePath) {
+			_, err := i18nBundle.LoadMessageFile(filePath)
+			if err != nil {
+				logrus.Errorf("load i18n file %+v", err)
+			}
+			continue
+		}
+
+		buf, err := fs.ReadFile(web.ResourcesFiles, fmt.Sprintf("resources/%s.json", file))
+		if err != nil {
+			if err != nil {
+				logrus.Errorf("load i18n file %+v", err)
+			}
+			continue
+		}
+		i18nBundle.ParseMessageFileBytes(buf, fmt.Sprintf("resources/%s.json", file))
+	}
+}
+
+func Message(lang string, id string) string {
+	localizer := i18n.NewLocalizer(i18nBundle, lang)
+	result, err := localizer.Localize(&i18n.LocalizeConfig{
+		DefaultMessage: &i18n.Message{
+			ID: id,
+		},
+	})
+	if err != nil {
+		// lfshook.NewLogger().Warn(err)
+		return id
+	}
+	return result
+}
+
+// func (b *i18n.Bundle) LoadMessageFileFS(fsys fs.FS, path string) (*i18n.MessageFile, error) {
+// 	buf, err := fs.ReadFile(fsys, path)
+// 	if err != nil {
+// 		return nil, err
+// 	}
+
+// 	return b.ParseMessageFileBytes(buf, path)
+// }

+ 218 - 0
pkg/lfshook/log.go

@@ -0,0 +1,218 @@
+// Package lfshook is hook for sirupsen/logrus that used for writing the logs to local files.
+package lfshook
+
+import (
+	"fmt"
+	"io"
+	"reflect"
+	"sync"
+
+	"github.com/sirupsen/logrus"
+	"gopkg.in/natefinch/lumberjack.v2"
+)
+
+// We are logging to file, strip colors to make the output more readable.
+var defaultFormatter = &logrus.TextFormatter{DisableColors: true}
+
+// PathMap is map for mapping a log level to a file's path.
+// Multiple levels may share a file, but multiple files may not be used for one level.
+// type PathMap map[logrus.Level]string
+
+// 一个 level 对应 一个 logger
+type LoggerMap map[logrus.Level]*lumberjack.Logger
+
+// WriterMap is map for mapping a log level to an io.Writer.
+// Multiple levels may share a writer, but multiple writers may not be used for one level.
+type WriterMap map[logrus.Level]io.Writer
+
+type RotateFileConfig struct {
+	Filename   string
+	MaxSize    int
+	MaxBackups int
+	MaxAge     int
+	Level      logrus.Level
+	Formatter  logrus.Formatter
+}
+
+// LfsHook is a hook to handle writing to local log files.
+type LfsHook struct {
+	loggers   LoggerMap
+	writers   WriterMap
+	levels    []logrus.Level
+	lock      *sync.Mutex
+	formatter logrus.Formatter
+
+	Config RotateFileConfig
+
+	defaultPath      string
+	defaultWriter    io.Writer
+	hasDefaultPath   bool
+	hasDefaultWriter bool
+}
+
+// NewHook returns new LFS hook.
+// Output can be a string, io.Writer, WriterMap or PathMap.
+// If using io.Writer or WriterMap, user is responsible for closing the used io.Writer.
+func NewHook(output interface{}, formatter logrus.Formatter) *LfsHook {
+	hook := &LfsHook{
+		lock: new(sync.Mutex),
+	}
+
+	hook.SetFormatter(formatter)
+
+	switch output.(type) {
+	case string:
+		hook.SetDefaultPath(output.(string))
+		break
+	case io.Writer:
+		hook.SetDefaultWriter(output.(io.Writer))
+		break
+	case LoggerMap:
+		hook.loggers = output.(LoggerMap)
+		for level := range output.(LoggerMap) {
+			hook.levels = append(hook.levels, level)
+		}
+		break
+	case WriterMap:
+		hook.writers = output.(WriterMap)
+		for level := range output.(WriterMap) {
+			hook.levels = append(hook.levels, level)
+		}
+		break
+	default:
+		panic(fmt.Sprintf("unsupported level map type: %v", reflect.TypeOf(output)))
+	}
+
+	return hook
+}
+
+// SetFormatter sets the format that will be used by hook.
+// If using text formatter, this method will disable color output to make the log file more readable.
+func (hook *LfsHook) SetFormatter(formatter logrus.Formatter) {
+	hook.lock.Lock()
+	defer hook.lock.Unlock()
+	if formatter == nil {
+		formatter = defaultFormatter
+	} else {
+		switch formatter.(type) {
+		case *logrus.TextFormatter:
+			textFormatter := formatter.(*logrus.TextFormatter)
+			textFormatter.DisableColors = true
+		}
+	}
+
+	hook.formatter = formatter
+}
+
+// SetDefaultPath sets default path for levels that don't have any defined output path.
+func (hook *LfsHook) SetDefaultPath(defaultPath string) {
+	hook.lock.Lock()
+	defer hook.lock.Unlock()
+	hook.defaultPath = defaultPath
+	hook.hasDefaultPath = true
+}
+
+// SetDefaultWriter sets default writer for levels that don't have any defined writer.
+func (hook *LfsHook) SetDefaultWriter(defaultWriter io.Writer) {
+	hook.lock.Lock()
+	defer hook.lock.Unlock()
+	hook.defaultWriter = defaultWriter
+	hook.hasDefaultWriter = true
+}
+
+// Fire writes the log file to defined path or using the defined writer.
+// User who run this function needs write permissions to the file or directory if the file does not yet exist.
+func (hook *LfsHook) Fire(entry *logrus.Entry) error {
+	hook.lock.Lock()
+	defer hook.lock.Unlock()
+	if hook.writers != nil || hook.hasDefaultWriter {
+		return hook.ioWrite(entry)
+	} else if hook.loggers != nil || hook.hasDefaultPath {
+		return hook.fileWrite(entry)
+	}
+
+	return nil
+}
+
+// Write a log line to an io.Writer.
+func (hook *LfsHook) ioWrite(entry *logrus.Entry) error {
+	var (
+		writer io.Writer
+		msg    []byte
+		err    error
+		ok     bool
+	)
+
+	if writer, ok = hook.writers[entry.Level]; !ok {
+		if hook.hasDefaultWriter {
+			writer = hook.defaultWriter
+		} else {
+			return nil
+		}
+	}
+
+	// use our formatter instead of entry.String()
+	msg, err = hook.formatter.Format(entry)
+
+	if err != nil {
+		log.Println("failed to generate string for entry:", err)
+		return err
+	}
+	_, err = writer.Write(msg)
+	return err
+}
+
+// Write a log line directly to a file.
+func (hook *LfsHook) fileWrite(entry *logrus.Entry) error {
+	var (
+		logger *lumberjack.Logger
+		msg    []byte
+		err    error
+		ok     bool
+	)
+
+	if logger, ok = hook.loggers[entry.Level]; !ok {
+		if hook.hasDefaultPath {
+			logger = &lumberjack.Logger{
+				Filename:   hook.defaultPath,
+				MaxSize:    10, // maxSize M
+				MaxBackups: 5,  // keep 5 file
+				MaxAge:     7,  //  7 day
+			}
+		} else {
+			return nil
+		}
+	}
+
+	// use our formatter instead of entry.String()
+	msg, err = hook.formatter.Format(entry)
+
+	if err != nil {
+		log.Println("failed to generate string for entry:", err)
+		return err
+	}
+
+	defer logger.Close()
+	logger.Write(msg)
+	return nil
+}
+
+// Levels returns configured log levels.
+func (hook *LfsHook) Levels() []logrus.Level {
+	return logrus.AllLevels
+}
+
+var log *logrus.Entry
+var once sync.Once
+
+//NewLogger 初始化 logger
+func NewLogger() *logrus.Entry {
+	once.Do(func() {
+		log = logrus.New().WithFields(
+			logrus.Fields{
+				"appname": "pbx-api-v4",
+			},
+		)
+	})
+	return log
+}

+ 91 - 0
pkg/utils/cmd.go

@@ -0,0 +1,91 @@
+package utils
+
+import (
+	"bytes"
+	"context"
+	"os/exec"
+	"pms-api-go/pkg/lfshook"
+	"strings"
+	"time"
+)
+
+//ExecCmdAsync 执行指定命令
+func ExecCmdAsync(cmdName string, arg ...string) (stdOut, errOut string, err error) {
+	lfshook.NewLogger().Infof("ExecCmdAsync cmd %s %s", cmdName, arg)
+	cmd := exec.Command(cmdName, arg...)
+	var stdout, stderr bytes.Buffer
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	err = cmd.Run()
+	if err != nil {
+		lfshook.NewLogger().Errorf("cmd.Run(%s) failed with %s\n", cmdName, err)
+	}
+	outStr, errStr := stdout.String(), stderr.String()
+	if len(outStr) > 0 {
+		lfshook.NewLogger().Debugf("cmd.Run(%s) %s", cmdName, outStr)
+	}
+	if len(errStr) > 0 {
+		lfshook.NewLogger().Errorf("cmd.Run(%s)%s", cmdName, errStr)
+	}
+	return outStr, errStr, err
+}
+
+//ExecCmd 执行指定命令
+func ExecCmd(cmdName string, arg ...string) (stdOut, errOut string, err error) {
+	///etc/scripts/sysinfo.sh 会不断刷新
+	if cmdName != "/etc/scripts/sysinfo.sh" {
+		lfshook.NewLogger().Infof("ExecCmd cmd %s %s", cmdName, arg)
+	}
+	cmd := exec.Command(cmdName, arg...)
+	var stdout, stderr bytes.Buffer
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	err = cmd.Start()
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return "", "", err
+	}
+
+	go func() {
+		err = cmd.Wait()
+		if err != nil {
+			lfshook.NewLogger().Errorf("cmd.Wait(%s) %s\n", cmdName, err)
+		}
+		cancel()
+	}()
+
+	<-ctx.Done()
+
+	outStr, errStr := stdout.String(), stderr.String()
+	if len(outStr) > 0 {
+		lfshook.NewLogger().Debugf("cmd.Start(%s) %s", cmdName, outStr)
+	}
+	if len(errStr) > 0 {
+		lfshook.NewLogger().Errorf("cmd.Start(%s)%s", cmdName, errStr)
+	}
+	return strings.TrimSpace(outStr), errStr, err
+}
+
+func Reboot(sleep time.Duration) {
+	go func() {
+		time.Sleep(sleep * time.Second)
+		cmd := "/sbin/reboot"
+		output, _ := exec.Command("bash", "-c", cmd).CombinedOutput()
+		lfshook.NewLogger().Info(cmd)
+		lfshook.NewLogger().Info(string(output))
+	}()
+}
+
+func RunCmd(cmd string) (output []byte, err error) {
+	lfshook.NewLogger().Debug(cmd)
+	output, err = exec.Command("bash", "-c", cmd).CombinedOutput()
+	lfshook.NewLogger().Debug("command res:", string(output), "\n")
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+	}
+	return
+}

+ 17 - 0
pkg/utils/common.go

@@ -0,0 +1,17 @@
+package utils
+
+import "fmt"
+
+var DialPrefix = "PJSIP"
+
+type Version struct {
+	GitCommitCode string `json:"gitCommitcode"`
+	GoVersion     string `json:"goVersion"`
+	BuildDate     string `json:"buildDate"`
+}
+
+func (version *Version) String() string {
+	return fmt.Sprintf("git commit code: %s\nbuild date: %s\ngo version: %s", version.GitCommitCode, version.BuildDate, version.GoVersion)
+}
+
+var VersionInstance = &Version{}

+ 22 - 0
pkg/utils/exit.go

@@ -0,0 +1,22 @@
+package utils
+
+import (
+	"os"
+	"os/signal"
+	"pms-api-go/pkg/lfshook"
+	"syscall"
+)
+
+func Exit() {
+	sigs := make(chan os.Signal, 1)
+	done := make(chan bool, 1)
+	// syscall.SIGSTOP windows not support
+	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+	go func() {
+		<-sigs
+		done <- true
+	}()
+	lfshook.NewLogger().Info("Server Start Awaiting Signal")
+	<-done
+	lfshook.NewLogger().Info("Exiting")
+}

+ 42 - 0
pkg/utils/file.go

@@ -0,0 +1,42 @@
+package utils
+
+import (
+	"os"
+
+	"github.com/sirupsen/logrus"
+	"gopkg.in/ini.v1"
+)
+
+// 缓存 timeZone 信息
+var timeZoneCache string
+
+// FileExists checks if a file exists and is not a directory before we
+// try using it to prevent further errors.
+func FileExists(filename string) bool {
+	info, err := os.Stat(filename)
+	if os.IsNotExist(err) {
+		return false
+	}
+	return !info.IsDir()
+}
+
+// pbx 不能正确读取时区, 读取自定义文件  /etc/asterisk/ntp.conf
+func GetLocationName() string {
+	if timeZoneCache != "" {
+		return timeZoneCache
+	}
+	filePath := "/etc/asterisk/ntp.conf"
+	_, err := os.Stat(filePath)
+	if err != nil {
+		logrus.Error(err)
+		return ""
+	}
+	iniFile, err := ini.Load(filePath)
+	if err != nil {
+		logrus.Error(err)
+		return ""
+	}
+	timeZoneCache = iniFile.Section("ntp").Key("TZNAME").Value()
+	logrus.Infof("use location %s", timeZoneCache)
+	return timeZoneCache
+}

+ 18 - 0
pkg/utils/time.go

@@ -0,0 +1,18 @@
+package utils
+
+import (
+	"strconv"
+	"strings"
+)
+
+//TimeStringToSecond 00:01:01 => 61s
+func TimeStringToSecond(input string) int {
+	times := strings.Split(input, ":")
+	timeHour := times[0]
+	timeMin := times[1]
+	timeSecond := times[2]
+	timeHourtInt, _ := strconv.Atoi(timeHour)
+	timeMinInt, _ := strconv.Atoi(timeMin)
+	timeSecondInt, _ := strconv.Atoi(timeSecond)
+	return timeHourtInt*3600 + timeMinInt*60 + timeSecondInt
+}

+ 149 - 0
pkg/utils/utils.go

@@ -0,0 +1,149 @@
+package utils
+
+import (
+	"flag"
+	"fmt"
+	"math/rand"
+	"os"
+	"strings"
+	"time"
+	"unicode"
+)
+
+//IsFlagPassed 判断字符串全是数字
+func IsDigit(str string) bool {
+	for _, x := range []rune(str) {
+		if !unicode.IsDigit(x) {
+			return false
+		}
+	}
+	return true
+}
+
+//IsFlagPassed 判断是否设置了指定 flag
+func IsFlagPassed(name string) bool {
+	found := false
+	flag.Visit(func(f *flag.Flag) {
+		if f.Name == name {
+			found = true
+		}
+	})
+	return found
+}
+
+//NumberOrStringToBool 字符串 1, yes 返回 true 否则 false
+func NumberOrStringToBool(input string) bool {
+	if input == "1" {
+		return true
+	}
+	if input == "yes" {
+		return true
+	}
+	return false
+}
+
+//NumberToBool 大于0 返回 true 否则 false
+func NumberToBool(input int) bool {
+	if input > 0 {
+		return true
+	} else {
+		return false
+	}
+}
+
+//Bool to int true 返回 1 否则 0
+func BoolToInt(input bool) int {
+	var output int
+	if input {
+		output = 1
+	} else {
+		output = 0
+	}
+	return output
+}
+
+//BoolToNumber true 返回 "1" false 返回  "0"
+func BoolToNumber(input bool) string {
+	if input {
+		return "1"
+	}
+	return "0"
+}
+
+//BoolToString true 返回 "yes" false 返回  "no"
+func BoolToString(input bool) string {
+	if input {
+		return "yes"
+	}
+	return "no"
+}
+
+//YesToOn  Yes 或 yes 返回 "on" 否则返回 "off"
+// 会议 Muted 的值可能是 By admin
+func YesToOn(input string) string {
+	if input == "Yes" || input == "yes" || strings.Contains(input, "By") {
+		return "on"
+	}
+	return "off"
+}
+
+//GetRandStr 生成随机密码
+func GetRandStr(baseStr string, length int) string {
+	r := rand.New(rand.NewSource(time.Now().UnixNano() + rand.Int63()))
+	bytes := make([]byte, length)
+	l := len(baseStr)
+	for i := 0; i < length; i++ {
+		bytes[i] = baseStr[r.Intn(l)]
+	}
+	return string(bytes)
+}
+
+//SqlLike sql like 头尾添加 %%
+func SqlLike(input string) string {
+	return fmt.Sprintf("%%%s%%", input)
+}
+
+// IsChannel 判断输入是否是 Channel
+func IsChannel(input string) bool {
+	if strings.Contains(input, "SIP") {
+		return true
+	}
+
+	if strings.Contains(input, "Local") {
+		return true
+	}
+
+	if strings.Contains(input, "DAHDI") {
+		return true
+	}
+	return false
+}
+
+//判断文件是否存在
+func PathExists(path string) bool {
+	_, err := os.Stat(path)
+	if err == nil {
+		return true
+	}
+	if os.IsNotExist(err) {
+		return false
+	}
+	return false
+}
+
+// 数组去重
+func RemoveDuplication(arr []string) []string {
+	set := make(map[string]struct{}, len(arr))
+	j := 0
+	for _, v := range arr {
+		_, ok := set[v]
+		if ok {
+			continue
+		}
+		set[v] = struct{}{}
+		arr[j] = v
+		j++
+	}
+
+	return arr[:j]
+}

+ 63 - 0
pkg/voicemail/parse.go

@@ -0,0 +1,63 @@
+package voicemail
+
+import (
+	"encoding/json"
+	"pms-api-go/pkg/lfshook"
+	"strings"
+
+	"gopkg.in/ini.v1"
+)
+
+// {"vmBox": "100", "newMsgs": "9"}
+type VocieMailNew struct {
+	VMBox   string `json:"vmBox"`
+	NewMsgs string `json:"newMsgs"`
+}
+
+func ParseVoiceMailNew(in string) map[string]string {
+	data := strings.Split(in, "\n")
+	result := make(map[string]string)
+	for _, item := range data {
+		member := &VocieMailNew{}
+		json.Unmarshal([]byte(item), member)
+		result[member.VMBox] = member.NewMsgs
+	}
+	return result
+}
+
+type VoiceMail struct {
+	ID        string `json:"id"`
+	Extension string `json:"extension"`
+	MsgPath   string `json:"msgPath"`
+	MsgID     string `json:"msgID"`
+	Date      string `json:"date"`
+	Duration  string `json:"duration"`
+	CallerID  string `json:"callerID"`
+	Type      string `json:"type"`
+	Origdate  string `json:"origdate"`
+}
+
+func ParseVoiceMail(in string) []*VoiceMail {
+	data := strings.Split(in, "\n")
+	result := make([]*VoiceMail, len(data))
+	for index, item := range data {
+		vmInfo := strings.Split(item, ",")
+		member := &VoiceMail{
+			ID:        vmInfo[0],
+			Extension: vmInfo[1],
+			MsgPath:   vmInfo[5],
+			MsgID:     vmInfo[4],
+			Type:      vmInfo[3],
+		}
+		iniInfo, err := ini.Load(vmInfo[2])
+		if err != nil {
+			lfshook.NewLogger().Error(err)
+		} else {
+			member.Duration = iniInfo.Section("message").Key("duration").Value()
+			member.Origdate = iniInfo.Section("message").Key("origdate").Value()
+			member.CallerID = iniInfo.Section("message").Key("callerid").Value()
+		}
+		result[index] = member
+	}
+	return result
+}

+ 23 - 0
pkg/weblog/index.go

@@ -0,0 +1,23 @@
+package weblog
+
+import (
+	"fmt"
+	"os"
+	"time"
+
+	"github.com/sirupsen/logrus"
+)
+
+var fail2banLog = "/var/log/invalid_web_visit.log"
+
+func AuthError(ip, msg string) {
+	f, err := os.OpenFile(fail2banLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+	if err != nil {
+		logrus.Error("auth open error", err)
+	}
+	defer f.Close()
+	context := fmt.Sprintf("%s pbx api auth error from '%s', msg: %s\n", time.Now(), ip, msg)
+	if _, err := f.Write([]byte(context)); err != nil {
+		logrus.Error("auth write error", err)
+	}
+}

+ 12 - 0
web/embed.go

@@ -0,0 +1,12 @@
+package web
+
+import "embed"
+
+//go:embed www/index.html
+var IndexFile string
+
+////go:embed www/socketio-client-tool/*
+var SocketIOClientTool embed.FS
+
+//go:embed resources/*
+var ResourcesFiles embed.FS

+ 179 - 0
web/resources/en_US.json

@@ -0,0 +1,179 @@
+{
+	"weblog": {
+		"weblog_file_name": "User operation logs.xlsx",
+		"weblog_file_worksheet_name": "Operation logs",
+		"weblog_title_time": "Time",
+		"weblog_title_username": "Username",
+		"weblog_title_role": "User",
+		"weblog_title_ip": "IP Address",
+		"weblog_title_operation": "Operation",
+		"weblog_role_admin": "Manager User",
+		"weblog_role_operator": "Operator User",
+		"weblog_role_extension": "Extension User",
+		"weblog_role_conference": "Conference Manager",
+		"weblog_role_bill": "Billing Manager",
+		"weblog_role_api": "API"
+	},
+	"pincode": {
+		"pincode_file_name": "Extensions PIN code list.xlsx",
+		"pincode_worksheet_name": "Extensions PIN code list",
+		"pincode_name": "Username",
+		"pincode_extension": "Extension",
+		"pincode_code": "PIN Code"
+	},
+	"exten_digital": {
+		"exten_digital_file_name": "Digital Extension.xlsx",
+		"exten_digital_worksheet_name": "Extension",
+		"exten_digital_prop_extension_number": "Extension Number",
+		"exten_digital_prop_enable": "Enable",
+		"exten_digital_prop_username": "Name",
+		"exten_digital_prop_password": "Password",
+		"exten_digital_prop_coocall_password": "CooCall Password",
+		"exten_digital_prop_pin_code": "Quick Register Code",
+		"exten_digital_prop_email": "Email",
+		"exten_digital_prop_outbound_cid_1": "Outbound CID 1",
+		"exten_digital_prop_outbound_cid_2": "Outbound CID 2",
+		"exten_digital_prop_mobile_number": "Mobile Number",
+		"exten_digital_prop_dial_plan_id": "Dial Permission(id,default is 1)",
+		"exten_digital_prop_language": "Language",
+		"exten_digital_prop_music_on_hold_id": "Music On Hold(id)",
+		"exten_digital_prop_voicemail_enable": "Voicemail",
+		"exten_digital_prop_voicemail_password": "Voicemail Password",
+		"exten_digital_prop_remote_extension": "Remote Extension",
+		"exten_digital_prop_regist_count": "Simultaneous Register Count(default is 1)",
+		"exten_digital_prop_video_call_enable": "Video Call Enable",
+		"exten_digital_prop_video_call_codec": "Video Codec",
+		"exten_digital_prop_web_rtc": "Web Portal",
+		"exten_digital_prop_web_login": "Web Login",
+		"exten_digital_prop_app": "App",
+		"exten_digital_prop_dnd": "DND",
+		"exten_digital_prop_recording": "Call Recording",
+		"exten_digital_prop_spy": "Call Spy",
+		"exten_digital_prop_regist_expiration": "Register Expiration",
+		"exten_digital_prop_transport": "Transport Protocol",
+		"exten_digital_prop_dtmf": "DTMF Mode",
+		"exten_digital_prop_srtp": "SRTP",
+		"exten_digital_prop_qualify": "Qualify(sec.)",
+		"exten_digital_prop_qualify_timeout": "Qualify Timeout(sec.)",
+		"exten_digital_prop_nat": "NAT Support",
+		"exten_digital_prop_iax": "IAX2 Extension",
+		"exten_digital_prop_audio_codec": "Audio Codec(s)",
+		"exten_digital_prop_permit": "Permit IP",
+		"exten_digital_prop_send_pai": "Send PAI",
+		"exten_digital_prop_send_rpid": "Send RPID",
+		"exten_digital_prop_rtp_timeout": "RTP Timeout",
+		"exten_digital_prop_group_number": "Pickup Group(Default 1)",
+		"exten_digital_prop_inband_progress": "Inband Progress",
+		"exten_digital_prop_white_list_tag_id": "White List Tag(id, default 0)"
+	},
+	"did": {
+		"did_template_file_name": "DID template file.xlsx",
+		"did_file_name": "DID list file.xlsx",
+		"did_file_worksheet_name": "DID",
+		"did_title_callDestNum": "DID Number",
+		"did_title_destType": "Destination Type",
+		"did_title_dest": "Destination",
+		"did_title_destTrunkType": "Destination Trunk Type",
+		"did_title_destTrunkAddNumber": "Destination Trunk Add Number",
+		"did_title_ringTone": "Ring Tone",
+		"did_error_title": "Type Error",
+		"did_error_message": "You must select TYPE from the select list!"
+	},
+	"phonebook": {
+		"phonebook_template_file_name": "Contact template file.xlsx",
+		"phonebook_contact_file_name": "Contact list file.xlsx",
+		"phonebook_contact_file_worksheet_name": "Contacts",
+		"phonebook_title_name": "Name",
+		"phonebook_title_phone": "Phone Number",
+		"phonebook_title_email": "E-Mail",
+		"phonebook_title_company": "Company Name",
+		"phonebook_title_dial_code": "Speed Dial Number"
+	},
+	"blacklist": {
+		"blacklist_template_file_name": "Blacklist Template File.xlsx",
+		"blacklist_file_name": "Blacklist File.xlsx",
+		"blacklist_file_worksheet_name": "Blacklist",
+		"blacklist_title_number": "Phone Number",
+		"blacklist_title_createUser": "Create User",
+		"blacklist_title_createTime": "Create Time"
+	},
+	"trunk": {
+		"trunk_sip_file_name": "Sip Trunk.xlsx",
+		"trunk_sip_worksheet_name": "Sip",
+		"trunk_sip_prop_name": "Trunk Name",
+		"trunk_sip_prop_enable": "Enable",
+		"trunk_sip_prop_trunk_type": "Trunk Type",
+		"trunk_sip_prop_auth": "Authentication",
+		"trunk_sip_prop_server": "Host",
+		"trunk_sip_prop_port": "Port",
+		"trunk_sip_prop_auth_user": "AuthUser",
+		"trunk_sip_prop_username": "User Name",
+		"trunk_sip_prop_password": "Password",
+		"trunk_sip_prop_contact": "Contact",
+		"trunk_sip_prop_regist_expiration": "Register Expiration",
+		"trunk_sip_prop_retry_interval": "Retry Interval",
+		"trunk_sip_prop_max_retry": "Max Retry",
+		"trunk_sip_prop_identify_by": "Identify By",
+		"trunk_sip_prop_max_expiration": "Max Expiration",
+		"trunk_sip_prop_fax_detect": "Fax Detect",
+		"trunk_sip_prop_qualify": "Qualify",
+		"trunk_sip_prop_srtp": "SRTP",
+		"trunk_sip_prop_nat": "NAT Surpport",
+		"trunk_sip_prop_client_uri": "Client URI",
+		"trunk_sip_prop_transport": "Transport Protocol",
+		"trunk_sip_prop_server_uri": "Server URI",
+		"trunk_sip_prop_language": "Prompts Language",
+		"trunk_sip_prop_aor_contact": "AOR Contact",
+		"trunk_sip_prop_call_max": "Simultaneous Calls",
+		"trunk_sip_prop_recording": "Call Recording",
+		"trunk_sip_prop_cid_preferred": "Preferred Outbound CID",
+		"trunk_sip_prop_from_user": "From User",
+		"trunk_sip_prop_outbound_cid": "Outbound CID",
+		"trunk_sip_prop_dtmf": "DTMF Mode",
+		"trunk_sip_prop_dial_plan_id": "Dial Permission(id,default is 0)",
+		"trunk_sip_prop_from_domain": "From Domain",
+		"trunk_sip_prop_send_pai": "Send PAI",
+		"trunk_sip_prop_send_rpid": "Send RPID",
+		"trunk_sip_prop_rtp_timeout": "RTP Timeout",
+		"trunk_sip_prop_video_codec": "Video Codec",
+		"trunk_sip_prop_audio_codec": "Audio Codec(s)",
+		"trunk_sip_prop_out_proxy": "Proxy",
+		"trunk_sip_prop_out_proxy_port": "Port"
+	},
+	"bill": {
+		"bill_rate_file_name": "Bill Rate.xlsx",
+		"bill_rate_file_worksheet_name": "Rate list",
+		"bill_rate_prop_enable": "Activate",
+		"bill_rate_prop_remark": "Remark",
+		"bill_rate_prop_prefix": "Prefix",
+		"bill_rate_prop_number_length": "Number Length",
+		"bill_rate_prop_init_cost": "Initial Cost",
+		"bill_rate_prop_init_time": "Initial Time(s)",
+		"bill_rate_prop_rate": "Rate",
+		"bill_rate_prop_bill_unit": "Billing Unit(s)",
+		"bill_rate_prop_bill_time_start": "Start Time",
+		"bill_rate_prop_bill_time_end": "End Time",
+		"bill_rate_prop_extension_list": "Extensions",
+		"bill_charge_log":"Charge Log.xlsx",
+		"bill_charge_time":"Time",
+		"bill_charge_extension":"Extension",
+		"bill_charge_pre_balance":"Pre Balance",
+		"bill_charge_charge_amount":"Charge Amount",
+		"bill_charge_post_balance":"Post Balance",
+		"bill_call_log":"Call Log.xlsx",
+		"bill_call_time":"Start Time",
+		"bill_call_src":"Src",
+		"bill_call_dst":"Dst",
+		"bill_call_duration":"Duration",
+		"bill_call_trunk":"Trunk",
+		"bill_call_cast":"Cast",
+		"bill_statistics_file_name": "Bill Call Statistics.xlsx",
+		"bill_statistics_file_worksheet_name": "Call log statistics",
+		"bill_statistics_title_search_time": "Statistics By",
+		"bill_statistics_title_extension": "Extension",
+		"bill_statistics_title_call": "Calls",
+		"bill_statistics_title_time": "Total Call Time",
+		"bill_statistics_title_time_avg": "Average Call Time",
+		"bill_statistics_title_total_cost": "Total Cost"
+	}
+}

BIN
web/resources/no_logo.png


+ 179 - 0
web/resources/zh_CN.json

@@ -0,0 +1,179 @@
+{
+	"weblog": {
+		"weblog_file_name": "操作日志.xlsx",
+		"weblog_file_worksheet_name": "操作日志",
+		"weblog_title_time": "时间",
+		"weblog_title_username": "用户名",
+		"weblog_title_role": "用户",
+		"weblog_title_ip": "IP地址",
+		"weblog_title_operation": "操作",
+		"weblog_role_admin": "管理员",
+		"weblog_role_operator": "接线员",
+		"weblog_role_extension": "分机用户",
+		"weblog_role_conference": "会议管理员",
+		"weblog_role_bill": "计费用户",
+		"weblog_role_api": "API"
+	},
+	"pincode": {
+		"pincode_file_name": "分机PIN码.xlsx",
+		"pincode_worksheet_name": "分机PIN码",
+		"pincode_name": "用户名",
+		"pincode_extension": "分机号",
+		"pincode_code": "PIN码"
+	},
+	"exten_digital": {
+		"exten_digital_file_name": "数字分机.xlsx",
+		"exten_digital_worksheet_name": "分机",
+		"exten_digital_prop_extension_number": "分机号",
+		"exten_digital_prop_enable": "启用",
+		"exten_digital_prop_username": "名称",
+		"exten_digital_prop_password": "密码",
+		"exten_digital_prop_coocall_password": "CooCall密码",
+		"exten_digital_prop_pin_code": "快速注册码",
+		"exten_digital_prop_email": "邮箱",
+		"exten_digital_prop_outbound_cid_1": "外呼显示号码1",
+		"exten_digital_prop_outbound_cid_2": "外呼显示号码2",
+		"exten_digital_prop_mobile_number": "手机号码",
+		"exten_digital_prop_dial_plan_id": "拨号权限(编号,默认1)",
+		"exten_digital_prop_language": "语言",
+		"exten_digital_prop_music_on_hold_id": "通话保持音乐(编号)",
+		"exten_digital_prop_voicemail_enable": "语音留言开启",
+		"exten_digital_prop_voicemail_password": "语音信箱密码",
+		"exten_digital_prop_remote_extension": "远程分机",
+		"exten_digital_prop_regist_count": "同时注册数量",
+		"exten_digital_prop_video_call_enable": "视频通话开启",
+		"exten_digital_prop_video_call_codec": "视频编码",
+		"exten_digital_prop_web_rtc": "网页分机",
+		"exten_digital_prop_web_login": "网页登录",
+		"exten_digital_prop_app": "App注册",
+		"exten_digital_prop_dnd": "免打扰",
+		"exten_digital_prop_recording": "通话录音",
+		"exten_digital_prop_spy": "通话监听",
+		"exten_digital_prop_regist_expiration": "注册超时",
+		"exten_digital_prop_transport": "传输协议",
+		"exten_digital_prop_dtmf": "DTMF模式",
+		"exten_digital_prop_srtp": "SRTP",
+		"exten_digital_prop_qualify": "在线检测间隔(秒)",
+		"exten_digital_prop_qualify_timeout": "在线检测超时(秒)",
+		"exten_digital_prop_nat": "NAT支持",
+		"exten_digital_prop_iax": "IAX2分机",
+		"exten_digital_prop_audio_codec": "语音编码",
+		"exten_digital_prop_permit": "受信地址",
+		"exten_digital_prop_send_pai": "发送PAI",
+		"exten_digital_prop_send_rpid": "发送RPID",
+		"exten_digital_prop_rtp_timeout": "RTP超时",
+		"exten_digital_prop_group_number": "代接组(默认 1)",
+		"exten_digital_prop_inband_progress": "Inband Progress",
+		"exten_digital_prop_white_list_tag_id": "白名单标签(编号, 默认0)"
+	},
+	"did": {
+		"did_template_file_name": "DID模板文件.xlsx",
+		"did_file_name": "DID列表文件.xlsx",
+		"did_file_worksheet_name": "DID",
+		"did_title_callDestNum": "DID号码",
+		"did_title_destType": "目的地类型",
+		"did_title_dest": "目的地",
+		"did_title_destTrunkType": "目的地中继类型",
+		"did_title_destTrunkAddNumber": "目的地中继送号",
+		"did_title_ringTone": "铃声",
+		"did_error_title": "类型错误",
+		"did_error_message": "只能从下拉列表中选择值!"
+	},
+	"phonebook": {
+		"phonebook_template_file_name": "联系人模板文件.xlsx",
+		"phonebook_contact_file_name": "联系人列表.xlsx",
+		"phonebook_contact_file_worksheet_name": "联系人",
+		"phonebook_title_name": "姓名",
+		"phonebook_title_phone": "电话号码",
+		"phonebook_title_email": "邮箱",
+		"phonebook_title_company": "公司",
+		"phonebook_title_dial_code": "拨号代码"
+	},
+	"blacklist": {
+		"blacklist_template_file_name": "黑名单模板文件.xlsx",
+		"blacklist_file_name": "黑名单列表.xlsx",
+		"blacklist_file_worksheet_name": "黑名单",
+		"blacklist_title_number": "电话号码",
+		"blacklist_title_createUser": "创建者",
+		"blacklist_title_createTime": "创建时间"
+	},
+	"trunk": {
+		"trunk_sip_file_name": "sip中继.xlsx",
+		"trunk_sip_worksheet_name": "sip",
+		"trunk_sip_prop_name": "中继名",
+		"trunk_sip_prop_enable": "启用",
+		"trunk_sip_prop_trunk_type": "中继类型",
+		"trunk_sip_prop_auth": "认证",
+		"trunk_sip_prop_server": "主机",
+		"trunk_sip_prop_port": "端口",
+		"trunk_sip_prop_auth_user": "认证用户",
+		"trunk_sip_prop_username": "用户名",
+		"trunk_sip_prop_password": "密码",
+		"trunk_sip_prop_contact": "联系人头域",
+		"trunk_sip_prop_regist_expiration": "注册超时",
+		"trunk_sip_prop_retry_interval": "重试间隔",
+		"trunk_sip_prop_max_retry": "最大重试次数",
+		"trunk_sip_prop_identify_by": "认证方式",
+		"trunk_sip_prop_max_expiration": "最大超时时间",
+		"trunk_sip_prop_fax_detect": "传真检测",
+		"trunk_sip_prop_qualify": "保活",
+		"trunk_sip_prop_srtp": "SRTP",
+		"trunk_sip_prop_nat": "NAT支持",
+		"trunk_sip_prop_client_uri": "客户端URI",
+		"trunk_sip_prop_transport": "传输协议",
+		"trunk_sip_prop_server_uri": "服务端URI",
+		"trunk_sip_prop_language": "提示音语言",
+		"trunk_sip_prop_aor_contact": "AOR Contact",
+		"trunk_sip_prop_call_max": "最高并发",
+		"trunk_sip_prop_recording": "通话录音",
+		"trunk_sip_prop_cid_preferred": "优先使用(外呼显示号码)",
+		"trunk_sip_prop_from_user": "来自用户",
+		"trunk_sip_prop_outbound_cid": "外呼显示号码",
+		"trunk_sip_prop_dtmf": "DTMF模式",
+		"trunk_sip_prop_dial_plan_id": "拨号权限(编号,默认是0)",
+		"trunk_sip_prop_from_domain": "From头域",
+		"trunk_sip_prop_send_pai": "发送PAI",
+		"trunk_sip_prop_send_rpid": "发送RPID",
+		"trunk_sip_prop_rtp_timeout": "RTP超时",
+		"trunk_sip_prop_video_codec": "视频编码",
+		"trunk_sip_prop_audio_codec": "语音编码",
+		"trunk_sip_prop_out_proxy": "Proxy",
+		"trunk_sip_prop_out_proxy_port": "Port"
+	},
+	"bill": {
+		"bill_rate_file_name": "计费费率.xlsx",
+		"bill_rate_file_worksheet_name": "费率列表",
+		"bill_rate_prop_enable": "启用",
+		"bill_rate_prop_remark": "备注",
+		"bill_rate_prop_prefix": "前缀",
+		"bill_rate_prop_number_length": "号码长度",
+		"bill_rate_prop_init_cost": "初始费用",
+		"bill_rate_prop_init_time": "初始时间",
+		"bill_rate_prop_rate": "费率",
+		"bill_rate_prop_bill_unit": "计费周期(秒)",
+		"bill_rate_prop_bill_time_start": "开始时间",
+		"bill_rate_prop_bill_time_end": "结束时间",
+		"bill_rate_prop_extension_list": "分机",
+		"bill_charge_log":"充值记录.xlsx",
+		"bill_charge_time":"充值时间",
+		"bill_charge_extension":"分机",
+		"bill_charge_pre_balance":"充值前余额",
+		"bill_charge_charge_amount":"充值金额",
+		"bill_charge_post_balance":"充值后余额",
+		"bill_call_log":"通话记录.xlsx",
+		"bill_call_time":"开始时间",
+		"bill_call_src":"主叫",
+		"bill_call_dst":"被叫",
+		"bill_call_duration":"时长",
+		"bill_call_trunk":"中继名",
+		"bill_call_cast":"话费",
+		"bill_statistics_file_name": "计费通话统计.xlsx",
+		"bill_statistics_file_worksheet_name": "通话记录统计",
+		"bill_statistics_title_search_time": "统计方式",
+		"bill_statistics_title_extension": "分机",
+		"bill_statistics_title_call": "所有通话",
+		"bill_statistics_title_time": "总通话时长",
+		"bill_statistics_title_time_avg": "平均通话时长",
+		"bill_statistics_title_total_cost": "总话费"
+	}
+}

+ 45 - 0
web/www/index.html

@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+
+<head>
+    <title>help</title>
+</head>
+
+<body>
+<div style="display: flex;
+    flex-direction: column;
+    align-items: center;">
+    <h3>swagger</h3>
+    <p style="display: flex;
+        flex-direction: column;">
+        <a href="/pbx/swagger/index.html" target="_blank">PBX 电话系统 API</a>
+        <a href="/ginapi/swagger/index.html" target="_blank">Panel API</a>
+        <a href="/bill/swagger/index.html" target="_blank">Bill API</a>
+        <a href="/webuser/swagger/index.html" target="_blank">WebUser API</a>
+    </p>
+</div>
+
+<div style="display: flex;
+    flex-direction: column;
+    align-items: center;">
+    <h3>debug</h3>
+    <p style="display: flex;
+        flex-direction: column;">
+        <a href="/debug/pprof/" target="_blank">debug pprof</a>
+    </p>
+</div>
+
+<div style="display: flex;
+    flex-direction: column;
+    align-items: center;">
+    <h3>socket.io</h3>
+    <p style="display: flex;
+        flex-direction: column;">
+        <a href="/socketio-client-tool" target="_blank">SocketIO</a>
+    </p>
+</div>
+
+<body>
+
+</html>