@@ -0,0 +1,710 @@
|
||||
package srv_im
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/service/adser"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/crypt"
|
||||
"91porn-server/common/enum/imad"
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/imclient"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/common/redis"
|
||||
"91porn-server/models/v/imusermod"
|
||||
"91porn-server/skd/skdg"
|
||||
"91porn-server/web/webg"
|
||||
)
|
||||
|
||||
// srvImRedis 自适应当前进程可用的 Redis 客户端:
|
||||
// skd cron 调用 SendAdNotify 时 webg.Redis 为 nil,必须用 skdg.Redis;
|
||||
// web 服务调用时反之。两端写的是同一个 Redis 实例。
|
||||
func srvImRedis() *redis.Client {
|
||||
if skdg.Redis != nil {
|
||||
return skdg.Redis
|
||||
}
|
||||
if webg.Redis != nil {
|
||||
return webg.Redis
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// srvImAdCenterCfg 同样的自适应思路:从当前进程的 Conf 里读 AdCenter 配置。
|
||||
type srvImAdCenter struct {
|
||||
ApiDomain string
|
||||
MerchantCode string
|
||||
AppCode string
|
||||
AesKey string
|
||||
}
|
||||
|
||||
func srvImAdCenterCfg() srvImAdCenter {
|
||||
if skdg.Conf != nil {
|
||||
c := skdg.Conf.AdCenter
|
||||
if c.ApiDomain != "" || c.MerchantCode != "" || c.AppCode != "" || c.AesKey != "" {
|
||||
return srvImAdCenter{ApiDomain: c.ApiDomain, MerchantCode: c.MerchantCode, AppCode: c.AppCode, AesKey: c.AesKey}
|
||||
}
|
||||
}
|
||||
if webg.Conf != nil {
|
||||
c := webg.Conf.AdCenter
|
||||
return srvImAdCenter{ApiDomain: c.ApiDomain, MerchantCode: c.MerchantCode, AppCode: c.AppCode, AesKey: c.AesKey}
|
||||
}
|
||||
return srvImAdCenter{}
|
||||
}
|
||||
|
||||
const (
|
||||
adNotifyType = "AD_NOTIFY"
|
||||
adNotifyTargetSpecified = "specified"
|
||||
adNotifyTargetOnline = "online"
|
||||
adNotifyOnlineScanLimit = 1000
|
||||
adNotifyPassthroughBatch = 100
|
||||
adNotifyVisibleCacheTTL = 24 * time.Hour
|
||||
maxAdNotifyAdsPerSlot = 10
|
||||
)
|
||||
|
||||
type AdNotifyReq struct {
|
||||
UIDs []uint64 `json:"uids"`
|
||||
UserIDs []uint64 `json:"userIds"`
|
||||
Position string `json:"position"`
|
||||
Positions []string `json:"positions"`
|
||||
Target string `json:"target"`
|
||||
TraceID string `json:"traceId"`
|
||||
MaxUsers int `json:"maxUsers"`
|
||||
ChannelType string `json:"channelType"`
|
||||
}
|
||||
|
||||
type AdNotifyResp struct {
|
||||
TraceID string `json:"traceId"`
|
||||
AdCount int `json:"adCount"`
|
||||
Positions []string `json:"positions"`
|
||||
Target string `json:"target"`
|
||||
CandidateUserCount int `json:"candidateUserCount"`
|
||||
TargetUserCount int `json:"targetUserCount"`
|
||||
OnlineUserCount int `json:"onlineUserCount"`
|
||||
OnlineStatusCheckedCount int `json:"onlineStatusCheckedCount"`
|
||||
OnlineStatusReturnedCount int `json:"onlineStatusReturnedCount"`
|
||||
OnlineStatusMissingCount int `json:"onlineStatusMissingCount"`
|
||||
OnlineStatusInvisibleCount int `json:"onlineStatusInvisibleCount"`
|
||||
MessageID string `json:"messageId"`
|
||||
MessageIDs []string `json:"messageIds"`
|
||||
MessageType int `json:"messageType"`
|
||||
ReceiverCount int `json:"receiverCount"`
|
||||
ImCreatedAt int64 `json:"imCreatedAt"`
|
||||
Sent bool `json:"sent"`
|
||||
}
|
||||
|
||||
type adNotifyTarget struct {
|
||||
UID uint64
|
||||
IMUserID int64
|
||||
}
|
||||
|
||||
type adNotifyPassthroughSummary struct {
|
||||
MessageID string
|
||||
MessageIDs []string
|
||||
MessageType int
|
||||
ReceiverCount int
|
||||
CreatedAt int64
|
||||
}
|
||||
|
||||
type adNotifyOnlineStatusSummary struct {
|
||||
Targets []adNotifyTarget
|
||||
CheckedCount int
|
||||
ReturnedCount int
|
||||
MissingCount int
|
||||
InvisibleCount int
|
||||
}
|
||||
|
||||
func SendAdNotify(req AdNotifyReq) (AdNotifyResp, stderr.Code, string) {
|
||||
now := time.Now()
|
||||
positionsReq := normalizeAdNotifyPositions(req)
|
||||
for _, position := range positionsReq {
|
||||
if !imad.IsPositionCode(position) {
|
||||
return AdNotifyResp{}, stderr.ErrParamError, "positions 必须为 IM 广告位标识"
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(req.TraceID) == "" {
|
||||
req.TraceID = fmt.Sprintf("%s_%d", adNotifyType, now.UnixNano())
|
||||
}
|
||||
targetMode := normalizeAdNotifyTarget(req.Target)
|
||||
if targetMode == "" {
|
||||
return AdNotifyResp{}, stderr.ErrParamError, "target 仅支持 online 或 specified"
|
||||
}
|
||||
|
||||
positions, adCount := resolveAdNotifyPositions(positionsReq)
|
||||
resp := AdNotifyResp{
|
||||
TraceID: req.TraceID,
|
||||
AdCount: adCount,
|
||||
Positions: positions,
|
||||
Target: targetMode,
|
||||
}
|
||||
if len(positions) == 0 {
|
||||
return resp, stderr.Success, ""
|
||||
}
|
||||
|
||||
payload, err := buildAdNotifyPayload(req, positions, now)
|
||||
if err != nil {
|
||||
return resp, stderr.ErrParamError, err.Error()
|
||||
}
|
||||
extInfo, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return resp, stderr.ErrParamError, err.Error()
|
||||
}
|
||||
|
||||
// online 模式:调 SendAppPassthrough 一次性广播给本商户/租户下所有在线用户,
|
||||
// 由 IM 平台自己 fan-out,省掉扫表 + 查在线 + 设可见 + 分批的流程
|
||||
if targetMode == adNotifyTargetOnline {
|
||||
return sendAdNotifyBroadcast(resp, req, string(extInfo))
|
||||
}
|
||||
|
||||
// specified 模式:仍按 uid 拉取目标、查在线状态、分批走 SendOnlinePassthrough
|
||||
targets, code, msg := collectAdNotifyTargets(targetMode, req)
|
||||
if code != stderr.Success {
|
||||
return resp, code, msg
|
||||
}
|
||||
resp.CandidateUserCount = len(targets)
|
||||
resp.TargetUserCount = len(targets)
|
||||
if len(targets) == 0 {
|
||||
return resp, stderr.Success, ""
|
||||
}
|
||||
|
||||
ensureAdNotifyOnlineStatusVisible(targets)
|
||||
onlineSummary, code, msg := filterOnlineAdNotifyTargets(targets)
|
||||
if code != stderr.Success {
|
||||
return resp, code, msg
|
||||
}
|
||||
onlineTargets := onlineSummary.Targets
|
||||
resp.OnlineUserCount = len(onlineTargets)
|
||||
resp.OnlineStatusCheckedCount = onlineSummary.CheckedCount
|
||||
resp.OnlineStatusReturnedCount = onlineSummary.ReturnedCount
|
||||
resp.OnlineStatusMissingCount = onlineSummary.MissingCount
|
||||
resp.OnlineStatusInvisibleCount = onlineSummary.InvisibleCount
|
||||
if len(onlineTargets) == 0 {
|
||||
return resp, stderr.Success, ""
|
||||
}
|
||||
|
||||
result, code, msg := sendAdNotifyPassthrough(onlineTargets, imclient.OnlinePassthroughRequest{
|
||||
PassthroughType: adNotifyType,
|
||||
Content: string(extInfo),
|
||||
ExtInfo: string(extInfo),
|
||||
ChannelType: req.ChannelType,
|
||||
})
|
||||
if code != stderr.Success {
|
||||
return resp, code, msg
|
||||
}
|
||||
resp.MessageID = result.MessageID
|
||||
resp.MessageIDs = result.MessageIDs
|
||||
resp.MessageType = result.MessageType
|
||||
resp.ReceiverCount = result.ReceiverCount
|
||||
resp.ImCreatedAt = result.CreatedAt
|
||||
resp.Sent = true
|
||||
return resp, stderr.Success, ""
|
||||
}
|
||||
|
||||
// logAdNotifyBroadcastCurl 把广播请求拼成等价 curl 命令打日志(INFO 级别)。
|
||||
// 仅包含 IM SDK 显式 header(X-Merchant-Code / X-App-Key / X-Client-Id / token),
|
||||
// 签名相关 header 由 imclient 内部按请求计算,curl 复现时需要去掉签名校验或对接时再加。
|
||||
func logAdNotifyBroadcastCurl(traceID, token string, body imclient.AppPassthroughRequest) {
|
||||
cfg := srvImV2Cfg()
|
||||
base := strings.TrimRight(cfg.BaseURL, "/")
|
||||
u := fmt.Sprintf("%s/api/endpoint/%s/%s/message/passthrough/send/batch/all",
|
||||
base, url.PathEscape(cfg.MerchantCode), url.PathEscape(cfg.TenantCode))
|
||||
raw, _ := json.Marshal(body)
|
||||
curl := fmt.Sprintf(`curl -X POST '%s' `+
|
||||
`-H 'Content-Type: application/json' `+
|
||||
`-H 'Accept: application/json' `+
|
||||
`-H 'X-Merchant-Code: %s' `+
|
||||
`-H 'X-App-Key: %s' `+
|
||||
`-H 'X-Client-Id: %s' `+
|
||||
`-H 'token: %s' `+
|
||||
`-d '%s'`,
|
||||
u, cfg.MerchantCode, cfg.AppKey, cfg.ClientID, token, string(raw))
|
||||
log.Info("SendAdNotify broadcast curl",
|
||||
log.Any("traceId", traceID), log.Any("curl", curl))
|
||||
}
|
||||
|
||||
// sendAdNotifyBroadcast 走 IM 平台的"全在线广播"接口
|
||||
// POST /api/endpoint/{merchantCode}/{tenantCode}/message/passthrough/send/batch/all
|
||||
// 一次调用就把消息推给本商户/租户下所有在线用户,无需先查用户列表
|
||||
func sendAdNotifyBroadcast(resp AdNotifyResp, req AdNotifyReq, extInfo string) (AdNotifyResp, stderr.Code, string) {
|
||||
c := newWebSDKClient()
|
||||
body := imclient.AppPassthroughRequest{
|
||||
PassthroughType: adNotifyType,
|
||||
Content: extInfo,
|
||||
ExtInfo: extInfo,
|
||||
ChannelType: req.ChannelType,
|
||||
}
|
||||
var result *imclient.PassthroughResult
|
||||
err := withAppToken(c, func(token string) error {
|
||||
// 打印等价 curl,方便联调
|
||||
logAdNotifyBroadcastCurl(req.TraceID, token, body)
|
||||
var sendErr error
|
||||
result, sendErr = c.SendAppPassthrough(body, token)
|
||||
return sendErr
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("SendAdNotify broadcast failed", log.Any("traceId", req.TraceID), log.E(err))
|
||||
return resp, stderr.Failure, err.Error()
|
||||
}
|
||||
if result != nil {
|
||||
resp.MessageID = result.MessageID
|
||||
resp.MessageType = result.MessageType
|
||||
resp.ReceiverCount = result.ReceiverCount
|
||||
resp.OnlineUserCount = result.ReceiverCount // 平台返回的就是实际推送到的在线数
|
||||
resp.ImCreatedAt = result.CreatedAt
|
||||
}
|
||||
resp.Sent = true
|
||||
return resp, stderr.Success, ""
|
||||
}
|
||||
|
||||
func normalizeAdNotifyTarget(target string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(target)) {
|
||||
case "":
|
||||
return adNotifyTargetSpecified
|
||||
case adNotifyTargetSpecified:
|
||||
return adNotifyTargetSpecified
|
||||
case adNotifyTargetOnline:
|
||||
return adNotifyTargetOnline
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func collectAdNotifyTargets(targetMode string, req AdNotifyReq) ([]adNotifyTarget, stderr.Code, string) {
|
||||
switch targetMode {
|
||||
case adNotifyTargetOnline:
|
||||
return scanAdNotifyIMUsers(req.MaxUsers)
|
||||
case adNotifyTargetSpecified:
|
||||
uids := normalizeUIDs(append(req.UIDs, req.UserIDs...))
|
||||
if len(uids) == 0 {
|
||||
return nil, stderr.ErrParamError, "userIds不能为空;全量在线推送请传 target=online"
|
||||
}
|
||||
return resolveAdNotifyTargetsByUIDs(uids)
|
||||
default:
|
||||
return nil, stderr.ErrParamError, "target 仅支持 online 或 specified"
|
||||
}
|
||||
}
|
||||
|
||||
func resolveAdNotifyTargetsByUIDs(uids []uint64) ([]adNotifyTarget, stderr.Code, string) {
|
||||
// uid → imUserId 走 imusermod(唯一真源)
|
||||
byUID, err := imusermod.IMUserIDMapByUIDs(uids)
|
||||
if err != nil {
|
||||
return nil, stderr.Failure, err.Error()
|
||||
}
|
||||
targets := make([]adNotifyTarget, 0, len(byUID))
|
||||
for _, uid := range uids {
|
||||
imUserID, ok := byUID[uid]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
targets = append(targets, adNotifyTarget{UID: uid, IMUserID: imUserID})
|
||||
}
|
||||
return targets, stderr.Success, ""
|
||||
}
|
||||
|
||||
func scanAdNotifyIMUsers(maxUsers int) ([]adNotifyTarget, stderr.Code, string) {
|
||||
targets := make([]adNotifyTarget, 0)
|
||||
// 直接扫 im_user 表(只含已注册 IM 的用户),按 uid 游标分页。
|
||||
// 表本身就小,且天然只有"有 imUserId 的用户",无需过滤 imUserId>0。
|
||||
var lastUID uint64
|
||||
for {
|
||||
limit := int64(adNotifyOnlineScanLimit)
|
||||
if maxUsers > 0 {
|
||||
remaining := int64(maxUsers - len(targets))
|
||||
if remaining <= 0 {
|
||||
break
|
||||
}
|
||||
if remaining < limit {
|
||||
limit = remaining
|
||||
}
|
||||
}
|
||||
list, err := imusermod.ListAfterUID(lastUID, limit)
|
||||
if err != nil {
|
||||
return nil, stderr.Failure, err.Error()
|
||||
}
|
||||
if len(list) == 0 {
|
||||
break
|
||||
}
|
||||
for i := range list {
|
||||
u := list[i]
|
||||
if u.UID == 0 || u.IMUserID <= 0 {
|
||||
continue
|
||||
}
|
||||
targets = append(targets, adNotifyTarget{UID: u.UID, IMUserID: u.IMUserID})
|
||||
}
|
||||
// 本批末条 uid 即下一页游标(按 uid 升序)
|
||||
lastUID = list[len(list)-1].UID
|
||||
// 本批不满 limit,说明已扫到末尾
|
||||
if int64(len(list)) < limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
return targets, stderr.Success, ""
|
||||
}
|
||||
|
||||
func resolveAdNotifyPositions(requested []string) ([]string, int) {
|
||||
if len(requested) > 0 {
|
||||
return requested, 0
|
||||
}
|
||||
slots, err := webAdSlots()
|
||||
if err != nil {
|
||||
return nil, 0
|
||||
}
|
||||
positions := listAdPositionsWithAds(slots)
|
||||
adCount := 0
|
||||
for _, position := range positions {
|
||||
ads := selectAdDetailsByPosition(slots, position)
|
||||
adCount += len(ads)
|
||||
}
|
||||
return positions, adCount
|
||||
}
|
||||
|
||||
func normalizeAdNotifyPositions(req AdNotifyReq) []string {
|
||||
positions := make([]string, 0, len(req.Positions)+1)
|
||||
if position := strings.TrimSpace(req.Position); position != "" {
|
||||
positions = append(positions, position)
|
||||
}
|
||||
positions = append(positions, req.Positions...)
|
||||
seen := make(map[string]struct{}, len(positions))
|
||||
normalized := make([]string, 0, len(positions))
|
||||
for _, position := range positions {
|
||||
position = strings.TrimSpace(position)
|
||||
if position == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[position]; ok {
|
||||
continue
|
||||
}
|
||||
seen[position] = struct{}{}
|
||||
normalized = append(normalized, position)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func buildAdNotifyPayload(req AdNotifyReq, positions []string, now time.Time) (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"type": adNotifyType,
|
||||
"positions": positions,
|
||||
"traceId": req.TraceID,
|
||||
"createdAt": now.UnixMilli(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ensureAdNotifyOnlineStatusVisible(targets []adNotifyTarget) {
|
||||
if len(targets) == 0 {
|
||||
return
|
||||
}
|
||||
c := newWebSDKClient()
|
||||
seen := make(map[int64]struct{}, len(targets))
|
||||
successCount := 0
|
||||
for _, target := range targets {
|
||||
if target.IMUserID <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[target.IMUserID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[target.IMUserID] = struct{}{}
|
||||
if !shouldEnsureAdNotifyOnlineStatusVisible(target.IMUserID) {
|
||||
continue
|
||||
}
|
||||
if err := withAppToken(c, func(token string) error {
|
||||
return c.SetOnlineStatus(imclient.SetOnlineStatusRequest{
|
||||
UserID: target.IMUserID,
|
||||
ShowOnlineStatus: true,
|
||||
}, token)
|
||||
}); err != nil {
|
||||
log.Warn("SendAdNotify SetOnlineStatusVisible failed",
|
||||
log.Any("uid", target.UID), log.Any("imUserId", target.IMUserID), log.E(err))
|
||||
continue
|
||||
}
|
||||
markAdNotifyOnlineStatusVisible(target.IMUserID)
|
||||
successCount++
|
||||
}
|
||||
log.Info("SendAdNotify SetOnlineStatusVisible summary",
|
||||
log.Any("candidateCount", len(seen)), log.Any("successCount", successCount))
|
||||
}
|
||||
|
||||
func shouldEnsureAdNotifyOnlineStatusVisible(imUserID int64) bool {
|
||||
r := srvImRedis()
|
||||
if r == nil {
|
||||
return true
|
||||
}
|
||||
cached, err := r.Get(adNotifyVisibleCacheKey(imUserID))
|
||||
if err != nil {
|
||||
log.Warn("SendAdNotify visible cache get failed", log.Any("imUserId", imUserID), log.E(err))
|
||||
return true
|
||||
}
|
||||
return cached == nil
|
||||
}
|
||||
|
||||
func markAdNotifyOnlineStatusVisible(imUserID int64) {
|
||||
r := srvImRedis()
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
if err := r.Set(adNotifyVisibleCacheKey(imUserID), 1, adNotifyVisibleCacheTTL); err != nil {
|
||||
log.Warn("SendAdNotify visible cache set failed", log.Any("imUserId", imUserID), log.E(err))
|
||||
}
|
||||
}
|
||||
|
||||
func adNotifyVisibleCacheKey(imUserID int64) string {
|
||||
return fmt.Sprintf("im:online_status_visible:%d", imUserID)
|
||||
}
|
||||
|
||||
func filterOnlineAdNotifyTargets(targets []adNotifyTarget) (adNotifyOnlineStatusSummary, stderr.Code, string) {
|
||||
imUserIDs := make([]int64, 0, len(targets))
|
||||
seen := make(map[int64]struct{}, len(targets))
|
||||
for _, target := range targets {
|
||||
if target.IMUserID <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[target.IMUserID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[target.IMUserID] = struct{}{}
|
||||
imUserIDs = append(imUserIDs, target.IMUserID)
|
||||
}
|
||||
summary := adNotifyOnlineStatusSummary{CheckedCount: len(imUserIDs)}
|
||||
statusByIMUserID := make(map[int64]bool, len(imUserIDs))
|
||||
invisibleCount := 0
|
||||
c := newWebSDKClient()
|
||||
for _, batch := range chunkInt64s(imUserIDs, maxIMOnlineStatusBatchSize) {
|
||||
var statuses []imclient.OnlineStatus
|
||||
err := withAppToken(c, func(token string) error {
|
||||
var statusErr error
|
||||
statuses, statusErr = c.BatchOnlineStatus(imclient.BatchOnlineStatusRequest{UserIDs: batch}, token)
|
||||
return statusErr
|
||||
})
|
||||
if err != nil {
|
||||
return summary, stderr.Failure, err.Error()
|
||||
}
|
||||
for _, item := range statuses {
|
||||
if item.UserID <= 0 {
|
||||
continue
|
||||
}
|
||||
statusByIMUserID[item.UserID] = item.Online
|
||||
if !item.Visible {
|
||||
invisibleCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
summary.ReturnedCount = len(statusByIMUserID)
|
||||
summary.MissingCount = summary.CheckedCount - summary.ReturnedCount
|
||||
if summary.MissingCount < 0 {
|
||||
summary.MissingCount = 0
|
||||
}
|
||||
summary.InvisibleCount = invisibleCount
|
||||
summary.Targets = onlineAdNotifyTargets(targets, statusByIMUserID)
|
||||
log.Info("SendAdNotify online status summary",
|
||||
log.Any("checkedCount", summary.CheckedCount),
|
||||
log.Any("returnedCount", summary.ReturnedCount),
|
||||
log.Any("missingCount", summary.MissingCount),
|
||||
log.Any("onlineCount", len(summary.Targets)),
|
||||
log.Any("invisibleCount", summary.InvisibleCount),
|
||||
log.Any("sampleIMUserIds", sampleInt64s(imUserIDs, 10)),
|
||||
)
|
||||
return summary, stderr.Success, ""
|
||||
}
|
||||
|
||||
func sampleInt64s(values []int64, limit int) []int64 {
|
||||
if limit <= 0 || len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(values) < limit {
|
||||
limit = len(values)
|
||||
}
|
||||
return append([]int64(nil), values[:limit]...)
|
||||
}
|
||||
|
||||
func onlineAdNotifyTargets(targets []adNotifyTarget, statusByIMUserID map[int64]bool) []adNotifyTarget {
|
||||
online := make([]adNotifyTarget, 0, len(targets))
|
||||
seen := make(map[int64]struct{}, len(targets))
|
||||
for _, target := range targets {
|
||||
if target.IMUserID <= 0 || !statusByIMUserID[target.IMUserID] {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[target.IMUserID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[target.IMUserID] = struct{}{}
|
||||
online = append(online, target)
|
||||
}
|
||||
return online
|
||||
}
|
||||
|
||||
func sendAdNotifyPassthrough(targets []adNotifyTarget, req imclient.OnlinePassthroughRequest) (adNotifyPassthroughSummary, stderr.Code, string) {
|
||||
var summary adNotifyPassthroughSummary
|
||||
c := newWebSDKClient()
|
||||
for _, batch := range chunkAdNotifyTargets(targets, adNotifyPassthroughBatch) {
|
||||
receiverIDs := make([]int64, 0, len(batch))
|
||||
for _, target := range batch {
|
||||
receiverIDs = append(receiverIDs, target.IMUserID)
|
||||
}
|
||||
req.ReceiverIDSet = receiverIDs
|
||||
var result *imclient.PassthroughResult
|
||||
err := withAppToken(c, func(token string) error {
|
||||
var sendErr error
|
||||
result, sendErr = c.SendOnlinePassthrough(req, token)
|
||||
return sendErr
|
||||
})
|
||||
if err != nil {
|
||||
return summary, stderr.Failure, err.Error()
|
||||
}
|
||||
if result == nil {
|
||||
continue
|
||||
}
|
||||
if summary.MessageID == "" {
|
||||
summary.MessageID = result.MessageID
|
||||
}
|
||||
summary.MessageType = result.MessageType
|
||||
summary.ReceiverCount += result.ReceiverCount
|
||||
summary.CreatedAt = result.CreatedAt
|
||||
summary.MessageIDs = append(summary.MessageIDs, result.MessageID)
|
||||
}
|
||||
return summary, stderr.Success, ""
|
||||
}
|
||||
|
||||
func chunkAdNotifyTargets(targets []adNotifyTarget, size int) [][]adNotifyTarget {
|
||||
if size <= 0 {
|
||||
size = adNotifyPassthroughBatch
|
||||
}
|
||||
chunks := make([][]adNotifyTarget, 0, (len(targets)+size-1)/size)
|
||||
for start := 0; start < len(targets); start += size {
|
||||
end := start + size
|
||||
if end > len(targets) {
|
||||
end = len(targets)
|
||||
}
|
||||
chunks = append(chunks, targets[start:end])
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
type webAdCenterResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
func webAdSlots() ([]adser.AdSlot, error) {
|
||||
cfg := srvImAdCenterCfg()
|
||||
if cfg.ApiDomain == "" || cfg.MerchantCode == "" || cfg.AppCode == "" || cfg.AesKey == "" {
|
||||
return nil, errors.New("adCenter 未配置")
|
||||
}
|
||||
foreverCacheKey := fmt.Sprintf("jtAdForever-%s-%s", cfg.MerchantCode, cfg.AppCode)
|
||||
redisKey := fmt.Sprintf("jtAd-%s-%s", cfg.MerchantCode, cfg.AppCode)
|
||||
r := srvImRedis()
|
||||
if r != nil {
|
||||
str, err := r.Get(redisKey)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("IM AdNotify 获取广告缓存异常:%v", err))
|
||||
}
|
||||
if str != nil {
|
||||
var cached []adser.AdSlot
|
||||
if err = json.Unmarshal([]byte(*str), &cached); err == nil {
|
||||
return cached, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req := adser.JtAdvertiseReq{
|
||||
MerchantCode: cfg.MerchantCode,
|
||||
AppCode: cfg.AppCode,
|
||||
AdStatus: 1,
|
||||
}
|
||||
var serverResp *webAdCenterResp
|
||||
body, _ := json.Marshal(req)
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&serverResp, cfg.ApiDomain+"/openapi/getAdvertiseList", nil, body)
|
||||
if err != nil {
|
||||
return webAdSlotsFromLastTime(foreverCacheKey, err)
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
return webAdSlotsFromLastTime(foreverCacheKey, fmt.Errorf("response status %d", code))
|
||||
}
|
||||
if serverResp == nil || serverResp.Code != 0 {
|
||||
msg := ""
|
||||
if serverResp != nil {
|
||||
msg = serverResp.Msg
|
||||
}
|
||||
return webAdSlotsFromLastTime(foreverCacheKey, fmt.Errorf("response code error: %s", msg))
|
||||
}
|
||||
adsData, err := crypt.AdDecrypt(serverResp.Data, cfg.AesKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var slots []adser.AdSlot
|
||||
if err = json.Unmarshal([]byte(adsData), &slots); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range slots {
|
||||
sort.Slice(slots[i].AdDetailInfoList, func(j, k int) bool {
|
||||
return slots[i].AdDetailInfoList[j].Sort < slots[i].AdDetailInfoList[k].Sort
|
||||
})
|
||||
}
|
||||
if r := srvImRedis(); r != nil {
|
||||
common.Go(func() {
|
||||
data, err := json.Marshal(&slots)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = r.Set(foreverCacheKey, data, time.Hour*72)
|
||||
_ = r.Set(redisKey, data, time.Minute)
|
||||
})
|
||||
}
|
||||
return slots, nil
|
||||
}
|
||||
|
||||
func webAdSlotsFromLastTime(cacheKey string, fallback error) ([]adser.AdSlot, error) {
|
||||
r := srvImRedis()
|
||||
if r == nil {
|
||||
return nil, fallback
|
||||
}
|
||||
str, err := r.Get(cacheKey)
|
||||
if err != nil || str == nil {
|
||||
return nil, fallback
|
||||
}
|
||||
var slots []adser.AdSlot
|
||||
if err = json.Unmarshal([]byte(*str), &slots); err != nil {
|
||||
return nil, fallback
|
||||
}
|
||||
return slots, nil
|
||||
}
|
||||
|
||||
func listAdPositionsWithAds(slots []adser.AdSlot) []string {
|
||||
positions := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
for _, slot := range slots {
|
||||
code := strings.TrimSpace(slot.AdvertiseLocationCode)
|
||||
if !imad.IsPositionCode(code) || len(slot.AdDetailInfoList) == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[code]; ok {
|
||||
continue
|
||||
}
|
||||
seen[code] = struct{}{}
|
||||
positions = append(positions, code)
|
||||
}
|
||||
sort.Strings(positions)
|
||||
return positions
|
||||
}
|
||||
|
||||
func selectAdDetailsByPosition(slots []adser.AdSlot, position string) []adser.AdDetailInfo {
|
||||
position = strings.TrimSpace(position)
|
||||
matches := make([]adser.AdDetailInfo, 0)
|
||||
for _, slot := range slots {
|
||||
if strings.TrimSpace(slot.AdvertiseLocationCode) != position {
|
||||
continue
|
||||
}
|
||||
matches = append(matches, slot.AdDetailInfoList...)
|
||||
}
|
||||
if len(matches) > maxAdNotifyAdsPerSlot {
|
||||
matches = matches[:maxAdNotifyAdsPerSlot]
|
||||
}
|
||||
return matches
|
||||
}
|
||||
Reference in New Issue
Block a user