Files
EchoChat/backend/go-service/config/config.go
bujinyuan c35097a0d8 fix(meeting): Task 16 修复 code-reviewer 审计 P2 七项 + Nit 七项
覆盖 Phase 2e-2 代码审查报告(docs/reviews/2026-04-23-phase2e-2-code-review.md)
P2/Nit 收尾批次,均在本仓库完成闭环;剩余 5 项登记推迟至 Phase 2f/3。

===== P2 七项 =====
- P2-1 cleanupUserResources 补 transport 清理
  · media-server 新增 DELETE /internal/v1/transports/:id + transport.service.closeTransport
  · Go MediaOrchestrator 接口新增 CloseTransport;HTTP 实现按 doCloseRequest 走 4xx 幂等 + 指数退避
  · meeting_signal_service.cleanupUserResources 新增 "transport:<id>" 分支
- P2-2 preview.vue 快速切设备竞态
  · previewSeq 序号 + nextTick 后 stale 判断,丢弃过期结果
  · onVideoChange/onAudioChange 走 scheduleRestartPreview 200ms 防抖
  · onBeforeUnmount 清理 changeDebounceTimer
- P2-3 room.vue onLoad redirectTo 后补 return
  · 引入 redirectingToJoin 守卫,跳转页不再执行 onMounted 初始化
- P2-6 generateUniqueRoomCode 重试上限监控
  · 重试后成功:Warn 日志(码空间健康度告警)
  · 重试耗尽:Error 日志 + ErrRoomCodeConflict
  · 修正 logs.Error 调用签名(去掉多余的 nil)
- P2-7 SendChatMessage 服务端长度 + 频率限制
  · 新增 ErrChatContentEmpty / ErrChatContentTooLong / ErrChatRateLimited
  · utf8.RuneCountInString 校验 500 字符上限
  · Redis INCR + EXPIRE 滑动窗口(30 条/60s,首次写入 EXPIRE 兜底)
  · controller.handleError 映射为 HTTP 400
- P2-8 MEETING_ENDED_REASON_LABEL 覆盖复核
  · 新增前端专属常量 MEETING_ENDED_REASON_KICKED + 文案
  · store/meeting.js _onMemberKicked 使用常量
  · 同步修复后端 OnWSDisconnect 硬编码 "ws_disconnect" → constants.MeetingLeftReasonDisconnect
- P2 已修 P2-1/2/3/6/7/8;P2-4(WS token 迁出 URL query)与 P2-5(Chat 服务拆分)登记推迟

===== Nit 七项 =====
- Nit: kind:id 解析改用 strings.SplitN
  · cleanupUserResources / pushExistingRoomState 两处同步
- Nit: resourceTTL 中央化
  · 新增 constants.MeetingResourceTrackTTLSeconds(3600)
  · meeting_signal_service.go resourceTTL 由 const 改 var 并引用常量
- Nit: ws/handler.go CheckOrigin 白名单
  · NewHandler 新增 serverCfg 依赖;checkOrigin 支持同源放行 / dev 模式放行 / release 模式白名单严格匹配
  · config.ServerConfig 新增 WSAllowedOrigins(逗号分隔)+ AllowedOrigins() / IsRelease() 辅助方法
  · provider.go 新增 provideServerConfig,wire_gen.go 同步
- Nit: http_media_orchestrator.go 超时 + CreateRouter 重试
  · 默认 TimeoutMS 5000→10000ms 兼容 Worker 冷启动
  · 新增 CreateRouterRetry(默认 1 次,300ms 退避),仅对非 404 错误重试
  · config.dev.yaml / config.docker.yaml 同步写入显式配置
- Nit: deploy-public.sh REDIS_PASSWORD × redis.conf 联动校验
  · 检测 REDIS_PASSWORD 与 redis.conf 的 requirepass 配对一致性
  · redis.conf 增加公网部署 requirepass 使用说明
- Nit: media-server internal-auth isPrivatePath 按 path 匹配
  · 剔除 query/hash 后再与白名单 startsWith,避免 "?" 语义混淆
- Nit: mediasoup-client.js in-flight 锁走读确认
  · finally 分支已覆盖 resolve/reject 两路,追加注释强化语义
- Nit 走读复核:_onMemberLeft 整槽关闭 vs _onProducerNew(closed=true) 精确匹配 producerId
  · 粒度正确,无需改动(登记结论)

===== 构建验证 =====
- go vet ./... / go build ./... 通过
- frontend npm run build:h5 通过(仅 uni-app legacy warning,无 error)
- media-server npx tsc --noEmit 通过

===== 审查追踪小节 =====
docs/reviews/2026-04-23-phase2e-2-code-review.md 追加 "Task 16 修复追踪(2026-04-24 更新)":
- 已修复一览(本批次 14 处 + 历次 commit cdaa39d / ea2bf96 / f5ae095 / 5ed14c2)
- 推迟登记表(P2-4 / P2-5 / 端口收敛 / appData 校验 / RFC3339 时间格式,共 5 项)

Made-with: Cursor
2026-04-23 17:45:03 +08:00

168 lines
6.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package config 提供应用配置管理功能
// 使用 Viper 读取 YAML 配置文件,支持环境变量覆盖
package config
import (
"fmt"
"strings"
"github.com/spf13/viper"
)
// Config 应用全局配置结构体
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
JWT JWTConfig `mapstructure:"jwt"`
Log LogConfig `mapstructure:"log"`
Minio MinioConfig `mapstructure:"minio"`
MediaServer MediaServerConfig `mapstructure:"media_server"`
Meeting MeetingConfig `mapstructure:"meeting"`
}
// MeetingConfig 会议模块生命周期参数Phase 2e-2 Task 8
// 所有字段均允许通过环境变量 ECHOCHAT_MEETING_* 覆盖,便于 E2E 测试以短时长加速场景触发
type MeetingConfig struct {
HostGraceSeconds int `mapstructure:"host_grace_seconds"` // host 掉线宽限期秒数,默认 120
EmptyRoomTTLSeconds int `mapstructure:"empty_room_ttl_seconds"` // 空房自动销毁 TTL 秒数,默认 300
CleanupIntervalSeconds int `mapstructure:"cleanup_interval_seconds"` // 兜底扫描周期秒数,默认 30
StaleRoomHours int `mapstructure:"stale_room_hours"` // 活跃超过此小时且无成员视为 stale默认 4
}
// MediaServerConfig Node media-server 接入配置Phase 2e-2 Task 7
// 与 media-server/.env 中的 MEDIA_INTERNAL_TOKEN / HTTP_PORT 成对使用
// BaseURL 需精确到协议与端口http://host:port不含末尾斜杠
type MediaServerConfig struct {
BaseURL string `mapstructure:"base_url"` // 如 http://localhost:3300
InternalToken string `mapstructure:"internal_token"` // 与 Node 共享密钥
TimeoutMS int `mapstructure:"timeout_ms"` // 创建类接口超时(毫秒),默认 10000
CloseTimeoutMS int `mapstructure:"close_timeout_ms"` // 关闭类接口超时(毫秒),默认 2000
CloseRetry int `mapstructure:"close_retry"` // 关闭类接口失败重试次数,默认 2
CreateRouterRetry int `mapstructure:"create_router_retry"` // Task 16 NitCreateRouter 5xx/网络错误时的重试次数,默认 1
}
// MinioConfig MinIO 对象存储配置
type MinioConfig struct {
Endpoint string `mapstructure:"endpoint"` // MinIO 服务地址host:port
AccessKey string `mapstructure:"access_key"` // 访问密钥
SecretKey string `mapstructure:"secret_key"` // 密钥
Bucket string `mapstructure:"bucket"` // 存储桶名称
UseSSL bool `mapstructure:"use_ssl"` // 是否使用 HTTPS
}
// ServerConfig HTTP 服务配置
type ServerConfig struct {
Port int `mapstructure:"port"` // 监听端口
Mode string `mapstructure:"mode"` // 运行模式: debug/release
// Task 16 NitWebSocket 升级握手 Origin 白名单(逗号分隔)
// - 空串 → dev 模式mode != release放行全部release 模式强制拒绝所有跨源(仅同源可建连)
// - 配置示例:"https://app.example.com,http://localhost:5173"
// - 环境变量覆盖ECHOCHAT_SERVER_WS_ALLOWED_ORIGINS="https://a.com,https://b.com"
WSAllowedOrigins string `mapstructure:"ws_allowed_origins"`
}
// AllowedOrigins 将逗号分隔的 WSAllowedOrigins 解析为 slice已去空并 trim
func (s *ServerConfig) AllowedOrigins() []string {
if s.WSAllowedOrigins == "" {
return nil
}
raw := strings.Split(s.WSAllowedOrigins, ",")
out := make([]string, 0, len(raw))
for _, o := range raw {
if v := strings.TrimSpace(o); v != "" {
out = append(out, v)
}
}
return out
}
// IsRelease 判断是否为生产运行模式
func (s *ServerConfig) IsRelease() bool {
return strings.EqualFold(s.Mode, "release")
}
// DatabaseConfig PostgreSQL 数据库配置
type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DBName string `mapstructure:"dbname"`
SSLMode string `mapstructure:"sslmode"`
MaxIdleConns int `mapstructure:"max_idle_conns"` // 最大空闲连接数
MaxOpenConns int `mapstructure:"max_open_conns"` // 最大打开连接数
}
// DSN 生成 PostgreSQL 连接字符串
func (d *DatabaseConfig) DSN() string {
return fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
d.Host, d.Port, d.User, d.Password, d.DBName, d.SSLMode,
)
}
// RedisConfig Redis 配置
type RedisConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"` // 数据库编号
}
// Addr 生成 Redis 连接地址
func (r *RedisConfig) Addr() string {
return fmt.Sprintf("%s:%d", r.Host, r.Port)
}
// JWTConfig JWT 认证配置
type JWTConfig struct {
Secret string `mapstructure:"secret"` // 签名密钥
AccessExpireMin int `mapstructure:"access_expire_min"` // Access Token 有效期(分钟)
RefreshExpireDay int `mapstructure:"refresh_expire_day"` // Refresh Token 有效期(天)
Issuer string `mapstructure:"issuer"` // 签发者
}
// LogConfig 日志配置
type LogConfig struct {
Level string `mapstructure:"level"` // debug/info/warn/error
Format string `mapstructure:"format"` // text(开发)/json(生产)
OutputPath string `mapstructure:"output_path"` // stdout 或文件路径
File LogFileConfig `mapstructure:"file"` // 日志文件轮转配置
}
// LogFileConfig 日志文件轮转配置(基于 lumberjack
type LogFileConfig struct {
Enable bool `mapstructure:"enable"` // 是否启用文件日志
Dir string `mapstructure:"dir"` // 日志文件目录
MaxSize int `mapstructure:"max_size"` // 单个文件最大大小MB超过后自动切割
MaxBackups int `mapstructure:"max_backups"` // 保留的旧日志文件最大数量
MaxAge int `mapstructure:"max_age"` // 旧日志文件保留天数
Compress bool `mapstructure:"compress"` // 是否压缩归档的旧日志文件
}
// Load 加载配置文件并返回 Config 实例
// configPath 为配置文件所在目录configName 为文件名(不含扩展名)
func Load(configPath, configName string) (*Config, error) {
v := viper.New()
v.SetConfigName(configName)
v.SetConfigType("yaml")
v.AddConfigPath(configPath)
// 环境变量覆盖ECHOCHAT_SERVER_PORT → server.port
v.SetEnvPrefix("ECHOCHAT")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
if err := v.ReadInConfig(); err != nil {
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("解析配置文件失败: %w", err)
}
return &cfg, nil
}