296 lines
9.5 KiB
Go
296 lines
9.5 KiB
Go
package regionimgroup
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha1"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"chatapp3-golang/internal/model"
|
|
"chatapp3-golang/internal/service/tencentim"
|
|
"chatapp3-golang/internal/utils"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
// ListRegionIMGroups 返回已配置的区域 IM 群组。
|
|
func (s *Service) ListRegionIMGroups(ctx context.Context, sysOrigin string) ([]RegionIMGroupResponse, error) {
|
|
sysOrigin = normalizeSysOrigin(sysOrigin)
|
|
var rows []model.VoiceRoomRegionIMGroup
|
|
if err := s.db.WithContext(ctx).
|
|
Where("sys_origin = ?", sysOrigin).
|
|
Order("region_code asc").
|
|
Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]RegionIMGroupResponse, 0, len(rows))
|
|
for _, row := range rows {
|
|
result = append(result, regionIMGroupResponse(row))
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// GetCurrentRegionIMGroup 返回当前登录用户所在区域的 IM 群组,供 App 加入区域广播群。
|
|
func (s *Service) GetCurrentRegionIMGroup(ctx context.Context, user AuthUser) (*RegionIMGroupResponse, error) {
|
|
sysOrigin := normalizeSysOrigin(user.SysOrigin)
|
|
regionCode, err := s.resolveUserRegion(ctx, sysOrigin, user.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
row, err := s.loadActiveRegionIMGroup(ctx, sysOrigin, regionCode)
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return &RegionIMGroupResponse{
|
|
SysOrigin: sysOrigin,
|
|
RegionCode: regionCode,
|
|
Status: imGroupStatusPending,
|
|
Configured: false,
|
|
}, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp := regionIMGroupResponse(*row)
|
|
resp.Configured = strings.TrimSpace(resp.GroupID) != ""
|
|
return &resp, nil
|
|
}
|
|
|
|
// SaveRegionIMGroup 保存人工录入的区域 IM 群组。
|
|
func (s *Service) SaveRegionIMGroup(ctx context.Context, req RegionIMGroupRequest) (*RegionIMGroupResponse, error) {
|
|
row, err := s.buildRegionIMGroupRow(req, imGroupStatusActive, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if strings.TrimSpace(row.GroupID) == "" {
|
|
row.Status = imGroupStatusPending
|
|
}
|
|
if err := s.upsertRegionIMGroup(ctx, row); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.loadRegionIMGroupResponse(ctx, row.SysOrigin, row.RegionCode)
|
|
}
|
|
|
|
// CreateRegionIMGroup 调用腾讯 IM 创建当前区域通用广播群并落库。
|
|
func (s *Service) CreateRegionIMGroup(ctx context.Context, req RegionIMGroupRequest) (*RegionIMGroupResponse, error) {
|
|
row, err := s.buildRegionIMGroupRow(req, imGroupStatusPending, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
existing, err := s.loadRegionIMGroup(ctx, row.SysOrigin, row.RegionCode)
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
}
|
|
if existing != nil && existing.Status == imGroupStatusActive && strings.TrimSpace(existing.GroupID) != "" && strings.TrimSpace(req.GroupID) == "" {
|
|
resp := regionIMGroupResponse(*existing)
|
|
return &resp, nil
|
|
}
|
|
if s.im == nil {
|
|
return nil, mapTencentIMError(tencentim.ErrNotConfigured)
|
|
}
|
|
|
|
createdGroupID, createErr := s.im.CreateAVChatRoom(ctx, row.GroupID, row.GroupName)
|
|
if createErr != nil {
|
|
row.Status = imGroupStatusFailed
|
|
row.LastError = trimErrorMessage(createErr.Error())
|
|
_ = s.upsertRegionIMGroup(context.Background(), row)
|
|
return nil, mapTencentIMError(createErr)
|
|
}
|
|
row.GroupID = createdGroupID
|
|
row.Status = imGroupStatusActive
|
|
row.LastError = ""
|
|
if err := s.upsertRegionIMGroup(ctx, row); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.loadRegionIMGroupResponse(ctx, row.SysOrigin, row.RegionCode)
|
|
}
|
|
|
|
// SendRegionCustomMessage 向指定区域通用 IM 群发送自定义消息。
|
|
func (s *Service) SendRegionCustomMessage(ctx context.Context, sysOrigin string, regionCode string, body any) (string, error) {
|
|
if s == nil || s.im == nil {
|
|
return "", tencentim.ErrNotConfigured
|
|
}
|
|
group, err := s.loadActiveRegionIMGroup(ctx, sysOrigin, regionCode)
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return "", fmt.Errorf("region im group is not configured sysOrigin=%s regionCode=%s", normalizeSysOrigin(sysOrigin), normalizeRegionCode(regionCode))
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := s.im.SendCustomGroupMessage(ctx, group.GroupID, body); err != nil {
|
|
return "", err
|
|
}
|
|
return group.GroupID, nil
|
|
}
|
|
|
|
func (s *Service) buildRegionIMGroupRow(req RegionIMGroupRequest, status string, lastError string) (model.VoiceRoomRegionIMGroup, error) {
|
|
sysOrigin := normalizeSysOrigin(req.SysOrigin)
|
|
regionCode := normalizeRegionCode(req.RegionCode)
|
|
if regionCode == "" {
|
|
return model.VoiceRoomRegionIMGroup{}, NewAppError(http.StatusBadRequest, "missing_region_code", "regionCode is required")
|
|
}
|
|
groupID := strings.TrimSpace(req.GroupID)
|
|
if groupID == "" {
|
|
groupID = defaultRegionIMGroupID(sysOrigin, regionCode)
|
|
}
|
|
groupName := strings.TrimSpace(req.GroupName)
|
|
if groupName == "" {
|
|
groupName = defaultRegionIMGroupName(sysOrigin, regionCode, req.RegionName)
|
|
}
|
|
id, err := utils.NextID()
|
|
if err != nil {
|
|
return model.VoiceRoomRegionIMGroup{}, err
|
|
}
|
|
now := time.Now()
|
|
return model.VoiceRoomRegionIMGroup{
|
|
ID: id,
|
|
SysOrigin: sysOrigin,
|
|
RegionCode: regionCode,
|
|
RegionName: strings.TrimSpace(req.RegionName),
|
|
GroupID: groupID,
|
|
GroupName: groupName,
|
|
Status: status,
|
|
LastError: trimErrorMessage(lastError),
|
|
CreateTime: now,
|
|
UpdateTime: now,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) upsertRegionIMGroup(ctx context.Context, row model.VoiceRoomRegionIMGroup) error {
|
|
now := time.Now()
|
|
row.UpdateTime = now
|
|
if row.CreateTime.IsZero() {
|
|
row.CreateTime = now
|
|
}
|
|
return s.db.WithContext(ctx).Clauses(clause.OnConflict{
|
|
Columns: []clause.Column{
|
|
{Name: "sys_origin"},
|
|
{Name: "region_code"},
|
|
},
|
|
DoUpdates: clause.Assignments(map[string]any{
|
|
"region_name": row.RegionName,
|
|
"group_id": row.GroupID,
|
|
"group_name": row.GroupName,
|
|
"status": row.Status,
|
|
"last_error": row.LastError,
|
|
"update_time": row.UpdateTime,
|
|
}),
|
|
}).Create(&row).Error
|
|
}
|
|
|
|
func (s *Service) loadRegionIMGroupResponse(ctx context.Context, sysOrigin string, regionCode string) (*RegionIMGroupResponse, error) {
|
|
row, err := s.loadRegionIMGroup(ctx, sysOrigin, regionCode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp := regionIMGroupResponse(*row)
|
|
return &resp, nil
|
|
}
|
|
|
|
func (s *Service) loadRegionIMGroup(ctx context.Context, sysOrigin string, regionCode string) (*model.VoiceRoomRegionIMGroup, error) {
|
|
var row model.VoiceRoomRegionIMGroup
|
|
err := s.db.WithContext(ctx).
|
|
Where("sys_origin = ? AND region_code = ?", normalizeSysOrigin(sysOrigin), normalizeRegionCode(regionCode)).
|
|
First(&row).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &row, nil
|
|
}
|
|
|
|
func (s *Service) loadActiveRegionIMGroup(ctx context.Context, sysOrigin string, regionCode string) (*model.VoiceRoomRegionIMGroup, error) {
|
|
var row model.VoiceRoomRegionIMGroup
|
|
err := s.db.WithContext(ctx).
|
|
Where("sys_origin = ? AND region_code = ? AND status = ? AND group_id <> ?", normalizeSysOrigin(sysOrigin), normalizeRegionCode(regionCode), imGroupStatusActive, "").
|
|
First(&row).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &row, nil
|
|
}
|
|
|
|
func regionIMGroupResponse(row model.VoiceRoomRegionIMGroup) RegionIMGroupResponse {
|
|
return RegionIMGroupResponse{
|
|
ID: row.ID,
|
|
SysOrigin: row.SysOrigin,
|
|
RegionCode: row.RegionCode,
|
|
RegionName: row.RegionName,
|
|
GroupID: row.GroupID,
|
|
GroupName: row.GroupName,
|
|
Status: row.Status,
|
|
LastError: row.LastError,
|
|
CreateTime: formatTime(row.CreateTime),
|
|
UpdateTime: formatTime(row.UpdateTime),
|
|
Configured: row.Status == imGroupStatusActive && strings.TrimSpace(row.GroupID) != "",
|
|
}
|
|
}
|
|
|
|
func normalizeRegionCode(value string) string {
|
|
return strings.ToUpper(strings.TrimSpace(value))
|
|
}
|
|
|
|
func defaultRegionIMGroupID(sysOrigin string, regionCode string) string {
|
|
normalizedSysOrigin := normalizeSysOrigin(sysOrigin)
|
|
normalizedRegionCode := normalizeRegionCode(regionCode)
|
|
groupSysOrigin := sanitizeIMGroupIDPart(normalizedSysOrigin)
|
|
if groupSysOrigin == "" {
|
|
groupSysOrigin = "SYS"
|
|
}
|
|
groupRegionCode := sanitizeIMGroupIDSegment(normalizedRegionCode)
|
|
if groupRegionCode == "" {
|
|
groupRegionCode = "REGION"
|
|
}
|
|
raw := "VRRP_" + groupSysOrigin + "_" + groupRegionCode
|
|
if groupRegionCode != normalizedRegionCode {
|
|
raw += "_" + shortRegionHash(normalizedSysOrigin, normalizedRegionCode)
|
|
}
|
|
if len(raw) <= 48 {
|
|
return raw
|
|
}
|
|
return raw[:39] + "_" + shortRegionHash(normalizedSysOrigin, normalizedRegionCode)
|
|
}
|
|
|
|
func shortRegionHash(sysOrigin string, regionCode string) string {
|
|
sum := sha1.Sum([]byte(normalizeSysOrigin(sysOrigin) + "|" + normalizeRegionCode(regionCode)))
|
|
return strings.ToUpper(hex.EncodeToString(sum[:])[:8])
|
|
}
|
|
|
|
func sanitizeIMGroupIDSegment(value string) string {
|
|
value = strings.ToUpper(strings.TrimSpace(value))
|
|
var builder strings.Builder
|
|
for _, r := range value {
|
|
if (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' {
|
|
builder.WriteRune(r)
|
|
continue
|
|
}
|
|
builder.WriteByte('_')
|
|
}
|
|
return strings.Trim(builder.String(), "_-")
|
|
}
|
|
|
|
func sanitizeIMGroupIDPart(value string) string {
|
|
result := sanitizeIMGroupIDSegment(value)
|
|
if result == "" {
|
|
return "VRRP_GROUP"
|
|
}
|
|
return result
|
|
}
|
|
|
|
func defaultRegionIMGroupName(sysOrigin string, regionCode string, regionName string) string {
|
|
name := strings.TrimSpace(regionName)
|
|
if name == "" {
|
|
name = normalizeRegionCode(regionCode)
|
|
}
|
|
return fmt.Sprintf("region-im-group-%s-%s", normalizeSysOrigin(sysOrigin), name)
|
|
}
|
|
|
|
func mapTencentIMError(err error) *AppError {
|
|
if errors.Is(err, tencentim.ErrNotConfigured) {
|
|
return NewAppError(http.StatusServiceUnavailable, "tencent_im_not_configured", "Tencent IM config is missing")
|
|
}
|
|
return NewAppError(http.StatusBadGateway, "tencent_im_request_failed", err.Error())
|
|
}
|