From 8598704a49f7a1b3642189689cf535de20fd3cd0 Mon Sep 17 00:00:00 2001 From: jacy-dukang Date: Tue, 7 Jul 2026 22:45:49 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=94=B9=E5=9B=9E=20user.runxian.top?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/h5-shop/src/pages/HomePage.tsx | 2 +- deploy/enable-runxian-dukang-ssl.sh | 16 +++--- deploy/nginx-runxian-dukang-ssl.conf | 63 +++++++++++++---------- deploy/nginx-runxian-dukang.conf | 32 ++++++------ deploy/remote-continue-m-domain.sh | 59 +++++---------------- scripts/configure-oss-cors.mjs | 2 +- server/dukang-api/.env.example | 6 +-- server/dukang-api/.env.production.example | 2 +- 8 files changed, 77 insertions(+), 105 deletions(-) diff --git a/apps/h5-shop/src/pages/HomePage.tsx b/apps/h5-shop/src/pages/HomePage.tsx index 96e432f..e78d00c 100644 --- a/apps/h5-shop/src/pages/HomePage.tsx +++ b/apps/h5-shop/src/pages/HomePage.tsx @@ -24,7 +24,7 @@ function formatMoney(n: number) { function formatScanError(e: unknown): string { const msg = e instanceof Error ? e.message : '扫码失败,请重试'; if (/invalid signature/i.test(msg)) { - return '微信 JSSDK 签名校验失败:请确认公众号已配置 JS 接口安全域名为 m.runxian.top,并刷新页面后重试'; + return '微信 JSSDK 签名校验失败:请确认公众号已配置 JS 接口安全域名为 user.runxian.top,并刷新页面后重试'; } return msg; } diff --git a/deploy/enable-runxian-dukang-ssl.sh b/deploy/enable-runxian-dukang-ssl.sh index 04650e4..046495e 100644 --- a/deploy/enable-runxian-dukang-ssl.sh +++ b/deploy/enable-runxian-dukang-ssl.sh @@ -1,11 +1,10 @@ #!/usr/bin/env bash # 为杜康 runxian.top 申请 Let's Encrypt 并切换 HTTPS 配置 -# 三端 H5 统一入口 m.runxian.top(微信网页授权单域名) +# 三端 H5 统一入口 user.runxian.top/{user,shop,partner}/ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" DOMAINS=( - m.runxian.top user.runxian.top shop.runxian.top partner.runxian.top @@ -27,8 +26,7 @@ mkdir -p /var/www/certbot /var/log/nginx/dukang echo "==> 申请证书..." certbot certonly --webroot -w /var/www/certbot \ - --cert-name m.runxian.top \ - -d m.runxian.top \ + --cert-name user.runxian.top \ -d user.runxian.top \ -d shop.runxian.top \ -d partner.runxian.top \ @@ -46,14 +44,14 @@ systemctl reload nginx echo "==> 验证..." for url in \ - https://m.runxian.top/user/ \ - https://m.runxian.top/shop/ \ - https://m.runxian.top/partner/ \ - https://user.runxian.top/ \ + https://user.runxian.top/user/ \ + https://user.runxian.top/shop/ \ + https://user.runxian.top/partner/ \ + https://shop.runxian.top/ \ https://webadmin.runxian.top/ \ https://dkapi.runxian.top/api/v1/health; do code="$(curl -sf -o /dev/null -w '%{http_code}' "$url" || echo fail)" echo " $url -> $code" done -echo "==> runxian.top 杜康域名 HTTPS 已启用(H5 统一入口 m.runxian.top)" +echo "==> runxian.top 杜康域名 HTTPS 已启用(H5 统一入口 user.runxian.top)" diff --git a/deploy/nginx-runxian-dukang-ssl.conf b/deploy/nginx-runxian-dukang-ssl.conf index d659637..bf131a2 100644 --- a/deploy/nginx-runxian-dukang-ssl.conf +++ b/deploy/nginx-runxian-dukang-ssl.conf @@ -1,10 +1,10 @@ # 杜康好客 — runxian.top HTTPS # 执行: deploy/enable-runxian-dukang-ssl.sh -# 三端 H5 统一入口 m.runxian.top/{user,shop,partner}/ +# 三端 H5 统一入口 user.runxian.top/{user,shop,partner}/ server { listen 80; - server_name m.runxian.top user.runxian.top shop.runxian.top partner.runxian.top; + server_name user.runxian.top shop.runxian.top partner.runxian.top m.runxian.top; location ^~ /.well-known/acme-challenge/ { root /var/www/certbot; @@ -12,7 +12,7 @@ server { } location / { - return 301 https://$host$request_uri; + return 301 https://user.runxian.top$request_uri; } } @@ -20,11 +20,22 @@ server { listen 443 ssl; server_name m.runxian.top; - access_log /var/log/nginx/dukang/runxian-m.access.log main; - error_log /var/log/nginx/dukang/runxian-m.error.log warn; + ssl_certificate /etc/letsencrypt/live/user.runxian.top/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/user.runxian.top/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; - ssl_certificate /etc/letsencrypt/live/m.runxian.top/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/m.runxian.top/privkey.pem; + return 301 https://user.runxian.top$request_uri; +} + +server { + listen 443 ssl; + server_name user.runxian.top; + + access_log /var/log/nginx/dukang/runxian-h5.access.log main; + error_log /var/log/nginx/dukang/runxian-h5.error.log warn; + + ssl_certificate /etc/letsencrypt/live/user.runxian.top/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/user.runxian.top/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; @@ -64,39 +75,33 @@ server { alias /opt/dukang-haoke/apps/h5-partner/dist/; try_files $uri $uri/ index.html; } -} -server { - listen 443 ssl; - server_name user.runxian.top; - - ssl_certificate /etc/letsencrypt/live/m.runxian.top/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/m.runxian.top/privkey.pem; - ssl_protocols TLSv1.2 TLSv1.3; - - return 301 https://m.runxian.top/user$request_uri; + # 旧 C 端根路径书签(无 /user 前缀)→ /user/... + location / { + return 302 /user$request_uri; + } } server { listen 443 ssl; server_name shop.runxian.top; - ssl_certificate /etc/letsencrypt/live/m.runxian.top/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/m.runxian.top/privkey.pem; + ssl_certificate /etc/letsencrypt/live/user.runxian.top/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/user.runxian.top/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; - return 301 https://m.runxian.top/shop$request_uri; + return 301 https://user.runxian.top/shop$request_uri; } server { listen 443 ssl; server_name partner.runxian.top; - ssl_certificate /etc/letsencrypt/live/m.runxian.top/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/m.runxian.top/privkey.pem; + ssl_certificate /etc/letsencrypt/live/user.runxian.top/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/user.runxian.top/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; - return 301 https://m.runxian.top/partner$request_uri; + return 301 https://user.runxian.top/partner$request_uri; } server { @@ -108,6 +113,8 @@ server { default_type "text/plain"; } + include /opt/dukang-haoke/deploy/nginx-deploy-webhook.conf; + location / { return 301 https://$host$request_uri; } @@ -120,13 +127,15 @@ server { access_log /var/log/nginx/dukang/runxian-api.access.log main; error_log /var/log/nginx/dukang/runxian-api.error.log warn; - ssl_certificate /etc/letsencrypt/live/m.runxian.top/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/m.runxian.top/privkey.pem; + ssl_certificate /etc/letsencrypt/live/user.runxian.top/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/user.runxian.top/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; client_max_body_size 20m; + include /opt/dukang-haoke/deploy/nginx-deploy-webhook.conf; + location / { proxy_pass http://127.0.0.1:8090; proxy_http_version 1.1; @@ -159,8 +168,8 @@ server { access_log /var/log/nginx/dukang/runxian-admin.access.log main; error_log /var/log/nginx/dukang/runxian-admin.error.log warn; - ssl_certificate /etc/letsencrypt/live/m.runxian.top/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/m.runxian.top/privkey.pem; + ssl_certificate /etc/letsencrypt/live/user.runxian.top/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/user.runxian.top/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; diff --git a/deploy/nginx-runxian-dukang.conf b/deploy/nginx-runxian-dukang.conf index 6487cf3..d260926 100644 --- a/deploy/nginx-runxian-dukang.conf +++ b/deploy/nginx-runxian-dukang.conf @@ -1,14 +1,13 @@ # 杜康好客 — runxian.top(HTTP) -# 三端 H5 统一入口 m.runxian.top/{user,shop,partner}/(微信网页授权单域名) -# 旧子域名 user/shop/partner 301 到统一入口;webadmin → 8094;dkapi → 8090 +# 三端 H5 统一入口 user.runxian.top/{user,shop,partner}/(微信网页授权单域名) +# shop/partner 子域名 301 到 user.runxian.top 路径;webadmin → 8094;dkapi → 8090 -# --- 统一 H5 入口(证书就绪前 HTTP;HTTPS 见 dukang-runxian-ssl.conf)--- server { listen 80; - server_name m.runxian.top; + server_name user.runxian.top; - access_log /var/log/nginx/dukang/runxian-m.access.log main; - error_log /var/log/nginx/dukang/runxian-m.error.log warn; + access_log /var/log/nginx/dukang/runxian-h5.access.log main; + error_log /var/log/nginx/dukang/runxian-h5.error.log warn; client_max_body_size 20m; @@ -51,28 +50,30 @@ server { alias /opt/dukang-haoke/apps/h5-partner/dist/; try_files $uri $uri/ index.html; } -} -# --- 旧 H5 子域名 → 统一入口(保留书签/推广链接)--- -server { - listen 80; - server_name user.runxian.top; - return 301 http://m.runxian.top/user$request_uri; + location / { + return 302 /user$request_uri; + } } server { listen 80; server_name shop.runxian.top; - return 301 http://m.runxian.top/shop$request_uri; + return 301 http://user.runxian.top/shop$request_uri; } server { listen 80; server_name partner.runxian.top; - return 301 http://m.runxian.top/partner$request_uri; + return 301 http://user.runxian.top/partner$request_uri; +} + +server { + listen 80; + server_name m.runxian.top; + return 301 http://user.runxian.top$request_uri; } -# --- HTTP: API --- server { listen 80; server_name dkapi.runxian.top; @@ -98,7 +99,6 @@ server { } } -# --- HTTP: admin-web --- server { listen 80; server_name webadmin.runxian.top; diff --git a/deploy/remote-continue-m-domain.sh b/deploy/remote-continue-m-domain.sh index 0d7ae1a..cf282de 100644 --- a/deploy/remote-continue-m-domain.sh +++ b/deploy/remote-continue-m-domain.sh @@ -1,10 +1,9 @@ #!/usr/bin/env bash -# 续部署:m.runxian.top 统一入口 + SSL 扩展 + nginx 切换 +# 续部署:user.runxian.top 统一路径入口 + nginx 切换 set -euo pipefail APP_ROOT="${APP_ROOT:-/opt/dukang-haoke}" -CERT_NAME="${CERT_CERT_NAME:-user.runxian.top}" -CERT_DIR="/etc/letsencrypt/live/${CERT_NAME}" +H5_BASE="https://user.runxian.top" cd "$APP_ROOT" @@ -12,63 +11,29 @@ echo "==> 1. PM2 重启(H5 dist 已含 /user|shop|partner/ 路径)" pm2 restart dukang-api dukang-h5-user dukang-h5-shop dukang-h5-partner dukang-admin-web pm2 save -echo "==> 2. 临时 ACME:m.runxian.top:80" -cat > /etc/nginx/conf.d/dukang-m-acme-temp.conf <<'EOF' -server { - listen 80; - server_name m.runxian.top; - location ^~ /.well-known/acme-challenge/ { - root /var/www/certbot; - default_type "text/plain"; - } - location / { return 404; } -} -EOF -nginx -t -systemctl reload nginx - -echo "==> 3. 扩展证书 SAN:加入 m.runxian.top" -certbot certonly --webroot -w /var/www/certbot \ - --expand --cert-name "$CERT_NAME" \ - -d user.runxian.top \ - -d shop.runxian.top \ - -d partner.runxian.top \ - -d webadmin.runxian.top \ - -d dkapi.runxian.top \ - -d m.runxian.top \ - --non-interactive --agree-tos -m admin@runxian.top - -echo "==> 4. 安装 nginx HTTPS 配置" +echo "==> 2. 安装 nginx HTTPS 配置" install -m 644 "$APP_ROOT/deploy/nginx-runxian-dukang-ssl.conf" /etc/nginx/conf.d/dukang-runxian.conf -sed -i "s|/etc/letsencrypt/live/m.runxian.top/|${CERT_DIR}/|g" /etc/nginx/conf.d/dukang-runxian.conf - -if ! grep -q nginx-deploy-webhook /etc/nginx/conf.d/dukang-runxian.conf; then - sed -i '/server_name dkapi.runxian.top;/a\ include /opt/dukang-haoke/deploy/nginx-deploy-webhook.conf;' \ - /etc/nginx/conf.d/dukang-runxian.conf -fi - -rm -f /etc/nginx/conf.d/dukang-m-acme-temp.conf nginx -t systemctl reload nginx -echo "==> 5. 更新 USER_H5_URL" +echo "==> 3. 更新 USER_H5_URL" ENV_FILE="$APP_ROOT/server/dukang-api/.env" if grep -q '^USER_H5_URL=' "$ENV_FILE"; then - sed -i 's|^USER_H5_URL=.*|USER_H5_URL=https://m.runxian.top/user|' "$ENV_FILE" + sed -i "s|^USER_H5_URL=.*|USER_H5_URL=${H5_BASE}/user|" "$ENV_FILE" else - echo 'USER_H5_URL=https://m.runxian.top/user' >> "$ENV_FILE" + echo "USER_H5_URL=${H5_BASE}/user" >> "$ENV_FILE" fi pm2 restart dukang-api -echo "==> 6. 验证" +echo "==> 4. 验证" for url in \ - "https://m.runxian.top/user/" \ - "https://m.runxian.top/shop/" \ - "https://m.runxian.top/partner/" \ - "https://user.runxian.top/" \ + "${H5_BASE}/user/" \ + "${H5_BASE}/shop/" \ + "${H5_BASE}/partner/" \ + "https://shop.runxian.top/" \ "https://dkapi.runxian.top/api/v1/health"; do code="$(curl -sk -o /dev/null -w '%{http_code}' "$url" || echo fail)" echo " $url -> $code" done -echo "==> m.runxian.top 部署完成" +echo "==> user.runxian.top 统一部署完成" diff --git a/scripts/configure-oss-cors.mjs b/scripts/configure-oss-cors.mjs index 3212980..28e19b8 100644 --- a/scripts/configure-oss-cors.mjs +++ b/scripts/configure-oss-cors.mjs @@ -57,7 +57,7 @@ const defaultOrigins = [ 'http://localhost:5173/user', 'http://localhost:5174/shop', 'http://localhost:5175/partner', - 'https://m.runxian.top', + 'https://user.runxian.top', ]; const allowedOrigin = (OSS_CORS_ORIGINS ?? defaultOrigins.join(',')) diff --git a/server/dukang-api/.env.example b/server/dukang-api/.env.example index b668735..26c15e3 100644 --- a/server/dukang-api/.env.example +++ b/server/dukang-api/.env.example @@ -26,15 +26,15 @@ MOCK_WECHAT=true WX_AUTHORIZE=false # C 端 H5 落地页(推广码二维码链接前缀) -# 本地开发:http://localhost:5173/user 生产统一入口:https://m.runxian.top/user +# 本地开发:http://localhost:5173/user 生产统一入口:https://user.runxian.top/user USER_H5_URL=http://localhost:5173/user # 反向代理后提取真实客户端 IP(下单 IP 定位) TRUST_PROXY=true # 微信 SDK(生产:WECHAT_AUTH_ENABLED=true,配置 WX_APP_ID / WX_APP_SECRET) -# OAuth 授权页由 /common/wechat/oauth-url 生成;三端统一入口 m.runxian.top/{user,shop,partner}。 -# 微信服务号「网页授权域名」「JS 接口安全域名」均配置 m.runxian.top(仅 1 个名额)。 +# OAuth 授权页由 /common/wechat/oauth-url 生成;三端统一入口 user.runxian.top/{user,shop,partner}。 +# 微信服务号「网页授权域名」「JS 接口安全域名」均配置 user.runxian.top(仅 1 个名额)。 WX_APP_ID= WX_APP_SECRET= WECHAT_AUTH_ENABLED=false diff --git a/server/dukang-api/.env.production.example b/server/dukang-api/.env.production.example index c211339..36f5a47 100644 --- a/server/dukang-api/.env.production.example +++ b/server/dukang-api/.env.production.example @@ -23,7 +23,7 @@ AUTO_APPROVE_STORE=false TRUST_PROXY=true # C 端 H5 落地页(推广码二维码;生产统一入口) -USER_H5_URL=https://m.runxian.top/user +USER_H5_URL=https://user.runxian.top/user WECHAT_AUTH_ENABLED=true WECHAT_PAY_ENABLED=true From c6af8480a43230845a9759c31ce72b6405f53893 Mon Sep 17 00:00:00 2001 From: jacy-dukang Date: Tue, 7 Jul 2026 22:53:18 +0800 Subject: [PATCH 2/3] =?UTF-8?q?runxian=20-dukang=20ssl=E8=AF=81=E4=B9=A6?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/nginx-runxian-dukang-ssl.conf | 27 +++++++++++++++++++++------ deploy/nginx-runxian-dukang.conf | 27 +++++++++++++++++++++------ 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/deploy/nginx-runxian-dukang-ssl.conf b/deploy/nginx-runxian-dukang-ssl.conf index bf131a2..3b854fa 100644 --- a/deploy/nginx-runxian-dukang-ssl.conf +++ b/deploy/nginx-runxian-dukang-ssl.conf @@ -62,18 +62,33 @@ server { } location /user/ { - alias /opt/dukang-haoke/apps/h5-user/dist/; - try_files $uri $uri/ index.html; + rewrite ^/user/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:8091; + 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; } location /shop/ { - alias /opt/dukang-haoke/apps/h5-shop/dist/; - try_files $uri $uri/ index.html; + rewrite ^/shop/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:8092; + 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; } location /partner/ { - alias /opt/dukang-haoke/apps/h5-partner/dist/; - try_files $uri $uri/ index.html; + rewrite ^/partner/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:8093; + 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; } # 旧 C 端根路径书签(无 /user 前缀)→ /user/... diff --git a/deploy/nginx-runxian-dukang.conf b/deploy/nginx-runxian-dukang.conf index d260926..a0e623c 100644 --- a/deploy/nginx-runxian-dukang.conf +++ b/deploy/nginx-runxian-dukang.conf @@ -37,18 +37,33 @@ server { } location /user/ { - alias /opt/dukang-haoke/apps/h5-user/dist/; - try_files $uri $uri/ index.html; + rewrite ^/user/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:8091; + 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; } location /shop/ { - alias /opt/dukang-haoke/apps/h5-shop/dist/; - try_files $uri $uri/ index.html; + rewrite ^/shop/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:8092; + 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; } location /partner/ { - alias /opt/dukang-haoke/apps/h5-partner/dist/; - try_files $uri $uri/ index.html; + rewrite ^/partner/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:8093; + 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; } location / { From 6dc051f26de84b71ccf4af776df198ff1d0201e4 Mon Sep 17 00:00:00 2001 From: jacy-dukang Date: Tue, 7 Jul 2026 23:33:34 +0800 Subject: [PATCH 3/3] chore(api): use .env.production on server, .env for local dev only Load production env without .env override; sync-api-env targets .env.production; PM2 NODE_ENV=production; Prisma uses with-api-env.cjs on deploy. Co-authored-by: Cursor --- .gitignore | 1 + deploy/auto-release.sh | 10 ++------ deploy/ecosystem.config.cjs | 3 +-- deploy/remote-continue-m-domain.sh | 2 +- deploy/remote-continue.sh | 2 +- deploy/remote-release.sh | 16 +++++++----- deploy/remote-setup.sh | 2 +- deploy/server.env.machine | 2 +- deploy/sync-api-env.sh | 19 ++++++++------ server/dukang-api/.env.example | 4 +-- server/dukang-api/.env.production.example | 4 +-- server/dukang-api/scripts/with-api-env.cjs | 30 ++++++++++++++++++++++ server/dukang-api/src/load-env.ts | 14 ++++------ 13 files changed, 68 insertions(+), 41 deletions(-) create mode 100644 server/dukang-api/scripts/with-api-env.cjs diff --git a/.gitignore b/.gitignore index 7bcbf3e..2d9b19d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ dist/ server/dukang-api/.env server/dukang-api/.env.development server/dukang-api/.env.production +server/dukang-api/.env.production.local deploy/deploy.env .DS_Store coverage/ diff --git a/deploy/auto-release.sh b/deploy/auto-release.sh index 2c3dede..3119f61 100644 --- a/deploy/auto-release.sh +++ b/deploy/auto-release.sh @@ -50,24 +50,18 @@ fi log "PULL $LOCAL_SHA -> $REMOTE_SHA" -# 保留生产密钥,避免 reset 覆盖 +# 保留服务器生产密钥,避免 reset 覆盖 ENV_BACKUP="$(mktemp)" -API_ENV_BACKUP="$(mktemp)" -API_ENV_DEV_BACKUP="$(mktemp)" API_ENV_PROD_BACKUP="$(mktemp)" cp "$ENV_FILE" "$ENV_BACKUP" 2>/dev/null || true -cp "$APP_ROOT/server/dukang-api/.env" "$API_ENV_BACKUP" 2>/dev/null || true -cp "$APP_ROOT/server/dukang-api/.env.development" "$API_ENV_DEV_BACKUP" 2>/dev/null || true cp "$APP_ROOT/server/dukang-api/.env.production" "$API_ENV_PROD_BACKUP" 2>/dev/null || true git checkout "$GIT_BRANCH" git reset --hard "$GIT_REMOTE/$GIT_BRANCH" cp "$ENV_BACKUP" "$ENV_FILE" 2>/dev/null || true -cp "$API_ENV_BACKUP" "$APP_ROOT/server/dukang-api/.env" 2>/dev/null || true -cp "$API_ENV_DEV_BACKUP" "$APP_ROOT/server/dukang-api/.env.development" 2>/dev/null || true cp "$API_ENV_PROD_BACKUP" "$APP_ROOT/server/dukang-api/.env.production" 2>/dev/null || true -rm -f "$ENV_BACKUP" "$API_ENV_BACKUP" "$API_ENV_DEV_BACKUP" "$API_ENV_PROD_BACKUP" +rm -f "$ENV_BACKUP" "$API_ENV_PROD_BACKUP" sed -i 's/\r$//' "$ENV_FILE" 2>/dev/null || true # 修复 Windows 换行 diff --git a/deploy/ecosystem.config.cjs b/deploy/ecosystem.config.cjs index d5f24fb..e9502cf 100644 --- a/deploy/ecosystem.config.cjs +++ b/deploy/ecosystem.config.cjs @@ -10,8 +10,7 @@ module.exports = { instances: 1, exec_mode: 'fork', env: { - // 预发/联调阶段用 development;正式上线后改回 production - NODE_ENV: 'development', + NODE_ENV: 'production', PORT: 8090, }, }, diff --git a/deploy/remote-continue-m-domain.sh b/deploy/remote-continue-m-domain.sh index cf282de..3ad2743 100644 --- a/deploy/remote-continue-m-domain.sh +++ b/deploy/remote-continue-m-domain.sh @@ -17,7 +17,7 @@ nginx -t systemctl reload nginx echo "==> 3. 更新 USER_H5_URL" -ENV_FILE="$APP_ROOT/server/dukang-api/.env" +ENV_FILE="$APP_ROOT/server/dukang-api/.env.production" if grep -q '^USER_H5_URL=' "$ENV_FILE"; then sed -i "s|^USER_H5_URL=.*|USER_H5_URL=${H5_BASE}/user|" "$ENV_FILE" else diff --git a/deploy/remote-continue.sh b/deploy/remote-continue.sh index d5d5609..638875a 100644 --- a/deploy/remote-continue.sh +++ b/deploy/remote-continue.sh @@ -10,7 +10,7 @@ export NODE_OPTIONS="--max-old-space-size=1024" pnpm install --filter @dukang/api... --filter @dukang/domain --filter @dukang/shared-types echo "==> env" -cat > "$APP_ROOT/server/dukang-api/.env" <<'ENV' +cat > "$APP_ROOT/server/dukang-api/.env.production" <<'ENV' DATABASE_URL="mysql://dukang:Dukang2026!@localhost:3306/dukang_haoke" REDIS_URL="redis://localhost:6379" JWT_SECRET="dukang-prod-jwt-secret-2026" diff --git a/deploy/remote-release.sh b/deploy/remote-release.sh index 6d6c2b0..90b6cdf 100644 --- a/deploy/remote-release.sh +++ b/deploy/remote-release.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# 在服务器上执行:拉取后的常规发版(不覆盖 .env、默认不 seed) +# 在服务器上执行:拉取后的常规发版(不覆盖 .env.production、默认不 seed) set -euo pipefail APP_ROOT="${APP_ROOT:-/opt/dukang-haoke}" @@ -46,17 +46,21 @@ pnpm approve-builds --all 2>/dev/null || true pnpm install --frozen-lockfile 2>/dev/null || pnpm install if [[ "$SKIP_DB" == false ]]; then - echo "==> 3. 数据库 schema 同步(构建前)" + echo "==> 3. 数据库 schema 同步(构建前,读取 .env.production)" cd "$APP_ROOT/server/dukang-api" - pnpm prisma:generate + if [[ ! -f .env.production ]]; then + echo "错误: 未找到 server/dukang-api/.env.production" >&2 + exit 1 + fi + node scripts/with-api-env.cjs pnpm prisma:generate if [[ "$DB_PUSH_ACCEPT_DATA_LOSS" == true ]]; then - npx prisma db push --accept-data-loss + node scripts/with-api-env.cjs npx prisma db push --accept-data-loss else - npx prisma db push + node scripts/with-api-env.cjs npx prisma db push fi if [[ "$RUN_SEED" == true ]]; then echo "==> 执行 seed" - pnpm prisma:seed + node scripts/with-api-env.cjs pnpm prisma:seed fi cd "$APP_ROOT" else diff --git a/deploy/remote-setup.sh b/deploy/remote-setup.sh index 3ee8669..a36d729 100644 --- a/deploy/remote-setup.sh +++ b/deploy/remote-setup.sh @@ -27,7 +27,7 @@ pnpm install --frozen-lockfile 2>/dev/null || pnpm install pnpm build echo "==> 5. 写入 API 生产环境变量" -cat > "$APP_ROOT/server/dukang-api/.env" <<'ENV' +cat > "$APP_ROOT/server/dukang-api/.env.production" <<'ENV' DATABASE_URL="mysql://dukang:Dukang2026!@localhost:3306/dukang_haoke" REDIS_URL="redis://localhost:6379" JWT_SECRET="dukang-prod-jwt-secret-2026" diff --git a/deploy/server.env.machine b/deploy/server.env.machine index ab57a20..c5acb1f 100644 --- a/deploy/server.env.machine +++ b/deploy/server.env.machine @@ -1,4 +1,4 @@ -# 服务器机器相关(覆盖 .env.development 中的同名项) +# 服务器机器相关(可合并到 .env.production 的参考项) PORT=8090 DATABASE_URL="mysql://dukang:Dukang2026!@localhost:3306/dukang_haoke" REDIS_URL="redis://localhost:6379" diff --git a/deploy/sync-api-env.sh b/deploy/sync-api-env.sh index 6f1ee62..3e656a7 100644 --- a/deploy/sync-api-env.sh +++ b/deploy/sync-api-env.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# 同步 API 环境变量到服务器 -# 用法: deploy/sync-api-env.sh [development|production] +# 同步 API 生产环境变量到服务器 .env.production +# 用法: deploy/sync-api-env.sh [production] set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -13,12 +13,15 @@ DEPLOY_PORT="22" DEPLOY_SSH_KEY="" APP_ROOT="/opt/dukang-haoke" -TARGET="${1:-development}" +TARGET="${1:-production}" case "$TARGET" in - development|dev) SRC_ENV="$REPO_ROOT/server/dukang-api/.env.development" ;; production|prod) SRC_ENV="$REPO_ROOT/server/dukang-api/.env.production" ;; + development|dev) + echo "错误: development 仅用于本地开发,请使用 .env / .env.development,不同步到服务器" >&2 + exit 1 + ;; *) - echo "用法: $0 [development|production]" >&2 + echo "用法: $0 [production]" >&2 exit 1 ;; esac @@ -29,7 +32,7 @@ if [[ -f "$ENV_FILE" ]]; then fi if [[ ! -f "$SRC_ENV" ]]; then - echo "错误: 未找到 $SRC_ENV" >&2 + echo "错误: 未找到 $SRC_ENV(可从 .env.production.example 复制)" >&2 exit 1 fi @@ -42,7 +45,7 @@ SSH_OPTS=(-o "StrictHostKeyChecking=accept-new" -p "$DEPLOY_PORT") [[ -n "$DEPLOY_SSH_KEY" ]] && SSH_OPTS+=(-i "$DEPLOY_SSH_KEY") REMOTE="${DEPLOY_USER}@${DEPLOY_HOST}" -echo "==> 同步 $TARGET 环境到 $REMOTE:$APP_ROOT/server/dukang-api/.env" -scp "${SSH_OPTS[@]}" "$SRC_ENV" "$REMOTE:$APP_ROOT/server/dukang-api/.env" +echo "==> 同步 production 环境到 $REMOTE:$APP_ROOT/server/dukang-api/.env.production" +scp "${SSH_OPTS[@]}" "$SRC_ENV" "$REMOTE:$APP_ROOT/server/dukang-api/.env.production" ssh "${SSH_OPTS[@]}" "$REMOTE" "pm2 restart dukang-api && sleep 2 && curl -sf -o /dev/null -w 'api-health:%{http_code}\n' http://127.0.0.1:8090/api/v1/health" echo "==> 完成" diff --git a/server/dukang-api/.env.example b/server/dukang-api/.env.example index 26c15e3..12592ac 100644 --- a/server/dukang-api/.env.example +++ b/server/dukang-api/.env.example @@ -1,8 +1,8 @@ # 杜康 API 环境变量模板 # 本地开发:复制为 .env 后按需修改(pnpm dev:api 仅读取 .env / .env.local) # 可选 .env.local — 本机覆盖(gitignore) -# 服务器部署:bash deploy/sync-api-env.sh development|production(同步到远端 .env) -# .env.development / .env.production — 部署源模板,本地 dev 不自动加载 +# 服务器:仅使用 .env.production(bash deploy/sync-api-env.sh production) +# .env.development — 本地参考模板,不部署到服务器 DATABASE_URL="mysql://root:root@localhost:3306/dukang_haoke" REDIS_URL="redis://localhost:6379" diff --git a/server/dukang-api/.env.production.example b/server/dukang-api/.env.production.example index 36f5a47..fa1f7f1 100644 --- a/server/dukang-api/.env.production.example +++ b/server/dukang-api/.env.production.example @@ -1,6 +1,6 @@ # 生产环境(真实第三方,关闭 Mock) -# 用途:deploy/sync-api-env.sh production 的源文件,同步到服务器 .env -# 本地开发请使用 .env(从 .env.example 复制) +# 用途:服务器 runtime 配置;bash deploy/sync-api-env.sh production 同步到远端 .env.production +# 本地开发请使用 .env(从 .env.example 复制),勿与生产混用 NODE_ENV=production diff --git a/server/dukang-api/scripts/with-api-env.cjs b/server/dukang-api/scripts/with-api-env.cjs new file mode 100644 index 0000000..06d3554 --- /dev/null +++ b/server/dukang-api/scripts/with-api-env.cjs @@ -0,0 +1,30 @@ +import { config } from 'dotenv'; +import { spawnSync } from 'child_process'; +import { existsSync } from 'fs'; +import { resolve } from 'path'; + +const apiRoot = resolve(__dirname, '..'); +const envFile = process.env.DUKANG_ENV_FILE ?? '.env.production'; +const envPath = resolve(apiRoot, envFile); + +if (!existsSync(envPath)) { + console.error(`[with-api-env] 未找到 ${envPath}`); + process.exit(1); +} + +config({ path: envPath, override: true }); + +const args = process.argv.slice(2); +if (args.length === 0) { + console.error('用法: node scripts/with-api-env.cjs [args...]'); + process.exit(1); +} + +const result = spawnSync(args[0], args.slice(1), { + cwd: apiRoot, + stdio: 'inherit', + env: process.env, + shell: true, +}); + +process.exit(result.status ?? 1); diff --git a/server/dukang-api/src/load-env.ts b/server/dukang-api/src/load-env.ts index b8f5ff8..e826b08 100644 --- a/server/dukang-api/src/load-env.ts +++ b/server/dukang-api/src/load-env.ts @@ -5,22 +5,18 @@ import { resolve } from 'path'; /** * 环境变量加载策略(后加载的文件覆盖先前的同名键): * - * - 本地开发(NODE_ENV !== production):仅 `.env` + `.env.local` - * `.env.development` 仅作 deploy/sync-api-env.sh 的源模板,不在本地 dev 自动加载 + * - 本地开发:仅 `.env` + `.env.local` + * `.env.development` 仅作参考模板,不自动加载 * - * - 线上(NODE_ENV=production):`.env.production` → `.env.production.local` → `.env` - * 服务器实际运行时以 sync 脚本写入的 `.env` 为准 + * - 服务器(NODE_ENV=production):仅 `.env.production` + `.env.production.local` + * 不使用 `.env`,避免与本地开发配置混淆 */ const apiRoot = resolve(__dirname, '..'); const nodeEnv = process.env.NODE_ENV ?? 'development'; const isProduction = nodeEnv === 'production'; const layers = isProduction - ? [ - resolve(apiRoot, '.env.production'), - resolve(apiRoot, '.env.production.local'), - resolve(apiRoot, '.env'), - ] + ? [resolve(apiRoot, '.env.production'), resolve(apiRoot, '.env.production.local')] : [resolve(apiRoot, '.env'), resolve(apiRoot, '.env.local')]; for (const file of layers) {