Files
fanqie-go/deploy.sh
T
chenxin 49d0a371fb ✨ feat(video): 新增视频 CENC 解密与流式代理播放
- 逆向 SpadeA(黑桃A) PlayAuth 混淆,从 spade_a 还原 AES-128 内容密钥
- 实现 cenc-aes-ctr MP4 解密(senc 逐样本 CTR + stsd entry 修复),实测可播放
- 新增 /video/proxy/{key} 代理:稳定 key + 磁盘缓存(二次命中提速 130x)+
  http.ServeContent 支持 Range/206/seek 流式播放
- /video 接口新增 output(url/proxy,默认 proxy) 与 quality(max/720p/档位key) 参数,
  可由 config fanqie.video.* 配置默认值;无匹配清晰度时明确报错
- 代理映射复用 audio_cache 存储,抽取 loadMediaProxyEntry 共用;挂接 7 天缓存清理
- deploy.sh 新增 --sync-nginx:备份→重写→校验→失败自动回滚,同步视频代理段配置
2026-10-10 12:58:25 +08:00

478 lines
16 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.
#!/bin/bash
# ============================================================
# Go 应用一键部署脚本
#
# 用法:
# ./deploy.sh # 编译 + 部署(自动版本号)
# ./deploy.sh v1.2.0 # 编译 + 部署(指定版本)
# ./deploy.sh v1.2.0 --init # 首次初始化(写 systemd/nginx 配置)
# ./deploy.sh v1.2.0 --no-pull # 跳过 git pull
# ./deploy.sh --build-only # 只编译,不部署
# ./deploy.sh --rollback # 交互式回滚到指定版本
# ./deploy.sh --rollback v1.0.0 # 直接回滚到指定版本
# ./deploy.sh --list # 列出所有已部署版本
# ./deploy.sh --sync-nginx # 更新线上 nginx 配置(备份+校验+失败回滚)
# ./deploy.sh --help # 显示帮助
# ============================================================
set -euo pipefail
# ==================== 加载本地配置 ====================
# .deploy.env(不入库)覆盖下方默认值;模板见 .deploy.env.example
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "${SCRIPT_DIR}/.deploy.env" ]; then
# shellcheck disable=SC1091
source "${SCRIPT_DIR}/.deploy.env"
fi
# ==================== 配置区(默认值,可被 .deploy.env 覆盖) ====================
APP_NAME="${APP_NAME:-fanqie}" # 应用名(用于服务名、目录名)
APP_DESC="${APP_DESC:-番茄小说api接口}" # 服务描述
RUN_USER="${RUN_USER:-$(whoami)}" # 运行用户(默认当前用户)
SRC_DIR="${SRC_DIR:-/root/code/novel/fanqie-go}" # 代码目录
BIN_DIR="${BIN_DIR:-/opt/${APP_NAME}/bin}" # 二进制存放目录
SERVICE_FILE="${SERVICE_FILE:-/etc/systemd/system/${APP_NAME}.service}"
NGINX_CONF="${NGINX_CONF:-/etc/nginx/conf.d/${APP_NAME}.conf}"
APP_PORT="${APP_PORT:-40180}" # 程序监听端口
DOMAIN="${DOMAIN:-fq.20010117.xyz}" # 域名
CERT_EMAIL="${CERT_EMAIL:-admin@${DOMAIN}}" # 证书申请邮箱(过期提醒)
BUILD_TARGET="${BUILD_TARGET:-./cmd/fanqie}" # go build 的包路径
ENV_FILE="${ENV_FILE:-${SRC_DIR}/.env}" # 应用环境变量(.env.example 为模板)
KEEP_VERSIONS="${KEEP_VERSIONS:-5}" # 保留最近几个版本
# ==================== 颜色与日志 ====================
C_GREEN='\033[1;32m'; C_YELLOW='\033[1;33m'; C_RED='\033[1;31m'; C_BLUE='\033[1;34m'; C_RESET='\033[0m'
log() { echo -e "${C_GREEN}[$(date '+%H:%M:%S')]${C_RESET} $*"; }
warn() { echo -e "${C_YELLOW}[警告]${C_RESET} $*"; }
err() { echo -e "${C_RED}[错误]${C_RESET} $*" >&2; }
info() { echo -e "${C_BLUE}[信息]${C_RESET} $*"; }
# 非 root 时自动加 sudo
SUDO=""
if [ "$(id -u)" -ne 0 ]; then
SUDO="sudo"
fi
# ==================== 参数解析 ====================
VERSION=""
INIT=false
NO_PULL=false
BUILD_ONLY=false
ROLLBACK=false
ROLLBACK_VERSION=""
LIST=false
FORCE=false
SYNC_NGINX=false
print_help() {
cat <<EOF
用法: $0 [选项] [版本号]
选项:
--init 首次初始化,写入 systemd 和 nginx 配置
--no-pull 跳过 git pull
--build-only 只编译,不部署、不重启服务
--force, -f 允许覆盖已存在的同名版本(默认报错退出)
--rollback [版本] 回滚到指定版本(不带版本号则交互式选择)
--list 列出所有已部署的版本
--sync-nginx 只更新 nginx 配置(备份旧配置,校验失败自动回滚)
--help, -h 显示此帮助
示例:
$0 # 编译 + 部署(自动版本号)
$0 v1.2.0 # 编译 + 部署(指定版本)
$0 v1.2.0 --init # 首次初始化部署
$0 --build-only # 只编译
$0 --rollback # 交互式回滚
$0 --rollback v1.0.0 # 回滚到 v1.0.0
$0 --list # 查看所有版本
EOF
}
i=0
ARGS=("$@")
while [ $i -lt ${#ARGS[@]} ]; do
arg="${ARGS[$i]}"
case "$arg" in
--init) INIT=true ;;
--no-pull) NO_PULL=true ;;
--build-only) BUILD_ONLY=true ;;
--rollback)
ROLLBACK=true
# 看下一个参数是不是版本号
if [ $((i+1)) -lt ${#ARGS[@]} ] && [[ "${ARGS[$((i+1))]}" != --* ]]; then
ROLLBACK_VERSION="${ARGS[$((i+1))]}"
i=$((i+1))
fi
;;
--list) LIST=true ;;
--sync-nginx) SYNC_NGINX=true ;;
--force|-f) FORCE=true ;;
--help|-h) print_help; exit 0 ;;
v*) VERSION="$arg" ;;
*) err "未知参数: $arg"; print_help; exit 1 ;;
esac
i=$((i+1))
done
VERSION="${VERSION:-v$(date +%Y%m%d-%H%M%S)}"
BUILD_TIME=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
# ==================== 辅助函数 ====================
# 原子切换软链接
switch_symlink() {
local target="$1"
$SUDO ln -sfn "$target" "${BIN_DIR}/${APP_NAME}-current.tmp"
$SUDO mv -Tf "${BIN_DIR}/${APP_NAME}-current.tmp" "${BIN_DIR}/${APP_NAME}-current"
}
# 列出所有版本
list_versions() {
if [ ! -d "${BIN_DIR}" ]; then
warn "二进制目录不存在: ${BIN_DIR}"
return 0
fi
local current
current=$(readlink -f "${BIN_DIR}/${APP_NAME}-current" 2>/dev/null | xargs -r basename || echo "")
info "已部署版本(${BIN_DIR}):"
echo ""
local i=1
for f in $(ls -1t ${BIN_DIR}/${APP_NAME}-v* 2>/dev/null); do
local name
name=$(basename "$f")
if [ "$name" = "$current" ]; then
echo -e " ${C_GREEN}* ${name} (当前版本)${C_RESET}"
else
echo " ${name}"
fi
i=$((i+1))
done
if [ $i -eq 1 ]; then
warn "没有找到任何版本"
fi
}
# 清理旧版本
cleanup_old_versions() {
log "清理旧版本(保留最近 ${KEEP_VERSIONS} 个)..."
local current
current=$(readlink -f "${BIN_DIR}/${APP_NAME}-current" 2>/dev/null | xargs -r basename || echo "")
local count=0
# 按时间倒序列出,保留前 N 个,且不删当前版本
for f in $(ls -1t ${BIN_DIR}/${APP_NAME}-v* 2>/dev/null); do
local name
name=$(basename "$f")
count=$((count+1))
if [ $count -gt ${KEEP_VERSIONS} ] && [ "$name" != "$current" ]; then
$SUDO rm -f "$f"
log " 已删除: $name"
fi
done
}
# 重启服务 + 验证
restart_and_verify() {
log "重启 ${APP_NAME} 服务..."
# 注意:不要用 "list-unit-files | grep -q"——grep -q 提前退出会让 systemctl 收到
# SIGPIPE,在 pipefail 下整条管道判假(unit 多的机器必踩),误报"服务不存在"
if systemctl cat "${APP_NAME}" >/dev/null 2>&1; then
$SUDO systemctl restart "${APP_NAME}"
$SUDO systemctl enable "${APP_NAME}" >/dev/null 2>&1 || true
else
warn "服务 ${APP_NAME}.service 不存在,请先执行 $0 --init"
return 0
fi
if command -v nginx >/dev/null 2>&1; then
$SUDO systemctl reload nginx 2>/dev/null || $SUDO nginx -s reload 2>/dev/null || true
fi
sleep 2
# 健康自检:确认服务真正响应,而非仅进程存活
if command -v curl >/dev/null 2>&1; then
if curl -fsS --max-time 5 "http://127.0.0.1:${APP_PORT}/healthz" >/dev/null 2>&1; then
log "健康检查通过: /healthz"
else
err "健康检查失败: http://127.0.0.1:${APP_PORT}/healthz"
warn "服务可能启动异常,如需回滚执行: $0 --rollback"
$SUDO journalctl -u "${APP_NAME}" -n 30 --no-pager || true
exit 1
fi
fi
echo ""
log "服务状态:"
$SUDO systemctl status "${APP_NAME}" --no-pager -l | head -n 15 || true
echo ""
log "最近日志:"
$SUDO journalctl -u "${APP_NAME}" -n 15 --no-pager || true
}
# 写入 nginx 配置(--init 与 --sync-nginx 共用的唯一模板来源)
write_nginx_conf() {
$SUDO tee "${NGINX_CONF}" >/dev/null <<NGINX_EOF
server {
listen 80;
server_name ${DOMAIN};
access_log /var/log/nginx/${APP_NAME}.access.log;
error_log /var/log/nginx/${APP_NAME}.error.log;
# 视频解密代理:关闭缓冲,分片(Range/206)直通,避免 nginx 对大文件二次落盘
location /video/proxy/ {
proxy_pass http://127.0.0.1:${APP_PORT};
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_buffering off;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location / {
proxy_pass http://127.0.0.1:${APP_PORT};
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
NGINX_EOF
}
# 更新已部署服务器的 nginx 配置:备份 → 重写 → 校验 → 失败回滚
sync_nginx_conf() {
local backup=""
if [ -f "${NGINX_CONF}" ]; then
backup="${NGINX_CONF}.bak.$(date +%Y%m%d%H%M%S)"
$SUDO cp "${NGINX_CONF}" "${backup}"
log "已备份原配置: ${backup}"
else
warn "nginx 配置不存在,将写入全新配置: ${NGINX_CONF}"
fi
write_nginx_conf
log "已写入新配置: ${NGINX_CONF}"
if $SUDO nginx -t; then
$SUDO systemctl reload nginx 2>/dev/null || $SUDO nginx -s reload 2>/dev/null || true
log "nginx 配置已更新并重载"
else
err "nginx -t 校验失败"
if [ -n "${backup}" ] && [ -f "${backup}" ]; then
$SUDO cp "${backup}" "${NGINX_CONF}"
if $SUDO nginx -t >/dev/null 2>&1; then
$SUDO systemctl reload nginx 2>/dev/null || $SUDO nginx -s reload 2>/dev/null || true
warn "已回滚到备份配置: ${backup}"
fi
fi
exit 1
fi
}
# ==================== --list ====================
if [ "$LIST" = true ]; then
list_versions
exit 0
fi
# ==================== --sync-nginx ====================
# 只更新 nginx 配置(给已部署服务器同步模板变更用),不动二进制与服务
if [ "$SYNC_NGINX" = true ]; then
command -v nginx >/dev/null 2>&1 || { err "未找到 nginx"; exit 1; }
sync_nginx_conf
exit 0
fi
# ==================== --rollback ====================
if [ "$ROLLBACK" = true ]; then
if [ ! -d "${BIN_DIR}" ]; then
err "二进制目录不存在: ${BIN_DIR}"
exit 1
fi
# 没指定版本,交互式选择
if [ -z "$ROLLBACK_VERSION" ]; then
list_versions
echo ""
read -rp "输入要回滚到的版本号(如 ${APP_NAME}-v1.0.0,直接回车取消): " TARGET
[ -z "$TARGET" ] && { info "已取消"; exit 0; }
ROLLBACK_VERSION="$TARGET"
fi
# 补全前缀
if [[ "$ROLLBACK_VERSION" != ${APP_NAME}-* ]]; then
ROLLBACK_VERSION="${APP_NAME}-${ROLLBACK_VERSION}"
fi
TARGET_PATH="${BIN_DIR}/${ROLLBACK_VERSION}"
if [ ! -f "$TARGET_PATH" ]; then
err "版本不存在: $TARGET_PATH"
exit 1
fi
log "回滚到: ${ROLLBACK_VERSION}"
switch_symlink "${ROLLBACK_VERSION}"
restart_and_verify
echo ""
log "✅ 已回滚到 ${ROLLBACK_VERSION}"
exit 0
fi
# ==================== 1. 前置检查 ====================
log "检查环境..."
command -v go >/dev/null 2>&1 || { err "未找到 go,请先安装 Go"; exit 1; }
command -v nginx >/dev/null 2>&1 || warn "未检测到 nginx"
cd "$SRC_DIR" || { err "代码目录不存在: $SRC_DIR"; exit 1; }
# ==================== 2. 拉取代码 ====================
if [ "$NO_PULL" = false ] && [ -d .git ]; then
log "拉取最新代码..."
git pull --ff-only || warn "git pull 失败,继续使用当前代码"
else
log "跳过 git pull"
fi
# ==================== 3. 编译 ====================
log "编译 ${APP_NAME} (版本: ${VERSION})..."
CGO_ENABLED=0 go build \
-ldflags="-s -w -X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME}" \
-o "${APP_NAME}-${VERSION}" \
"${BUILD_TARGET}"
[ -f "${APP_NAME}-${VERSION}" ] || { err "编译产物不存在"; exit 1; }
log "编译完成: ${APP_NAME}-${VERSION}"
# ==================== --build-only ====================
if [ "$BUILD_ONLY" = true ]; then
echo ""
log "✅ 仅编译模式,产物:$(pwd)/${APP_NAME}-${VERSION}"
echo " 如需部署,请执行: $0 ${VERSION} --no-pull"
exit 0
fi
# ==================== 4. 部署二进制 ====================
log "部署到 ${BIN_DIR}..."
$SUDO mkdir -p "${BIN_DIR}"
# 同名版本默认拒绝覆盖,防止 --rollback 回滚到被覆盖的旧文件
if [ -e "${BIN_DIR}/${APP_NAME}-${VERSION}" ]; then
if [ "$FORCE" = true ]; then
warn "版本 ${VERSION} 已存在,--force 覆盖"
else
err "版本 ${VERSION} 已存在于 ${BIN_DIR},换版本号或加 --force 覆盖"; exit 1
fi
fi
$SUDO mv "${APP_NAME}-${VERSION}" "${BIN_DIR}/"
$SUDO chmod +x "${BIN_DIR}/${APP_NAME}-${VERSION}"
switch_symlink "${APP_NAME}-${VERSION}"
log "软链接已切换到: ${APP_NAME}-${VERSION}"
cleanup_old_versions
# ==================== 5. 首次初始化 ====================
if [ "$INIT" = true ]; then
log "初始化 systemd 服务: ${SERVICE_FILE}"
$SUDO tee "${SERVICE_FILE}" >/dev/null <<EOF
[Unit]
Description=${APP_DESC}
After=network.target
[Service]
Type=simple
User=${RUN_USER}
Group=${RUN_USER}
WorkingDirectory=${SRC_DIR}
ExecStart=${BIN_DIR}/${APP_NAME}-current
EnvironmentFile=-${ENV_FILE}
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
log "初始化 Nginx 配置: ${NGINX_CONF}"
write_nginx_conf
# ---- Certbot 续期后自动重载 Nginx(deploy-hook 只在续期成功时执行)----
HOOK_FILE="/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh"
if [ ! -f "${HOOK_FILE}" ]; then
log "创建 Certbot 续期重载 Hook: ${HOOK_FILE}"
$SUDO mkdir -p /etc/letsencrypt/renewal-hooks/deploy/
$SUDO tee "${HOOK_FILE}" >/dev/null <<'HOOK'
#!/bin/bash
if /usr/sbin/nginx -t 2>/dev/null; then
systemctl reload nginx
fi
HOOK
$SUDO chmod +x "${HOOK_FILE}"
fi
# ---- 首次初始化自动申请 HTTPS 证书(一次性;续期由 certbot.timer 自动处理)----
if [ ! -d "/etc/letsencrypt/live/${DOMAIN}" ]; then
if command -v certbot >/dev/null 2>&1; then
log "未检测到证书,尝试自动申请: ${DOMAIN}"
$SUDO certbot --nginx -d "${DOMAIN}" --non-interactive --agree-tos -m "${CERT_EMAIL}" --redirect \
|| warn "证书申请失败,请检查域名解析与 80 端口;可稍后手动执行 certbot --nginx -d ${DOMAIN}"
else
warn "未安装 certbot,跳过证书申请。可手动执行: dnf install certbot python3-certbot-nginx"
fi
else
log "证书已存在,跳过申请"
fi
if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" != "Disabled" ]; then
log "检测到 SELinux,放行 Nginx 网络连接..."
$SUDO setsebool -P httpd_can_network_connect 1 || warn "setsebool 失败"
fi
if command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active --quiet firewalld; then
log "放行防火墙 80/443..."
$SUDO firewall-cmd --permanent --add-service=http >/dev/null
$SUDO firewall-cmd --permanent --add-service=https >/dev/null
$SUDO firewall-cmd --reload >/dev/null
fi
log "重载 systemd..."
$SUDO systemctl daemon-reload
fi
# ==================== 6. 校验 Nginx ====================
if [ -f "${NGINX_CONF}" ]; then
log "校验 Nginx 配置..."
$SUDO nginx -t || { err "Nginx 配置有误"; exit 1; }
fi
# ==================== 7. 重启服务 + 验证 ====================
restart_and_verify
echo ""
log "✅ 部署完成!"
echo " 版本: ${VERSION}"
echo " 二进制: ${BIN_DIR}/${APP_NAME}-current"
echo " 端口: ${APP_PORT}"
echo " 域名: http://${DOMAIN}"
echo ""
echo "常用命令:"
echo " 查看状态: sudo systemctl status ${APP_NAME}"
echo " 查看日志: sudo journalctl -u ${APP_NAME} -f"
echo " 重启服务: sudo systemctl restart ${APP_NAME}"
echo " 列出版本: $0 --list"
echo " 回滚版本: $0 --rollback"