@@ -0,0 +1,312 @@
|
||||
package imser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/imclient"
|
||||
"91porn-server/models/v/imusermod"
|
||||
"91porn-server/models/v/usermod"
|
||||
)
|
||||
|
||||
const friendListFirstPageNow int64 = 1
|
||||
|
||||
type IMUserMapping struct {
|
||||
UID uint64
|
||||
IMUserID int64
|
||||
}
|
||||
|
||||
type IMFriendItem struct {
|
||||
FriendIMUserID int64 `json:"friendImUserId"`
|
||||
UID uint64 `json:"uid,omitempty"`
|
||||
NickName string `json:"nickName,omitempty"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
Status int `json:"status"`
|
||||
CreatedAt int64 `json:"createdAt,omitempty"`
|
||||
UpdatedAt int64 `json:"updatedAt,omitempty"`
|
||||
}
|
||||
|
||||
type IMHistoryMessage struct {
|
||||
MessageID string `json:"messageId"`
|
||||
SenderID int64 `json:"senderId"`
|
||||
ReceiverID int64 `json:"receiverId"`
|
||||
SenderUserID uint64 `json:"senderUserId,omitempty"`
|
||||
SenderNickName string `json:"senderNickName,omitempty"`
|
||||
SenderAvatar string `json:"senderAvatar,omitempty"`
|
||||
ReceiverUserID uint64 `json:"receiverUserId,omitempty"`
|
||||
ReceiverNickName string `json:"receiverNickName,omitempty"`
|
||||
ReceiverAvatar string `json:"receiverAvatar,omitempty"`
|
||||
Content string `json:"content"`
|
||||
MessageType string `json:"messageType"`
|
||||
Seq int64 `json:"seq"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
FromSelf bool `json:"fromSelf"`
|
||||
}
|
||||
|
||||
// ResolveStoredIMUserID 解析用户的 IM 平台 user_id:
|
||||
// 优先 usermod.ImUserID → imusermod 映射表 → 都没有则调用 IM 注册接口创建。
|
||||
// 保证返回的 imUserId > 0,或返回错误,调用方无需再判 0。
|
||||
func ResolveStoredIMUserID(uid uint64) (int64, error) {
|
||||
// 直接复用 EnsureSDKUser:它的"查→兜底注册"流程跟这里需要的语义一致。
|
||||
return EnsureSDKUser(uid)
|
||||
}
|
||||
|
||||
func ResolveUIDByIMUserID(imUserID int64) (uint64, error) {
|
||||
if imUserID <= 0 {
|
||||
return 0, fmt.Errorf("imUserId is empty")
|
||||
}
|
||||
// imUserId → uid 以 imusermod 为唯一真源
|
||||
mapping, err := imusermod.FindByIMUserIDQuiet(imUserID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if mapping.UID <= 0 {
|
||||
return 0, fmt.Errorf("im user not found")
|
||||
}
|
||||
return mapping.UID, nil
|
||||
}
|
||||
|
||||
func BusinessUsersByIMUserIDs(imUserIDs []int64) (map[int64]*usermod.User, error) {
|
||||
out := make(map[int64]*usermod.User)
|
||||
ids := uniquePositiveInt64(imUserIDs)
|
||||
if len(ids) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
// imUserId → uid 走 imusermod(唯一真源),再按 uid 批量取业务用户
|
||||
mappings, err := imusermod.FindByIMUserIDsQuiet(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uids := make([]uint64, 0, len(mappings))
|
||||
uidByIM := make(map[int64]uint64, len(mappings))
|
||||
for _, mapping := range mappings {
|
||||
if mapping.UID <= 0 || mapping.IMUserID <= 0 {
|
||||
continue
|
||||
}
|
||||
uids = append(uids, mapping.UID)
|
||||
uidByIM[mapping.IMUserID] = mapping.UID
|
||||
}
|
||||
if len(uids) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
usersByUID, err := usermod.FindUsersMapByUID(uids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for imUserID, uid := range uidByIM {
|
||||
if u := usersByUID[uid]; u != nil {
|
||||
out[imUserID] = u
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func EnsureFriendsBidirectional(uid, peerUID uint64) (selfIMUserID, peerIMUserID int64, added bool, err error) {
|
||||
if uid == 0 || peerUID == 0 {
|
||||
return 0, 0, false, fmt.Errorf("uid is empty")
|
||||
}
|
||||
if uid == peerUID {
|
||||
return 0, 0, false, fmt.Errorf("cannot add self")
|
||||
}
|
||||
c := newSDKClient()
|
||||
if !c.Enabled() {
|
||||
return 0, 0, false, nil
|
||||
}
|
||||
selfIMUserID, err = EnsureSDKUser(uid)
|
||||
if err != nil {
|
||||
return 0, 0, false, err
|
||||
}
|
||||
peerIMUserID, err = EnsureSDKUser(peerUID)
|
||||
if err != nil {
|
||||
return 0, 0, false, err
|
||||
}
|
||||
addedForward, err := ensureFriendDirect(c, selfIMUserID, peerIMUserID)
|
||||
if err != nil {
|
||||
return 0, 0, false, err
|
||||
}
|
||||
addedReverse, err := ensureFriendDirect(c, peerIMUserID, selfIMUserID)
|
||||
if err != nil {
|
||||
return 0, 0, false, err
|
||||
}
|
||||
return selfIMUserID, peerIMUserID, addedForward || addedReverse, nil
|
||||
}
|
||||
|
||||
func FriendList(uid uint64, now int64) (imUserID int64, usedNow int64, nextNow int64, friends []IMFriendItem, err error) {
|
||||
c := newSDKClient()
|
||||
if !c.Enabled() {
|
||||
return 0, 0, 0, []IMFriendItem{}, nil
|
||||
}
|
||||
imUserID, err = EnsureSDKUser(uid)
|
||||
if err != nil {
|
||||
return 0, 0, 0, nil, err
|
||||
}
|
||||
if now <= 0 {
|
||||
now = friendListFirstPageNow
|
||||
}
|
||||
var relations []imclient.Friend
|
||||
err = withAppToken(c, func(token string) error {
|
||||
var listErr error
|
||||
relations, listErr = c.FriendList(imUserID, now, token)
|
||||
return listErr
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, 0, nil, err
|
||||
}
|
||||
friendIMIDs := make([]int64, 0, len(relations))
|
||||
for _, relation := range relations {
|
||||
if relation.FriendID > 0 {
|
||||
friendIMIDs = append(friendIMIDs, relation.FriendID)
|
||||
}
|
||||
}
|
||||
userByIM, err := BusinessUsersByIMUserIDs(friendIMIDs)
|
||||
if err != nil {
|
||||
return 0, 0, 0, nil, err
|
||||
}
|
||||
nextNow = now
|
||||
items := make([]IMFriendItem, 0, len(relations))
|
||||
for _, relation := range relations {
|
||||
if relation.UpdatedAt > nextNow {
|
||||
nextNow = relation.UpdatedAt
|
||||
}
|
||||
item := IMFriendItem{
|
||||
FriendIMUserID: relation.FriendID,
|
||||
Remark: relation.Remark,
|
||||
Status: relation.Status,
|
||||
CreatedAt: relation.CreatedAt,
|
||||
UpdatedAt: relation.UpdatedAt,
|
||||
}
|
||||
if u := userByIM[relation.FriendID]; u != nil {
|
||||
item.UID = u.UID
|
||||
item.NickName = u.Name
|
||||
item.Avatar = common.BindUrl(appg.Conf.URL.OriginUrl, u.Portrait)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return imUserID, now, nextNow, items, nil
|
||||
}
|
||||
|
||||
func HistoryMessages(uid, peerUID uint64, req imclient.HistoryMessageRequest) (selfIMUserID, peerIMUserID int64, messages []IMHistoryMessage, err error) {
|
||||
c := newSDKClient()
|
||||
if !c.Enabled() {
|
||||
return 0, 0, []IMHistoryMessage{}, nil
|
||||
}
|
||||
selfIMUserID, err = EnsureSDKUser(uid)
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
peerIMUserID, err = ResolveStoredIMUserID(peerUID)
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
if peerIMUserID <= 0 {
|
||||
return 0, 0, nil, fmt.Errorf("peer im user not found")
|
||||
}
|
||||
req.UserID1 = selfIMUserID
|
||||
req.UserID2 = peerIMUserID
|
||||
var history []imclient.MessageDetail
|
||||
err = withAppToken(c, func(token string) error {
|
||||
var historyErr error
|
||||
history, historyErr = c.HistoryMessages(req, token)
|
||||
return historyErr
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
messageIMIDs := make([]int64, 0, len(history)*2)
|
||||
for _, msg := range history {
|
||||
if msg.SenderID > 0 {
|
||||
messageIMIDs = append(messageIMIDs, msg.SenderID)
|
||||
}
|
||||
if msg.ReceiverID > 0 {
|
||||
messageIMIDs = append(messageIMIDs, msg.ReceiverID)
|
||||
}
|
||||
}
|
||||
userByIM, err := BusinessUsersByIMUserIDs(messageIMIDs)
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
out := make([]IMHistoryMessage, 0, len(history))
|
||||
for _, msg := range history {
|
||||
item := IMHistoryMessage{
|
||||
MessageID: msg.MessageID,
|
||||
SenderID: msg.SenderID,
|
||||
ReceiverID: msg.ReceiverID,
|
||||
Content: msg.Content,
|
||||
MessageType: msg.MessageType,
|
||||
Seq: msg.Seq,
|
||||
CreatedAt: msg.CreatedAt,
|
||||
FromSelf: msg.SenderID == selfIMUserID,
|
||||
}
|
||||
if u := userByIM[msg.SenderID]; u != nil {
|
||||
item.SenderUserID = u.UID
|
||||
item.SenderNickName = u.Name
|
||||
item.SenderAvatar = common.BindUrl(appg.Conf.URL.OriginUrl, u.Portrait)
|
||||
}
|
||||
if u := userByIM[msg.ReceiverID]; u != nil {
|
||||
item.ReceiverUserID = u.UID
|
||||
item.ReceiverNickName = u.Name
|
||||
item.ReceiverAvatar = common.BindUrl(appg.Conf.URL.OriginUrl, u.Portrait)
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
return selfIMUserID, peerIMUserID, out, nil
|
||||
}
|
||||
|
||||
func ensureFriendDirect(c *imclient.Client, userIMID, friendIMID int64) (bool, error) {
|
||||
var added bool
|
||||
err := withAppToken(c, func(token string) error {
|
||||
relations, err := c.FriendList(userIMID, friendListFirstPageNow, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, relation := range relations {
|
||||
if relation.FriendID == friendIMID && relation.Status == 0 {
|
||||
added = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
err = c.DirectAddFriend(imclient.DirectAddFriendRequest{
|
||||
UserID: userIMID,
|
||||
FriendID: friendIMID,
|
||||
}, token)
|
||||
if err == nil {
|
||||
added = true
|
||||
}
|
||||
return err
|
||||
})
|
||||
if isDuplicateFriendError(err) {
|
||||
return false, nil
|
||||
}
|
||||
return added, err
|
||||
}
|
||||
|
||||
func isDuplicateFriendError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
msg := strings.ToLower(err.Error())
|
||||
return strings.Contains(msg, "already") ||
|
||||
strings.Contains(msg, "exists") ||
|
||||
strings.Contains(msg, "重复") ||
|
||||
strings.Contains(msg, "已存在") ||
|
||||
strings.Contains(msg, "好友关系已存在")
|
||||
}
|
||||
|
||||
func uniquePositiveInt64(in []int64) []int64 {
|
||||
out := make([]int64, 0, len(in))
|
||||
seen := make(map[int64]struct{}, len(in))
|
||||
for _, v := range in {
|
||||
if v <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[v]; ok {
|
||||
continue
|
||||
}
|
||||
seen[v] = struct{}{}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package imser
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant"
|
||||
"91porn-server/common/crypt"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/v/usermod"
|
||||
)
|
||||
|
||||
// GetSign 获取IM sign
|
||||
func GetSign(uid uint64, ua ua.UA) string {
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil || u == nil {
|
||||
return ""
|
||||
}
|
||||
// IsVip := false
|
||||
// if userModel.VipExpireDate.After(time.Now()) {
|
||||
// IsVip = true
|
||||
// }
|
||||
var voiceChatNotify string
|
||||
if common.IsSpecifyVerBaseOnUa(ua, constant.Ver3_1_0) {
|
||||
voiceChatNotify = "支持语音聊天"
|
||||
} else {
|
||||
voiceChatNotify = "不支持语音聊天"
|
||||
}
|
||||
ul := url.Values{}
|
||||
ul.Add("id", strconv.FormatUint(uid, 10))
|
||||
ul.Add("appId", appg.Conf.Kefu.AppID)
|
||||
ul.Add("platName", appg.Conf.Kefu.PlatName)
|
||||
ul.Add("userName", u.Name+"**"+ua.SysType+"**"+ua.DevType+"**"+ua.Ver+"**"+voiceChatNotify)
|
||||
ul.Add("isVip", "false")
|
||||
ul.Add("avatar", common.BindUrl(appg.Conf.URL.OriginUrl, u.Portrait))
|
||||
dcodeurl, _ := url.QueryUnescape(ul.Encode())
|
||||
sign, err := crypt.AesEncrypt(dcodeurl, appg.Conf.Kefu.AppSecret)
|
||||
if err != nil {
|
||||
log.Warn("create im sign wrong", log.Any("warn", err))
|
||||
return ""
|
||||
}
|
||||
return "/kefu/?sign=" + hex.EncodeToString(sign) + "&appId=" + appg.Conf.Kefu.AppID + "&theme=" + appg.Conf.Kefu.Theme
|
||||
}
|
||||
|
||||
// GetWhiteSign 获取White IM sign
|
||||
func GetWhiteSign() string {
|
||||
return "/kefu/?" + "appId=" + appg.Conf.Kefu.AppID + "&anonymous=true"
|
||||
}
|
||||
|
||||
// GetSignNew 新获取IM sign
|
||||
func GetSignNew(uid uint64, ua ua.UA) string {
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil || u == nil {
|
||||
return ""
|
||||
}
|
||||
// IsVip := false
|
||||
// if userModel.VipExpireDate.After(time.Now()) {
|
||||
// IsVip = true
|
||||
// }
|
||||
var voiceChatNotify string
|
||||
if common.IsSpecifyVerBaseOnUa(ua, constant.Ver3_1_0) {
|
||||
voiceChatNotify = "支持语音聊天"
|
||||
} else {
|
||||
voiceChatNotify = "不支持语音聊天"
|
||||
}
|
||||
ul := url.Values{}
|
||||
ul.Add("id", strconv.FormatUint(uid, 10))
|
||||
ul.Add("appId", appg.Conf.Kefu.AppID)
|
||||
ul.Add("platName", appg.Conf.Kefu.PlatName)
|
||||
ul.Add("userName", u.Name+"**"+ua.SysType+"**"+ua.DevType+"**"+ua.Ver+"**"+voiceChatNotify)
|
||||
ul.Add("isVip", "false")
|
||||
ul.Add("avatar", common.BindUrl(appg.Conf.URL.OriginUrl, u.Portrait))
|
||||
dcodeurl, _ := url.QueryUnescape(ul.Encode())
|
||||
sign, err := crypt.AesEncrypt(dcodeurl, appg.Conf.Kefu.AppSecret)
|
||||
if err != nil {
|
||||
log.Warn("create im sign wrong", log.Any("warn", err))
|
||||
return ""
|
||||
}
|
||||
return "/kefu/customer/im?sign=" + hex.EncodeToString(sign) + "&appId=" + appg.Conf.Kefu.AppID + "&theme=" + appg.Conf.Kefu.Theme
|
||||
}
|
||||
|
||||
// GetWhiteSignNew 新获取White IM sign
|
||||
func GetWhiteSignNew() string {
|
||||
return "/kefu/customer/im?appId=" + appg.Conf.Kefu.AppID + "&anonymous=true"
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
package imser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/imclient"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/redis"
|
||||
"91porn-server/models/v/imusermod"
|
||||
"91porn-server/models/v/usermod"
|
||||
|
||||
"github.com/go-redsync/redsync/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultIMV2DynamicConfigDomain = "https://sdk.api.cdflow.cn"
|
||||
defaultIMV2SocketURL = "wss://wss.cdflow.cn:32214"
|
||||
|
||||
// ensureSDKUser 抢锁失败后,轮询 imUser 表的间隔与最大次数(10 × 200ms = 最多等 2s)
|
||||
imSDKEnsureWaitInterval = 200 * time.Millisecond
|
||||
imSDKEnsureWaitTries = 10
|
||||
)
|
||||
|
||||
var appTokenCache = struct {
|
||||
sync.RWMutex
|
||||
token string
|
||||
expiresAt time.Time
|
||||
}{}
|
||||
|
||||
type SDKAuthInfo struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
ImToken string `json:"imToken"`
|
||||
Token string `json:"token,omitempty"`
|
||||
UserID uint64 `json:"userId"`
|
||||
ImUserID int64 `json:"imUserId"`
|
||||
Registered bool `json:"registered"`
|
||||
NewlyRegistered bool `json:"newlyRegistered,omitempty"`
|
||||
DynamicConfigDomain string `json:"dynamicConfigDomain"`
|
||||
SocketURL string `json:"socketURL"`
|
||||
SysType string `json:"sysType,omitempty"`
|
||||
OS string `json:"os,omitempty"`
|
||||
OSType string `json:"osType,omitempty"`
|
||||
}
|
||||
|
||||
type SDKPublicConfig struct {
|
||||
Enable bool `json:"enable"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
SocketURL string `json:"socketUrl"`
|
||||
MerchantCode string `json:"merchantCode"`
|
||||
TenantCode string `json:"tenantCode"`
|
||||
}
|
||||
|
||||
func SDKEnabled() bool {
|
||||
return newSDKClient().Enabled()
|
||||
}
|
||||
|
||||
func GetSDKPublicConfig() SDKPublicConfig {
|
||||
cfg := sdkConfig()
|
||||
return SDKPublicConfig{
|
||||
Enable: newSDKClient().Enabled(),
|
||||
BaseURL: cfg.BaseURL,
|
||||
SocketURL: appg.Conf.ImV2.SocketURL,
|
||||
MerchantCode: cfg.MerchantCode,
|
||||
TenantCode: cfg.TenantCode,
|
||||
}
|
||||
}
|
||||
|
||||
func GetSDKAuth(uid uint64) (*SDKAuthInfo, error) {
|
||||
c := newSDKClient()
|
||||
if !c.Enabled() {
|
||||
reason := c.DisabledReason()
|
||||
log.Error("GetSDKAuth im sdk disabled", log.Any("uid", uid), log.Any("reason", reason))
|
||||
return nil, fmt.Errorf("im sdk is disabled: %s", reason)
|
||||
}
|
||||
// 确保 IM 用户存在(内部以 imUser 表为准 + 并发锁),并得知本次是否新注册
|
||||
imUserID, newlyRegistered, err := ensureSDKUser(uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := withAppToken(c, func(token string) error {
|
||||
return c.SetOnlineStatus(imclient.SetOnlineStatusRequest{
|
||||
UserID: imUserID,
|
||||
ShowOnlineStatus: true,
|
||||
}, token)
|
||||
}); err != nil {
|
||||
log.Warn("set im online status visible failed", log.Any("uid", uid), log.Any("imUserId", imUserID), log.E(err))
|
||||
}
|
||||
token, err := cachedUserToken(c, imUserID)
|
||||
if err != nil {
|
||||
log.Error("get user im token fail", log.Any("uid", uid), log.Any("imUserId", imUserID), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
return &SDKAuthInfo{
|
||||
Enabled: true,
|
||||
ImToken: token,
|
||||
Token: token,
|
||||
UserID: uid,
|
||||
ImUserID: imUserID,
|
||||
Registered: imUserID > 0,
|
||||
NewlyRegistered: newlyRegistered,
|
||||
DynamicConfigDomain: dynamicConfigDomain(),
|
||||
SocketURL: socketURL(),
|
||||
OS: "web",
|
||||
OSType: "web",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func SendPrivateMessage(sendUID, takeUID uint64, content string, imgURLs []string) error {
|
||||
c := newSDKClient()
|
||||
if !c.Enabled() {
|
||||
return nil
|
||||
}
|
||||
senderID, err := EnsureSDKUser(sendUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
receiverID, err := EnsureSDKUser(takeUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if content != "" {
|
||||
if err = withAppToken(c, func(token string) error {
|
||||
return c.SendMessage(imclient.SendMessageRequest{
|
||||
SenderID: senderID,
|
||||
ReceiverID: receiverID,
|
||||
Content: content,
|
||||
MessageType: imclient.MessageTypeText,
|
||||
}, token)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, imgURL := range imgURLs {
|
||||
if imgURL == "" {
|
||||
continue
|
||||
}
|
||||
if err = withAppToken(c, func(token string) error {
|
||||
return c.SendMessage(imclient.SendMessageRequest{
|
||||
SenderID: senderID,
|
||||
ReceiverID: receiverID,
|
||||
Content: "",
|
||||
MessageType: imclient.MessageTypeImage,
|
||||
Attachment: &imclient.Attachment{
|
||||
URL: imgURL,
|
||||
},
|
||||
}, token)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SyncSDKBaseInfo(uid uint64, name, portrait, signature *string) error {
|
||||
c := newSDKClient()
|
||||
if !c.Enabled() {
|
||||
return nil
|
||||
}
|
||||
imUserID, err := EnsureSDKUser(uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := imclient.UpdateBaseInfoRequest{
|
||||
UserID: imUserID,
|
||||
}
|
||||
if name != nil {
|
||||
req.Name = *name
|
||||
}
|
||||
if portrait != nil {
|
||||
req.ImgURL = common.BindUrl(appg.Conf.URL.OriginUrl, *portrait)
|
||||
}
|
||||
if signature != nil {
|
||||
req.Signature = *signature
|
||||
}
|
||||
if req.Name == "" && req.ImgURL == "" && req.Signature == "" {
|
||||
return nil
|
||||
}
|
||||
return withAppToken(c, func(token string) error {
|
||||
return c.UpdateBaseInfo(req, token)
|
||||
})
|
||||
}
|
||||
|
||||
// EnsureSDKUser 确保 IM 用户存在并返回 imUserID。
|
||||
func EnsureSDKUser(uid uint64) (int64, error) {
|
||||
imUserID, _, err := ensureSDKUser(uid)
|
||||
return imUserID, err
|
||||
}
|
||||
|
||||
// ensureSDKUser 确保 IM 用户存在并返回 imUserID;newlyRegistered 表示本次是否真正发起了 SDK 注册
|
||||
// (命中 imUser 表或回填历史用户均为 false)。以 imUser 表为准判断是否已注册。
|
||||
func ensureSDKUser(uid uint64) (int64, bool, error) {
|
||||
if uid == 0 {
|
||||
log.Warn("EnsureSDKUser uid is zero")
|
||||
return 0, false, fmt.Errorf("uid is empty")
|
||||
}
|
||||
// fast path:以 imUser 表为准,已注册直接返回
|
||||
if mapping, _ := imusermod.FindByUIDQuiet(uid); mapping.IMUserID > 0 {
|
||||
return mapping.IMUserID, false, nil
|
||||
}
|
||||
// 未注册 → 抢锁串行化,防止并发重复注册。只抢一次、不自旋。
|
||||
mu := redis.BuildLock(appg.Redis, redisconst.IMSDKEnsureLockKey(uid),
|
||||
redsync.WithExpiry(10*time.Second),
|
||||
redsync.WithTries(1),
|
||||
)
|
||||
if err := mu.Lock(); err != nil {
|
||||
// 没抢到锁 → 已有别的请求在注册:每 200ms 轮询一次 imUser 表,最多等 2s
|
||||
for i := 0; i < imSDKEnsureWaitTries; i++ {
|
||||
time.Sleep(imSDKEnsureWaitInterval)
|
||||
if mapping, _ := imusermod.FindByUIDQuiet(uid); mapping.IMUserID > 0 {
|
||||
return mapping.IMUserID, false, nil
|
||||
}
|
||||
}
|
||||
return 0, false, fmt.Errorf("ensure sdk user busy: wait imUser timeout, uid=%d", uid)
|
||||
}
|
||||
defer func() { _, _ = mu.Unlock() }()
|
||||
// 双重检查:等锁期间可能已被其他请求注册完,避免重复注册
|
||||
if mapping, _ := imusermod.FindByUIDQuiet(uid); mapping.IMUserID > 0 {
|
||||
return mapping.IMUserID, false, nil
|
||||
}
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("EnsureSDKUser usermod.FindUserByUID fail uid=%d", uid), log.E(err))
|
||||
return 0, false, err
|
||||
}
|
||||
if u == nil {
|
||||
log.Warn(fmt.Sprintf("EnsureSDKUser user is nil uid=%d", uid))
|
||||
return 0, false, fmt.Errorf("user %d not found", uid)
|
||||
}
|
||||
// 一切以 imUser 表为准:表里无映射即视为未注册,直接走 SDK 注册(不信任 user.ImUserID,不回填)
|
||||
c := newSDKClient()
|
||||
thirdPartyID := strconv.FormatUint(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(uid),
|
||||
Nickname: u.Name,
|
||||
Avatar: u.Portrait,
|
||||
}, token)
|
||||
return registerErr
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("EnsureSDKUser c.Register fail uid=%d", uid), log.E(err))
|
||||
return 0, false, err
|
||||
}
|
||||
if imUserID <= 0 {
|
||||
log.Warn(fmt.Sprintf("EnsureSDKUser im register returned empty user id uid=%d", uid), log.E(err))
|
||||
return 0, false, fmt.Errorf("im register returned empty user id")
|
||||
}
|
||||
if err = imusermod.UpsertByUID(uid, imUserID, thirdPartyID); err != nil {
|
||||
log.Warn(fmt.Sprintf("EnsureSDKUser upsert fail uid=%d", uid), log.E(err))
|
||||
return 0, false, err
|
||||
}
|
||||
return imUserID, true, nil
|
||||
}
|
||||
|
||||
// InitAppToken 启动时异步取一次 appToken(不阻断启动),并每小时刷新一次,写入内存+redis。
|
||||
func InitAppToken() {
|
||||
if !SDKEnabled() {
|
||||
return
|
||||
}
|
||||
common.Go(func() {
|
||||
if err := refreshAppToken(); err != nil {
|
||||
log.Warn("init app token failed", log.E(err))
|
||||
}
|
||||
})
|
||||
common.Go(func() {
|
||||
ticker := time.NewTicker(time.Hour)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
if err := refreshAppToken(); err != nil {
|
||||
log.Warn("refresh app token failed", log.E(err))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// refreshAppToken 从 SDK 取最新 appToken,写入内存 + redis。
|
||||
func refreshAppToken() error {
|
||||
c := newSDKClient()
|
||||
if !c.Enabled() {
|
||||
return nil
|
||||
}
|
||||
token, err := c.AppToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if token == "" {
|
||||
return fmt.Errorf("im app token empty")
|
||||
}
|
||||
ttl := sdkTokenTTL()
|
||||
cacheSeconds := ttl - 60
|
||||
if cacheSeconds < 60 {
|
||||
cacheSeconds = ttl
|
||||
}
|
||||
setToken(token, time.Duration(cacheSeconds)*time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
// setToken 写入内存 + redis。
|
||||
func setToken(token string, ttl time.Duration) {
|
||||
appTokenCache.Lock()
|
||||
appTokenCache.token = token
|
||||
appTokenCache.expiresAt = time.Now().Add(ttl)
|
||||
appTokenCache.Unlock()
|
||||
if err := appg.Redis.Set(redisconst.IMSDKAppTokenKey, token, ttl); err != nil {
|
||||
log.Warn("set app token to redis fail", log.E(err))
|
||||
}
|
||||
}
|
||||
|
||||
// getToken 取 appToken:优先内存(读锁),取不到再取 redis(命中则回种内存)。
|
||||
func getToken() string {
|
||||
appTokenCache.RLock()
|
||||
token := appTokenCache.token
|
||||
appTokenCache.RUnlock()
|
||||
if token != "" {
|
||||
return token
|
||||
}
|
||||
if v, err := appg.Redis.Get(redisconst.IMSDKAppTokenKey); err == nil && v != nil && *v != "" {
|
||||
appTokenCache.Lock()
|
||||
appTokenCache.token = *v
|
||||
appTokenCache.Unlock()
|
||||
return *v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func appToken(c *imclient.Client) (string, error) {
|
||||
if token := getToken(); token != "" {
|
||||
return token, nil
|
||||
}
|
||||
// 内存和 redis 都没有(启动初期或刷新失败)→ 兜底同步取一次
|
||||
if err := refreshAppToken(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if token := getToken(); token != "" {
|
||||
return token, nil
|
||||
}
|
||||
return "", fmt.Errorf("im app token unavailable")
|
||||
}
|
||||
|
||||
func invalidateAppToken() {
|
||||
appTokenCache.Lock()
|
||||
appTokenCache.token = ""
|
||||
appTokenCache.expiresAt = time.Time{}
|
||||
appTokenCache.Unlock()
|
||||
_, _ = appg.Redis.Del(redisconst.IMSDKAppTokenKey)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// cachedUserToken 获取 IM 用户 token,缓存 12 小时,避免每次鉴权都请求 SDK。
|
||||
// 缓存按 imUserID 维度,Redis 故障时降级为直接请求,不阻断鉴权。
|
||||
func cachedUserToken(c *imclient.Client, imUserID int64) (string, error) {
|
||||
key := redisconst.IMSDKUserTokenKey(imUserID)
|
||||
if cached, err := appg.Redis.Get(key); err != nil {
|
||||
log.Warn("get im user token cache fail", log.Any("imUserId", imUserID), log.E(err))
|
||||
} else if cached != nil && *cached != "" {
|
||||
return *cached, nil
|
||||
}
|
||||
token, err := c.UserToken(imUserID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if token != "" {
|
||||
if err = appg.Redis.Set(key, token, redisconst.IMSDKUserTokenExpire); err != nil {
|
||||
log.Warn("set im user token cache fail", log.Any("imUserId", imUserID), log.E(err))
|
||||
}
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func sdkTokenTTL() int64 {
|
||||
ttl := sdkConfig().TokenTTL
|
||||
if ttl <= 0 {
|
||||
ttl = 86400
|
||||
}
|
||||
return ttl
|
||||
}
|
||||
|
||||
func newSDKClient() *imclient.Client {
|
||||
return imclient.New(sdkConfig())
|
||||
}
|
||||
|
||||
func sdkConfig() imclient.Config {
|
||||
cfg := appg.Conf.ImV2
|
||||
return 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: 86400,
|
||||
}
|
||||
}
|
||||
|
||||
func dynamicConfigDomain() string {
|
||||
if domain := strings.TrimRight(appg.Conf.ImV2.DynamicConfigDomain, "/"); domain != "" {
|
||||
return domain
|
||||
}
|
||||
rawBaseURL := strings.TrimSpace(appg.Conf.ImV2.BaseURL)
|
||||
if rawBaseURL != "" {
|
||||
if u, err := url.Parse(rawBaseURL); err == nil && u.Scheme != "" && u.Host != "" {
|
||||
return u.Scheme + "://" + u.Host
|
||||
}
|
||||
}
|
||||
return defaultIMV2DynamicConfigDomain
|
||||
}
|
||||
|
||||
func socketURL() string {
|
||||
if socket := strings.TrimSpace(appg.Conf.ImV2.SocketURL); socket != "" {
|
||||
return socket
|
||||
}
|
||||
return defaultIMV2SocketURL
|
||||
}
|
||||
|
||||
func sdkPassword(uid uint64) string {
|
||||
return fmt.Sprintf("hjll%014d", uid%100000000000000)
|
||||
}
|
||||
|
||||
func LogSDKSendError(sendUID, takeUID uint64, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
log.Warn("im sdk send private message error", log.Any("sendUID", sendUID), log.Any("takeUID", takeUID), log.E(err))
|
||||
}
|
||||
Reference in New Issue
Block a user