From c7c3c02bc3fb71e03d846010efc7ca5cf8fed325 Mon Sep 17 00:00:00 2001 From: vet Date: Mon, 13 Apr 2026 01:27:34 +0700 Subject: [PATCH] init --- 01-init-env.sh | 106 +++++++++ 02-patch-config.sh | 483 +++++++++++++++++++++++++++++++++++++++++ 03-start-infra.sh | 227 +++++++++++++++++++ 04-build.sh | 104 +++++++++ 05-start.sh | 138 ++++++++++++ 06-install-frontend.sh | 95 ++++++++ 07-start-frontend.sh | 196 +++++++++++++++++ README.md | 280 ++++++++++++++++++++++++ check-conn.sh | 180 +++++++++++++++ common.sh | 225 +++++++++++++++++++ logs.sh | 205 +++++++++++++++++ remove-infra.sh | 42 ++++ restart.sh | 125 +++++++++++ setup.sh | 87 ++++++++ status.sh | 90 ++++++++ stop-frontend.sh | 54 +++++ stop-infra.sh | 51 +++++ stop.sh | 42 ++++ 18 files changed, 2730 insertions(+) create mode 100755 01-init-env.sh create mode 100755 02-patch-config.sh create mode 100755 03-start-infra.sh create mode 100755 04-build.sh create mode 100755 05-start.sh create mode 100755 06-install-frontend.sh create mode 100755 07-start-frontend.sh create mode 100644 README.md create mode 100755 check-conn.sh create mode 100755 common.sh create mode 100755 logs.sh create mode 100755 remove-infra.sh create mode 100755 restart.sh create mode 100755 setup.sh create mode 100755 status.sh create mode 100755 stop-frontend.sh create mode 100755 stop-infra.sh create mode 100755 stop.sh diff --git a/01-init-env.sh b/01-init-env.sh new file mode 100755 index 0000000..8fac632 --- /dev/null +++ b/01-init-env.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# ============================================================================= +# 01-init-env.sh — 生成 .env.deploy-test 配置模板(测试服务器环境) +# +# 作用:在项目根目录生成 .env.deploy-test,填写各服务的连接信息 +# 后续步骤:编辑 .env.deploy-test,然后执行 02-patch-config.sh +# ============================================================================= +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +init_dirs +init_script_log # ← 脚本执行日志 + +header "步骤 1 / 5 — 初始化 .env.deploy-test 配置" + +if [[ -f "$ENV_FILE" ]]; then + warn ".env.deploy-test 已存在,跳过创建" + echo "" + echo -e "${BOLD}当前配置:${NC}" + grep -v '^\s*#' "$ENV_FILE" | grep -v '^\s*$' | sed 's/^/ /' + echo "" + echo -e "如需重置:${YELLOW}rm $ENV_FILE && $0${NC}" + exit 0 +fi + +cat > "$ENV_FILE" <<'EOF' +# ============================================================================= +# 测试服务器环境配置 — .env.deploy-test +# 部署场景:测试服务器(有公网 IP),所有服务本地运行 +# 编辑完成后执行:./deploy-test/02-patch-config.sh +# ============================================================================= + +# ── 测试服务器公网 IP(必填)───────────────────────────────────────────────── +# 本机(测试服务器)的公网 IP,LiveKit WebRTC 媒体流需要通过此 IP 对外暴露 +# 本地 Mac 环境(deploy-local)也会引用此地址连接 LiveKit +DEPLOY_TEST_IP=54.116.29.247 + +# ── MongoDB(远程服务,必填)───────────────────────────────────────────────── +# open-im-server / chat / build-server 共用同一个连接,只是 DB 名不同 +MONGO_HOST=47.237.103.4 +MONGO_PORT=27017 +MONGO_USERNAME=minio_pC5wMB +MONGO_PASSWORD=rI57PJsJhnz_qlRkfnTa0RPT +MONGO_AUTHSOURCE=openim_v3 +MONGO_DATABASE=openim_v3 # open-im-server / chat 使用 +BUILD_MONGO_DATABASE=build # build-server 使用 + +# ── Amazon S3 — open-im-server(IM 聊天文件存储,必填)────────────────────── +# 对应 open-im-server/config/openim-rpc-third.yml → object.aws +OPENIM_AWS_REGION=ap-southeast-1 +OPENIM_AWS_BUCKET=im1688 +OPENIM_AWS_ACCESS_KEY_ID=AKIA5TMMSZWVFYCLKJ2G +OPENIM_AWS_SECRET_ACCESS_KEY=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 +# 自定义 Endpoint(CloudFlare R2 / 其他 S3 兼容服务),留空则使用 AWS 官方 +OPENIM_AWS_ENDPOINT= +OPENIM_AWS_PUBLIC_READ=true + +# ── Amazon S3 — build-server(App APK/IPA 构建产物存储,必填)─────────────── +# 对应 build-server/config/config.yaml → aws +BUILD_AWS_REGION=ap-east-1 +BUILD_AWS_BUCKET=im-hk-apk +BUILD_AWS_ACCESS_KEY=AKIASJ7PFAWCXUDC7KQV +BUILD_AWS_SECRET_KEY=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 + +# ── Redis(Docker 本地运行)───────────────────────────────────────────────── +REDIS_PORT=6379 +REDIS_PASSWORD=openIM123 + +# ── Kafka(Docker 本地运行,KRaft 模式)──────────────────────────────────── +KAFKA_PORT=9092 + +# ── Etcd(Docker 本地运行,服务发现注册中心)─────────────────────────────── +ETCD_PORT=2379 + +# ── LiveKit Server(Docker 本地运行,使用本机公网 IP)────────────────────── +# LiveKit 通过 Docker 启动(容器名: dev-livekit),复用 dev-redis。 +# WebRTC 媒体流需要公网 IP,使用上方 DEPLOY_TEST_IP。 +# +# LIVEKIT_NODE_IP: = DEPLOY_TEST_IP,WebRTC 客户端通过此 IP 直连媒体流 +# LIVEKIT_URL: 后端服务连接 LiveKit 的地址(服务器内部用回环即可) +# LIVEKIT_API_KEY / LIVEKIT_API_SECRET: 来自 livekit/livekit.yaml → keys 段 +LIVEKIT_NODE_IP=54.116.29.247 # 与 DEPLOY_TEST_IP 保持一致 +LIVEKIT_URL=ws://127.0.0.1:7880 +LIVEKIT_API_KEY=API8462dba2 +LIVEKIT_API_SECRET=U0l7/3IQjWzusK2eOrWlGmLD5jSzALvV2G5tIxGQaQc= + +# ── Cloudflare Stream(livestream 服务使用)────────────────────────────────── +# 来源: livestream/config.yaml → cloudflare 段(若已有值请从该文件复制) +CF_ACCOUNT_ID= +CF_API_TOKEN= +CF_EMAIL= +CF_API_KEY= +CF_CUSTOMER_CODE= + +# ── 腾讯云 RTC(livecloud 服务使用)───────────────────────────────────────── +# 来源: livecloud/config/config.yml → cloud.tencent 段 +TENCENT_SDK_APP_ID=20033091 +TENCENT_SDK_SECRET_KEY=cceba44084aaa04f8c48a1858ffd5385875c3a5ec006d34278d9d3714b40e3b0 +EOF + +success ".env.deploy-test 已创建: $ENV_FILE" +echo "" +echo -e "${BOLD}下一步:${NC}" +echo -e " 1. 确认 DEPLOY_TEST_IP 等关键配置正确:" +echo -e " ${CYAN}vim $ENV_FILE${NC}" +echo -e " 2. 将配置写入各服务 YAML:" +echo -e " ${CYAN}./deploy-test/02-patch-config.sh${NC}" diff --git a/02-patch-config.sh b/02-patch-config.sh new file mode 100755 index 0000000..2bfcd15 --- /dev/null +++ b/02-patch-config.sh @@ -0,0 +1,483 @@ +#!/usr/bin/env bash +# ============================================================================= +# 02-patch-config.sh — 将 .env.local 中的变量写入各服务 YAML 配置文件 +# +# 影响文件: +# open-im-server/config/redis.yml kafka.yml discovery.yml +# open-im-server/config/mongodb.yml minio.yml openim-rpc-third.yml +# chat/config/redis.yml discovery.yml mongodb.yml +# meetingmsg/manifest/config/config.yaml +# +# 后续步骤:03-start-infra.sh(启动 Docker 基础设施) +# ============================================================================= +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +load_env +init_script_log # ← 脚本执行日志 + +header "步骤 2 / 5 — 修改服务配置文件" + +OPENIM_CONF="$ROOT_DIR/open-im-server/config" +CHAT_CONF="$ROOT_DIR/chat/config" + +[[ ! -d "$OPENIM_CONF" ]] && { error "目录不存在: $OPENIM_CONF"; exit 1; } +[[ ! -d "$CHAT_CONF" ]] && { error "目录不存在: $CHAT_CONF"; exit 1; } + +# ────────────────────────────────────────────────────────────────────────────── +# open-im-server +# ────────────────────────────────────────────────────────────────────────────── +step "open-im-server 配置" + +# Redis +cat > "$OPENIM_CONF/redis.yml" < "$OPENIM_CONF/kafka.yml" < "$OPENIM_CONF/discovery.yml" < "$OPENIM_CONF/mongodb.yml" < "$OPENIM_CONF/minio.yml" < "$CHAT_CONF/redis.yml" < "$CHAT_CONF/discovery.yml" < "$CHAT_CONF/mongodb.yml" < "$MM_CONF" < "$LK_CONF" < "$LC_CONF" </dev/null || true +cat > "$LS_CONF" < "$BS_CONF" < "$MH5_ENV" < 以下变量 > 代码中的生产默认值 + +# 弹幕 WebSocket:meetingmsg 服务(:8000) +VITE_WS_BASE_URL=ws://${DEPLOY_TEST_IP}:8000 + +# 直播间 API:livestream 服务(:8081) +VITE_LIVE_API_BASE_URL=http://${DEPLOY_TEST_IP}:8081 +EOF +success " meetingh5/.env.local → ws=${DEPLOY_TEST_IP}:8000, liveApi=${DEPLOY_TEST_IP}:8081" + +echo "" +success "所有配置文件已更新!" +echo "" +echo -e "${BOLD}已修改配置摘要:${NC}" +echo " Redis → 127.0.0.1:${REDIS_PORT} password=${REDIS_PASSWORD} (Docker)" +echo " Kafka → 127.0.0.1:${KAFKA_PORT} (Docker)" +echo " Etcd → 127.0.0.1:${ETCD_PORT} (Docker)" +echo " MongoDB → ${MONGO_HOST}:${MONGO_PORT} DB(openim)=${MONGO_DATABASE} DB(build)=${BUILD_MONGO_DATABASE}" +echo " LiveKit → ${LIVEKIT_URL} node_ip=${LIVEKIT_NODE_IP} key=${LIVEKIT_API_KEY}" +echo " Tencent RTC → sdk_app_id=${TENCENT_SDK_APP_ID}" +echo " S3 (openim) → s3://${OPENIM_AWS_BUCKET} region=${OPENIM_AWS_REGION}" +echo " S3 (build) → s3://${BUILD_AWS_BUCKET} region=${BUILD_AWS_REGION}" +echo " MeetingMsg → webhook afterSendGroupMsg=enabled → 127.0.0.1:8000" +echo " MeetingH5 → ws=${DEPLOY_TEST_IP}:8000, liveApi=${DEPLOY_TEST_IP}:8081" +echo "" +echo -e "${BOLD}下一步:${NC}" +echo -e " 启动 Docker 基础设施(Redis/Kafka/Etcd):" +echo -e " ${CYAN}./deploy-test/03-start-infra.sh${NC}" diff --git a/03-start-infra.sh b/03-start-infra.sh new file mode 100755 index 0000000..c01dacb --- /dev/null +++ b/03-start-infra.sh @@ -0,0 +1,227 @@ +#!/usr/bin/env bash +# ============================================================================= +# 03-start-infra.sh — 启动 Docker 基础设施(Redis / Kafka / Etcd / LiveKit) +# +# 数据目录: .local-dev/docker-data// +# 容器日志: .local-dev/docker-logs//-YYYYMMDD.log(每日一文件) +# 脚本日志: .local-dev/script-logs/03-start-infra-.log +# +# 后续步骤:04-build.sh(编译后端服务) +# ============================================================================= +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +load_env +init_dirs +init_script_log # ← 脚本执行日志 +require_docker_running + +# Docker 日志驱动公共参数(JSON 文件,最多 5 个 50MB 轮转) +LOG_OPTS=( + --log-driver json-file + --log-opt max-size=50m + --log-opt max-file=5 +) + +header "步骤 3 / 5 — 启动 Docker 基础设施" + +# ────────────────────────────────────────────────────────────────────────────── +# Redis +# ────────────────────────────────────────────────────────────────────────────── +step "Redis" + +if docker ps --format '{{.Names}}' | grep -q '^dev-redis$'; then + success "Redis 已在运行 (container=dev-redis) :${REDIS_PORT}" +elif docker ps -a --format '{{.Names}}' | grep -q '^dev-redis$'; then + info "重新启动已有容器 dev-redis..." + docker start dev-redis > /dev/null + success "Redis 已启动 :${REDIS_PORT}" +else + info "创建并启动 Redis 容器..." + docker run -d \ + --name dev-redis \ + --restart unless-stopped \ + -p "${REDIS_PORT}:6379" \ + -v "${DATA_DIR}/redis:/data" \ + "${LOG_OPTS[@]}" \ + redis:7-alpine \ + redis-server --requirepass "${REDIS_PASSWORD:-openIM123}" --appendonly yes \ + > /dev/null + success "Redis 容器已创建并启动 :${REDIS_PORT} (密码: ${REDIS_PASSWORD:-openIM123})" +fi + +sleep 1 +if docker exec dev-redis redis-cli -a "${REDIS_PASSWORD:-openIM123}" ping 2>/dev/null | grep -q PONG; then + success "Redis 连通性: PONG ✓" +else + warn "Redis 未响应 PING,请查看日志: ./deploy-test/logs.sh redis" +fi +start_docker_logger "dev-redis" + +# ────────────────────────────────────────────────────────────────────────────── +# Kafka(KRaft 模式) +# ────────────────────────────────────────────────────────────────────────────── +step "Kafka (KRaft)" + +if docker ps --format '{{.Names}}' | grep -q '^dev-kafka$'; then + success "Kafka 已在运行 (container=dev-kafka) :${KAFKA_PORT}" +elif docker ps -a --format '{{.Names}}' | grep -q '^dev-kafka$'; then + info "重新启动已有容器 dev-kafka..." + docker start dev-kafka > /dev/null + info "等待 Kafka 就绪 (8s)..." + sleep 8 + success "Kafka 已启动 :${KAFKA_PORT}" +else + info "创建并启动 Kafka 容器(首次拉取镜像可能较慢)..." + KAFKA_CLUSTER_ID="MkU3OEVBNTcwNTJENDM2Qk" + + docker run -d \ + --name dev-kafka \ + --restart unless-stopped \ + -p "${KAFKA_PORT}:9092" \ + -v "${DATA_DIR}/kafka:/bitnami/kafka" \ + -e KAFKA_CFG_NODE_ID=0 \ + -e KAFKA_CFG_PROCESS_ROLES=controller,broker \ + -e KAFKA_CFG_LISTENERS="PLAINTEXT://:9092,CONTROLLER://:9093" \ + -e KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP="CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" \ + -e KAFKA_CFG_CONTROLLER_QUORUM_VOTERS="0@localhost:9093" \ + -e KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER \ + -e KAFKA_CFG_ADVERTISED_LISTENERS="PLAINTEXT://127.0.0.1:${KAFKA_PORT}" \ + -e KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true \ + -e KAFKA_KRAFT_CLUSTER_ID="$KAFKA_CLUSTER_ID" \ + "${LOG_OPTS[@]}" \ + bitnami/kafka:3.7 \ + > /dev/null + + info "等待 Kafka 就绪 (15s)..." + sleep 15 + success "Kafka 容器已创建并启动 :${KAFKA_PORT}" +fi +start_docker_logger "dev-kafka" + +# 初始化必要 Topics +step "Kafka Topics 初始化" +TOPICS=(toRedis toMongo toPush toOfflinePush) +for topic in "${TOPICS[@]}"; do + if docker exec dev-kafka kafka-topics.sh \ + --bootstrap-server localhost:9092 \ + --list 2>/dev/null | grep -q "^${topic}$"; then + info " topic 已存在: $topic" + else + docker exec dev-kafka kafka-topics.sh \ + --create \ + --topic "$topic" \ + --bootstrap-server localhost:9092 \ + --partitions 8 \ + --replication-factor 1 \ + 2>/dev/null \ + && success " ✓ 创建 topic: $topic" \ + || warn " ✗ 创建失败: $topic(Kafka 未就绪,重试: ./deploy-test/03-start-infra.sh)" + fi +done + +# ────────────────────────────────────────────────────────────────────────────── +# Etcd +# ────────────────────────────────────────────────────────────────────────────── +step "Etcd" + +if docker ps --format '{{.Names}}' | grep -q '^dev-etcd$'; then + success "Etcd 已在运行 (container=dev-etcd) :${ETCD_PORT}" +elif docker ps -a --format '{{.Names}}' | grep -q '^dev-etcd$'; then + info "重新启动已有容器 dev-etcd..." + docker start dev-etcd > /dev/null + sleep 2 + success "Etcd 已启动 :${ETCD_PORT}" +else + info "创建并启动 Etcd 容器..." + docker run -d \ + --name dev-etcd \ + --restart unless-stopped \ + -p "${ETCD_PORT}:2379" \ + -v "${DATA_DIR}/etcd:/etcd-data" \ + -e ALLOW_NONE_AUTHENTICATION=yes \ + -e ETCD_DATA_DIR=/etcd-data \ + "${LOG_OPTS[@]}" \ + bitnami/etcd:3.5 \ + > /dev/null + sleep 2 + success "Etcd 容器已创建并启动 :${ETCD_PORT}" +fi + +if docker exec dev-etcd etcdctl endpoint health 2>/dev/null | grep -q 'is healthy'; then + success "Etcd 连通性: healthy ✓" +else + warn "Etcd 未返回健康状态,可能仍在启动中" +fi +start_docker_logger "dev-etcd" + +# ────────────────────────────────────────────────────────────────────────────── +# LiveKit(本地 Docker 容器,复用 dev-redis) +# ────────────────────────────────────────────────────────────────────────────── +step "LiveKit" + +LK_CONF="$ROOT_DIR/livekit/livekit.yaml" + +if [[ ! -f "$LK_CONF" ]]; then + warn "livekit/livekit.yaml 不存在,请先执行: ./deploy-test/02-patch-config.sh" +else + if docker ps --format '{{.Names}}' | grep -q '^dev-livekit$'; then + success "LiveKit 已在运行 (container=dev-livekit) :7880" + elif docker ps -a --format '{{.Names}}' | grep -q '^dev-livekit$'; then + info "重新启动已有容器 dev-livekit..." + docker start dev-livekit > /dev/null + sleep 2 + success "LiveKit 已启动 :7880" + else + info "创建并启动 LiveKit 容器(首次拉取镜像需要一点时间)..." + + EXTRA_HOSTS="" + [[ "$(uname -s)" == "Linux" ]] && EXTRA_HOSTS="--add-host host.docker.internal:host-gateway" + + # shellcheck disable=SC2086 + docker run -d \ + --name dev-livekit \ + --restart unless-stopped \ + -p 7880:7880 \ + -p 7882:7882/tcp \ + -p 7882:7882/udp \ + -p 50000-51000:50000-51000/udp \ + -v "${LK_CONF}:/etc/livekit.yaml:ro" \ + "${LOG_OPTS[@]}" \ + $EXTRA_HOSTS \ + livekit/livekit-server:latest \ + --config /etc/livekit.yaml \ + > /dev/null + + sleep 3 + if docker ps --format '{{.Names}}' | grep -q '^dev-livekit$'; then + success "LiveKit 容器已创建并启动 :7880" + info " 公网 IP: ${LIVEKIT_NODE_IP} (WebRTC 媒体流直连)" + info " API Key: ${LIVEKIT_API_KEY}" + else + error "LiveKit 启动失败,查看日志: ./deploy-test/logs.sh livekit" + fi + fi + start_docker_logger "dev-livekit" +fi + +# ────────────────────────────────────────────────────────────────────────────── +# 汇总 +# ────────────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}基础设施状态:${NC}" +print_container_status "Redis" "dev-redis" "${REDIS_PORT}" +print_container_status "Kafka" "dev-kafka" "${KAFKA_PORT}" +print_container_status "Etcd" "dev-etcd" "${ETCD_PORT}" +print_container_status "LiveKit" "dev-livekit" "7880" +echo "" +echo -e "${BOLD}日志目录:${NC}" +echo " Docker 容器日志: $DOCKER_LOG_DIR/" +echo " 本脚本执行日志: $_CURRENT_SCRIPT_LOG" +echo "" +echo -e " LiveKit 公网: ${LIVEKIT_NODE_IP}:50000-51000/udp (WebRTC 媒体流)" +echo "" +success "Docker 基础设施已就绪!" +echo "" +echo -e "${BOLD}下一步:${NC}" +echo -e " 编译所有后端 Go 服务:" +echo -e " ${CYAN}./deploy-test/04-build.sh${NC}" diff --git a/04-build.sh b/04-build.sh new file mode 100755 index 0000000..af938d1 --- /dev/null +++ b/04-build.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# ============================================================================= +# 04-build.sh — 编译所有后端 Go 服务 +# +# 编译产物输出至 .local-dev/bin/ +# 支持只编译单个服务:./04-build.sh [service-name] +# +# 可用服务名: openim-server, chat-rpc, admin-rpc, chat-api, admin-api, +# meetingmsg, livecloud, livestream +# +# 后续步骤:05-start.sh(启动后端服务) +# ============================================================================= +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +init_dirs +init_script_log # ← 脚本执行日志 +require_tools go + +header "步骤 4 / 5 — 编译后端 Go 服务" + +# 编译单个服务的函数 +# 用法: _build +_build() { + local name="$1" dir="$ROOT_DIR/$2" pkg="$3" + local out="$BUILD_DIR/$name" + + info "编译 ${BOLD}$name${NC} ..." + local start_ts=$SECONDS + + if (cd "$dir" && go build -o "$out" "$pkg"); then + local elapsed=$(( SECONDS - start_ts )) + success " ✓ $name → $out (${elapsed}s)" + else + error " ✗ $name 编译失败" + return 1 + fi +} + +# 服务列表:名称 | 源码目录(相对 ROOT) | 包路径 +declare -A SVC_DIR=( + [openim-server]="open-im-server" + [chat-rpc]="chat" + [admin-rpc]="chat" + [chat-api]="chat" + [admin-api]="chat" + [meetingmsg]="meetingmsg" + [livecloud]="livecloud" + [livestream]="livestream" + [build-server]="build-server" +) + +declare -A SVC_PKG=( + [openim-server]="./cmd/main.go" + [chat-rpc]="./cmd/rpc/chat-rpc/" + [admin-rpc]="./cmd/rpc/admin-rpc/" + [chat-api]="./cmd/api/chat-api/" + [admin-api]="./cmd/api/admin-api/" + [meetingmsg]="." + [livecloud]="." + [livestream]="." + [build-server]="." +) + +# ── 判断是编译单个还是全部 ──────────────────────────────────────────────────── +TARGET="${1:-all}" + +if [[ "$TARGET" == "all" ]]; then + step "编译全部服务(共 ${#SVC_DIR[@]} 个)" + FAILED=() + for svc in openim-server chat-rpc admin-rpc chat-api admin-api meetingmsg livecloud livestream build-server; do + _build "$svc" "${SVC_DIR[$svc]}" "${SVC_PKG[$svc]}" || FAILED+=("$svc") + done + + echo "" + if [[ ${#FAILED[@]} -eq 0 ]]; then + success "所有服务编译完成!" + ls -lh "$BUILD_DIR/" | awk 'NR>1 {printf " %-20s %s\n", $NF, $5}' + else + error "以下服务编译失败: ${FAILED[*]}" + echo "" + echo "排查建议:" + echo " 1. cd $ROOT_DIR/ && go mod tidy" + echo " 2. 检查 Go 版本: go version(推荐 1.21+)" + echo " 3. 检查模块依赖: go mod download" + exit 1 + fi +else + # 编译单个服务 + if [[ -z "${SVC_DIR[$TARGET]:-}" ]]; then + error "未知服务: $TARGET" + echo "可用: ${!SVC_DIR[*]}" + exit 1 + fi + step "编译单个服务: $TARGET" + _build "$TARGET" "${SVC_DIR[$TARGET]}" "${SVC_PKG[$TARGET]}" + success "$TARGET 编译完成" +fi + +echo "" +echo -e "${BOLD}下一步:${NC}" +echo -e " 启动所有后端服务:" +echo -e " ${CYAN}./deploy-test/05-start.sh${NC}" +echo -e " 或只启动单个服务:" +echo -e " ${CYAN}./deploy-test/05-start.sh openim-server${NC}" diff --git a/05-start.sh b/05-start.sh new file mode 100755 index 0000000..7c218f9 --- /dev/null +++ b/05-start.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# ============================================================================= +# 05-start.sh — 启动后端 Go 服务 +# +# 用法: +# ./05-start.sh # 按依赖顺序启动全部服务 +# ./05-start.sh # 只启动指定服务 +# +# 启动顺序(有依赖关系): +# openim-server → chat-rpc / admin-rpc → chat-api / admin-api +# → meetingmsg / livecloud / livestream +# +# 日志文件: .local-dev/logs/.log +# PID 文件: .local-dev/pids/.pid +# ============================================================================= +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +load_env +init_dirs +init_script_log # ← 脚本执行日志 + +header "步骤 5 / 5 — 启动后端服务" + +TARGET="${1:-all}" + +# ── 服务启动配置 ───────────────────────────────────────────────────────────── +# 格式: svc_workdir[name] svc_args[name] +declare -A svc_workdir=( + [openim-server]="$ROOT_DIR/open-im-server" + [chat-rpc]="$ROOT_DIR/chat" + [admin-rpc]="$ROOT_DIR/chat" + [chat-api]="$ROOT_DIR/chat" + [admin-api]="$ROOT_DIR/chat" + [meetingmsg]="$ROOT_DIR/meetingmsg" + [livecloud]="$ROOT_DIR/livecloud" + [livestream]="$ROOT_DIR/livestream" + [build-server]="$ROOT_DIR/build-server" +) + +declare -A svc_args=( + [openim-server]="-c $ROOT_DIR/open-im-server/config" + [chat-rpc]="-c $ROOT_DIR/chat/config" + [admin-rpc]="-c $ROOT_DIR/chat/config" + [chat-api]="-c $ROOT_DIR/chat/config" + [admin-api]="-c $ROOT_DIR/chat/config" + [meetingmsg]="" + [livecloud]="" + [livestream]="" + [build-server]="" +) + +declare -A svc_desc=( + [openim-server]=":10002 (API) :10001 (MsgGateway WS)" + [chat-rpc]="内部 RPC" + [admin-rpc]="内部 RPC" + [chat-api]=":10008" + [admin-api]=":10009" + [meetingmsg]=":8000 (WS)" + [livecloud]=":8080" + [livestream]=":8081" + [build-server]=":8281" +) + +_start_one() { + local svc="$1" + start_svc "$svc" \ + "$BUILD_DIR/$svc" \ + "${svc_args[$svc]}" \ + "${svc_workdir[$svc]}" +} + +# ── 启动全部(有序) ────────────────────────────────────────────────────────── +_start_all() { + step "第 1 组: openim-server(核心 IM 服务)" + _start_one openim-server + + info "等待 openim-server 将 RPC 注册到 Etcd... (8s)" + sleep 8 + + step "第 2 组: chat RPC 服务" + _start_one chat-rpc + _start_one admin-rpc + + info "等待 chat RPC 注册到 Etcd... (4s)" + sleep 4 + + step "第 3 组: chat API 服务" + _start_one chat-api + _start_one admin-api + + step "第 4 组: 业务服务" + _start_one meetingmsg + _start_one livecloud + _start_one livestream + _start_one build-server + + echo "" + echo -e "${BOLD}服务汇总:${NC}" + for svc in openim-server chat-rpc admin-rpc chat-api admin-api meetingmsg livecloud livestream build-server; do + print_svc_status "$svc" "${svc_desc[$svc]}" + done + + echo "" + echo -e "${BOLD}常用地址:${NC}" + echo " IM API: http://localhost:10002" + echo " IM WebSocket: ws://localhost:10001" + echo " Chat API: http://localhost:10008" + echo " Admin API: http://localhost:10009" + echo " MeetingMsg WS: ws://localhost:8000" + echo " Livecloud: http://localhost:8080" + echo " Livestream: http://localhost:8081" + echo " Build Server: http://localhost:8281" + echo "" + echo -e "${BOLD}查看日志:${NC}" + echo -e " ${CYAN}./deploy-test/logs.sh openim-server${NC}" + echo -e " ${CYAN}./deploy-test/logs.sh chat-api${NC}" +} + +# ── 启动单个 ────────────────────────────────────────────────────────────────── +_start_single() { + local svc="$1" + if [[ -z "${svc_workdir[$svc]:-}" ]]; then + error "未知服务: $svc" + echo "可用: ${!svc_workdir[*]}" + exit 1 + fi + step "启动: $svc" + _start_one "$svc" + echo "" + print_svc_status "$svc" "${svc_desc[$svc]}" +} + +# ── 入口 ───────────────────────────────────────────────────────────────────── +if [[ "$TARGET" == "all" ]]; then + _start_all +else + _start_single "$TARGET" +fi diff --git a/06-install-frontend.sh b/06-install-frontend.sh new file mode 100755 index 0000000..e2323c2 --- /dev/null +++ b/06-install-frontend.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# ============================================================================= +# 06-install-frontend.sh — 安装前端项目依赖 +# +# 首次使用或依赖变更后运行(对应后端的 04-build.sh) +# +# 用法: +# ./06-install-frontend.sh # 安装全部前端项目依赖 +# ./06-install-frontend.sh # 只安装指定项目 +# +# 可用项目: pc, meetingh5, h5, cms, build-cms, build-down +# ============================================================================= +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +init_dirs +init_script_log # ← 脚本执行日志 + +header "前端依赖安装" + +# ── 工具检查 ────────────────────────────────────────────────────────────────── +_check_pm() { + local pm="$1" + if ! command -v "$pm" &>/dev/null; then + error "$pm 未安装" + case "$pm" in + yarn) echo " → npm install -g yarn" ;; + pnpm) echo " → npm install -g pnpm" ;; + npm) echo " → https://nodejs.org/" ;; + esac + return 1 + fi + return 0 +} + +# ── 安装单个项目 ────────────────────────────────────────────────────────────── +# 用法: _install <显示名> <目录> <包管理器> +_install() { + local name="$1" dir="$ROOT_DIR/$2" pm="$3" + + if [[ ! -d "$dir" ]]; then + warn " 目录不存在,跳过: $dir" + return 0 + fi + + _check_pm "$pm" || return 1 + + local logfile="$LOG_DIR/install-${name}.log" + info "安装 ${BOLD}$name${NC} 依赖 ($pm install) ..." + + if [[ -d "$dir/node_modules" ]]; then + warn " node_modules 已存在,执行增量安装(如需全量重装请先 rm -rf $dir/node_modules)" + fi + + local start_ts=$SECONDS + (cd "$dir" && "$pm" install 2>&1) | tee -a "$logfile" | tail -5 + local elapsed=$(( SECONDS - start_ts )) + + success " ✓ $name 安装完成 (${elapsed}s) → 日志: $logfile" +} + +# ── 项目列表 ────────────────────────────────────────────────────────────────── +TARGET="${1:-all}" + +case "$TARGET" in + all) + step "安装全部前端项目依赖(共 6 个)" + _install "pc" "pc" "yarn" + _install "meetingh5" "meetingh5" "npm" + _install "h5" "h5" "npm" + _install "cms" "cms" "pnpm" + _install "build-cms" "build-cms" "pnpm" + _install "build-down" "build-down" "npm" + echo "" + success "所有前端依赖安装完成!" + echo "" + echo -e "${BOLD}下一步:${NC}" + echo -e " 启动前端开发服务器:" + echo -e " ${CYAN}./deploy-test/07-start-frontend.sh${NC}" + ;; + pc|meetingh5|h5|cms|build-cms|build-down) + local PM + case "$TARGET" in + pc) PM="yarn" ;; + cms|build-cms) PM="pnpm" ;; + *) PM="npm" ;; + esac + step "安装 $TARGET 依赖" + _install "$TARGET" "$TARGET" "$PM" + ;; + *) + error "未知项目: $TARGET" + echo "可用: pc, meetingh5, h5, cms, build-cms, build-down" + exit 1 + ;; +esac diff --git a/07-start-frontend.sh b/07-start-frontend.sh new file mode 100755 index 0000000..08f4d4f --- /dev/null +++ b/07-start-frontend.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +# ============================================================================= +# 07-start-frontend.sh — 启动前端开发服务器 +# +# 用法: +# ./07-start-frontend.sh # 启动全部前端项目 +# ./07-start-frontend.sh # 只启动指定项目 +# +# 项目与端口: +# pc → Electron + Vite :7777 +# meetingh5 → React + Vite :5188 +# h5 → Vue + Vite :3003 +# cms → UMI Max :8001 +# build-cms → UMI Max :8002 +# build-down → UMI v3 :8003 +# +# 日志文件: .local-dev/logs/fe-.log +# PID 文件: .local-dev/pids/fe-.pid +# ============================================================================= +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +init_dirs +init_script_log # ← 脚本执行日志 + +header "启动前端开发服务器" + +# ── 前端服务配置 ────────────────────────────────────────────────────────────── +# name → (目录, 包管理器, 启动命令, 环境变量, 端口描述) +declare -A FE_DIR=( + [pc]="pc" + [meetingh5]="meetingh5" + [h5]="h5" + [cms]="cms" + [build-cms]="build-cms" + [build-down]="build-down" +) + +declare -A FE_PM=( + [pc]="yarn" + [meetingh5]="npm" + [h5]="npm" + [cms]="pnpm" + [build-cms]="pnpm" + [build-down]="npm" +) + +declare -A FE_CMD=( + [pc]="yarn dev" + [meetingh5]="npm run dev" + [h5]="npm run dev" + [cms]="pnpm run dev" + [build-cms]="pnpm run dev" + [build-down]="npm run dev" +) + +# cms/build-cms/build-down 不配置端口时默认都是 8000,需手动指定 +declare -A FE_ENV=( + [pc]="" + [meetingh5]="" + [h5]="" + [cms]="PORT=8001" + [build-cms]="PORT=8002" + [build-down]="PORT=8003" +) + +declare -A FE_PORT=( + [pc]=":7777 (Electron Vite 调试服务器)" + [meetingh5]=":5188" + [h5]=":3003" + [cms]=":8001" + [build-cms]=":8002" + [build-down]=":8003" +) + +# ── 启动单个前端服务 ────────────────────────────────────────────────────────── +_start_fe() { + local name="$1" + local dir="$ROOT_DIR/${FE_DIR[$name]}" + local pm="${FE_PM[$name]}" + local cmd="${FE_CMD[$name]}" + local env_prefix="${FE_ENV[$name]}" + local pidfile="$PID_DIR/fe-${name}.pid" + local logfile="$LOG_DIR/fe-${name}.log" + + # 已在运行 + if [[ -f "$pidfile" ]] && kill -0 "$(cat "$pidfile")" 2>/dev/null; then + warn "$name 已在运行 (PID=$(cat "$pidfile")),跳过" + return 0 + fi + + # 目录检查 + if [[ ! -d "$dir" ]]; then + warn "$name 目录不存在 ($dir),跳过" + return 0 + fi + + # 依赖检查 + if [[ ! -d "$dir/node_modules" ]]; then + warn "$name node_modules 不存在,请先执行: ./deploy-test/06-install-frontend.sh $name" + return 1 + fi + + # 包管理器检查 + if ! command -v "$pm" &>/dev/null; then + error "$name 需要 $pm,未安装" + return 1 + fi + + info "启动 ${BOLD}$name${NC} ..." + + # 写日志分隔符 + { + echo "" + echo "──── 启动 $(date '+%Y-%m-%d %H:%M:%S') ────" + } >> "$logfile" + + # 后台启动(带环境变量前缀) + ( + cd "$dir" + if [[ -n "$env_prefix" ]]; then + # shellcheck disable=SC2086 + nohup env $env_prefix $cmd >> "$logfile" 2>&1 & + else + # shellcheck disable=SC2086 + nohup $cmd >> "$logfile" 2>&1 & + fi + echo $! > "$pidfile" + ) + + sleep 2 + if [[ -f "$pidfile" ]] && kill -0 "$(cat "$pidfile")" 2>/dev/null; then + success " ✓ $name (PID=$(cat "$pidfile")) ${FE_PORT[$name]} → $logfile" + else + error " ✗ $name 启动失败,查看日志:" + tail -20 "$logfile" 2>/dev/null || true + return 1 + fi +} + +# ── 入口 ───────────────────────────────────────────────────────────────────── +TARGET="${1:-all}" +FE_PROJECTS=(pc meetingh5 h5 cms build-cms build-down) + +_all_valid() { + for p in "${FE_PROJECTS[@]}"; do + [[ "$p" == "$1" ]] && return 0 + done + return 1 +} + +if [[ "$TARGET" == "all" ]]; then + step "启动全部前端开发服务器" + FAILED=() + for proj in "${FE_PROJECTS[@]}"; do + _start_fe "$proj" || FAILED+=("$proj") + done + + echo "" + echo -e "${BOLD}前端服务汇总:${NC}" + for proj in "${FE_PROJECTS[@]}"; do + local_pidfile="$PID_DIR/fe-${proj}.pid" + if [[ -f "$local_pidfile" ]] && kill -0 "$(cat "$local_pidfile")" 2>/dev/null; then + printf " ${GREEN}●${NC} %-14s PID=%-7s %s\n" "$proj" "$(cat "$local_pidfile")" "${FE_PORT[$proj]}" + else + printf " ${RED}○${NC} %-14s 未运行 %s\n" "$proj" "${FE_PORT[$proj]}" + fi + done + + echo "" + echo -e "${BOLD}访问地址:${NC}" + echo " PC (Electron): yarn dev 启动后自动打开窗口" + echo " H5: http://${DEPLOY_TEST_IP}:3003" + echo " CMS: http://${DEPLOY_TEST_IP}:8001" + echo " Build CMS: http://${DEPLOY_TEST_IP}:8002" + echo " Build Download: http://${DEPLOY_TEST_IP}:8003" + echo "" + echo -e "${BOLD}MeetingH5 访问地址(后端 URL 由 .env.local 默认设置,也可通过 URL 参数覆盖):${NC}" + echo " 默认: http://${DEPLOY_TEST_IP}:5188" + echo " 显式指定后端: http://${DEPLOY_TEST_IP}:5188?ws=ws://${DEPLOY_TEST_IP}:8000&liveApi=http://${DEPLOY_TEST_IP}:8081" + echo " 说明: ws → meetingmsg 弹幕 WebSocket (:8000)" + echo " liveApi → livestream 直播间 API (:8081)" + + if [[ ${#FAILED[@]} -gt 0 ]]; then + echo "" + warn "以下项目启动失败: ${FAILED[*]}" + echo " 可能原因: node_modules 未安装,执行: ./deploy-test/06-install-frontend.sh" + fi +else + if ! _all_valid "$TARGET"; then + error "未知项目: $TARGET" + echo "可用: ${FE_PROJECTS[*]}" + exit 1 + fi + step "启动前端项目: $TARGET" + _start_fe "$TARGET" +fi diff --git a/README.md b/README.md new file mode 100644 index 0000000..775b32e --- /dev/null +++ b/README.md @@ -0,0 +1,280 @@ +# deploy-test — 测试服务器部署脚本集 + +> **适用场景**:部署在**有公网 IP 的测试服务器**上。所有服务(后端、前端、Docker 基础设施、LiveKit)均在本机运行。 +> +> 如果你在本机 Mac 开发,请使用 `deploy-local/` 目录。 + +--- + +## 两套环境对比 + +| 项目 | deploy-test(本目录)| deploy-local/ | +|------|---------------------|----------------| +| 适用机器 | 测试服务器(有公网 IP) | 本机 Mac(无公网 IP) | +| 配置文件 | `.env.deploy-test` | `.env.deploy-local` | +| 运行时目录 | `.deploy-test/` | `.deploy-local/` | +| LiveKit | 本机 Docker 启动,使用公网 IP | 指向本目录服务器的 LiveKit | +| Redis/Kafka/Etcd | 本机 Docker | 本机 Docker | +| 后端服务 | 本机进程 | 本机进程 | +| 前端服务 | 本机进程(可选) | 本机进程 | + +--- + +## 目录结构 + +``` +deploy-test/ +├── common.sh # 公共函数库(路径、日志函数) +├── 01-init-env.sh # 步骤1:生成 .env.deploy-test 配置模板 +├── 02-patch-config.sh # 步骤2:将 .env.deploy-test 写入各服务 YAML +├── 03-start-infra.sh # 步骤3:启动 Docker 容器(Redis/Kafka/Etcd/LiveKit) +├── 04-build.sh # 步骤4:编译所有后端 Go 服务 +├── 05-start.sh # 步骤5:启动所有后端服务 +├── 06-install-frontend.sh # 步骤6:安装前端依赖(可选) +├── 07-start-frontend.sh # 步骤7:启动前端开发服务器(可选) +├── stop.sh # 停止后端服务 +├── stop-infra.sh # 停止 Docker 容器(含 LiveKit) +├── stop-frontend.sh # 停止前端服务 +├── remove-infra.sh # 删除 Docker 容器及数据(危险!) +├── restart.sh # 重启指定服务(支持 --build) +├── status.sh # 查看所有服务状态 +├── logs.sh # 查看日志(统一入口) +├── check-conn.sh # 验证 MongoDB / S3 连接 +└── setup.sh # 一键完整部署(首次使用) +``` + +运行时目录(`.deploy-test/`,已加入 `.gitignore`): + +``` +.deploy-test/ +├── bin/ # Go 编译产物 +├── pids/ # PID 文件 +├── logs/ # 后端/前端服务日志 +├── docker-data/ # Docker 数据卷 +├── docker-logs/ # Docker 容器日志(按日期滚动) +└── script-logs/ # 脚本执行日志(带时间戳) +``` + +--- + +## 快速开始 + +### 首次使用 + +```bash +# 一键执行(推荐) +./deploy-test/setup.sh +``` + +### 分步执行 + +```bash +# 1. 生成配置模板 +./deploy-test/01-init-env.sh + +# 2. 修改配置(重要:确认 DEPLOY_TEST_IP 等信息正确) +vim .env.deploy-test + +# 3. 将配置写入各服务 YAML(包括 livekit/livekit.yaml) +./deploy-test/02-patch-config.sh + +# 4. 启动 Docker 基础设施(Redis / Kafka / Etcd / LiveKit) +./deploy-test/03-start-infra.sh + +# 5. 编译后端服务 +./deploy-test/04-build.sh + +# 6. 启动后端服务 +./deploy-test/05-start.sh + +# 7. 安装前端依赖(可选) +./deploy-test/06-install-frontend.sh + +# 8. 启动前端开发服务器(可选) +./deploy-test/07-start-frontend.sh +``` + +--- + +## 配置文件(`.env.deploy-test`) + +```bash +# ══ 测试服务器公网 IP ═══════════════════════════════════════════ +DEPLOY_TEST_IP=54.116.29.247 # 本机公网 IP(LiveKit WebRTC 必需) + +# ══ MongoDB ════════════════════════════════════════════════════ +MONGO_HOST=47.237.103.4 +MONGO_PORT=27017 +MONGO_USERNAME=minio_pC5wMB +MONGO_PASSWORD=rI57PJsJhnz_qlRkfnTa0RPT +MONGO_AUTHSOURCE=openim_v3 +MONGO_DATABASE=openim_v3 +BUILD_MONGO_DATABASE=build + +# ══ Amazon S3 ══════════════════════════════════════════════════ +OPENIM_AWS_REGION=ap-southeast-1 +OPENIM_AWS_BUCKET=im1688 +OPENIM_AWS_ACCESS_KEY_ID=xxx +OPENIM_AWS_SECRET_ACCESS_KEY=xxx + +BUILD_AWS_REGION=ap-east-1 +BUILD_AWS_BUCKET=im-hk-apk +BUILD_AWS_ACCESS_KEY=xxx +BUILD_AWS_SECRET_KEY=xxx + +# ══ Docker Redis / Kafka / Etcd ════════════════════════════════ +REDIS_PORT=6379 +REDIS_PASSWORD=openIM123 +KAFKA_PORT=9092 +ETCD_PORT=2379 + +# ══ LiveKit(本机 Docker,使用公网 IP)════════════════════════ +LIVEKIT_NODE_IP=54.116.29.247 # 与 DEPLOY_TEST_IP 保持一致 +LIVEKIT_URL=ws://127.0.0.1:7880 +LIVEKIT_API_KEY=API8462dba2 +LIVEKIT_API_SECRET=xxx + +# ══ Cloudflare Stream / 腾讯云 RTC ════════════════════════════ +CF_ACCOUNT_ID= +CF_API_TOKEN= +TENCENT_SDK_APP_ID=xxx +TENCENT_SDK_SECRET_KEY=xxx +``` + +--- + +## 服务地址 + +### 后端服务 + +| 服务 | 端口 | +|------|------| +| openim-server | :10002 (HTTP) / :10001 (WS) | +| chat-api | :10008 | +| admin-api | :10009 | +| meetingmsg | :8000 (WS) | +| livecloud | :8080 | +| livestream | :8081 | +| build-server | :8281 | + +### 前端开发服务器(可选) + +| 项目 | 端口 | 说明 | +|------|------|------| +| pc (Electron) | :7777 | Electron 桌面客户端 | +| meetingh5 | :5188 | 直播观看 H5(弹幕+视频) | +| h5 | :3003 | 移动端 H5 | +| cms | :8001 | 后台管理 | +| build-cms | :8002 | 构建管理后台 | +| build-down | :8003 | 下载页 | + +> **meetingh5 访问方式** +> +> `02-patch-config.sh` 会自动生成 `meetingh5/.env.local`,设置默认后端地址: +> +> ``` +> # 直接访问(使用 .env.local 中的默认后端) +> http://:5188 +> +> # 或显式传入 URL 参数(优先级最高) +> http://:5188?ws=ws://:8000&liveApi=http://:8081 +> ``` +> +> - `ws` → meetingmsg 弹幕 WebSocket `:8000` +> - `liveApi` → livestream 直播间 API `:8081` + +### Docker 基础设施 + +| 服务 | 端口 | +|------|------| +| Redis | :6379 | +| Kafka | :9092 | +| Etcd | :2379 | +| LiveKit | :7880 (API) / :7882 (TCP) / :50000-51000/udp (WebRTC) | + +--- + +## LiveKit 说明 + +本测试服务器运行本地 LiveKit 容器,WebRTC 媒体流通过公网 IP 对外暴露。 + +``` +测试服务器(DEPLOY_TEST_IP: 54.116.29.247) +│ +├── dev-redis :6379 ←── dev-livekit 通过 host.docker.internal 访问 +└── dev-livekit + :7880 → HTTP API(后端连接) + :7882/tcp+udp → WebRTC fallback + :50000-51000/udp → WebRTC 媒体流(客户端直连公网 IP) +``` + +**防火墙必须开放**:7880/tcp、7882/tcp+udp、50000-51000/udp + +本机 Mac(deploy-local)的 LiveKit 连接地址为 `ws://54.116.29.247:7880`,与此保持一致。 + +--- + +## 日志体系 + +``` +.deploy-test/ +├── script-logs/ ← 每次脚本执行的完整输出(带时间戳,自动去除颜色码) +├── logs/ ← 后端/前端服务进程 stdout+stderr +└── docker-logs/ ← Docker 容器日志(每日一文件) + ├── redis/ + ├── kafka/ + ├── etcd/ + └── livekit/ +``` + +```bash +# 查看所有日志概览 +./deploy-test/logs.sh + +# 实时跟踪某个服务 +./deploy-test/logs.sh openim-server +./deploy-test/logs.sh livekit +./deploy-test/logs.sh cms + +# 查看脚本执行历史 +./deploy-test/logs.sh scripts +./deploy-test/logs.sh scripts --last # 最新一次完整输出 +``` + +--- + +## 日常操作 + +```bash +# 早上开机 +./deploy-test/03-start-infra.sh # Docker 容器(含 LiveKit) +./deploy-test/05-start.sh # 后端服务 + +# 查看状态 +./deploy-test/status.sh + +# 重启单个后端服务 +./deploy-test/restart.sh chat-api +./deploy-test/restart.sh chat-api --build # 重编译 + 重启 + +# 下班关机 +./deploy-test/stop.sh # 后端进程 +./deploy-test/stop-infra.sh # Docker 容器(含 LiveKit,数据保留) +``` + +--- + +## 故障排查 + +```bash +# 验证 MongoDB / S3 连接 +./deploy-test/check-conn.sh + +# 查看 LiveKit 日志(WebRTC 不通时) +./deploy-test/logs.sh livekit --last + +# 重置 Docker 环境(删除所有数据) +./deploy-test/remove-infra.sh +./deploy-test/02-patch-config.sh +./deploy-test/03-start-infra.sh +``` diff --git a/check-conn.sh b/check-conn.sh new file mode 100755 index 0000000..653040e --- /dev/null +++ b/check-conn.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# ============================================================================= +# check-conn.sh — 验证远程服务连接(MongoDB 和 Amazon S3) +# +# 用法: +# ./check-conn.sh # 同时检查 MongoDB 和 S3 +# ./check-conn.sh mongo # 只检查 MongoDB +# ./check-conn.sh s3 # 只检查 S3 +# ============================================================================= +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +load_env +init_script_log # ← 脚本执行日志 + +TARGET="${1:-all}" + +header "远程服务连接检查" + +# ────────────────────────────────────────────────────────────────────────────── +# MongoDB +# ────────────────────────────────────────────────────────────────────────────── +check_mongo() { + step "MongoDB: ${MONGO_HOST}:${MONGO_PORT}/${MONGO_DATABASE}" + + echo -e " Host: ${MONGO_HOST}" + echo -e " Port: ${MONGO_PORT}" + echo -e " Database: ${MONGO_DATABASE}" + echo -e " AuthSource: ${MONGO_AUTHSOURCE}" + echo -e " Username: ${MONGO_USERNAME}" + echo "" + + MONGO_URI="mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOST}:${MONGO_PORT}/${MONGO_DATABASE}?authSource=${MONGO_AUTHSOURCE}&directConnection=true" + + # 方法1:mongosh + if command -v mongosh &>/dev/null; then + info "使用 mongosh 验证..." + if mongosh "$MONGO_URI" --quiet --eval \ + 'db.runCommand({ping:1}); db.getSiblingDB("'"${MONGO_DATABASE}"'").getCollectionNames().slice(0,5)' \ + 2>/dev/null; then + success "MongoDB 连接正常 ✓" + else + error "MongoDB 连接失败!请检查 .env.local 中的配置" + echo "" + echo " 排查步骤:" + echo " 1. 确认 MongoDB 服务器 ${MONGO_HOST} 可从本机访问" + echo " 2. 确认端口 ${MONGO_PORT} 已开放防火墙" + echo " 3. 确认用户名/密码/authSource 正确" + echo " 4. 手动测试: mongosh \"$MONGO_URI\"" + fi + # 方法2:nc 端口连通 + elif command -v nc &>/dev/null; then + info "mongosh 未安装,使用 nc 检查端口..." + if nc -z -w5 "${MONGO_HOST}" "${MONGO_PORT}" 2>/dev/null; then + success "MongoDB 端口 ${MONGO_HOST}:${MONGO_PORT} 可达 ✓" + warn "(未验证认证,安装 mongosh 可做完整测试)" + else + error "MongoDB 端口不可达: ${MONGO_HOST}:${MONGO_PORT}" + fi + # 方法3:Python pymongo + elif command -v python3 &>/dev/null && python3 -c "import pymongo" 2>/dev/null; then + info "使用 Python pymongo 验证..." + python3 - < [endpoint] +_check_s3_bucket() { + local label="$1" key_id="$2" secret_key="$3" region="$4" bucket="$5" endpoint="${6:-}" + + echo -e " Bucket: ${bucket}" + echo -e " Region: ${region}" + echo -e " AccessKey: ${key_id}" + [[ -n "$endpoint" ]] && echo -e " Endpoint: ${endpoint}" + echo "" + + if [[ "${key_id}" == "YOUR_"* || -z "${key_id}" ]]; then + error "S3 AccessKeyID 未配置,请编辑 .env.local" + return 1 + fi + + if command -v aws &>/dev/null; then + info "使用 awscli 验证..." + local endpoint_arg="" + [[ -n "$endpoint" ]] && endpoint_arg="--endpoint-url $endpoint" + + local result rc=0 + result=$( + AWS_ACCESS_KEY_ID="$key_id" \ + AWS_SECRET_ACCESS_KEY="$secret_key" \ + AWS_DEFAULT_REGION="$region" \ + aws s3 ls "s3://${bucket}" $endpoint_arg 2>&1 | head -5 + ) || rc=$? + + if [[ $rc -eq 0 ]]; then + success "S3 Bucket 可访问 ✓" + [[ -n "$result" ]] && echo "$result" | sed 's/^/ /' || echo " (Bucket 为空)" + else + error "S3 访问失败!错误: $result" + echo " 排查: 确认 AccessKey/SecretKey、Bucket 名称、IAM s3:ListBucket 权限" + fi + + # 测试写入 + info "测试写入权限..." + local test_key="local-dev-test-$(date +%s)" + local write_ok=false + AWS_ACCESS_KEY_ID="$key_id" \ + AWS_SECRET_ACCESS_KEY="$secret_key" \ + AWS_DEFAULT_REGION="$region" \ + aws s3 cp /dev/stdin "s3://${bucket}/${test_key}" \ + $endpoint_arg --content-type text/plain \ + <<< "local-dev-test" 2>/dev/null && write_ok=true || true + + if $write_ok; then + AWS_ACCESS_KEY_ID="$key_id" AWS_SECRET_ACCESS_KEY="$secret_key" \ + AWS_DEFAULT_REGION="$region" \ + aws s3 rm "s3://${bucket}/${test_key}" $endpoint_arg 2>/dev/null || true + success "S3 写入权限正常 ✓" + else + warn "S3 写入测试失败(Bucket 可读但可能无写权限)" + fi + else + warn "awscli 未安装,跳过验证(brew install awscli)" + echo " 手动验证: AWS_ACCESS_KEY_ID=${key_id} aws s3 ls s3://${bucket}" + fi +} + +check_s3() { + step "S3 (open-im-server) — IM 文件存储" + _check_s3_bucket \ + "openim" \ + "${OPENIM_AWS_ACCESS_KEY_ID}" \ + "${OPENIM_AWS_SECRET_ACCESS_KEY}" \ + "${OPENIM_AWS_REGION}" \ + "${OPENIM_AWS_BUCKET}" \ + "${OPENIM_AWS_ENDPOINT:-}" + + echo "" + step "S3 (build-server) — App APK/IPA 构建产物" + _check_s3_bucket \ + "build" \ + "${BUILD_AWS_ACCESS_KEY}" \ + "${BUILD_AWS_SECRET_KEY}" \ + "${BUILD_AWS_REGION}" \ + "${BUILD_AWS_BUCKET}" +} + +# ────────────────────────────────────────────────────────────────────────────── +# 入口 +# ────────────────────────────────────────────────────────────────────────────── +case "$TARGET" in + all) check_mongo; echo ""; check_s3 ;; + mongo) check_mongo ;; + s3) check_s3 ;; + *) + error "未知参数: $TARGET" + echo "用法: $0 [all|mongo|s3]" + exit 1 + ;; +esac + +echo "" diff --git a/common.sh b/common.sh new file mode 100755 index 0000000..343c03d --- /dev/null +++ b/common.sh @@ -0,0 +1,225 @@ +#!/usr/bin/env bash +# ============================================================================= +# common.sh — 公共函数库,供各子脚本 source 引入 +# 不可直接执行 +# ============================================================================= + +# 防止重复加载 +[[ -n "${_COMMON_LOADED:-}" ]] && return 0 +_COMMON_LOADED=1 + +# ── 根目录(workspace46/)────────────────────────────────────────────────────── +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# ── 运行时目录(测试服务器环境) ─────────────────────────────────────────────── +LOG_DIR="$ROOT_DIR/.deploy-test/logs" # 后端服务日志 +PID_DIR="$ROOT_DIR/.deploy-test/pids" # PID 文件(含日志收集进程) +BUILD_DIR="$ROOT_DIR/.deploy-test/bin" # 编译产物 +DATA_DIR="$ROOT_DIR/.deploy-test/docker-data" # Docker 数据卷 +DOCKER_LOG_DIR="$ROOT_DIR/.deploy-test/docker-logs" # Docker 容器日志 +SCRIPT_LOG_DIR="$ROOT_DIR/.deploy-test/script-logs" # 脚本执行日志 +ENV_FILE="$ROOT_DIR/.env.deploy-test" + +# ── 颜色 ─────────────────────────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' + +info() { echo -e "${CYAN}[INFO]${NC} $*"; } +success() { echo -e "${GREEN}[OK]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } +step() { echo -e "\n${BOLD}${BLUE}▶ $*${NC}"; } +header() { + echo "" + echo -e "${BOLD}${BLUE}══════════════════════════════════════════${NC}" + echo -e "${BOLD}${BLUE} $*${NC}" + echo -e "${BOLD}${BLUE}══════════════════════════════════════════${NC}" +} + +# ── 初始化运行时目录 ──────────────────────────────────────────────────────────── +init_dirs() { + mkdir -p "$LOG_DIR" "$PID_DIR" "$BUILD_DIR" "$DATA_DIR" \ + "$DOCKER_LOG_DIR" "$SCRIPT_LOG_DIR" +} + +# ────────────────────────────────────────────────────────────────────────────── +# 脚本执行日志 +# 调用位置:每个脚本 init_dirs 之后 +# 效果:所有输出(stdout+stderr)同时写入 .local-dev/script-logs/-.log +# ────────────────────────────────────────────────────────────────────────────── +init_script_log() { + local script_name + script_name="$(basename "${BASH_SOURCE[1]:-$0}" .sh)" + local ts; ts="$(date +%Y%m%d-%H%M%S)" + export _CURRENT_SCRIPT_LOG="$SCRIPT_LOG_DIR/${script_name}-${ts}.log" + mkdir -p "$SCRIPT_LOG_DIR" + + # 写入文件头(纯文本,不含颜色码) + { + echo "========================================" + echo "Script : $script_name" + echo "Started: $(date '+%Y-%m-%d %H:%M:%S')" + echo "========================================" + } > "$_CURRENT_SCRIPT_LOG" + + # exec:将所有后续输出同时流向终端和日志文件 + # 用 sed 去除 ANSI 颜色码,保证日志文件可读 + exec > >(tee >(sed $'s/\033\\[[0-9;]*m//g' >> "$_CURRENT_SCRIPT_LOG")) 2>&1 + + info "脚本日志 → $_CURRENT_SCRIPT_LOG" +} + +# ────────────────────────────────────────────────────────────────────────────── +# Docker 容器日志收集 +# ────────────────────────────────────────────────────────────────────────────── + +# 启动后台日志收集进程:docker logs -f → 本地文件(按日期滚动) +start_docker_logger() { + local cname="$1" + local svc="${cname#dev-}" # dev-redis → redis + local log_dir="$DOCKER_LOG_DIR/$svc" + local logfile="$log_dir/${svc}-$(date +%Y%m%d).log" + local pid_file="$PID_DIR/docker-log-${cname}.pid" + + mkdir -p "$log_dir" + + # 停止已有的收集进程 + if [[ -f "$pid_file" ]] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then + kill "$(cat "$pid_file")" 2>/dev/null || true + sleep 0.3 + fi + + # 写分隔符,区分每次启动会话 + { + echo "" + echo "──── 容器启动 $(date '+%Y-%m-%d %H:%M:%S') ────" + } >> "$logfile" + + # 后台跟踪容器日志(docker logs 本身已含历史,--tail 0 只取新增) + # 首次启动时先 dump 当前快照,再 follow 新增 + docker logs "$cname" >> "$logfile" 2>&1 || true + docker logs -f --tail 0 "$cname" >> "$logfile" 2>&1 & + echo $! > "$pid_file" + + info " 容器日志 → $logfile" +} + +# 停止容器日志收集进程 +stop_docker_logger() { + local cname="$1" + local pid_file="$PID_DIR/docker-log-${cname}.pid" + if [[ -f "$pid_file" ]]; then + local pid; pid=$(cat "$pid_file") + kill "$pid" 2>/dev/null || true + rm -f "$pid_file" + fi +} + +# ── 加载 .env.local ───────────────────────────────────────────────────────────── +load_env() { + if [[ ! -f "$ENV_FILE" ]]; then + error ".env.local 不存在,请先执行: ./deploy-test/01-init-env.sh" + exit 1 + fi + set -a + # shellcheck source=/dev/null + source "$ENV_FILE" + set +a +} + +# ── 检查必要工具 ──────────────────────────────────────────────────────────────── +require_tools() { + local missing=() + for tool in "$@"; do + command -v "$tool" &>/dev/null || missing+=("$tool") + done + if [[ ${#missing[@]} -gt 0 ]]; then + error "缺少必要工具: ${missing[*]}" + for t in "${missing[@]}"; do + case "$t" in + go) echo " → 安装 Go: https://go.dev/dl/" ;; + docker) echo " → 安装 Docker Desktop: https://www.docker.com/products/docker-desktop/" ;; + esac + done + exit 1 + fi +} + +# ── Docker daemon 检查 ────────────────────────────────────────────────────────── +require_docker_running() { + require_tools docker + if ! docker info &>/dev/null; then + error "Docker daemon 未运行,请启动 Docker Desktop" + exit 1 + fi +} + +# ── 启动单个后端服务(nohup 后台) ─────────────────────────────────────────────── +start_svc() { + local name="$1" bin="$2" args="${3:-}" workdir="${4:-$ROOT_DIR}" + local pidfile="$PID_DIR/$name.pid" logfile="$LOG_DIR/$name.log" + + if [[ -f "$pidfile" ]] && kill -0 "$(cat "$pidfile")" 2>/dev/null; then + warn "$name 已在运行 (PID=$(cat "$pidfile")),跳过" + return 0 + fi + + [[ ! -f "$bin" ]] && { error "$name 二进制不存在 ($bin),请先执行 04-build.sh"; return 1; } + + info "启动 $name ..." + ( + cd "$workdir" + # shellcheck disable=SC2086 + nohup "$bin" $args > "$logfile" 2>&1 & + echo $! > "$pidfile" + ) + sleep 1 + if kill -0 "$(cat "$pidfile")" 2>/dev/null; then + success " $name 已启动 (PID=$(cat "$pidfile")) → $logfile" + else + error " $name 启动失败,查看日志:" + tail -20 "$logfile" 2>/dev/null || true + return 1 + fi +} + +# ── 停止单个后端服务 ──────────────────────────────────────────────────────────── +stop_svc() { + local name="$1" pidfile="$PID_DIR/$name.pid" + if [[ -f "$pidfile" ]]; then + local pid; pid=$(cat "$pidfile") + if kill -0 "$pid" 2>/dev/null; then + kill "$pid" && success "$name 已停止 (PID=$pid)" + else + warn "$name 进程 $pid 不存在(可能已退出)" + fi + rm -f "$pidfile" + else + warn "$name 没有 PID 记录(未运行)" + fi +} + +# ── Docker 容器状态打印 ───────────────────────────────────────────────────────── +print_container_status() { + local label="$1" cname="$2" port="$3" + if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then + printf " ${GREEN}●${NC} %-12s container=%-14s :%-5s\n" "$label" "$cname" "$port" + elif docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then + printf " ${YELLOW}○${NC} %-12s stopped=%-14s :%-5s\n" "$label" "$cname" "$port" + else + printf " ${RED}✗${NC} %-12s (未创建) :%-5s\n" "$label" "$port" + fi +} + +# ── 后端服务状态打印 ──────────────────────────────────────────────────────────── +print_svc_status() { + local name="$1" desc="$2" pidfile="$PID_DIR/$name.pid" + if [[ -f "$pidfile" ]] && kill -0 "$(cat "$pidfile")" 2>/dev/null; then + printf " ${GREEN}●${NC} %-18s PID=%-7s %s\n" "$name" "$(cat "$pidfile")" "$desc" + else + printf " ${RED}○${NC} %-18s %-11s %s\n" "$name" "未运行" "$desc" + fi +} + +# ── 所有后端服务名列表 ────────────────────────────────────────────────────────── +ALL_SVCS=(openim-server chat-rpc admin-rpc chat-api admin-api meetingmsg livecloud livestream build-server) diff --git a/logs.sh b/logs.sh new file mode 100755 index 0000000..0bbfb56 --- /dev/null +++ b/logs.sh @@ -0,0 +1,205 @@ +#!/usr/bin/env bash +# ============================================================================= +# logs.sh — 查看服务日志 +# +# 用法: +# ./logs.sh # tail -f 实时跟踪日志 +# ./logs.sh --last # 只显示最后 100 行(不跟踪) +# ./logs.sh -n 50 # 显示最后 50 行并跟踪 +# ./logs.sh scripts # 列出所有脚本执行日志 +# ./logs.sh scripts --last # 查看最新一次脚本日志 +# +# 日志目录一览: +# .local-dev/logs/ — 后端服务运行日志 +# .local-dev/docker-logs/ — Docker 容器日志(每日一文件) +# .local-dev/script-logs/ — 脚本执行日志(带时间戳) +# +# 后端服务: openim-server, chat-rpc, admin-rpc, chat-api, admin-api, +# meetingmsg, livecloud, livestream, build-server +# 前端服务: pc, meetingh5, h5, cms, build-cms, build-down +# Docker: redis, kafka, etcd, livekit +# ============================================================================= +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +# 注意:logs.sh 不调用 init_script_log,避免 tail -f 被重定向到日志文件 + +SVC="${1:-}" +OPT="${2:-}" +NLINES="${3:-100}" + +# ── 无参数:打印概览 ────────────────────────────────────────────────────────── +if [[ -z "$SVC" ]]; then + echo "" + echo -e "${BOLD}用法:${NC} $0 [--last|-n ]" + echo "" + + echo -e "${BOLD}后端服务日志${NC} ($LOG_DIR/):" + for svc in "${ALL_SVCS[@]}"; do + local_log="$LOG_DIR/$svc.log" + if [[ -f "$local_log" ]]; then + size=$(du -sh "$local_log" 2>/dev/null | awk '{print $1}') + printf " %-18s %s (%s)\n" "$svc" "$local_log" "$size" + else + printf " %-18s (无日志)\n" "$svc" + fi + done + + echo "" + echo -e "${BOLD}Docker 容器日志${NC} ($DOCKER_LOG_DIR/):" + for svc in redis kafka etcd livekit; do + log_dir="$DOCKER_LOG_DIR/$svc" + if [[ -d "$log_dir" ]]; then + latest=$(ls -t "$log_dir"/*.log 2>/dev/null | head -1 || echo "") + if [[ -n "$latest" ]]; then + size=$(du -sh "$latest" 2>/dev/null | awk '{print $1}') + printf " %-10s %s (%s)\n" "$svc" "$latest" "$size" + else + printf " %-10s (目录存在,暂无日志文件)\n" "$svc" + fi + else + printf " %-10s (未启动)\n" "$svc" + fi + done + + echo "" + echo -e "${BOLD}前端服务日志${NC} ($LOG_DIR/fe-*.log):" + FE_LIST=(pc meetingh5 h5 cms build-cms build-down) + for fe in "${FE_LIST[@]}"; do + fe_log="$LOG_DIR/fe-${fe}.log" + if [[ -f "$fe_log" ]]; then + size=$(du -sh "$fe_log" 2>/dev/null | awk '{print $1}') + printf " %-14s %s (%s)\n" "$fe" "$fe_log" "$size" + else + printf " %-14s (无日志)\n" "$fe" + fi + done + + echo "" + echo -e "${BOLD}脚本执行日志${NC} ($SCRIPT_LOG_DIR/):" + if [[ -d "$SCRIPT_LOG_DIR" ]]; then + ls -t "$SCRIPT_LOG_DIR"/*.log 2>/dev/null | head -5 | while read -r f; do + size=$(du -sh "$f" 2>/dev/null | awk '{print $1}') + printf " %s (%s)\n" "$(basename "$f")" "$size" + done + count=$(ls "$SCRIPT_LOG_DIR"/*.log 2>/dev/null | wc -l | tr -d ' ') + [[ "$count" -gt 5 ]] && echo " ... 共 $count 个文件" + else + echo " (尚无脚本日志)" + fi + echo "" + exit 0 +fi + +# ── 脚本执行日志 ────────────────────────────────────────────────────────────── +if [[ "$SVC" == "scripts" ]]; then + if [[ ! -d "$SCRIPT_LOG_DIR" ]] || [[ -z "$(ls "$SCRIPT_LOG_DIR"/*.log 2>/dev/null)" ]]; then + warn "暂无脚本执行日志 ($SCRIPT_LOG_DIR/)" + exit 0 + fi + + if [[ "$OPT" == "--last" ]]; then + latest=$(ls -t "$SCRIPT_LOG_DIR"/*.log | head -1) + info "最新脚本日志: $latest" + echo "──────────────────────────────────────" + cat "$latest" + else + info "所有脚本执行日志 ($SCRIPT_LOG_DIR/):" + ls -lht "$SCRIPT_LOG_DIR"/*.log 2>/dev/null | awk '{printf " %-8s %s %s\n", $5, $6" "$7" "$8, $9}' + echo "" + echo "查看最新: $0 scripts --last" + echo "查看指定: cat $SCRIPT_LOG_DIR/" + fi + exit 0 +fi + +# ── Docker 容器日志 ─────────────────────────────────────────────────────────── +_docker_log() { + local cname="$1" + local log_dir="$DOCKER_LOG_DIR/${cname#dev-}" + local latest_file + + info "$cname 容器日志" + + # 展示日志文件路径 + if [[ -d "$log_dir" ]]; then + latest_file=$(ls -t "$log_dir"/*.log 2>/dev/null | head -1 || echo "") + if [[ -n "$latest_file" ]]; then + info "本地日志文件: $latest_file" + fi + fi + + echo "──────────────────────────────────────" + + if [[ "$OPT" == "--last" ]]; then + docker logs --tail "$NLINES" "$cname" 2>&1 + else + docker logs -f --tail "${NLINES}" "$cname" 2>&1 + fi +} + +case "$SVC" in + redis) _docker_log "dev-redis"; exit 0 ;; + kafka) _docker_log "dev-kafka"; exit 0 ;; + etcd) _docker_log "dev-etcd"; exit 0 ;; + livekit) _docker_log "dev-livekit"; exit 0 ;; +esac + +# ── 前端服务日志 ────────────────────────────────────────────────────────────── +FE_LIST=(pc meetingh5 h5 cms build-cms build-down) +for _fe in "${FE_LIST[@]}"; do + if [[ "$SVC" == "$_fe" ]]; then + LOGFILE="$LOG_DIR/fe-${SVC}.log" + pidfile="$PID_DIR/fe-${SVC}.pid" + if [[ ! -f "$LOGFILE" ]]; then + error "日志文件不存在: $LOGFILE" + echo " $SVC 可能尚未启动。启动命令: ./deploy-test/07-start-frontend.sh $SVC" + exit 1 + fi + if [[ -f "$pidfile" ]] && kill -0 "$(cat "$pidfile")" 2>/dev/null; then + info "$SVC 正在运行 (PID=$(cat "$pidfile"))" + else + warn "$SVC 当前未运行(显示历史日志)" + fi + info "日志文件: $LOGFILE" + size=$(du -sh "$LOGFILE" 2>/dev/null | awk '{print $1}') + info "文件大小: $size" + echo "──────────────────────────────────────" + if [[ "$OPT" == "--last" ]]; then + tail -n "${NLINES}" "$LOGFILE" + elif [[ "$OPT" == "-n" ]]; then + tail -f -n "${NLINES}" "$LOGFILE" + else + tail -f -n 100 "$LOGFILE" + fi + exit 0 + fi +done + +# ── 后端服务日志 ────────────────────────────────────────────────────────────── +LOGFILE="$LOG_DIR/$SVC.log" + +if [[ ! -f "$LOGFILE" ]]; then + error "日志文件不存在: $LOGFILE" + echo " 服务 $SVC 可能尚未启动。启动命令: ./deploy-test/05-start.sh $SVC" + exit 1 +fi + +PIDFILE="$PID_DIR/$SVC.pid" +if [[ -f "$PIDFILE" ]] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + info "$SVC 正在运行 (PID=$(cat "$PIDFILE"))" +else + warn "$SVC 当前未运行(显示历史日志)" +fi + +info "日志文件: $LOGFILE" +size=$(du -sh "$LOGFILE" 2>/dev/null | awk '{print $1}') +info "文件大小: $size" +echo "──────────────────────────────────────" + +if [[ "$OPT" == "--last" ]]; then + tail -n "${NLINES}" "$LOGFILE" +elif [[ "$OPT" == "-n" ]]; then + tail -f -n "${NLINES}" "$LOGFILE" +else + tail -f -n 100 "$LOGFILE" +fi diff --git a/remove-infra.sh b/remove-infra.sh new file mode 100755 index 0000000..30bfa60 --- /dev/null +++ b/remove-infra.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# ============================================================================= +# remove-infra.sh — 删除 Docker 容器及本地持久化数据 +# +# ⚠️ 危险操作:本地 Redis / Kafka / Etcd 数据将全部清除 +# 适用场景:环境损坏需重置,或希望全新干净启动 +# ============================================================================= +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +require_docker_running +init_script_log # ← 脚本执行日志 + +step "删除 Docker 基础设施容器及数据" + +echo "" +warn "⚠️ 此操作将删除以下内容:" + echo " 容器: dev-livekit, dev-redis, dev-kafka, dev-etcd" +echo " 数据: $DATA_DIR/" +echo "" +read -p "确认删除?(输入 yes 继续): " -r CONFIRM +if [[ "$CONFIRM" != "yes" ]]; then + info "已取消" + exit 0 +fi + +echo "" + for cname in dev-livekit dev-redis dev-kafka dev-etcd; do + if docker ps -a --format '{{.Names}}' | grep -q "^${cname}$"; then + docker rm -f "$cname" > /dev/null && success "已删除容器: $cname" + else + info "容器不存在,跳过: $cname" + fi +done + +if [[ -d "$DATA_DIR" ]]; then + rm -rf "$DATA_DIR" + success "已删除数据目录: $DATA_DIR" +fi + +echo "" +success "清理完成" +echo -e " 重新初始化: ${CYAN}./deploy-test/03-start-infra.sh${NC}" diff --git a/restart.sh b/restart.sh new file mode 100755 index 0000000..5d884fd --- /dev/null +++ b/restart.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# ============================================================================= +# restart.sh — 重启指定服务 +# +# 用法: +# ./restart.sh # 重启后端服务(使用已有二进制) +# ./restart.sh --build # 先重新编译再重启 +# ./restart.sh # 重启 Docker 容器(redis/kafka/etcd) +# +# 示例: +# ./restart.sh chat-api +# ./restart.sh chat-api --build +# ./restart.sh redis +# ============================================================================= +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +load_env +init_script_log # ← 脚本执行日志 + +SVC="${1:-}" +OPT="${2:-}" + +[[ -z "$SVC" ]] && { error "用法: $0 [--build]"; exit 1; } + +# ── 服务配置(与 05-start.sh 保持一致)─────────────────────────────────────── +declare -A svc_workdir=( + [openim-server]="$ROOT_DIR/open-im-server" + [chat-rpc]="$ROOT_DIR/chat" + [admin-rpc]="$ROOT_DIR/chat" + [chat-api]="$ROOT_DIR/chat" + [admin-api]="$ROOT_DIR/chat" + [meetingmsg]="$ROOT_DIR/meetingmsg" + [livecloud]="$ROOT_DIR/livecloud" + [livestream]="$ROOT_DIR/livestream" +) + +declare -A svc_args=( + [openim-server]="-c $ROOT_DIR/open-im-server/config" + [chat-rpc]="-c $ROOT_DIR/chat/config" + [admin-rpc]="-c $ROOT_DIR/chat/config" + [chat-api]="-c $ROOT_DIR/chat/config" + [admin-api]="-c $ROOT_DIR/chat/config" + [meetingmsg]="" + [livecloud]="" + [livestream]="" +) + +declare -A svc_src_dir=( + [openim-server]="open-im-server" + [chat-rpc]="chat" + [admin-rpc]="chat" + [chat-api]="chat" + [admin-api]="chat" + [meetingmsg]="meetingmsg" + [livecloud]="livecloud" + [livestream]="livestream" +) + +declare -A svc_src_pkg=( + [openim-server]="./cmd/main.go" + [chat-rpc]="./cmd/rpc/chat-rpc/" + [admin-rpc]="./cmd/rpc/admin-rpc/" + [chat-api]="./cmd/api/chat-api/" + [admin-api]="./cmd/api/admin-api/" + [meetingmsg]="." + [livecloud]="." + [livestream]="." +) + +# ── Docker 容器重启 ─────────────────────────────────────────────────────────── +_restart_docker() { + local label="$1" cname="$2" + require_docker_running + if docker ps -a --format '{{.Names}}' | grep -q "^${cname}$"; then + info "重启容器 $cname ..." + docker restart "$cname" > /dev/null + sleep 2 + if docker ps --format '{{.Names}}' | grep -q "^${cname}$"; then + success "$label 容器已重启 (container=$cname)" + else + error "$label 重启失败" + fi + else + warn "容器 $cname 不存在,请先执行 03-start-infra.sh" + fi +} + +# ── 处理 Docker 容器 ────────────────────────────────────────────────────────── +case "$SVC" in + redis) _restart_docker "Redis" "dev-redis"; exit 0 ;; + kafka) _restart_docker "Kafka" "dev-kafka"; exit 0 ;; + etcd) _restart_docker "Etcd" "dev-etcd"; exit 0 ;; +esac + +# ── 处理后端服务 ────────────────────────────────────────────────────────────── +if [[ -z "${svc_workdir[$SVC]:-}" ]]; then + error "未知服务: $SVC" + echo "后端服务: ${!svc_workdir[*]}" + echo "Docker: redis, kafka, etcd" + exit 1 +fi + +step "重启服务: $SVC" + +# 是否先重新编译 +if [[ "$OPT" == "--build" ]]; then + info "重新编译 $SVC ..." + dir="$ROOT_DIR/${svc_src_dir[$SVC]}" + pkg="${svc_src_pkg[$SVC]}" + (cd "$dir" && go build -o "$BUILD_DIR/$SVC" "$pkg") && \ + success "编译完成" || { error "编译失败"; exit 1; } +fi + +# 停止旧进程 +stop_svc "$SVC" +sleep 1 + +# 启动新进程 +start_svc "$SVC" \ + "$BUILD_DIR/$SVC" \ + "${svc_args[$SVC]}" \ + "${svc_workdir[$SVC]}" + +echo "" +echo -e "查看日志: ${CYAN}./deploy-test/logs.sh $SVC${NC}" diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..252f46d --- /dev/null +++ b/setup.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# ============================================================================= +# setup.sh — 一键完整部署(首次使用) +# +# 按顺序执行: +# 01-init-env.sh → 生成 .env.local +# 02-patch-config.sh → 写入服务 YAML 配置 +# 03-start-infra.sh → 启动 Docker 容器 +# 04-build.sh → 编译所有 Go 服务 +# 05-start.sh → 启动所有后端服务 +# +# 各步骤均可独立重新执行,不必从头来过。 +# ============================================================================= +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" +init_dirs +init_script_log # ← 脚本执行日志 + +header "一键完整部署(首次使用)" + +echo -e "${BOLD}基础设施策略:${NC}" +echo " Redis / Kafka / Etcd → Docker 容器(本地)" +echo " MongoDB → 远程服务" +echo " 文件存储 → Amazon S3" +echo "" + +# ── 步骤 1:初始化 .env.local ───────────────────────────────────────────────── +step "[1/5] 初始化 .env.local" +bash "$SCRIPT_DIR/01-init-env.sh" + +echo "" +warn "请确认 .env.local 中的 MongoDB 和 AWS S3 配置已正确填写!" +echo -e " ${CYAN}vim $ENV_FILE${NC}" +echo "" +read -p "配置已填写好,继续执行?(y/N): " -n 1 -r REPLY; echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + info "已暂停,编辑完成后重新执行: ./deploy-test/setup.sh" + info "或跳过此步直接从步骤 2 开始: ./deploy-test/02-patch-config.sh" + exit 0 +fi + +# ── 步骤 2:写入服务配置 ────────────────────────────────────────────────────── +step "[2/5] 修改服务配置文件" +bash "$SCRIPT_DIR/02-patch-config.sh" + +# ── 步骤 3:启动 Docker 基础设施 ───────────────────────────────────────────── +step "[3/5] 启动 Docker 基础设施" +bash "$SCRIPT_DIR/03-start-infra.sh" + +info "等待基础设施就绪 (5s)..." +sleep 5 + +# ── 步骤 4:编译 ────────────────────────────────────────────────────────────── +step "[4/5] 编译所有后端服务" +bash "$SCRIPT_DIR/04-build.sh" + +# ── 步骤 5:启动 ────────────────────────────────────────────────────────────── +step "[5/5] 启动所有后端服务" +bash "$SCRIPT_DIR/05-start.sh" + +# ── 完成汇总 ───────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════╗${NC}" +echo -e "${BOLD}${GREEN}║ 本地环境部署完成! ║${NC}" +echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════╝${NC}" +echo "" +echo -e "${BOLD}服务地址:${NC}" +echo " IM API: http://localhost:10002" +echo " IM WebSocket: ws://localhost:10001" +echo " Chat API: http://localhost:10008" +echo " Admin API: http://localhost:10009" +echo " MeetingMsg WS: ws://localhost:8000" +echo " Livecloud: http://localhost:8080" +echo " Livestream: http://localhost:8081" +echo "" +echo -e "${BOLD}日常命令:${NC}" +echo " ./deploy-test/status.sh # 查看全部状态" +echo " ./deploy-test/logs.sh # 实时日志" +echo " ./deploy-test/restart.sh # 重启服务" +echo " ./deploy-test/restart.sh --build # 重编译并重启" +echo " ./deploy-test/check-conn.sh # 验证 MongoDB/S3" +echo "" +echo -e "${BOLD}停止服务:${NC}" +echo " ./deploy-test/stop.sh # 停止后端进程" +echo " ./deploy-test/stop-infra.sh # 停止 Docker 容器" +echo "" diff --git a/status.sh b/status.sh new file mode 100755 index 0000000..d77665a --- /dev/null +++ b/status.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# ============================================================================= +# status.sh — 查看所有服务运行状态 +# +# 显示内容: +# - Docker 容器状态(Redis/Kafka/Etcd) +# - 远程服务配置摘要(MongoDB/S3) +# - 后端服务进程状态(PID + 监听端口) +# ============================================================================= +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +load_env 2>/dev/null || true + +header "服务运行状态" + +# ── Docker 基础设施 ─────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}[ Docker 基础设施 ]${NC}" +print_container_status "Redis" "dev-redis" "${REDIS_PORT:-6379}" +print_container_status "Kafka" "dev-kafka" "${KAFKA_PORT:-9092}" +print_container_status "Etcd" "dev-etcd" "${ETCD_PORT:-2379}" +print_container_status "LiveKit" "dev-livekit" "7880" +printf " ${CYAN}◉${NC} %-10s 公网 %s:50000-51000/udp (WebRTC)\n" "" "${LIVEKIT_NODE_IP:-?}" + +# ── 远程服务 ───────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}[ 远程服务(连接配置)]${NC}" +printf " ${CYAN}◉${NC} %-10s %s\n" "MongoDB" \ + "${MONGO_HOST:-?}:${MONGO_PORT:-27017}/${MONGO_DATABASE:-?} (authSource=${MONGO_AUTHSOURCE:-?})" +printf " ${CYAN}◉${NC} %-10s %s\n" "S3" \ + "s3://${AWS_BUCKET:-?} region=${AWS_REGION:-?}" + +# ── 后端服务 ───────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}[ 后端服务 ]${NC}" +print_svc_status "openim-server" ":10002 (API) :10001 (MsgGateway WS)" +print_svc_status "chat-rpc" "内部 RPC → Etcd" +print_svc_status "admin-rpc" "内部 RPC → Etcd" +print_svc_status "chat-api" ":10008" +print_svc_status "admin-api" ":10009" +print_svc_status "meetingmsg" ":8000 (WS)" +print_svc_status "livecloud" ":8080" +print_svc_status "livestream" ":8081" +print_svc_status "build-server" ":8281" + +# ── 前端服务 ───────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}[ 前端开发服务器 ]${NC}" +declare -A FE_PORT_MAP=( + [pc]="7777 (Electron Vite)" + [meetingh5]="5188" + [h5]="3003" + [cms]="8001" + [build-cms]="8002" + [build-down]="8003" +) +for fe in pc meetingh5 h5 cms build-cms build-down; do + pidfile="$PID_DIR/fe-${fe}.pid" + logfile="$LOG_DIR/fe-${fe}.log" + if [[ -f "$pidfile" ]] && kill -0 "$(cat "$pidfile")" 2>/dev/null; then + printf " ${GREEN}●${NC} %-14s PID=%-7s :%s\n" "$fe" "$(cat "$pidfile")" "${FE_PORT_MAP[$fe]}" + else + printf " ${RED}○${NC} %-14s 未运行 :%s\n" "$fe" "${FE_PORT_MAP[$fe]}" + fi +done + +# ── 端口占用检查 ────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}[ 端口占用 ]${NC}" +PORTS=(10002 10001 10008 10009 8000 8080 8081 8281 7777 5188 3003 8001 8002 8003) +for port in "${PORTS[@]}"; do + pid=$(lsof -ti :"$port" 2>/dev/null | head -1 || true) + if [[ -n "$pid" ]]; then + cmd=$(ps -p "$pid" -o comm= 2>/dev/null || echo "?") + printf " ${GREEN}●${NC} :%d PID=%-6s (%s)\n" "$port" "$pid" "$cmd" + else + printf " ${YELLOW}○${NC} :%d (未监听)\n" "$port" + fi +done + +# ── 快速操作提示 ────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}[ 快捷命令 ]${NC}" +echo " ./deploy-test/logs.sh 查看日志(支持前端: pc/h5/cms 等)" +echo " ./deploy-test/restart.sh 重启后端服务" +echo " ./deploy-test/restart.sh --build 重编译并重启" +echo " ./deploy-test/07-start-frontend.sh 启动前端服务" +echo " ./deploy-test/stop-frontend.sh 停止前端服务" +echo " ./deploy-test/check-conn.sh 验证 MongoDB/S3 连接" +echo "" diff --git a/stop-frontend.sh b/stop-frontend.sh new file mode 100755 index 0000000..7dc27b7 --- /dev/null +++ b/stop-frontend.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# ============================================================================= +# stop-frontend.sh — 停止前端开发服务器 +# +# 用法: +# ./stop-frontend.sh # 停止全部前端服务 +# ./stop-frontend.sh # 只停止指定项目 +# +# 可用项目: pc, meetingh5, h5, cms, build-cms, build-down +# ============================================================================= +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +init_dirs +init_script_log # ← 脚本执行日志 + +FE_PROJECTS=(pc meetingh5 h5 cms build-cms build-down) +TARGET="${1:-all}" + +_stop_fe() { + local name="$1" pidfile="$PID_DIR/fe-${name}.pid" + if [[ -f "$pidfile" ]]; then + local pid; pid=$(cat "$pidfile") + if kill -0 "$pid" 2>/dev/null; then + # 杀掉整个进程组(覆盖 npm/pnpm/yarn 子进程) + kill -- -"$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' ')" 2>/dev/null || kill "$pid" 2>/dev/null || true + success "$name 已停止 (PID=$pid)" + else + warn "$name 进程不存在(可能已退出)" + fi + rm -f "$pidfile" + else + warn "$name 没有 PID 记录(未运行)" + fi +} + +if [[ "$TARGET" == "all" ]]; then + step "停止全部前端开发服务器" + for proj in "${FE_PROJECTS[@]}"; do + _stop_fe "$proj" + done + success "所有前端服务已停止" +else + local_valid=false + for p in "${FE_PROJECTS[@]}"; do + [[ "$p" == "$TARGET" ]] && local_valid=true && break + done + if ! $local_valid; then + error "未知项目: $TARGET" + echo "可用: ${FE_PROJECTS[*]}" + exit 1 + fi + step "停止: $TARGET" + _stop_fe "$TARGET" +fi diff --git a/stop-infra.sh b/stop-infra.sh new file mode 100755 index 0000000..9ad2d27 --- /dev/null +++ b/stop-infra.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# ============================================================================= +# stop-infra.sh — 停止 Docker 基础设施容器 +# +# 用法: +# ./stop-infra.sh # 停止 Redis / Kafka / Etcd +# ./stop-infra.sh redis # 只停止 Redis +# ./stop-infra.sh kafka # 只停止 Kafka +# ./stop-infra.sh etcd # 只停止 Etcd +# +# 注意:仅停止容器,不删除数据。重新启动执行: 03-start-infra.sh +# ============================================================================= +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +require_docker_running +init_script_log # ← 脚本执行日志 + +TARGET="${1:-all}" + +_stop_container() { + local label="$1" cname="$2" + # 先停日志收集进程 + stop_docker_logger "$cname" + if docker ps --format '{{.Names}}' | grep -q "^${cname}$"; then + docker stop "$cname" > /dev/null && success "$label 容器已停止 (container=$cname)" + else + warn "$label 容器未在运行 (container=$cname)" + fi +} + +case "$TARGET" in + all) + step "停止所有 Docker 基础设施" + _stop_container "LiveKit" "dev-livekit" + _stop_container "Kafka" "dev-kafka" + _stop_container "Redis" "dev-redis" + _stop_container "Etcd" "dev-etcd" + echo "" + success "所有 Docker 容器已停止(数据已保留)" + echo -e " 重新启动: ${CYAN}./deploy-test/03-start-infra.sh${NC}" + ;; + redis) _stop_container "Redis" "dev-redis" ;; + kafka) _stop_container "Kafka" "dev-kafka" ;; + etcd) _stop_container "Etcd" "dev-etcd" ;; + livekit) _stop_container "LiveKit" "dev-livekit" ;; + *) + error "未知组件: $TARGET" + echo "可用: redis, kafka, etcd, livekit, all" + exit 1 + ;; +esac diff --git a/stop.sh b/stop.sh new file mode 100755 index 0000000..82ebd34 --- /dev/null +++ b/stop.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# ============================================================================= +# stop.sh — 停止后端 Go 服务 +# +# 用法: +# ./stop.sh # 停止全部后端服务 +# ./stop.sh # 只停止指定服务 +# +# 注意:此命令只停止后端进程,不影响 Docker 容器(Redis/Kafka/Etcd) +# 如需停止 Docker 容器,执行: ./stop-infra.sh +# ============================================================================= +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" +init_dirs +init_script_log # ← 脚本执行日志 + +TARGET="${1:-all}" + +if [[ "$TARGET" == "all" ]]; then + step "停止所有后端服务" + for svc in "${ALL_SVCS[@]}"; do + stop_svc "$svc" + done + echo "" + success "所有后端服务已停止" + echo "" + echo -e "如需停止 Docker 基础设施:" + echo -e " ${CYAN}./deploy-test/stop-infra.sh${NC}" +else + step "停止服务: $TARGET" + # 验证服务名合法 + local_valid=false + for svc in "${ALL_SVCS[@]}"; do + [[ "$svc" == "$TARGET" ]] && local_valid=true && break + done + if ! $local_valid; then + error "未知服务: $TARGET" + echo "可用: ${ALL_SVCS[*]}" + exit 1 + fi + stop_svc "$TARGET" +fi