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 } } refreshHighWinMessage(data, messageType) refreshCPRelationMessage(data, messageType) 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 refreshHighWinMessage(data map[string]any, messageType string) { target := highWinPlayTarget(messageType) if target == "" { return } user := firstTextValue(data["userNickname"], data["nickname"], data["account"], data["actualAccount"], data["userId"]) multiple := firstTextValue(data["multiple"], data["winMultiple"]) amount := firstTextValue(data["winAmount"], data["awardAmount"]) if user == "" || multiple == "" || amount == "" { return } data["msg"] = fmt.Sprintf("%s play %s get x %s win %s coins!!!", user, target, multiple, amount) } func highWinPlayTarget(messageType string) string { switch strings.TrimSpace(messageType) { case "GAME_LUCKY_GIFT": return "lucky_gift" case "GAME_BAISHUN_WIN": return "game" default: return "" } } func refreshCPRelationMessage(data map[string]any, messageType string) { if strings.TrimSpace(messageType) != "CP_RELATION_BROADCAST" { return } user := firstTextValue(data["userNickname"], data["nickname"], data["account"], data["actualAccount"], data["userId"]) cpUser := firstTextValue(data["cpUserNickname"], data["acceptNickname"], data["toUserName"], data["cpUserAccount"], data["acceptAccount"], data["cpUserId"], data["acceptUserId"]) if user == "" || cpUser == "" { return } switch strings.ToUpper(strings.TrimSpace(firstTextValue(data["relationType"]))) { case "BROTHER", "BROTHERS": data["msg"] = fmt.Sprintf("%s与%s结为兄弟", user, cpUser) case "SISTERS", "SISTER": data["msg"] = fmt.Sprintf("%s与%s结为姐妹", user, cpUser) default: data["msg"] = fmt.Sprintf("%s与%s get cp relation", user, cpUser) } } func firstTextValue(values ...any) string { for _, value := range values { text := strings.TrimSpace(fmt.Sprint(value)) if text != "" && text != "" { return text } } return "" } 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 } }