fix game room rps im config

This commit is contained in:
zhx 2026-06-11 19:31:39 +08:00
parent cc84e14860
commit 8628de3dbd
7 changed files with 178 additions and 55 deletions

View File

@ -8,6 +8,16 @@ wallet_service_addr: "wallet-service:13004"
user_service_addr: "user-service:13005"
activity_service_addr: "activity-service:13006"
launch_session_ttl: "15m"
tencent_im:
# Docker/testbox 联调房内猜拳 PK 房间群消息;必须和 gateway/room 使用同一个 IM 应用。
enabled: true
sdk_app_id: 20036101
secret_key: "dcdf53135f27e7cf8541c4ae9798fb0cb2f0e4d6c5c20022dbaea053f35cbb8c"
admin_identifier: "900100100"
admin_user_sig_ttl: "24h"
endpoint: "adminapisgp.im.qcloud.com"
group_type: "ChatRoom"
request_timeout: "5s"
rocketmq:
enabled: true
name_servers: ["rocketmq-namesrv:9876"]

View File

@ -8,6 +8,16 @@ wallet_service_addr: "wallet-service:13004"
user_service_addr: "user-service:13005"
activity_service_addr: "10.2.1.16:13006"
launch_session_ttl: "15m"
tencent_im:
# 房内猜拳发起、应战、揭晓和结算事件都走腾讯 IM 房间群自定义消息。
enabled: true
sdk_app_id: 1400000000
secret_key: "TENCENT_IM_SECRET_KEY"
admin_identifier: "TENCENT_IM_ADMIN_IDENTIFIER"
admin_user_sig_ttl: "24h"
endpoint: "adminapisgp.im.qcloud.com"
group_type: "ChatRoom"
request_timeout: "5s"
rocketmq:
enabled: true
name_servers: []

View File

@ -8,6 +8,16 @@ wallet_service_addr: "127.0.0.1:13004"
user_service_addr: "127.0.0.1:13005"
activity_service_addr: "127.0.0.1:13006"
launch_session_ttl: "15m"
tencent_im:
# 本地联调房内猜拳 PK 房间群消息;必须和 gateway/room 使用同一个 IM 应用。
enabled: true
sdk_app_id: 20036101
secret_key: "dcdf53135f27e7cf8541c4ae9798fb0cb2f0e4d6c5c20022dbaea053f35cbb8c"
admin_identifier: "900100100"
admin_user_sig_ttl: "24h"
endpoint: "adminapisgp.im.qcloud.com"
group_type: "ChatRoom"
request_timeout: "5s"
rocketmq:
enabled: true
name_servers: ["127.0.0.1:19876"]

View File

@ -98,14 +98,21 @@ func New(cfg config.Config) (*App, error) {
svc := gameservice.New(gameservice.Config{LaunchSessionTTL: cfg.LaunchSessionTTL}, repo, walletClient, userClient, client.NewActivityGrowthClient(activityConn))
// 骰子服务复用同一个 MySQL repository 和 wallet client局事实走 dice 表,金币事实仍走 game_orders + wallet-service。
diceSvc := diceservice.New(diceservice.Config{}, repo, walletClient)
roomRPSPublisher, err := roomrpsservice.NewTencentIMPublisherFromEnv()
if err != nil {
_ = listener.Close()
_ = activityConn.Close()
_ = userConn.Close()
_ = walletConn.Close()
_ = repo.Close()
return nil, err
var roomRPSPublisher roomrpsservice.Publisher
if cfg.TencentIM.Enabled {
// 房内 RPS 的“发起 PK”需要同步投递腾讯 IM 房间群消息;启动阶段先创建 REST client避免运行中才发现配置缺失。
roomRPSPublisher, err = roomrpsservice.NewTencentIMPublisher(cfg.TencentIM.RESTConfig())
if err != nil {
_ = listener.Close()
_ = activityConn.Close()
_ = userConn.Close()
_ = walletConn.Close()
_ = repo.Close()
return nil, err
}
} else {
// 显式关闭时保留 HTTP/RPC 功能,便于纯本地测试;测试服和线上必须在 YAML 中打开,否则用户看不到房内 PK。
logx.Warn(context.Background(), "room_rps_im_disabled", slog.String("reason", "tencent_im.enabled=false"))
}
// 房内猜拳独立于骰子和独立猜拳:配置、挑战状态机和 IM 事件全部由 room-rps service 承接transport 只做 RPC 适配。
roomRPSSvc := roomrpsservice.New(roomrpsservice.Config{}, userClient, roomRPSPublisher)

View File

@ -3,11 +3,13 @@ package config
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"hyapp/pkg/configx"
"hyapp/pkg/logx"
"hyapp/pkg/tencentim"
)
// Config 描述 game-service 启动所需的最小依赖。
@ -17,15 +19,58 @@ type Config struct {
Environment string `yaml:"environment"`
GRPCAddr string `yaml:"grpc_addr"`
// HealthHTTPAddr 是给 CLB/发布脚本探活使用的 HTTP 监听地址,不承载业务流量。
HealthHTTPAddr string `yaml:"health_http_addr"`
MySQLDSN string `yaml:"mysql_dsn"`
WalletServiceAddr string `yaml:"wallet_service_addr"`
UserServiceAddr string `yaml:"user_service_addr"`
ActivityServiceAddr string `yaml:"activity_service_addr"`
LaunchSessionTTL time.Duration `yaml:"launch_session_ttl"`
RocketMQ RocketMQConfig `yaml:"rocketmq"`
OutboxWorker OutboxWorkerConfig `yaml:"outbox_worker"`
Log logx.Config `yaml:"log"`
HealthHTTPAddr string `yaml:"health_http_addr"`
MySQLDSN string `yaml:"mysql_dsn"`
WalletServiceAddr string `yaml:"wallet_service_addr"`
UserServiceAddr string `yaml:"user_service_addr"`
ActivityServiceAddr string `yaml:"activity_service_addr"`
LaunchSessionTTL time.Duration `yaml:"launch_session_ttl"`
// TencentIM 是房内猜拳向房间群投递 PK 事件的腾讯 IM REST 配置;缺配置时必须显式关闭,不能靠环境变量隐式漂移。
TencentIM TencentIMConfig `yaml:"tencent_im"`
RocketMQ RocketMQConfig `yaml:"rocketmq"`
OutboxWorker OutboxWorkerConfig `yaml:"outbox_worker"`
Log logx.Config `yaml:"log"`
}
// TencentIMConfig 描述 game-service 发房内 RPS 群自定义消息需要的服务端 IM 配置。
type TencentIMConfig struct {
// Enabled 控制房内 RPS 是否向腾讯 IM 房间群投递事件;开启后缺少密钥或管理员账号应启动失败。
Enabled bool `yaml:"enabled"`
// SDKAppID 是腾讯云 IM 应用 ID必须和 gateway 签发 UserSig 使用同一个应用。
SDKAppID int64 `yaml:"sdk_app_id"`
// SecretKey 只用于服务端签 admin UserSig不能出现在响应或业务日志里。
SecretKey string `yaml:"secret_key"`
// AdminIdentifier 是服务端 REST 发房间 PK 消息的管理员账号。
AdminIdentifier string `yaml:"admin_identifier"`
// AdminUserSigTTL 控制 REST 调用签名有效期,默认 24h。
AdminUserSigTTL time.Duration `yaml:"admin_user_sig_ttl"`
// Endpoint 是腾讯 IM REST 地域入口,例如 adminapisgp.im.qcloud.com。
Endpoint string `yaml:"endpoint"`
// GroupType 是房间群类型;房内 RPS 复用房间群,默认 ChatRoom。
GroupType string `yaml:"group_type"`
// RequestTimeout 是单次腾讯 IM REST 调用预算,避免创建 PK 时无限等待外部服务。
RequestTimeout time.Duration `yaml:"request_timeout"`
}
// RESTConfig 收敛为共享腾讯 IM client 配置,调用方不用重复处理默认超时和群类型。
func (c TencentIMConfig) RESTConfig() tencentim.RESTConfig {
timeout := c.RequestTimeout
if timeout <= 0 {
timeout = 5 * time.Second
}
ttl := c.AdminUserSigTTL
if ttl <= 0 {
ttl = 24 * time.Hour
}
return tencentim.RESTConfig{
SDKAppID: c.SDKAppID,
SecretKey: c.SecretKey,
AdminIdentifier: c.AdminIdentifier,
AdminUserSigTTL: ttl,
Endpoint: c.Endpoint,
GroupType: c.GroupType,
HTTPClient: &http.Client{Timeout: timeout},
}
}
type RocketMQConfig struct {
@ -68,7 +113,14 @@ func Default() Config {
UserServiceAddr: "127.0.0.1:13005",
ActivityServiceAddr: "127.0.0.1:13006",
LaunchSessionTTL: 15 * time.Minute,
RocketMQ: defaultRocketMQConfig(),
TencentIM: TencentIMConfig{
AdminIdentifier: "administrator",
AdminUserSigTTL: 24 * time.Hour,
Endpoint: tencentim.DefaultEndpoint,
GroupType: tencentim.DefaultGroupType,
RequestTimeout: 5 * time.Second,
},
RocketMQ: defaultRocketMQConfig(),
OutboxWorker: OutboxWorkerConfig{
Enabled: false,
PollInterval: time.Second,
@ -138,6 +190,10 @@ func (cfg *Config) Normalize() error {
if cfg.LaunchSessionTTL <= 0 {
cfg.LaunchSessionTTL = 15 * time.Minute
}
cfg.normalizeTencentIM()
if cfg.TencentIM.Enabled && (cfg.TencentIM.SDKAppID <= 0 || cfg.TencentIM.SecretKey == "" || cfg.TencentIM.AdminIdentifier == "") {
return errors.New("tencent_im enabled requires sdk_app_id, secret_key and admin_identifier")
}
cfg.RocketMQ = normalizeRocketMQConfig(cfg.RocketMQ)
if cfg.OutboxWorker.PollInterval <= 0 {
cfg.OutboxWorker.PollInterval = time.Second
@ -162,6 +218,28 @@ func (cfg *Config) Normalize() error {
return nil
}
func (cfg *Config) normalizeTencentIM() {
cfg.TencentIM.SecretKey = strings.TrimSpace(cfg.TencentIM.SecretKey)
cfg.TencentIM.AdminIdentifier = strings.TrimSpace(cfg.TencentIM.AdminIdentifier)
if cfg.TencentIM.AdminIdentifier == "" {
cfg.TencentIM.AdminIdentifier = "administrator"
}
cfg.TencentIM.Endpoint = strings.TrimSpace(cfg.TencentIM.Endpoint)
if cfg.TencentIM.Endpoint == "" {
cfg.TencentIM.Endpoint = tencentim.DefaultEndpoint
}
cfg.TencentIM.GroupType = strings.TrimSpace(cfg.TencentIM.GroupType)
if cfg.TencentIM.GroupType == "" {
cfg.TencentIM.GroupType = tencentim.DefaultGroupType
}
if cfg.TencentIM.AdminUserSigTTL <= 0 {
cfg.TencentIM.AdminUserSigTTL = 24 * time.Hour
}
if cfg.TencentIM.RequestTimeout <= 0 {
cfg.TencentIM.RequestTimeout = 5 * time.Second
}
}
func defaultRocketMQConfig() RocketMQConfig {
return RocketMQConfig{
Enabled: false,

View File

@ -25,3 +25,44 @@ func TestLoadConfigsKeepActivityServiceAddr(t *testing.T) {
})
}
}
func TestLoadConfigsEnableRoomRPSIM(t *testing.T) {
tests := []struct {
name string
path string
}{
{name: "local", path: "../../configs/config.yaml"},
{name: "docker", path: "../../configs/config.docker.yaml"},
{name: "tencent", path: "../../configs/config.tencent.example.yaml"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg, err := Load(tt.path)
if err != nil {
t.Fatalf("Load %s config failed: %v", tt.name, err)
}
if !cfg.TencentIM.Enabled {
t.Fatalf("room-rps tencent_im must be enabled in %s config", tt.name)
}
if cfg.TencentIM.SDKAppID <= 0 || cfg.TencentIM.SecretKey == "" || cfg.TencentIM.AdminIdentifier == "" {
t.Fatalf("room-rps tencent_im config is incomplete: sdk_app_id_set=%t secret_key_set=%t admin_identifier_set=%t", cfg.TencentIM.SDKAppID > 0, cfg.TencentIM.SecretKey != "", cfg.TencentIM.AdminIdentifier != "")
}
if cfg.TencentIM.Endpoint != "adminapisgp.im.qcloud.com" {
t.Fatalf("room-rps tencent_im endpoint mismatch: %q", cfg.TencentIM.Endpoint)
}
})
}
}
func TestNormalizeRejectsEnabledIncompleteTencentIM(t *testing.T) {
cfg := Default()
cfg.TencentIM.Enabled = true
cfg.TencentIM.SDKAppID = 0
cfg.TencentIM.SecretKey = ""
cfg.TencentIM.AdminIdentifier = ""
if err := cfg.Normalize(); err == nil {
t.Fatal("Normalize should reject enabled incomplete tencent_im config")
}
}

View File

@ -6,8 +6,6 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"os"
"sort"
"strconv"
"strings"
@ -101,33 +99,10 @@ func New(config Config, user UserClient, publisher Publisher) *Service {
}
}
// NewTencentIMPublisherFromEnv 按环境变量创建真实腾讯 IM publisher。
// 密钥、账号和地域入口都属于敏感或部署差异配置,因此这里只读取环境,不在 game-service 默认配置里伪造。
func NewTencentIMPublisherFromEnv() (Publisher, error) {
sdkAppID, _ := strconv.ParseInt(strings.TrimSpace(os.Getenv("TENCENT_IM_SDK_APP_ID")), 10, 64)
secretKey := strings.TrimSpace(os.Getenv("TENCENT_IM_SECRET_KEY"))
adminIdentifier := strings.TrimSpace(os.Getenv("TENCENT_IM_ADMIN_IDENTIFIER"))
if sdkAppID <= 0 || secretKey == "" || adminIdentifier == "" {
// 未配置 IM 时不阻断本地服务启动;真实 IM smoke 会显式带环境变量,并要求事件投递成功。
return nil, nil
}
ttl := time.Hour
if raw := strings.TrimSpace(os.Getenv("TENCENT_IM_ADMIN_USERSIG_TTL")); raw != "" {
parsed, err := time.ParseDuration(raw)
if err != nil || parsed <= 0 {
return nil, fmt.Errorf("TENCENT_IM_ADMIN_USERSIG_TTL is invalid")
}
ttl = parsed
}
return tencentim.NewRESTClient(tencentim.RESTConfig{
SDKAppID: sdkAppID,
SecretKey: secretKey,
AdminIdentifier: adminIdentifier,
AdminUserSigTTL: ttl,
Endpoint: envOr("TENCENT_IM_ENDPOINT", tencentim.DefaultEndpoint),
GroupType: tencentim.DefaultGroupType,
HTTPClient: &http.Client{Timeout: 10 * time.Second},
})
// NewTencentIMPublisher 创建真实腾讯 IM publisher。
// game-service 的房内 PK 发起接口是同步返回给客户端的入口,因此 IM 配置必须来自已校验的 YAML而不是进程环境变量的隐式副作用。
func NewTencentIMPublisher(config tencentim.RESTConfig) (Publisher, error) {
return tencentim.NewRESTClient(config)
}
func (s *Service) GetConfig(ctx context.Context, app string) (*gamev1.RoomRPSConfig, int64, error) {
@ -768,11 +743,3 @@ func newChallengeID(app string) string {
}
return fmt.Sprintf("rps_%s_%d_%s", appcode.Normalize(app), time.Now().UnixMilli(), hex.EncodeToString(entropy[:]))
}
func envOr(key string, fallback string) string {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
return value
}