fix(nginx): WebSocket trailing slash, 413 on file upload, domain+HTTPS support

- nginx: /ws/im/ → /ws/im (trailing slash broke ?token= WebSocket connections)
- nginx: add /api/file/ location before /api/ with 500m limit (fixes 413)
- deploy.sh: default DEPLOY_HOST to localhost instead of 127.0.0.1
- deploy.sh: add interactive domain/HTTPS configuration step (0c)
  - optional custom domain with validation
  - optional HTTPS via Let's Encrypt certbot (standalone, before nginx starts)
  - generates SSL nginx config (two-server-block) and docker-compose.override.yml
  - SDK_IM_WS_URL and imWsUrl use _WS_SCHEME (ws/wss) based on protocol
- deploy.sh: add info() helper function

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
徐勤民 2026-05-19 18:12:48 +08:00
父节点 be04c311b9
当前提交 d2599a0c1e
共有 2 个文件被更改,包括 243 次插入14 次删除

查看文件

@ -63,8 +63,9 @@ server {
} }
# IM WebSocket 长连接(客户端消息收发) # IM WebSocket 长连接(客户端消息收发)
location /ws/im/ { # 注意:不加尾部斜杠,否则 /ws/im?token=xxx 不匹配nginx prefix matching 不含 ?
proxy_pass http://im-service:8082/ws/im/; location /ws/im {
proxy_pass http://im-service:8082/ws/im;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
@ -83,6 +84,18 @@ server {
proxy_read_timeout 60s; proxy_read_timeout 60s;
} }
# ----------- 文件服务 API 路径file-service:8086-----------
# 注意:必须在通用 /api/ 之前声明,防止走 tenant-service 的 100m 限制
location /api/file/ {
proxy_pass http://file-service:8086/api/file/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 500m;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
# ----------- 核心 APItenant-service:9001----------- # ----------- 核心 APItenant-service:9001-----------
# 注意tenant-service 运行在 9001 端口(不是 8080 # 注意tenant-service 运行在 9001 端口(不是 8080
# 包含认证、租户管理、App 管理、SDK 配置、私有化部署状态 # 包含认证、租户管理、App 管理、SDK 配置、私有化部署状态

查看文件

@ -21,8 +21,8 @@
# - 在 XuqmGroup-PrivateDeploy 仓库根目录下执行本脚本 # - 在 XuqmGroup-PrivateDeploy 仓库根目录下执行本脚本
# #
# 覆盖默认值(可通过环境变量传入): # 覆盖默认值(可通过环境变量传入):
# DEPLOY_HOST 目标机器 IP / 主机名(默认 127.0.0.1 # DEPLOY_HOST 目标机器 IP / 主机名(默认 localhost
# 纯 IP 部署无需域名,局域网内可全功能使用 # 部署向导会提示配置域名与 HTTPS
# REGISTRY_PASSWORD ACR 密码(默认 xuqinmin1022 # REGISTRY_PASSWORD ACR 密码(默认 xuqinmin1022
# MYSQL_ROOT_PASSWORD、MYSQL_PASSWORD、REDIS_PASSWORD 同理 # MYSQL_ROOT_PASSWORD、MYSQL_PASSWORD、REDIS_PASSWORD 同理
@ -42,8 +42,12 @@ IMAGE_TAG="latest"
# 部署主机(用于健康检查 HTTP 请求) # 部署主机(用于健康检查 HTTP 请求)
# 可以是 IP 地址,无需域名,局域网内即可完整使用所有服务 # 可以是 IP 地址,无需域名,局域网内即可完整使用所有服务
DEPLOY_HOST="${DEPLOY_HOST:-127.0.0.1}" DEPLOY_HOST="${DEPLOY_HOST:-localhost}"
CONSOLE_BASE="http://${DEPLOY_HOST}" CONSOLE_BASE="http://${DEPLOY_HOST}"
_HTTP_SCHEME="http"
_WS_SCHEME="ws"
DEPLOY_DOMAIN=""
USE_HTTPS=false
# MySQLmanaged 模式,由 Docker 容器托管) # MySQLmanaged 模式,由 Docker 容器托管)
MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-XuqmRoot@2026}" MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-XuqmRoot@2026}"
@ -68,6 +72,7 @@ TOTAL_STEPS=8
# 工具函数 # 工具函数
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
log() { printf '\n\033[1;36m[%d/%d] %s\033[0m\n' "$STEP" "$TOTAL_STEPS" "$*"; } log() { printf '\n\033[1;36m[%d/%d] %s\033[0m\n' "$STEP" "$TOTAL_STEPS" "$*"; }
info() { printf ' \033[1;36m→\033[0m %s\n' "$*"; }
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; } ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
warn() { printf ' \033[33m⚠\033[0m %s\n' "$*"; } warn() { printf ' \033[33m⚠\033[0m %s\n' "$*"; }
fail() { printf '\n\033[1;31mERROR: %s\033[0m\n' "$*" >&2; exit 1; } fail() { printf '\n\033[1;31mERROR: %s\033[0m\n' "$*" >&2; exit 1; }
@ -180,6 +185,40 @@ else
_BOOTSTRAP_PASSWORD="already-migrated-do-not-use" _BOOTSTRAP_PASSWORD="already-migrated-do-not-use"
fi fi
# ---------------------------------------------------------------------------
# 0c. 域名 / HTTPS 配置(交互式)
# ---------------------------------------------------------------------------
printf '\n\033[1;33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m\n'
printf '\033[1;33m 访问域名配置(可选)\033[0m\n'
printf '\033[1;33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m\n'
printf ' 默认使用 localhost适合本机或局域网访问\n'
printf ' 如有公网域名,可在此配置并自动申请 HTTPS 证书\n\n'
read -rp " 是否使用自定义域名?[y/N]: " _domain_yn
if [[ "${_domain_yn:-N}" =~ ^[Yy]$ ]]; then
while [ -z "$DEPLOY_DOMAIN" ]; do
read -rp " 请输入域名(如 xuqm.example.com,不含 http://: " DEPLOY_DOMAIN
if ! printf '%s' "$DEPLOY_DOMAIN" | grep -qE '^[a-zA-Z0-9][a-zA-Z0-9._-]+\.[a-zA-Z]{2,}$'; then
warn "域名格式不正确,请重新输入(例: xuqm.example.com"
DEPLOY_DOMAIN=""
fi
done
DEPLOY_HOST="$DEPLOY_DOMAIN"
read -rp " 是否配置 HTTPS需域名已解析到本机,将自动申请 Let's Encrypt 证书)?[y/N]: " _https_yn
if [[ "${_https_yn:-N}" =~ ^[Yy]$ ]]; then
USE_HTTPS=true
_HTTP_SCHEME="https"
_WS_SCHEME="wss"
CONSOLE_BASE="https://${DEPLOY_DOMAIN}"
else
CONSOLE_BASE="http://${DEPLOY_DOMAIN}"
fi
ok "域名: ${DEPLOY_DOMAIN},协议: ${_HTTP_SCHEME}"
else
ok "使用默认地址: ${CONSOLE_BASE}"
fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Step 1 — 预检 # Step 1 — 预检
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -316,7 +355,7 @@ PUSH_DOMAIN=${CONSOLE_BASE}
# SDK 对外服务地址(客户端 SDK 使用) # SDK 对外服务地址(客户端 SDK 使用)
SDK_FILE_SERVICE_URL=${CONSOLE_BASE} SDK_FILE_SERVICE_URL=${CONSOLE_BASE}
SDK_IM_API_URL=${CONSOLE_BASE} SDK_IM_API_URL=${CONSOLE_BASE}
SDK_IM_WS_URL=ws://${DEPLOY_HOST}/ws/im SDK_IM_WS_URL=${_WS_SCHEME}://${DEPLOY_HOST}/ws/im
# 系统 IM 通信应用 key私有化服务间消息通知使用此 app_key 连接 IM 服务) # 系统 IM 通信应用 key私有化服务间消息通知使用此 app_key 连接 IM 服务)
# 与公有化平台 xuqinmin12 租户下的平台系统应用 key 保持一致 # 与公有化平台 xuqinmin12 租户下的平台系统应用 key 保持一致
@ -335,13 +374,192 @@ EOF
chmod 600 "$ROOT_DIR/config/tenant/bootstrap.env" chmod 600 "$ROOT_DIR/config/tenant/bootstrap.env"
ok "config/tenant/bootstrap.env 已写入 (chmod 600)" ok "config/tenant/bootstrap.env 已写入 (chmod 600)"
# config/nginx/conf.d/xuqm.conf — 从仓库中的文件复制(不再内嵌 heredoc,避免两处维护 # config/nginx/conf.d/xuqm.conf
mkdir -p "$ROOT_DIR/config/nginx/conf.d" mkdir -p "$ROOT_DIR/config/nginx/conf.d"
NGINX_SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/config/nginx/conf.d/xuqm.conf" NGINX_CONF_PATH="$ROOT_DIR/config/nginx/conf.d/xuqm.conf"
if [ ! -f "$NGINX_SRC" ]; then if [ ! -f "$NGINX_CONF_PATH" ]; then
fail "nginx 配置文件不存在: $NGINX_SRC(请确保在仓库根目录运行本脚本)" fail "nginx 配置文件不存在: $NGINX_CONF_PATH(请确保在仓库根目录运行本脚本)"
fi fi
# (以下 heredoc 仅作本地备份,deploy 时以上面复制为准,此段不执行)
if $USE_HTTPS; then
# ── HTTPS 模式:安装 certbot → 申请证书 → 生成 SSL nginx 配置 → compose override ──
# 安装 certbot若未安装
if ! command -v certbot >/dev/null 2>&1; then
info "安装 certbot ..."
if [ "$(command -v apt-get)" ]; then
apt-get install -y -qq certbot 2>/dev/null || fail "certbot 安装失败,请手动执行: apt install certbot"
elif [ "$(command -v yum)" ]; then
yum install -y -q certbot 2>/dev/null || fail "certbot 安装失败,请手动执行: yum install certbot"
else
fail "无法自动安装 certbot,请手动安装后重新运行"
fi
fi
ok "certbot $(certbot --version 2>&1 | head -1)"
# 申请 Let's Encrypt 证书standalone,此时 nginx 尚未启动)
CERT_DIR="/etc/letsencrypt/live/${DEPLOY_DOMAIN}"
if [ -f "${CERT_DIR}/fullchain.pem" ]; then
ok "证书已存在,跳过申请: ${CERT_DIR}"
else
info "申请 Let's Encrypt 证书(域名: ${DEPLOY_DOMAIN},需要端口 80 可达)..."
certbot certonly --standalone -d "$DEPLOY_DOMAIN" \
--agree-tos --register-unsafely-without-email --non-interactive \
|| fail "证书申请失败,请确认域名 ${DEPLOY_DOMAIN} 已解析到本机且端口 80 可访问"
ok "证书已申请: ${CERT_DIR}"
fi
# 生成包含 SSL 的 nginx 配置(覆盖仓库静态文件)
cat > "$NGINX_CONF_PATH" << NGINX_CONF
# =============================================================================
# XuqmGroup 私有化部署 — Nginx HTTPS 配置(由 deploy.sh 自动生成)
# 域名: ${DEPLOY_DOMAIN}
# =============================================================================
# HTTP → HTTPS 重定向
server {
listen 80;
server_name ${DEPLOY_DOMAIN};
return 301 https://\$host\$request_uri;
}
server {
listen 443 ssl;
server_name ${DEPLOY_DOMAIN};
ssl_certificate /etc/letsencrypt/live/${DEPLOY_DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${DEPLOY_DOMAIN}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
charset utf-8;
client_max_body_size 100m;
location /health {
return 200 "ok\n";
add_header Content-Type text/plain;
}
location /api/v1/updates/ {
proxy_pass http://update-service:8084/api/v1/updates/;
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_read_timeout 60s;
}
location /api/v1/rn/ {
proxy_pass http://update-service:8084/api/v1/rn/;
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_read_timeout 120s;
}
location /api/im/ {
proxy_pass http://im-service:8082/api/im/;
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_read_timeout 60s;
}
location /ws/im {
proxy_pass http://im-service:8082/ws/im;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_read_timeout 3600s;
}
location /api/license/ {
proxy_pass http://license-service:8085/api/license/;
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_read_timeout 60s;
}
location /api/file/ {
proxy_pass http://file-service:8086/api/file/;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
client_max_body_size 500m;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location /api/ {
proxy_pass http://tenant-service:9001/api/;
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_read_timeout 60s;
}
location /actuator/ {
proxy_pass http://tenant-service:9001/actuator/;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
}
location /file/ {
proxy_pass http://file-service:8086/file/;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
client_max_body_size 500m;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location /docs/ {
proxy_pass http://tenant-web:80/docs/;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
}
location /ops {
proxy_pass http://ops-web:80;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
}
location / {
proxy_pass http://tenant-web:80;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header Accept-Encoding "";
sub_filter 'wss://im.dev.xuqinmin.com/ws/im' 'wss://\$host/ws/im';
sub_filter 'https://dev.xuqinmin.com' 'https://\$host';
sub_filter_once off;
sub_filter_types text/javascript application/javascript;
}
}
NGINX_CONF
ok "nginx HTTPS 配置已生成(${DEPLOY_DOMAIN}"
# docker-compose override挂载 /etc/letsencrypt 到 nginx 容器
cat > "$ROOT_DIR/docker-compose.override.yml" << 'OVERRIDE_YAML'
# 由 deploy.sh 自动生成 — HTTPS 模式需要挂载 Let's Encrypt 证书
services:
nginx:
volumes:
- /etc/letsencrypt:/etc/letsencrypt:ro
OVERRIDE_YAML
ok "docker-compose.override.yml 已生成SSL 证书挂载)"
else
ok "config/nginx/conf.d/xuqm.conf 就绪HTTP 模式,使用仓库内文件)"
fi
# (以下 heredoc 为静态配置的备份注释,不执行)
: <<'NGINX_CONF' : <<'NGINX_CONF'
# ============================================================================= # =============================================================================
# XuqmGroup 私有化部署 — Nginx 路由配置 # XuqmGroup 私有化部署 — Nginx 路由配置
@ -474,8 +692,6 @@ server {
} }
} }
NGINX_CONF NGINX_CONF
# 实际使用仓库中的文件(已跳过上方 heredoc
ok "config/nginx/conf.d/xuqm.conf 就绪(使用仓库内文件)"
# config/vendors/push.env — 推送服务厂商凭据(初始为关闭状态,按需开启) # config/vendors/push.env — 推送服务厂商凭据(初始为关闭状态,按需开启)
mkdir -p "$ROOT_DIR/config/vendors" mkdir -p "$ROOT_DIR/config/vendors"
@ -528,7 +744,7 @@ cat > "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" <<EOF
"controlBaseUrl": "${CONSOLE_BASE}", "controlBaseUrl": "${CONSOLE_BASE}",
"fileBaseUrl": "${CONSOLE_BASE}", "fileBaseUrl": "${CONSOLE_BASE}",
"imApiBaseUrl": "${CONSOLE_BASE}", "imApiBaseUrl": "${CONSOLE_BASE}",
"imWsUrl": "ws://${DEPLOY_HOST}", "imWsUrl": "${_WS_SCHEME}://${DEPLOY_HOST}/ws/im",
"features": { "features": {
"file": true, "file": true,
"im": true, "im": true,