Files
fanqie-go/deploy.sh
T
chenxin 9eb2567247 ✨ feat(bookSource): 聚合书源适配阅读 app 并完善评论体验
- 聚合书源取代早期书源版本:发现页筛选宫格/分类宫格、x:t:m:d: 前缀搜索、
  fqtab 类型随书切换、段评气泡(内置六种样式可配);showCmt 能力检测,魔改版
  原地弹窗(点遮罩关闭)、官方阅读降级 startBrowser;jsLib 不引用规则变量,
  searchUrl 修正编码与作用域
- 评论页:注入绝对 API_BASE,修复弹窗 WebView 内切换排序/懒加载失败;
  externalBaseURL 识别 X-Forwarded-Proto,气泡 ident 不再硬编码 http;
  顶栏固定(拖拽把手)+评论区独立滚动
- 部署:证书方案从 certbot 迁移到 acme.sh(CERT_DIR 可配置)
- 忽略 .codegraph 本地索引目录
2026-10-10 12:57:00 +08:00

575 lines
19 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 v1.2.0 --no-autostart # 部署后不开机自启
# ./deploy.sh v1.2.0 --no-restart # 只部署,不重启服务
# ./deploy.sh --stop # 停止服务
# ./deploy.sh --start # 启动服务
# ./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}}" # 证书申请邮箱(过期提醒)
CERT_DIR="${CERT_DIR:-/etc/nginx/ssl}" # 证书目录(acme.sh --install-cert 安装位置)
BUILD_TARGET="${BUILD_TARGET:-./cmd/fanqie}" # go build 的包路径
ENV_FILE="${ENV_FILE:-${SRC_DIR}/.env}" # 应用环境变量(.env.example 为模板)
KEEP_VERSIONS="${KEEP_VERSIONS:-5}" # 保留最近几个版本
AUTO_START="${AUTO_START:-true}" # 服务是否开机自启(--no-autostart 覆盖)
# ==================== 颜色与日志 ====================
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
AUTO_START="${AUTO_START:-true}"
NO_RESTART=false
STOP=false
START=false
print_help() {
cat <<EOF
用法: $0 [选项] [版本号]
选项:
--init 首次初始化,写入 systemd 和 nginx 配置
--no-pull 跳过 git pull
--no-autostart 服务不设置开机自启(默认自启)
--no-restart 只部署不重启服务(自启设置仍会应用)
--stop 停止服务后退出
--start 启动服务后退出
--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 ;;
--no-autostart) AUTO_START=false ;;
--no-restart) NO_RESTART=true ;;
--stop) STOP=true ;;
--start) START=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
}
# 应用开机自启设置(AUTO_START 由 --no-autostart / .deploy.env 决定)
apply_autostart() {
if [ "$AUTO_START" = true ]; then
$SUDO systemctl enable "${APP_NAME}" >/dev/null 2>&1 || true
else
$SUDO systemctl disable "${APP_NAME}" >/dev/null 2>&1 || true
info "开机自启已禁用(--no-autostart)"
fi
}
# 重启服务 + 验证
restart_and_verify() {
# 注意:不要用 "list-unit-files | grep -q"——grep -q 提前退出会让 systemctl 收到
# SIGPIPE,在 pipefail 下整条管道判假(unit 多的机器必踩),误报"服务不存在"
if ! systemctl cat "${APP_NAME}" >/dev/null 2>&1; then
warn "服务 ${APP_NAME}.service 不存在,请先执行 $0 --init"
return 0
fi
apply_autostart
if [ "$NO_RESTART" = true ]; then
info "跳过服务重启(--no-restart),需要时执行: sudo systemctl restart ${APP_NAME}"
return 0
fi
log "重启 ${APP_NAME} 服务..."
$SUDO systemctl restart "${APP_NAME}"
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 共用的唯一模板来源)
# 模板自带完整 443/证书配置(证书存在时启用),不依赖签发工具注入——
# 这样 --sync-nginx 覆盖配置时不会丢失 HTTPS 段(注入式配置会被覆盖回 80-only)
write_nginx_conf() {
# 反代 location(80 无证书时与 443 共用)
local proxy_locations
proxy_locations=$(cat <<LOC_EOF
# 视频解密代理:关闭缓冲,分片(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;
}
LOC_EOF
)
local ssl_block=""
if [ -f "${CERT_DIR}/${DOMAIN}/fullchain.pem" ]; then
log "检测到 ${DOMAIN} 证书,配置启用 443"
ssl_block=$(cat <<SSL_EOF
server {
listen 443 ssl http2;
server_name ${DOMAIN};
ssl_certificate ${CERT_DIR}/${DOMAIN}/fullchain.pem;
ssl_certificate_key ${CERT_DIR}/${DOMAIN}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
access_log /var/log/nginx/${APP_NAME}.access.log;
error_log /var/log/nginx/${APP_NAME}.error.log;
${proxy_locations}
}
SSL_EOF
)
else
warn "未找到 ${CERT_DIR}/${DOMAIN}/ 证书,本次仅写 80 配置(不跳转 HTTPS);证书签发后重跑: $0 --sync-nginx"
fi
# 80 端口:有证书时仅 ACME 验证 + 301 跳转;无证书时直接反代
local root_location
if [ -n "${ssl_block}" ]; then
root_location=' location / {
return 301 https://$host$request_uri;
}'
else
root_location="${proxy_locations}"
fi
$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;
# ACME http-01 验证(acme.sh webroot 模式依赖)
location /.well-known/acme-challenge/ {
root /var/www/html;
}
${root_location}
}${ssl_block}
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
# ==================== --stop / --start ====================
if [ "$STOP" = true ] || [ "$START" = true ]; then
systemctl cat "${APP_NAME}" >/dev/null 2>&1 || { err "服务 ${APP_NAME}.service 不存在,请先执行 $0 --init"; exit 1; }
if [ "$STOP" = true ]; then
$SUDO systemctl stop "${APP_NAME}"
log "✅ 已停止 ${APP_NAME}(开机自启设置不变)"
else
$SUDO systemctl start "${APP_NAME}"
sleep 1
$SUDO systemctl status "${APP_NAME}" --no-pager -l | head -n 15 || true
fi
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
# ---- 首次初始化自动申请 HTTPS 证书(一次性;续期由 acme.sh cron 自动处理)----
if [ -f "${CERT_DIR}/${DOMAIN}/fullchain.pem" ]; then
log "证书已存在,跳过申请"
else
# 定位 acme.sh:PATH → 当前用户目录 → root 目录(sudo 后 HOME 可能被重置)
ACME_SH="$(command -v acme.sh 2>/dev/null || true)"
for c in "${HOME}/.acme.sh/acme.sh" "/root/.acme.sh/acme.sh"; do
if [ -z "$ACME_SH" ] && [ -x "$c" ]; then ACME_SH="$c"; fi
done
if [ -z "$ACME_SH" ]; then
err "未找到 acme.sh,请先安装: curl https://get.acme.sh | sh -s email=${CERT_EMAIL}"
exit 1
fi
log "未检测到证书,使用 acme.sh 申请: ${DOMAIN}"
$SUDO mkdir -p /var/www/html "${CERT_DIR}/${DOMAIN}"
if $SUDO "$ACME_SH" --issue -d "${DOMAIN}" -w /var/www/html --server letsencrypt --keylength ec-256 -m "${CERT_EMAIL}" \
&& $SUDO "$ACME_SH" --install-cert -d "${DOMAIN}" --ecc \
--fullchain-file "${CERT_DIR}/${DOMAIN}/fullchain.pem" \
--key-file "${CERT_DIR}/${DOMAIN}/privkey.pem" \
--reloadcmd "systemctl reload nginx"; then
log "证书申请成功,写入 443 配置..."
write_nginx_conf
else
warn "证书申请失败,请检查域名解析与 80 端口;成功后重跑: $0 --sync-nginx"
fi
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"