Files
EchoChat/scripts/deploy-public.sh
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

280 lines
10 KiB
Bash
Executable File
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.

#!/usr/bin/env bash
# ============================================================
# EchoChat 公网部署脚本Phase 2e-2 Task 14
# ------------------------------------------------------------
# 职责:
# 1. 校验 deploy/.env 内关键参数ANNOUNCED_IP / 密码替换等)
# 2. 给出防火墙/安全组端口放通 checklist本地用 nc 探测)
# 3. 启动 docker compose --profile public含 media-server + coturn
#
# 不做的事:
# - 不替你改 iptables / ufw不同云/OS 命令不一,避免误操作)
# - 不替你改 hosts / DNS域名解析请预先完成
#
# 用法:
# cp deploy/.env.public.example deploy/.env
# vim deploy/.env # 按 checklist 替换所有 _REPLACE_WITH_*_
# ./scripts/deploy-public.sh
# ============================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
DEPLOY_DIR="$ROOT_DIR/deploy"
ENV_FILE="$DEPLOY_DIR/.env"
COLOR_GREEN='\033[0;32m'
COLOR_YELLOW='\033[0;33m'
COLOR_RED='\033[0;31m'
COLOR_CYAN='\033[0;36m'
COLOR_RESET='\033[0m'
log_info() { printf "${COLOR_CYAN}[INFO]${COLOR_RESET} %s\n" "$*"; }
log_ok() { printf "${COLOR_GREEN}[OK]${COLOR_RESET} %s\n" "$*"; }
log_warn() { printf "${COLOR_YELLOW}[WARN]${COLOR_RESET} %s\n" "$*"; }
log_err() { printf "${COLOR_RED}[ERR]${COLOR_RESET} %s\n" "$*"; }
# 计数器
ERR_COUNT=0
WARN_COUNT=0
err() { log_err "$*"; ERR_COUNT=$((ERR_COUNT + 1)); }
warn() { log_warn "$*"; WARN_COUNT=$((WARN_COUNT + 1)); }
# ------------------------------------------------------------
# Step 1env 文件校验
# ------------------------------------------------------------
step_validate_env() {
log_info "=== Step 1/4: 校验 deploy/.env ==="
if [[ ! -f "$ENV_FILE" ]]; then
err "deploy/.env 不存在"
log_info "请先执行cp deploy/.env.public.example deploy/.env 并填写关键参数"
return 1
fi
# shellcheck disable=SC1090
set -a
source "$ENV_FILE"
set +a
# DEPLOY_MODE 必须是 public防止误把 local 的 env 复制过来)
if [[ "${DEPLOY_MODE:-}" != "public" ]]; then
warn "deploy/.env 中 DEPLOY_MODE=\"${DEPLOY_MODE:-<unset>}\",不是 public确认你要走公网部署"
fi
# MEDIASOUP_ANNOUNCED_IP 必填
if [[ -z "${MEDIASOUP_ANNOUNCED_IP:-}" ]]; then
err "MEDIASOUP_ANNOUNCED_IP 未设置,远端浏览器将无法建连 mediasoup"
log_info "请填写服务器的公网 IPMEDIASOUP_ANNOUNCED_IP=203.0.113.42"
else
log_ok "MEDIASOUP_ANNOUNCED_IP=${MEDIASOUP_ANNOUNCED_IP}"
fi
# 检查占位符没被替换
local placeholders=(
POSTGRES_PASSWORD
REDIS_PASSWORD
MINIO_ROOT_PASSWORD
JWT_SECRET
MEDIA_INTERNAL_TOKEN
TURN_USERNAME
TURN_PASSWORD
)
for var in "${placeholders[@]}"; do
local val="${!var:-}"
if [[ -z "$val" ]]; then
# REDIS_PASSWORD 允许为空(仅内网访问时)
if [[ "$var" == "REDIS_PASSWORD" ]]; then
warn "$var 为空(仅当 Redis 不对公网暴露时可接受)"
else
err "$var 为空"
fi
elif [[ "$val" == _REPLACE_WITH_* ]]; then
err "$var 仍是占位符(${val}),请替换为真实值"
fi
done
# JWT_SECRET 长度
if [[ -n "${JWT_SECRET:-}" && ${#JWT_SECRET} -lt 32 ]]; then
warn "JWT_SECRET 长度仅 ${#JWT_SECRET} 字符,推荐 >= 64 字符openssl rand -hex 32"
fi
# Task 16 Nit代码审查 2026-04-23REDIS_PASSWORD 与 redis.conf requirepass 联动校验
# 背景REDIS_PASSWORD 只影响 go-service / media-server 的连接侧,
# 若 deploy/docker/redis/redis.conf 未设置 requirepassRedis 本身会接受无密码连接(相当于裸奔)
local redis_conf="$DEPLOY_DIR/docker/redis/redis.conf"
if [[ -f "$redis_conf" ]]; then
local has_requirepass
has_requirepass=$(grep -E '^\s*requirepass\s+\S+' "$redis_conf" || true)
if [[ -n "${REDIS_PASSWORD:-}" ]]; then
if [[ -z "$has_requirepass" ]]; then
err "REDIS_PASSWORD 已设置,但 $redis_conf 未启用 requirepass —— Redis 仍允许空密码登录,公网暴露时务必同步开启"
log_info "建议:在 redis.conf 追加 requirepass \$REDIS_PASSWORD或在 docker-compose command 中传入 --requirepass"
else
log_ok "Redis requirepass 已在 redis.conf 启用,与 REDIS_PASSWORD 联动"
fi
else
if [[ -n "$has_requirepass" ]]; then
warn "redis.conf 启用了 requirepass但 deploy/.env 的 REDIS_PASSWORD 为空 —— go-service/media-server 连接会失败"
fi
fi
else
warn "未找到 $redis_conf,跳过 Redis 密码联动校验"
fi
# TURN 开关一致性
if [[ "${TURN_ENABLED:-false}" != "true" ]]; then
warn "TURN_ENABLED=${TURN_ENABLED:-false},公网部署下建议开启 TURN 作为对称 NAT fallback"
fi
if (( ERR_COUNT > 0 )); then
log_err "env 校验失败:$ERR_COUNT 个错误,$WARN_COUNT 个警告"
return 1
fi
log_ok "env 校验通过($WARN_COUNT 个警告)"
}
# ------------------------------------------------------------
# Step 2防火墙/端口占用检查(本机视角)
# ------------------------------------------------------------
# 说明:云安全组需要在云控制台放通,本脚本只能检测本机 iptables + 当前占用
step_check_ports() {
log_info "=== Step 2/4: 本机端口占用检查 ==="
local ports=(
"8085/tcp|Go backend API"
"3300/tcp|media-server (internal, 不建议暴露公网)"
"40000/udp|mediasoup RTC (端口范围起点)"
"40199/udp|mediasoup RTC (端口范围终点)"
)
if [[ "${TURN_ENABLED:-false}" == "true" ]]; then
ports+=(
"${TURN_LISTEN_PORT:-3478}/udp|coturn STUN/TURN"
"${TURN_LISTEN_PORT:-3478}/tcp|coturn STUN/TURN"
"${TURN_TLS_PORT:-5349}/tcp|coturn TLS"
)
fi
for entry in "${ports[@]}"; do
local port="${entry%%|*}"
local desc="${entry##*|}"
local port_num="${port%/*}"
local proto="${port##*/}"
if [[ "$proto" == "tcp" ]]; then
if lsof -iTCP:"$port_num" -sTCP:LISTEN -n -P >/dev/null 2>&1; then
warn "TCP:$port_num 已被占用(${desc}compose 启动可能失败"
else
log_ok "TCP:$port_num 空闲(${desc}"
fi
else
# UDP 只做提示,不中断
log_ok "UDP:$port_num 待 docker compose 绑定(${desc}"
fi
done
printf "\n${COLOR_YELLOW}⚠️ 云安全组/iptables 放通清单${COLOR_RESET}(本脚本无法自动执行,请手动确认):\n"
printf " TCP:8085 Go backend API\n"
printf " UDP:40000-40199 mediasoup RTC 流量\n"
printf " TCP:40000-40199 mediasoup ICE-TCP fallback\n"
if [[ "${TURN_ENABLED:-false}" == "true" ]]; then
printf " UDP:${TURN_LISTEN_PORT:-3478} coturn STUN/TURN\n"
printf " TCP:${TURN_LISTEN_PORT:-3478} coturn STUN/TURN\n"
printf " TCP:${TURN_TLS_PORT:-5349} coturn TLS\n"
printf " UDP:${TURN_MIN_PORT:-49160}-${TURN_MAX_PORT:-49200} coturn 中继流量\n"
fi
echo ""
}
# ------------------------------------------------------------
# Step 3Docker 环境自检
# ------------------------------------------------------------
step_check_docker() {
log_info "=== Step 3/4: Docker 环境自检 ==="
if ! command -v docker >/dev/null 2>&1; then
err "未检测到 docker 命令"
return 1
fi
log_ok "docker: $(docker --version)"
if ! docker info >/dev/null 2>&1; then
err "docker daemon 无法连接sudo systemctl start docker?"
return 1
fi
log_ok "docker daemon 可访问"
if ! docker compose version >/dev/null 2>&1; then
err "未检测到 docker compose需 Compose V2 plugin"
return 1
fi
log_ok "docker compose: $(docker compose version | head -n1)"
}
# ------------------------------------------------------------
# Step 4启动 public profile
# ------------------------------------------------------------
step_launch() {
log_info "=== Step 4/4: 启动 docker compose (public profile) ==="
if (( ERR_COUNT > 0 )); then
log_err "前面校验有错误,拒绝启动(先修正后再跑脚本)"
return 1
fi
cd "$DEPLOY_DIR"
if [[ "${TURN_ENABLED:-false}" == "true" ]]; then
log_info "TURN_ENABLED=true使用 --profile public 启动(含 coturn"
docker compose -f docker-compose.dev.yml --profile public up -d --build
else
log_info "TURN_ENABLED=false跳过 coturn仅默认服务"
docker compose -f docker-compose.dev.yml up -d --build
fi
log_ok "启动指令已发出,等待 healthcheck最多 180 秒)..."
local waited=0
while (( waited < 180 )); do
local health
health="$(docker inspect -f '{{.State.Health.Status}}' echochat-media-server 2>/dev/null || echo 'starting')"
if [[ "$health" == "healthy" ]]; then
log_ok "media-server healthy$waited 秒)"
break
fi
if [[ "$health" == "unhealthy" ]]; then
log_err "media-server unhealthy查看日志docker logs echochat-media-server"
return 1
fi
sleep 3
waited=$((waited + 3))
done
printf "\n${COLOR_GREEN}=============== 公网部署完成 ===============${COLOR_RESET}\n"
printf " Go 后端 API: http://${MEDIASOUP_ANNOUNCED_IP}:8085\n"
printf " media-server: http://127.0.0.1:3300 (仅内网,勿对外暴露)\n"
if [[ "${TURN_ENABLED:-false}" == "true" ]]; then
printf " coturn TURN: ${MEDIASOUP_ANNOUNCED_IP}:${TURN_LISTEN_PORT:-3478}\n"
fi
printf "\n 查看状态: docker compose -f deploy/docker-compose.dev.yml ps\n"
printf " 查看日志: docker compose -f deploy/docker-compose.dev.yml logs -f [service]\n"
printf " 停止全栈: ./scripts/stop.sh full\n"
printf "${COLOR_GREEN}============================================${COLOR_RESET}\n"
}
main() {
step_validate_env || exit 1
step_check_ports # Step 2不会 return 1只收集 warn
step_check_docker || exit 1 # Step 3
# 二次确认(至少暴露 1 个明显错误时已退出,这里只对警告放行)
if [[ "${1:-}" != "--yes" ]] && (( WARN_COUNT > 0 )); then
printf "\n${COLOR_YELLOW}上述 %d 个警告仅提示,不阻塞启动。继续部署?(y/N): ${COLOR_RESET}" "$WARN_COUNT"
read -r confirm
case "$confirm" in
y|Y|yes|YES) ;;
*) log_info "已取消部署"; exit 0 ;;
esac
fi
step_launch
}
main "$@"