711 lines
21 KiB
Go
711 lines
21 KiB
Go
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
|
||
}
|