diff --git a/call/config.go b/call/config.go index f046581..303bed1 100644 --- a/call/config.go +++ b/call/config.go @@ -64,6 +64,10 @@ var ( configTgRobot []*common.ConfigTgRobot configBetDraw []*common.ConfigActivityBetDraw configActivityPopup []*common.ConfigActivityPopup + // 客服 + configCustomerRobot []*common.ConfigCustomerRobot + customerOrderLabel []*common.CustomerOrderLabel + configCustomer *common.ConfigCustomer ) var ( diff --git a/call/customer.go b/call/customer.go new file mode 100644 index 0000000..99c42d2 --- /dev/null +++ b/call/customer.go @@ -0,0 +1,96 @@ +package call + +import ( + "server/common" + "server/db" + + "github.com/liangdas/mqant/log" +) + +// LoadConfigCustomerRobot 客服机器人配置 +func LoadConfigCustomerRobot() (err error) { + var one []*common.ConfigCustomerRobot + if _, err = db.Mysql().QueryAll("", "", &common.ConfigCustomerRobot{}, &one); err != nil { + log.Error("err:%v", err) + return err + } + configCustomerRobot = one + return nil +} + +// GetCustomerRobot 获取机器人配置 +func GetCustomerRobot(parentId int) []*common.ConfigCustomerRobot { + if configCustomerRobot == nil { + if LoadConfigCustomerRobot() != nil { + return nil + } + } + var res []*common.ConfigCustomerRobot + for i := 0; i < len(configCustomerRobot); i++ { + if configCustomerRobot[i].ParentId == parentId { + res = append(res, configCustomerRobot[i]) + } + } + return res +} + +// GetCustomerRobotById 获取机器人配置 +func GetCustomerRobotById(parentId int) *common.ConfigCustomerRobot { + if configCustomerRobot == nil { + if LoadConfigCustomerRobot() != nil { + return nil + } + } + for i := 0; i < len(configCustomerRobot); i++ { + if configCustomerRobot[i].ID == parentId { + return configCustomerRobot[i] + } + } + return nil +} + +// GetCustomerRobotMsg 机器人人工服务 +func GetCustomerRobotMsg(Id int) *common.ConfigCustomerRobot { + if configCustomerRobot == nil { + if LoadConfigCustomerRobot() != nil { + return nil + } + } + for i := 0; i < len(configCustomerRobot); i++ { + if configCustomerRobot[i].ParentId == Id { + return configCustomerRobot[i] + } + } + return nil +} + +// LoadConfigCustomerLabel 客服机器人配置 +func LoadConfigCustomerLabel() (err error) { + var one []*common.CustomerOrderLabel + if _, err = db.Mysql().QueryAll("", "", &common.CustomerOrderLabel{}, &one); err != nil { + log.Error("err:%v", err) + return err + } + customerOrderLabel = one + return nil +} + +// LoadConfigCustomer 客服字段配置 +func LoadConfigCustomer() (err error) { + one := new(common.ConfigCustomer) + if err = db.Mysql().Get(one); err != nil { + return err + } + configCustomer = one + return nil +} + +// GetConfigCustomer 获取客服字段配置 +func GetConfigCustomer() *common.ConfigCustomer { + if configCustomer == nil { + if LoadConfigCustomer() != nil { + return nil + } + } + return configCustomer +} diff --git a/call/reload.go b/call/reload.go index 4a50267..92d8e2c 100644 --- a/call/reload.go +++ b/call/reload.go @@ -466,4 +466,33 @@ func CommonReload(c map[int][]func(*pb.ReloadGameConfig) error) { return nil }} } + // 客服 + if _, ok := c[common.ReloadConfigCustomerRobot]; !ok { + c[common.ReloadConfigCustomerRobot] = []func(*pb.ReloadGameConfig) error{func(rgc *pb.ReloadGameConfig) error { + if err := LoadConfigCustomerRobot(); err != nil { + log.Error("error : [%s]", err.Error()) + return err + } + return nil + }} + } + if _, ok := c[common.ReloadConfigCustomerLabel]; !ok { + c[common.ReloadConfigCustomerLabel] = []func(*pb.ReloadGameConfig) error{func(rgc *pb.ReloadGameConfig) error { + if err := LoadConfigCustomerLabel(); err != nil { + log.Error("error : [%s]", err.Error()) + return err + } + return nil + }} + } + if _, ok := c[common.ReloadConfigCustomer]; !ok { + c[common.ReloadConfigCustomer] = []func(*pb.ReloadGameConfig) error{func(rgc *pb.ReloadGameConfig) error { + if err := LoadConfigCustomer(); err != nil { + log.Error("error : [%s]", err.Error()) + return err + } + return nil + }} + } + // ///// } diff --git a/common/config.go b/common/config.go index 8ff7751..dde4b84 100644 --- a/common/config.go +++ b/common/config.go @@ -52,6 +52,10 @@ const ( ReloadConfigTgRobot // tg机器人配置 ReloadConfigBetDraw // 下注抽奖 ReloadConfigActivityPopup // 活动弹窗 + + ReloadConfigCustomerRobot // 客服系统机器人配置 + ReloadConfigCustomerLabel // 客服系统订单标签 + ReloadConfigCustomer // 客服系统订单标签 ) // GetConfigStructByType 获取相应配置的结构 diff --git a/common/customer.go b/common/customer.go new file mode 100644 index 0000000..24345c2 --- /dev/null +++ b/common/customer.go @@ -0,0 +1,92 @@ +package common + +// 客服工单状态 +const ( + CustomerOrderCreate = iota + 1 // 创建工单 + CustomerOrderAllocate // 工单已分配 + CustomerOrderProcessing // 工单处理中 + CustomerOrderDelayed // 工单延期 + CustomerOrderComplete // 工单已完成 工单到此结束 +) + +// 客服系统机器人配置 +type ConfigCustomerRobot struct { + ID int `gorm:"primarykey"` + ParentId int `gorm:"column:parent_id;default:0;type:int(11);comment:父类id 0表示一级标题" json:"ParentId" web:"ParentId"` + Label string `gorm:"column:label;type:varchar(128);comment:工单标签描述" json:"Label" web:"Label"` + Title string `gorm:"column:title;type:varchar(128);comment:标题" json:"Title" web:"Title"` + Content string `gorm:"column:content;type:varchar(1280);comment:内容" json:"Content" web:"Content"` + Image string `gorm:"column:image;type:varchar(1280);comment:图片用英文逗号分隔地址" json:"Image" web:"Image"` + IsEnd bool `gorm:"column:is_end;not null;type:bool;comment:是否截止" json:"IsEnd" web:"IsEnd"` +} + +func (c *ConfigCustomerRobot) TableName() string { + return "config_customer_robot" +} + +// 客服系统聊天配置 +type CustomerChatData struct { + ID int `gorm:"primarykey"` + Title int `gorm:"column:title;not null;type:int(11);comment:用玩家uid做聊天的title" json:"Title" web:"Title"` + Uid int `gorm:"column:uid;not null;type:int(11);comment:玩家uid或者客服人员uid" json:"Uid" web:"Uid"` + Time int64 `gorm:"column:time;type:int(11);default:0;comment:当前时间" json:"Time" web:"Time"` + Type int `gorm:"column:type;type:int(11);default:0;comment:消息类型 1表示文字消息,2表示图片消息" json:"Type" web:"Type"` + Content string `gorm:"column:content;type:varchar(1280);comment:消息内容 文字消息直接显示 图片消息用英文逗号分隔地址" json:"Content" web:"Content"` + IsRead bool `gorm:"column:is_read;type:bool;comment:消息是否已读" json:"IsRead" web:"IsRead"` +} + +func (c *CustomerChatData) TableName() string { + return "customer_chat_data" +} + +// 人工服务工单 +type CustomerOrder struct { + ID int `gorm:"primarykey"` + Uid int `gorm:"column:uid;not null;type:int(11);comment:玩家uid" json:"Uid" web:"Uid"` + Status int `gorm:"column:status;not null;type:int(11);comment:工单状态 1创建工单(未分配工单),2已分配工单,3工单处理中,4延期,5完成" json:"Status" web:"Status"` + Start int64 `gorm:"column:start;type:int(11);default:0;comment:工单开始时间" json:"Start" web:"Start"` + End int64 `gorm:"column:end;type:int(11);default:0;comment:工单结束时间" json:"End" web:"End"` + CustomerUid int `gorm:"column:customer_uid;type:int(11);default:0;comment:客服人员uid,0表示未分配工单, -1表示机器人处理的订单" json:"CustomerUid" web:"CustomerUid"` + Vip int `gorm:"column:vip;type:int(11);default:0;comment:玩家vip等级" json:"Vip" web:"Vip"` + Label1 int `gorm:"column:Label1;type:int(11);default:0;comment:处理人员标记该工单类型" json:"Label1" web:"Label1"` + Label2 int `gorm:"column:label2;type:int(11);default:0;comment:处理人员标记该工单类型" json:"Label2" web:"Label2"` + Label3 int `gorm:"column:label3;type:int(11);default:0;comment:处理人员标记该工单类型" json:"Label3" web:"Label3"` + Label4 int `gorm:"column:label4;type:int(11);default:0;comment:处理人员标记该工单类型" json:"Label4" web:"Label4"` + Label5 int `gorm:"column:label5;type:int(11);default:0;comment:处理人员标记该工单类型" json:"Label5" web:"Label5"` + UnRead int `gorm:"column:un_read;type:int(11);default:0;comment:未读消息条数" json:"UnRead" web:"UnRead"` +} + +func (c *CustomerOrder) TableName() string { + return "customer_order" +} + +// 客服标签分组 +type CustomerOrderLabel struct { + ID int `gorm:"primarykey"` + Label string `gorm:"column:label;type:varchar(20);comment:工单标签描述" json:"Label" web:"Label"` +} + +func (c *CustomerOrderLabel) TableName() string { + return "customer_order_label" +} + +// 客服配置 +type ConfigCustomer struct { + ID int `gorm:"primarykey"` + Vip int `gorm:"column:vip;type:int(11);default:3;comment:默认走人工服务vip等级" json:"Vip" web:"Vip"` +} + +func (c *ConfigCustomer) TableName() string { + return "config_customer" +} + +// 客服黑名单 +type CustomerBlackUser struct { + ID int `gorm:"primarykey"` + Time int64 `gorm:"column:time;type:int(20);default:0;comment:当前时间" json:"Time" web:"time"` + Uid int `gorm:"column:uid;not null;type:int(11);uniqueIndex:uid;comment:玩家uid" json:"Uid" web:"uid"` +} + +func (c *CustomerBlackUser) TableName() string { + return "customer_black_user" +} diff --git a/config/config.go b/config/config.go index 606437f..b6a8de0 100644 --- a/config/config.go +++ b/config/config.go @@ -108,6 +108,7 @@ type Configure struct { TotalWithdrawPer int64 // 整体总赠送比例限制 BreakLimit int64 // 低于该值弹出破产礼包 SelectID int + ImageURL string } Hall struct { FacebookParams FacebookParams @@ -137,8 +138,9 @@ type Configure struct { SettleTime int } Customer struct { - Addr string - DB string + Addr string + DB string + ImageURL string } MaxPlayerCount int64 WorkID int diff --git a/main.go b/main.go index 3717981..704bd38 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "runtime" "server/modules/common" "server/modules/crash" + "server/modules/customer" "server/modules/pay" "strconv" "time" @@ -123,6 +124,7 @@ func main() { pay.Module(), // blockpay.Module(), crash.Module(), + customer.Module(), ) if err != nil { diff --git a/modules/customer/app/response.go b/modules/customer/app/response.go new file mode 100644 index 0000000..11cb9e1 --- /dev/null +++ b/modules/customer/app/response.go @@ -0,0 +1,114 @@ +package app + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "server/modules/customer/bdb" + "server/modules/customer/values" + "time" + + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" + "gorm.io/gorm" +) + +type Gin struct { + C *gin.Context + R + User *values.User +} + +type R struct { + Data interface{} `json:"data"` + Msg string `json:"msg"` + Code int `json:"code"` +} + +func NewApp(c *gin.Context) *Gin { + g := &Gin{C: c, R: R{Code: values.CodeOK, Msg: "ok"}} + user, ex := c.Get("user") + if ex { + g.User = user.(*values.User) + if len(g.User.Channels) > 0 { + json.Unmarshal([]byte(g.User.Channels), &g.User.SChannels) + } + } + return g +} + +// Response setting gin.JSON +func (g *Gin) Response() { + if g.Code == values.CodeRetry { + g.Msg = "内部错误" + } else if g.Code == values.CodePower { + g.Msg = "您无权操作此项" + } + g.C.JSON(http.StatusOK, g.R) +} + +func (g *Gin) RecordEdit(t int, detail string) { + u, _ := g.C.Get("user") + user := u.(*values.User) + one := &values.EditHistory{Model: t, Detail: detail, Time: time.Now().Unix(), Operator: user.Name, UID: int(user.ID)} + bdb.BackDB.Create(one) +} + +func (g *Gin) S(one interface{}) (pass bool) { + err := g.C.ShouldBind(one) + if err != nil { + g.R.Code = values.CodeParam + g.R.Msg = err.Error() + log.Error("bind %v err:%v", reflect.TypeOf(one), err) + return + } + pass = true + return +} + +// U 统一json解析方法 +func (g *Gin) U(src []byte, tar interface{}) (pass bool) { + err := json.Unmarshal(src, tar) + if err != nil { + log.Error("%v err:%v", reflect.TypeOf(tar), err) + g.R.Code = values.CodeRetry + g.R.Msg = err.Error() + return + } + pass = true + return +} + +// MCommit 提交事务 +func (g *Gin) MCommit(tx *gorm.DB) { + if g.Code == values.CodeOK { + if err := tx.Commit().Error; err != nil { + tx.Rollback() + return + } + } else { + tx.Rollback() + return + } +} + +// CheckPower 检查权限是否合法 +func (g *Gin) CheckPower(power string) bool { + newMap := map[int][]int{} + if err := json.Unmarshal([]byte(power), &newMap); err != nil { + log.Error("err:%v", err) + g.Code = values.CodeParam + g.Msg = err.Error() + return false + } + for i := range newMap { + if !values.IsValidPower(i) { + log.Error("invalid power:%v", i) + g.Code = values.CodeParam + g.Msg = fmt.Sprintf("未知权限参数%v", i) + return false + } + } + return true +} diff --git a/modules/customer/bdb/exec.go b/modules/customer/bdb/exec.go new file mode 100644 index 0000000..57d1627 --- /dev/null +++ b/modules/customer/bdb/exec.go @@ -0,0 +1,21 @@ +package bdb + +import ( + "encoding/json" + "server/modules/customer/values" + + "github.com/liangdas/mqant/log" +) + +func GetPowerByRole(role int) map[int][]int { + one := &values.Role{Role: role} + BackDB.Get(one) + ret := map[int][]int{} + if len(one.Power) > 0 { + err := json.Unmarshal([]byte(one.Power), &ret) + if err != nil { + log.Error("err:%v", err) + } + } + return ret +} diff --git a/modules/customer/bdb/mysql.go b/modules/customer/bdb/mysql.go new file mode 100644 index 0000000..339fad3 --- /dev/null +++ b/modules/customer/bdb/mysql.go @@ -0,0 +1,35 @@ +package bdb + +import ( + "fmt" + "server/config" + mdb "server/db/mysql" + "server/modules/customer/values" +) + +var BackDB *mdb.MysqlClient + +func InitMysql() { + var err error + fmt.Println(config.GetConfig().Customer.DB) + BackDB, err = mdb.InitMysqlClient(config.GetConfig().Customer.DB, false) + if err != nil { + panic(err) + } +} + +// MigrateDB 自动数据库迁移 +func MigrateDB() { + err := BackDB.C().AutoMigrate(new(values.User)) + if err != nil { + panic(err) + } + err = BackDB.C().AutoMigrate(new(values.EditHistory)) + if err != nil { + panic(err) + } + err = BackDB.C().AutoMigrate(new(values.Role)) + if err != nil { + panic(err) + } +} diff --git a/modules/customer/config.go b/modules/customer/config.go new file mode 100644 index 0000000..cd72fa6 --- /dev/null +++ b/modules/customer/config.go @@ -0,0 +1,35 @@ +package customer + +import ( + "server/call" + "server/pb" +) + +func loadConfig() error { + c := map[int][]func(*pb.ReloadGameConfig) error{} + // c[common.ReloadTypeExcel] = []func(*pb.ReloadGameConfig) error{ReloadExcel} + // c[common.ReloadTypeNewControlConfig] = []func(*pb.ReloadGameConfig) error{func(rgc *pb.ReloadGameConfig) error { + // if err := call.LoadNewControlConfig(); err != nil { + // log.Error("err:%v", err) + // return err + // } + // return nil + // }} + // c[common.ReloadTypeSingleControlConfig] = []func(*pb.ReloadGameConfig) error{func(rgc *pb.ReloadGameConfig) error { + // if err := call.LoadSingleControlConfig(); err != nil { + // log.Error("err:%v", err) + // return err + // } + // return nil + // }} + call.LoadConfigs(c) + // game.LoadConfigs(c) + return nil +} + +// func ReloadExcel(c *pb.ReloadGameConfig) error { +// if err := call.LoadGoodsConfig(); err != nil { +// return err +// } +// return nil +// } diff --git a/modules/customer/handler/account/account.go b/modules/customer/handler/account/account.go new file mode 100644 index 0000000..e3f1299 --- /dev/null +++ b/modules/customer/handler/account/account.go @@ -0,0 +1,38 @@ +// 账号相关的接口实现 +package handler + +import ( + "server/common" + "server/db" + "server/modules/customer/app" + "server/modules/customer/bdb" + "server/modules/customer/values" + "server/util" + + "github.com/liangdas/mqant/log" + + "github.com/gin-gonic/gin" +) + +func Login(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.LoginReq) + if !a.S(req) { + return + } + one := &values.User{Account: req.Account, Password: req.Pass} + if err := bdb.BackDB.Get(one); err != nil { + a.Code = values.CodeParam + a.Msg = "账号不存在" + return + } + token := util.GetSimpleRandomString(6) + err := db.Redis().SetJsonData(common.GetBackendTokenKey(token), one, values.RedisTokenEx) + if err != nil { + log.Error(err.Error()) + } + a.Data = values.LoginResp{Role: one.Role, Token: token, Power: one.Power, Id: int(one.ID)} +} diff --git a/modules/customer/handler/chat/chat.go b/modules/customer/handler/chat/chat.go new file mode 100644 index 0000000..9d258ca --- /dev/null +++ b/modules/customer/handler/chat/chat.go @@ -0,0 +1,357 @@ +package chat + +import ( + "fmt" + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" + "gorm.io/gorm" + "server/call" + "server/common" + "server/db" + "server/modules/customer/app" + "server/modules/customer/values" + "server/pb" + "time" +) + +// 获取聊天历史 +func GetCustomerHistory(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.GetCustomerHistoryReq) + if !a.S(req) { + return + } + var resp values.GetCustomerHistoryResp + Count, err := db.Mysql().QueryList(req.Page-1, req.Num, fmt.Sprintf("title = %v", req.Title), "time DESC", &common.CustomerChatData{}, &resp.List) + if err != nil { + if err != gorm.ErrRecordNotFound { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } + } + resp.Count = Count + a.Data = resp +} + +// 消息已读 +func ReadMessage(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := &values.ReadMessageReq{} + if !a.S(req) { + return + } + + for i := 0; i < len(req.List); i++ { + err := db.Mysql().Update(&common.CustomerChatData{ID: req.List[i], Title: req.Title}, map[string]interface{}{"is_read": true}) + if err != nil { + log.Error(err.Error()) + } + } + + if req.OrderId != 0 { + db.Mysql().Update(&common.CustomerOrder{ID: req.OrderId}, map[string]interface{}{ + "un_read": 0, + }) + } + + // 需要通知在线玩家刷新客服消息 + // session := call.GetUserSession(req.Title) + // if session == nil { + // return + // } + // call.SendSS(session, int(pb.ServerCommonCmd_CMD_BS_CustomerMsgResp), nil, "common") +} + +// 玩家发送消息 +func SendMessage(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := &values.SendMessageReq{} + if !a.S(req) { + return + } + + req.Time = time.Now().Unix() + err := db.Mysql().Create(&req) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } + + // 需要通知在线玩家刷新客服消息 + session := call.GetUserSession(req.Title) + if session == nil { + return + } + call.SendSS(session, int(pb.ServerCommonCmd_CMD_BS_CustomerMsgResp), nil, "common") +} + +// 根据订单状态获取订单列表 +func GetCustomerOrder(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := &values.GetCustomerOrderReq{} + if !a.S(req) { + return + } + + var resp values.GetCustomerOrderResp + + var flag bool + var query string + + if req.Label != 0 { + query += fmt.Sprintf(" (label1 = %v OR label2 = %v OR label3 = %v OR label4 = %v OR label5 = %v) ", req.Label, req.Label, req.Label, req.Label, req.Label) + flag = true + } + + if req.Uid != 0 { + if flag { + query += " AND " + } + query += fmt.Sprintf(" uid = %v ", req.Uid) + flag = true + } + if req.CustomerUid != 0 { + if flag { + query += " AND " + } + query += fmt.Sprintf(" customer_uid = %v ", req.CustomerUid) + flag = true + } + + if req.Status != 0 { + if flag { + query += " AND " + } + query = fmt.Sprintf(" status = %v", req.Status) + flag = true + } else { + if req.Status2 != 0 { + if flag { + query += " AND " + } + query = fmt.Sprintf(" status <= %v", req.Status2) + flag = true + } + } + if req.Vip != 0 { + if flag { + query += " AND " + } + query += fmt.Sprintf(" vip >= %v ", req.Vip) + flag = true + } + + var order = "start DESC" + if req.Order == 2 { + order = "start ASC" + } + if req.Order == 3 { + order = "un_read DESC" + } + + count, err := db.Mysql().QueryList(req.Page-1, req.Num, query, order, &common.CustomerOrder{}, &resp.List) + if err != nil && err != gorm.ErrRecordNotFound { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } + + resp.Count = count + a.Data = resp +} + +// 工单处理状态改变 +func ChangeCustomerOrderStatus(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := &values.ChangeCustomerOrderReq{} + if !a.S(req) { + return + } + + for i := 0; i < len(req.List); i++ { + update := make(map[string]interface{}) + /*if req.List[i].Status != 0 { + update["status"] = req.List[i].Status + } + if req.List[i].Label1 != 0 { + update["label1"] = req.List[i].Label1 + } + if req.List[i].Label2 != 0 { + update["label2"] = req.List[i].Label2 + } + if req.List[i].Label3 != 0 { + update["label3"] = req.List[i].Label3 + } + if req.List[i].Label4 != 0 { + update["label4"] = req.List[i].Label4 + } + if req.List[i].Label5 != 0 { + update["label5"] = req.List[i].Label5 + } + if len(update) < 1 { + continue + }*/ + update["status"] = req.List[i].Status + update["label1"] = req.List[i].Label1 + update["label2"] = req.List[i].Label2 + update["label3"] = req.List[i].Label3 + update["label4"] = req.List[i].Label4 + update["label5"] = req.List[i].Label5 + + // 完成工单 + update["end"] = time.Now().Unix() + update["un_read"] = 0 + err := db.Mysql().Update(&common.CustomerOrder{ID: req.List[i].Id}, update) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } + } +} + +// 分配工单到客服人员 +func CustomerOrderAllocate(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := &values.CustomerOrderAllocateReq{} + if !a.S(req) { + return + } + + tx := db.Mysql().Begin() + defer a.MCommit(tx) + + for i := 0; i < len(req.List); i++ { + err := tx.Exec("UPDATE customer_order SET customer_uid = ?, status = ? WHERE id = ? AND status != ?", req.List[i].Uid, common.CustomerOrderAllocate, req.List[i].OrderId, common.CustomerOrderComplete).Error + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + } + } +} + +// 编辑标签 +func EditCustomerOrderLabel(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := &values.EditCustomerOrderLabelReq{} + if !a.S(req) { + return + } + update := make(map[string]interface{}) + update["label1"] = req.LabelId1 + update["label2"] = req.LabelId2 + update["label3"] = req.LabelId3 + update["label4"] = req.LabelId4 + update["label5"] = req.LabelId5 + err := db.Mysql().Update(&common.CustomerOrder{ID: req.OrderId}, update) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } +} + +// 获取玩家信息 +func GetPlayerInfo(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + + req := &values.GetPlayerInfoReq{} + if !a.S(req) { + return + } + + var resp values.GetPlayerInfoResp + + // 玩家信息 + player, err := call.GetUserXInfo(req.Uid, "channel_id", "nick", "avatar", "birth", "mobile") + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } + resp.Uid = req.Uid + resp.Nick = player.Nick + resp.Avatar = player.Avatar + resp.Birth = player.Birth + resp.Phone = player.Mobile + resp.Channel = player.ChannelID + + // 渠道信息 + channel := &common.Channel{ChannelID: player.ChannelID} + err = db.Mysql().Get(channel) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } + resp.Package = channel.PackName + + // vip + resp.Vip = call.GetVIP(req.Uid).Level + + order := &common.RechargeInfo{UID: req.Uid} + err = db.Mysql().Get(order) + if err != nil { + log.Error(err.Error()) + } + + resp.Withdraw = order.TotalWithdraw + resp.Recharge = order.TotalRecharge + + if db.Mysql().Exist(&common.CustomerBlackUser{Uid: req.Uid}) { + resp.CustomerBlack = true + } + + a.Data = resp +} + +// 玩家历史客诉记录 +func ComplaintHistory(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + + req := &values.ComplaintHistoryReq{} + if !a.S(req) { + return + } + + var resp values.ComplaintHistoryResp + + count, err := db.Mysql().QueryList(req.Page-1, req.Num, fmt.Sprintf("uid = %v", req.Uid), "", &common.CustomerOrder{}, &resp.List) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } + + resp.Count = count + a.Data = resp +} diff --git a/modules/customer/handler/common/common.go b/modules/customer/handler/common/common.go new file mode 100644 index 0000000..2c03034 --- /dev/null +++ b/modules/customer/handler/common/common.go @@ -0,0 +1,261 @@ +package common + +import ( + "fmt" + "io" + "mime" + "net/http" + "os" + "server/call" + "server/common" + "server/config" + "server/db" + utils "server/modules/backend/util" + "server/modules/customer/app" + "server/modules/customer/bdb" + "server/modules/customer/values" + "server/util" + "strings" + "time" + + "github.com/olivere/elastic/v7" + + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" +) + +// 上传图片 并返回图片地址 +func UploadImage(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + + // 解析请求中的 multipart/form-data + err := c.Request.ParseMultipartForm(10 << 20) // 限制上传文件的大小为 10MB + if err != nil { + log.Error("图片超过10Mb, err:%v", err.Error()) + a.Code = values.CodeParam + return + } + + // 打开上传的文件 + file, header, err := c.Request.FormFile("file") + if err != nil { + log.Error("打开上传图片失败, err:%v", err.Error()) + a.Code = values.CodeParam + return + } + defer file.Close() + + // 获取上传文件的 MIME 类型 + contentType := header.Header.Get("Content-Type") + + // 根据 MIME 类型获取文件扩展名 + ext, err := mime.ExtensionsByType(contentType) + if err != nil || len(ext) == 0 { + log.Error("获取上传图片格式失败, err:%v", err.Error()) + a.Code = values.CodeParam + return + } + imageName := call.SnowNode().Generate().String() + ext[0] + + // 读取上传的文件内容 + data, err := io.ReadAll(file) + if err != nil { + log.Error("读取图片内容失败, err:%v", err.Error()) + a.Code = values.CodeRetry + return + } + + // 获取当前本地时间 + now := time.Now() + zeroTime := now.Format("20060102") + rounded := now.Hour() + dir := fmt.Sprintf("%v/%v/%v", values.ImagePath, zeroTime, rounded) + + // 创建保存文件的目标文件 + err = createDirIfNotExist(dir) + if err != nil { + log.Error("创建图片保存目录, err:%v", err.Error()) + a.Code = values.CodeParam + return + } + + // 将上传的文件复制到目标文件 + err = os.WriteFile(dir+"/"+imageName, data, 0644) // 写入文件 + if err != nil { + a.Code = values.CodeRetry + return + } + + name := strings.Replace(dir+"/"+imageName, "/", ",", -1) + a.Data = config.GetConfig().Customer.ImageURL + name +} + +// 检测文件目录是否存在 +func createDirIfNotExist(path string) error { + // 检查目录是否存在 + _, err := os.Stat(path) + if err == nil { + // 目录已存在,直接返回 + return nil + } + + // 目录不存在,创建目录 + if os.IsNotExist(err) { + err = os.MkdirAll(path, 0755) + if err != nil { + return err + } + } + + return nil +} + +// 返回图片 +func DownImage(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + // req := &values.DownLoadImageReq{} + // if !a.S(req) { + // return + // } + + name := c.Query("path") + path := strings.Replace(name, ",", "/", -1) + + // 打开要返回的图片文件 + file, err := os.Open(path) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + defer file.Close() + + // 读取图片文件的内容 + data, err := io.ReadAll(file) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + arr := strings.Split(path, ".") + + // 返回图片 + c.Data(http.StatusOK, "image/"+arr[len(arr)-1], data) +} + +// OptList 操作日志 +func OptList(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.EditHistoryListReq) + if !a.S(req) { + return + } + resp := values.EditHistoryListResp{} + if err := bdb.BackDB.C().Order("time Desc").Limit(int(req.Num)).Offset(int((req.Page - 1) * req.Num)).Find(&resp.List).Error; err != nil { + log.Error("err:%v", err) + } + resp.Count = bdb.BackDB.Count(&values.EditHistory{}, "") + a.Data = resp +} + +func GoodList(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() +} + +func ProductList(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + products := call.GetConfigPayProduct() + resp := &values.ProductListResp{List: products} + a.Data = resp +} + +func GamesList(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + a.Data = values.GamesListResp{} +} + +func ChannelList(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + ret := call.GetChannelListReal() + if ret == nil { + a.Code = values.CodeRetry + return + } + resp := values.ChannelListResp{} + for _, v := range ret { + if v.Show != 2 { + continue + } + if len(a.User.SChannels) > 0 { + if !util.SliceContain(a.User.SChannels, v.ChannelID) { + continue + } + } + resp.List = append(resp.List, v) + } + a.Data = resp +} + +func UserInfo(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + resp := values.UserInfoResp{Name: a.User.Name, Role: a.User.Role, Power: a.User.Power} + a.Data = resp +} + +func GameInfo(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + + var resp values.GameInfoResp + + a.Data = resp +} + +func FeedbackList(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.FeedbackListReq) + if !a.S(req) { + return + } + var resp values.FeedbackListResp + su, eu := utils.GetQueryUnix(req.Start, req.End) + q := elastic.NewBoolQuery() + q.Must(elastic.NewRangeQuery("time").Gt(su)) + q.Must(elastic.NewRangeQuery("time").Lt(eu)) + var list []*common.ESFeedback + count, err := db.ES().QueryList(common.ESIndexBackFeedback, req.Page-1, req.Num, q, &list, "time", false) + if err != nil { + log.Error("err:%v", err) + } + resp.List = list + resp.Count = count + a.Data = resp +} diff --git a/modules/customer/handler/gm/getGameUserInfo.go b/modules/customer/handler/gm/getGameUserInfo.go new file mode 100644 index 0000000..3487b94 --- /dev/null +++ b/modules/customer/handler/gm/getGameUserInfo.go @@ -0,0 +1,146 @@ +package gm + +import ( + "fmt" + "server/call" + "server/common" + "server/db" + "server/modules/backend/models" + "server/modules/backend/values" + "server/modules/customer/app" + uutil "server/util" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" + "github.com/olivere/elastic/v7" +) + +func GetGameUserInfo(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.GetGameUserInfoReq) + if !a.S(req) { + return + } + user := &common.PlayerDBInfo{} + switch req.Data.(type) { + case int, int64, float32, float64: + id := uutil.GetInt(req.Data) + if id <= 999999999 { + user.Id = id + } else { + user.Mobile = strconv.Itoa(id) + } + case string: + str := req.Data.(string) + if len(str) == 10 { + user.Mobile = str + } else { + id, err := strconv.Atoi(str) + if err != nil { + a.Code = values.CodeParam + a.Msg = "玩家不存在" + return + } + user.Id = id + } + } + err := db.Mysql().Get(user) + if err != nil { + log.Error("err:%v", err) + a.Code = values.CodeParam + a.Msg = "玩家不存在" + return + } + uid := user.Id + ur, _ := call.GetUserXInfo(uid, "online") + info := &common.RechargeInfo{UID: uid} + db.Mysql().Get(info) + lg := &common.LoginRecord{UID: uid} + db.Mysql().GetLast(lg) + resp := values.GetGameUserInfoResp{ + UID: user.Id, + Nick: user.Nick, + Channel: user.ChannelID, + Phone: user.Mobile, + OpenID: *user.Openid, + Online: ur.Online == common.PlayerOnline, + Status: user.Status, + Birth: user.Birth, + // PlayCount: util.GetGUserPlayCount(uid), + IP: user.IP, + LastLogin: lg.Time, + Tag: user.Tag, + UserGameData: GetUserGameInfo(user.Id), + OutputData: models.GetOutputData(0, 0, 0, uid), + Gpsadid: user.DeviceId, + } + + // re := call.GetRechargeInfo(uid) + // resp.Recharge = re.TotalRecharge + // resp.Withdraw = re.TotalWithdraw + // resp.NeedBet = call.GetUserNeedBet(uid) + // resp.Cash = call.GetUserCurrency(uid, common.CurrencyINR) + db.Mysql().C().Table("users").Select("id").Where("deviceid = ?", user.DeviceId).Scan(&resp.SubAccount) + a.Data = resp +} + +// 获取用户游戏信息 +func GetUserGameInfo(uid int) map[string][]values.UserGameInfo { + q := elastic.NewBoolQuery() + q.Filter(elastic.NewTermQuery("uid", uid)) + q.Filter(elastic.NewTermsQuery("event", common.GetGameEvents()...)) + buk, err := db.ES().Group2SumBy(common.ESIndexBalance, "exi1", "exi2", "value", q, "", false, 0) + if err != nil { + log.Error("err:%v", err) + } + ret := map[string][]values.UserGameInfo{} + var totalCount, totalProfit int64 + for _, v := range buk.Buckets { + provider := call.GetConfigGameProvider(uutil.GetInt(v.Key)) + if provider == nil { + continue + } + games := []values.UserGameInfo{} + for _, k := range v.Sub1.Buckets { + thisGame := call.GetConfigGameListByGameID(provider.ProviderID, uutil.GetInt(k.Key)) + if thisGame == nil { + continue + } + count := uutil.GetInt64(k.Doc_count) + profit := uutil.GetInt64(k.Sub2.Value) + totalCount += count + totalProfit += profit + games = append(games, values.UserGameInfo{ + GameName: thisGame.Name + fmt.Sprintf("(%d)", thisGame.GameID), + GameCount: count, + Profit: profit, + }) + } + ret[provider.ProviderName] = games + } + ret["总计"] = []values.UserGameInfo{{GameCount: totalCount, Profit: totalProfit}} + return ret +} + +// func getUserGameData(uid int, gameId *int, win, lost *bool) values.UserGameInfo { +// var userGameInfo values.UserGameInfo +// if gameId == nil { +// userGameInfo.GameId = 0 +// } else { +// userGameInfo.GameId = *gameId +// } +// userGameInfo.GameCount = models.GetWinGameCountByBalance(nil, nil, &uid, nil, gameId, nil, nil) +// userGameInfo.WinCount = models.GetWinGameCountByBalance(nil, nil, &uid, nil, gameId, nil, win) +// userGameInfo.LoseCount = models.GetWinGameCountByBalance(nil, nil, &uid, nil, gameId, nil, lost) + +// userGameInfo.Profit = models.GetGameProfitByUid(nil, nil, &uid, nil, gameId, nil, nil) +// userGameInfo.WinProfit = models.GetGameProfitByUid(nil, nil, &uid, nil, gameId, nil, win) +// userGameInfo.LoseProfit = models.GetGameProfitByUid(nil, nil, &uid, nil, gameId, nil, lost) +// userGameInfo.WinPer = util.GetPer(userGameInfo.WinCount, userGameInfo.GameCount) + +// return userGameInfo +// } diff --git a/modules/customer/handler/gm/gm.go b/modules/customer/handler/gm/gm.go new file mode 100644 index 0000000..3f265c8 --- /dev/null +++ b/modules/customer/handler/gm/gm.go @@ -0,0 +1,401 @@ +package gm + +import ( + "fmt" + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" + "server/call" + "server/common" + "server/db" + "server/modules/customer/app" + "server/modules/customer/values" + "server/natsClient" + "server/pb" + "server/util" +) + +// 获取客服机器人消息 +func GetCustomerRobot(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + + req := &values.GetCustomerRobotReq{} + if !a.S(req) { + return + } + + resp := values.GetCustomerRobotResp{} + if _, err := db.Mysql().QueryAll(fmt.Sprintf("parent_id = %v", req.ParentId), "", &common.ConfigCustomerRobot{}, &resp.List); err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + + a.Data = resp +} + +// 编辑客服机器人消息 +func EditCustomerRobot(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := &values.EditCustomerRobotReq{} + if !a.S(req) { + return + } + + var resp values.EditCustomerRobotResp + + for i := 0; i < len(req.List); i++ { + if req.List[i] != nil { + // 新增 + if req.List[i].ID == 0 { + log.Debug("req:%+v", req.List[i]) + err := db.Mysql().Create(req.List[i]) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } + log.Debug("req:%+v", req.List[i]) + resp.List = append(resp.List, req.List[i]) + } else { + // 修改 + log.Debug("req:%+v", req.List[i]) + u := &common.ConfigCustomerRobot{} + u.ID = req.List[i].ID + update := util.StructToMap(req.List[i], "json") + if err := db.Mysql().Update(u, update); err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + } + } + } + + err := call.Publish(natsClient.TopicReloadConfig, &pb.ReloadGameConfig{Type: common.ReloadConfigCustomerRobot}) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } + + // 写入日志 + for i := 0; i < len(req.List); i++ { + if req.List[i] != nil { + a.RecordEdit(values.PowerGM, fmt.Sprintf("编辑客服机器人配置:%v", *req.List[i])) + } + } + a.Data = resp +} + +// 删除客服机器人消息 +func DelCustomerRobot(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := &values.DelCustomerRobotReq{} + if !a.S(req) { + return + } + + for i := 0; i < len(req.List); i++ { + if req.List[i] != nil { + log.Debug("req:%+v", *req.List[i]) + err := db.Mysql().Del(&common.ConfigCustomerRobot{}, " id = ?", *req.List[i]) + if err != nil { + log.Error(err.Error()) + } + } + } + + err := call.Publish(natsClient.TopicReloadConfig, &pb.ReloadGameConfig{Type: common.ReloadConfigCustomerRobot}) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + } + + // 写入日志 + for i := 0; i < len(req.List); i++ { + if req.List[i] != nil { + a.RecordEdit(values.PowerGM, fmt.Sprintf("删除客服机器人配置:%v", *req.List[i])) + } + } +} + +// 获取标签 +func GetCustomerLabel(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + + resp := values.GetCustomerLabelResp{} + if _, err := db.Mysql().QueryAll("", "", &common.CustomerOrderLabel{}, &resp.List); err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + + a.Data = resp +} + +// 编辑标签 +func EditCustomerLabel(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := &values.EditCustomerLabelReq{} + if !a.S(req) { + return + } + + for i := 0; i < len(req.List); i++ { + if req.List[i] != nil { + // 新增 + if req.List[i].ID == 0 { + log.Debug("req:%+v", req.List[i]) + err := db.Mysql().Create(req.List[i]) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } + } else { + // 修改 + log.Debug("req:%+v", req.List[i]) + u := &common.CustomerOrderLabel{} + u.ID = req.List[i].ID + update := util.StructToMap(req.List[i], "json") + if err := db.Mysql().Update(u, update); err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + } + } + } + + err := call.Publish(natsClient.TopicReloadConfig, &pb.ReloadGameConfig{Type: common.ReloadConfigCustomerLabel}) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + } + + // 写入日志 + for i := 0; i < len(req.List); i++ { + if req.List[i] != nil { + a.RecordEdit(values.PowerGM, fmt.Sprintf("编辑工单标签配置:%v", *req.List[i])) + } + } +} + +// 删除标签 +func DelCustomerLabel(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := &values.DelCustomerLabelReq{} + if !a.S(req) { + return + } + + for i := 0; i < len(req.List); i++ { + if req.List[i] != nil { + log.Debug("req:%+v", req.List[i]) + err := db.Mysql().Del(&common.ConfigCustomerRobot{}, " id = ?", *req.List[i]) + if err != nil { + log.Error(err.Error()) + } + } + } + + err := call.Publish(natsClient.TopicReloadConfig, &pb.ReloadGameConfig{Type: common.ReloadConfigCustomerLabel}) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + } + + // 写入日志 + for i := 0; i < len(req.List); i++ { + if req.List[i] != nil { + a.RecordEdit(values.PowerGM, fmt.Sprintf("删除工单标签配置:%v", *req.List[i])) + } + } +} + +// 客服系统配置 +func GetConfigCustomer(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + + resp := values.GetConfigCustomerResp{} + if _, err := db.Mysql().QueryAll("", "", &common.ConfigCustomer{}, &resp.List); err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + + a.Data = resp +} + +// 客服系统配置 +func EditConfigCustomer(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := &values.EditConfigCustomerReq{} + if !a.S(req) { + return + } + + for i := 0; i < len(req.List); i++ { + if req.List[i] != nil { + // 新增 + if req.List[i].ID == 0 { + log.Debug("req:%+v", req.List[i]) + err := db.Mysql().Create(req.List[i]) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } + } else { + // 修改 + log.Debug("req:%+v", req.List[i]) + u := &common.ConfigCustomer{} + u.ID = req.List[i].ID + update := util.StructToMap(req.List[i], "json") + if err := db.Mysql().Update(u, update); err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + } + } + } + + err := call.Publish(natsClient.TopicReloadConfig, &pb.ReloadGameConfig{Type: common.ReloadConfigCustomer}) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + } + + // 写入日志 + for i := 0; i < len(req.List); i++ { + if req.List[i] != nil { + a.RecordEdit(values.PowerGM, fmt.Sprintf("编辑客服系统配置:%v", *req.List[i])) + } + } +} + +// 客服系统配置 +func DelConfigCustomer(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := &values.DelConfigCustomerReq{} + if !a.S(req) { + return + } + + for i := 0; i < len(req.List); i++ { + if req.List[i] != nil { + log.Debug("req:%+v", req.List[i]) + err := db.Mysql().Del(&common.ConfigCustomer{}, " id = ?", *req.List[i]) + if err != nil { + log.Error(err.Error()) + } + } + } + + err := call.Publish(natsClient.TopicReloadConfig, &pb.ReloadGameConfig{Type: common.ReloadConfigCustomer}) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + } + + // 写入日志 + for i := 0; i < len(req.List); i++ { + if req.List[i] != nil { + a.RecordEdit(values.PowerGM, fmt.Sprintf("删除客服系统配置:%v", *req.List[i])) + } + } +} + +// 客服黑名单 +func GetCustomerBlackUser(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + + req := &values.CustomerBlackUserReq{} + if !a.S(req) { + return + } + + var resp values.CustomerBlackUserResp + count, err := db.Mysql().QueryList(req.Page-1, req.Num, "", "", &common.CustomerBlackUser{}, &resp.List) + if err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + resp.Count = count + a.Data = resp +} + +// 客服黑名单 +func EditCustomerBlackUser(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := &values.EditCustomerBlackUserReq{} + if !a.S(req) { + return + } + + for i := 0; i < len(req.List); i++ { + if req.List[i] != nil { + log.Debug("opt:%v, req:%+v", req.Opt, *req.List[i]) + if req.Opt == 1 { + err := db.Mysql().Create(&common.CustomerBlackUser{Uid: *req.List[i]}) + if err != nil { + log.Error(err.Error()) + } + } else { + data := &common.CustomerBlackUser{Uid: *req.List[i]} + err := db.Mysql().Get(data) + if err == nil { + db.Mysql().Del(data) + } + } + } + } + + var str string + if req.Opt == 1 { + str = "添加" + } else { + str = "删除" + } + // 写入日志 + for i := 0; i < len(req.List); i++ { + if req.List[i] != nil { + a.RecordEdit(values.PowerGM, fmt.Sprintf("%v客服系统黑名单 uid:%v", str, *req.List[i])) + } + } +} diff --git a/modules/customer/handler/guser/activeUserList.go b/modules/customer/handler/guser/activeUserList.go new file mode 100644 index 0000000..0d7cac4 --- /dev/null +++ b/modules/customer/handler/guser/activeUserList.go @@ -0,0 +1,121 @@ +package guser + +import ( + "fmt" + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" + "github.com/olivere/elastic/v7" + "server/common" + "server/db" + utils "server/modules/backend/util" + "server/modules/backend/values" + "server/modules/customer/app" + "server/util" +) + +func ActiveUserList(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.ActiveUserListReq) + if !a.S(req) { + return + } + var resp values.ActiveUserListResp + + su, eu := utils.GetQueryUnix(req.Start, req.End) + + queryCount := " SELECT COUNT(*) FROM login_record WHERE " + + str := fmt.Sprintf(" time >= %d AND time < %d ", su, eu) + + if req.Channel != nil { + str += fmt.Sprintf(" AND channel_id = %d ", *req.Channel) + } + + if req.Status != nil { + str += fmt.Sprintf(" AND status = %v ", req.Status) + } + + var count int64 + err := db.Mysql().QueryBySql(queryCount+str, &count) + if err != nil { + log.Error(err.Error()) + } + + sql := fmt.Sprintf( + `SELECT d.id,d.nick,d.status,d.birth,d.cash,d.bind_cash,d.online,d.time,IFNULL(c.total_charge,0) as total_charge,IFNULL(c.total_withdraw,0) as total_withdraw from + (SELECT id,nick,status,birth,cash,bind_cash,online,b.time from users as a + INNER JOIN + (SELECT uid,max(time) as time from login_record WHERE time >= %d and time < %d GROUP BY uid) as b + on a.id = b.uid + LIMIT %d,%d + ) as d + LEFT JOIN + (SELECT uid,total_charge,total_withdraw from recharge_info) as c + on d.id = c.uid`, su, eu, (req.Page-1)*req.Num, req.Num) + + list := []values.ActiveUserOne{} + + if err := db.Mysql().C().Raw(sql).Scan(&list).Error; err != nil { + log.Error("err:%v", err) + } + + uids := []interface{}{} + for _, v := range list { + uids = append(uids, v.ID) + resp.List = append(resp.List, values.ActiveUserInfo{ + UID: int64(v.ID), + Nick: v.Nick, + Status: v.Status, + Birth: v.Birth, + LastLogin: v.Time, + Recharge: v.TotalCharge, + Withdraw: v.TotalWithdraw, + Cash: v.Cash, + TotalCash: v.Cash + v.BindCash, + Online: v.Online, + }) + } + + q := elastic.NewBoolQuery() + q.Filter(elastic.NewRangeQuery("time").Gte(list[0].Birth)) + q.Filter(elastic.NewRangeQuery("event").Gte(common.CurrencyEventGameSettle)) + q.Filter(elastic.NewRangeQuery("event").Lt(common.CurrencyEventGameBet)) + q.Must(elastic.NewTermsQuery("uid", uids...)) + totalGameCount, _ := db.ES().GroupBy(common.ESIndexBalance, "uid", q, len(list)) + q.Filter(elastic.NewRangeQuery("value").Gt(0)) + winGameCount, _ := db.ES().GroupBy(common.ESIndexBalance, "uid", q, len(list)) + q = elastic.NewBoolQuery() + q.Filter(elastic.NewRangeQuery("time").Gte(su)) + q.Filter(elastic.NewRangeQuery("time").Lt(eu)) + q.Filter(elastic.NewRangeQuery("event").Gte(common.CurrencyEventGameSettle)) + q.Filter(elastic.NewRangeQuery("event").Lt(common.CurrencyEventGameBet)) + q.Must(elastic.NewTermsQuery("uid", uids...)) + todayGameCount, _ := db.ES().GroupBy(common.ESIndexBalance, "uid", q, len(list)) + for i, v := range resp.List { + for _, u := range totalGameCount.Buckets { + if util.GetInt64(u.Key) == v.UID { + resp.List[i].GameCount = int64(u.Doc_count) + break + } + } + var winCount int64 + for _, u := range winGameCount.Buckets { + if util.GetInt64(u.Key) == v.UID { + winCount = int64(u.Doc_count) + break + } + } + resp.List[i].WinPer = utils.GetPer(winCount, resp.List[i].GameCount) + for _, u := range todayGameCount.Buckets { + if util.GetInt64(u.Key) == v.UID { + resp.List[i].TodayCount = int64(u.Doc_count) + break + } + } + } + resp.Count = count + a.Data = resp +} diff --git a/modules/customer/handler/guser/addUserBlackList.go b/modules/customer/handler/guser/addUserBlackList.go new file mode 100644 index 0000000..4963e84 --- /dev/null +++ b/modules/customer/handler/guser/addUserBlackList.go @@ -0,0 +1,86 @@ +package guser + +import ( + "fmt" + "server/call" + "server/common" + "server/db" + "server/modules/backend/values" + "server/modules/customer/app" + + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" +) + +// 将用户相关信息拉入黑名单 +func AddUserBlackList(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.AddUserBlackListReq) + if !a.S(req) { + return + } + user, err := call.GetUserInfo(req.UID) + if err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + list := []common.RechargeOrder{} + db.Mysql().QueryList(0, 20, fmt.Sprintf("uid = %v and event = %v", user.Id, common.CurrencyEventWithDraw), "", common.RechargeOrder{}, &list) + banAccounts := map[string]struct{}{} + for _, v := range list { + info := common.WithdrawCommon{} + json.Unmarshal([]byte(v.PayAccount), &info) + acc := "" + if info.PayType == common.PayTypeBank { + if info.BankCardNo != "" { + acc = info.BankCardNo + } + } else if info.PayType == common.PayTypeUPI { + if info.BankCode != "" { + acc = info.BankCode + } + } + if acc == "" { + continue + } + if _, ok := banAccounts[acc]; !ok { + banAccounts[acc] = struct{}{} + } + } + if len(banAccounts) == 0 { + black := &common.BlackList{ + Phone: user.Mobile, + IP: user.IP, + } + if user.DeviceId != "00000000-0000-0000-0000-000000000000" { + black.DeviceID = user.DeviceId + } + err := db.Mysql().Create(black) + if err != nil { + log.Error("err:%v", err) + a.Code = values.CodeParam + a.Msg = "该用户信息已在黑名单里" + } + } else { + for k := range banAccounts { + black := &common.BlackList{ + Phone: user.Mobile, + IP: user.IP, + PayAccount: k, + } + if user.DeviceId != "00000000-0000-0000-0000-000000000000" { + black.DeviceID = user.DeviceId + } + err := db.Mysql().Create(black) + if err != nil { + log.Error("err:%v", err) + a.Code = values.CodeParam + a.Msg = "该用户信息已在黑名单里" + } + } + } +} diff --git a/modules/customer/handler/guser/addUserTag.go b/modules/customer/handler/guser/addUserTag.go new file mode 100644 index 0000000..c8a4c7a --- /dev/null +++ b/modules/customer/handler/guser/addUserTag.go @@ -0,0 +1,31 @@ +package guser + +import ( + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" + "server/common" + "server/db" + "server/modules/backend/values" + "server/modules/customer/app" +) + +func AddUserTag(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.UserTagReq) + if !a.S(req) { + return + } + + err := db.Mysql().C().Model(&common.PlayerDBInfo{}).Where("id = ?", req.Uid).Updates(map[string]interface{}{ + "tag": req.Tag, + }).Error + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } + +} diff --git a/modules/customer/handler/guser/banUserList.go b/modules/customer/handler/guser/banUserList.go new file mode 100644 index 0000000..cb399db --- /dev/null +++ b/modules/customer/handler/guser/banUserList.go @@ -0,0 +1,57 @@ +package guser + +import ( + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" + "github.com/olivere/elastic/v7" + "server/call" + "server/common" + "server/db" + "server/modules/backend/models" + utils "server/modules/backend/util" + "server/modules/backend/values" + "server/modules/customer/app" +) + +func BanUserList(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.BanUserListReq) + if !a.S(req) { + return + } + var resp values.BanUserListResp + su, eu := utils.GetQueryUnix(req.Start, req.End) + + q := elastic.NewBoolQuery() + q.Must(elastic.NewRangeQuery("Time").Gt(su)) + q.Must(elastic.NewRangeQuery("Time").Lt(eu)) + + var list []*common.ESBlackList + + count, err := db.ES().QueryList(common.ESIndexBackBlackList, req.Page-1, req.Num, q, &list, "Time", false) + if err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + + for i := 0; i < len(list); i++ { + user, err := call.GetUserInfo(list[i].UID) + if err != nil { + log.Error(err.Error()) + continue + } + resp.List = append(resp.List, &values.BanUserInfo{ + ESBlackList: *list[i], + Birth: user.Birth, + Recharge: models.GetRechargeTotalByUid(&user.Id), + Withdraw: models.GetWithdrawTotalByUid(&user.Id), + Channel: user.ChannelID, + }) + } + resp.Count = count + a.Data = resp +} diff --git a/modules/customer/handler/guser/bigRUserData.go b/modules/customer/handler/guser/bigRUserData.go new file mode 100644 index 0000000..a27bd0a --- /dev/null +++ b/modules/customer/handler/guser/bigRUserData.go @@ -0,0 +1 @@ +package guser diff --git a/modules/customer/handler/guser/controlCardData.go b/modules/customer/handler/guser/controlCardData.go new file mode 100644 index 0000000..a27bd0a --- /dev/null +++ b/modules/customer/handler/guser/controlCardData.go @@ -0,0 +1 @@ +package guser diff --git a/modules/customer/handler/guser/editGameUserGold.go b/modules/customer/handler/guser/editGameUserGold.go new file mode 100644 index 0000000..a27bd0a --- /dev/null +++ b/modules/customer/handler/guser/editGameUserGold.go @@ -0,0 +1 @@ +package guser diff --git a/modules/customer/handler/guser/editGameUserStatus.go b/modules/customer/handler/guser/editGameUserStatus.go new file mode 100644 index 0000000..a58347c --- /dev/null +++ b/modules/customer/handler/guser/editGameUserStatus.go @@ -0,0 +1,56 @@ +package guser + +import ( + "fmt" + "server/call" + "server/common" + "server/db" + "server/modules/backend/values" + "server/modules/customer/app" + "server/natsClient" + "server/pb" + + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" +) + +func EditGameUserStatus(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.EditGameUserStatusReq) + if !a.S(req) { + return + } + if req.Status <= common.AccountStatus || req.Status >= common.AccountStatusAll { + a.Code = values.CodeParam + a.Msg = "请求状态不合法" + return + } + user, err := call.GetUserInfo(req.UID) + if err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + if user.Status == req.Status { + a.Code = values.CodeParam + a.Msg = "无内容修改" + return + } + if err = call.UpdateUserXInfo(&common.PlayerDBInfo{Id: req.UID}, map[string]interface{}{"status": req.Status}); err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + if req.Status == common.AccountStatusLimit { // 封禁账号要通知全服,踢出玩家 + db.Redis().Delkey(common.GetRedisKeyToken(db.Redis().GetUserToken(req.UID))) + db.Redis().Delkey(common.GetRedisKeyUser(req.UID)) + err = call.Publish(natsClient.TopicInnerOptPlayer, &pb.InnerOptPlayer{UID: uint32(req.UID), Opt: common.OptPlayerTypeKick}) + if err != nil { + log.Error(err.Error()) + } + } + a.RecordEdit(values.PowerGUser, fmt.Sprintf("修改玩家%v状态:%v", req.UID, req.Status)) +} diff --git a/modules/customer/handler/guser/getGameUserAllBalance.go b/modules/customer/handler/guser/getGameUserAllBalance.go new file mode 100644 index 0000000..5b28ec4 --- /dev/null +++ b/modules/customer/handler/guser/getGameUserAllBalance.go @@ -0,0 +1,34 @@ +package guser + +import ( + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" + "server/common" + "server/db" + "server/modules/backend/values" + "server/modules/customer/app" +) + +func GetGameUserAllBalance(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.GetGameUserAllBalanceReq) + if !a.S(req) { + return + } + resp := values.GetGameUserAllBalanceResp{ + List: []common.CurrencyBalance{}, + } + var count int64 + var err error + count, err = db.ES().QueryPlayerAllBalance(&req.UID, req.Page-1, req.Num, req.Start, req.End, &resp.List) + if err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + resp.Count = count + a.Data = resp +} diff --git a/modules/customer/handler/guser/getGameUserControlBalance.go b/modules/customer/handler/guser/getGameUserControlBalance.go new file mode 100644 index 0000000..8d9529e --- /dev/null +++ b/modules/customer/handler/guser/getGameUserControlBalance.go @@ -0,0 +1,47 @@ +package guser + +import ( + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" + "server/common" + "server/db" + "server/modules/backend/models" + utils "server/modules/backend/util" + "server/modules/backend/values" + "server/modules/customer/app" +) + +func GetGameUserControlBalance(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.GetGameUserControlBalanceReq) + if !a.S(req) { + return + } + resp := values.GetGameUserControlBalanceResp{ + List: []common.CurrencyBalance{}, + } + s, e := utils.GetQueryUnix(req.Start, req.End) + count, err := db.ES().QueryPlayerControlBalance(req.UID, req.Page-1, req.Num, &s, &e, &resp.List, req.ControlType) + if err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + resp.Count = count + + event := int(common.CurrencyEventGameSettle) + // 查询控杀次数 + resp.ControlCount = models.GetControlCount(&s, &e, &req.UID, &event, false) + loseCount := models.GetControlCount(&s, &e, &req.UID, &event, true) + resp.ControlPer = utils.GetPer(loseCount, resp.ControlCount) + + // 幸运次数 + resp.LuckCount = models.GetLuckCount(&s, &e, &req.UID, nil, &event, false, false) + winCount := models.GetLuckCount(&s, &e, &req.UID, nil, &event, true, false) + resp.LuckPer = utils.GetPer(winCount, resp.LuckCount) + + a.Data = resp +} diff --git a/modules/customer/handler/guser/getGameUserInfo.go b/modules/customer/handler/guser/getGameUserInfo.go new file mode 100644 index 0000000..b82c727 --- /dev/null +++ b/modules/customer/handler/guser/getGameUserInfo.go @@ -0,0 +1,245 @@ +package guser + +import ( + "fmt" + "reflect" + "server/call" + "server/common" + "server/db" + "server/modules/backend/models" + "server/modules/backend/util" + "server/modules/backend/values" + "server/modules/customer/app" + uutil "server/util" + "sort" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" + "github.com/olivere/elastic/v7" +) + +func GetGameUserInfo(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.GetGameUserInfoReq) + if !a.S(req) { + return + } + user := &common.PlayerDBInfo{} + switch req.Data.(type) { + case int, int64, float32, float64: + id := uutil.GetInt(req.Data) + if id <= 999999999 { + user.Id = id + } else { + user.Mobile = strconv.Itoa(id) + } + case string: + str := req.Data.(string) + if len(str) == 10 { + user.Mobile = str + } else { + id, err := strconv.Atoi(str) + if err != nil { + a.Code = values.CodeParam + a.Msg = "玩家不存在" + return + } + user.Id = id + } + } + err := db.Mysql().Get(user) + if err != nil { + log.Error("err:%v", err) + a.Code = values.CodeParam + a.Msg = "玩家不存在" + return + } + uid := user.Id + ur, _ := call.GetUserXInfo(uid, "online") + info := &common.RechargeInfo{UID: uid} + err = db.Mysql().Get(info) + if err != nil { + log.Error(err.Error()) + } + control := &common.PlayerControl{UID: uid} + err = db.Mysql().Get(control) + if err != nil { + log.Error(err.Error()) + } + lg := &common.LoginRecord{UID: uid} + err = db.Mysql().GetLast(lg) + if err != nil { + log.Error(err.Error()) + } + + pointControl := &common.ConfigPointControl{UID: uid} + db.Mysql().Get(pointControl) + per := 0 + tar := "" + if control.NewControlFin == 1 { + if control.NewControlLevel == 0 { + control.NewControlLevel = 1 + } + con := call.GetNewControlConfigByID(control.NewControlLevel) + if con != nil { + control.TargetValue = con.ControlNum + per = con.ControlPer + tar = fmt.Sprintf("%v", control.TargetValue) + } + } else { + if control.ControlSection > 0 { + re := &common.RechargeInfo{UID: uid} + db.Mysql().Get(re) + con := call.GetSignleControlConfigByRecharge(re.TotalCharge) + if con != nil { + rcon := reflect.ValueOf(con).Elem() + per = int(rcon.FieldByName(fmt.Sprintf("ControlPer%v", control.ControlSection)).Int()) + } + } + tar = uutil.FormatFloat(float64(control.TargetValue)/100, 2) + } + black := &common.ConfigMillionBlack{UID: uid} + db.Mysql().Get(black) + resp := values.GetGameUserInfoResp{ + UID: user.Id, + Nick: user.Nick, + Recharge: info.TotalCharge, + Withdraw: info.TotoalWithdraw, + Channel: user.ChannelID, + Phone: user.Mobile, + OpenID: *user.Openid, + Cash: user.Cash, + Total: user.BindCash + user.Cash, + Online: ur.Online == common.PlayerOnline, + Status: user.Status, + Birth: user.Birth, + // PlayCount: util.GetGUserPlayCount(uid), + IP: user.IP, + LastLogin: lg.Time, + Tag: user.Tag, + IsFinishNewControl: control.NewControlFin == 2, + NewControlLevel: control.NewControlLevel, + ControlLevel: control.ControlLevel, + ControlSection: control.ControlSection, + ControlValue: uutil.FormatFloat(float64(control.ControlValue)/100, 2), + ControlPer: per, + TargetValue: tar, + FreeAmount: uutil.FormatFloat(float64(control.FreeAmount)/100, 2), + PointControlValue: uutil.FormatFloat(float64(pointControl.CurControlNum)/100, 2), + PointControlTargetValue: uutil.FormatFloat(float64(pointControl.ControlNum), 2), + PointControlPer: pointControl.ControlPer, + UserGameData: GetUserGameInfo(user.Id), + Gpsadid: user.DeviceId, + Black: black, + } + + _, err = db.Mysql().QueryAll(fmt.Sprintf("deviceid = '%s'", user.DeviceId), "", &common.PlayerDBInfo{}, &resp.SubAccount) + if err != nil { + log.Error(err.Error()) + } + a.Data = resp +} + +// 获取用户游戏信息 +func GetUserGameInfo(uid int) []values.UserGameInfo { + q := elastic.NewBoolQuery() + q.Filter(elastic.NewRangeQuery("uid").Gte(uid)) + q.Filter(elastic.NewRangeQuery("uid").Lt(uid + 1)) + q.Filter(elastic.NewRangeQuery("event").Gte(common.CurrencyEventGameSettle)) + q.Filter(elastic.NewRangeQuery("event").Lt(common.CurrencyEventGameBet)) + totalGameCount, _ := db.ES().GroupBy(common.ESIndexBalance, "desc.keyword", q, len(common.Games)) + q.Filter(elastic.NewRangeQuery("value").Gt(0)) + winGameCount, _ := db.ES().GroupBy(common.ESIndexBalance, "desc.keyword", q, len(common.Games)) + + q = elastic.NewBoolQuery() + q.Filter(elastic.NewRangeQuery("uid").Gte(uid)) + q.Filter(elastic.NewRangeQuery("uid").Lt(uid + 1)) + q.Filter(elastic.NewRangeQuery("event").Gte(common.CurrencyEventGameSettle)) + q.Filter(elastic.NewRangeQuery("event").Lt(common.CurrencyEventGameBet)) + totalBulk := new(common.GroupSumBuckets) + db.ES().GroupSumBy(common.ESIndexBalance, "desc.keyword", q, &totalBulk, "", false, 5000, "value") + + q.Filter(elastic.NewRangeQuery("value").Gt(0)) + winBulk := new(common.GroupSumBuckets) + db.ES().GroupSumBy(common.ESIndexBalance, "desc.keyword", q, &winBulk, "", false, 5000, "value") + + tmp := map[int]values.UserGameInfo{} + for _, v := range common.RoomGameIDs { + tmp[v] = values.UserGameInfo{GameId: v, WinPer: "0%"} + } + for _, v := range common.MillionGameIDs { + tmp[uutil.GetInt(v)] = values.UserGameInfo{GameId: uutil.GetInt(v), WinPer: "0%"} + } + for _, v := range totalGameCount.Buckets { + id := uutil.GetInt(v.Key) + m := tmp[id] + m.GameCount += int64(v.Doc_count) + tmp[id] = m + } + for _, v := range winGameCount.Buckets { + id := uutil.GetInt(v.Key) + m := tmp[id] + m.WinCount += int64(v.Doc_count) + m.LoseCount = m.GameCount - m.WinCount + tmp[id] = m + } + for _, v := range totalBulk.Buckets { + id := uutil.GetInt(v.Key) + m := tmp[id] + m.Profit += int64(v.Value.Value) + m.WinPer = util.GetPer(m.WinCount, m.GameCount) + tmp[id] = m + } + for _, v := range winBulk.Buckets { + id := uutil.GetInt(v.Key) + m := tmp[id] + m.WinProfit += int64(v.Value.Value) + m.LoseProfit = m.Profit - m.WinProfit + m.WinPer = util.GetPer(m.WinCount, m.GameCount) + tmp[id] = m + } + + var res []values.UserGameInfo + total := values.UserGameInfo{} + for _, v := range tmp { + res = append(res, v) + total.GameCount += v.GameCount + total.WinCount += v.WinCount + total.LoseCount += v.LoseCount + total.Profit += v.Profit + total.WinProfit += v.WinProfit + total.LoseProfit += v.LoseProfit + } + total.WinPer = util.GetPer(total.WinCount, total.GameCount) + + sort.Slice(res, func(i, j int) bool { + return res[i].GameCount > res[j].GameCount + }) + + // 汇总 + res = append([]values.UserGameInfo{total}, res...) + return res +} + +func getUserGameData(uid int, gameId *int, win, lost *bool) values.UserGameInfo { + var userGameInfo values.UserGameInfo + if gameId == nil { + userGameInfo.GameId = 0 + } else { + userGameInfo.GameId = *gameId + } + userGameInfo.GameCount = models.GetWinGameCountByBalance(nil, nil, &uid, nil, gameId, nil, nil) + userGameInfo.WinCount = models.GetWinGameCountByBalance(nil, nil, &uid, nil, gameId, nil, win) + userGameInfo.LoseCount = models.GetWinGameCountByBalance(nil, nil, &uid, nil, gameId, nil, lost) + + userGameInfo.Profit = models.GetGameProfitByUid(nil, nil, &uid, nil, gameId, nil, nil) + userGameInfo.WinProfit = models.GetGameProfitByUid(nil, nil, &uid, nil, gameId, nil, win) + userGameInfo.LoseProfit = models.GetGameProfitByUid(nil, nil, &uid, nil, gameId, nil, lost) + userGameInfo.WinPer = util.GetPer(userGameInfo.WinCount, userGameInfo.GameCount) + + return userGameInfo +} diff --git a/modules/customer/handler/guser/getGameUserList.go b/modules/customer/handler/guser/getGameUserList.go new file mode 100644 index 0000000..be04391 --- /dev/null +++ b/modules/customer/handler/guser/getGameUserList.go @@ -0,0 +1,231 @@ +package guser + +import ( + "fmt" + "server/call" + "server/common" + "server/db" + "server/modules/backend/models" + utils "server/modules/backend/util" + "server/modules/backend/values" + "server/modules/customer/app" + "server/util" + "time" + + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" + "github.com/olivere/elastic/v7" +) + +func GetGameUserList(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.GetGameUserListReq) + if !a.S(req) { + return + } + su, eu := utils.GetQueryUnix(req.Start, req.End) + + var sqlList, sqlTotal, sqlCount string + if req.ExcWithdraw { + // list + sqlList = fmt.Sprintf("SELECT a.id AS UID,"+ // 用户uid + "a.nick AS Nick,"+ // 用户昵称 + "a.status AS Status,"+ // 用户状态 + "a.mobile AS Phone,"+ // 用户手机号 + "a.birth AS Birth,"+ // 用户生日 + "(a.cash+a.bind_cash) AS TotalCash,"+ // 用户总金额 + "a.cash AS Cash,"+ // 用户可提现现金 + "IFNULL(b.total_charge,0) AS Recharge,"+ // 用户充值金额 + "IFNULL(b.total_withdraw,0) AS Withdraw,"+ // 用户提现金额 + "a.online AS Online "+ // 用户是否在线 + "FROM users AS a LEFT JOIN recharge_info AS b ON a.id = b.uid WHERE b.total_withdraw >= b.total_charge AND a.role <> 100 AND a.birth >= %d AND a.birth < %d", su, eu) + + // total + sqlTotal = fmt.Sprintf("SELECT SUM(a.cash+a.bind_cash) AS CashTotal,"+ // 总金额 + "SUM(a.cash) AS WithdrawableTotal,"+ // 总可提现现金 + "SUM(b.total_charge) AS RechargeTotal,"+ // 总充值 + "SUM(b.total_withdraw) AS WithdrawHistoryTotal, "+ // 历史提现 + "COUNT((IF(a.cash>=10000,TRUE,NULL))) AS WithdrawPlayerCount "+ // 提现玩家数量 + "FROM users AS a LEFT JOIN recharge_info AS b ON a.id = b.uid WHERE b.total_withdraw >= b.total_charge AND a.role <> 100 AND a.birth >= %d AND a.birth < %d", su, eu) + + // count + sqlCount = fmt.Sprintf("SELECT count(*) FROM users AS a LEFT JOIN recharge_info AS b ON a.id = b.uid WHERE b.total_withdraw >= b.total_charge AND a.role<>100 AND a.birth >= %d AND a.birth < %d", su, eu) + } else { + // list + sqlList = fmt.Sprintf("SELECT a.id AS UID,"+ // 用户uid + "a.nick AS Nick,"+ // 用户昵称 + "a.status AS Status,"+ // 用户状态 + "a.mobile AS Phone,"+ // 用户手机号 + "a.birth AS Birth,"+ // 用户生日 + "(a.cash+a.bind_cash) AS TotalCash,"+ // 用户总金额 + "a.cash AS Cash,"+ // 用户可提现现金 + "IFNULL(b.total_charge,0) AS Recharge,"+ // 用户充值金额 + "IFNULL(b.total_withdraw,0) AS Withdraw,"+ // 用户提现金额 + "a.online AS Online "+ // 用户是否在线 + "FROM users AS a LEFT JOIN recharge_info AS b ON a.id = b.uid WHERE a.role <> 100 AND a.birth >= %d AND a.birth < %d", su, eu) + + // total + sqlTotal = fmt.Sprintf("SELECT SUM(a.cash+a.bind_cash) AS CashTotal,"+ // 总金额 + "SUM(a.cash) AS WithdrawableTotal,"+ // 总可提现现金 + "SUM(b.total_charge) AS RechargeTotal,"+ // 总充值 + "SUM(b.total_withdraw) AS WithdrawHistoryTotal, "+ // 历史提现 + "COUNT((IF(a.cash>=10000,TRUE,NULL))) AS WithdrawPlayerCount "+ // 提现玩家数量 + "FROM users AS a LEFT JOIN recharge_info AS b ON a.id = b.uid WHERE a.role <> 100 AND a.birth >= %d AND a.birth < %d", su, eu) + + // count + sqlCount = fmt.Sprintf("SELECT count(*) FROM users WHERE role<>100 AND birth >= %d AND birth < %d", su, eu) + } + + order := "a.birth" + sort := "" + if req.Order != nil { + if *req.Order < 0 { + sort = " desc" + *req.Order = -*req.Order + } + switch *req.Order { + case 1: // 1是按玩家充值金额排序 + order = "b.total_charge" + case 2: // 2是按金币数量排序 + order = "a.cash+a.bind_cash" + case 3: // 3是按可提现总额排序 + order = "a.cash" + case 4: // 4是按历史提现金额排序 + order = "b.total_withdraw" + case 5: // 5是按注册时间排序 + order = "a.birth" + default: + a.Code = values.CodeParam + a.Msg = "排序参数不合法" + return + } + } + + if req.Channel != nil { + sqlList += fmt.Sprintf(" and a.channel_id = %d ", *req.Channel) + sqlTotal += fmt.Sprintf(" and a.channel_id = %d ", *req.Channel) + sqlCount += fmt.Sprintf(" and channel_id = %d ", *req.Channel) + } + + if req.Status != nil { + sqlList += fmt.Sprintf(" and a.status = %v", *req.Status) + sqlCount += fmt.Sprintf(" and status = %v", *req.Status) + sqlTotal += fmt.Sprintf(" and status = %v", *req.Status) + } + if req.Online != nil { + sqlList += fmt.Sprintf(" and a.online = %v", *req.Online) + sqlCount += fmt.Sprintf(" and online = %v", *req.Online) + sqlTotal += fmt.Sprintf(" and online = %v", *req.Online) + } + + if req.CoinLimit != nil { + CoinLimit := *req.CoinLimit + if len(CoinLimit) < 2 { + sqlList += fmt.Sprintf(" and %d <= (a.cash + a.bind_cash) and (a.cash + a.bind_cash) <= %d", 0, 3000) + sqlCount += fmt.Sprintf(" and %d <= (cash + bind_cash) and (cash + bind_cash) <= %d", 0, 3000) + sqlTotal += fmt.Sprintf(" and %d <= (cash + bind_cash) and (cash + bind_cash) <= %d", 0, 3000) + } else { + if CoinLimit[1] == 0 { + sqlList += fmt.Sprintf(" and %d <= (a.cash + a.bind_cash) ", CoinLimit[0]) + sqlCount += fmt.Sprintf(" and %d <= (cash + bind_cash) ", CoinLimit[0]) + sqlTotal += fmt.Sprintf(" and %d <= (cash + bind_cash) ", CoinLimit[0]) + } else { + sqlList += fmt.Sprintf(" and %d <= (a.cash + a.bind_cash) and (a.cash + a.bind_cash) <= %d", CoinLimit[0], CoinLimit[1]) + sqlCount += fmt.Sprintf(" and %d <= (cash + bind_cash) and (cash + bind_cash) <= %d", CoinLimit[0], CoinLimit[1]) + sqlTotal += fmt.Sprintf(" and %d <= (cash + bind_cash) and (cash + bind_cash) <= %d", CoinLimit[0], CoinLimit[1]) + } + } + } + + sqlList += fmt.Sprintf(" ORDER BY %v %v LIMIT %v OFFSET %v", order, sort, req.Num, (req.Page-1)*req.Num) + resp := values.GetGameUserListResp{} + if err := db.Mysql().C().Raw(sqlList).Scan(&resp.List).Error; err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + + // isWin := true + uids := []interface{}{} + for i, v := range resp.List { + lg := &common.LoginRecord{UID: v.UID} + err := db.Mysql().GetLast(lg) + if err != nil { + log.Error(err.Error()) + } + resp.List[i].LastLogin = lg.Time + + uids = append(uids, v.UID) + // winCount := models.GetWinGameCountByBalance(nil, nil, &resp.List[i].UID, req.Channel, nil, nil, &isWin) + // resp.List[i].GameCount = models.GetGameCountByBalance(nil, nil, &resp.List[i].UID, req.Channel, nil, nil) + // resp.List[i].WinPer = utils.GetPer(winCount, resp.List[i].GameCount) + + controlInfo := &common.PlayerControl{UID: v.UID} + err = db.Mysql().GetLast(controlInfo) + if err != nil { + log.Error(err.Error()) + } + resp.List[i].ControlInfo = *controlInfo + + data := common.CurrencyBalance{} + q := elastic.NewBoolQuery() + q.Filter(elastic.NewRangeQuery("uid").Gte(resp.List[i].UID)) + q.Filter(elastic.NewRangeQuery("uid").Lt(resp.List[i].UID + 1)) + q.Filter(elastic.NewRangeQuery("event").Gte(common.CurrencyEventGameSettle)) + q.Filter(elastic.NewRangeQuery("event").Lt(common.CurrencyEventGameBet)) + err = db.ES().QueryOne(common.ESIndexBalance, q, &data, "id", false) + if err != nil { + log.Error("err:%v", err) + resp.List[i].PlayerStatus = nil + } else { + if data.Time >= (time.Now().Unix() - 60) { + resp.List[i].PlayerStatus = map[string]string{ + "gameId": data.Desc, + "roomId": data.RoomName, + } + } + } + + user, _ := call.GetUserInfo(v.UID) + resp.List[i].AccountCount = int(db.Mysql().Count(&common.PlayerDBInfo{}, fmt.Sprintf("deviceid = '%v'", user.DeviceId))) + } + + total := models.GetGameCountByUIDs(uids, false) + win := models.GetGameCountByUIDs(uids, true) + for i, v := range resp.List { + for _, u := range total.Buckets { + if util.GetInt(u.Key) == v.UID { + resp.List[i].GameCount = int64(u.Doc_count) + break + } + } + var winCount int64 + for _, u := range win.Buckets { + if util.GetInt(u.Key) == v.UID { + winCount = int64(u.Doc_count) + break + } + } + resp.List[i].WinPer = utils.GetPer(winCount, resp.List[i].GameCount) + } + + if err := db.Mysql().C().Raw(sqlCount).Scan(&resp.Count).Error; err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + if err := db.Mysql().C().Raw(sqlTotal).Scan(&resp.Total).Error; err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + + resp.Total.WithdrawPlayerPer = utils.GetPer(resp.Total.WithdrawPlayerCount, resp.Count) + + resp.Total.UnRechargePlayerCash, resp.Total.UnRechargePlayerAmount = models.GetUnRechargePlayerAmount(req.Channel, su, eu) + + a.Data = resp +} diff --git a/modules/customer/handler/guser/getGameUserPlayData.go b/modules/customer/handler/guser/getGameUserPlayData.go new file mode 100644 index 0000000..d49f675 --- /dev/null +++ b/modules/customer/handler/guser/getGameUserPlayData.go @@ -0,0 +1,53 @@ +package guser + +import ( + "server/common" + "server/db" + "server/modules/backend/values" + "server/modules/customer/app" + + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" +) + +func GetGameUserPlayData(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.GetGameUserPlayDataReq) + if !a.S(req) { + return + } + var ret []common.CurrencyBalance + + var count int64 + var err error + if req.Games != nil { + count, err = db.ES().QueryPlayerBalance(req.UID, req.Page-1, req.Num, int(common.CurrencyEventGameSettle), req.Start, req.End, &ret, *req.Games) + } else { + count, err = db.ES().QueryPlayerBalance(req.UID, req.Page-1, req.Num, int(common.CurrencyEventGameSettle), req.Start, req.End, &ret) + } + if err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + resp := values.GetGameUserPlayDataResp{ + List: []values.OneGameUserPlayData{}, + Count: count, + } + for _, v := range ret { + resp.List = append(resp.List, values.OneGameUserPlayData{ + Time: v.Time, + GameID: v.Desc, + RoomName: v.RoomName, + Value: v.Value, + Balance: v.Balance, + UUID: v.Extern, + ControlType: v.ControlType, + DramaID: v.CardsLevel, + }) + } + a.Data = resp +} diff --git a/modules/customer/handler/guser/getGameUserPlayDetail.go b/modules/customer/handler/guser/getGameUserPlayDetail.go new file mode 100644 index 0000000..a27bd0a --- /dev/null +++ b/modules/customer/handler/guser/getGameUserPlayDetail.go @@ -0,0 +1 @@ +package guser diff --git a/modules/customer/handler/guser/getGameUserRechargeHistory.go b/modules/customer/handler/guser/getGameUserRechargeHistory.go new file mode 100644 index 0000000..ec50c77 --- /dev/null +++ b/modules/customer/handler/guser/getGameUserRechargeHistory.go @@ -0,0 +1,62 @@ +package guser + +import ( + "fmt" + "server/common" + "server/db" + utils "server/modules/backend/util" + "server/modules/backend/values" + "server/modules/customer/app" + + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" +) + +func GetGameUserRechargeHistory(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.GetGameUserRechargeHistoryReq) + if !a.S(req) { + return + } + resp := values.GetGameUserRechargeHistoryResp{} + var ret []common.RechargeOrder + if req.UID > 0 { + count, err := db.Mysql().QueryPlayerRWHistory(&req.UID, nil, req.Page-1, req.Num, []int{int(common.CurrencyEventReCharge), common.CurrencyEventGMRecharge}, req.Start, req.End, &ret, req.Status) + if err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + resp.Count = count + for _, v := range ret { + resp.List = append(resp.List, values.OneRechargeList{UID: v.UID, Time: v.CreatedAt.Unix(), CallbackTime: v.CallbackTime, + Amount: v.Amount, Status: int(v.Status), MyOrderID: v.OrderID, OrderID: v.APIPayID, PayChannel: v.PayChannel, PaySource: v.PaySource}) + } + + resp.RechargeCount = db.Mysql().Count(&common.RechargeOrder{}, fmt.Sprintf("uid = %v and (event = %v or event = %v)", req.UID, common.CurrencyEventReCharge, common.CurrencyEventGMRecharge)) + re := &common.RechargeInfo{UID: req.UID} + err = db.Mysql().Get(re) + if err != nil { + log.Error(err.Error()) + } + resp.RechargeTotal = re.TotalCharge + successCount := db.Mysql().Count(&common.RechargeOrder{}, fmt.Sprintf("uid = %v and (event = %v or event = %v) and status = %v", req.UID, common.CurrencyEventReCharge, common.CurrencyEventGMRecharge, common.StatusROrderPay)) + resp.RechargeSuccessPer = utils.GetPer(successCount, resp.RechargeCount) + } else if req.OrderID != "" { + resp.Count, _ = db.Mysql().QueryList(req.Page-1, req.Num, fmt.Sprintf(`event = %v and (apipayid = "%v" or orderid = "%v")`, common.CurrencyEventReCharge, req.OrderID, req.OrderID), "created_at desc", &common.RechargeOrder{}, &ret) + // log.Debug("ret:%v", ret) + for _, v := range ret { + resp.List = append(resp.List, values.OneRechargeList{UID: v.UID, Time: v.CreatedAt.Unix(), CallbackTime: v.CallbackTime, + MyOrderID: v.OrderID, OrderID: v.APIPayID, Amount: v.Amount, Status: int(v.Status), PayChannel: v.PayChannel, PaySource: v.PaySource}) + } + } else { + a.Code = values.CodeParam + a.Msg = "查询条件有误" + return + } + + a.Data = resp +} diff --git a/modules/customer/handler/guser/lostPlayerData.go b/modules/customer/handler/guser/lostPlayerData.go new file mode 100644 index 0000000..a27bd0a --- /dev/null +++ b/modules/customer/handler/guser/lostPlayerData.go @@ -0,0 +1 @@ +package guser diff --git a/modules/customer/handler/guser/lostUserData.go b/modules/customer/handler/guser/lostUserData.go new file mode 100644 index 0000000..8efc582 --- /dev/null +++ b/modules/customer/handler/guser/lostUserData.go @@ -0,0 +1,243 @@ +package guser + +import ( + "fmt" + "server/common" + "server/db" + "server/modules/backend/models" + utils "server/modules/backend/util" + "server/modules/backend/values" + "server/modules/customer/app" + "time" + + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" +) + +// 流失玩家数据 +func LostUserData(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.LostUserDataReq) + if !a.S(req) { + return + } + resp := values.LostUserDataResp{} + + su, eu := utils.GetQueryUnix(req.Start, req.End) + + switch req.Sort { + case 1: + resp.Count, resp.List = getAllLostUser(su, eu, req.Page, req.Num, req.Channel) + case 2: + resp.Count, resp.List = getPayLostUser(su, eu, req.Page, req.Num, req.Channel) + case 3: + resp.Count, resp.List = getActiveLostUser(su, eu, req.Page, req.Num, req.Channel) + case 4: + resp.Count, resp.List = getNewLostUser(su, eu, req.Page, req.Num, req.Channel) + default: + resp.Count, resp.List = getAllLostUser(su, eu, req.Page, req.Num, req.Channel) + } + + a.Data = resp +} + +// 获取所有流失用户 +func getAllLostUser(su, eu int64, page, num int, channel *int) (int64, []values.LostUserData) { + var oneDay = 24 * 60 * 60 + now := time.Now().Unix() + + queryUser := " SELECT u.id, u.nick, u.bind_cash, u.cash, u.birth FROM users u LEFT JOIN (SELECT a.* FROM login_record a INNER JOIN ( SELECT uid, MAX( created_at ) created_at FROM login_record WHERE UNIX_TIMESTAMP(date) >= %d GROUP BY uid ) b ON a.uid = b.uid AND a.created_at = b.created_at WHERE a.uid = b.uid AND a.created_at = b.created_at AND UNIX_TIMESTAMP(a.date) >= %d) r ON u.id = r.uid WHERE " + queryCount := " SELECT COUNT(DISTINCT(u.id)) FROM users u LEFT JOIN (SELECT a.* FROM login_record a INNER JOIN ( SELECT uid, MAX( created_at ) created_at FROM login_record WHERE UNIX_TIMESTAMP(date) >= %d GROUP BY uid ) b ON a.uid = b.uid AND a.created_at = b.created_at WHERE a.uid = b.uid AND a.created_at = b.created_at AND UNIX_TIMESTAMP(a.date) >= %d) r ON u.id = r.uid WHERE " + + str := fmt.Sprintf(" u.birth >= %d AND u.birth < %d ", su, eu) + + if channel != nil { + str += fmt.Sprintf(" AND u.channel_id = %d AND r.channel_id = %d", *channel, *channel) + } + + // 七天未登录的用户 UNIX_TIMESTAMP('20210816') + str += fmt.Sprintf(" AND (%d - UNIX_TIMESTAMP(r.date) > %d ) ", now, 7*oneDay) + + var count int64 + err := db.Mysql().QueryBySql(fmt.Sprintf(queryCount+str, su, su), &count) + if err != nil { + log.Error(err.Error()) + return 0, nil + } + + str += " GROUP BY u.id " + + str += fmt.Sprintf(" LIMIT %d, %d ", (page-1)*num, num) + + var users []common.PlayerDBInfo + err = db.Mysql().QueryBySql(fmt.Sprintf(queryUser+str, su, su), &users) + if err != nil { + log.Error(err.Error()) + return 0, nil + } + + return count, getLostUserInfo(users) +} + +// 获取付费流失用户 +func getPayLostUser(su, eu int64, page, num int, channel *int) (int64, []values.LostUserData) { + var oneDay = 24 * 60 * 60 + now := time.Now().Unix() + + queryUser := " SELECT u.id, u.nick, u.bind_cash, u.cash, u.birth FROM users u LEFT JOIN (SELECT a.* FROM login_record a INNER JOIN ( SELECT uid, MAX( created_at ) created_at FROM login_record WHERE UNIX_TIMESTAMP(date) >= %d GROUP BY uid ) b ON a.uid = b.uid AND a.created_at = b.created_at WHERE a.uid = b.uid AND a.created_at = b.created_at AND UNIX_TIMESTAMP(a.date) >= %d) r ON u.id = r.uid LEFT JOIN (SELECT * FROM recharge_order WHERE `event` = %d AND `status` = %d AND callback_time > %d) re ON u.id = re.uid WHERE u.id = re.uid AND " + queryCount := " SELECT COUNT(DISTINCT(u.id)) FROM users u LEFT JOIN (SELECT a.* FROM login_record a INNER JOIN ( SELECT uid, MAX( created_at ) created_at FROM login_record WHERE UNIX_TIMESTAMP(date) >= %d GROUP BY uid ) b ON a.uid = b.uid AND a.created_at = b.created_at WHERE a.uid = b.uid AND a.created_at = b.created_at AND UNIX_TIMESTAMP(a.date) >= %d) r ON u.id = r.uid LEFT JOIN (SELECT * FROM recharge_order WHERE `event` = %d AND `status` = %d AND callback_time > %d) re ON u.id = re.uid WHERE u.id = re.uid AND " + + str := fmt.Sprintf(" u.birth >= %d AND u.birth < %d ", su, eu) + + if channel != nil { + str += fmt.Sprintf(" AND u.channel_id = %d AND r.channel_id = %d", *channel, *channel) + } + + // 三天未登录的用户 UNIX_TIMESTAMP('20210816') + str += fmt.Sprintf(" AND (%d - UNIX_TIMESTAMP(r.date) > %d ) ", now, 7*oneDay) + + var count int64 + err := db.Mysql().QueryBySql(fmt.Sprintf(queryCount+str, su, su, common.CurrencyEventReCharge, common.StatusROrderPay, su), &count) + if err != nil { + log.Error(err.Error()) + return 0, nil + } + + str += " GROUP BY u.id " + + str += fmt.Sprintf(" LIMIT %d, %d ", (page-1)*num, num) + + var users []common.PlayerDBInfo + err = db.Mysql().QueryBySql(fmt.Sprintf(queryUser+str, su, su, common.CurrencyEventReCharge, common.StatusROrderPay, su), &users) + if err != nil { + log.Error(err.Error()) + return 0, nil + } + + return count, getLostUserInfo(users) +} + +// 获取活跃流失用户 +func getActiveLostUser(su, eu int64, page, num int, channel *int) (int64, []values.LostUserData) { + var oneDay = 24 * 60 * 60 + now := time.Now().Unix() + + queryUser := fmt.Sprintf(" SELECT u.id, u.nick, u.bind_cash, u.cash, u.birth FROM users u LEFT JOIN ( SELECT uid, MAX(date) date, MAX( created_at ) created_at FROM login_record WHERE UNIX_TIMESTAMP(date) >= %d GROUP BY uid ) b ON u.id = b.uid WHERE u.id = b.uid AND ", su) + + " FROM_UNIXTIME(u.birth,'%Y%m%d') != b.date AND " + queryCount := fmt.Sprintf(" SELECT COUNT(DISTINCT(u.id)) FROM users u LEFT JOIN ( SELECT uid, MAX(date) date, MAX( created_at ) created_at FROM login_record WHERE UNIX_TIMESTAMP(date) >= %d GROUP BY uid ) b ON u.id = b.uid WHERE u.id = b.uid AND ", su) + + " FROM_UNIXTIME(u.birth,'%Y%m%d') != b.date AND " + + str := fmt.Sprintf(" u.birth >= %d AND u.birth < %d ", su, eu) + + if channel != nil { + str += fmt.Sprintf(" AND u.channel_id = %d AND b.channel_id = %d", *channel, *channel) + } + + // 七天未登录的用户 UNIX_TIMESTAMP('20210816') + str += fmt.Sprintf(" AND (%d - UNIX_TIMESTAMP(b.date) > %d ) ", now, 7*oneDay) + + var count int64 + err := db.Mysql().QueryBySql(queryCount+str, &count) + if err != nil { + log.Error(err.Error()) + return 0, nil + } + + str += " GROUP BY u.id " + + str += fmt.Sprintf(" LIMIT %d, %d ", (page-1)*num, num) + + var users []common.PlayerDBInfo + err = db.Mysql().QueryBySql(queryUser+str, &users) + if err != nil { + log.Error(err.Error()) + return 0, nil + } + + return count, getLostUserInfo(users) +} + +// 获取新用户流失 +func getNewLostUser(su, eu int64, page, num int, channel *int) (int64, []values.LostUserData) { + + queryUser := fmt.Sprintf(" SELECT u.id, u.nick, u.bind_cash, u.cash, u.birth FROM users u LEFT JOIN ( SELECT uid, MAX( created_at ) created_at, MAX(date) date FROM login_record WHERE UNIX_TIMESTAMP(date) >= %d GROUP BY uid ) b ON u.id = b.uid WHERE u.id = b.uid ", su) + + "AND FROM_UNIXTIME(u.birth,'%Y%m%d') = b.date AND" + queryCount := fmt.Sprintf(" SELECT COUNT(DISTINCT(u.id)) FROM users u LEFT JOIN ( SELECT uid, MAX( created_at )created_at, MAX(date) date FROM login_record WHERE UNIX_TIMESTAMP(date) >= %d GROUP BY uid ) b ON u.id = b.uid WHERE u.id = b.uid ", su) + + "AND FROM_UNIXTIME(u.birth,'%Y%m%d') = b.date AND" + + str := fmt.Sprintf(" u.birth >= %d AND u.birth < %d ", su, eu) + if channel != nil { + str += fmt.Sprintf(" AND u.channel_id = %d ", *channel) + str += fmt.Sprintf(" AND u.channel_id = %d ", *channel) + } + + // 7天未登录的用户 UNIX_TIMESTAMP('20210816') + str += fmt.Sprintf(" AND (%d - UNIX_TIMESTAMP(b.date) > %d ) ", time.Now().Unix(), 7*24*60*60) + + var count int64 + err := db.Mysql().QueryBySql(queryCount+str, &count) + if err != nil { + log.Error(err.Error()) + return 0, nil + } + + str += fmt.Sprintf(" LIMIT %d, %d ", (page-1)*num, num) + + var users []common.PlayerDBInfo + err = db.Mysql().QueryBySql(queryUser+str, &users) + if err != nil { + log.Error(err.Error()) + return 0, nil + } + + return count, getLostUserInfo(users) +} + +func getLostUserInfo(users []common.PlayerDBInfo) []values.LostUserData { + var res []values.LostUserData + for i := 0; i < len(users); i++ { + var data values.LostUserData + + // 用户昵称 + data.Nick = users[i].Nick + // 用户uid + data.Uid = users[i].Id + // 用户生日 + data.Birth = users[i].Birth + + var record common.LoginRecord + err := db.Mysql().C().Model(&common.LoginRecord{}).Where(" uid = ?", users[i].Id).Last(&record).Error + if err != nil { + log.Error(err.Error()) + } + // 最后登录时间 + data.LastLogin = record.Time + // 玩家游戏局数 + data.GameCount = models.GetGameCountByBalance(nil, nil, &users[i].Id, nil, nil, nil) + // 玩家剩余账户金额 + data.Amount = users[i].BindCash + users[i].Cash + // 玩家账户可提现金额 + data.Cash = users[i].Cash + // 玩家总提现金额 + data.WithDrawAmount = getPlayerAmountBySql(users[i].Id, int(common.CurrencyEventWithDraw), common.StatusROrderFinish) + // 玩家总充值金额 + data.RechargeAmount = getPlayerAmountBySql(users[i].Id, int(common.CurrencyEventReCharge), common.StatusROrderPay) + + res = append(res, data) + } + return res +} + +// 获取玩家充值金额 +func getPlayerAmountBySql(uid, event, status int) int64 { + var amount int64 + amountTotal := "SELECT IFNULL(SUM(amount),0) as Amount FROM recharge_order WHERE uid = %d AND event = %v AND status = %v " + err := db.Mysql().QueryBySql(fmt.Sprintf(amountTotal, uid, event, status), &amount) + if err != nil { + log.Error(err.Error()) + } + return amount +} diff --git a/modules/customer/handler/guser/lostUserDetail.go b/modules/customer/handler/guser/lostUserDetail.go new file mode 100644 index 0000000..61e8bb2 --- /dev/null +++ b/modules/customer/handler/guser/lostUserDetail.go @@ -0,0 +1,48 @@ +package guser + +import ( + "github.com/gin-gonic/gin" + "server/common" + "server/modules/backend/models" + utils "server/modules/backend/util" + "server/modules/backend/values" + "server/modules/customer/app" +) + +func LostUserDetail(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.LostUserDetailReq) + if !a.S(req) { + return + } + resp := values.LostUserDetailResp{} + + for i := 0; i < len(common.RoomGameIDs); i++ { + resp.List = append(resp.List, getLostUserDetail(req.Uid, common.RoomGameIDs[i])) + } + + for i := 0; i < len(common.MillionGameIDs); i++ { + resp.List = append(resp.List, getLostUserDetail(req.Uid, common.MillionGameIDs[i].(int))) + } + + a.Data = resp +} + +func getLostUserDetail(uid int, gameId int) values.LostUserDetail { + var lostUserDetail values.LostUserDetail + + lostUserDetail.GameId = gameId + + isWin := true + winCount := models.GetWinGameCountByBalance(nil, nil, &uid, nil, &gameId, nil, &isWin) + lostUserDetail.GameCount = models.GetGameCountByBalance(nil, nil, &uid, nil, &gameId, nil) + + lostUserDetail.WinPer = utils.GetPer(winCount, lostUserDetail.GameCount) + + lostUserDetail.BreakPer = utils.GetPer(models.GetPlayerBreakCount(nil, nil, &uid, &gameId, nil, nil), lostUserDetail.GameCount) + + return lostUserDetail +} diff --git a/modules/customer/handler/guser/rechargeRank.go b/modules/customer/handler/guser/rechargeRank.go new file mode 100644 index 0000000..99851f3 --- /dev/null +++ b/modules/customer/handler/guser/rechargeRank.go @@ -0,0 +1,96 @@ +package guser + +import ( + "fmt" + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" + "server/call" + "server/common" + "server/db" + utils "server/modules/backend/util" + "server/modules/backend/values" + "server/modules/customer/app" +) + +// 充值排行榜 +func RechargeRank(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.RechargeRankReq) + if !a.S(req) { + return + } + su, eu := utils.GetQueryUnix(req.Start, req.End) + resp := values.RechargeRankResp{} + + var num int64 = 100 + if req.Num != nil { + num = *req.Num + } + + var str string + + if req.Status == 1 { + str = fmt.Sprintf("SELECT uid, SUM(amount) AS amount, COUNT(amount) as event FROM recharge_order WHERE event = %d AND status = %d AND callback_time >= %d AND callback_time < %d GROUP BY uid ORDER BY amount DESC LIMIT 0, %d", + common.CurrencyEventReCharge, + common.StatusROrderPay, + su, + eu, + num) + } else { + str = fmt.Sprintf("SELECT uid, SUM(amount) AS amount, COUNT(amount) as event FROM recharge_order WHERE event = %d AND status = %d AND callback_time >= %d AND callback_time < %d GROUP BY uid ORDER BY amount DESC LIMIT 0, %d", + common.CurrencyEventWithDraw, + common.StatusROrderFinish, + su, + eu, + num) + } + + var users []common.RechargeOrder + + err := db.Mysql().QueryBySql(str, &users) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } + + for i := 0; i < len(users); i++ { + var temp values.RechargeRank + temp.Date = su + temp.Uid = users[i].UID + if req.Status == 1 { + temp.TodayRechargeAmount = users[i].Amount + temp.TodayRechargeOrderCount = int64(users[i].Event) + + sql := fmt.Sprintf("event = %v and status = %v and callback_time >= '%v' and callback_time < '%v' and uid = %v", common.CurrencyEventWithDraw, common.StatusROrderFinish, su, eu, users[i].UID) + temp.TodayWithDrawAmount = db.Mysql().Sum(&common.RechargeOrder{}, sql, "amount") + temp.TodayWithDrawOrderCount = db.Mysql().Count(&common.RechargeOrder{}, sql) + } else { + temp.TodayWithDrawAmount = users[i].Amount + temp.TodayWithDrawOrderCount = int64(users[i].Event) + + sql := fmt.Sprintf("event = %v and status = %v and callback_time >= '%v' and callback_time < '%v' and uid = %v", common.CurrencyEventReCharge, common.StatusROrderPay, su, eu, users[i].UID) + temp.TodayRechargeAmount = db.Mysql().Sum(&common.RechargeOrder{}, sql, "amount") + temp.TodayRechargeOrderCount = db.Mysql().Count(&common.RechargeOrder{}, sql) + } + + // 玩家总提现金额 + temp.WithDrawAmount = getPlayerAmountBySql(users[i].UID, int(common.CurrencyEventWithDraw), common.StatusROrderFinish) + // 玩家总充值金额 + temp.RechargeAmount = getPlayerAmountBySql(users[i].UID, int(common.CurrencyEventReCharge), common.StatusROrderPay) + // 玩家生日 + res, _ := call.GetUserInfo(users[i].UID) + temp.Birth = res.Birth + + // 用户渠道 + temp.Channel = users[i].ChannelID + + resp.List = append(resp.List, &temp) + } + resp.Count = int64(len(users)) + + a.Data = resp +} diff --git a/modules/customer/handler/power/power.go b/modules/customer/handler/power/power.go new file mode 100644 index 0000000..057ecc2 --- /dev/null +++ b/modules/customer/handler/power/power.go @@ -0,0 +1,208 @@ +package handler + +import ( + "fmt" + "server/modules/customer/app" + "server/modules/customer/bdb" + "server/modules/customer/values" + + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" +) + +func UserList(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + resp := values.UserListResp{} + if err := bdb.BackDB.C().Find(&resp.List).Error; err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + a.Data = resp +} + +func AddUser(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.AddUserReq) + if !a.S(req) { + return + } + log.Debug("adduser:%+v", req) + if req.Role == values.UserRoleAdmin { + a.Code = values.CodeParam + a.Msg = "不可添加超级管理员" + return + } + var roles []*values.Role + bdb.BackDB.C().Find(&roles) + var r *values.Role + for _, v := range roles { + if req.Role == v.Role { + r = v + } + } + if r == nil { + log.Error("invalid role:%v", req.Role) + a.Code = values.CodeParam + a.Msg = "请求角色不存在" + return + } + one := &values.User{Name: req.Name, Account: req.Account, Password: req.Password, Role: req.Role, Power: r.Power, Phone: req.Phone, Channels: req.Channels} + if err := bdb.BackDB.Create(one); err != nil { + log.Error("err:%v", err) + a.Code = values.CodeParam + a.Msg = err.Error() + return + } + a.RecordEdit(values.PowerManageUser, fmt.Sprintf("新增用户:%v", req.Name)) +} + +func EditUserPower(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.EditPowerReq) + if !a.S(req) { + return + } + log.Debug("edit power:%+v", req) + user := new(values.User) + user.ID = uint(req.ID) + if err := bdb.BackDB.Get(user); err != nil { + log.Error("err:%v", err) + a.Code = values.CodeParam + a.Msg = "账户不存在" + return + } + if user.Role == values.UserRoleAdmin { + a.Code = values.CodeParam + a.Msg = "不可修改超级管理员" + return + } + update := map[string]interface{}{} + if req.Power != nil { + if !a.CheckPower(*req.Power) { + return + } + update["power"] = *req.Power + } + if req.Name != nil { + update["name"] = *req.Name + } + if req.Account != nil { + update["account"] = *req.Account + } + if req.Password != nil { + update["password"] = *req.Password + } + if req.Role != nil { + if *req.Role == values.UserRoleAdmin { + a.Code = values.CodeParam + a.Msg = "不可修改为超级管理员" + return + } + update["role"] = *req.Role + } + if req.Phone != nil { + update["phone"] = *req.Phone + } + if req.Channels != nil { + update["channels"] = *req.Channels + } + if len(update) == 0 { + a.Code = values.CodeParam + a.Msg = "无内容修改" + return + } + if err := bdb.BackDB.Update(&values.User{ID: uint(req.ID)}, update); err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + a.RecordEdit(values.PowerManageUser, fmt.Sprintf("修改用户权限:%v", req.ID)) +} + +func DelUser(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.DelUserReq) + if !a.S(req) { + return + } + user := new(values.User) + user.ID = uint(req.ID) + if err := bdb.BackDB.C().Delete(user).Where("id = ?", req.ID).Error; err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } +} + +func RoleList(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + resp := values.RoleListResp{} + if err := bdb.BackDB.C().Find(&resp.List).Error; err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + a.Data = resp +} + +func AddRole(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.AddRoleReq) + if !a.S(req) { + return + } + if !a.CheckPower(req.Power) { + return + } + one := &values.Role{Role: req.Role, Name: req.Name, Power: req.Power} + if err := bdb.BackDB.Create(one); err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + a.RecordEdit(values.PowerManageRole, fmt.Sprintf("新增角色:%v", req.Name)) +} + +func EditRole(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.Response() + }() + req := new(values.EditRoleReq) + if !a.S(req) { + return + } + if !a.CheckPower(req.Power) { + return + } + one := &values.Role{Role: req.Role, Name: req.Name, Power: req.Power} + if err := bdb.BackDB.Update(&values.Role{ID: uint(req.ID)}, one); err != nil { + log.Error("err:%v", err) + a.Code = values.CodeRetry + return + } + err := bdb.BackDB.Update(&values.User{Role: req.Role}, map[string]interface{}{"power": req.Power}) + if err != nil { + log.Error(err.Error()) + } + a.RecordEdit(values.PowerManageRole, fmt.Sprintf("修改角色:%v", req.ID)) +} diff --git a/modules/customer/middleware/cross.go b/modules/customer/middleware/cross.go new file mode 100644 index 0000000..efd24a8 --- /dev/null +++ b/modules/customer/middleware/cross.go @@ -0,0 +1,48 @@ +package middleware + +import ( + "net/http" + "server/modules/customer/app" + + "github.com/gin-gonic/gin" +) + +// 跨域访问:cross origin resource share +func CrosHandler() gin.HandlerFunc { + return func(context *gin.Context) { + method := context.Request.Method + origin := context.Request.Header.Get("Origin") // 请求头部 + if origin != "" { + // 接收客户端发送的origin (重要!) + context.Writer.Header().Set("Access-Control-Allow-Origin", "*") + + // 设置允许访问所有域 + context.Header("Access-Control-Allow-Origin", "*") + + // 服务器支持的所有跨域请求的方法 + context.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE") + + // 允许跨域设置可以返回其他子段,可以自定义字段 + context.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma,token,openid,opentoken,istoken") + + // 允许浏览器(客户端)可以解析的头部 (重要) + context.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar") + + // 设置缓存时间 + context.Header("Access-Control-Max-Age", "172800") + + // 允许客户端传递校验信息比如 cookie (重要) + context.Header("Access-Control-Allow-Credentials", "true") + + // 设置返回格式是json + context.Set("content-type", "application/json") + } + + if method == "OPTIONS" { + context.JSON(http.StatusOK, app.R{Code: http.StatusOK, Data: "Options Request!"}) + } + + // 处理请求 + context.Next() + } +} diff --git a/modules/customer/middleware/power.go b/modules/customer/middleware/power.go new file mode 100644 index 0000000..aca8f0c --- /dev/null +++ b/modules/customer/middleware/power.go @@ -0,0 +1,68 @@ +package middleware + +import ( + "server/modules/customer/app" + "server/modules/customer/bdb" + "server/modules/customer/values" + "strings" + + "github.com/gin-gonic/gin" +) + +// 进行权限校验 +func PowerMiddleWare() gin.HandlerFunc { + return func(c *gin.Context) { + path := c.Request.RequestURI + if PassURL(path) { + c.Next() + return + } + a := app.NewApp(c) + if !powerPass(a.User, path) { + a.Code = values.CodePower + a.Response() + c.Abort() + return + } + c.Next() + } +} + +func powerPass(u *values.User, path string) bool { + if u == nil { + return false + } + if u.Role == values.UserRoleAdmin { + return true + } + // 第一步找到主页签 + p := 0 + for s, v := range values.PowerMap { + if strings.Contains(path, s) { + p = v + break + } + } + // 不在权限控制范围 + if p == 0 { + return true + } + buttons, ok := bdb.GetPowerByRole(u.Role)[p] + if !ok { + return false + } + pbutton, ok2 := values.PowerButtonMap[p] + // 该页签没有子按钮 + if !ok2 { + return true + } + for i, v := range pbutton { + if v == path { + if i > len(pbutton)-1 { + return true + } + return buttons[i] == 1 + } + } + return true +} diff --git a/modules/customer/middleware/recover.go b/modules/customer/middleware/recover.go new file mode 100644 index 0000000..6a3c22b --- /dev/null +++ b/modules/customer/middleware/recover.go @@ -0,0 +1,21 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" + "runtime" +) + +func Recovery() gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if err := recover(); err != nil { + buf := make([]byte, 1024) + runtime.Stack(buf, false) + log.Error("panic(%+v), stack:\n%s", err, string(buf)) + return + } + }() + c.Next() + } +} diff --git a/modules/customer/middleware/token.go b/modules/customer/middleware/token.go new file mode 100644 index 0000000..fe5b862 --- /dev/null +++ b/modules/customer/middleware/token.go @@ -0,0 +1,93 @@ +package middleware + +import ( + "server/common" + "server/db" + "server/modules/customer/app" + "server/modules/customer/values" + "strings" + + "github.com/liangdas/mqant/log" + + "github.com/gin-gonic/gin" +) + +var ( + passURLs = map[string]struct{}{ + "/account/login": {}, + "/image/download": {}, + "/gm/customer/config/info": {}, + } +) + +// 进行token校验 +func TokenMiddleWare() gin.HandlerFunc { + return func(c *gin.Context) { + + // 检查是否排除当前url + + // exclude := regexp.MustCompile("/static/*") + + path := c.Request.RequestURI + + if PassURL(path) { + c.Next() + return + } + + // 对用户的token进行校验 并对token续期 + token := c.GetHeader("token") + one := new(values.User) + if err := db.Redis().GetJsonData(common.GetBackendTokenKey(token), one); err != nil || one.Account == "" { + app := app.NewApp(c) + app.Code = values.CodeToken + app.Msg = "登录已过期,请重新登录" + app.Response() + c.Abort() + return + } + // if len(one.Power) > 0 { + // err := json.Unmarshal([]byte(one.Power), &one.PowerMap) + // if err != nil { + // log.Error("err:%v", err) + // app := app.NewApp(c) + // app.Code = values.CodePower + // app.Response() + // c.Abort() + // return + // } + // } + c.Set("user", one) + c.Next() + // 刷新token过期时间 + defer func() { + if token == values.AdminToken { + return + } + err := db.Redis().Expire(common.GetBackendTokenKey(token), values.RedisTokenEx) + if err != nil { + log.Error(err.Error()) + } + }() + } +} + +// PassURL 过滤url +func PassURL(path string) bool { + index := strings.Index(path, "?") + if index > 0 { + path = path[:index] + } + _, ok := passURLs[path] + if !ok { + // index := strings.LastIndex(path, "/") + // url := path[:index+1] + "*" + // _, ok = passURLs[url] + for k := range passURLs { + if strings.Contains(path, k) { + return true + } + } + } + return ok +} diff --git a/modules/customer/module.go b/modules/customer/module.go new file mode 100644 index 0000000..eaa1e8f --- /dev/null +++ b/modules/customer/module.go @@ -0,0 +1,113 @@ +package customer + +import ( + "context" + "net/http" + "server/call" + "server/config" + "server/db" + edb "server/db/es" + mdb "server/db/mysql" + rdb "server/db/redis" + "server/modules/customer/bdb" + "server/modules/customer/routers" + "server/util" + "time" + + "github.com/liangdas/mqant/conf" + "github.com/liangdas/mqant/log" + "github.com/liangdas/mqant/module" + basemodule "github.com/liangdas/mqant/module/base" +) + +var ( + Module = func() module.Module { + this := new(Customer) + return this + } + BackDB = new(mdb.MysqlClient) +) + +type Customer struct { + basemodule.BaseModule + httpSvr *http.Server + addr string +} + +func (b *Customer) GetType() string { + // 很关键,需要与配置文件中的Module配置对应 + return "customer" +} +func (b *Customer) Version() string { + // 可以在监控时了解代码版本 + return "1.0.0" +} +func (b *Customer) OnInit(app module.App, settings *conf.ModuleSettings) { + b.BaseModule.OnInit(b, app, settings) + + call.NewCaller(b) + + db.InitDB(&mdb.MysqlClient{}, &rdb.RedisClient{}, &edb.EsClient{}) + + bdb.InitMysql() + + // 自动初始化后台数据库 + bdb.MigrateDB() + + // 加载配置 + util.Go(func() { + loadConfig() + }) + + b.addr = config.GetConfig().Customer.Addr + + call.NewSnowflake(int64(config.GetConfig().WorkID)) + + call.InitReload(b.App.Transport()) +} + +func (b *Customer) startHttpServer() { + router := routers.SetUpRouter() + srv := &http.Server{ + Addr: b.addr, + Handler: router, + } + + go func() { + if err := srv.ListenAndServe(); err != nil { + panic(err) + } + }() + // returning reference so caller can call Shutdown() + b.httpSvr = srv +} + +func (b *Customer) Run(closeSig chan bool) { + log.Info("customer: starting HTTP server :%s", b.addr) + call.InitTimeWheel(closeSig) + call.InitWarn(b.App.Transport()) + b.startHttpServer() + <-closeSig + log.Info("customer: stopping HTTP server") + +} + +func (b *Customer) OnDestroy() { + // The context is used to inform the server it has 5 seconds to finish + // the request it is currently handling + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // now close the server gracefully ("shutdown") + // timeout could be given instead of nil as a https://golang.org/pkg/context/ + if err := b.httpSvr.Shutdown(ctx); err != nil { + log.Error("OnDestroy Customer Shutdown error:%v", err) + } + + log.Info("Customer: done. exiting") + + // 一定别忘了继承 + b.BaseModule.OnDestroy() + + log.Info("Customer 模块已销毁") +} diff --git a/modules/customer/routers/routers.go b/modules/customer/routers/routers.go new file mode 100644 index 0000000..916ca6a --- /dev/null +++ b/modules/customer/routers/routers.go @@ -0,0 +1,35 @@ +package routers + +import ( + "server/config" + "server/modules/customer/middleware" + + "github.com/gin-gonic/gin" +) + +func SetUpRouter() *gin.Engine { + release := config.GetBase().Release + if release { + gin.SetMode(gin.ReleaseMode) + // 禁用控制台颜色 + gin.DisableConsoleColor() + } else { + gin.SetMode(gin.DebugMode) + } + r := gin.New() + // 跨域处理 + r.Use(middleware.CrosHandler()) + r.Use(middleware.TokenMiddleWare()) + r.Use(middleware.PowerMiddleWare()) + r.Use(middleware.Recovery()) + + gmHandle(r) + account(r) + power(r) + common(r) + chat(r) + mail(r) + guser(r) + + return r +} diff --git a/modules/customer/routers/routers_account.go b/modules/customer/routers/routers_account.go new file mode 100644 index 0000000..f2b7f3a --- /dev/null +++ b/modules/customer/routers/routers_account.go @@ -0,0 +1,11 @@ +// 账号相关的接口 +package routers + +import ( + "github.com/gin-gonic/gin" + handler "server/modules/customer/handler/account" +) + +func account(e *gin.Engine) { + e.POST("/account/login", handler.Login) +} diff --git a/modules/customer/routers/routers_chat.go b/modules/customer/routers/routers_chat.go new file mode 100644 index 0000000..b2193ad --- /dev/null +++ b/modules/customer/routers/routers_chat.go @@ -0,0 +1,42 @@ +package routers + +import ( + "github.com/gin-gonic/gin" + handler2 "server/modules/backend/handler/guser" + handler "server/modules/customer/handler/chat" +) + +func chat(e *gin.Engine) { + // 工单分配 + e.POST("/order/allocate", handler.CustomerOrderAllocate) + + // 聊天历史 + e.POST("/customer/history", handler.GetCustomerHistory) + + // 消息已读 + e.POST("/customer/read/message", handler.ReadMessage) + + // 发送消息 + e.POST("/customer/send/message", handler.SendMessage) + + // 工单列表 + e.POST("/customer/order/list", handler.GetCustomerOrder) + + // 改变工单状态 + e.POST("/customer/order/change", handler.ChangeCustomerOrderStatus) + + // 编辑工单标签 + e.POST("/customer/label/edit", handler.EditCustomerOrderLabel) + + // 玩家信息 + e.POST("/customer/player/info", handler.GetPlayerInfo) + + // 玩家充值历史 + e.POST("/customer/rechargeHistory", handler2.GetGameUserRechargeHistory) + + // 玩家提现历史 + e.POST("/customer/withdrawHistory", handler2.GetGameUserWithdrawHistory) + + // 玩家客诉历史 + e.POST("/customer/complaint/history", handler.ComplaintHistory) +} diff --git a/modules/customer/routers/routers_common.go b/modules/customer/routers/routers_common.go new file mode 100644 index 0000000..a2c1965 --- /dev/null +++ b/modules/customer/routers/routers_common.go @@ -0,0 +1,28 @@ +// 账号相关的接口 +package routers + +import ( + handler "server/modules/customer/handler/common" + + "github.com/gin-gonic/gin" +) + +func common(e *gin.Engine) { + // 上传图片 + e.POST("/image/upload", handler.UploadImage) + + // 获取图片 + e.GET("/image/download", handler.DownImage) + + // 操作日志查看 + e.POST("/option/log/list", handler.OptList) + + e.GET("/common/goodsList", handler.GoodList) + e.GET("/common/productList", handler.ProductList) + e.GET("/common/gameList", handler.GamesList) + e.GET("/common/channelList", handler.ChannelList) + e.GET("/common/userInfo", handler.UserInfo) + e.GET("/common/getGameInfo", handler.GameInfo) + + e.POST("/common/feedbackList", handler.FeedbackList) +} diff --git a/modules/customer/routers/routers_gm.go b/modules/customer/routers/routers_gm.go new file mode 100644 index 0000000..153bea1 --- /dev/null +++ b/modules/customer/routers/routers_gm.go @@ -0,0 +1,27 @@ +package routers + +import ( + "github.com/gin-gonic/gin" + handler "server/modules/customer/handler/gm" +) + +func gmHandle(e *gin.Engine) { + // 客服机器人配置 + e.POST("/gm/customer/robot/info", handler.GetCustomerRobot) + e.POST("/gm/customer/robot/edit", handler.EditCustomerRobot) + e.POST("/gm/customer/robot/del", handler.DelCustomerRobot) + + // 客服订单标签 + e.GET("/gm/order/label/info", handler.GetCustomerLabel) + e.POST("/gm/order/label/edit", handler.EditCustomerLabel) + e.POST("/gm/order/label/del", handler.DelCustomerLabel) + + // 客服系统配置 + e.GET("/gm/customer/config/info", handler.GetConfigCustomer) + e.POST("/gm/customer/config/edit", handler.EditConfigCustomer) + e.POST("/gm/customer/config/del", handler.DelConfigCustomer) + + // 客服系统黑名单 + e.POST("/gm/customer/black/list", handler.GetCustomerBlackUser) + e.POST("/gm/customer/black/edit", handler.EditCustomerBlackUser) +} diff --git a/modules/customer/routers/routers_guser.go b/modules/customer/routers/routers_guser.go new file mode 100644 index 0000000..2787491 --- /dev/null +++ b/modules/customer/routers/routers_guser.go @@ -0,0 +1,10 @@ +package routers + +import ( + "github.com/gin-gonic/gin" + handler "server/modules/customer/handler/gm" +) + +func guser(e *gin.Engine) { + e.POST("/guser/info", handler.GetGameUserInfo) +} diff --git a/modules/customer/routers/routers_mail.go b/modules/customer/routers/routers_mail.go new file mode 100644 index 0000000..42ca781 --- /dev/null +++ b/modules/customer/routers/routers_mail.go @@ -0,0 +1,13 @@ +package routers + +import ( + "github.com/gin-gonic/gin" + handler "server/modules/backend/handler/mail" +) + +func mail(e *gin.Engine) { + e.POST("/mail/draftList", handler.DraftList) + e.POST("/mail/draftCreate", handler.DraftCreate) + e.POST("/mail/draftEdit", handler.DraftEdit) + e.POST("/mail/draftOpt", handler.DraftOpt) +} diff --git a/modules/customer/routers/routers_power.go b/modules/customer/routers/routers_power.go new file mode 100644 index 0000000..9a37e8e --- /dev/null +++ b/modules/customer/routers/routers_power.go @@ -0,0 +1,16 @@ +package routers + +import ( + "github.com/gin-gonic/gin" + handler "server/modules/customer/handler/power" +) + +func power(e *gin.Engine) { + e.GET("/power/user/list", handler.UserList) + e.POST("/power/user/add", handler.AddUser) + e.POST("/power/user/edit", handler.EditUserPower) + e.POST("/power/user/del", handler.DelUser) + e.GET("/power/role/list", handler.RoleList) + e.POST("/power/role/add", handler.AddRole) + e.POST("/power/role/edit", handler.EditRole) +} diff --git a/modules/customer/values/account.go b/modules/customer/values/account.go new file mode 100644 index 0000000..8f873c2 --- /dev/null +++ b/modules/customer/values/account.go @@ -0,0 +1,36 @@ +package values + +import ( + "time" +) + +// 用户身份 +const ( + UserRoleAdmin = iota + 1 // 管理员 + UserRole1 // 1级 + UserRole2 // 2级 + UserRole3 // 3级 +) + +const ( + RedisTokenEx = 60 * time.Minute + AdminToken = "lrmfe2hFQOSqvv4Z" // 常驻调用token +) + +// LoginReq 登录请求 +type LoginReq struct { + Account string `json:"Account" binding:"required"` + Pass string `json:"Pass" binding:"required"` +} + +// LoginResp 登录返回 +// Power:权限 +// Role:账户等级,1管理员 +// Token:身份码,登录成功后的所有请求需在header里加入token字段以进行后续请求 +// 账户id +type LoginResp struct { + Power interface{} + Token string + Role int + Id int +} diff --git a/modules/customer/values/chat.go b/modules/customer/values/chat.go new file mode 100644 index 0000000..50ed9d1 --- /dev/null +++ b/modules/customer/values/chat.go @@ -0,0 +1,119 @@ +package values + +import "server/common" + +// GetCustomerHistoryReq 获取聊天信息 +type GetCustomerHistoryReq struct { + Title int `json:"Title" binding:"required"` // 玩家uid + Page int `json:"Page" binding:"required"` + Num int `json:"Num" binding:"required"` +} + +// GetCustomerHistoryResp 获取聊天信息 +type GetCustomerHistoryResp struct { + List []*common.CustomerChatData + Count int64 +} + +// ReadMessageReq 消息已读接口 +type ReadMessageReq struct { + OrderId int // 订单id + Title int `json:"Title" binding:"required"` // 玩家uid + List []int `json:"List" binding:"required"` // 传入需要标记已读消息的id +} + +// 发送消息请求 +type SendMessageReq struct { + common.CustomerChatData +} + +// 查询订单请求 +type GetCustomerOrderReq struct { + Page int `json:"Page" binding:"required"` + Num int `json:"Num" binding:"required"` + CustomerUid int `json:"CustomerUid"` // uid + Uid int `json:"Uid"` // 玩家uid + Status int `json:"Status"` // 工单状态码 等于 匹配 + Vip int `json:"Vip"` // vip等级限制 + Label int `json:"Label"` // 标签 + Order int `json:"Order"` // 默认按创建时间逆序, 1表示按创建时间逆序, 2表示按创建时间正序 3表示按消息未读数排序 + Status2 int `json:"Status2"` // 工单状态码 小于等于 匹配 +} + +type GetCustomerOrderResp struct { + List []*common.CustomerOrder + Count int64 +} + +// 改变工单状态 +type ChangeCustomerOrderReq struct { + List []*CustomerOrderStatus +} + +type CustomerOrderStatus struct { + Id int `json:"Id"` // 改订单id + Status int `json:"Status"` // 工单状态码 + Label1 int `json:"Label1"` // 工单标签 + Label2 int `json:"Label2"` // 工单标签 + Label3 int `json:"Label3"` // 工单标签 + Label4 int `json:"Label4"` // 工单标签 + Label5 int `json:"Label5"` // 工单标签 +} + +// 分配工单到客服人员 +type CustomerOrderAllocateReq struct { + List []*OrderAllocate +} + +type OrderAllocate struct { + Uid int // 客服人员uid + OrderId int // 分配工单Id +} + +// 分配工单到客服人员 +type CustomerOrderAllocateResp struct { + List []*OrderAllocate +} + +// 编辑订单标签 +type EditCustomerOrderLabelReq struct { + OrderId int `json:"OrderId" binding:"required"` // 编辑的订单id + LabelId1 int `json:"LabelId1" binding:"required"` // 编辑的标签id + LabelId2 int `json:"LabelId2" binding:"required"` // 编辑的标签id + LabelId3 int `json:"LabelId3" binding:"required"` // 编辑的标签id + LabelId4 int `json:"LabelId4" binding:"required"` // 编辑的标签id + LabelId5 int `json:"LabelId5" binding:"required"` // 编辑的标签id +} + +// 玩家信息 +type GetPlayerInfoReq struct { + Uid int `json:"Uid" binding:"required"` // 客诉玩家uid +} + +// 玩家信息响应 +type GetPlayerInfoResp struct { + Uid int // 客诉玩家uid + Nick string // 昵称 + Avatar string // 头像 + Birth int64 // 注册日期 + Phone string // 玩家电话号码 + Channel int // 渠道 + Package string // 游戏包名 + Vip int // 玩家vip等级 + Recharge int64 // 玩家充值总金额 + Withdraw int64 // 玩家提现总金额 + CustomerBlack bool // 是否被拉黑 +} + +// 玩家客诉历史请求 +type ComplaintHistoryReq struct { + Uid int `json:"Uid" binding:"required"` // 玩家uid + Page int `json:"Page" binding:"required"` + Num int `json:"Num" binding:"required"` +} + +// 玩家客诉历史响应 +type ComplaintHistoryResp struct { + List []*common.CustomerOrder + Count int64 +} diff --git a/modules/customer/values/common.go b/modules/customer/values/common.go new file mode 100644 index 0000000..e6fbef6 --- /dev/null +++ b/modules/customer/values/common.go @@ -0,0 +1,76 @@ +package values + +import "server/common" + +// 图片存放地址 +var ( + ImagePath = "images" +) + +// DownLoadImageReq 图片请求 +type DownLoadImageReq struct { + ImagePath string `json:"ImagePath" binding:"required"` // 图片地址 +} + +// EditHistoryListReq 操作日志请求 +type EditHistoryListReq struct { + Page uint `json:"Page" binding:"required"` + Num uint `json:"Num" binding:"required"` +} + +// EditHistoryListResp 操作日志 +type EditHistoryListResp struct { + List []EditHistory + Count int64 +} + +// // GoodListResp 请求物品列表返回 +// type GoodListResp struct { +// List []*common.SheetGoods_config +// } + +// ProductListResp 请求充值列表返回 +type ProductListResp struct { + List []*common.ConfigPayProduct +} + +// GamesListResp 游戏列表 +type GamesListResp struct { + List []string +} + +// ChannelListResp 渠道列表 +type ChannelListResp struct { + List []*common.Channel +} + +// UserInfoResp 获取用户信息 +type UserInfoResp struct { + Name string + Role int + Power string +} + +// GameInfoResp 游戏信息,游戏名称跟游戏id +type GameInfoResp struct { + List []GameInfo + RoomId []int +} + +type GameInfo struct { + GameId int + Name string +} + +// FeedbackListReq 玩家反馈 +type FeedbackListReq struct { + Start string `json:"Start" binding:"required"` + End string `json:"End" binding:"required"` + Page int `json:"Page" binding:"required"` + Num int `json:"Num" binding:"required"` +} + +type FeedbackListResp struct { + List []*common.ESFeedback + Count int64 +} diff --git a/modules/customer/values/errorCode.go b/modules/customer/values/errorCode.go new file mode 100644 index 0000000..92c426f --- /dev/null +++ b/modules/customer/values/errorCode.go @@ -0,0 +1,10 @@ +package values + +// 错误码 +const ( + CodeOK = iota + CodeRetry + CodeToken + CodeParam + CodePower // 权限不足 +) diff --git a/modules/customer/values/gm.go b/modules/customer/values/gm.go new file mode 100644 index 0000000..95eea35 --- /dev/null +++ b/modules/customer/values/gm.go @@ -0,0 +1,76 @@ +package values + +import "server/common" + +// GetCustomerRobotReq 客服机器人请求 +type GetCustomerRobotReq struct { + ParentId int // 该消息父类id +} + +// GetCustomerRobotResp 客服机器人响应 +type GetCustomerRobotResp struct { + List []*common.ConfigCustomerRobot +} + +// EditCustomerRobotReq 编辑客服机器人消息 +type EditCustomerRobotReq struct { + List []*common.ConfigCustomerRobot +} + +// EditCustomerRobotResp 编辑客服机器人消息 +type EditCustomerRobotResp struct { + List []*common.ConfigCustomerRobot +} + +// DelCustomerRobotReq 删除客服机器人消息 +type DelCustomerRobotReq struct { + List []*int // id +} + +// GetCustomerLabelResp 订单标签响应 +type GetCustomerLabelResp struct { + List []*common.CustomerOrderLabel +} + +// EditCustomerLabelReq 编辑订单标签响应 +type EditCustomerLabelReq struct { + List []*common.CustomerOrderLabel +} + +// DelCustomerLabelReq 删除订单标签响应 +type DelCustomerLabelReq struct { + List []*int // id +} + +// GetConfigCustomerResp 客服系统配置 +type GetConfigCustomerResp struct { + List []*common.ConfigCustomer +} + +// EditConfigCustomerReq 客服系统配置 +type EditConfigCustomerReq struct { + List []*common.ConfigCustomer +} + +// DelConfigCustomerReq 客服系统配置 +type DelConfigCustomerReq struct { + List []*int // id +} + +// 客服黑名单请求 +type CustomerBlackUserReq struct { + Page int `json:"Page" binding:"required"` + Num int `json:"Num" binding:"required"` +} + +// 客服黑名单请求 +type CustomerBlackUserResp struct { + List []*common.CustomerBlackUser + Count int64 +} + +// DelConfigCustomerReq 客服系统配置 +type EditCustomerBlackUserReq struct { + Opt int // 1是添加黑名单 2是删除黑名单 + List []*int // uid 传玩家uid +} diff --git a/modules/customer/values/powers.go b/modules/customer/values/powers.go new file mode 100644 index 0000000..b425ae5 --- /dev/null +++ b/modules/customer/values/powers.go @@ -0,0 +1,109 @@ +package values + +// 权限对应页签 +const ( + PowerAll = iota + // 系统管理从1开始 + PowerGM // 系统配置 + PowerManageRole // 角色管理账户权限 + PowerManageUser // 用户管理账户权限 + PowerMail // 邮件权限 + PowerOrderAllocate // 工单分配权限 + PowerOrderProcessing // 工单处理权限 + PowerOptionLog // 操作日志权限 + PowerMax1 +) + +// 数据管理从100开始 +const ( + PowerRealData = iota + 100 // 实时数据 100 + PowerGameData // 游戏概况 101 + PowerNewPlayData // 新增用户分析 102 + PowerActivePlayData // 用户活跃分析 103 + PowerFinancialData // 系统货币 104 + PowerRechargeData // 收入概要 105 + PowerPlayData // 用户牌局 106 + PowerRechargeOrder // 订单明细 107 + PowerShareData // 分享数据 108 + PowerWithdrawOrder // 提现明细 109 + PowerGame // 游戏牌局 110 + PowerRealProfit // 实时盈亏 111 + PowerRedActivity // 红包雨活动 112 + PowerPlayerProfit // 玩家收益 113 + PowerEventTrack // 打点数据 114 + PowerMax2 +) + +func IsValidPower(p int) bool { + if p > PowerAll && p < PowerMax1 || p >= PowerRealData && p < PowerMax2 { + return true + } + return false +} + +var ( + // 权限映射表 + PowerMap = map[string]int{ + "/gm": PowerGM, + "/power/role": PowerManageRole, + "/power/user": PowerManageUser, + "/mail": PowerMail, + "/order/allocate": PowerOrderAllocate, + "/customer": PowerOrderProcessing, + "/option/log": PowerOptionLog, + } + // 页面按钮权限 + PowerButtonMap = map[int][]string{} +) + +type UserListResp struct { + List []User +} + +// AddUserReq 新增角色 +type AddUserReq struct { + Role int `json:"Role" binding:"required"` + Name string `json:"Name" binding:"required"` + Account string `json:"Account" binding:"required"` + Password string `json:"Password" binding:"required"` + Phone string `json:"Phone" binding:"required"` + Channels string `json:"Channels"` +} + +// DelUserReq 删除角色 +type DelUserReq struct { + ID int `json:"ID" binding:"required"` +} + +type RoleListResp struct { + List []Role +} + +// AddRoleReq 新增角色 +type AddRoleReq struct { + Role int `json:"Role" binding:"required"` + Name string `json:"Name" binding:"required"` + Power string `json:"Power" binding:"required"` +} + +// EditRoleReq 编辑角色 +type EditRoleReq struct { + ID int `json:"ID" binding:"required"` + Role int `json:"Role" binding:"required"` + Name string `json:"Name" binding:"required"` + Power string `json:"Power" binding:"required"` +} + +// EditPowerReq 编辑权限 +// ID 账户id +// Power 用户编辑后的权限 +type EditPowerReq struct { + ID int `json:"ID" binding:"required"` + Power *string `json:"Power"` + Role *int `json:"Role"` + Name *string `json:"Name"` + Account *string `json:"Account"` + Password *string `json:"Password"` + Phone *string `json:"Phone"` + Channels *string `json:"Channels"` +} diff --git a/modules/customer/values/tables.go b/modules/customer/values/tables.go new file mode 100644 index 0000000..d143e97 --- /dev/null +++ b/modules/customer/values/tables.go @@ -0,0 +1,52 @@ +package values + +// User 后台用户 +type User struct { + Name string `gorm:"column:name;type:varchar(32);uniqueIndex:name;not null;comment:名字" json:"Name"` + Account string `gorm:"column:account;type:varchar(32);uniqueIndex:account;not null;comment:账号" json:"Account"` + Password string `gorm:"column:password;type:varchar(32);not null;comment:密码" json:"Password"` + ID uint `gorm:"primarykey"` + Role int `gorm:"column:role;type:tinyint(4);not null;comment:角色" json:"Role"` + Power string `gorm:"column:power;type:varchar(512);not null;comment:权限" json:"Power"` + PowerMap map[int][]int `gorm:"-" json:"PowerMap"` + Phone string `gorm:"column:phone;type:varchar(32);uniqueIndex:phone;comment:手机号" json:"Phone"` + Channels string `gorm:"column:channels;type:varchar(512);not null;comment:拥有权限的包,为空时代表所有包都有权限" json:"Channels"` + SChannels []int `gorm:"-"` +} + +func (u *User) TableName() string { + return "users" +} + +// EditHistory 后台修改操作历史 +// Operator 操作人 +// Detail 修改内容 +// Time 时间 +// Model 操作模块,与权限列表对应 +type EditHistory struct { + Operator string `gorm:"column:operator;type:varchar(32);not null;comment:操作人" json:"Operator"` + Detail string `gorm:"column:detail;type:varchar(256);not null;comment:修改明细" json:"Detail"` + ID uint `gorm:"primarykey" json:"-"` + Time int64 `gorm:"column:time;type:bigint(20);default:0;comment:操作时间" json:"Time"` + UID int `gorm:"column:uid;type:int(11);comment:操作人id" json:"UID"` + Model int `gorm:"column:model;type:int(11);comment:操作功能模块" json:"Model"` +} + +func (e *EditHistory) TableName() string { + return "edit_history" +} + +// Role 后台角色 +// Role 角色等级 1是超管 +// Power 权限 +// Name 角色名 +type Role struct { + ID uint `gorm:"primarykey"` + Role int `gorm:"column:role;type:tinyint(4);uniqueIndex:role;not null;comment:角色等级" json:"Role"` + Power string `gorm:"column:power;type:varchar(512);not null;comment:权限" json:"Power"` + Name string `gorm:"column:name;type:varchar(32);uniqueIndex:name;not null;comment:角色名" json:"Name"` +} + +func (u *Role) TableName() string { + return "role" +} diff --git a/modules/web/handler/activity.go b/modules/web/handler/activity.go index 72698b3..573cf58 100644 --- a/modules/web/handler/activity.go +++ b/modules/web/handler/activity.go @@ -72,6 +72,8 @@ func GetUserTaskStatus(a *app.Gin) (ret []*values.OneTask) { taskId := call.CheckTask(call.Task{Uid: a.UID, Value: 0, Types: []common.TaskType{common.TaskTypeDownload}}) // 直接领取下载奖励 TaskComplete(a, &DrawTaskReq{TaskID: taskId}) + a.Code = values.CodeOK + a.Msg = "" } } // 非次数任务,需转换目标数值 diff --git a/modules/web/handler/customer.go b/modules/web/handler/customer.go new file mode 100644 index 0000000..83bb397 --- /dev/null +++ b/modules/web/handler/customer.go @@ -0,0 +1,334 @@ +package handler + +import ( + "fmt" + "io" + "mime" + "net/http" + "os" + "server/call" + "server/common" + "server/config" + "server/db" + "server/modules/web/app" + "server/modules/web/values" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/liangdas/mqant/log" + "gorm.io/gorm" +) + +// 加载客服系统配置 +func GetCustomerConfig(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.ResponseB() + }() + var resp values.GetCustomerConfigResp + resp.Config = call.GetConfigCustomer() + a.Data = resp +} + +// 获取客服机器人消息 +func GetCustomerRobotList(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.ResponseB() + }() + req := new(values.GetCustomerRobotReq) + if !a.SB(req) { + return + } + var resp values.GetCustomerRobotResp + + if req.ParentId == 0 { + resp.List = call.GetCustomerRobot(req.ParentId) + if len(resp.List) > 0 { + resp.Parent = resp.List[0] + } + resp.List = call.GetCustomerRobot(resp.List[0].ID) + } else { + resp.List = call.GetCustomerRobot(req.ParentId) + resp.Parent = call.GetCustomerRobotById(req.ParentId) + } + a.Data = resp +} + +// 机器人客服消息 +func GetCustomerRobotMsg(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.ResponseB() + }() + req := new(values.GetCustomerRobotMsgReq) + if !a.SB(req) { + return + } + var resp values.GetCustomerRobotMsgResp + resp.Content = call.GetCustomerRobotMsg(req.Id) + + // 获取玩家vip等级 + vip := call.GetVIP(a.UID) + + data := &common.CustomerOrder{Uid: a.UID, Start: time.Now().Unix(), Vip: vip.Level, Status: common.CustomerOrderCreate} + + // 创建机器人订单 + err := db.Mysql().Create(data) + if err != nil { + log.Error(err.Error()) + } + + // 10秒内玩家没有选择满意是否就默认完成工单 + time.AfterFunc(10*time.Second, func() { + err = db.Mysql().Update(data, map[string]interface{}{ + "status": common.CustomerOrderComplete, + }) + if err != nil { + log.Error(err.Error()) + } + }) + + a.Data = resp +} + +// 获取聊天历史 +func GetCustomerHistory(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.ResponseB() + }() + req := new(values.GetCustomerHistoryReq) + if !a.SB(req) { + return + } + var resp values.GetCustomerHistoryResp + Count, err := db.Mysql().QueryList(req.Page-1, req.Num, fmt.Sprintf("title = %v", a.UID), "id DESC", &common.CustomerChatData{}, &resp.List) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } + resp.Count = Count + + // 查询玩家上一次客服处理是否结束 + data := &common.CustomerOrder{Uid: a.UID} + err = db.Mysql().GetLast(data) + if err != nil { + if err != gorm.ErrRecordNotFound { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } + } + + // 是否客服黑名单 + if db.Mysql().Exist(&common.CustomerBlackUser{Uid: a.UID}) { + resp.IsBlack = true + } + + resp.Status = data.Status + + // 是否客服黑名单 + if db.Mysql().Exist(&common.CustomerBlackUser{Uid: a.UID}) { + resp.IsBlack = true + } + + a.Data = resp +} + +// 上传图片 +func UploadImage(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.ResponseB() + }() + + // 解析请求中的 multipart/form-data + err := c.Request.ParseMultipartForm(10 << 20) // 限制上传文件的大小为 10MB + if err != nil { + log.Error("图片超过10Mb, err:%v", err.Error()) + a.Code = values.CodeParam + return + } + + // 打开上传的文件 + file, header, err := c.Request.FormFile("file") + if err != nil { + log.Error("打开上传图片失败, err:%v", err.Error()) + a.Code = values.CodeParam + return + } + defer file.Close() + + // 获取上传文件的 MIME 类型 + contentType := header.Header.Get("Content-Type") + // 根据 MIME 类型获取文件扩展名 + ext, err := mime.ExtensionsByType(contentType) + if err != nil || len(ext) == 0 { + log.Error("获取上传图片格式失败, err:%v", err.Error()) + a.Code = values.CodeParam + return + } + imageName := call.SnowNode().Generate().String() + ext[0] + // 读取上传的文件内容 + data, err := io.ReadAll(file) + if err != nil { + log.Error("读取图片内容失败, err:%v", err.Error()) + a.Code = values.CodeRetry + return + } + + // 获取当前日期 + now := time.Now() + zeroTime := now.Format("20060102") + rounded := now.Hour() + dir := fmt.Sprintf("%v/%v/%v", "images", zeroTime, rounded) + + // 创建保存文件的目标文件 + err = createDirIfNotExist(dir) + if err != nil { + log.Error("创建图片保存目录, err:%v", err.Error()) + a.Code = values.CodeParam + return + } + // 将上传的文件复制到目标文件 + err = os.WriteFile(dir+"/"+imageName, data, 0644) // 写入文件 + if err != nil { + a.Code = values.CodeRetry + return + } + name := strings.Replace(dir+"/"+imageName, "/", ",", -1) + a.Data = config.GetConfig().Web.ImageURL + name +} + +// 检测文件目录是否存在 +func createDirIfNotExist(path string) error { + // 检查目录是否存在 + _, err := os.Stat(path) + if err == nil { + // 目录已存在,直接返回 + return nil + } + // 目录不存在,创建目录 + if os.IsNotExist(err) { + err = os.MkdirAll(path, 0755) + if err != nil { + return err + } + } + return nil +} + +// 获取图片 +func DownloadImage(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.ResponseB() + }() + + name := c.Query("path") + path := strings.Replace(name, ",", "/", -1) + + // 打开要返回的图片文件 + file, err := os.Open(path) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + defer file.Close() + + // 读取图片文件的内容 + data, err := io.ReadAll(file) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + arr := strings.Split(path, ".") + + // 返回图片 + c.Data(http.StatusOK, "image/"+arr[len(arr)-1], data) +} + +// 消息已读 +func ReadMessage(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.ResponseB() + }() + req := &values.ReadMessageReq{} + if !a.SB(req) { + return + } + + for i := 0; i < len(req.List); i++ { + err := db.Mysql().Update(&common.CustomerChatData{ID: req.List[i], Title: a.UID}, map[string]interface{}{"is_read": true}) + if err != nil { + log.Error(err.Error()) + } + } +} + +// 玩家发送消息 +func SendMessage(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.ResponseB() + }() + req := &values.SendMessageReq{} + if !a.SB(req) { + return + } + req.Title = a.UID + req.Uid = a.UID + req.Time = time.Now().Unix() + err := db.Mysql().Create(&req) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } + err = db.Mysql().UpdateW(&common.CustomerOrder{}, map[string]interface{}{ + "un_read": gorm.Expr("un_read + 1"), + }, fmt.Sprintf("uid = %v AND status < %v", a.UID, common.CustomerOrderComplete)) + if err != nil { + log.Error(err.Error()) + } + a.Data = req.ID +} + +// 转接人工服务 +func CreateCustomerOrder(c *gin.Context) { + a := app.NewApp(c) + defer func() { + a.ResponseB() + }() + + if db.Mysql().Exist(&common.CustomerBlackUser{Uid: a.UID}) { + // 该用户在黑名单 + log.Info("uid:%v 客服黑名单用户,不创建人工服务", a.UID) + return + } + + // 查询玩家上一次客服是否结束 + var order []*common.CustomerOrder + + count, err := db.Mysql().QueryList(0, 1, fmt.Sprintf("uid = %v AND status != %v", a.UID, common.CustomerOrderComplete), "", &common.CustomerOrder{}, &order) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } else { + if count == 0 { + vip := call.GetVIP(a.UID) + err = db.Mysql().Create(&common.CustomerOrder{Uid: a.UID, Start: time.Now().Unix(), Vip: vip.Level, Status: common.CustomerOrderCreate}) + if err != nil { + log.Error(err.Error()) + a.Code = values.CodeRetry + return + } + } + } +} diff --git a/modules/web/middleware/token.go b/modules/web/middleware/token.go index f7b44d2..120f9c1 100644 --- a/modules/web/middleware/token.go +++ b/modules/web/middleware/token.go @@ -55,6 +55,7 @@ var ( "/activity/betDraw/info": {}, "/activity/betDraw/record": {}, "/activity/activityPopup/info": {}, + "/customer/image/download": {}, } ) diff --git a/modules/web/routers/routers.go b/modules/web/routers/routers.go index f8255d6..97a8d03 100644 --- a/modules/web/routers/routers.go +++ b/modules/web/routers/routers.go @@ -53,6 +53,7 @@ func SetUpRouter() *gin.Engine { task(auth) telegram(auth) ad(auth) + customer(auth) } provider := r.Group("provider/") if config.GetBase().Release { diff --git a/modules/web/routers/routers_customer.go b/modules/web/routers/routers_customer.go new file mode 100644 index 0000000..ddfe0cc --- /dev/null +++ b/modules/web/routers/routers_customer.go @@ -0,0 +1,35 @@ +package routers + +import ( + "github.com/gin-gonic/gin" + "server/modules/web/handler" +) + +func customer(e *gin.RouterGroup) { + // 获取客服系统配置 + e.GET("/customer/config", handler.GetCustomerConfig) + + // 获取机器人信息 + e.POST("/customer/robot/list", handler.GetCustomerRobotList) + + // 获取机器人消息 + e.POST("/customer/robot/msg/list", handler.GetCustomerRobotMsg) + + // 获取客服聊天历史 + e.POST("/customer/history", handler.GetCustomerHistory) + + // 上传图片 + e.POST("/customer/upload/image", handler.UploadImage) + + // 加载图片 + e.GET("/customer/image/download", handler.DownloadImage) + + // 读消息 + e.POST("/customer/read/message", handler.ReadMessage) + + // 发送消息 + e.POST("/customer/send/message", handler.SendMessage) + + // 创建客服工单 + e.GET("/customer/create/order", handler.CreateCustomerOrder) +} diff --git a/modules/web/values/customer.go b/modules/web/values/customer.go new file mode 100644 index 0000000..4ea6786 --- /dev/null +++ b/modules/web/values/customer.go @@ -0,0 +1,63 @@ +package values + +import "server/common" + +// GetCustomerConfigResp 客服系统配置返回 +type GetCustomerConfigResp struct { + Config *common.ConfigCustomer +} + +// GetCustomerHistoryReq 获取聊天信息 +type GetCustomerHistoryReq struct { + Page int `json:"Page" binding:"required"` + Num int `json:"Num" binding:"required"` +} + +// GetCustomerHistoryResp 获取聊天信息 +type GetCustomerHistoryResp struct { + List []*common.CustomerChatData + Count int64 + Status int // 上一次工单状态 + IsBlack bool // 是否客服黑名单 +} + +// GetCustomerRobotReq 客服机器人请求 +type GetCustomerRobotReq struct { + ParentId int // 该消息父类id +} + +// GetCustomerRobotResp 客服机器人请求 +type GetCustomerRobotResp struct { + List []*common.ConfigCustomerRobot + Parent *common.ConfigCustomerRobot +} + +// GetCustomerRobotMsgReq 客服机器人请求 +type GetCustomerRobotMsgReq struct { + Id int // 该消息父类id +} + +// GetCustomerRobotMsgResp 客服机器人请求 +type GetCustomerRobotMsgResp struct { + Content *common.ConfigCustomerRobot +} + +// UploadImageResp 图片上传返回 +type UploadImageResp struct { + Path string // 返回图片地址 +} + +// DownloadImageReq 加载图片地址 +type DownloadImageReq struct { + Path string `json:"Path" binding:"required"` // 返回图片地址 +} + +// ReadMessageReq 消息已读接口 +type ReadMessageReq struct { + List []int `json:"List" binding:"required"` // 传入需要标记已读消息的id +} + +// 发送消息请求 +type SendMessageReq struct { + common.CustomerChatData +} diff --git a/pb/proto/platform.proto b/pb/proto/platform.proto index 96d2edb..30da3b1 100644 --- a/pb/proto/platform.proto +++ b/pb/proto/platform.proto @@ -134,3 +134,32 @@ message Product { repeated CurrencyPair Rewards = 2; // 充值获得的物品 int64 Amount = 3; // 充值的金额 } + + + +/****************************************通用平台类消息开始**********************************************/ +enum ServerCommonCmd{ + CMD_COMMON_INVALID = 0; // 无效 + CMD_NA_1 = 1; // NA + CMD_FREEZE_PLAYER_RESP = 2; // 账户被冻结 + CMD_GET_PLAYER_BALANCE_REQ = 3; // 请求金钱:NULL + CMD_GET_PLAYER_BALANCE_RESP = 4; // 返回金钱:GetPlayerBalanceResponse + CMD_NA_5 = 5; // NA + CMD_SYSMESSAGE_TO_USER_RESP = 6; // 系统消息:MessageToUserResp + CMD_NA_7 = 7; + CMD_PHP_2_USER_COMMON_RESP = 8; // 这个是转发PHP到客户端的协议:对应的结构PHP_2_USER_DATA_REQ是在web_msg_define.proto中定义的,消息中是嵌套结构,嵌套的协议定义由客户端和PHP自行商定 + CMD_GET_USER_ATTRI_REQ = 9; // 获取用户属性 MSG_GET_USER_ATTRI_REQ + CMD_GET_USER_ATTRI_RESP = 10; // 获取用户属性 MSG_GET_USER_ATTRI_RESP + CMD_UPDATE_USER_ATTRI_REQ = 11; // 更新用户属性 MSG_UPDATE_USER_ATTRI_REQ + CMD_UPDATE_USER_ATTRI_RESP = 12; // 更新用户属性 MSG_UPDATE_USER_ATTRI_RESP + CMD_GET_BONUS_REQ = 13; // NULL + CMD_GET_BONUS_RESP = 14; // MSG_GET_BONUS_RESP + CMD_BS_REQ = 15; + CMD_BS_RESP = 16; // MSG_MSG_QUE + CMD_BS_RedPointResp = 17; // 红点推送 RedPoint + CMD_BS_ConfigChangeResp = 18; // 配置变动通知 ConfigChangeResp + CMD_BS_BonusChangeResp = 19; // bonus变动通知 BonusChangeResp + CMD_BS_ActivityResp = 20; // 活动获得物品通知 ActivityResp + CMD_BS_SetAmountShareResp = 21; // 游戏结算分享活动 + CMD_BS_CustomerMsgResp = 22; // 客服消息通知 +} \ No newline at end of file