- 聚合书源取代早期书源版本:发现页筛选宫格/分类宫格、x:t:m:d: 前缀搜索、 fqtab 类型随书切换、段评气泡(内置六种样式可配);showCmt 能力检测,魔改版 原地弹窗(点遮罩关闭)、官方阅读降级 startBrowser;jsLib 不引用规则变量, searchUrl 修正编码与作用域 - 评论页:注入绝对 API_BASE,修复弹窗 WebView 内切换排序/懒加载失败; externalBaseURL 识别 X-Forwarded-Proto,气泡 ident 不再硬编码 http; 顶栏固定(拖拽把手)+评论区独立滚动 - 部署:证书方案从 certbot 迁移到 acme.sh(CERT_DIR 可配置);新增服务生命 周期参数 --stop/--start/--no-restart/--no-autostart;--clean 一键清理全部 部署内容(服务/nginx/二进制/证书,含 certbot 残留),acme.sh 定位抽为 find_acme_sh 供 init/clean 共用 - 忽略 .codegraph 本地索引目录
645 lines
22 KiB
Bash
Executable File
645 lines
22 KiB
Bash
Executable File
#!/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 --clean # 清除所有部署内容(服务/nginx/证书),重新 --init 前用
|
||
# ./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
|
||
CLEAN=false
|
||
|
||
print_help() {
|
||
cat <<EOF
|
||
用法: $0 [选项] [版本号]
|
||
|
||
选项:
|
||
--init 首次初始化,写入 systemd 和 nginx 配置
|
||
--no-pull 跳过 git pull
|
||
--no-autostart 服务不设置开机自启(默认自启)
|
||
--no-restart 只部署不重启服务(自启设置仍会应用)
|
||
--stop 停止服务后退出
|
||
--start 启动服务后退出
|
||
--clean 清除本脚本部署的全部内容(服务/nginx/二进制/证书,含 certbot 残留;
|
||
保留 SRC_DIR 下的 .env/配置/数据库),之后重新 --init 即可
|
||
--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 ;;
|
||
--clean) CLEAN=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
|
||
}
|
||
|
||
# 定位 acme.sh:PATH → 当前用户目录 → root 目录(sudo 后 HOME 可能被重置)
|
||
find_acme_sh() {
|
||
local acme
|
||
acme="$(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" ] && [ -x "$c" ]; then acme="$c"; fi
|
||
done
|
||
echo "$acme"
|
||
}
|
||
|
||
# 应用开机自启设置(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
|
||
|
||
# ==================== --clean ====================
|
||
# 清除本脚本部署的全部内容,便于重新 --init 初次部署
|
||
if [ "$CLEAN" = true ]; then
|
||
warn "将删除以下内容:"
|
||
echo " - systemd 服务: ${SERVICE_FILE}"
|
||
echo " - nginx 配置: ${NGINX_CONF}(含 --sync-nginx 的 .bak.* 备份)"
|
||
echo " - 二进制目录: ${BIN_DIR}"
|
||
echo " - 证书: ${CERT_DIR}/${DOMAIN}(并移除 acme.sh 对该域名的管理)"
|
||
echo " - certbot 残留: ${DOMAIN} 的证书、续期 timer 与 hook"
|
||
echo " (保留 ${SRC_DIR} 下的 .env / 配置 / 数据库;acme.sh 本体保留供重新部署使用)"
|
||
read -rp "确认清理?输入 yes 继续: " REPLY
|
||
[ "$REPLY" = "yes" ] || { info "已取消"; exit 0; }
|
||
|
||
# 服务
|
||
if systemctl cat "${APP_NAME}" >/dev/null 2>&1; then
|
||
$SUDO systemctl disable --now "${APP_NAME}" || true
|
||
$SUDO rm -f "${SERVICE_FILE}"
|
||
$SUDO systemctl daemon-reload
|
||
log "已删除服务 ${APP_NAME}.service"
|
||
else
|
||
info "服务不存在,跳过"
|
||
fi
|
||
|
||
# nginx 配置(含 --sync-nginx 产生的 .bak.* 备份)
|
||
if [ -f "${NGINX_CONF}" ] || ls "${NGINX_CONF}".bak.* >/dev/null 2>&1; then
|
||
$SUDO rm -f "${NGINX_CONF}" "${NGINX_CONF}".bak.*
|
||
if $SUDO nginx -t 2>/dev/null; then
|
||
$SUDO systemctl reload nginx 2>/dev/null || $SUDO nginx -s reload 2>/dev/null || true
|
||
fi
|
||
log "已删除 nginx 配置(含 .bak.* 备份)并重载"
|
||
fi
|
||
|
||
# 二进制与版本目录
|
||
$SUDO rm -rf "${BIN_DIR}"
|
||
log "已删除二进制目录: ${BIN_DIR}"
|
||
|
||
# 证书:移除 acme.sh 管理(--remove 不吊销,LE 证书自然过期)+ 删安装目录
|
||
ACME_SH="$(find_acme_sh)"
|
||
if [ -n "$ACME_SH" ]; then
|
||
$SUDO "$ACME_SH" --remove -d "${DOMAIN}" >/dev/null 2>&1 || true
|
||
fi
|
||
$SUDO rm -rf "${CERT_DIR}/${DOMAIN}"
|
||
log "已清除证书: ${CERT_DIR}/${DOMAIN}"
|
||
|
||
# certbot 残留(历史部署用 certbot 时遗留)
|
||
if command -v certbot &>/dev/null; then
|
||
$SUDO systemctl disable --now certbot.timer >/dev/null 2>&1 || true
|
||
$SUDO certbot delete --cert-name "${DOMAIN}" --non-interactive >/dev/null 2>&1 || true
|
||
fi
|
||
$SUDO rm -rf "/etc/letsencrypt/live/${DOMAIN}" "/etc/letsencrypt/archive/${DOMAIN}" \
|
||
"/etc/letsencrypt/renewal/${DOMAIN}.conf" \
|
||
"/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh"
|
||
log "已清理 certbot 残留"
|
||
|
||
echo ""
|
||
log "✅ 清理完成。重新部署: $0 --init"
|
||
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="$(find_acme_sh)"
|
||
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"
|