修复区域缓存
This commit is contained in:
parent
fbe9e922f8
commit
dd2f4ea6c5
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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{}
|
||||
|
||||
@ -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"`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user