diff --git a/business/jxstore/cms/im.go b/business/jxstore/cms/im.go new file mode 100644 index 000000000..e163b2ab3 --- /dev/null +++ b/business/jxstore/cms/im.go @@ -0,0 +1,95 @@ +package cms + +import ( + "encoding/json" + "time" + + "git.rosy.net.cn/baseapi/utils" + + "git.rosy.net.cn/baseapi/platformapi/mtwmapi" + "git.rosy.net.cn/jx-callback/business/model" + "git.rosy.net.cn/jx-callback/globals/api" +) + +// JXMsg 京西消息结构体 +type JXMsg struct { + SendType string `json:"sendType"` //消息发送方 jx-商家;mt-美团;elm-饿了么 + Data interface{} `json:"data"` //美团/饿了么 单聊消息 +} + +var ( + rdb = api.Cacher +) + +const ( + SendTypeJx = "jx" //消息发送方jx标识符 + ExpireTimeDay = 24 * time.Hour //redis一天过期时间 +) + +// GetUserList 查询门店-用户消息列表(与当前商铺聊天的所有用户) appid:vendorStoreID:10/11(mt/elm) +func GetUserList(keys []string) (retVal []interface{}) { + if len(keys) == 0 { + return nil + } + for _, v := range keys { + if value := rdb.Get(v); value != nil { + retVal = append(retVal, value) + } + } + if len(retVal) > 0 { + return retVal + } + return nil +} + +// GetOneUserDetail 获取当前用户(单个)所有聊天记录 appID:vendorStoreID:10:userID +func GetOneUserDetail(msgID string) (retVal interface{}) { + if retVal = rdb.Get(msgID); retVal != nil { + return retVal + } + return nil +} + +// SetMessageDetail 存储门店用户聊天detail 门店发送 +func SetMessageDetail(data interface{}, vendorID string) error { + if vendorID == model.IMVendorIDMT { + param := JXMsg{ + SendType: SendTypeJx, + Data: data, + } + //生成与美团消息id + req := data.(mtwmapi.SingleChat) + key := GenMtMsgDetailID(req.AppPoiCode, req.AppID, req.OpenUserID) + //存储 + paramM, _ := json.Marshal(param) + err := rdb.RPush(key, string(paramM)) + ok, err := rdb.ExpireResult(key, ExpireTimeDay) + if err != nil || !ok { + return err + } + } + if vendorID == model.IMVendorIDELM { + + } + return nil +} + +//1 美团 + +// GenMtMsgDetailID 生成查询详细聊天记录ID +func GenMtMsgDetailID(vendorStoreID string, appID, userID int) string { + temp := utils.Int2Str(appID) + ":" + vendorStoreID + ":10:" + utils.Int2Str(userID) + return temp +} + +// SetMessageRead 平台方设置消息已读(不使用) +//func SetMessageRead(msgID, vendorStoreID, vendorID string) (err error) { +// if vendorID == model.IMVendorIDMT { +// err = api.MtwmAPI.MsgRead(vendorStoreID, msgID, vendorID) +// +// } +// if vendorID == model.IMVendorIDELM { +// +// } +// return nil +//} diff --git a/business/jxutils/cache/cache.go b/business/jxutils/cache/cache.go index e4682120e..391173fdc 100644 --- a/business/jxutils/cache/cache.go +++ b/business/jxutils/cache/cache.go @@ -11,4 +11,14 @@ type ICacher interface { Get(key string) interface{} GetAs(key string, ptr interface{}) error Keys(prefix string) ([]string, error) + + FlushDB() error + Incr(key string) error + LRange(key string) (retVal []string) + Exists(keys ...string) (int64, error) + RPush(key string, value interface{}) error + Expire(key string, expiration time.Duration) error + LRem(key string, count int, value interface{}) error + LSet(key string, index int, value interface{}) error + ExpireResult(key string, expiration time.Duration) (bool, error) } diff --git a/business/jxutils/cache/redis/redis.go b/business/jxutils/cache/redis/redis.go index 37e6d0bb0..38dfd5d59 100644 --- a/business/jxutils/cache/redis/redis.go +++ b/business/jxutils/cache/redis/redis.go @@ -73,3 +73,47 @@ func (c *Cacher) GetAs(key string, ptr interface{}) error { func (c *Cacher) Keys(prefix string) ([]string, error) { return c.client.Keys(prefix + "*").Result() } + +func (c *Cacher) RPush(key string, value interface{}) error { + return c.client.RPush(key, value).Err() +} + +func (c *Cacher) FlushDB() error { + return c.client.FlushDB().Err() +} + +func (c *Cacher) Expire(key string, expiration time.Duration) error { + return c.client.Expire(key, expiration).Err() +} + +func (c *Cacher) ExpireResult(key string, expiration time.Duration) (bool, error) { + ok, err := c.client.Expire(key, expiration).Result() + if err != nil { + return false, err + } + return ok, nil +} + +func (c *Cacher) Exists(keys ...string) (int64, error) { + ret := c.client.Exists(keys...) + return ret.Val(), ret.Err() +} + +func (c *Cacher) Incr(key string) error { + return c.client.Incr(key).Err() +} + +func (c *Cacher) LRange(key string) (retVal []string) { + if c.client.LLen(key).Val() > 0 { + retVal = c.client.LRange(key, 0, -1).Val() + } + return retVal +} + +func (c *Cacher) LSet(key string, index int, value interface{}) error { + return c.client.LSet(key, int64(index), value).Err() +} + +func (c *Cacher) LRem(key string, count int, value interface{}) error { + return c.client.LRem(key, int64(count), value).Err() +} diff --git a/business/jxutils/cache/redis/redis_test.go b/business/jxutils/cache/redis/redis_test.go index 0a314b783..3f1ffe8ba 100644 --- a/business/jxutils/cache/redis/redis_test.go +++ b/business/jxutils/cache/redis/redis_test.go @@ -13,7 +13,7 @@ var ( func init() { testinit1.Init() - cacher = New("localhost", 6379, "") + cacher = New("localhost", 6379, "123456") //ysq本地redis } type TestType struct { diff --git a/business/jxutils/unipush/push.go b/business/jxutils/unipush/push.go index 4baa74d79..1b79eb49e 100644 --- a/business/jxutils/unipush/push.go +++ b/business/jxutils/unipush/push.go @@ -4,9 +4,10 @@ import ( "encoding/json" "errors" "fmt" + "time" + "git.rosy.net.cn/jx-callback/business/authz/autils" "git.rosy.net.cn/jx-callback/globals/api2" - "time" "git.rosy.net.cn/baseapi/utils" "git.rosy.net.cn/jx-callback/business/model" @@ -19,6 +20,7 @@ const ( SoundsFileNewOrder = "newOrder.caf" SoundsFileNewAfsOrder = "afsOrder.caf" SoundsFileNewCancelOrder = "cancelOrder.caf" + SoundsFileNewImMsg = "" //TODO 待定 ) // NotifyNewOrder 推送新订单 @@ -108,6 +110,43 @@ func NotifyOrderCanceled(order *model.GoodsOrder) (err error) { return err } +// NotifyImNewMessage IM 新消息通知 +func NotifyImNewMessage(vendorStoreID string, vendorID int) error { + if len(vendorStoreID) == 0 { + return nil + } + var ( + temp = 0 + msg = MsgContext{} + ) + if vendorID == 10 { //美团 + temp = 1 + } else if vendorID == 11 { //饿了么 + temp = 3 + } + + store, err := dao.GetStoreBaseByVendorStoreID(vendorStoreID, temp) + if err != nil { + return fmt.Errorf("未查到此门店:%s", vendorStoreID) + } + + cid, err := GetStoreBoosCID(store.ID) + if err != nil { + globals.SugarLogger.Errorf("IM消息推送,获取门店老板CID错误 %v", err) + return err + } + fmt.Print(cid) + msg.MsgType = "newImMsg" + msg.VendorName = model.VendorChineseNames[temp] + msg.StoreTitle = store.Name + msg.Context = "老板,你有新的用户消息,请及时查看!" + context, _ := json.Marshal(msg) + body := fmt.Sprintf(msg.Context+"(%s)", model.VendorChineseNames[temp]) + + pushMsgByUniApp(store.ID, store.Name, cid, string(context), body, SoundsFileNewImMsg) + return err +} + // GetStoreBoosCID /************************************************************/ func GetStoreBoosCID(storeId int) ([]string, error) { // 根据订单获取门店的老板cid diff --git a/business/model/api_config.go b/business/model/api_config.go index cdb2bf2a9..0b0d6de8a 100644 --- a/business/model/api_config.go +++ b/business/model/api_config.go @@ -2,6 +2,9 @@ package model // VendorIDJD, VendorIDMTWM与VendorIDELM的定义和老系统是兼容的 const ( + IMVendorIDMT = "10" //美团IM + IMVendorIDELM = "11" //饿了么im + VendorTypeUnknown = 0 // 未知 VendorTypePurchase = 1 // 购物平台 VendorTypeDelivery = 2 // 快递平台 diff --git a/business/model/dao/store.go b/business/model/dao/store.go index f215fa866..35ed12a53 100644 --- a/business/model/dao/store.go +++ b/business/model/dao/store.go @@ -1646,3 +1646,17 @@ func BindJXPrintToStore(storeId int64, printSn, printKey string) error { _, err := ExecuteSQL(GetDB(), sql, []interface{}{printSn, printKey, model.VendorIDJxprint, model.NO, storeId}) return err } + +// GetStoreBaseByVendorStoreID 根据vendorStoreID获取门店基本信息 +func GetStoreBaseByVendorStoreID(vendorStoreID string, vendorID int) (storeDetail *model.Store, err error) { + if len(vendorStoreID) == 0 { + return nil, errors.New("vendorStoreID不能为空") + } + + DefaultTimeValue := utils.Str2Time("1970-01-01 00:00:00") + sql := `SELECT t.* FROM store t WHERE t.id = (SELECT s.store_id FROM store_map s WHERE s.vendor_store_id= ? AND s.vendor_id= ? AND s.deleted_at= ? )` + if err := GetRow(GetDB(), &storeDetail, sql, []interface{}{vendorStoreID, vendorID, DefaultTimeValue}); err != nil { + return nil, err + } + return storeDetail, err +} diff --git a/business/partner/printer/trendit/trendit.go b/business/partner/printer/trendit/trendit.go index 75d8ac6da..cf481b47b 100644 --- a/business/partner/printer/trendit/trendit.go +++ b/business/partner/printer/trendit/trendit.go @@ -102,9 +102,9 @@ func (p PrinterHandler) PrintOrder(ctx *jxcontext.Context, store *model.Store, s } content := "" if store.PrinterFontSize == partner.PrinterFontSizeBig || store.PrinterFontSize == partner.PrinterFontSizeBig2 { - content = p.getOrderContentBig(order, store.Tel1, storeDetail) + content = p.getOrderContentBigV2(order, store.Tel1, storeDetail) } else { - content = p.getOrderContent(order, store.Tel1, storeDetail) + content = p.getOrderContentV2(order, store.Tel1, storeDetail) } count := 0 for { @@ -163,6 +163,135 @@ func (p PrinterHandler) SetSound(ctx *jxcontext.Context, sn, id2, sound string) return err } +//不包括价格版本 +func (c *PrinterHandler) getOrderContentV2(order *model.GoodsOrder, storeTel string, storeDetail *dao.StoreDetail) (content string) { + expectedDeliveryTime := order.ExpectedDeliveredTime + if utils.IsTimeZero(expectedDeliveryTime) { + expectedDeliveryTime = order.OrderCreatedAt.Add(1 * time.Hour) + } + getCode := "" + if order.VendorID == model.VendorIDEBAI { + getCode = fmt.Sprintf("饿百取货码:%s", jxutils.GetEbaiOrderGetCode(order)) + } + if order.BuyerComment == "" { + order.BuyerComment = "客户电话: " + order.ConsigneeMobile + ",配送遇到问题,可联系18048531223取消配送单,禁止未配送直接完成订单!" + } + orderParams := []interface{}{} + orderFmt := `` + if storeDetail != nil { + if storeDetail.BrandIsPrint == model.NO { + orderFmt += `%s` + if order.VendorOrgCode == "34665" { + orderParams = append(orderParams, globals.StoreNameEbai2) + } else { + orderParams = append(orderParams, storeDetail.BrandName) + } + } + } + orderFmt += ` +-------------------------------- +下单时间: %s +期望送达: %s +客户姓名: %s +客户电话: %s +订单编号: %s
+%s#%d +%s +` + getCode + `-------------------------------- +客户地址: %s +-------------------------------- +客户备注:%s +-------------------------------- +商品列表 +商品名` + trenditapi.StrRepeat(" ", 20) + `数量 +` + trenditapi.StrRepeat("-", 32) + ` +` + orderParams = append(orderParams, + utils.Time2Str(order.OrderCreatedAt), + utils.Time2Str(expectedDeliveryTime), + order.ConsigneeName, + order.ConsigneeMobile, + order.VendorOrderID, + jxutils.GetVendorName(order.VendorID), + order.OrderSeq, + order.VendorOrderID, + order.ConsigneeAddress, + order.BuyerComment, + ) + + for k, sku := range order.Skus { + orderFmt += trenditapi.FormatPrintOrderItemV2(sku.SkuName, sku.Count, k+1) + } + orderFmt += `
共%d种%d件商品 +--------------#%d完-------------` + orderParams = append(orderParams, order.SkuCount, order.GoodsCount, order.OrderSeq) + return fmt.Sprintf(orderFmt, orderParams...) +} +func (c *PrinterHandler) getOrderContentBigV2(order *model.GoodsOrder, storeTel string, storeDetail *dao.StoreDetail) (content string) { + expectedDeliveryTime := order.ExpectedDeliveredTime + if utils.IsTimeZero(expectedDeliveryTime) { + expectedDeliveryTime = order.OrderCreatedAt.Add(1 * time.Hour) + } + getCode := "" + if order.VendorID == model.VendorIDEBAI { + getCode = fmt.Sprintf("饿百取货码:%s", jxutils.GetEbaiOrderGetCode(order)) + } + if order.BuyerComment == "" { + order.BuyerComment = "客户电话: " + order.ConsigneeMobile + ",配送遇到问题,可联系18048531223取消配送单,禁止未配送直接完成订单!" + } + orderParams := []interface{}{} + orderFmt := `` + if storeDetail != nil { + if storeDetail.BrandIsPrint == model.NO { + orderFmt += `%s` + if order.VendorOrgCode == "34665" { + orderParams = append(orderParams, globals.StoreNameEbai2) + } else { + orderParams = append(orderParams, storeDetail.BrandName) + } + } + } + orderFmt += ` +-------------------------------- +下单时间: %s +期望送达: %s +客户姓名: %s +客户电话: %s +订单编号: %s
+%s#%d +%s +` + getCode + `-------------------------------- +客户地址: %s +-------------------------------- +客户备注:%s +-------------------------------- +商品列表 +商品名` + trenditapi.StrRepeat(" ", 20) + `数量 +` + trenditapi.StrRepeat("-", 32) + ` +` + orderParams = append(orderParams, + utils.Time2Str(order.OrderCreatedAt), + utils.Time2Str(expectedDeliveryTime), + order.ConsigneeName, + order.ConsigneeMobile, + order.VendorOrderID, + jxutils.GetVendorName(order.VendorID), + order.OrderSeq, + order.VendorOrderID, + order.ConsigneeAddress, + order.BuyerComment, + ) + + for k, sku := range order.Skus { + orderFmt += trenditapi.FormatPrintOrderItemBigV2(sku.SkuName, sku.Count, k+1) + } + orderFmt += `
共%d种%d件商品 +--------------#%d完-------------` + orderParams = append(orderParams, order.SkuCount, order.GoodsCount, order.OrderSeq) + return fmt.Sprintf(orderFmt, orderParams...) +} + +//打印单价版本 //正常尺寸打印模板 func (c *PrinterHandler) getOrderContent(order *model.GoodsOrder, storeTel string, storeDetail *dao.StoreDetail) (content string) { expectedDeliveryTime := order.ExpectedDeliveredTime diff --git a/business/partner/printer/xpyun/xpyun.go b/business/partner/printer/xpyun/xpyun.go index 6d602d50e..af0851fbc 100644 --- a/business/partner/printer/xpyun/xpyun.go +++ b/business/partner/printer/xpyun/xpyun.go @@ -33,25 +33,26 @@ func (c *PrinterHandler) GetVendorID() int { } func (c *PrinterHandler) PrintMsg(ctx *jxcontext.Context, sn, copies, voiceType, msgContent string) (printerStatus *partner.PrinterStatus, err error) { if sn != "" { - if globals.EnableStoreWrite { - globals.SugarLogger.Debugf("printMsg voiceType====%s", voiceType) - printOrderID, err1 := api.XpyunAPI.Print(&xpyunapi.PrintRequest{ - Sn: sn, - Content: msgContent, - Copies: 1, - Voice: utils.Str2Int(voiceType), - Mode: xpyunapi.ModeCheckYes, - ExpiresIn: xpyunapi.ExpiresInDefault, - }) - if err1 == nil && printOrderID != "" { - printerStatus, err = c.GetPrinterStatus(ctx, sn, "") - } - } - } else { - printerStatus = &partner.PrinterStatus{ - PrintResult: partner.PrintResultNoPrinter, + //if globals.EnableStoreWrite { + globals.SugarLogger.Debugf("printMsg voiceType====%s", voiceType) + printOrderID, err1 := api.XpyunAPI.Print(&xpyunapi.PrintRequest{ + Sn: sn, + Content: msgContent, + Copies: 1, + Voice: utils.Str2Int(voiceType), + Mode: xpyunapi.ModeCheckYes, + ExpiresIn: xpyunapi.ExpiresInDefault, + }) + if err1 == nil && printOrderID != "" { + printerStatus, err = c.GetPrinterStatus(ctx, sn, "") } + //} } + //else { + // printerStatus = &partner.PrinterStatus{ + // PrintResult: partner.PrintResultNoPrinter, + // } + //} return printerStatus, err } @@ -107,9 +108,11 @@ func (c *PrinterHandler) PrintOrder(ctx *jxcontext.Context, store *model.Store, } content := "" if store.PrinterFontSize == partner.PrinterFontSizeBig || store.PrinterFontSize == partner.PrinterFontSizeBig2 { - content = c.getOrderContentBig(order, store.Tel1, storeDetail) + //content = c.getOrderContentBig(order, store.Tel1, storeDetail) + content = c.getOrderContentBigV2(order, store.Tel1, storeDetail) } else { - content = c.getOrderContent(order, store.Tel1, storeDetail) + //content = c.getOrderContent(order, store.Tel1, storeDetail) + content = c.getOrderContentV2(order, store.Tel1, storeDetail) } count := 0 for { @@ -169,6 +172,134 @@ func (c *PrinterHandler) SetSound(ctx *jxcontext.Context, sn, id2, sound string) return err } +//不包含价格版本 +func (c *PrinterHandler) getOrderContentV2(order *model.GoodsOrder, storeTel string, storeDetail *dao.StoreDetail) (content string) { + expectedDeliveryTime := order.ExpectedDeliveredTime + if utils.IsTimeZero(expectedDeliveryTime) { + expectedDeliveryTime = order.OrderCreatedAt.Add(1 * time.Hour) + } + getCode := "" + if order.VendorID == model.VendorIDEBAI { + getCode = fmt.Sprintf("饿百取货码:%s", jxutils.GetEbaiOrderGetCode(order)) + } + if order.BuyerComment == "" { + order.BuyerComment = "客户电话: " + order.ConsigneeMobile + ",配送遇到问题,可联系18048531223取消配送单,禁止未配送直接完成订单!" + } + orderParams := []interface{}{} + orderFmt := `` + if storeDetail != nil { + if storeDetail.BrandIsPrint == model.NO { + orderFmt += `%s` + if order.VendorOrgCode == "34665" { + orderParams = append(orderParams, globals.StoreNameEbai2) + } else { + orderParams = append(orderParams, storeDetail.BrandName) + } + } + } + orderFmt += ` +` + xpyunapi.StrRepeat("-", 32) + ` +下单时间: %s +预计送达: %s +客户姓名: %s +客户电话: %s
+订单编号: %s
+%s#%d +%s +` + getCode + + xpyunapi.StrRepeat("-", 32) + ` +客户地址: %s +` + xpyunapi.StrRepeat("-", 32) + `客户备注: %s +` + xpyunapi.StrRepeat("-", 32) + ` +商品列表 +商品名` + xpyunapi.StrRepeat(" ", 20) + `数量
` + xpyunapi.StrRepeat("-", 32) + orderParams = append(orderParams, + utils.Time2Str(order.OrderCreatedAt), + utils.Time2Str(expectedDeliveryTime), + order.ConsigneeName, + order.ConsigneeMobile, + order.VendorOrderID, + jxutils.GetVendorName(order.VendorID), + order.OrderSeq, + order.VendorOrderID, + order.ConsigneeAddress, + order.BuyerComment, + ) + + for k, sku := range order.Skus { + orderFmt += xpyunapi.FormatPrintOrderItemV2(sku.SkuName, sku.Count, k+1) + } + orderFmt += `
共%d种%d件商品` + orderFmt += `
` + orderFmt += "" + xpyunapi.StrRepeat("-", 14) + "#%d完" + xpyunapi.StrRepeat("-", 13) + `

` + orderParams = append(orderParams, order.SkuCount, order.GoodsCount, order.OrderSeq) + return fmt.Sprintf(orderFmt, orderParams...) +} + +func (c *PrinterHandler) getOrderContentBigV2(order *model.GoodsOrder, storeTel string, storeDetail *dao.StoreDetail) (content string) { + expectedDeliveryTime := order.ExpectedDeliveredTime + if utils.IsTimeZero(expectedDeliveryTime) { + expectedDeliveryTime = order.OrderCreatedAt.Add(1 * time.Hour) + } + getCode := "" + if order.VendorID == model.VendorIDEBAI { + getCode = fmt.Sprintf("饿百取货码:%s", jxutils.GetEbaiOrderGetCode(order)) + } + if order.BuyerComment == "" { + order.BuyerComment = "客户电话: " + order.ConsigneeMobile + ",配送遇到问题,可联系18048531223取消配送单,禁止未配送直接完成订单!" + } + orderParams := []interface{}{} + orderFmt := `` + if storeDetail != nil { + if storeDetail.BrandIsPrint == model.NO { + orderFmt += `%s` + if order.VendorOrgCode == "34665" { + orderParams = append(orderParams, globals.StoreNameEbai2) + } else { + orderParams = append(orderParams, storeDetail.BrandName) + } + } + } + orderFmt += ` +` + xpyunapi.StrRepeat("-", 32) + ` +下单时间: %s +预计送达: %s +客户姓名: %s +客户电话: %s
+订单编号: %s
+%s#%d +%s +` + getCode + + xpyunapi.StrRepeat("-", 32) + ` +客户地址: %s +` + xpyunapi.StrRepeat("-", 32) + `客户备注: %s +` + xpyunapi.StrRepeat("-", 32) + ` +商品列表 +商品名` + xpyunapi.StrRepeat(" ", 20) + `数量
` + xpyunapi.StrRepeat("-", 32) + orderParams = append(orderParams, + utils.Time2Str(order.OrderCreatedAt), + utils.Time2Str(expectedDeliveryTime), + order.ConsigneeName, + order.ConsigneeMobile, + order.VendorOrderID, + jxutils.GetVendorName(order.VendorID), + order.OrderSeq, + order.VendorOrderID, + order.ConsigneeAddress, + order.BuyerComment, + ) + + for k, sku := range order.Skus { + orderFmt += xpyunapi.FormatPrintOrderItemV2(sku.SkuName, sku.Count, k+1) + } + orderFmt += `
共%d种%d件商品` + orderFmt += `
` + orderFmt += "" + xpyunapi.StrRepeat("-", 14) + "#%d完" + xpyunapi.StrRepeat("-", 13) + `

` + orderParams = append(orderParams, order.SkuCount, order.GoodsCount, order.OrderSeq) + return fmt.Sprintf(orderFmt, orderParams...) +} + +//包含价格版本 // 新订单正常尺寸打印模板 func (c *PrinterHandler) getOrderContent(order *model.GoodsOrder, storeTel string, storeDetail *dao.StoreDetail) (content string) { expectedDeliveryTime := order.ExpectedDeliveredTime diff --git a/business/partner/purchase/ebai/callback.go b/business/partner/purchase/ebai/callback.go index 46198d1d6..f846a9471 100644 --- a/business/partner/purchase/ebai/callback.go +++ b/business/partner/purchase/ebai/callback.go @@ -33,6 +33,8 @@ func OnCallbackMsg(msg *ebaiapi.CallbackMsg) (response *ebaiapi.CallbackResponse storeDetail, err := dao.GetStoreDetailByVendorStoreID(dao.GetDB(), vendorStoreID, model.VendorIDEBAI, "") _, err = netprinter.PrintStoreStatus(jxcontext.AdminCtx, storeDetail, model.VendorIDMTWM, -9) response = api.EbaiAPI.Err2CallbackResponse(msg.Cmd, err, msg.Cmd) + } else if msg.Cmd == ebaiapi.CmdImMessageSendEvent || msg.Cmd == ebaiapi.CmdImMessageReadEvent { + response = CurPurchaseHandler.OnImMessage(msg) } } return response diff --git a/business/partner/purchase/ebai/im.go b/business/partner/purchase/ebai/im.go new file mode 100644 index 000000000..1799bcabf --- /dev/null +++ b/business/partner/purchase/ebai/im.go @@ -0,0 +1,23 @@ +package ebai + +import ( + "encoding/json" + + "git.rosy.net.cn/jx-callback/business/partner/purchase/im" + + "git.rosy.net.cn/baseapi/platformapi/ebaiapi" + "git.rosy.net.cn/jx-callback/globals/api" +) + +const ( + IMVendorIDELM = 11 //饿了么 +) + +// OnImMessage 用户/骑手 发送/已读消息 回调 +func (p *PurchaseHandler) OnImMessage(msg *ebaiapi.CallbackMsg) (response *ebaiapi.CallbackResponse) { + str, err := json.Marshal(msg.Data) + + im.ReadMsgFromVendor(IMVendorIDELM, msg.Source, str) + + return api.EbaiAPI.Err2CallbackResponse(msg.Cmd, err, nil) +} diff --git a/business/partner/purchase/im/im.go b/business/partner/purchase/im/im.go new file mode 100644 index 000000000..551c6d810 --- /dev/null +++ b/business/partner/purchase/im/im.go @@ -0,0 +1,355 @@ +package im + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + + "git.rosy.net.cn/jx-callback/globals" + + "git.rosy.net.cn/baseapi/platformapi/ebaiapi" + + "git.rosy.net.cn/baseapi/platformapi/mtwmapi" + "git.rosy.net.cn/baseapi/utils" + push "git.rosy.net.cn/jx-callback/business/jxutils/unipush" + "git.rosy.net.cn/jx-callback/globals/api" + "github.com/gorilla/websocket" +) + +// SendToVendor 向平台发消息 +func SendToVendor(msg []byte) { + var ( + w http.ResponseWriter + sendData SendData + err error + elmAppID = api.EbaiAPI.GetSource() + ) + + //解析数据 + if err = json.Unmarshal(msg, &sendData); err != nil { + return + } + + //存储数据 + ReadMsgFromClient(sendData.VendorID, elmAppID, sendData.Data) + + //发送信息 + if sendData.VendorID == VendorIDMT { + temp, _ := json.Marshal(sendData.Data) + Send(temp) + } + if sendData.VendorID == VendorIDELM { + param := sendData.Data.(ebaiapi.BusinessSendMsgReq) + if err := api.EbaiAPI.BusinessSendMsg(¶m); err != nil { + globals.SugarLogger.Debugf("elm发送信息错误:%v", err) + return + } + } + + if err != nil { + ClientRender(w, Fail, FailMsg, map[string]string{ + "errMsg": fmt.Sprintf("%v", err), + }) + } else { + ClientRender(w, SuccessCode, SuccessMsg, map[string]interface{}{ + "vendorID": sendData.VendorID, + "msg": "ok", + }) + } + return +} + +func Send(data []byte) { + //生成完整url + fullUrl := GenFullUrl() //clientID暂时不用 + + conn, resp, err := websocket.DefaultDialer.Dial(fullUrl, nil) + if err != nil || resp.StatusCode != 101 { + fmt.Printf("连接失败:%v http响应不成功", err) + } + //关闭 + defer func(conn *websocket.Conn) { + err := conn.Close() + if err != nil { + return + } + }(conn) + + err = conn.WriteMessage(websocket.TextMessage, data) + if err != nil { + fmt.Println(err) + } + + for { + _, msg, err := conn.ReadMessage() + if err != nil { + break + } else { + temp := string(msg) + if temp != HeartCheckSuccess { + ReadMsgFromVendor(VendorIDMT, "", msg) + } + } + fmt.Printf("%s receive: %s\n", conn.RemoteAddr(), string(msg)) + } +} + +// ReadMsgFromClient 存储客户端发送的消息 +func ReadMsgFromClient(vendorID int, elmAppID string, msg interface{}) { + var ( + err error + jxMsg = &JXMsg{} + userList = &UserMessageList{} + ) + + data, err := json.Marshal(msg) + if err != nil { + return + } + + if vendorID == VendorIDMT { + var MtSingleChat = mtwmapi.SingleChat{} + err = json.Unmarshal(data, &MtSingleChat) + jxMsg = &JXMsg{ + SendType: SendTypeJx, + Data: MtSingleChat, + } + userList = &UserMessageList{ + VendorID: VendorIDMT, + UserID: utils.Int2Str(MtSingleChat.OpenUserID), + LatestMsg: MtSingleChat.MsgContent, + LatestTime: MtSingleChat.Cts, + } + } + if vendorID == VendorIDELM { + var ElmData = ebaiapi.ImMessageSend{} + err = json.Unmarshal(data, &ElmData) + jxMsg = &JXMsg{ + SendType: SendTypeJx, + Data: ElmData, + } + userList = &UserMessageList{ + VendorID: VendorIDMT, + UserID: ElmData.PayLoad.GroupID, + LatestMsg: ElmData.PayLoad.Content, + LatestTime: int(ElmData.PayLoad.CreateTime), + } + } + + //1 存储详细聊天记录list + if err = SetMessageDetail(jxMsg, vendorID, elmAppID); err != nil { + return + } + //2 存储展示列表时单条数据 + if err = SetUserList(jxMsg, userList, vendorID, elmAppID); err != nil { + return + } +} + +// ReadMsgFromVendor 读取数据并存储到redis +func ReadMsgFromVendor(vendorID int, elmAppID string, msg []byte) { + if string(msg) == "" { + return + } + var ( + err error + vendorStoreID string + jxMsg = &JXMsg{} + userList = &UserMessageList{} + ) + if vendorID == VendorIDMT { + var MtSingleChat = mtwmapi.SingleChat{} + err = json.Unmarshal(msg, &MtSingleChat) + jxMsg = &JXMsg{ + SendType: SendTypeMt, + Data: MtSingleChat, + } + userList = &UserMessageList{ + VendorID: VendorIDMT, + UserID: utils.Int2Str(MtSingleChat.OpenUserID), + LatestMsg: MtSingleChat.MsgContent, + LatestTime: MtSingleChat.Cts, + } + vendorStoreID = MtSingleChat.AppPoiCode + } + if vendorID == VendorIDELM { + var ElmData = ebaiapi.ImMessageSend{} + err = json.Unmarshal(msg, &ElmData) + jxMsg = &JXMsg{ + SendType: SendTypeElm, + Data: ElmData, + } + userList = &UserMessageList{ + VendorID: VendorIDMT, + UserID: ElmData.PayLoad.GroupID, + LatestMsg: ElmData.PayLoad.Content, + LatestTime: int(ElmData.PayLoad.CreateTime), + } + } + + //1 存储详细聊天记录list + if err = SetMessageDetail(jxMsg, vendorID, elmAppID); err != nil { + return + } + //2 存储展示列表时单条数据 + if err = SetUserList(jxMsg, userList, vendorID, elmAppID); err != nil { + return + } + //3 cid推送新消息 + err = PushMsgByCid(vendorStoreID, vendorID) + //4 长链接通知给客户端 + ToClientChan <- clientInfo{Code: SuccessCode, Msg: fmt.Sprintf("%v", err), Data: jxMsg} +} + +// PushMsgByCid 通过cid push用户 +func PushMsgByCid(vendorStoreID string, vendorID int) error { + if err := push.NotifyImNewMessage(vendorStoreID, vendorID); err != nil { + return err + } + return nil +} + +// SetMessageDetail 赋值 +//格式 AppID:AppPoiCode:10:OpenUserID +func SetMessageDetail(req *JXMsg, vendorID int, elmAppID string) error { + //生成京西消息ID detail + msgID := GenMsgDetailID(req, vendorID, elmAppID) + + data, _ := json.Marshal(req) + err := rdb.RPush(msgID, string(data)) + ok, err := rdb.ExpireResult(msgID, ExpireTimeDay) + if err != nil || !ok { + return err + } + return nil +} + +// SetUserList 赋值 +//AppPoiCode:10 [userid1:{SingleChat},userid2:{}] +func SetUserList(jxMsg *JXMsg, userList *UserMessageList, vendorID int, elmAppID string) error { + //生成msgID + msgID := GenMsgListID(jxMsg, vendorID, elmAppID) + + //获取未读消息条数并删除旧数据 + cnt, err := GetNewAndTrim(msgID, userList.UserID) + if cnt > 0 { + userList.NewMessageNum = cnt + } else { + userList.NewMessageNum = 1 + } + //存储当前数据 + data, _ := json.Marshal(userList) + err = rdb.RPush(msgID, string(data)) + + ok, err := rdb.ExpireResult(msgID, ExpireTimeDay) + if err != nil || !ok { + return err + } + return nil +} + +// GetNewAndTrim 获取未读条数并清除旧数据 +func GetNewAndTrim(key string, flag string) (cnt int, err error) { + cnt = 0 + if n, err := rdb.Exists(key); n > 0 && err == nil { + s2 := rdb.LRange(key) + for i := 0; i < len(s2); i++ { + v := UserMessageList{} + _ = json.Unmarshal([]byte(s2[i]), &v) + if v.UserID == flag { + err = rdb.LSet(key, i, "del") + err = rdb.LRem(key, 0, "del") + s2 = append(s2[:i], s2[i+1:]...) + i-- + if v.NewMessageNum == 0 { //目前为首条 + cnt++ //赋值1 + } else { + cnt = v.NewMessageNum + } + } + } + } else { + return 0, nil + } + return cnt, err +} + +// GenMsgDetailID 生成查询详细聊天记录ID +func GenMsgDetailID(jxMsg *JXMsg, vendorID int, elmAppID string) (msgID string) { + if vendorID == VendorIDMT { + var d1 = jxMsg.Data.(mtwmapi.SingleChat) + msgID = utils.Int2Str(d1.AppID) + ":" + d1.AppPoiCode + ":10:" + utils.Int2Str(d1.OpenUserID) + } + if vendorID == VendorIDELM { + var d2 = jxMsg.Data.(ebaiapi.ImMessageSend) + msgID = elmAppID + ":" + d2.PlatformShopID + ":11:" + d2.PayLoad.GroupID + } + return msgID +} + +// GenMsgListID 生成展示列表时单条数据ID(部分) +func GenMsgListID(jxMsg *JXMsg, vendorID int, elmAppID string) (msgID string) { + if vendorID == VendorIDMT { + var d1 = jxMsg.Data.(mtwmapi.SingleChat) + msgID = utils.Int2Str(d1.AppID) + ":" + d1.AppPoiCode + ":10" + } + if vendorID == VendorIDELM { + var d2 = jxMsg.Data.(ebaiapi.ImMessageSend) + msgID = elmAppID + ":" + d2.PlatformShopID + ":11" + } + return msgID +} + +// GetImUserList 获取门店用户聊天列表 +func GetImUserList(req []RelInfo) (retVal []interface{}, err error) { + if len(req) == 0 { + return nil, errors.New("msgID不允许为空") + } + var keys []string + for _, i := range req { + key := i.AppID + ":" + i.VendorStoreID + ":" + i.VendorID + keys = append(keys, key) + } + for _, j := range keys { + temp := rdb.Get(j) + retVal = append(retVal, temp) + } + return retVal, err +} + +// GetImChatDetail 获取门店用户聊天详情 +func GetImChatDetail(req []UserRelInfo) (retVal []interface{}, err error) { + if len(req) == 0 { + return nil, errors.New("msgID不允许为空") + } + var keys []string + for _, i := range req { + key := i.AppID + ":" + i.VendorStoreID + ":" + i.VendorID + ":" + i.UserID + keys = append(keys, key) + } + for _, j := range keys { + temp := rdb.Get(j) + retVal = append(retVal, temp) + } + return retVal, err +} + +// SetJxMsgRead 设置jx消息已读 userID(美团:openUserID;饿了么:groupID) +func SetJxMsgRead(appID, vendorStoreID, vendorID, userID string) error { + key := appID + ":" + vendorStoreID + ":" + vendorID + if n, err := rdb.Exists(key); n > 0 && err == nil { + s2 := rdb.LRange(key) + for i := 0; i < len(s2); i++ { + v := UserMessageList{} + _ = json.Unmarshal([]byte(s2[i]), &v) + if v.UserID == userID { + err = rdb.LSet(key, i, "del") + err = rdb.LRem(key, 0, "del") + s2 = append(s2[:i], s2[i+1:]...) + i-- + } + } + } + return nil +} diff --git a/business/partner/purchase/im/im_model.go b/business/partner/purchase/im/im_model.go new file mode 100644 index 000000000..1c88c29c9 --- /dev/null +++ b/business/partner/purchase/im/im_model.go @@ -0,0 +1,275 @@ +package im + +import ( + "encoding/json" + "flag" + "fmt" + "git.rosy.net.cn/baseapi/utils" + "io" + "log" + "net" + "net/http" + "sync" + "time" + + "git.rosy.net.cn/baseapi/platformapi/mtwmapi" + "git.rosy.net.cn/jx-callback/globals/api" + "github.com/gazeboxu/mapstructure" + "github.com/gorilla/websocket" + "gopkg.in/ini.v1" +) + +// ClientManager 连接管理 +type ClientManager struct { + ClientIdMap map[string]*Client // 全部的连接 + ClientIdMapLock sync.RWMutex // 读写锁 + + Connect chan *Client // 连接处理 + DisConnect chan *Client // 断开连接处理 + + GroupLock sync.RWMutex + Groups map[string][]string + + SystemClientsLock sync.RWMutex + SystemClients map[string][]string +} + +// Client 客户端连接信息 +type Client struct { + ClientId string // 标识ID + Socket *websocket.Conn // 用户连接 + ConnectTime uint64 // 首次连接时间 + IsDeleted bool // 是否删除或下线 + UserId string // 业务端标识用户ID + //Extend string // 扩展字段,用户可以自定义 + //GroupList []string +} + +//channel通道结构体 +type clientInfo struct { + ClientId string `json:"clientId" validate:"required"` //链接ID + Data interface{} + SendUserId string + MessageId string + Code int + Msg string +} + +// RetData 统一返回值结构体 +type RetData struct { + Code int `json:"code"` //响应code + Msg string `json:"msg"` //响应msg success/fail + Data interface{} `json:"data"` //信息 + + //MessageType string `json:"messageType"` //消息类型 heart-心跳检测;send-发送消息;receive-接收消息 + //MessageId string `json:"messageId"` //发送/接收信息 id + //UserId string `json:"userId"` //必须是平台方userID +} + +type global struct { + LocalHost string //本机内网IP + ServerList map[string]string + ServerListLock sync.RWMutex +} +type commonConf struct { + HttpPort string + RPCPort string + Cluster bool + CryptoKey string +} + +// SendData 客户端写入参数 +type SendData struct { + //ClientId string `json:"clientId" validate:"required"` //链接ID + VendorID int `json:"vendorID"` //消息来源平台ID + Data interface{} `json:"data"` //发送给平台 美团/饿了么消息结构体 + //返回值 + //Code int `json:"code"` + //Msg string `json:"msg"` + //SendUserId string `json:"sendUserId"` +} + +// JXMsg 京西消息结构体 +type JXMsg struct { + SendType string `json:"sendType"` //消息发送方 jx-商家;mt-美团;elm-饿了么 + Data interface{} `json:"data"` //美团/饿了么 单聊消息 +} + +// GetUserListReq 获取门店用户聊天列表 +type GetUserListReq struct { + VendorStoreID string `json:"vendorStoreID"` //平台门店id + VendorID string `json:"vendorID"` //平台标识id + AppID string `json:"appID"` //应用ID +} + +type GetChatDetailReq struct { + VendorStoreID string `json:"vendorStoreID"` //平台门店id + VendorID string `json:"vendorID"` //平台标识id + AppID string `json:"appID"` //应用ID + UserID string `json:"userID"` //userID/groupID +} + +// UserMessageList 用户消息列表 +type UserMessageList struct { + VendorID int `json:"vendorID"` //平台品牌 10-美团 11-饿了么 + UserID string `json:"userID"` //用户ID + NewMessageNum int `json:"NewMessageNum"` //新消息数量 + LatestMsg string `json:"latestMsg"` //最新一条消息 + LatestTime int `json:"latestTime"` //最新一条消息发送时间 +} + +type RelInfo struct { + VendorStoreID string `json:"vendorStoreID"` //平台门店id + VendorID string `json:"vendorID"` //平台标识id + AppID string `json:"appID"` //应用ID +} + +type UserRelInfo struct { + VendorStoreID string `json:"vendorStoreID"` //平台门店id + VendorID string `json:"vendorID"` //平台标识id + AppID string `json:"appID"` //应用ID + UserID string `json:"userID"` //用户id/groupID +} + +var ( + cfg *ini.File + rdb = api.Cacher + Manager = NewClientManager() // 管理者 + CommonSetting = &commonConf{} + GlobalSetting = &global{} + ToClientChan chan clientInfo + heartbeatInterval = 60 * time.Second // 心跳间隔 + HeartCheckMsg = "~#HHHBBB#~" //心跳检测消息 + HeartCheckSuccess = "HB" //成功发送返回心跳消息 + VendorIDMT = 10 //im美团 + VendorIDELM = 11 //im饿了么 + SendTypeJx = "jx" //京西客户端发送方标识 + SendTypeMt = "mt" //美团用户发送方标识符 + SendTypeElm = "elm" //饿了么用户发送方标识符 + MTIMPushUrl = "wss://wpush.meituan.com/websocket" //buildPushConnect建立长连接 +) + +const ( + ExpireTimeDay = 24 * time.Hour //redis一天过期时间 + maxMessageSize = 8192 // 最大的消息大小 +) + +type renderData struct { + ClientId string `json:"clientId"` +} + +const ( + SuccessCode = 0 + SuccessMsg = "success" + Fail = -1 + FailMsg = "fail" + + SYSTEM_ID_ERROR = -1001 + ONLINE_MESSAGE_CODE = 1001 + OFFLINE_MESSAGE_CODE = 1002 +) + +// Render 统一返回值 +func Render(conn *websocket.Conn, messageId string, code int, message string, data interface{}) error { + return conn.WriteJSON(RetData{ + Code: code, + Msg: message, + Data: data, + }) +} + +func ClientRender(w http.ResponseWriter, code int, msg string, data interface{}) (str string) { + var retData RetData + + retData.Code = code + retData.Msg = msg + retData.Data = data + + retJson, _ := json.Marshal(retData) + str = string(retJson) + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _, _ = io.WriteString(w, str) + return +} + +func ConnRender(conn *websocket.Conn, data interface{}) (err error) { + err = conn.WriteJSON(RetData{ + Code: SuccessCode, + Msg: "success", + Data: data, + }) + return +} + +// Default 给默认值 +func Default() { + CommonSetting = &commonConf{ + HttpPort: "6000", + RPCPort: "7000", + Cluster: false, + CryptoKey: "Adba723b7fe06819", + } + + GlobalSetting = &global{ + LocalHost: getIntranetIp(), + ServerList: make(map[string]string), + } +} + +// Setup 初始化全局设置变量 +func Setup() { + configFile := flag.String("c", "conf/app.ini", "-c conf/app.ini") + + var err error + cfg, err = ini.Load(*configFile) + if err != nil { + log.Fatalf("setting.Setup, fail to parse 'conf/app.ini': %v", err) + } + + mapTo("common", CommonSetting) + + GlobalSetting = &global{ + LocalHost: getIntranetIp(), + ServerList: make(map[string]string), + } + fmt.Printf("LocalHost=%s\n ServerList=%s\n", GlobalSetting.LocalHost, utils.Format4Output(GlobalSetting.ServerList, false)) +} + +// mapTo map section +func mapTo(section string, v interface{}) { + err := cfg.Section(section).MapTo(v) + if err != nil { + log.Fatalf("Cfg.MapTo %s err: %v", section, err) + } +} + +//获取本机IP +func getIntranetIp() string { + addrs, _ := net.InterfaceAddrs() + for _, addr := range addrs { + // 检查ip地址判断是否回环地址 + if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + return ipnet.IP.String() + } + + } + } + return "" +} + +// GenFullUrl 组装完整websocket url以及生成clientID +func GenFullUrl() (fullUrl string) { + resp, err := api.MtwmAPI.GetConnectionToken() + if err != nil { + return "" + } + retVal := mtwmapi.GetConnTokenResp{} + err = mapstructure.Decode(resp, &retVal) + fullUrl = MTIMPushUrl + "/" + retVal.AppKey + "/" + retVal.ConnectionToken + //clientID = api.MtwmAPI.GetAppID() + ":" + retVal.ConnectionToken + //打印输出 + //fmt.Printf("Create websocket connect failCount:%d", retVal.UserCount) + return fullUrl +} diff --git a/business/partner/purchase/im/im_server.go b/business/partner/purchase/im/im_server.go new file mode 100644 index 000000000..f49bd23fe --- /dev/null +++ b/business/partner/purchase/im/im_server.go @@ -0,0 +1,253 @@ +package im + +import ( + "errors" + "fmt" + "net/http" + "time" + + "git.rosy.net.cn/baseapi/utils" + + "git.rosy.net.cn/jx-callback/globals" + "github.com/gorilla/websocket" +) + +func Init() { + //初始化 + ToClientChan = make(chan clientInfo, 1000) + //写入全局变量 + //Default() + + Setup() + //建立长链接 + //StartWebSocket(res, req) + Send([]byte(HeartCheckMsg)) + + //启动定时器 + PingTimer() + + go WriteMessage() + + go Manager.Start() + + fmt.Printf("服务器启动成功,端口号:%s\n", CommonSetting.HttpPort) +} + +func StartWebSocket(w http.ResponseWriter, r *http.Request) { + + conn, err := (&websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + // 允许所有CORS跨域请求 + CheckOrigin: func(r *http.Request) bool { + return true + }, + }).Upgrade(w, r, nil) + + if err != nil { + globals.SugarLogger.Errorf("upgrade error: %v", err) + http.NotFound(w, r) + return + } + + //设置读取消息大小上线 + conn.SetReadLimit(maxMessageSize) + + clientId := "" + temp := r.Header["Clientid"] + if temp[0] != "" { + clientId = temp[0] + } else { + clientId = "defaultClientIDJXCS" + } + + clientSocket := NewClient(clientId, conn) + + //读取客户端消息 + clientSocket.Read() + + if err = ConnRender(conn, renderData{ClientId: clientId}); err != nil { + _ = conn.Close() + return + } + + // 用户连接事件 + Manager.Connect <- clientSocket + +} + +// PingTimer 定时器发送心跳 +func PingTimer() { + go func() { + ticker := time.NewTicker(heartbeatInterval) + defer ticker.Stop() + //测试用 + i := 0 + for { + i++ + <-ticker.C + for clientId, conn := range Manager.AllClient() { + if err := conn.Socket.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(time.Second)); err != nil { + Manager.DisConnect <- conn + globals.SugarLogger.Debugf("发送心跳失败: %s 总连接数:%d", clientId, Manager.Count()) + } + if err := ConnRender(conn.Socket, renderData{ClientId: clientId}); err != nil { + return + } + globals.SugarLogger.Debugf("clientId=%s,i=%d", clientId, i) + } + } + }() +} + +// WriteMessage 监听并发送给客户端信息 +func WriteMessage() { + i := 0 + for { + clientInfo := <-ToClientChan + //广播发送通知所有客户端 + i++ + fmt.Printf("WriteMessage clientInfo=%s i=%d", utils.Format4Output(clientInfo, false), i) + if Manager.AllClient() != nil { + for _, conn := range Manager.AllClient() { + if err := Render(conn.Socket, clientInfo.MessageId, clientInfo.Code, clientInfo.Msg, clientInfo.Data); err != nil { + Manager.DisConnect <- conn + } + } + } else { + globals.SugarLogger.Debugf("无客户端连接,请检查") + return + } + //if conn, err := Manager.GetByClientId(clientInfo.ClientId); err == nil && conn != nil { + // if err := Render(conn.Socket, clientInfo.MessageId, clientInfo.Code, clientInfo.Msg, clientInfo.Data); err != nil { + // Manager.DisConnect <- conn + // } + //} + } +} + +// Start 管道处理程序 +func (manager *ClientManager) Start() { + for { + select { + case client := <-manager.Connect: + // 建立连接事件 + manager.EventConnect(client) + case conn := <-manager.DisConnect: + // 断开连接事件 + manager.EventDisconnect(conn) + } + } +} + +//从客户端读取数据 +func (c *Client) Read() { + go func() { + for { + messageType, msg, err := c.Socket.ReadMessage() + if err != nil { + if messageType == -1 && websocket.IsCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { + Manager.DisConnect <- c + return + } else if messageType != websocket.PingMessage { + return + } + } else { + SendToVendor(msg) + } + } + }() +} + +// 以下为连接事件操作******************************************* + +// EventConnect 建立连接事件 +func (manager *ClientManager) EventConnect(client *Client) { + manager.AddClient(client) +} + +// EventDisconnect 断开连接事件 +func (manager *ClientManager) EventDisconnect(client *Client) { + //关闭连接 + _ = client.Socket.Close() + manager.DelClient(client) + //标记销毁 + client.IsDeleted = true + client = nil +} + +//以下为客户端Client操作******************************************* + +// NewClient 初始化Client +func NewClient(clientId string, socket *websocket.Conn) *Client { + return &Client{ + ClientId: clientId, + Socket: socket, + ConnectTime: uint64(time.Now().Unix()), + IsDeleted: false, + } +} + +// AddClient 添加客户端 +func (manager *ClientManager) AddClient(client *Client) { + manager.ClientIdMapLock.Lock() + defer manager.ClientIdMapLock.Unlock() + + manager.ClientIdMap[client.ClientId] = client +} + +// DelClient 删除客户端 +func (manager *ClientManager) DelClient(client *Client) { + manager.delClientIdMap(client.ClientId) + +} + +// 删除clientIdMap +func (manager *ClientManager) delClientIdMap(clientId string) { + manager.ClientIdMapLock.Lock() + defer manager.ClientIdMapLock.Unlock() + + delete(manager.ClientIdMap, clientId) +} + +// GetByClientId 通过clientId获取client +func (manager *ClientManager) GetByClientId(clientId string) (*Client, error) { + manager.ClientIdMapLock.RLock() + defer manager.ClientIdMapLock.RUnlock() + + if client, ok := manager.ClientIdMap[clientId]; !ok { + return nil, errors.New("客户端不存在") + } else { + return client, nil + } +} + +// AllClient 获取所有的客户端 +func (manager *ClientManager) AllClient() map[string]*Client { + manager.ClientIdMapLock.RLock() + defer manager.ClientIdMapLock.RUnlock() + + return manager.ClientIdMap +} + +//与客户端的交互操作************************* + +// NewClientManager 初始化客户端管理 +func NewClientManager() (clientManager *ClientManager) { + clientManager = &ClientManager{ + ClientIdMap: make(map[string]*Client), + Connect: make(chan *Client, 10000), + DisConnect: make(chan *Client, 10000), + Groups: make(map[string][]string, 100), + SystemClients: make(map[string][]string, 100), + } + + return +} + +// Count 获取客户端数量 +func (manager *ClientManager) Count() int { + manager.ClientIdMapLock.RLock() + defer manager.ClientIdMapLock.RUnlock() + return len(manager.ClientIdMap) +} diff --git a/conf/app.ini b/conf/app.ini new file mode 100644 index 000000000..e69de29bb diff --git a/controllers/im.go b/controllers/im.go new file mode 100644 index 000000000..493b6455c --- /dev/null +++ b/controllers/im.go @@ -0,0 +1,93 @@ +package controllers + +import ( + "encoding/json" + + "git.rosy.net.cn/jx-callback/business/partner/purchase/im" + "github.com/astaxie/beego/server/web" +) + +type IMController struct { + web.Controller +} + +// @Title IM初始化长链接 +// @Description IM初始化长链接 +// @Success 200 {object} controllers.CallResult +// @Failure 200 {object} controllers.CallResult +// @router /StartWebSocket [get] +//func (c *IMController) StartWebSocket() { +// var ( +// w http.ResponseWriter +// r *http.Request +// ) +// im.StartWebSocket(w, r) +//} + +// @Title IM获取门店用户聊天列表 +// @Description IM获取门店用户聊天列表 +// @Param token header string true "认证token" +// @Param payLoad formData string true "平台应用映射关系" +// @Success 200 {object} controllers.CallResult +// @Failure 200 {object} controllers.CallResult +// @router /GetIMUserList [get] +func (c *IMController) GetIMUserList() { + c.callGetIMUserList(func(params *tImGetIMUserListParams) (retVal interface{}, errCode string, err error) { + var relInfo []im.RelInfo + if err = json.Unmarshal([]byte(params.PayLoad), &relInfo); err != nil { + retVal, err = im.GetImUserList(relInfo) + } + return retVal, "", err + }) +} + +// @Title IM获取单个用户聊天详情 +// @Description IM获取单个用户聊天详情 +// @Param token header string true "认证token" +// @Param payLoad formData string true "平台用户应用映射关系" +// @Success 200 {object} controllers.CallResult +// @Failure 200 {object} controllers.CallResult +// @router /GetImChatDetail [get] +func (c *IMController) GetImChatDetail() { + c.callGetImChatDetail(func(params *tImGetImChatDetailParams) (retVal interface{}, errCode string, err error) { + var temp []im.UserRelInfo + if err = json.Unmarshal([]byte(params.PayLoad), &temp); err == nil { + retVal, err = im.GetImChatDetail(temp) + } + return retVal, "", err + }) +} + +// @Title IM设置门店与单个用户已读 +// @Description IM设置门店与单个用户已读 +// @Param token header string true "认证token" +// @Param appID formData string true "应用id" +// @Param vendorStoreID formData string true "平台门店id" +// @Param vendorID formData string true "平台id" +// @Param userID formData string true "用户id/会话id" +// @Success 200 {object} controllers.CallResult +// @Failure 200 {object} controllers.CallResult +// @router /SetImMsgRead [post] +func (c *IMController) SetImMsgRead() { + c.callSetImMsgRead(func(params *tImSetImMsgReadParams) (retVal interface{}, errCode string, err error) { + err = im.SetJxMsgRead(params.AppID, params.VendorStoreID, params.VendorID, params.UserID) + return nil, "", err + }) +} + +// @Title 向平台商发送信息 +// @Description 向平台商发送信息 +// @Param token header string true "认证token" +// @Param sendData formData string true "平台商消息结构体" +// @Success 200 {object} controllers.CallResult +// @Failure 200 {object} controllers.CallResult +// @router /SendToVendor [post] +//func (c *IMController) SendToVendor() { +// c.callSendToVendor(func(params *tImSendToVendorParams) (retVal interface{}, errCode string, err error) { +// var sendData im.SendData +// if err = json.Unmarshal([]byte(params.SendData), &sendData); err == nil { +// im.SendToVendor(sendData) +// } +// return nil, "", err +// }) +//} diff --git a/globals/api/api.go b/globals/api/api.go index d8c2ab1fc..6cb943829 100644 --- a/globals/api/api.go +++ b/globals/api/api.go @@ -293,7 +293,11 @@ func Init() { BaiDuNaviAPI = baidunavi.New(beego.AppConfig.DefaultString("baidunaviAK", ""), beego.AppConfig.DefaultString("baidunaviSK", "")) QiniuAPI = qbox.NewMac(beego.AppConfig.DefaultString("qiniuAK", ""), beego.AppConfig.DefaultString("qiniuSK", "")) ShowAPI = showapi.New(beego.AppConfig.DefaultInt("showAppID", 0), beego.AppConfig.DefaultString("showAppSecret", "")) - Cacher = redis.New(beego.AppConfig.DefaultString("redisHost", "localhost"), beego.AppConfig.DefaultInt("redisPort", 0), beego.AppConfig.DefaultString("redisPassword", "")) + + //Cacher = redis.New(beego.AppConfig.DefaultString("redisHost", ""), beego.AppConfig.DefaultInt("redisPort", ""), beego.AppConfig.DefaultString("redisPassword", "")) + //Todo 本地测试用 + Cacher = redis.New(beego.AppConfig.DefaultString("redisHost", "127.0.0.1"), beego.AppConfig.DefaultInt("redisPort", 6379), beego.AppConfig.DefaultString("redisPassword", "123456")) + AliUpcAPI = aliupcapi.New(beego.AppConfig.DefaultString("aliUpcAppCode", "")) DingDingAPI = dingdingapi.NewWithAgentID(beego.AppConfig.DefaultInt64("dingdingAgentID", 0), beego.AppConfig.DefaultString("dingdingCorpID", ""), beego.AppConfig.DefaultString("dingdingAppKey", ""), beego.AppConfig.DefaultString("dingdingSecret", ""), diff --git a/globals/beegodb/beegodb.go b/globals/beegodb/beegodb.go index b2b3d56a8..d2590df44 100644 --- a/globals/beegodb/beegodb.go +++ b/globals/beegodb/beegodb.go @@ -6,25 +6,24 @@ import ( "git.rosy.net.cn/jx-callback/business/model/legacymodel" "git.rosy.net.cn/jx-callback/globals" "github.com/astaxie/beego/client/orm" - "github.com/astaxie/beego/server/web" ) func Init() { // set default database // orm.RegisterDataBase(aliasName, driverName, dataSource, params) //正式服务器 - orm.RegisterDataBase("default", "mysql", web.AppConfig.DefaultString("dbConnectStr", "")) - orm.RegisterDataBase("c4beta", "mysql", "ubuntu:WebServer@1@tcp(111.231.218.230:3306)/jxd_dev_0?charset=utf8mb4&loc=Local&parseTime=true") - orm.RegisterDataBase("api", "mysql", "root:WebServer@1@tcp(127.0.0.1:3306)/api?charset=utf8mb4&loc=Local&parseTime=true") + //orm.RegisterDataBase("default", "mysql", web.AppConfig.DefaultString("dbConnectStr", "")) + //orm.RegisterDataBase("c4beta", "mysql", "ubuntu:WebServer@1@tcp(111.231.218.230:3306)/jxd_dev_0?charset=utf8mb4&loc=Local&parseTime=true") + //orm.RegisterDataBase("api", "mysql", "root:WebServer@1@tcp(127.0.0.1:3306)/api?charset=utf8mb4&loc=Local&parseTime=true") // 本地测试服调试 // orm.RegisterDataBase("default", "mysql", web.AppConfig.DefaultString("dbConnectStr", "")) //orm.RegisterDataBase("api", "mysql", "root:WebServer@1@tcp(127.0.0.1:3306)/api?charset=utf8mb4&loc=Local&parseTime=true") //orm.RegisterDataBase("c4beta", "mysql", "ubuntu:WebServer@1@tcp(127.0.0.1:3306)/jxd_dev_0?charset=utf8mb4&loc=Local&parseTime=true") //本地服务器测试用 -ysq - //orm.RegisterDataBase("default", "mysql", "root:123456@tcp(127.0.0.1:3306)/jxd_dev_0?charset=utf8mb4&loc=Local&parseTime=true") - //orm.RegisterDataBase("c4beta", "mysql", "root:123456@tcp(127.0.0.1:3306)/jxd_dev_0?charset=utf8mb4&loc=Local&parseTime=true") - //orm.RegisterDataBase("api", "mysql", "root:123456@tcp(127.0.0.1:3306)/api?charset=utf8mb4&loc=Local&parseTime=true") + orm.RegisterDataBase("default", "mysql", "root:123456@tcp(127.0.0.1:3306)/jxd_dev_0?charset=utf8mb4&loc=Local&parseTime=true") + orm.RegisterDataBase("c4beta", "mysql", "root:123456@tcp(127.0.0.1:3306)/jxd_dev_0?charset=utf8mb4&loc=Local&parseTime=true") + orm.RegisterDataBase("api", "mysql", "root:123456@tcp(127.0.0.1:3306)/api?charset=utf8mb4&loc=Local&parseTime=true") // 开启sql打印 //orm.Debug = true diff --git a/main.go b/main.go index 457eca468..dbe4b3ea5 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,8 @@ import ( "os" "time" + "git.rosy.net.cn/jx-callback/business/partner/purchase/im" + "git.rosy.net.cn/jx-callback/business/enterprise" "git.rosy.net.cn/jx-callback/business/jxcallback/auto_delivery" @@ -93,6 +95,15 @@ func Init() { misc.Init() enterprise.Init() // 初始化enterprise key auto_delivery.Init() // 初始化骑手列表 + + go im.Init() //初始化ws连接 + go http.HandleFunc("/v2/im/StartWebSocket", im.StartWebSocket) + + //test + //mux := http.NewServeMux() + //mux.HandleFunc("/v2/im/StartWebSocket", im.StartWebSocket) + //go http.ListenAndServe(":8082", mux) + } // 返回true表示非运行服务 diff --git a/routers/commentsRouter_controllers.go b/routers/commentsRouter_controllers.go index 1bdfbe821..2bcb2583c 100644 --- a/routers/commentsRouter_controllers.go +++ b/routers/commentsRouter_controllers.go @@ -4304,6 +4304,40 @@ func init() { MethodParams: param.Make(), Filters: nil, Params: nil}) + //im + web.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:IMController"] = append(web.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:IMController"], + web.ControllerComments{ + Method: "GetIMUserList", + Router: `/GetIMUserList`, + AllowHTTPMethods: []string{"get"}, + MethodParams: param.Make(), + Filters: nil, + Params: nil}) + web.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:IMController"] = append(web.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:IMController"], + web.ControllerComments{ + Method: "GetImChatDetail", + Router: `/GetImChatDetail`, + AllowHTTPMethods: []string{"get"}, + MethodParams: param.Make(), + Filters: nil, + Params: nil}) + web.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:IMController"] = append(web.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:IMController"], + web.ControllerComments{ + Method: "SetImMsgRead", + Router: `/SetImMsgRead`, + AllowHTTPMethods: []string{"post"}, + MethodParams: param.Make(), + Filters: nil, + Params: nil}) + //web.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:IMController"] = append(web.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:IMController"], + // web.ControllerComments{ + // Method: "StartWebSocket", + // Router: `/StartWebSocket`, + // AllowHTTPMethods: []string{"get"}, + // MethodParams: param.Make(), + // Filters: nil, + // Params: nil}) + //web.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:FnController"] = append(web.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:FnController"], // web.ControllerComments{ // Method: "FnStore", diff --git a/routers/router.go b/routers/router.go index ef6e2191d..2554e47ab 100644 --- a/routers/router.go +++ b/routers/router.go @@ -9,7 +9,6 @@ package routers import ( "git.rosy.net.cn/jx-callback/controllers" - "github.com/astaxie/beego/server/web" beecontext "github.com/astaxie/beego/server/web/context" ) @@ -166,6 +165,11 @@ func init() { &controllers.VersionController{}, ), ), + web.NSNamespace("/im", + web.NSInclude( + &controllers.IMController{}, + ), + ), ) web.AddNamespace(ns) @@ -193,6 +197,8 @@ func init() { web.AutoRouter(&controllers.TiktokController{}) // 订单 web.AutoRouter(&controllers.TiktokShopController{}) // 门店授权 web.AutoRouter(&controllers.LogisticsController{}) // 抖音快递信息同步 + //web.AutoRouter(&controllers.IMController{}) //im + // 如下都是用于检测存活的空接口 web.Any("/", func(ctx *beecontext.Context) { ctx.WriteString("pong\n")