#!/usr/bin/env bash set -Eeuo pipefail umask 077 SERVICE_USER=kaidi SERVICE_GROUP=kaidi CONFIG_ROOT=/etc/kaidi STATE_ROOT=/var/lib/kaidi UPDATE_STATE_ROOT=/var/lib/kaidi-update LOG_ROOT=/var/log/kaidi PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 RELEASE_API_URL=https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest INIT_ARMED=false INIT_COMMITTED=false INIT_RELEASE_ROOT= INIT_APP_ROOT= LOG_LOCALE=${KAIDI_LOG_LOCALE:-zh-CN} SERVICE_USER_CREATED=false SERVICE_GROUP_CREATED=false CREATED_PATHS=() localize_message() { local message=$1 [ "$LOG_LOCALE" = en ] && { printf '%s' "$message"; return; } case "$message" in "Run with sudo or as root") printf '请使用 sudo 或 root 运行' ;; "Baota initialization only supports Linux") printf '宝塔初始化仅支持 Linux' ;; "Extract the release"*) printf '请先将发布包解压到 APP_ROOT/releases/VERSION' ;; "The supported Baota project root"*) printf '宝塔项目根目录必须是 /www/wwwroot/kaidi' ;; "The extracted release is incomplete") printf '解压后的发布包不完整' ;; "The extracted operations scripts are incomplete") printf '发布包中的运维脚本不完整' ;; "The release public key is missing") printf '缺少发布公钥' ;; "The release public-key fingerprint is invalid") printf '发布公钥指纹不匹配' ;; "Existing user "*" has unexpected home directory "*) printf '现有用户目录不符合 Kaidi 约定:%s' "${message#Existing user }" ;; "Existing user "*" is not a member"*) printf '现有 Kaidi 用户不属于服务用户组' ;; "A nologin shell is required") printf '服务用户必须使用 nologin shell' ;; *" contains a line break") printf '配置项包含换行符,已拒绝:%s' "${message%% contains a line break*}" ;; "find is required"|"openssl is required"|"sha256sum is required") printf '缺少初始化依赖命令:%s' "${message%% is required}" ;; "Kaidi is already initialized"*) printf 'Kaidi 已初始化,请先执行卸载流程再重新安装' ;; "The old kaidi-finance.service"*) printf '检测到旧 kaidi-finance.service,请先执行卸载流程' ;; "Old Kaidi update units"*) printf '检测到旧 Kaidi 更新单元,请先执行卸载流程' ;; "The Baota current release link"*) printf '宝塔 current 发布链接已存在,请先执行卸载流程' ;; "Port must be an integer"*) printf '端口必须是 1024 到 65535 的整数' ;; "Initialization failed"*) printf '初始化失败,本次创建的文件已回滚,可以修正原因后重试' ;; "Manual deployment is initialized"*) printf '宝塔手动部署初始化完成:%s' "${message#Manual deployment is initialized for Kaidi Finance }" ;; "Create the Baota Spring Boot project"*) printf '请在宝塔创建 Spring Boot 项目,项目路径:%s' "${message#Create the Baota Spring Boot project with }" ;; "Baota environment variables: leave empty") printf '宝塔环境变量请全部留空' ;; "Setup code: "*) printf '安装码位置:%s' "${message#Setup code: }" ;; *) printf '%s' "$message" ;; esac } log() { printf '[kaidi-baota-init] %s\n' "$(localize_message "$*")"; } die() { printf '[kaidi-baota-init] 错误:%s\n' "$(localize_message "$*")" >&2; exit 1; } rollback_initialization() { local rc=$? index path service_uid trap - EXIT HUP INT TERM if [ "$rc" -ne 0 ] && [ "$INIT_ARMED" = true ] && [ "$INIT_COMMITTED" != true ]; then systemctl disable --now kaidi-update.path >/dev/null 2>&1 || true systemctl stop kaidi-update.service >/dev/null 2>&1 || true rm -f /etc/systemd/system/kaidi-update.service /etc/systemd/system/kaidi-update.path systemctl daemon-reload >/dev/null 2>&1 || true rm -f "$CONFIG_ROOT/kaidi.env" "$CONFIG_ROOT/update.env" \ "$CONFIG_ROOT/release-public.pem" \ "$UPDATE_STATE_ROOT/status.json" /root/kaidi-first-login.txt rm -f "$INIT_APP_ROOT/current.next" if [ -n "$INIT_RELEASE_ROOT" ] \ && [ "$(readlink "$INIT_APP_ROOT/current" 2>/dev/null || true)" = "$INIT_RELEASE_ROOT" ]; then rm -f "$INIT_APP_ROOT/current" fi rm -f "$CONFIG_ROOT"/*.next.* "$UPDATE_STATE_ROOT"/*.next.* "$STATE_ROOT/setup"/*.next.* if [ "$SERVICE_USER_CREATED" = true ]; then service_uid=$(id -u "$SERVICE_USER" 2>/dev/null || true) [ -z "$service_uid" ] || userdel --force "$SERVICE_USER" >/dev/null 2>&1 || true fi if [ "$SERVICE_GROUP_CREATED" = true ]; then groupdel "$SERVICE_GROUP" >/dev/null 2>&1 || true fi for ((index=${#CREATED_PATHS[@]} - 1; index >= 0; index--)); do path=${CREATED_PATHS[$index]} rmdir -- "$path" >/dev/null 2>&1 || true done log "Initialization failed; files created by this attempt were rolled back and the command can be retried" fi exit "$rc" } trap rollback_initialization EXIT trap 'exit 129' HUP trap 'exit 130' INT trap 'exit 143' TERM sha256_file() { sha256sum "$1" | awk '{print $1}' } record_path() { [ -e "$1" ] || [ -L "$1" ] || CREATED_PATHS+=("$1") } random_secret() { openssl rand -base64 36 | tr -d '\n/+=' | cut -c1-36 } write_env_file() { local output=$1 temporary name value escaped shift temporary="${output}.next.$$" : > "$temporary" while [ "$#" -gt 0 ]; do name=$1 value=$2 shift 2 case "$value" in *$'\n'*|*$'\r'*) die "$name contains a line break" ;; esac escaped=${value//\\/\\\\} escaped=${escaped//\"/\\\"} printf '%s="%s"\n' "$name" "$escaped" >> "$temporary" done mv -f "$temporary" "$output" } ensure_service_identity() { local existing_home nologin_path if ! getent group "$SERVICE_GROUP" >/dev/null 2>&1; then groupadd --system "$SERVICE_GROUP" SERVICE_GROUP_CREATED=true fi if id "$SERVICE_USER" >/dev/null 2>&1; then existing_home=$(getent passwd "$SERVICE_USER" | awk -F: '{print $6}') [ "$existing_home" = "$STATE_ROOT" ] \ || die "Existing user $SERVICE_USER has unexpected home directory $existing_home" id -nG "$SERVICE_USER" | tr ' ' '\n' | grep -Fxq "$SERVICE_GROUP" \ || die "Existing user $SERVICE_USER is not a member of group $SERVICE_GROUP" return 0 fi nologin_path=$(command -v nologin 2>/dev/null || true) [ -n "$nologin_path" ] || nologin_path=/usr/sbin/nologin [ -x "$nologin_path" ] || die "A nologin shell is required" useradd --system --gid "$SERVICE_GROUP" --home-dir "$STATE_ROOT" --shell "$nologin_path" "$SERVICE_USER" SERVICE_USER_CREATED=true } main() { local script_dir release_root releases_root app_root version app_port field_key setup_code setup_hash [ "$(id -u)" -eq 0 ] || die "Run with sudo or as root" [ "$(uname -s)" = Linux ] || die "Baota initialization only supports Linux" for command in find openssl sha256sum; do command -v "$command" >/dev/null 2>&1 || die "$command is required" done script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P) release_root=$(cd "$script_dir/.." && pwd -P) releases_root=$(dirname "$release_root") app_root=$(dirname "$releases_root") [ "$(basename "$releases_root")" = releases ] \ || die "Extract the release into APP_ROOT/releases/VERSION before initialization" [ "$app_root" = /www/wwwroot/kaidi ] \ || die "The supported Baota project root is /www/wwwroot/kaidi" INIT_RELEASE_ROOT=$release_root INIT_APP_ROOT=$app_root version=$(tr -d '\r\n' < "$release_root/VERSION") [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.+-]+)?$ ]] \ && [ -s "$release_root/app.jar" ] && [ -s "$release_root/public/index.html" ] \ || die "The extracted release is incomplete" [ -x "$release_root/ops/update.sh" ] && [ -x "$release_root/ops/baota-start.sh" ] \ && [ -x "$release_root/ops/baota-init.sh" ] \ || die "The extracted operations scripts are incomplete" [ -s "$release_root/ops/release-public.pem" ] \ || die "The release public key is missing" [ "$(sha256_file "$release_root/ops/release-public.pem")" = "$PUBLIC_KEY_SHA256" ] \ || die "The release public-key fingerprint is invalid" [ ! -e "$CONFIG_ROOT/kaidi.env" ] && [ ! -e "$CONFIG_ROOT/update.env" ] \ && [ ! -e "$STATE_ROOT/setup/locked" ] \ || die "Kaidi is already initialized; run the published purge workflow before a fresh installation" [ ! -e /etc/systemd/system/kaidi-finance.service ] \ || die "The old kaidi-finance.service still exists; run the published purge workflow first" [ ! -e /etc/systemd/system/kaidi-update.service ] \ && [ ! -e /etc/systemd/system/kaidi-update.path ] \ || die "Old Kaidi update units still exist; run the published purge workflow first" [ ! -e "$app_root/current" ] && [ ! -L "$app_root/current" ] \ || die "The Baota current release link already exists; run the published purge workflow first" app_port=${1:-18080} [[ "$app_port" =~ ^[0-9]{1,5}$ ]] && [ "$app_port" -ge 1024 ] && [ "$app_port" -le 65535 ] \ || die "Port must be an integer between 1024 and 65535" INIT_ARMED=true ensure_service_identity for path in "$app_root" "$releases_root" "$STATE_ROOT" "$STATE_ROOT/files" "$STATE_ROOT/tmp" \ "$LOG_ROOT" "$STATE_ROOT/setup" "$UPDATE_STATE_ROOT" "$UPDATE_STATE_ROOT/inbox" "$CONFIG_ROOT"; do record_path "$path" done install -d -o root -g "$SERVICE_GROUP" -m 0750 "$app_root" "$releases_root" install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 \ "$STATE_ROOT" "$STATE_ROOT/files" "$STATE_ROOT/tmp" "$LOG_ROOT" install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0700 "$STATE_ROOT/setup" install -d -o root -g "$SERVICE_GROUP" -m 0750 "$UPDATE_STATE_ROOT" install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 "$UPDATE_STATE_ROOT/inbox" install -d -o root -g "$SERVICE_GROUP" -m 0750 "$CONFIG_ROOT" chown -R root:"$SERVICE_GROUP" "$release_root" find "$release_root" -type d -exec chmod 0750 {} + find "$release_root" -type f -exec chmod 0640 {} + chmod 0750 "$release_root/ops/update.sh" "$release_root/ops/baota-start.sh" "$release_root/ops/baota-init.sh" ln -sfn "$release_root" "$app_root/current.next" mv -Tf "$app_root/current.next" "$app_root/current" field_key=$(openssl rand -base64 32 | tr -d '\n') setup_code="KD-$(random_secret)" setup_hash=$(printf '%s' "$setup_code" | sha256sum | awk '{print $1}') # The Baota process manager does not own a restartable application unit. # Keep its updater files for diagnostics, but do not expose online update # actions until the panel lifecycle is integrated with the transaction state machine. # The setup context has no datasource. Empty values avoid Baota treating # a development placeholder as a real local MySQL dependency. write_env_file "$CONFIG_ROOT/kaidi.env" \ SPRING_PROFILES_ACTIVE production \ SERVER_ADDRESS 127.0.0.1 \ SERVER_PORT "$app_port" \ SESSION_COOKIE_SECURE false \ DB_URL '' \ DB_USERNAME '' \ DB_PASSWORD '' \ FIELD_ENCRYPTION_KEY "$field_key" \ FILE_STORAGE_ROOT "$STATE_ROOT/files" \ FILE_STORAGE_TEMP "$STATE_ROOT/tmp" \ FILE_SCANNER_ENABLED false \ FINANCE_BOOTSTRAP_ENABLED false \ FINANCE_BOOTSTRAP_PASSWORD '' \ FINANCE_SETUP_ENABLED true \ FINANCE_SETUP_TOKEN_SHA256 "$setup_hash" \ FINANCE_SETUP_ENV_FILE "$STATE_ROOT/setup/application.env" \ FINANCE_SETUP_MARKER_FILE "$STATE_ROOT/setup/locked" \ FINANCE_SETUP_RESTART_AFTER_COMPLETE true \ FINANCE_UPDATE_ENABLED false \ UPDATE_RELEASE_BASE_URL '' \ UPDATE_RELEASE_API_URL "$RELEASE_API_URL" \ UPDATE_RELEASE_TOKEN '' \ UPDATE_REQUEST_FILE "$UPDATE_STATE_ROOT/inbox/request.json" \ UPDATE_STATUS_FILE "$UPDATE_STATE_ROOT/status.json" \ KAIDI_PID_FILE "$STATE_ROOT/kaidi.pid" chown root:"$SERVICE_GROUP" "$CONFIG_ROOT/kaidi.env" chmod 0640 "$CONFIG_ROOT/kaidi.env" # Baota owns the Java process in this mode. Do not install a second # systemd updater; online update is intentionally systemd-only until the # panel lifecycle can participate in the transaction state machine. install -m 0644 "$release_root/ops/release-public.pem" "$CONFIG_ROOT/release-public.pem" printf '{"state":"CURRENT","message":"宝塔发布已准备","targetVersion":"%s","updatedAt":"%s"}\n' \ "$version" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$UPDATE_STATE_ROOT/status.json" chmod 0644 "$UPDATE_STATE_ROOT/status.json" cat > /root/kaidi-first-login.txt <