2026-05-08 18:55:47 +08:00

256 lines
6.6 KiB
Go

package regionimgroup
import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
"net/http"
"strconv"
"strings"
"chatapp3-golang/internal/model"
"chatapp3-golang/internal/service/tencentim"
"gorm.io/gorm"
)
// SendRegionBroadcast 向当前区域 IM 群发送自定义广播消息。
func (s *Service) SendRegionBroadcast(ctx context.Context, req RegionBroadcastRequest) (*RegionBroadcastResponse, error) {
if s == nil || s.im == nil {
return nil, mapTencentIMError(tencentim.ErrNotConfigured)
}
sysOrigin := normalizeSysOrigin(req.SysOrigin)
messageType := strings.TrimSpace(req.Type)
if messageType == "" {
return nil, NewAppError(http.StatusBadRequest, "missing_broadcast_type", "type is required")
}
data := cloneBroadcastData(req.Data)
senderUserID := req.SenderUserID.Int64()
if senderUserID <= 0 {
senderUserID = firstInt64Value(data["senderUserId"], data["sendUserId"], data["userId"])
}
regionCode := normalizeRegionCode(req.RegionCode)
if regionCode == "" {
if senderUserID <= 0 {
return nil, NewAppError(http.StatusBadRequest, "missing_sender_user_id", "senderUserId is required when regionCode is empty")
}
resolvedRegionCode, err := s.resolveUserRegion(ctx, sysOrigin, senderUserID)
if err != nil {
return nil, err
}
regionCode = resolvedRegionCode
}
group, err := s.loadActiveRegionIMGroup(ctx, sysOrigin, regionCode)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, NewAppError(http.StatusNotFound, "region_im_group_not_configured",
fmt.Sprintf("region im group is not configured sysOrigin=%s regionCode=%s", sysOrigin, regionCode))
}
if err != nil {
return nil, err
}
data["sysOrigin"] = sysOrigin
data["regionCode"] = regionCode
if senderUserID > 0 {
if err := s.enrichBroadcastSender(ctx, data, senderUserID); err != nil {
return nil, err
}
}
if acceptUserID := firstAcceptUserID(data); acceptUserID > 0 {
if err := s.enrichBroadcastAcceptUser(ctx, data, acceptUserID); err != nil {
return nil, err
}
}
body := map[string]any{
"type": messageType,
"data": data,
}
if err := s.im.SendCustomGroupMessage(ctx, group.GroupID, body); err != nil {
return nil, mapTencentIMError(err)
}
return &RegionBroadcastResponse{
SysOrigin: sysOrigin,
RegionCode: regionCode,
GroupID: group.GroupID,
Type: messageType,
}, nil
}
func cloneBroadcastData(data map[string]any) map[string]any {
if len(data) == 0 {
return map[string]any{}
}
clone := make(map[string]any, len(data))
for key, value := range data {
clone[key] = value
}
return clone
}
func (s *Service) enrichBroadcastSender(ctx context.Context, data map[string]any, userID int64) error {
profile, err := s.loadBroadcastUserProfile(ctx, userID)
if err != nil {
return err
}
account := strings.TrimSpace(profile.Account)
if account == "" {
account = strconv.FormatInt(userID, 10)
}
setIfMissing(data, "sendUserId", strconv.FormatInt(userID, 10))
setIfMissing(data, "senderUserId", strconv.FormatInt(userID, 10))
setIfMissing(data, "account", account)
setIfMissing(data, "actualAccount", account)
setIfMissing(data, "userNickname", profile.UserNickname)
setIfMissing(data, "nickname", profile.UserNickname)
setIfMissing(data, "userAvatar", profile.UserAvatar)
setIfMissing(data, "countryCode", profile.CountryCode)
setIfMissing(data, "countryName", profile.CountryName)
return nil
}
func (s *Service) enrichBroadcastAcceptUser(ctx context.Context, data map[string]any, userID int64) error {
profile, err := s.loadBroadcastUserProfile(ctx, userID)
if err != nil {
return err
}
account := strings.TrimSpace(profile.Account)
if account == "" {
account = strconv.FormatInt(userID, 10)
}
setIfMissing(data, "acceptUserId", strconv.FormatInt(userID, 10))
setIfMissing(data, "toUserId", strconv.FormatInt(userID, 10))
setIfMissing(data, "acceptAccount", account)
setIfMissing(data, "acceptNickname", profile.UserNickname)
setIfMissing(data, "toUserName", profile.UserNickname)
setIfMissing(data, "acceptUserAvatar", profile.UserAvatar)
setIfMissing(data, "toUserAvatarUrl", profile.UserAvatar)
return nil
}
func (s *Service) loadBroadcastUserProfile(ctx context.Context, userID int64) (model.UserBaseInfo, error) {
var user model.UserBaseInfo
err := s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
account := strconv.FormatInt(userID, 10)
return model.UserBaseInfo{
ID: userID,
Account: account,
UserNickname: account,
}, nil
}
if err != nil {
return model.UserBaseInfo{}, err
}
if strings.TrimSpace(user.Account) == "" {
user.Account = strconv.FormatInt(userID, 10)
}
if strings.TrimSpace(user.UserNickname) == "" {
user.UserNickname = user.Account
}
return user, nil
}
func setIfMissing(data map[string]any, key string, value any) {
if data == nil || value == nil {
return
}
if existing, exists := data[key]; exists && strings.TrimSpace(fmt.Sprint(existing)) != "" {
return
}
data[key] = value
}
func firstAcceptUserID(data map[string]any) int64 {
if id := firstInt64Value(data["acceptUserId"], data["toUserId"]); id > 0 {
return id
}
return firstInt64FromList(data["acceptUserIds"])
}
func firstInt64Value(values ...any) int64 {
for _, value := range values {
if parsed := int64FromAny(value); parsed > 0 {
return parsed
}
}
return 0
}
func firstInt64FromList(value any) int64 {
switch typed := value.(type) {
case []any:
for _, item := range typed {
if parsed := int64FromAny(item); parsed > 0 {
return parsed
}
}
case []int64:
for _, item := range typed {
if item > 0 {
return item
}
}
case []string:
for _, item := range typed {
if parsed := int64FromAny(item); parsed > 0 {
return parsed
}
}
default:
return int64FromAny(typed)
}
return 0
}
func int64FromAny(value any) int64 {
switch typed := value.(type) {
case nil:
return 0
case int:
return int64(typed)
case int8:
return int64(typed)
case int16:
return int64(typed)
case int32:
return int64(typed)
case int64:
return typed
case uint:
if uint64(typed) > math.MaxInt64 {
return 0
}
return int64(typed)
case uint8:
return int64(typed)
case uint16:
return int64(typed)
case uint32:
return int64(typed)
case uint64:
if typed > math.MaxInt64 {
return 0
}
return int64(typed)
case float32:
return int64(typed)
case float64:
return int64(typed)
case json.Number:
parsed, _ := typed.Int64()
return parsed
case string:
parsed, _ := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
return parsed
default:
parsed, _ := strconv.ParseInt(strings.TrimSpace(fmt.Sprint(typed)), 10, 64)
return parsed
}
}