修复区域缓存

This commit is contained in:
hy001 2026-05-09 18:01:08 +08:00
parent fbe9e922f8
commit dd2f4ea6c5
11 changed files with 302 additions and 21 deletions

View File

@ -53,8 +53,9 @@ func main() {
signInRewardService := signinreward.NewSignInRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
taskCenterService := taskcenter.NewService(app.Config, app.Repository.DB, &app.Gateways)
vipService := vip.NewService(app.Config, app.Repository.DB, &app.Gateways)
regionIMGroupService := regionimgroup.NewService(app.Config, app.Repository.DB)
regionIMGroupService := regionimgroup.NewService(app.Config, app.Repository.DB, &app.Gateways)
voiceRoomRedPacketService := voiceroomredpacket.NewService(app.Config, app.Repository.DB, app.Repository.Redis, app.Gateways.Wallet, regionIMGroupService, app.Gateways.Room)
voiceRoomRedPacketService.SetRegionResolver(&app.Gateways)
weekStarService := weekstar.NewWeekStarService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
roomTurnoverRewardService := roomturnoverreward.NewService(app.Config, app.Repository.DB, &app.Gateways)
hostCenterService := hostcenter.NewService(app.Config, &app.Gateways)

View File

@ -24,8 +24,9 @@ func main() {
registerRewardService := registerreward.NewRegisterRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
weekStarService := weekstar.NewWeekStarService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
regionIMGroupService := regionimgroup.NewService(app.Config, app.Repository.DB)
regionIMGroupService := regionimgroup.NewService(app.Config, app.Repository.DB, &app.Gateways)
voiceRoomRedPacketService := voiceroomredpacket.NewService(app.Config, app.Repository.DB, app.Repository.Redis, app.Gateways.Wallet, regionIMGroupService, app.Gateways.Room)
voiceRoomRedPacketService.SetRegionResolver(&app.Gateways)
workerCtx, workerCancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer workerCancel()

View File

@ -126,6 +126,11 @@ func (g *Gateways) GetUserRegion(ctx context.Context, userID int64, authorizatio
return g.Profile.GetUserRegion(ctx, userID, authorization)
}
// ResolveRegionCodeByCountryCode 透传到 Java 区域配置网关。
func (g *Gateways) ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error) {
return g.Profile.ResolveRegionCodeByCountryCode(ctx, sysOrigin, countryCode)
}
// GetUserProfileByAccount 透传到资料网关。
func (g *Gateways) GetUserProfileByAccount(ctx context.Context, sysOrigin, account string) (UserProfile, error) {
return g.Profile.GetUserProfileByAccount(ctx, sysOrigin, account)
@ -316,6 +321,11 @@ func (g *ProfileGateway) GetUserRegion(ctx context.Context, userID int64, author
return g.client.GetUserRegion(ctx, userID, authorization)
}
// ResolveRegionCodeByCountryCode 根据 Java 区域配置把国家码解析成区域编码。
func (g *ProfileGateway) ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error) {
return g.client.ResolveRegionCodeByCountryCode(ctx, sysOrigin, countryCode)
}
// GetUserProfileByAccount 根据账号查询用户资料。
func (g *ProfileGateway) GetUserProfileByAccount(ctx context.Context, sysOrigin, account string) (UserProfile, error) {
return g.client.GetUserProfileByAccount(ctx, sysOrigin, account)

View File

@ -12,14 +12,17 @@ import (
"net/url"
"strconv"
"strings"
"sync"
"time"
"chatapp3-golang/internal/config"
)
type Client struct {
cfg config.Config
httpClient *http.Client
cfg config.Config
httpClient *http.Client
regionConfigCacheMu sync.Mutex
regionConfigCacheMap map[string]regionConfigCacheEntry
}
type UserCredential struct {
@ -55,6 +58,21 @@ type UserRegion struct {
RegionCode string `json:"regionCode"`
}
type RegionConfig struct {
RegionCode string `json:"regionCode"`
SysOrigin string `json:"sysOrigin"`
RegionName string `json:"regionName"`
CountryCodes string `json:"countryCodes"`
Del bool `json:"del"`
}
type regionConfigCacheEntry struct {
expiresAt time.Time
items []RegionConfig
}
const regionConfigCacheTTL = time.Minute
type RoomContributionActivityCount struct {
ID string `json:"id"`
RoomID Int64Value `json:"roomId"`
@ -370,6 +388,7 @@ func New(cfg config.Config) *Client {
httpClient: &http.Client{
Timeout: cfg.HTTP.Timeout,
},
regionConfigCacheMap: map[string]regionConfigCacheEntry{},
}
}
@ -715,6 +734,75 @@ func (c *Client) GetUserRegion(ctx context.Context, userID int64, authorization
return resp.Body, nil
}
func (c *Client) ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error) {
countryCode = strings.ToUpper(strings.TrimSpace(countryCode))
if countryCode == "" {
return "", nil
}
configs, err := c.ListRegionConfigs(ctx, sysOrigin)
if err != nil {
return "", err
}
for _, item := range configs {
if item.Del {
continue
}
if !regionConfigContainsCountryCode(item.CountryCodes, countryCode) {
continue
}
regionCode := strings.TrimSpace(item.RegionCode)
if regionCode != "" {
return regionCode, nil
}
}
return "", nil
}
func (c *Client) ListRegionConfigs(ctx context.Context, sysOrigin string) ([]RegionConfig, error) {
sysOrigin = strings.ToUpper(strings.TrimSpace(sysOrigin))
if sysOrigin == "" {
sysOrigin = "LIKEI"
}
now := time.Now()
c.regionConfigCacheMu.Lock()
if entry, ok := c.regionConfigCacheMap[sysOrigin]; ok && now.Before(entry.expiresAt) {
items := append([]RegionConfig(nil), entry.items...)
c.regionConfigCacheMu.Unlock()
return items, nil
}
c.regionConfigCacheMu.Unlock()
endpoint := strings.TrimRight(c.cfg.Java.OtherBaseURL, "/") + "/region-config/client/listByCondition"
var resp resultResponse[[]RegionConfig]
if err := c.postJSON(ctx, endpoint, map[string]string{"sysOrigin": sysOrigin}, nil, &resp); err != nil {
return nil, err
}
items := append([]RegionConfig(nil), resp.Body...)
c.regionConfigCacheMu.Lock()
if c.regionConfigCacheMap == nil {
c.regionConfigCacheMap = map[string]regionConfigCacheEntry{}
}
c.regionConfigCacheMap[sysOrigin] = regionConfigCacheEntry{
expiresAt: now.Add(regionConfigCacheTTL),
items: append([]RegionConfig(nil), items...),
}
c.regionConfigCacheMu.Unlock()
return items, nil
}
func regionConfigContainsCountryCode(countryCodes string, countryCode string) bool {
countryCode = strings.ToUpper(strings.TrimSpace(countryCode))
if countryCode == "" {
return false
}
for _, item := range strings.Split(countryCodes, ",") {
if strings.ToUpper(strings.TrimSpace(item)) == countryCode {
return true
}
}
return false
}
func (c *Client) GetUserProfileByAccount(ctx context.Context, sysOrigin, account string) (UserProfile, error) {
endpoint := c.cfg.Java.OtherBaseURL + "/user-profile/client/getByAccount?sysOrigin=" +
url.QueryEscape(strings.TrimSpace(sysOrigin)) + "&account=" + url.QueryEscape(strings.TrimSpace(account))

View File

@ -1,8 +1,14 @@
package integration
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"chatapp3-golang/internal/config"
)
func TestUserTotalRechargeAcceptsNumericTimestamps(t *testing.T) {
@ -29,3 +35,45 @@ func TestUserTotalRechargeAcceptsNumericTimestamps(t *testing.T) {
t.Fatalf("Amount = %q, want 1.60", item.Amount)
}
}
func TestResolveRegionCodeByCountryCodeUsesRegionConfig(t *testing.T) {
calls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
if r.Method != http.MethodPost || r.URL.Path != "/region-config/client/listByCondition" {
t.Fatalf("request = %s %s, want POST /region-config/client/listByCondition", r.Method, r.URL.Path)
}
var payload map[string]string
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request: %v", err)
}
if payload["sysOrigin"] != "LIKEI" {
t.Fatalf("sysOrigin = %q, want LIKEI", payload["sysOrigin"])
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"body":[{"regionCode":"中东区","sysOrigin":"LIKEI","countryCodes":"JO, EG, KSA","del":false},{"regionCode":"US","sysOrigin":"LIKEI","countryCodes":"US","del":true}]}`))
}))
defer server.Close()
client := New(config.Config{
HTTP: config.HTTPConfig{Timeout: time.Second},
Java: config.JavaConfig{OtherBaseURL: server.URL},
})
got, err := client.ResolveRegionCodeByCountryCode(context.Background(), " likei ", " ksa ")
if err != nil {
t.Fatalf("ResolveRegionCodeByCountryCode() error = %v", err)
}
if got != "中东区" {
t.Fatalf("region = %q, want 中东区", got)
}
got, err = client.ResolveRegionCodeByCountryCode(context.Background(), "LIKEI", "eg")
if err != nil {
t.Fatalf("ResolveRegionCodeByCountryCode() cached error = %v", err)
}
if got != "中东区" {
t.Fatalf("cached region = %q, want 中东区", got)
}
if calls != 1 {
t.Fatalf("region config calls = %d, want cached 1", calls)
}
}

View File

@ -25,6 +25,15 @@ func (s *Service) resolveUserRegion(ctx context.Context, sysOrigin string, userI
return "", NewAppError(http.StatusBadRequest, "voice_room_region_missing", "user country code is missing")
}
if s != nil && s.regionResolver != nil {
regionCode, err := s.regionResolver.ResolveRegionCodeByCountryCode(ctx, sysOrigin, countryCode)
if err == nil {
if normalized := normalizeRegionCode(regionCode); normalized != "" {
return normalized, nil
}
}
}
var mapping model.VoiceRoomRegionCountry
err = s.db.WithContext(ctx).
Where("sys_origin = ? AND country_code = ? AND enabled = ?", sysOrigin, countryCode, true).

View File

@ -133,6 +133,42 @@ func TestSendRegionBroadcastResolvesRegionAndEnrichesGiftPayload(t *testing.T) {
}
}
func TestSendRegionBroadcastPrefersJavaRegionResolver(t *testing.T) {
service, db := newRegionIMGroupTestService(t)
service.regionResolver = fakeRegionResolver{
regions: map[string]string{"LIKEI|SA": "中东区"},
}
fakeIM := &fakeIMGateway{}
service.im = fakeIM
now := time.Now()
if err := db.Create(&model.VoiceRoomRegionIMGroup{
ID: 3003,
SysOrigin: "LIKEI",
RegionCode: "中东区",
RegionName: "Middle East",
GroupID: "@region-middle-east",
GroupName: "region-middle-east",
Status: imGroupStatusActive,
CreateTime: now,
UpdateTime: now,
}).Error; err != nil {
t.Fatalf("seed im group: %v", err)
}
resp, err := service.SendRegionBroadcast(context.Background(), RegionBroadcastRequest{
SysOrigin: "LIKEI",
SenderUserID: flexibleInt64(1001),
Type: "SEND_GIFT",
Data: map[string]any{"roomId": "9001"},
})
if err != nil {
t.Fatalf("SendRegionBroadcast() error = %v", err)
}
if resp.RegionCode != "中东区" || resp.GroupID != "@region-middle-east" {
t.Fatalf("SendRegionBroadcast() response = %+v, want Java region group", resp)
}
}
func newRegionIMGroupTestService(t *testing.T) (*Service, *gorm.DB) {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
@ -191,3 +227,16 @@ func (g *fakeIMGateway) SendCustomGroupMessage(_ context.Context, groupID string
g.sent = append(g.sent, fakeIMSendRecord{groupID: groupID, body: body})
return nil
}
type fakeRegionResolver struct {
regions map[string]string
err error
}
func (g fakeRegionResolver) ResolveRegionCodeByCountryCode(_ context.Context, sysOrigin string, countryCode string) (string, error) {
if g.err != nil {
return "", g.err
}
key := strings.ToUpper(strings.TrimSpace(sysOrigin)) + "|" + strings.ToUpper(strings.TrimSpace(countryCode))
return strings.TrimSpace(g.regions[key]), nil
}

View File

@ -36,18 +36,28 @@ type imGateway interface {
SendCustomGroupMessage(ctx context.Context, groupID string, body any) error
}
// Service 负责区域通用 IM 群组创建、查询和区域广播。
type Service struct {
cfg config.Config
db dbHandle
im imGateway
type regionResolver interface {
ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error)
}
func NewService(cfg config.Config, db dbHandle) *Service {
// Service 负责区域通用 IM 群组创建、查询和区域广播。
type Service struct {
cfg config.Config
db dbHandle
im imGateway
regionResolver regionResolver
}
func NewService(cfg config.Config, db dbHandle, resolvers ...regionResolver) *Service {
var resolver regionResolver
if len(resolvers) > 0 {
resolver = resolvers[0]
}
return &Service{
cfg: cfg,
db: db,
im: tencentim.NewClient(cfg.TencentIM),
cfg: cfg,
db: db,
im: tencentim.NewClient(cfg.TencentIM),
regionResolver: resolver,
}
}

View File

@ -108,6 +108,15 @@ func (s *Service) resolveUserRegion(ctx context.Context, sysOrigin string, userI
return "", NewAppError(http.StatusBadRequest, "voice_room_region_missing", "user country code is missing")
}
if s != nil && s.regionResolver != nil {
regionCode, err := s.regionResolver.ResolveRegionCodeByCountryCode(ctx, sysOrigin, countryCode)
if err == nil {
if normalized := strings.ToUpper(strings.TrimSpace(regionCode)); normalized != "" {
return normalized, nil
}
}
}
var mapping model.VoiceRoomRegionCountry
err = s.db.WithContext(ctx).
Where("sys_origin = ? AND country_code = ? AND enabled = ?", sysOrigin, countryCode, true).

View File

@ -176,6 +176,38 @@ func TestDelayedPacketSendsRegionIMBroadcastOnly(t *testing.T) {
}
}
func TestResolveUserRegionPrefersJavaRegionResolver(t *testing.T) {
service, _, _ := newVoiceRoomRedPacketTestService(t)
service.SetRegionResolver(fakeRegionResolver{
regions: map[string]string{"LIKEI|SA": "中东区"},
})
fakeRegionIM := &fakeRegionIMGateway{}
service.regionIM = fakeRegionIM
ctx := context.Background()
sender := AuthUser{UserID: 1001, SysOrigin: "LIKEI"}
presence, err := service.Heartbeat(ctx, sender, PresenceRequest{RoomID: 9001})
if err != nil {
t.Fatalf("Heartbeat() error = %v", err)
}
if presence.RegionCode != "中东区" {
t.Fatalf("presence region = %q, want Java region", presence.RegionCode)
}
_, err = service.Send(ctx, sender, SendRequest{
RequestID: "req-delayed-java-region",
RoomID: 9001,
PacketMode: packetModeDelayed,
TotalAmount: 15000,
TotalCount: 5,
})
if err != nil {
t.Fatalf("Send() error = %v", err)
}
if len(fakeRegionIM.sent) != 1 || fakeRegionIM.sent[0].regionCode != "中东区" {
t.Fatalf("sent region messages = %+v, want one Java-region message", fakeRegionIM.sent)
}
}
func TestImmediatePacketSendsRoomIMMessage(t *testing.T) {
service, _, _ := newVoiceRoomRedPacketTestService(t)
fakeIM := &fakeIMGateway{}
@ -410,6 +442,19 @@ func (g *fakeRegionIMGateway) SendRegionCustomMessage(_ context.Context, sysOrig
return "@region-" + strings.ToLower(regionCode), nil
}
type fakeRegionResolver struct {
regions map[string]string
err error
}
func (g fakeRegionResolver) ResolveRegionCodeByCountryCode(_ context.Context, sysOrigin string, countryCode string) (string, error) {
if g.err != nil {
return "", g.err
}
key := strings.ToUpper(strings.TrimSpace(sysOrigin)) + "|" + strings.ToUpper(strings.TrimSpace(countryCode))
return strings.TrimSpace(g.regions[key]), nil
}
type fakeWalletGateway struct {
balance map[int64]int64
events map[string]struct{}

View File

@ -96,16 +96,21 @@ type roomProfileGateway interface {
MapRoomProfiles(ctx context.Context, roomIDs []int64) (map[int64]integration.RoomProfile, error)
}
type regionResolver interface {
ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error)
}
// Service 承接语音房红包配置、发送、领取、在线态和退款补偿。
type Service struct {
cfg config.Config
db voiceRoomRedPacketDB
redis redis.Cmdable
wallet walletGateway
im imGateway
regionIM regionIMGateway
roomProfiles roomProfileGateway
notifier *redisNotifier
cfg config.Config
db voiceRoomRedPacketDB
redis redis.Cmdable
wallet walletGateway
im imGateway
regionIM regionIMGateway
roomProfiles roomProfileGateway
regionResolver regionResolver
notifier *redisNotifier
}
func NewService(cfg config.Config, db voiceRoomRedPacketDB, cache redis.Cmdable, wallet walletGateway, regionIM regionIMGateway, roomProfiles ...roomProfileGateway) *Service {
@ -124,6 +129,12 @@ func NewService(cfg config.Config, db voiceRoomRedPacketDB, cache redis.Cmdable,
return service
}
func (s *Service) SetRegionResolver(resolver regionResolver) {
if s != nil {
s.regionResolver = resolver
}
}
type ConfigResponse struct {
Configured bool `json:"configured"`
ID int64 `json:"id,string"`