From ddb8746954549a468cf217d9abf6c4c60a3879b6 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 11 May 2026 10:41:36 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AE=89=E8=A3=85=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/composer.json | 1 + backend/config/autoload/cors.php | 38 +++++ backend/config/autoload/middlewares.php | 1 + deploy/podman/env/datahub-backend.env.example | 43 +++++ .../podman/env/datahub-frontend.env.example | 5 + deploy/podman/images/frontend/Dockerfile | 13 ++ .../images/frontend/docker-entrypoint.sh | 22 +++ deploy/podman/images/frontend/nginx.conf | 21 +++ deploy/podman/images/postgres/Dockerfile | 11 ++ .../postgres/initdb/01-create-extensions.sql | 11 ++ .../podman/quadlet/datahub-backend.container | 26 +++ .../podman/quadlet/datahub-frontend.container | 18 ++ .../podman/quadlet/datahub-postgres.container | 31 ++++ .../podman/quadlet/datahub-rabbitmq.container | 30 ++++ deploy/podman/quadlet/datahub.network | 9 + deploy/podman/scripts/check-prereqs.sh | 149 +++++++++++++++++ deploy/podman/scripts/configure-env.sh | 65 ++++++++ deploy/podman/scripts/create-secrets.sh | 57 +++++++ deploy/podman/scripts/install.sh | 144 ++++++++++++++++ deploy/podman/scripts/setup-data-dirs.sh | 38 +++++ deploy/podman/scripts/uninstall.sh | 130 +++++++++++++++ deploy/podman/scripts/update.sh | 156 ++++++++++++++++++ frontend/index.html | 1 + frontend/public/config.js | 8 + frontend/src/utils/request.ts | 16 +- 25 files changed, 1043 insertions(+), 1 deletion(-) create mode 100644 backend/config/autoload/cors.php create mode 100644 deploy/podman/env/datahub-backend.env.example create mode 100644 deploy/podman/env/datahub-frontend.env.example create mode 100644 deploy/podman/images/frontend/Dockerfile create mode 100644 deploy/podman/images/frontend/docker-entrypoint.sh create mode 100644 deploy/podman/images/frontend/nginx.conf create mode 100644 deploy/podman/images/postgres/Dockerfile create mode 100644 deploy/podman/images/postgres/initdb/01-create-extensions.sql create mode 100644 deploy/podman/quadlet/datahub-backend.container create mode 100644 deploy/podman/quadlet/datahub-frontend.container create mode 100644 deploy/podman/quadlet/datahub-postgres.container create mode 100644 deploy/podman/quadlet/datahub-rabbitmq.container create mode 100644 deploy/podman/quadlet/datahub.network create mode 100644 deploy/podman/scripts/check-prereqs.sh create mode 100644 deploy/podman/scripts/configure-env.sh create mode 100644 deploy/podman/scripts/create-secrets.sh create mode 100644 deploy/podman/scripts/install.sh create mode 100644 deploy/podman/scripts/setup-data-dirs.sh create mode 100644 deploy/podman/scripts/uninstall.sh create mode 100644 deploy/podman/scripts/update.sh create mode 100644 frontend/public/config.js diff --git a/backend/composer.json b/backend/composer.json index 493b2be..7c228e7 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -21,6 +21,7 @@ "hyperf/config": "~3.1.0", "hyperf/constants": "~3.1.0", "hyperf/crontab": "~3.1.0", + "gokure/hyperf-cors": "^2.1", "hyperf/database-pgsql": "^3.1", "hyperf/db-connection": "~3.1.0", "hyperf/engine": "^2.10", diff --git a/backend/config/autoload/cors.php b/backend/config/autoload/cors.php new file mode 100644 index 0000000..0cb4bd8 --- /dev/null +++ b/backend/config/autoload/cors.php @@ -0,0 +1,38 @@ + ['api/*'], + + // 允许的方法 + 'allowed_methods' => ['*'], + + // 允许的源(通配;若改成 supports_credentials=true,必须列具体域名) + 'allowed_origins' => ['*'], + + // 允许的源(正则匹配,按需启用) + 'allowed_origins_patterns' => [], + + // 允许的请求头 + 'allowed_headers' => ['*'], + + // 暴露给前端的响应头 + 'exposed_headers' => [], + + // 预检请求缓存时间(秒) + 'max_age' => 7200, + + // 是否允许携带凭证(cookie / 客户端证书);本项目用 Bearer token,保持 false + 'supports_credentials' => false, +]; diff --git a/backend/config/autoload/middlewares.php b/backend/config/autoload/middlewares.php index 9ae93d1..a51055c 100644 --- a/backend/config/autoload/middlewares.php +++ b/backend/config/autoload/middlewares.php @@ -11,6 +11,7 @@ declare(strict_types=1); */ return [ 'http' => [ + \Gokure\HyperfCors\CorsMiddleware::class, \App\Middleware\RequestLogMiddleware::class, ], ]; diff --git a/deploy/podman/env/datahub-backend.env.example b/deploy/podman/env/datahub-backend.env.example new file mode 100644 index 0000000..097db43 --- /dev/null +++ b/deploy/podman/env/datahub-backend.env.example @@ -0,0 +1,43 @@ +# 部署到 /var/container/data/datahub/env/datahub-backend.env +# 仅存放非敏感配置;密码/token 由 podman secret 注入(见 scripts/create-secrets.sh) + +APP_NAME=datahub +APP_ENV=prod +SCAN_CACHEABLE=true + +# --- PostgreSQL (TimescaleDB) --- +DB_DRIVER=pgsql +DB_HOST=datahub-postgres +DB_PORT=5432 +DB_DATABASE=datahub +DB_USERNAME=datahub +DB_CHARSET=utf8 +DB_COLLATION=utf8_unicode_ci +DB_PREFIX= +DB_SCHEMA=public +DB_TIMEZONE=Asia/Shanghai +DB_SSL_MODE=disable +DB_MAX_IDLE_TIME=60 +# DB_PASSWORD 由 podman secret 注入 + +# --- RabbitMQ --- +AMQP_HOST=datahub-rabbitmq +AMQP_PORT=5672 +AMQP_USER=user +AMQP_ADMIN_USER=user +AMQP_VHOST=dataflow +AMQP_MAX_RETRIES=3 +AMQP_CONSUMER_DEBUG_DELAY=0 +RABBITMQ_MANAGEMENT_PORT=15672 +# AMQP_PASSWORD / AMQP_ADMIN_PASSWORD 由 podman secret 注入 + +# --- JWT --- +JWT_HEADER_NAME=Authorization +SIMPLE_JWT_TTL=7200 +SIMPLE_JWT_REFRESH_TTL=2592000 +SIMPLE_JWT_PREFIX=dataflow +# SIMPLE_JWT_SECRET 由 podman secret 注入 + +# --- 外部依赖 --- +TOOLS_HOST=https://store-api-v2.wpic-tools.com/ +# TOOLS_TOKEN 由 podman secret 注入 diff --git a/deploy/podman/env/datahub-frontend.env.example b/deploy/podman/env/datahub-frontend.env.example new file mode 100644 index 0000000..913f637 --- /dev/null +++ b/deploy/podman/env/datahub-frontend.env.example @@ -0,0 +1,5 @@ +# 部署到 /var/container/data/datahub/env/datahub-frontend.env +# 浏览器访问后端 API 的完整地址(含协议、主机、端口) +# 由 scripts/configure-env.sh 交互式生成,也可手动维护 + +API_BASE_URL=http://__SERVER_IP_OR_DOMAIN__:9501 diff --git a/deploy/podman/images/frontend/Dockerfile b/deploy/podman/images/frontend/Dockerfile new file mode 100644 index 0000000..441746f --- /dev/null +++ b/deploy/podman/images/frontend/Dockerfile @@ -0,0 +1,13 @@ +FROM node:22-alpine AS builder +WORKDIR /app +COPY frontend/package*.json ./ +RUN npm ci +COPY frontend/ ./ +RUN npm run build + +FROM nginx:alpine +COPY --from=builder /app/dist /usr/share/nginx/html +COPY deploy/podman/images/frontend/nginx.conf /etc/nginx/conf.d/default.conf +COPY deploy/podman/images/frontend/docker-entrypoint.sh /docker-entrypoint.d/40-inject-config.sh +RUN chmod +x /docker-entrypoint.d/40-inject-config.sh +EXPOSE 80 diff --git a/deploy/podman/images/frontend/docker-entrypoint.sh b/deploy/podman/images/frontend/docker-entrypoint.sh new file mode 100644 index 0000000..365cb94 --- /dev/null +++ b/deploy/podman/images/frontend/docker-entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/sh +# 由 nginx 官方镜像的入口脚本自动执行(位于 /docker-entrypoint.d/) +# 将 config.js 中的占位符替换为运行期环境变量 + +set -e + +CONFIG_FILE=/usr/share/nginx/html/config.js + +if [ ! -f "$CONFIG_FILE" ]; then + echo "[entrypoint] $CONFIG_FILE 不存在,跳过注入" >&2 + exit 0 +fi + +if [ -z "${API_BASE_URL:-}" ]; then + echo "[entrypoint] 错误:必须设置 API_BASE_URL 环境变量" >&2 + exit 1 +fi + +# 用 | 作分隔符,避免 URL 里的 / 干扰 +sed -i "s|__API_BASE_URL__|${API_BASE_URL}|g" "$CONFIG_FILE" + +echo "[entrypoint] 已注入 API_BASE_URL=${API_BASE_URL}" diff --git a/deploy/podman/images/frontend/nginx.conf b/deploy/podman/images/frontend/nginx.conf new file mode 100644 index 0000000..81add8e --- /dev/null +++ b/deploy/podman/images/frontend/nginx.conf @@ -0,0 +1,21 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; + gzip_min_length 1024; + + location ~* \.(?:css|js|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|ico|webp)$ { + expires 7d; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/deploy/podman/images/postgres/Dockerfile b/deploy/podman/images/postgres/Dockerfile new file mode 100644 index 0000000..421f8f9 --- /dev/null +++ b/deploy/podman/images/postgres/Dockerfile @@ -0,0 +1,11 @@ +FROM docker.io/timescale/timescaledb:latest-pg16 + +LABEL maintainer="WPIC Datahub" \ + app.name="datahub-postgres" + +ENV TZ=Asia/Shanghai \ + LANG=en_US.utf8 + +COPY deploy/podman/images/postgres/initdb/ /docker-entrypoint-initdb.d/ + +EXPOSE 5432 diff --git a/deploy/podman/images/postgres/initdb/01-create-extensions.sql b/deploy/podman/images/postgres/initdb/01-create-extensions.sql new file mode 100644 index 0000000..26265bb --- /dev/null +++ b/deploy/podman/images/postgres/initdb/01-create-extensions.sql @@ -0,0 +1,11 @@ +-- 首次初始化时执行(仅在 $PGDATA 为空时触发) +-- 在 datahub 数据库内启用 TimescaleDB 扩展 +-- 迁移 2026_01_29_141058_convert_orders_to_hypertable.php 等依赖此扩展 + +\connect datahub + +CREATE EXTENSION IF NOT EXISTS timescaledb; + +-- 可按需追加其他扩展,例如: +-- CREATE EXTENSION IF NOT EXISTS pg_stat_statements; +-- CREATE EXTENSION IF NOT EXISTS pg_trgm; diff --git a/deploy/podman/quadlet/datahub-backend.container b/deploy/podman/quadlet/datahub-backend.container new file mode 100644 index 0000000..0a7ffcf --- /dev/null +++ b/deploy/podman/quadlet/datahub-backend.container @@ -0,0 +1,26 @@ +[Unit] +Description=Datahub Backend (Hyperf) +After=network-online.target datahub-postgres.service datahub-rabbitmq.service +Requires=datahub-postgres.service datahub-rabbitmq.service + +[Container] +ContainerName=datahub-backend +Image=localhost/datahub-backend:latest +Network=datahub.network +Volume=/var/container/data/datahub/backend-runtime:/opt/www/runtime:Z +PublishPort=9501:9501 + +EnvironmentFile=/var/container/data/datahub/env/datahub-backend.env + +Secret=datahub-pg-password,type=env,target=DB_PASSWORD +Secret=datahub-rabbitmq-password,type=env,target=AMQP_PASSWORD +Secret=datahub-rabbitmq-password,type=env,target=AMQP_ADMIN_PASSWORD +Secret=datahub-jwt-secret,type=env,target=SIMPLE_JWT_SECRET +Secret=datahub-tools-token,type=env,target=TOOLS_TOKEN + +[Service] +Restart=always +TimeoutStartSec=180 + +[Install] +WantedBy=default.target diff --git a/deploy/podman/quadlet/datahub-frontend.container b/deploy/podman/quadlet/datahub-frontend.container new file mode 100644 index 0000000..81798bf --- /dev/null +++ b/deploy/podman/quadlet/datahub-frontend.container @@ -0,0 +1,18 @@ +[Unit] +Description=Datahub Frontend (Nginx) +After=network-online.target datahub-backend.service +Requires=datahub-backend.service + +[Container] +ContainerName=datahub-frontend +Image=localhost/datahub-frontend:latest +Network=datahub.network +PublishPort=8080:80 + +EnvironmentFile=/var/container/data/datahub/env/datahub-frontend.env + +[Service] +Restart=always + +[Install] +WantedBy=default.target diff --git a/deploy/podman/quadlet/datahub-postgres.container b/deploy/podman/quadlet/datahub-postgres.container new file mode 100644 index 0000000..8d0b92e --- /dev/null +++ b/deploy/podman/quadlet/datahub-postgres.container @@ -0,0 +1,31 @@ +[Unit] +Description=Datahub PostgreSQL (TimescaleDB) +After=network-online.target +Requires=datahub-network.service + +[Container] +ContainerName=datahub-postgres +Image=localhost/datahub-postgres:latest +Network=datahub.network +Volume=/var/container/data/datahub/postgres:/var/lib/postgresql/data:Z,U +PublishPort=127.0.0.1:5416:5432 + +Environment=POSTGRES_DB=datahub +Environment=POSTGRES_USER=datahub +Environment=POSTGRES_PASSWORD_FILE=/run/secrets/datahub-pg-password +Environment=TZ=Asia/Shanghai +Environment=PGDATA=/var/lib/postgresql/data/pgdata + +Secret=datahub-pg-password,type=mount,target=datahub-pg-password,mode=0444 + +HealthCmd=pg_isready -U datahub -d datahub +HealthInterval=10s +HealthTimeout=5s +HealthRetries=10 + +[Service] +Restart=always +TimeoutStartSec=180 + +[Install] +WantedBy=default.target diff --git a/deploy/podman/quadlet/datahub-rabbitmq.container b/deploy/podman/quadlet/datahub-rabbitmq.container new file mode 100644 index 0000000..2b344f4 --- /dev/null +++ b/deploy/podman/quadlet/datahub-rabbitmq.container @@ -0,0 +1,30 @@ +[Unit] +Description=Datahub RabbitMQ +After=network-online.target +Requires=datahub-network.service + +[Container] +ContainerName=datahub-rabbitmq +Image=docker.io/library/rabbitmq:3-management-alpine +Network=datahub.network +Volume=/var/container/data/datahub/rabbitmq:/var/lib/rabbitmq:Z,U +PublishPort=127.0.0.1:5672:5672 +PublishPort=127.0.0.1:15672:15672 + +Environment=RABBITMQ_DEFAULT_USER=user +Environment=RABBITMQ_DEFAULT_PASS_FILE=/run/secrets/datahub-rabbitmq-password +Environment=RABBITMQ_DEFAULT_VHOST=dataflow + +Secret=datahub-rabbitmq-password,type=mount,target=datahub-rabbitmq-password,mode=0444 + +HealthCmd=rabbitmq-diagnostics -q ping +HealthInterval=15s +HealthTimeout=10s +HealthRetries=10 + +[Service] +Restart=always +TimeoutStartSec=180 + +[Install] +WantedBy=default.target diff --git a/deploy/podman/quadlet/datahub.network b/deploy/podman/quadlet/datahub.network new file mode 100644 index 0000000..dcfa900 --- /dev/null +++ b/deploy/podman/quadlet/datahub.network @@ -0,0 +1,9 @@ +[Unit] +Description=Datahub internal network + +[Network] +NetworkName=datahub +Driver=bridge + +[Install] +WantedBy=default.target diff --git a/deploy/podman/scripts/check-prereqs.sh b/deploy/podman/scripts/check-prereqs.sh new file mode 100644 index 0000000..289bfba --- /dev/null +++ b/deploy/podman/scripts/check-prereqs.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +# 部署环境体检脚本 - 判断阶段 0-1 是否已完成 +# +# 用法:bash check-prereqs.sh +# +# 退出码:0 = 全部 OK;1 = 有必须修复的项目(MISS) + +# 不开启 set -e:需要继续检查所有项目 + +B='\033[34m'; G='\033[32m'; Y='\033[33m'; R='\033[31m'; N='\033[0m' + +OK_COUNT=0 +WARN_COUNT=0 +MISS_COUNT=0 +FIX_CMDS=() + +ok() { echo -e "${G}[OK]${N} $1"; OK_COUNT=$((OK_COUNT+1)); } +warn() { echo -e "${Y}[WARN]${N} $1"; WARN_COUNT=$((WARN_COUNT+1)); [ -n "${2:-}" ] && { echo " 修复: $2"; FIX_CMDS+=("$2"); }; } +miss() { echo -e "${R}[MISS]${N} $1"; MISS_COUNT=$((MISS_COUNT+1)); [ -n "${2:-}" ] && { echo " 修复: $2"; FIX_CMDS+=("$2"); }; } + +echo -e "${B}=== datahub 部署环境体检 ===${N}" +echo "用户: $USER ($(id -u))" +echo "主机: $(hostname)" +echo "OS: $(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME" || echo unknown)" +echo + +# ---------- 1. 必需的可执行程序 ---------- + +# podman + 版本 +if command -v podman >/dev/null 2>&1; then + VER=$(podman --version | awk '{print $3}') + MAJOR=$(echo "$VER" | cut -d. -f1) + MINOR=$(echo "$VER" | cut -d. -f2) + if [[ "$MAJOR" =~ ^[0-9]+$ ]] && { (( MAJOR > 4 )) || { (( MAJOR == 4 )) && (( MINOR >= 4 )); }; }; then + ok "podman v$VER (≥ 4.4 支持 Quadlet)" + else + miss "podman v$VER 版本过旧(需 ≥ 4.4)" "升级 podman 到 4.4+(Ubuntu 22.04 用 22.10 PPA,或装 podman-stable)" + fi +else + miss "podman 未安装" "sudo apt install -y podman" +fi + +# systemctl +command -v systemctl >/dev/null 2>&1 \ + && ok "systemctl 可用" \ + || miss "systemctl 不可用(非 systemd 系统?)" + +# systemctl --user 是否可用(rootless 关键) +if systemctl --user list-units >/dev/null 2>&1; then + ok "systemctl --user 可用(rootless 必需)" +else + miss "systemctl --user 不可用" "确保用 SSH 直连登录(非 sudo su 切来),并且 XDG_RUNTIME_DIR=/run/user/\$(id -u) 已设置" +fi + +# uidmap +command -v newuidmap >/dev/null 2>&1 \ + && ok "uidmap (newuidmap) 已安装" \ + || miss "uidmap 未安装(rootless 必需)" "sudo apt install -y uidmap" + +# slirp4netns +command -v slirp4netns >/dev/null 2>&1 \ + && ok "slirp4netns 已安装" \ + || miss "slirp4netns 未安装" "sudo apt install -y slirp4netns" + +# fuse-overlayfs(推荐) +command -v fuse-overlayfs >/dev/null 2>&1 \ + && ok "fuse-overlayfs 已安装(rootless 存储驱动推荐)" \ + || warn "fuse-overlayfs 未安装(不影响功能,但 rootless 性能更好)" "sudo apt install -y fuse-overlayfs" + +# git +command -v git >/dev/null 2>&1 \ + && ok "git $(git --version | awk '{print $3}')" \ + || miss "git 未安装" "sudo apt install -y git" + +# ---------- 2. rootless 用户配置 ---------- + +# subuid +if grep -q "^${USER}:" /etc/subuid 2>/dev/null; then + ok "/etc/subuid: $(grep "^${USER}:" /etc/subuid)" +else + miss "/etc/subuid 缺少 $USER 映射" "sudo usermod --add-subuids 100000-165535 $USER" +fi + +# subgid +if grep -q "^${USER}:" /etc/subgid 2>/dev/null; then + ok "/etc/subgid: $(grep "^${USER}:" /etc/subgid)" +else + miss "/etc/subgid 缺少 $USER 映射" "sudo usermod --add-subgids 100000-165535 $USER" +fi + +# lingering +if loginctl show-user "$USER" 2>/dev/null | grep -q "Linger=yes"; then + ok "lingering 已启用(登出 / 重启后服务依然运行)" +else + warn "lingering 未启用(用户登出后服务停 + 开机不自启)" "sudo loginctl enable-linger $USER" +fi + +# ---------- 3. 数据目录可达性 ---------- + +if [ -d /var/container/data/datahub ]; then + if [ -w /var/container/data/datahub ]; then + ok "/var/container/data/datahub 已存在且可写" + else + warn "/var/container/data/datahub 存在但当前用户不可写" "sudo chown $USER:$USER /var/container/data/datahub" + fi +else + if sudo -n true 2>/dev/null; then + ok "/var/container/data/datahub 不存在(setup-data-dirs.sh 会创建,sudo 免密可用)" + else + warn "/var/container/data/datahub 不存在(setup-data-dirs.sh 将提示输入 sudo 密码)" + fi +fi + +# ---------- 4. 网络端口(仅提示) ---------- + +for port in 8080 9501; do + if ss -ltn 2>/dev/null | awk '{print $4}' | grep -qE ":${port}\$"; then + warn "端口 $port 已被占用" "lsof -i :$port 查看占用进程" + fi +done + +# ---------- 汇总 ---------- + +echo +echo "────────────────────────────────────" +echo -e "结果: ${G}${OK_COUNT} OK${N} / ${Y}${WARN_COUNT} WARN${N} / ${R}${MISS_COUNT} MISS${N}" +echo + +if (( MISS_COUNT > 0 )); then + echo -e "${R}存在必须修复的项目${N},请按上面的'修复:'命令处理后重新运行本脚本。" + if (( ${#FIX_CMDS[@]} > 0 )); then + echo + echo -e "${B}--- 一键复制修复命令 ---${N}" + # 去重 + printf '%s\n' "${FIX_CMDS[@]}" | awk '!seen[$0]++' + echo -e "${B}--- end ---${N}" + fi + exit 1 +fi + +if (( WARN_COUNT > 0 )); then + echo -e "${Y}有警告项${N}(不阻止部署,建议处理)" + echo "可执行 install.sh,但部分功能可能受限(例如 lingering 未启用 → 不自启)" + exit 0 +fi + +echo -e "${G}环境已就绪,可以执行:${N}" +echo -e " ${B}bash deploy/podman/scripts/install.sh${N}" +exit 0 diff --git a/deploy/podman/scripts/configure-env.sh b/deploy/podman/scripts/configure-env.sh new file mode 100644 index 0000000..80d2920 --- /dev/null +++ b/deploy/podman/scripts/configure-env.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# 交互式生成 env 文件 +# 用法:bash configure-env.sh + +set -euo pipefail + +CONF_DIR=/var/container/data/datahub/env +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(cd "$SCRIPT_DIR/../../.." && pwd) + +if [ ! -d "$CONF_DIR" ]; then + echo "[!] $CONF_DIR 不存在,请先执行: bash $SCRIPT_DIR/setup-data-dirs.sh" >&2 + exit 1 +fi + +# ---------- 1. backend env ---------- +BACKEND_ENV="$CONF_DIR/datahub-backend.env" +BACKEND_TPL="$REPO_ROOT/deploy/podman/env/datahub-backend.env.example" + +if [ -f "$BACKEND_ENV" ]; then + echo "[=] $BACKEND_ENV 已存在,跳过" +else + cp "$BACKEND_TPL" "$BACKEND_ENV" + chmod 600 "$BACKEND_ENV" + echo "[+] 已生成 $BACKEND_ENV(如需调整 TOOLS_HOST 等参数请手动编辑)" +fi + +# ---------- 2. frontend env:交互式询问 API_BASE_URL ---------- +FRONTEND_ENV="$CONF_DIR/datahub-frontend.env" + +CURRENT_VAL="" +if [ -f "$FRONTEND_ENV" ]; then + CURRENT_VAL=$(grep -E "^API_BASE_URL=" "$FRONTEND_ENV" | sed 's/^API_BASE_URL=//') + echo "[=] 当前 $FRONTEND_ENV 中 API_BASE_URL=$CURRENT_VAL" +fi + +DEFAULT_IP=$(hostname -I 2>/dev/null | awk '{print $1}') +DEFAULT_URL="http://${DEFAULT_IP:-127.0.0.1}:9501" +SUGGEST="${CURRENT_VAL:-$DEFAULT_URL}" + +echo +echo "请输入浏览器访问后端 API 的完整地址。" +echo "示例:http://192.168.1.100:9501 或 https://api.example.com" +read -rp "API_BASE_URL [$SUGGEST]: " INPUT +API_BASE_URL="${INPUT:-$SUGGEST}" + +if ! [[ "$API_BASE_URL" =~ ^https?:// ]]; then + echo "[!] 地址必须以 http:// 或 https:// 开头" >&2 + exit 1 +fi + +cat > "$FRONTEND_ENV" </dev/null 2>&1; then + echo "未检测到 podman,请先安装" >&2 + exit 1 +fi + +create_secret() { + local name=$1 + local prompt=$2 + local default_cmd=${3:-} + + if podman secret exists "$name" 2>/dev/null; then + read -rp "secret [$name] 已存在,是否替换?(y/N): " ans + if [[ "${ans,,}" != "y" ]]; then + echo " 跳过 $name" + return + fi + podman secret rm "$name" >/dev/null + fi + + local value + if [[ -n "$default_cmd" ]]; then + read -rp "$prompt(直接回车自动生成): " -s value + echo + if [[ -z "$value" ]]; then + value=$(eval "$default_cmd") + echo " 已自动生成" + fi + else + read -rp "$prompt: " -s value + echo + if [[ -z "$value" ]]; then + echo " 值不能为空" >&2 + exit 1 + fi + fi + + printf '%s' "$value" | podman secret create "$name" - + echo " ✓ 创建 $name" +} + +echo "=== 创建 datahub podman secrets ===" +echo + +create_secret datahub-pg-password "PostgreSQL datahub 用户密码" +create_secret datahub-rabbitmq-password "RabbitMQ user 用户密码" +create_secret datahub-jwt-secret "JWT 签名 secret" "openssl rand -hex 32" +create_secret datahub-tools-token "TOOLS_TOKEN(外部 store-api 鉴权 token)" + +echo +echo "完成。当前 secrets:" +podman secret ls diff --git a/deploy/podman/scripts/install.sh b/deploy/podman/scripts/install.sh new file mode 100644 index 0000000..3c0dc77 --- /dev/null +++ b/deploy/podman/scripts/install.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# datahub 一键部署脚本(阶段 3-7) +# +# 假设阶段 0-2 已完成:podman 已装 / subuid 已配 / lingering 已开 / 代码已 clone +# +# 用法:bash deploy/podman/scripts/install.sh +# +# 本脚本是幂等的:可重复运行;已存在的资源会跳过或询问。 + +set -euo pipefail + +# ---------- 输出辅助 ---------- +B='\033[34m'; G='\033[32m'; Y='\033[33m'; R='\033[31m'; N='\033[0m' +step() { echo -e "\n${B}==> [$1/5] $2${N}"; } +ok() { echo -e "${G}[OK]${N} $*"; } +warn() { echo -e "${Y}[!]${N} $*"; } +err() { echo -e "${R}[ERR]${N} $*" >&2; } + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(cd "$SCRIPT_DIR/../../.." && pwd) +QUADLET_DIR="$HOME/.config/containers/systemd" + +# ---------- 前置检查 ---------- +echo -e "${B}=== datahub 部署脚本 ===${N}" +echo "工作目录: $REPO_ROOT" +echo + +# 调用体检脚本;MISS 不为 0 时退出码 1 → 整个安装流程中止 +if ! bash "$SCRIPT_DIR/check-prereqs.sh"; then + err "环境体检未通过,请按上面提示处理后重试" + exit 1 +fi +echo + +# ---------- 1. 数据目录 ---------- +step 1 "创建数据目录" +bash "$SCRIPT_DIR/setup-data-dirs.sh" + +# ---------- 2. env 文件 ---------- +step 2 "生成 env 文件(交互)" +bash "$SCRIPT_DIR/configure-env.sh" + +# ---------- 3. secrets ---------- +step 3 "创建 podman secrets(交互)" +bash "$SCRIPT_DIR/create-secrets.sh" + +# ---------- 4. 构建镜像 ---------- +step 4 "构建 3 个镜像" +cd "$REPO_ROOT" + +echo -e "${B} -> datahub-postgres${N}" +podman build -t localhost/datahub-postgres:latest \ + -f deploy/podman/images/postgres/Dockerfile . + +echo -e "${B} -> datahub-backend${N}" +podman build -t localhost/datahub-backend:latest \ + -f backend/Dockerfile backend/ + +echo -e "${B} -> datahub-frontend${N}" +podman build -t localhost/datahub-frontend:latest \ + -f deploy/podman/images/frontend/Dockerfile . + +ok "镜像构建完成" +podman images --format " {{.Repository}}:{{.Tag}} ({{.Size}})" | grep "^ localhost/datahub-" || true + +# ---------- 5. Quadlet + 启动 ---------- +step 5 "部署 Quadlet 单元并启动服务" + +mkdir -p "$QUADLET_DIR" +cp "$REPO_ROOT"/deploy/podman/quadlet/* "$QUADLET_DIR/" +ok "Quadlet 单元已复制到 $QUADLET_DIR" + +systemctl --user daemon-reload + +# 等待容器达到 healthy(仅对配置了 healthcheck 的容器) +wait_healthy() { + local container=$1 + local timeout=${2:-90} + local elapsed=0 + while (( elapsed < timeout )); do + local status + status=$(podman inspect --format '{{.State.Health.Status}}' "$container" 2>/dev/null || echo "starting") + if [[ "$status" == "healthy" ]]; then + ok " $container healthy(${elapsed}s)" + return 0 + fi + sleep 3 + elapsed=$((elapsed + 3)) + printf "." + done + echo + err "$container 在 ${timeout}s 内未达到 healthy" + return 1 +} + +# 启动顺序:PG → MQ → Backend → Frontend +echo -e "${B} 启动 datahub-postgres...${N}" +systemctl --user start datahub-postgres.service +wait_healthy datahub-postgres 90 || { journalctl --user -u datahub-postgres.service -n 50 --no-pager; exit 1; } + +echo -e "${B} 启动 datahub-rabbitmq...${N}" +systemctl --user start datahub-rabbitmq.service +wait_healthy datahub-rabbitmq 90 || { journalctl --user -u datahub-rabbitmq.service -n 50 --no-pager; exit 1; } + +echo -e "${B} 启动 datahub-backend...${N}" +systemctl --user start datahub-backend.service +sleep 5 +if ! systemctl --user is-active --quiet datahub-backend.service; then + err "datahub-backend 未运行" + journalctl --user -u datahub-backend.service -n 50 --no-pager + exit 1 +fi +ok " datahub-backend 已运行" + +echo -e "${B} 启动 datahub-frontend...${N}" +systemctl --user start datahub-frontend.service +sleep 3 +if ! systemctl --user is-active --quiet datahub-frontend.service; then + err "datahub-frontend 未运行" + journalctl --user -u datahub-frontend.service -n 50 --no-pager + exit 1 +fi +ok " datahub-frontend 已运行" + +# ---------- 完成 ---------- +HOST_IP=$(hostname -I 2>/dev/null | awk '{print $1}') +echo +echo -e "${G}========================================${N}" +echo -e "${G} 部署完成!${N}" +echo -e "${G}========================================${N}" +echo +echo "服务访问:" +echo " 前端: http://${HOST_IP:-localhost}:8080" +echo " 后端: http://${HOST_IP:-localhost}:9501" +echo " RabbitMQ 控制台: http://${HOST_IP:-localhost}:15672 (账号 user)" +echo +echo "下一步(首次部署必做):" +echo -e " ${Y}podman exec -it datahub-backend php bin/hyperf.php migrate${N}" +echo +echo "常用命令:" +echo " podman ps # 看容器状态" +echo " systemctl --user status datahub-backend # 看服务状态" +echo " journalctl --user -u datahub-backend -f # 看实时日志" +echo " podman exec -it datahub-backend sh # 进容器调试" diff --git a/deploy/podman/scripts/setup-data-dirs.sh b/deploy/podman/scripts/setup-data-dirs.sh new file mode 100644 index 0000000..6544224 --- /dev/null +++ b/deploy/podman/scripts/setup-data-dirs.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# 创建宿主机数据目录 /var/container/data/datahub/ +# 所有运行期产物(数据卷 + env 配置)统一放在此根目录下 +# +# 用法:bash setup-data-dirs.sh + +set -euo pipefail + +DATA_BASE=/var/container/data/datahub +SUBDIRS=(postgres rabbitmq backend-runtime env) + +# 1. 父目录需要 root 创建并把所有权移交给当前用户 +if [ ! -d "$DATA_BASE" ]; then + echo "[*] $DATA_BASE 不存在,需要 sudo 创建" + sudo mkdir -p "$DATA_BASE" + sudo chown "$USER:$USER" "$DATA_BASE" +fi + +# 2. 创建子目录 +for d in "${SUBDIRS[@]}"; do + full="$DATA_BASE/$d" + if [ -d "$full" ]; then + echo "[=] $full 已存在,跳过" + else + mkdir -p "$full" + echo "[+] 创建 $full" + fi +done + +echo +echo "完成。当前结构:" +ls -la "$DATA_BASE" + +echo +echo "说明:" +echo " - postgres / rabbitmq 目录在容器首次启动时由 podman 自动 chown 给容器内用户(:U 标志)" +echo " - backend-runtime 容器以 root 运行,无需额外 chown" +echo " - env/ 存放 datahub-backend.env / datahub-frontend.env(由 configure-env.sh 生成)" diff --git a/deploy/podman/scripts/uninstall.sh b/deploy/podman/scripts/uninstall.sh new file mode 100644 index 0000000..0b3cf0c --- /dev/null +++ b/deploy/podman/scripts/uninstall.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# datahub 卸载脚本 +# +# 默认行为:停服务 + 移除 Quadlet + 删 secrets,保留数据目录和镜像 +# +# 用法:bash uninstall.sh [选项] +# --purge-images 同时删除 3 个本地镜像 +# --purge-data 同时删除 /var/container/data/datahub/ 数据目录(危险) +# -y, --yes 不询问确认(用于自动化) + +set -euo pipefail + +B='\033[34m'; G='\033[32m'; Y='\033[33m'; R='\033[31m'; N='\033[0m' +ok() { echo -e "${G}[OK]${N} $*"; } +warn() { echo -e "${Y}[!]${N} $*"; } + +PURGE_IMAGES=false +PURGE_DATA=false +ASSUME_YES=false + +for arg in "$@"; do + case "$arg" in + --purge-images) PURGE_IMAGES=true ;; + --purge-data) PURGE_DATA=true ;; + -y|--yes) ASSUME_YES=true ;; + -h|--help) + sed -n '2,11p' "$0" + exit 0 + ;; + *) echo "未知参数: $arg" >&2; exit 1 ;; + esac +done + +QUADLET_DIR="$HOME/.config/containers/systemd" +DATA_DIR=/var/container/data/datahub +SERVICES=(datahub-frontend datahub-backend datahub-rabbitmq datahub-postgres) +QUADLET_FILES=( + datahub-frontend.container + datahub-backend.container + datahub-rabbitmq.container + datahub-postgres.container + datahub.network +) +SECRETS=(datahub-pg-password datahub-rabbitmq-password datahub-jwt-secret datahub-tools-token) +IMAGES=(localhost/datahub-postgres:latest localhost/datahub-backend:latest localhost/datahub-frontend:latest) + +echo -e "${B}=== datahub 卸载脚本 ===${N}" +echo +echo "将执行以下操作:" +echo " - 停止 4 个 systemd 服务" +echo " - 移除 ${#QUADLET_FILES[@]} 个 Quadlet 单元" +echo " - 删除 ${#SECRETS[@]} 个 podman secrets" +echo -n " - 镜像: " +[ "$PURGE_IMAGES" = true ] && echo -e "${R}删除${N}" || echo "保留" +echo -n " - 数据目录 $DATA_DIR: " +[ "$PURGE_DATA" = true ] && echo -e "${R}删除(数据将丢失)${N}" || echo "保留" +echo + +if [ "$ASSUME_YES" = false ]; then + read -rp "确认继续?(y/N): " ans + [[ "${ans,,}" == "y" ]] || { echo "已取消"; exit 0; } +fi + +# 1. 停服务 +echo -e "\n${B}-> 停止服务${N}" +for svc in "${SERVICES[@]}"; do + if systemctl --user is-active --quiet "$svc.service" 2>/dev/null; then + systemctl --user stop "$svc.service" || true + echo " 停止 $svc" + fi +done + +# 2. 移除 Quadlet 文件 +echo -e "\n${B}-> 移除 Quadlet 单元${N}" +for f in "${QUADLET_FILES[@]}"; do + if [ -f "$QUADLET_DIR/$f" ]; then + rm -f "$QUADLET_DIR/$f" + echo " 删除 $f" + fi +done +systemctl --user daemon-reload + +# 3. 清理残留容器(保险起见) +for c in datahub-postgres datahub-rabbitmq datahub-backend datahub-frontend; do + if podman container exists "$c" 2>/dev/null; then + podman rm -f "$c" >/dev/null + echo " 删除残留容器 $c" + fi +done + +# 4. 删 secrets +echo -e "\n${B}-> 删除 secrets${N}" +for s in "${SECRETS[@]}"; do + if podman secret exists "$s" 2>/dev/null; then + podman secret rm "$s" >/dev/null + echo " 删除 $s" + fi +done + +# 5. 删镜像(可选) +if [ "$PURGE_IMAGES" = true ]; then + echo -e "\n${B}-> 删除镜像${N}" + for img in "${IMAGES[@]}"; do + if podman image exists "$img" 2>/dev/null; then + podman rmi "$img" >/dev/null && echo " 删除 $img" + fi + done +fi + +# 6. 删数据目录(危险,双重确认) +if [ "$PURGE_DATA" = true ]; then + echo -e "\n${R}-> 删除数据目录${N}" + if [ "$ASSUME_YES" = false ]; then + echo -e "${R}警告:将永久删除 $DATA_DIR 及全部数据${N}" + read -rp "请输入完整的 'yes' 以确认: " ans + if [ "$ans" != "yes" ]; then + echo "数据目录保留" + ASSUME_YES=skip_data + fi + fi + if [ "$ASSUME_YES" != "skip_data" ]; then + sudo rm -rf "$DATA_DIR" + echo " 数据目录已删除" + fi +fi + +echo +ok "卸载完成" +[ "$PURGE_DATA" = false ] && warn "数据保留在 $DATA_DIR(重新部署可直接恢复)" +[ "$PURGE_IMAGES" = false ] && warn "镜像保留(podman images | grep datahub 可见)" diff --git a/deploy/podman/scripts/update.sh b/deploy/podman/scripts/update.sh new file mode 100644 index 0000000..ab8accd --- /dev/null +++ b/deploy/podman/scripts/update.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# datahub 更新脚本:git pull + 智能重建 + 滚动重启 +# +# 用法:bash update.sh [选项] +# --force-rebuild 不看 git diff,强制重建所有镜像 +# --skip-migrate 检测到新迁移时不提示,直接跳过 + +set -euo pipefail + +B='\033[34m'; G='\033[32m'; Y='\033[33m'; R='\033[31m'; N='\033[0m' +step() { echo -e "\n${B}=> $*${N}"; } +ok() { echo -e "${G}[OK]${N} $*"; } +warn() { echo -e "${Y}[!]${N} $*"; } +err() { echo -e "${R}[ERR]${N} $*" >&2; } + +FORCE_REBUILD=false +SKIP_MIGRATE=false +for arg in "$@"; do + case "$arg" in + --force-rebuild) FORCE_REBUILD=true ;; + --skip-migrate) SKIP_MIGRATE=true ;; + -h|--help) + sed -n '2,7p' "$0" + exit 0 + ;; + *) echo "未知参数: $arg" >&2; exit 1 ;; + esac +done + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(cd "$SCRIPT_DIR/../../.." && pwd) +QUADLET_DIR="$HOME/.config/containers/systemd" + +cd "$REPO_ROOT" + +# 1. 必须是 git 仓库且工作区干净 +if [ ! -d .git ]; then + err "$REPO_ROOT 不是 git 仓库" + exit 1 +fi +if ! git diff --quiet || ! git diff --cached --quiet; then + err "工作区有未提交的变更,先 stash 或 commit" + git status --short + exit 1 +fi + +# 2. git pull +step "git pull" +OLD_HEAD=$(git rev-parse HEAD) +git pull --ff-only +NEW_HEAD=$(git rev-parse HEAD) + +if [ "$OLD_HEAD" = "$NEW_HEAD" ] && [ "$FORCE_REBUILD" = false ]; then + ok "代码已是最新,无需更新(如需强制重建请加 --force-rebuild)" + exit 0 +fi + +# 3. 检测变更范围 +if [ "$FORCE_REBUILD" = true ]; then + CH_BACKEND=true; CH_FRONTEND=true; CH_QUADLET=true + CH_PG_IMAGE=true; CH_MIGRATIONS=true + warn "强制重建:忽略 git diff" +else + DIFF=$(git diff --name-only "$OLD_HEAD" "$NEW_HEAD") + matches() { echo "$DIFF" | grep -qE "$1"; } + + CH_BACKEND=$(matches '^backend/' && echo true || echo false) + CH_FRONTEND=$(matches '^(frontend/|deploy/podman/images/frontend/)' && echo true || echo false) + CH_QUADLET=$(matches '^deploy/podman/quadlet/' && echo true || echo false) + CH_PG_IMAGE=$(matches '^deploy/podman/images/postgres/' && echo true || echo false) + CH_MIGRATIONS=$(matches '^backend/migrations/' && echo true || echo false) +fi + +echo +echo "变更检测:" +printf " %-20s %s\n" "backend image:" "$CH_BACKEND" +printf " %-20s %s\n" "frontend image:" "$CH_FRONTEND" +printf " %-20s %s\n" "postgres image:" "$CH_PG_IMAGE" +printf " %-20s %s\n" "quadlet files:" "$CH_QUADLET" +printf " %-20s %s\n" "migrations:" "$CH_MIGRATIONS" + +# 4. 重建镜像 +if [ "$CH_PG_IMAGE" = "true" ]; then + step "重建 postgres 镜像" + podman build -t localhost/datahub-postgres:latest \ + -f deploy/podman/images/postgres/Dockerfile . +fi + +if [ "$CH_BACKEND" = "true" ]; then + step "重建 backend 镜像" + podman build -t localhost/datahub-backend:latest \ + -f backend/Dockerfile backend/ +fi + +if [ "$CH_FRONTEND" = "true" ]; then + step "重建 frontend 镜像" + podman build -t localhost/datahub-frontend:latest \ + -f deploy/podman/images/frontend/Dockerfile . +fi + +# 5. 同步 Quadlet(任何 quadlet 改动或强制时) +if [ "$CH_QUADLET" = "true" ] || [ "$FORCE_REBUILD" = true ]; then + step "同步 Quadlet 单元" + cp deploy/podman/quadlet/* "$QUADLET_DIR/" + systemctl --user daemon-reload + ok "Quadlet 已更新" +fi + +# 6. 滚动重启受影响的服务 +step "重启受影响的服务" +restart_svc() { + local svc=$1 + echo " 重启 $svc.service" + systemctl --user restart "$svc.service" + sleep 2 + if ! systemctl --user is-active --quiet "$svc.service"; then + err "$svc 重启失败" + journalctl --user -u "$svc.service" -n 40 --no-pager + exit 1 + fi +} + +[ "$CH_PG_IMAGE" = "true" ] && restart_svc datahub-postgres +[ "$CH_BACKEND" = "true" ] && restart_svc datahub-backend +[ "$CH_FRONTEND" = "true" ] && restart_svc datahub-frontend + +# Quadlet 改了但镜像没改时,仍需重启对应服务 +if [ "$CH_QUADLET" = "true" ]; then + DIFF_Q=$(git diff --name-only "$OLD_HEAD" "$NEW_HEAD" -- deploy/podman/quadlet/ 2>/dev/null || true) + [ "$CH_PG_IMAGE" = "false" ] && echo "$DIFF_Q" | grep -q "datahub-postgres" && restart_svc datahub-postgres + [ "$CH_BACKEND" = "false" ] && echo "$DIFF_Q" | grep -q "datahub-backend" && restart_svc datahub-backend + [ "$CH_FRONTEND" = "false" ] && echo "$DIFF_Q" | grep -q "datahub-frontend" && restart_svc datahub-frontend + echo "$DIFF_Q" | grep -q "datahub-rabbitmq" && restart_svc datahub-rabbitmq +fi + +# 7. 迁移提示 +if [ "$CH_MIGRATIONS" = "true" ] && [ "$SKIP_MIGRATE" = false ]; then + echo + warn "检测到新迁移文件:" + git diff --name-only "$OLD_HEAD" "$NEW_HEAD" -- backend/migrations/ | sed 's/^/ /' + read -rp "立即执行迁移?(Y/n): " ans + ans="${ans,,}" + if [[ -z "$ans" || "$ans" == "y" ]]; then + podman exec -it datahub-backend php bin/hyperf.php migrate + ok "迁移完成" + else + warn "跳过。后续可手动执行:podman exec -it datahub-backend php bin/hyperf.php migrate" + fi +elif [ "$CH_MIGRATIONS" = "true" ]; then + warn "检测到新迁移但已 --skip-migrate,请手动执行" +fi + +echo +ok "更新完成" +echo " $OLD_HEAD" +echo " → $NEW_HEAD" diff --git a/frontend/index.html b/frontend/index.html index 9e5fc8f..019b425 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,6 +8,7 @@
+ diff --git a/frontend/public/config.js b/frontend/public/config.js new file mode 100644 index 0000000..b3c74bf --- /dev/null +++ b/frontend/public/config.js @@ -0,0 +1,8 @@ +// 运行期注入的配置文件 +// Vite 构建时此文件原样拷贝到 dist/;nginx 容器启动时由 docker-entrypoint.d/40-inject-config.sh +// 将 __API_BASE_URL__ 替换为真实地址(来自 API_BASE_URL 环境变量) +// +// 本地 dev 模式占位符不会被替换,request.ts 会自动 fallback 到 VITE_API_BASE_URL +window.__APP_CONFIG__ = { + apiBaseUrl: '__API_BASE_URL__', +}; diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index 026469f..5b32350 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -13,7 +13,21 @@ interface RequestOptions extends RequestInit { timeout?: number } -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '' +// 运行期配置优先(容器启动时由 nginx entrypoint 注入到 /config.js 写入 window.__APP_CONFIG__) +// 构建期 VITE_API_BASE_URL 作为本地 dev 的 fallback +function resolveApiBaseUrl(): string { + if (typeof window !== 'undefined') { + const runtime = (window as unknown as { __APP_CONFIG__?: { apiBaseUrl?: string } }).__APP_CONFIG__ + const url = runtime?.apiBaseUrl + // 占位符未替换时(如本地直接打开 public/config.js)忽略 + if (url && !(url.startsWith('__') && url.endsWith('__'))) { + return url + } + } + return import.meta.env.VITE_API_BASE_URL || '' +} + +const API_BASE_URL = resolveApiBaseUrl() async function request(url: string, options: RequestOptions = {}): Promise { url = `${API_BASE_URL}${url}`