From c8dfa50f24c29af930c920611ece663331a73f92 Mon Sep 17 00:00:00 2001 From: jacy-dukang Date: Tue, 7 Jul 2026 20:51:35 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E9=9A=90=E8=97=8FID=E7=9A=84=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/h5-partner/src/pages/StoreDetailPage.tsx | 12 +++--------- apps/h5-shop/src/pages/RecordsPage.tsx | 2 +- apps/h5-shop/src/pages/RedeemConfirmPage.tsx | 2 +- apps/h5-shop/src/pages/RedeemSuccessPage.tsx | 2 +- apps/h5-user/src/pages/MinePage.tsx | 2 -- apps/h5-user/src/pages/PayPage.tsx | 13 ++++++++++++- 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/apps/h5-partner/src/pages/StoreDetailPage.tsx b/apps/h5-partner/src/pages/StoreDetailPage.tsx index 2f53652..eb23634 100644 --- a/apps/h5-partner/src/pages/StoreDetailPage.tsx +++ b/apps/h5-partner/src/pages/StoreDetailPage.tsx @@ -195,15 +195,9 @@ export default function StoreDetailPage() {

管理信息

-
-
- -

{storeStatusLabel(status)}

-
-
- -

{String(store.id)}

-
+
+ +

{storeStatusLabel(status)}

diff --git a/apps/h5-shop/src/pages/RecordsPage.tsx b/apps/h5-shop/src/pages/RecordsPage.tsx index af5cbe0..138c8f3 100644 --- a/apps/h5-shop/src/pages/RecordsPage.tsx +++ b/apps/h5-shop/src/pages/RecordsPage.tsx @@ -134,7 +134,7 @@ export default function RecordsPage() {
订单号 - {String(r.redeemNo || r.id)} + {r.redeemNo ? String(r.redeemNo) : '—'}

核销时间: {new Date(String(r.createdAt)).toLocaleString('zh-CN', { hour12: false }).slice(0, 16)} diff --git a/apps/h5-shop/src/pages/RedeemConfirmPage.tsx b/apps/h5-shop/src/pages/RedeemConfirmPage.tsx index ae69ed8..e4f6597 100644 --- a/apps/h5-shop/src/pages/RedeemConfirmPage.tsx +++ b/apps/h5-shop/src/pages/RedeemConfirmPage.tsx @@ -84,7 +84,7 @@ export default function RedeemConfirmPage() { } const previewAmount = preview?.amount ?? 0; - const userLabel = preview?.user?.nickname || preview?.user?.phone || preview?.user?.userNo || '—'; + const userLabel = preview?.user?.nickname || preview?.user?.phone || '—'; return (

diff --git a/apps/h5-shop/src/pages/RedeemSuccessPage.tsx b/apps/h5-shop/src/pages/RedeemSuccessPage.tsx index a05c11c..a2a0ce4 100644 --- a/apps/h5-shop/src/pages/RedeemSuccessPage.tsx +++ b/apps/h5-shop/src/pages/RedeemSuccessPage.tsx @@ -22,7 +22,7 @@ export default function RedeemSuccessPage() { const storeName = (location.state as { storeName?: string })?.storeName || '当前门店'; const user = (location.state as { user?: { nickname?: string; phone?: string; userNo?: string } })?.user; - const userLabel = user?.nickname || user?.phone || user?.userNo || '—'; + const userLabel = user?.nickname || user?.phone || '—'; const amount = Number(result?.amount ?? 0); const redeemNo = String(result?.redeemNo || '—'); const createdAt = result?.createdAt diff --git a/apps/h5-user/src/pages/MinePage.tsx b/apps/h5-user/src/pages/MinePage.tsx index 3dbe594..b7161f2 100644 --- a/apps/h5-user/src/pages/MinePage.tsx +++ b/apps/h5-user/src/pages/MinePage.tsx @@ -77,7 +77,6 @@ export default function MinePage() { } const nickname = profile?.nickname || '用户'; - const userNo = profile?.userNo || ''; const avatar = profile?.avatarUrl || DEFAULT_AVATAR; const hasWechat = !!profile?.hasWechat; @@ -98,7 +97,6 @@ export default function MinePage() {

{nickname}

- {userNo && ID: {userNo}} {hasWechat ? '微信会员' : '好客会员'}
diff --git a/apps/h5-user/src/pages/PayPage.tsx b/apps/h5-user/src/pages/PayPage.tsx index 92f1ff0..86da844 100644 --- a/apps/h5-user/src/pages/PayPage.tsx +++ b/apps/h5-user/src/pages/PayPage.tsx @@ -40,6 +40,7 @@ export default function PayPage() { const [msg, setMsg] = useState(''); const [showBindPhone, setShowBindPhone] = useState(false); const [wxSessionKey, setWxSessionKey] = useState(null); + const [orderNo, setOrderNo] = useState(''); const refreshPayReadiness = useCallback(async () => { try { @@ -72,6 +73,16 @@ export default function PayPage() { refreshPayReadiness(); }, [refreshPayReadiness]); + useEffect(() => { + if (!orderId) { + setOrderNo(''); + return; + } + request<{ orderNo?: string }>('USER_H5', `/trade/orders/${orderId}`) + .then((order) => setOrderNo(order.orderNo || '')) + .catch(() => setOrderNo('')); + }, [orderId]); + useEffect(() => { if (!isWechatEnv()) return; handleWechatAuthCallback() @@ -181,7 +192,7 @@ export default function PayPage() { )} {msg &&

{msg}

} -

订单号 {orderId}

+ {orderNo &&

订单号 {orderNo}

}
+ {wxAuthorize && ( + <>
其他登录方式
+ + )}
- {!bindMode && ( + {!bindMode && wxAuthorize && ( <>
diff --git a/packages/shared-types/src/config.ts b/packages/shared-types/src/config.ts index 82b718c..3bf977a 100644 --- a/packages/shared-types/src/config.ts +++ b/packages/shared-types/src/config.ts @@ -6,6 +6,8 @@ export interface AppConfig { autoApproveStore: boolean; /** preV1 Mock 微信授权登录:点击授权按钮走 Mock 流程直接登录,不接真实微信 */ mockWechat: boolean; + /** 登录后是否走微信 SDK OAuth 授权(WX_AUTHORIZE=false 时三端跳过授权流程) */ + wxAuthorize: boolean; wechatAuthEnabled: boolean; wechatPayEnabled: boolean; wxAppId: string; @@ -35,6 +37,7 @@ export function loadAppConfig(env?: Record): AppConf mockDeliveryAuto: e.MOCK_DELIVERY_AUTO !== 'false', autoApproveStore: e.AUTO_APPROVE_STORE !== 'false', mockWechat: e.MOCK_WECHAT === 'true', + wxAuthorize: e.WX_AUTHORIZE !== 'false', wechatAuthEnabled: e.WECHAT_AUTH_ENABLED === 'true', wechatPayEnabled: e.WECHAT_PAY_ENABLED === 'true' || e.MOCK_PAY === 'false', wxAppId: e.WX_APP_ID ?? '', diff --git a/packages/shared-types/src/wechat.ts b/packages/shared-types/src/wechat.ts index 8cc1d5d..40c5fa0 100644 --- a/packages/shared-types/src/wechat.ts +++ b/packages/shared-types/src/wechat.ts @@ -28,8 +28,16 @@ export type ClientRuntimeConfig = { mockPay: boolean; wechatPayEnabled: boolean; mockSms: boolean; + mockWechat?: boolean; + /** false 时三端跳过微信 SDK OAuth 授权 */ + wxAuthorize?: boolean; }; +/** 是否启用登录后微信 SDK 授权(默认 true,仅 WX_AUTHORIZE=false 时关闭) */ +export function isWxAuthorizeEnabled(config?: Pick | null): boolean { + return config?.wxAuthorize !== false; +} + export interface WechatLoginResult { accessToken?: string; refreshToken?: string; diff --git a/server/dukang-api/.env.example b/server/dukang-api/.env.example index b961347..fbe4485 100644 --- a/server/dukang-api/.env.example +++ b/server/dukang-api/.env.example @@ -22,6 +22,8 @@ AUTO_APPROVE_STORE=true # 微信 OAuth Mock(preV1 本地联调):须在微信内置浏览器内打开 H5,走 OAuth 回跳带 mock code; # 非微信浏览器不会发起 /login/wechat 请求。生产请 MOCK_WECHAT=false 且 WECHAT_AUTH_ENABLED=true。 MOCK_WECHAT=true +# 登录后是否走微信 SDK OAuth 授权(本地 false 可仅用短信登录/核销,不影响支付 Mock) +WX_AUTHORIZE=false # C 端 H5 落地页(推广码二维码链接前缀) USER_H5_URL=http://localhost:5173 diff --git a/server/dukang-api/.env.production.example b/server/dukang-api/.env.production.example index 09e496c..9c5dedf 100644 --- a/server/dukang-api/.env.production.example +++ b/server/dukang-api/.env.production.example @@ -24,6 +24,8 @@ TRUST_PROXY=true WECHAT_AUTH_ENABLED=true WECHAT_PAY_ENABLED=true +# 登录后走微信 SDK OAuth 授权(生产/预发建议 true) +WX_AUTHORIZE=true WX_APP_ID= WX_APP_SECRET= WX_MCH_ID= diff --git a/server/dukang-api/src/modules/common/client-config.controller.ts b/server/dukang-api/src/modules/common/client-config.controller.ts index 9307744..87a5b23 100644 --- a/server/dukang-api/src/modules/common/client-config.controller.ts +++ b/server/dukang-api/src/modules/common/client-config.controller.ts @@ -11,6 +11,7 @@ export class ClientConfigController { wechatPayEnabled: cfg.wechatPayEnabled, mockSms: cfg.mockSms, mockWechat: cfg.mockWechat, + wxAuthorize: cfg.wxAuthorize, }; } } From eb961c3a74ce19a147e8b4ab58bdc4022270ac2a Mon Sep 17 00:00:00 2001 From: jacy-dukang Date: Tue, 7 Jul 2026 22:07:47 +0800 Subject: [PATCH 4/4] feat(h5): unify WeChat OAuth under m.runxian.top path routing Serve user/shop/partner H5 under /user/, /shop/, /partner/ on a single authorized domain; update nginx, SSL script, and router base paths. Co-authored-by: Cursor --- .../src/contexts/PartnerSessionContext.tsx | 3 +- apps/h5-partner/src/lib/api.ts | 6 +- apps/h5-partner/src/main.tsx | 3 +- apps/h5-partner/vite.config.ts | 1 + apps/h5-shop/src/main.tsx | 3 +- apps/h5-shop/src/pages/HomePage.tsx | 2 +- apps/h5-shop/vite.config.ts | 1 + apps/h5-user/src/lib/pay-wechat.ts | 3 +- apps/h5-user/src/main.tsx | 3 +- apps/h5-user/vite.config.ts | 1 + deploy/enable-runxian-dukang-ssl.sh | 14 ++- deploy/nginx-runxian-dukang-ssl.conf | 99 +++++++++++++------ deploy/nginx-runxian-dukang.conf | 66 +++++++++---- packages/weixin-sdk/src/app-path.ts | 25 +++++ packages/weixin-sdk/src/index.ts | 1 + scripts/configure-oss-cors.mjs | 4 + server/dukang-api/.env.example | 6 +- server/dukang-api/.env.production.example | 3 + .../modules/ops/admin-promo-codes.service.ts | 2 +- 19 files changed, 178 insertions(+), 68 deletions(-) create mode 100644 packages/weixin-sdk/src/app-path.ts diff --git a/apps/h5-partner/src/contexts/PartnerSessionContext.tsx b/apps/h5-partner/src/contexts/PartnerSessionContext.tsx index 0a6af33..ceb2183 100644 --- a/apps/h5-partner/src/contexts/PartnerSessionContext.tsx +++ b/apps/h5-partner/src/contexts/PartnerSessionContext.tsx @@ -1,4 +1,5 @@ import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react'; +import { toAppPath } from '@dukang/weixin-sdk'; import { clearAuth, isLoggedIn, request } from '../lib/api'; export type PartnerAccount = { @@ -43,7 +44,7 @@ export function PartnerSessionProvider({ children }: { children: ReactNode }) { const logout = useCallback(() => { clearAuth(); setAccount(null); - window.location.href = '/login'; + window.location.href = toAppPath('/login'); }, []); useEffect(() => { diff --git a/apps/h5-partner/src/lib/api.ts b/apps/h5-partner/src/lib/api.ts index 06e737a..16df1e6 100644 --- a/apps/h5-partner/src/lib/api.ts +++ b/apps/h5-partner/src/lib/api.ts @@ -1,3 +1,5 @@ +import { isOnAppPath, toAppPath } from '@dukang/weixin-sdk'; + export const apiBase = '/api/v1'; export async function request(clientApp: string, path: string, options: RequestInit = {}): Promise { @@ -12,8 +14,8 @@ export async function request(clientApp: string, path: string, options: Reque const json = await res.json().catch(() => ({ code: res.status, message: '网络异常' })); if (res.status === 401 || json.code === 401) { clearAuth(); - if (typeof window !== 'undefined' && !window.location.pathname.startsWith('/login')) { - window.location.href = '/login'; + if (typeof window !== 'undefined' && !isOnAppPath('/login')) { + window.location.href = toAppPath('/login'); } throw new Error(json.message || '登录已过期,请重新登录'); } diff --git a/apps/h5-partner/src/main.tsx b/apps/h5-partner/src/main.tsx index fef9f9a..9ad9251 100644 --- a/apps/h5-partner/src/main.tsx +++ b/apps/h5-partner/src/main.tsx @@ -1,13 +1,14 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; +import { getRouterBasename } from '@dukang/weixin-sdk'; import App from './App'; import { PartnerSessionProvider } from './contexts/PartnerSessionContext'; import './styles.css'; ReactDOM.createRoot(document.getElementById('root')!).render( - + diff --git a/apps/h5-partner/vite.config.ts b/apps/h5-partner/vite.config.ts index 3482978..ea673dc 100644 --- a/apps/h5-partner/vite.config.ts +++ b/apps/h5-partner/vite.config.ts @@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react'; import path from 'path'; export default defineConfig({ + base: '/partner/', plugins: [react()], resolve: { alias: { diff --git a/apps/h5-shop/src/main.tsx b/apps/h5-shop/src/main.tsx index 50cf420..29a6ad7 100644 --- a/apps/h5-shop/src/main.tsx +++ b/apps/h5-shop/src/main.tsx @@ -1,13 +1,14 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; +import { getRouterBasename } from '@dukang/weixin-sdk'; import { StoreSessionProvider } from './contexts/StoreSessionContext'; import App from './App'; import './styles.css'; ReactDOM.createRoot(document.getElementById('root')!).render( - + diff --git a/apps/h5-shop/src/pages/HomePage.tsx b/apps/h5-shop/src/pages/HomePage.tsx index 0fc1fc4..96e432f 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 接口安全域名为 shop.runxian.top,并刷新页面后重试'; + return '微信 JSSDK 签名校验失败:请确认公众号已配置 JS 接口安全域名为 m.runxian.top,并刷新页面后重试'; } return msg; } diff --git a/apps/h5-shop/vite.config.ts b/apps/h5-shop/vite.config.ts index c03c985..c33c06a 100644 --- a/apps/h5-shop/vite.config.ts +++ b/apps/h5-shop/vite.config.ts @@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react'; import path from 'path'; export default defineConfig({ + base: '/shop/', plugins: [react()], resolve: { alias: { diff --git a/apps/h5-user/src/lib/pay-wechat.ts b/apps/h5-user/src/lib/pay-wechat.ts index 8387bfb..85a5bba 100644 --- a/apps/h5-user/src/lib/pay-wechat.ts +++ b/apps/h5-user/src/lib/pay-wechat.ts @@ -1,5 +1,6 @@ import type { ClientRuntimeConfig, WechatLoginResult } from '@dukang/shared-types'; import { isWxAuthorizeEnabled } from '@dukang/shared-types'; +import { toAppPath } from '@dukang/weixin-sdk'; import { isWechatEnv, weixinSdk } from './weixin'; import { request, saveSession, type UserProfile } from './api'; @@ -48,5 +49,5 @@ export async function authorizeWechatForPay(): Promise } export function buildLoginReturnUrl(pathname: string, search: string) { - return `/login?return=${encodeURIComponent(`${pathname}${search}`)}`; + return `${toAppPath('/login')}?return=${encodeURIComponent(`${pathname}${search}`)}`; } diff --git a/apps/h5-user/src/main.tsx b/apps/h5-user/src/main.tsx index a170668..fcde7ac 100644 --- a/apps/h5-user/src/main.tsx +++ b/apps/h5-user/src/main.tsx @@ -1,12 +1,13 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; +import { getRouterBasename } from '@dukang/weixin-sdk'; import App from './App'; import './styles.css'; ReactDOM.createRoot(document.getElementById('root')!).render( - + , diff --git a/apps/h5-user/vite.config.ts b/apps/h5-user/vite.config.ts index ff8d8a9..5daf8b8 100644 --- a/apps/h5-user/vite.config.ts +++ b/apps/h5-user/vite.config.ts @@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react'; import path from 'path'; export default defineConfig({ + base: '/user/', plugins: [react()], resolve: { alias: { diff --git a/deploy/enable-runxian-dukang-ssl.sh b/deploy/enable-runxian-dukang-ssl.sh index 4240266..04650e4 100644 --- a/deploy/enable-runxian-dukang-ssl.sh +++ b/deploy/enable-runxian-dukang-ssl.sh @@ -1,9 +1,11 @@ #!/usr/bin/env bash -# 为杜康 runxian.top 五域名申请 Let's Encrypt 并切换 HTTPS 配置 +# 为杜康 runxian.top 申请 Let's Encrypt 并切换 HTTPS 配置 +# 三端 H5 统一入口 m.runxian.top(微信网页授权单域名) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" DOMAINS=( + m.runxian.top user.runxian.top shop.runxian.top partner.runxian.top @@ -25,7 +27,8 @@ mkdir -p /var/www/certbot /var/log/nginx/dukang echo "==> 申请证书..." certbot certonly --webroot -w /var/www/certbot \ - --cert-name user.runxian.top \ + --cert-name m.runxian.top \ + -d m.runxian.top \ -d user.runxian.top \ -d shop.runxian.top \ -d partner.runxian.top \ @@ -43,13 +46,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://shop.runxian.top/ \ - https://partner.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 已启用" +echo "==> runxian.top 杜康域名 HTTPS 已启用(H5 统一入口 m.runxian.top)" diff --git a/deploy/nginx-runxian-dukang-ssl.conf b/deploy/nginx-runxian-dukang-ssl.conf index b240a5c..d659637 100644 --- a/deploy/nginx-runxian-dukang-ssl.conf +++ b/deploy/nginx-runxian-dukang-ssl.conf @@ -1,28 +1,16 @@ -# 杜康好客 — runxian.top HTTPS(证书申请后启用) +# 杜康好客 — runxian.top HTTPS # 执行: deploy/enable-runxian-dukang-ssl.sh - -map $host $dukang_runxian_port { - user.runxian.top 8091; - shop.runxian.top 8092; - partner.runxian.top 8093; - webadmin.runxian.top 8094; -} +# 三端 H5 统一入口 m.runxian.top/{user,shop,partner}/ server { listen 80; - server_name user.runxian.top shop.runxian.top partner.runxian.top; + server_name m.runxian.top user.runxian.top shop.runxian.top partner.runxian.top; location ^~ /.well-known/acme-challenge/ { root /var/www/certbot; default_type "text/plain"; } - location ~ ^/MP_verify_.*\.txt$ { - root /opt/dukang-haoke/apps/h5-user/dist; - default_type text/plain; - access_log off; - } - location / { return 301 https://$host$request_uri; } @@ -30,18 +18,24 @@ server { server { listen 443 ssl; - server_name user.runxian.top shop.runxian.top partner.runxian.top; + server_name m.runxian.top; - access_log /var/log/nginx/dukang/runxian-h5.access.log main; - error_log /var/log/nginx/dukang/runxian-h5.error.log warn; + 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_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; ssl_prefer_server_ciphers on; client_max_body_size 20m; + location ~ ^/MP_verify_.*\.txt$ { + root /opt/dukang-haoke/apps/h5-user/dist; + default_type text/plain; + access_log off; + } + location /api/ { proxy_pass http://127.0.0.1:8090; proxy_http_version 1.1; @@ -52,14 +46,57 @@ server { proxy_set_header Connection ""; } - location / { - proxy_pass http://127.0.0.1:$dukang_runxian_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; + location = / { + return 302 /user/; } + + location /user/ { + alias /opt/dukang-haoke/apps/h5-user/dist/; + try_files $uri $uri/ index.html; + } + + location /shop/ { + alias /opt/dukang-haoke/apps/h5-shop/dist/; + try_files $uri $uri/ index.html; + } + + location /partner/ { + 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; +} + +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_protocols TLSv1.2 TLSv1.3; + + return 301 https://m.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_protocols TLSv1.2 TLSv1.3; + + return 301 https://m.runxian.top/partner$request_uri; } server { @@ -83,8 +120,8 @@ 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/user.runxian.top/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/user.runxian.top/privkey.pem; + 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; ssl_prefer_server_ciphers on; @@ -122,8 +159,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/user.runxian.top/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/user.runxian.top/privkey.pem; + 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; ssl_prefer_server_ciphers on; diff --git a/deploy/nginx-runxian-dukang.conf b/deploy/nginx-runxian-dukang.conf index 7247625..6487cf3 100644 --- a/deploy/nginx-runxian-dukang.conf +++ b/deploy/nginx-runxian-dukang.conf @@ -1,21 +1,16 @@ -# 杜康好客 — runxian.top 别名域名(与 lingshivip.cn 同后端) -# user/shop/partner → 8091/8092/8093;webadmin → 8094;dkapi → 8090 -# 证书:certbot --cert-name user.runxian.top -d user.runxian.top -d shop.runxian.top ... +# 杜康好客 — runxian.top(HTTP) +# 三端 H5 统一入口 m.runxian.top/{user,shop,partner}/(微信网页授权单域名) +# 旧子域名 user/shop/partner 301 到统一入口;webadmin → 8094;dkapi → 8090 -map $host $dukang_runxian_port { - user.runxian.top 8091; - shop.runxian.top 8092; - partner.runxian.top 8093; - webadmin.runxian.top 8094; -} - -# --- HTTP: H5 三端(校验文件 + API;证书就绪后见 dukang-runxian-ssl.conf)--- +# --- 统一 H5 入口(证书就绪前 HTTP;HTTPS 见 dukang-runxian-ssl.conf)--- server { listen 80; - server_name user.runxian.top shop.runxian.top partner.runxian.top; + server_name m.runxian.top; - access_log /var/log/nginx/dukang/runxian-h5.access.log main; - error_log /var/log/nginx/dukang/runxian-h5.error.log warn; + access_log /var/log/nginx/dukang/runxian-m.access.log main; + error_log /var/log/nginx/dukang/runxian-m.error.log warn; + + client_max_body_size 20m; location ^~ /.well-known/acme-challenge/ { root /var/www/certbot; @@ -38,14 +33,43 @@ server { proxy_set_header Connection ""; } - location / { - proxy_pass http://127.0.0.1:$dukang_runxian_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; + location = / { + return 302 /user/; } + + location /user/ { + alias /opt/dukang-haoke/apps/h5-user/dist/; + try_files $uri $uri/ index.html; + } + + location /shop/ { + alias /opt/dukang-haoke/apps/h5-shop/dist/; + try_files $uri $uri/ index.html; + } + + location /partner/ { + 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; +} + +server { + listen 80; + server_name shop.runxian.top; + return 301 http://m.runxian.top/shop$request_uri; +} + +server { + listen 80; + server_name partner.runxian.top; + return 301 http://m.runxian.top/partner$request_uri; } # --- HTTP: API --- diff --git a/packages/weixin-sdk/src/app-path.ts b/packages/weixin-sdk/src/app-path.ts new file mode 100644 index 0000000..010af01 --- /dev/null +++ b/packages/weixin-sdk/src/app-path.ts @@ -0,0 +1,25 @@ +/// + +function normalizeBase(base?: string): string { + if (!base || base === '/') return ''; + return base.replace(/\/$/, ''); +} + +/** React Router basename(来自 Vite `base`) */ +export function getRouterBasename(baseUrl?: string): string | undefined { + const base = normalizeBase(baseUrl ?? import.meta.env?.BASE_URL); + return base || undefined; +} + +/** 拼接带 Vite base 前缀的应用内路径(用于 window.location 跳转) */ +export function toAppPath(path: string, baseUrl?: string): string { + const base = normalizeBase(baseUrl ?? import.meta.env?.BASE_URL); + const normalized = path.startsWith('/') ? path : `/${path}`; + return `${base}${normalized}`; +} + +export function isOnAppPath(path: string, baseUrl?: string): boolean { + if (typeof window === 'undefined') return false; + const target = toAppPath(path, baseUrl); + return window.location.pathname === target || window.location.pathname.startsWith(`${target}/`); +} diff --git a/packages/weixin-sdk/src/index.ts b/packages/weixin-sdk/src/index.ts index d94f1be..abbedb6 100644 --- a/packages/weixin-sdk/src/index.ts +++ b/packages/weixin-sdk/src/index.ts @@ -1,4 +1,5 @@ export { isWechatBrowser, isMiniProgram, getRuntimePlatform } from './env'; +export { getRouterBasename, toAppPath, isOnAppPath } from './app-path'; export { initWechatJssdk, ensureJssdkReady, isJssdkReady, normalizeJssdkPageUrl, stripOAuthParamsFromLocation } from './jssdk'; export { getWechatLocation, diff --git a/scripts/configure-oss-cors.mjs b/scripts/configure-oss-cors.mjs index c760789..3212980 100644 --- a/scripts/configure-oss-cors.mjs +++ b/scripts/configure-oss-cors.mjs @@ -54,6 +54,10 @@ const defaultOrigins = [ 'http://127.0.0.1:5173', 'http://127.0.0.1:5174', 'http://127.0.0.1:5175', + 'http://localhost:5173/user', + 'http://localhost:5174/shop', + 'http://localhost:5175/partner', + 'https://m.runxian.top', ]; const allowedOrigin = (OSS_CORS_ORIGINS ?? defaultOrigins.join(',')) diff --git a/server/dukang-api/.env.example b/server/dukang-api/.env.example index fbe4485..b668735 100644 --- a/server/dukang-api/.env.example +++ b/server/dukang-api/.env.example @@ -26,13 +26,15 @@ MOCK_WECHAT=true WX_AUTHORIZE=false # C 端 H5 落地页(推广码二维码链接前缀) -USER_H5_URL=http://localhost:5173 +# 本地开发:http://localhost:5173/user 生产统一入口:https://m.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 生成;C/合伙人/总部 H5 均须在微信内置浏览器内授权。 +# OAuth 授权页由 /common/wechat/oauth-url 生成;三端统一入口 m.runxian.top/{user,shop,partner}。 +# 微信服务号「网页授权域名」「JS 接口安全域名」均配置 m.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 9c5dedf..c211339 100644 --- a/server/dukang-api/.env.production.example +++ b/server/dukang-api/.env.production.example @@ -22,6 +22,9 @@ AUTO_APPROVE_STORE=false TRUST_PROXY=true +# C 端 H5 落地页(推广码二维码;生产统一入口) +USER_H5_URL=https://m.runxian.top/user + WECHAT_AUTH_ENABLED=true WECHAT_PAY_ENABLED=true # 登录后走微信 SDK OAuth 授权(生产/预发建议 true) diff --git a/server/dukang-api/src/modules/ops/admin-promo-codes.service.ts b/server/dukang-api/src/modules/ops/admin-promo-codes.service.ts index d17b9ce..a9addcf 100644 --- a/server/dukang-api/src/modules/ops/admin-promo-codes.service.ts +++ b/server/dukang-api/src/modules/ops/admin-promo-codes.service.ts @@ -9,7 +9,7 @@ import { CreatePromoCodeDto } from './dto/admin-mutate.dto'; import { AdminPromoCodesQueryDto } from './dto/admin-query.dto'; function userH5Base(): string { - return (process.env.USER_H5_URL || 'http://localhost:5173').replace(/\/$/, ''); + return (process.env.USER_H5_URL || 'http://localhost:5173/user').replace(/\/$/, ''); } function buildLandingUrl(code: string): string {