@@ -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
|
||||
}
|
||||
@@ -0,0 +1,785 @@
|
||||
package srv_im
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/imclient"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/v/imusermod"
|
||||
"91porn-server/models/v/sessionmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/skd/skdg"
|
||||
"91porn-server/web/webg"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
maxIMOnlineStatusBatchSize = 100
|
||||
defaultIMTokenTTL = int64(86400)
|
||||
imUserSyncPageSize = int64(500)
|
||||
)
|
||||
|
||||
var webAppTokenCache = struct {
|
||||
sync.Mutex
|
||||
token string
|
||||
expiresAt time.Time
|
||||
}{}
|
||||
|
||||
type UserListReq struct {
|
||||
UID *uint64 `json:"uid"`
|
||||
HasIM *bool `json:"hasIm"`
|
||||
IsUp *bool `json:"isUp"`
|
||||
PageNum int64 `json:"pageNum"`
|
||||
PageSize int64 `json:"pageSize"`
|
||||
}
|
||||
|
||||
type UserInfo struct {
|
||||
UID uint64 `json:"uid"`
|
||||
ImUserID int64 `json:"imUserId"`
|
||||
NickName string `json:"nickName"`
|
||||
Avatar string `json:"avatar"`
|
||||
IsUp bool `json:"isUp"`
|
||||
}
|
||||
|
||||
type UserListResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List []UserInfo `json:"list"`
|
||||
}
|
||||
|
||||
type SyncUsersReq struct {
|
||||
UIDs []uint64 `json:"uids" binding:"required"`
|
||||
}
|
||||
|
||||
type SyncUsersResp struct {
|
||||
Total int `json:"total"`
|
||||
Success int `json:"success"`
|
||||
Skipped int `json:"skipped"`
|
||||
Failed int `json:"failed"`
|
||||
}
|
||||
|
||||
type FriendAddDirectReq struct {
|
||||
UID uint64 `json:"uid" binding:"required"`
|
||||
PeerUID uint64 `json:"peerUid" binding:"required"`
|
||||
}
|
||||
|
||||
type OnlineStatusReq struct {
|
||||
UIDs []uint64 `json:"uids" binding:"required"`
|
||||
}
|
||||
|
||||
type OnlineStatusInfo struct {
|
||||
UID uint64 `json:"uid"`
|
||||
ImUserID int64 `json:"imUserId"`
|
||||
Online bool `json:"online"`
|
||||
}
|
||||
|
||||
type PassthroughReq struct {
|
||||
UIDs []uint64 `json:"uids"`
|
||||
UserIDs []uint64 `json:"userIds"`
|
||||
PassthroughType string `json:"passthroughType"`
|
||||
EventType string `json:"eventType"`
|
||||
Content string `json:"content"`
|
||||
ExtInfo string `json:"extInfo"`
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
ChannelType string `json:"channelType"`
|
||||
SenderUID uint64 `json:"senderUid"`
|
||||
AllApp bool `json:"allApp"`
|
||||
OnlineOnly bool `json:"onlineOnly"`
|
||||
}
|
||||
|
||||
type PassthroughResp struct {
|
||||
TargetCount int `json:"targetCount"`
|
||||
Result *imclient.PassthroughResult `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
type SendMessageReq struct {
|
||||
UID uint64 `json:"uid" binding:"required"`
|
||||
PeerUID uint64 `json:"peerUid" binding:"required"`
|
||||
Content string `json:"content" binding:"required"`
|
||||
}
|
||||
|
||||
type SendMessageResp struct {
|
||||
UID uint64 `json:"uid"`
|
||||
ImUserID int64 `json:"imUserId"`
|
||||
PeerUID uint64 `json:"peerUid"`
|
||||
PeerImUserID int64 `json:"peerImUserId"`
|
||||
Content string `json:"content"`
|
||||
MessageType int `json:"messageType"`
|
||||
}
|
||||
|
||||
type SyncDialogUsersStats struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Total int `json:"total"`
|
||||
Skipped int `json:"skipped"`
|
||||
Success int `json:"success"`
|
||||
Failed int `json:"failed"`
|
||||
FriendSuccess int `json:"friendSuccess"`
|
||||
FriendFailed int `json:"friendFailed"`
|
||||
}
|
||||
|
||||
type imFriendPair struct {
|
||||
UID uint64
|
||||
PeerUID uint64
|
||||
}
|
||||
|
||||
func ListUsers(req UserListReq) (UserListResp, stderr.Code, string) {
|
||||
if req.PageNum <= 0 {
|
||||
req.PageNum = 1
|
||||
}
|
||||
if req.PageSize <= 0 || req.PageSize > 100 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
filter := buildUserListFilter(req)
|
||||
skip := (req.PageNum - 1) * req.PageSize
|
||||
opts := options.Find().
|
||||
SetSort(bson.M{"uid": -1}).
|
||||
SetSkip(skip).
|
||||
SetLimit(req.PageSize)
|
||||
var total int64
|
||||
users, _, err := usermod.FetchList(filter, opts, &total)
|
||||
if err != nil {
|
||||
return UserListResp{}, stderr.Failure, err.Error()
|
||||
}
|
||||
resp := UserListResp{Total: total, List: make([]UserInfo, 0, len(users))}
|
||||
for _, user := range users {
|
||||
resp.List = append(resp.List, userInfoFromUser(user))
|
||||
}
|
||||
return resp, stderr.Success, ""
|
||||
}
|
||||
|
||||
func SyncUsers(req SyncUsersReq) (SyncUsersResp, stderr.Code, string) {
|
||||
uids := normalizeUIDs(req.UIDs)
|
||||
resp := SyncUsersResp{Total: len(uids)}
|
||||
if len(uids) == 0 {
|
||||
return resp, stderr.ErrParamError, "uids不能为空"
|
||||
}
|
||||
c := newWebSDKClient()
|
||||
if !c.Enabled() {
|
||||
return resp, stderr.Failure, "IM 未配置"
|
||||
}
|
||||
for _, uid := range uids {
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil || user == nil || user.UID == 0 {
|
||||
resp.Failed++
|
||||
continue
|
||||
}
|
||||
if imusermod.IMUserIDByUID(user.UID) > 0 {
|
||||
resp.Skipped++
|
||||
continue
|
||||
}
|
||||
if _, err = ensureUserRegistered(c, user); err != nil {
|
||||
resp.Failed++
|
||||
continue
|
||||
}
|
||||
resp.Success++
|
||||
}
|
||||
return resp, stderr.Success, ""
|
||||
}
|
||||
|
||||
func AddFriendDirect(req FriendAddDirectReq) (stderr.Code, string) {
|
||||
if req.UID == 0 || req.PeerUID == 0 || req.UID == req.PeerUID {
|
||||
return stderr.ErrParamError, "uid/peerUid错误"
|
||||
}
|
||||
c := newWebSDKClient()
|
||||
userIMID, peerIMID, err := ensurePairRegistered(c, req.UID, req.PeerUID)
|
||||
if err != nil {
|
||||
return stderr.Failure, err.Error()
|
||||
}
|
||||
if err = ensureFriendDirect(c, userIMID, peerIMID); err != nil {
|
||||
return stderr.Failure, err.Error()
|
||||
}
|
||||
if err = ensureFriendDirect(c, peerIMID, userIMID); err != nil {
|
||||
return stderr.Failure, err.Error()
|
||||
}
|
||||
return stderr.Success, ""
|
||||
}
|
||||
|
||||
func BatchOnlineStatus(req OnlineStatusReq) ([]OnlineStatusInfo, stderr.Code, string) {
|
||||
uids := normalizeUIDs(req.UIDs)
|
||||
if len(uids) == 0 {
|
||||
return []OnlineStatusInfo{}, stderr.Success, ""
|
||||
}
|
||||
mappings, err := imusermod.FindByUIDsQuiet(uids)
|
||||
if err != nil {
|
||||
return nil, stderr.Failure, err.Error()
|
||||
}
|
||||
imIDs := make([]int64, 0, len(mappings))
|
||||
byIMID := make(map[int64]uint64, len(mappings))
|
||||
seen := make(map[int64]struct{}, len(mappings))
|
||||
for _, m := range mappings {
|
||||
if m.IMUserID <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[m.IMUserID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[m.IMUserID] = struct{}{}
|
||||
imIDs = append(imIDs, m.IMUserID)
|
||||
byIMID[m.IMUserID] = m.UID
|
||||
}
|
||||
if len(imIDs) == 0 {
|
||||
return []OnlineStatusInfo{}, stderr.Success, ""
|
||||
}
|
||||
c := newWebSDKClient()
|
||||
statuses := make([]imclient.OnlineStatus, 0, len(imIDs))
|
||||
for _, batch := range chunkInt64s(imIDs, maxIMOnlineStatusBatchSize) {
|
||||
var items []imclient.OnlineStatus
|
||||
err := withAppToken(c, func(token string) error {
|
||||
var batchErr error
|
||||
items, batchErr = c.BatchOnlineStatus(imclient.BatchOnlineStatusRequest{UserIDs: batch}, token)
|
||||
return batchErr
|
||||
})
|
||||
if err != nil {
|
||||
return nil, stderr.Failure, err.Error()
|
||||
}
|
||||
statuses = append(statuses, items...)
|
||||
}
|
||||
resp := make([]OnlineStatusInfo, 0, len(statuses))
|
||||
for _, item := range statuses {
|
||||
resp = append(resp, OnlineStatusInfo{
|
||||
UID: byIMID[item.UserID],
|
||||
ImUserID: item.UserID,
|
||||
Online: item.Online,
|
||||
})
|
||||
}
|
||||
return resp, stderr.Success, ""
|
||||
}
|
||||
|
||||
func SendPassthrough(req PassthroughReq) (PassthroughResp, stderr.Code, string) {
|
||||
passthroughType := strings.TrimSpace(req.PassthroughType)
|
||||
if passthroughType == "" {
|
||||
passthroughType = strings.TrimSpace(req.EventType)
|
||||
}
|
||||
if passthroughType == "" {
|
||||
return PassthroughResp{}, stderr.ErrParamError, "passthroughType不能为空"
|
||||
}
|
||||
c := newWebSDKClient()
|
||||
content := strings.TrimSpace(req.Content)
|
||||
extInfo := strings.TrimSpace(req.ExtInfo)
|
||||
if req.Payload != nil {
|
||||
payloadBytes, err := json.Marshal(req.Payload)
|
||||
if err != nil {
|
||||
return PassthroughResp{}, stderr.ErrParamError, err.Error()
|
||||
}
|
||||
if content == "" {
|
||||
content = string(payloadBytes)
|
||||
}
|
||||
if extInfo == "" {
|
||||
extInfo = string(payloadBytes)
|
||||
}
|
||||
}
|
||||
if extInfo == "" {
|
||||
extInfo = "{}"
|
||||
}
|
||||
senderID, err := resolveSenderIMID(c, req.SenderUID)
|
||||
if err != nil {
|
||||
return PassthroughResp{}, stderr.Failure, err.Error()
|
||||
}
|
||||
if req.AllApp {
|
||||
var result *imclient.PassthroughResult
|
||||
err := withAppToken(c, func(token string) error {
|
||||
var sendErr error
|
||||
result, sendErr = c.SendAppPassthrough(imclient.AppPassthroughRequest{
|
||||
SenderID: senderID,
|
||||
PassthroughType: passthroughType,
|
||||
Content: content,
|
||||
ExtInfo: extInfo,
|
||||
ChannelType: req.ChannelType,
|
||||
}, token)
|
||||
return sendErr
|
||||
})
|
||||
if err != nil {
|
||||
return PassthroughResp{}, stderr.Failure, err.Error()
|
||||
}
|
||||
return PassthroughResp{Result: result}, stderr.Success, ""
|
||||
}
|
||||
uids := normalizeUIDs(append(req.UIDs, req.UserIDs...))
|
||||
if len(uids) == 0 {
|
||||
return PassthroughResp{}, stderr.ErrParamError, "uids不能为空;全应用透传需显式传 allApp=true"
|
||||
}
|
||||
imIDs, err := loadRegisteredIMIDs(uids)
|
||||
if err != nil {
|
||||
return PassthroughResp{}, stderr.Failure, err.Error()
|
||||
}
|
||||
if len(imIDs) == 0 {
|
||||
return PassthroughResp{}, stderr.ErrParamError, "没有匹配到可发送透传的用户"
|
||||
}
|
||||
var result *imclient.PassthroughResult
|
||||
err = withAppToken(c, func(token string) error {
|
||||
var sendErr error
|
||||
result, sendErr = c.SendOnlinePassthrough(imclient.OnlinePassthroughRequest{
|
||||
SenderID: senderID,
|
||||
ReceiverIDSet: imIDs,
|
||||
PassthroughType: passthroughType,
|
||||
Content: content,
|
||||
ExtInfo: extInfo,
|
||||
ChannelType: req.ChannelType,
|
||||
}, token)
|
||||
return sendErr
|
||||
})
|
||||
if err != nil {
|
||||
return PassthroughResp{}, stderr.Failure, err.Error()
|
||||
}
|
||||
return PassthroughResp{TargetCount: len(imIDs), Result: result}, stderr.Success, ""
|
||||
}
|
||||
|
||||
func SendMessage(req SendMessageReq) (SendMessageResp, stderr.Code, string) {
|
||||
content := strings.TrimSpace(req.Content)
|
||||
if content == "" {
|
||||
return SendMessageResp{}, stderr.ErrParamError, "content不能为空"
|
||||
}
|
||||
if req.UID == 0 || req.PeerUID == 0 || req.UID == req.PeerUID {
|
||||
return SendMessageResp{}, stderr.ErrParamError, "uid/peerUid错误"
|
||||
}
|
||||
c := newWebSDKClient()
|
||||
userIMID, peerIMID, err := ensurePairRegistered(c, req.UID, req.PeerUID)
|
||||
if err != nil {
|
||||
return SendMessageResp{}, stderr.Failure, err.Error()
|
||||
}
|
||||
if err = ensureFriendDirect(c, userIMID, peerIMID); err != nil {
|
||||
return SendMessageResp{}, stderr.Failure, err.Error()
|
||||
}
|
||||
if err = ensureFriendDirect(c, peerIMID, userIMID); err != nil {
|
||||
return SendMessageResp{}, stderr.Failure, err.Error()
|
||||
}
|
||||
messageType := imclient.MessageTypeText
|
||||
if err = withAppToken(c, func(token string) error {
|
||||
return c.SendMessage(imclient.SendMessageRequest{
|
||||
SenderID: userIMID,
|
||||
ReceiverID: peerIMID,
|
||||
Content: content,
|
||||
MessageType: messageType,
|
||||
}, token)
|
||||
}); err != nil {
|
||||
return SendMessageResp{}, stderr.Failure, err.Error()
|
||||
}
|
||||
return SendMessageResp{
|
||||
UID: req.UID,
|
||||
ImUserID: userIMID,
|
||||
PeerUID: req.PeerUID,
|
||||
PeerImUserID: peerIMID,
|
||||
Content: content,
|
||||
MessageType: messageType,
|
||||
}, stderr.Success, ""
|
||||
}
|
||||
|
||||
func SyncDialogUsers() (SyncDialogUsersStats, error) {
|
||||
c := newWebSDKClient()
|
||||
if !c.Enabled() {
|
||||
return SyncDialogUsersStats{Enabled: false}, nil
|
||||
}
|
||||
syncIDs, pairs, err := CollectDialogUserIDs()
|
||||
if err != nil {
|
||||
return SyncDialogUsersStats{Enabled: true}, err
|
||||
}
|
||||
stats := SyncDialogUsersStats{Enabled: true, Total: len(syncIDs)}
|
||||
imIDs := make(map[uint64]int64, len(syncIDs))
|
||||
for uid := range syncIDs {
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil || user == nil || user.UID == 0 {
|
||||
stats.Failed++
|
||||
continue
|
||||
}
|
||||
if imid := imusermod.IMUserIDByUID(user.UID); imid > 0 {
|
||||
imIDs[user.UID] = imid
|
||||
stats.Skipped++
|
||||
continue
|
||||
}
|
||||
imUserID, err := ensureUserRegistered(c, user)
|
||||
if err != nil {
|
||||
stats.Failed++
|
||||
continue
|
||||
}
|
||||
imIDs[user.UID] = imUserID
|
||||
stats.Success++
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
for _, pair := range pairs {
|
||||
userIMID := imIDs[pair.UID]
|
||||
peerIMID := imIDs[pair.PeerUID]
|
||||
if userIMID <= 0 || peerIMID <= 0 {
|
||||
stats.FriendFailed++
|
||||
continue
|
||||
}
|
||||
if err := ensureFriendDirect(c, userIMID, peerIMID); err != nil {
|
||||
stats.FriendFailed++
|
||||
continue
|
||||
}
|
||||
stats.FriendSuccess++
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func CollectDialogUserIDs() (map[uint64]struct{}, []imFriendPair, error) {
|
||||
syncIDs := make(map[uint64]struct{})
|
||||
pairSet := make(map[string]struct{})
|
||||
pairs := make([]imFriendPair, 0)
|
||||
var lastID primitive.ObjectID
|
||||
for {
|
||||
filter := bson.M{}
|
||||
if !lastID.IsZero() {
|
||||
filter["_id"] = bson.M{"$gt": lastID}
|
||||
}
|
||||
opts := options.Find().
|
||||
SetProjection(bson.M{"_id": 1, "sendUid": 1, "takeUid": 1}).
|
||||
SetSort(bson.M{"_id": 1}).
|
||||
SetLimit(imUserSyncPageSize)
|
||||
sessions, err := sessionmod.FindManyByFilter(filter, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(sessions) == 0 {
|
||||
break
|
||||
}
|
||||
for _, session := range sessions {
|
||||
if session.SendUid > 0 {
|
||||
syncIDs[session.SendUid] = struct{}{}
|
||||
}
|
||||
if session.TakeUid > 0 {
|
||||
syncIDs[session.TakeUid] = struct{}{}
|
||||
}
|
||||
addIMFriendPair(pairSet, &pairs, session.SendUid, session.TakeUid)
|
||||
}
|
||||
lastID = sessions[len(sessions)-1].ID
|
||||
if int64(len(sessions)) < imUserSyncPageSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
return syncIDs, pairs, nil
|
||||
}
|
||||
|
||||
func buildUserListFilter(req UserListReq) bson.M {
|
||||
filter := bson.M{}
|
||||
andFilters := bson.A{}
|
||||
if req.UID != nil && *req.UID > 0 {
|
||||
filter["uid"] = *req.UID
|
||||
}
|
||||
if req.HasIM != nil {
|
||||
if *req.HasIM {
|
||||
filter["imUserId"] = bson.M{"$gt": 0}
|
||||
} else {
|
||||
andFilters = append(andFilters, bson.M{"$or": bson.A{
|
||||
bson.M{"imUserId": bson.M{"$exists": false}},
|
||||
bson.M{"imUserId": bson.M{"$lte": 0}},
|
||||
}})
|
||||
}
|
||||
}
|
||||
if req.IsUp != nil {
|
||||
if *req.IsUp {
|
||||
andFilters = append(andFilters, creatorFilter())
|
||||
} else {
|
||||
andFilters = append(andFilters, bson.M{"$nor": bson.A{creatorFilter()}})
|
||||
}
|
||||
}
|
||||
if len(andFilters) > 0 {
|
||||
filter["$and"] = andFilters
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
func creatorFilter() bson.M {
|
||||
return bson.M{"$or": bson.A{
|
||||
bson.M{"originalUp": true},
|
||||
bson.M{"officialCert": true},
|
||||
bson.M{"superUser": true},
|
||||
bson.M{"merchantUser": bson.M{"$gt": 0}},
|
||||
bson.M{"vidUploadCount": bson.M{"$gt": 0}},
|
||||
bson.M{"coverUploadCount": bson.M{"$gt": 0}},
|
||||
bson.M{"upTag": bson.M{"$ne": ""}},
|
||||
}}
|
||||
}
|
||||
|
||||
func userInfoFromUser(user *usermod.User) UserInfo {
|
||||
if user == nil {
|
||||
return UserInfo{}
|
||||
}
|
||||
return UserInfo{
|
||||
UID: user.UID,
|
||||
ImUserID: imusermod.IMUserIDByUID(user.UID),
|
||||
NickName: user.Name,
|
||||
Avatar: user.Portrait,
|
||||
IsUp: isCreator(user),
|
||||
}
|
||||
}
|
||||
|
||||
func isCreator(user *usermod.User) bool {
|
||||
return user != nil && (user.OfficialCert || user.SuperUser ||
|
||||
user.MerchantUser > 0 || user.VidUploadCount > 0 || user.CoverUploadCount > 0 || user.UpTag != "")
|
||||
}
|
||||
|
||||
func ensurePairRegistered(c *imclient.Client, uid, peerUID uint64) (int64, int64, error) {
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
peer, err := usermod.FindUserByUID(peerUID)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
userIMID, err := ensureUserRegistered(c, user)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("sync user im id failed: %w", err)
|
||||
}
|
||||
peerIMID, err := ensureUserRegistered(c, peer)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("sync peer im id failed: %w", err)
|
||||
}
|
||||
return userIMID, peerIMID, nil
|
||||
}
|
||||
|
||||
func ensureUserRegistered(c *imclient.Client, user *usermod.User) (int64, error) {
|
||||
if user == nil || user.UID == 0 {
|
||||
return 0, fmt.Errorf("user is empty")
|
||||
}
|
||||
// 以 imusermod 为准:有映射即已注册
|
||||
if mapping, err := imusermod.FindByUIDQuiet(user.UID); err == nil && mapping.IMUserID > 0 {
|
||||
return mapping.IMUserID, nil
|
||||
}
|
||||
thirdPartyID := strconv.FormatUint(user.UID, 10)
|
||||
var imUserID int64
|
||||
err := withAppToken(c, func(token string) error {
|
||||
var registerErr error
|
||||
imUserID, registerErr = c.Register(imclient.RegisterRequest{
|
||||
ThirdPartyID: thirdPartyID,
|
||||
Password: sdkPassword(user.UID),
|
||||
Nickname: user.Name,
|
||||
Avatar: user.Portrait,
|
||||
}, token)
|
||||
return registerErr
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if imUserID <= 0 {
|
||||
return 0, fmt.Errorf("im register returned empty user id")
|
||||
}
|
||||
if err = imusermod.UpsertByUID(user.UID, imUserID, thirdPartyID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return imUserID, nil
|
||||
}
|
||||
|
||||
func resolveSenderIMID(c *imclient.Client, senderUID uint64) (int64, error) {
|
||||
if senderUID == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
user, err := usermod.FindUserByUID(senderUID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return ensureUserRegistered(c, user)
|
||||
}
|
||||
|
||||
func loadRegisteredIMIDs(uids []uint64) ([]int64, error) {
|
||||
mappings, err := imusermod.FindByUIDsQuiet(normalizeUIDs(uids))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imIDs := make([]int64, 0, len(mappings))
|
||||
seen := make(map[int64]struct{}, len(mappings))
|
||||
for _, m := range mappings {
|
||||
if m.IMUserID <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[m.IMUserID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[m.IMUserID] = struct{}{}
|
||||
imIDs = append(imIDs, m.IMUserID)
|
||||
}
|
||||
return imIDs, nil
|
||||
}
|
||||
|
||||
func ensureFriendDirect(c *imclient.Client, userIMID, friendIMID int64) error {
|
||||
if userIMID <= 0 || friendIMID <= 0 || userIMID == friendIMID {
|
||||
return nil
|
||||
}
|
||||
err := withAppToken(c, func(token string) error {
|
||||
return c.DirectAddFriend(imclient.DirectAddFriendRequest{
|
||||
UserID: userIMID,
|
||||
FriendID: friendIMID,
|
||||
Archive: true,
|
||||
}, token)
|
||||
})
|
||||
if err == nil || isDuplicateFriendError(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func isDuplicateFriendError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
msg := strings.ToLower(err.Error())
|
||||
return strings.Contains(msg, "already") ||
|
||||
strings.Contains(msg, "exist") ||
|
||||
strings.Contains(msg, "重复") ||
|
||||
strings.Contains(msg, "已是好友") ||
|
||||
strings.Contains(msg, "好友关系")
|
||||
}
|
||||
|
||||
func addIMFriendPair(pairSet map[string]struct{}, pairs *[]imFriendPair, uid, peerUID uint64) {
|
||||
addDirectedIMFriendPair(pairSet, pairs, uid, peerUID)
|
||||
addDirectedIMFriendPair(pairSet, pairs, peerUID, uid)
|
||||
}
|
||||
|
||||
func addDirectedIMFriendPair(pairSet map[string]struct{}, pairs *[]imFriendPair, uid, peerUID uint64) {
|
||||
if uid == 0 || peerUID == 0 || uid == peerUID {
|
||||
return
|
||||
}
|
||||
key := fmt.Sprintf("%d:%d", uid, peerUID)
|
||||
if _, ok := pairSet[key]; ok {
|
||||
return
|
||||
}
|
||||
pairSet[key] = struct{}{}
|
||||
*pairs = append(*pairs, imFriendPair{UID: uid, PeerUID: peerUID})
|
||||
}
|
||||
|
||||
func normalizeUIDs(uids []uint64) []uint64 {
|
||||
seen := make(map[uint64]struct{}, len(uids))
|
||||
normalized := make([]uint64, 0, len(uids))
|
||||
for _, uid := range uids {
|
||||
if uid == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[uid]; ok {
|
||||
continue
|
||||
}
|
||||
seen[uid] = struct{}{}
|
||||
normalized = append(normalized, uid)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func chunkInt64s(ids []int64, size int) [][]int64 {
|
||||
if size <= 0 {
|
||||
size = maxIMOnlineStatusBatchSize
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
chunks := make([][]int64, 0, (len(ids)+size-1)/size)
|
||||
for start := 0; start < len(ids); start += size {
|
||||
end := start + size
|
||||
if end > len(ids) {
|
||||
end = len(ids)
|
||||
}
|
||||
chunks = append(chunks, ids[start:end])
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
// srvImV2Cfg 拿当前进程可用的 ImV2 配置:
|
||||
// skd cron 调用 SendAdNotify 时 webg.Conf 为 nil,必须用 skdg.Conf;
|
||||
// web 服务调用反之。两侧 imv2 段必须配同样的内容。
|
||||
type srvImV2 struct {
|
||||
BaseURL string
|
||||
DynamicConfigDomain string
|
||||
SocketURL string
|
||||
MerchantCode string
|
||||
TenantCode string
|
||||
AppKey string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
}
|
||||
|
||||
func srvImV2Cfg() srvImV2 {
|
||||
if skdg.Conf != nil {
|
||||
c := skdg.Conf.ImV2
|
||||
if c.BaseURL != "" || c.AppKey != "" {
|
||||
return srvImV2{
|
||||
BaseURL: c.BaseURL, DynamicConfigDomain: c.DynamicConfigDomain, SocketURL: c.SocketURL,
|
||||
MerchantCode: c.MerchantCode, TenantCode: c.TenantCode, AppKey: c.AppKey,
|
||||
ClientID: c.ClientID, ClientSecret: c.ClientSecret,
|
||||
}
|
||||
}
|
||||
}
|
||||
if webg.Conf != nil {
|
||||
c := webg.Conf.ImV2
|
||||
return srvImV2{
|
||||
BaseURL: c.BaseURL, DynamicConfigDomain: c.DynamicConfigDomain, SocketURL: c.SocketURL,
|
||||
MerchantCode: c.MerchantCode, TenantCode: c.TenantCode, AppKey: c.AppKey,
|
||||
ClientID: c.ClientID, ClientSecret: c.ClientSecret,
|
||||
}
|
||||
}
|
||||
return srvImV2{}
|
||||
}
|
||||
|
||||
func newWebSDKClient() *imclient.Client {
|
||||
cfg := srvImV2Cfg()
|
||||
return imclient.New(imclient.Config{
|
||||
Enable: true,
|
||||
BaseURL: cfg.BaseURL,
|
||||
MerchantCode: cfg.MerchantCode,
|
||||
TenantCode: cfg.TenantCode,
|
||||
AppKey: cfg.AppKey,
|
||||
ClientID: cfg.ClientID,
|
||||
ClientSecret: cfg.ClientSecret,
|
||||
SignKey: imclient.DefaultSignKey,
|
||||
AESKey: imclient.DefaultAESKey,
|
||||
EnableSign: true,
|
||||
EncryptTimestamp: true,
|
||||
TokenTTL: defaultIMTokenTTL,
|
||||
})
|
||||
}
|
||||
|
||||
func appToken(c *imclient.Client) (string, error) {
|
||||
if !c.Enabled() {
|
||||
return "", fmt.Errorf("IM 未配置")
|
||||
}
|
||||
webAppTokenCache.Lock()
|
||||
defer webAppTokenCache.Unlock()
|
||||
if webAppTokenCache.token != "" && time.Now().Before(webAppTokenCache.expiresAt) {
|
||||
return webAppTokenCache.token, nil
|
||||
}
|
||||
token, err := c.AppToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
webAppTokenCache.token = token
|
||||
cacheSeconds := defaultIMTokenTTL - 60
|
||||
if cacheSeconds < 60 {
|
||||
cacheSeconds = defaultIMTokenTTL
|
||||
}
|
||||
webAppTokenCache.expiresAt = time.Now().Add(time.Duration(cacheSeconds) * time.Second)
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func invalidateAppToken() {
|
||||
webAppTokenCache.Lock()
|
||||
defer webAppTokenCache.Unlock()
|
||||
webAppTokenCache.token = ""
|
||||
webAppTokenCache.expiresAt = time.Time{}
|
||||
}
|
||||
|
||||
func withAppToken(c *imclient.Client, fn func(token string) error) error {
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
token, err := appToken(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = fn(token)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if attempt == 0 && imclient.IsSessionExpired(err) {
|
||||
invalidateAppToken()
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("im app token retry exhausted")
|
||||
}
|
||||
|
||||
func sdkPassword(uid uint64) string {
|
||||
return fmt.Sprintf("hjll%014d", uid%100000000000000)
|
||||
}
|
||||
Reference in New Issue
Block a user