merge(dev_jacy): unify H5 under m.runxian.top for WeChat OAuth
This commit is contained in:
@@ -2,11 +2,13 @@ import { useEffect, useId, useRef, useState } from 'react';
|
|||||||
import { uploadFileToOss, type OssMediaType } from '../lib/upload';
|
import { uploadFileToOss, type OssMediaType } from '../lib/upload';
|
||||||
import {
|
import {
|
||||||
authorizePartnerWechat,
|
authorizePartnerWechat,
|
||||||
|
fetchClientConfig,
|
||||||
fetchPartnerProfile,
|
fetchPartnerProfile,
|
||||||
needsWechatAuth,
|
needsWechatAuth,
|
||||||
type PartnerProfile,
|
type PartnerProfile,
|
||||||
} from '../lib/wechat-auth';
|
} from '../lib/wechat-auth';
|
||||||
import { isWechatEnv, weixinSdk } from '../lib/weixin';
|
import { isWechatEnv, weixinSdk } from '../lib/weixin';
|
||||||
|
import type { ClientRuntimeConfig } from '@dukang/shared-types';
|
||||||
|
|
||||||
type OssUploadFieldProps = {
|
type OssUploadFieldProps = {
|
||||||
value?: string;
|
value?: string;
|
||||||
@@ -42,17 +44,19 @@ export default function OssUploadField({
|
|||||||
const [authorizing, setAuthorizing] = useState(false);
|
const [authorizing, setAuthorizing] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [profile, setProfile] = useState<PartnerProfile | null>(null);
|
const [profile, setProfile] = useState<PartnerProfile | null>(null);
|
||||||
|
const [clientConfig, setClientConfig] = useState<ClientRuntimeConfig | null>(null);
|
||||||
|
|
||||||
const resolvedAccept =
|
const resolvedAccept =
|
||||||
accept ?? (mediaType === 'VIDEO' ? 'video/*' : mediaType === 'FILE' ? 'image/*,.pdf' : 'image/*');
|
accept ?? (mediaType === 'VIDEO' ? 'video/*' : mediaType === 'FILE' ? 'image/*,.pdf' : 'image/*');
|
||||||
const useWechatPicker = isWechatEnv() && mediaType !== 'VIDEO';
|
const useWechatPicker = isWechatEnv() && mediaType !== 'VIDEO';
|
||||||
const needsAuth = useWechatPicker && needsWechatAuth(profile) && wechatReady !== true;
|
const needsAuth = useWechatPicker && needsWechatAuth(profile, clientConfig) && wechatReady !== true;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!useWechatPicker) return;
|
if (!useWechatPicker) return;
|
||||||
void fetchPartnerProfile()
|
void Promise.all([fetchPartnerProfile(), fetchClientConfig()])
|
||||||
.then((me) => {
|
.then(([me, config]) => {
|
||||||
setProfile(me);
|
setProfile(me);
|
||||||
|
setClientConfig(config);
|
||||||
onWechatReadyChange?.(!!me.hasWechat);
|
onWechatReadyChange?.(!!me.hasWechat);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react';
|
import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react';
|
||||||
|
import { toAppPath } from '@dukang/weixin-sdk';
|
||||||
import { clearAuth, isLoggedIn, request } from '../lib/api';
|
import { clearAuth, isLoggedIn, request } from '../lib/api';
|
||||||
|
|
||||||
export type PartnerAccount = {
|
export type PartnerAccount = {
|
||||||
@@ -43,7 +44,7 @@ export function PartnerSessionProvider({ children }: { children: ReactNode }) {
|
|||||||
const logout = useCallback(() => {
|
const logout = useCallback(() => {
|
||||||
clearAuth();
|
clearAuth();
|
||||||
setAccount(null);
|
setAccount(null);
|
||||||
window.location.href = '/login';
|
window.location.href = toAppPath('/login');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isOnAppPath, toAppPath } from '@dukang/weixin-sdk';
|
||||||
|
|
||||||
export const apiBase = '/api/v1';
|
export const apiBase = '/api/v1';
|
||||||
|
|
||||||
export async function request<T>(clientApp: string, path: string, options: RequestInit = {}): Promise<T> {
|
export async function request<T>(clientApp: string, path: string, options: RequestInit = {}): Promise<T> {
|
||||||
@@ -12,8 +14,8 @@ export async function request<T>(clientApp: string, path: string, options: Reque
|
|||||||
const json = await res.json().catch(() => ({ code: res.status, message: '网络异常' }));
|
const json = await res.json().catch(() => ({ code: res.status, message: '网络异常' }));
|
||||||
if (res.status === 401 || json.code === 401) {
|
if (res.status === 401 || json.code === 401) {
|
||||||
clearAuth();
|
clearAuth();
|
||||||
if (typeof window !== 'undefined' && !window.location.pathname.startsWith('/login')) {
|
if (typeof window !== 'undefined' && !isOnAppPath('/login')) {
|
||||||
window.location.href = '/login';
|
window.location.href = toAppPath('/login');
|
||||||
}
|
}
|
||||||
throw new Error(json.message || '登录已过期,请重新登录');
|
throw new Error(json.message || '登录已过期,请重新登录');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { WechatLoginResult } from '@dukang/shared-types';
|
import type { ClientRuntimeConfig, WechatLoginResult } from '@dukang/shared-types';
|
||||||
|
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
|
||||||
import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk';
|
import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk';
|
||||||
import { isWechatEnv, weixinSdk } from './weixin';
|
import { isWechatEnv, weixinSdk } from './weixin';
|
||||||
import { request, saveAuth } from './api';
|
import { request, saveAuth } from './api';
|
||||||
@@ -11,15 +12,28 @@ export type PartnerProfile = {
|
|||||||
hasWechat?: boolean;
|
hasWechat?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function fetchClientConfig(): Promise<ClientRuntimeConfig> {
|
||||||
|
return request<ClientRuntimeConfig>('PARTNER_H5', '/common/client-config');
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchPartnerProfile(): Promise<PartnerProfile> {
|
export async function fetchPartnerProfile(): Promise<PartnerProfile> {
|
||||||
return request<PartnerProfile>('PARTNER_H5', '/partner/me');
|
return request<PartnerProfile>('PARTNER_H5', '/partner/me');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 微信内上传照片前需完成公众号授权绑定 */
|
/** 微信内上传照片前需完成公众号授权绑定 */
|
||||||
export function needsWechatAuth(profile: PartnerProfile | null): boolean {
|
export function needsWechatAuth(
|
||||||
|
profile: PartnerProfile | null,
|
||||||
|
config?: Pick<ClientRuntimeConfig, 'wxAuthorize'> | null,
|
||||||
|
): boolean {
|
||||||
|
if (config && !isWxAuthorizeEnabled(config)) return false;
|
||||||
return isWechatEnv() && !!profile && !profile.hasWechat;
|
return isWechatEnv() && !!profile && !profile.hasWechat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function checkNeedsWechatAuth(profile: PartnerProfile | null): Promise<boolean> {
|
||||||
|
const config = await fetchClientConfig();
|
||||||
|
return needsWechatAuth(profile, config);
|
||||||
|
}
|
||||||
|
|
||||||
/** 处理微信登录/绑定结果,返回是否已拿到 token 可进入首页 */
|
/** 处理微信登录/绑定结果,返回是否已拿到 token 可进入首页 */
|
||||||
export function handlePartnerWechatLoginResult(result: WechatLoginResult): boolean {
|
export function handlePartnerWechatLoginResult(result: WechatLoginResult): boolean {
|
||||||
if (!result.accessToken) return false;
|
if (!result.accessToken) return false;
|
||||||
@@ -29,6 +43,8 @@ export function handlePartnerWechatLoginResult(result: WechatLoginResult): boole
|
|||||||
|
|
||||||
export async function handlePartnerWechatCallback(): Promise<WechatLoginResult | null> {
|
export async function handlePartnerWechatCallback(): Promise<WechatLoginResult | null> {
|
||||||
if (!isWechatEnv()) return null;
|
if (!isWechatEnv()) return null;
|
||||||
|
const config = await fetchClientConfig();
|
||||||
|
if (!isWxAuthorizeEnabled(config)) return null;
|
||||||
return weixinSdk.handleOAuthCallback();
|
return weixinSdk.handleOAuthCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +53,8 @@ export async function handlePartnerWechatCallback(): Promise<WechatLoginResult |
|
|||||||
* 返回 true = 已登录;void = 已跳转授权页等待回调。
|
* 返回 true = 已登录;void = 已跳转授权页等待回调。
|
||||||
*/
|
*/
|
||||||
export async function loginPartnerWithWechat(): Promise<boolean | void> {
|
export async function loginPartnerWithWechat(): Promise<boolean | void> {
|
||||||
|
const config = await fetchClientConfig();
|
||||||
|
if (!isWxAuthorizeEnabled(config)) return false;
|
||||||
if (!isWechatEnv()) {
|
if (!isWechatEnv()) {
|
||||||
throw new Error(WECHAT_INAPP_REQUIRED_MSG);
|
throw new Error(WECHAT_INAPP_REQUIRED_MSG);
|
||||||
}
|
}
|
||||||
@@ -48,11 +66,15 @@ export async function loginPartnerWithWechat(): Promise<boolean | void> {
|
|||||||
* 短信登录成功后于微信内自动发起 OAuth,将 openId 绑定到当前合伙人账号(便于同一微信后续免登)。
|
* 短信登录成功后于微信内自动发起 OAuth,将 openId 绑定到当前合伙人账号(便于同一微信后续免登)。
|
||||||
*/
|
*/
|
||||||
export async function bindPartnerWechatAfterSmsLogin(): Promise<void> {
|
export async function bindPartnerWechatAfterSmsLogin(): Promise<void> {
|
||||||
|
const config = await fetchClientConfig();
|
||||||
|
if (!isWxAuthorizeEnabled(config)) return;
|
||||||
if (!isWechatEnv()) return;
|
if (!isWechatEnv()) return;
|
||||||
await weixinSdk.login();
|
await weixinSdk.login();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function authorizePartnerWechat(): Promise<WechatLoginResult | void> {
|
export async function authorizePartnerWechat(): Promise<WechatLoginResult | void> {
|
||||||
|
const config = await fetchClientConfig();
|
||||||
|
if (!isWxAuthorizeEnabled(config)) return;
|
||||||
if (!isWechatEnv()) {
|
if (!isWechatEnv()) {
|
||||||
throw new Error(WECHAT_INAPP_REQUIRED_MSG);
|
throw new Error(WECHAT_INAPP_REQUIRED_MSG);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import { getRouterBasename } from '@dukang/weixin-sdk';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { PartnerSessionProvider } from './contexts/PartnerSessionContext';
|
import { PartnerSessionProvider } from './contexts/PartnerSessionContext';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter basename={getRouterBasename()}>
|
||||||
<PartnerSessionProvider>
|
<PartnerSessionProvider>
|
||||||
<App />
|
<App />
|
||||||
</PartnerSessionProvider>
|
</PartnerSessionProvider>
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
|||||||
import { request, saveAuth } from '../lib/api';
|
import { request, saveAuth } from '../lib/api';
|
||||||
import {
|
import {
|
||||||
bindPartnerWechatAfterSmsLogin,
|
bindPartnerWechatAfterSmsLogin,
|
||||||
|
fetchClientConfig,
|
||||||
handlePartnerWechatCallback,
|
handlePartnerWechatCallback,
|
||||||
handlePartnerWechatLoginResult,
|
handlePartnerWechatLoginResult,
|
||||||
loginPartnerWithWechat,
|
loginPartnerWithWechat,
|
||||||
} from '../lib/wechat-auth';
|
} from '../lib/wechat-auth';
|
||||||
import { isWechatEnv } from '../lib/weixin';
|
import { isWechatEnv } from '../lib/weixin';
|
||||||
|
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
|
||||||
import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk';
|
import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk';
|
||||||
|
|
||||||
const REMEMBER_PHONE_KEY = 'partner_remember_phone';
|
const REMEMBER_PHONE_KEY = 'partner_remember_phone';
|
||||||
@@ -47,16 +49,23 @@ export default function LoginPage() {
|
|||||||
const [wxLoading, setWxLoading] = useState(false);
|
const [wxLoading, setWxLoading] = useState(false);
|
||||||
const [msg, setMsg] = useState('');
|
const [msg, setMsg] = useState('');
|
||||||
const [codeCooldown, setCodeCooldown] = useState(0);
|
const [codeCooldown, setCodeCooldown] = useState(0);
|
||||||
|
const [wxAuthorize, setWxAuthorize] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isWechatEnv()) return;
|
fetchClientConfig()
|
||||||
|
.then((config) => setWxAuthorize(isWxAuthorizeEnabled(config)))
|
||||||
|
.catch(() => setWxAuthorize(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isWechatEnv() || !wxAuthorize) return;
|
||||||
void handlePartnerWechatCallback()
|
void handlePartnerWechatCallback()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
if (handlePartnerWechatLoginResult(result)) navigate('/');
|
if (handlePartnerWechatLoginResult(result)) navigate('/');
|
||||||
})
|
})
|
||||||
.catch((e) => setMsg(formatWechatError(e)));
|
.catch((e) => setMsg(formatWechatError(e)));
|
||||||
}, [navigate]);
|
}, [navigate, wxAuthorize]);
|
||||||
|
|
||||||
function formatWechatError(e: unknown): string {
|
function formatWechatError(e: unknown): string {
|
||||||
const text = e instanceof Error ? e.message : '微信登录失败';
|
const text = e instanceof Error ? e.message : '微信登录失败';
|
||||||
@@ -129,7 +138,7 @@ export default function LoginPage() {
|
|||||||
});
|
});
|
||||||
saveAuth(data);
|
saveAuth(data);
|
||||||
persistRememberAccount(phone);
|
persistRememberAccount(phone);
|
||||||
if (isWechatEnv()) {
|
if (isWechatEnv() && wxAuthorize) {
|
||||||
setMsg('登录成功,正在关联微信…');
|
setMsg('登录成功,正在关联微信…');
|
||||||
await bindPartnerWechatAfterSmsLogin();
|
await bindPartnerWechatAfterSmsLogin();
|
||||||
return;
|
return;
|
||||||
@@ -264,6 +273,8 @@ export default function LoginPage() {
|
|||||||
{!loading && <span className="material-symbols-outlined">arrow_forward</span>}
|
{!loading && <span className="material-symbols-outlined">arrow_forward</span>}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{wxAuthorize && (
|
||||||
|
<>
|
||||||
<div className="partner-auth-divider"><span>其他登录方式</span></div>
|
<div className="partner-auth-divider"><span>其他登录方式</span></div>
|
||||||
<button type="button" className="partner-btn-wechat" onClick={wechatLogin} disabled={wxLoading}>
|
<button type="button" className="partner-btn-wechat" onClick={wechatLogin} disabled={wxLoading}>
|
||||||
<svg viewBox="0 0 24 24" fill="#07C160" aria-hidden>
|
<svg viewBox="0 0 24 24" fill="#07C160" aria-hidden>
|
||||||
@@ -271,6 +282,8 @@ export default function LoginPage() {
|
|||||||
</svg>
|
</svg>
|
||||||
<span>{wxLoading ? '登录中...' : '微信一键授权'}</span>
|
<span>{wxLoading ? '登录中...' : '微信一键授权'}</span>
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<label className="partner-checkbox-row">
|
<label className="partner-checkbox-row">
|
||||||
<input type="checkbox" checked={agreed} onChange={(e) => setAgreed(e.target.checked)} />
|
<input type="checkbox" checked={agreed} onChange={(e) => setAgreed(e.target.checked)} />
|
||||||
|
|||||||
@@ -195,15 +195,9 @@ export default function StoreDetailPage() {
|
|||||||
|
|
||||||
<section className="partner-form-card" style={{ margin: 0, background: 'var(--color-surface-container)' }}>
|
<section className="partner-form-card" style={{ margin: 0, background: 'var(--color-surface-container)' }}>
|
||||||
<h3 className="headline-md" style={{ borderLeft: '4px solid var(--color-subtle-gray)', paddingLeft: 12, marginBottom: 16 }}>管理信息</h3>
|
<h3 className="headline-md" style={{ borderLeft: '4px solid var(--color-subtle-gray)', paddingLeft: 12, marginBottom: 16 }}>管理信息</h3>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
<div>
|
||||||
<div>
|
<label className="label-md text-muted">状态</label>
|
||||||
<label className="label-md text-muted">状态</label>
|
<p className="body-md" style={{ fontWeight: 500 }}>{storeStatusLabel(status)}</p>
|
||||||
<p className="body-md" style={{ fontWeight: 500 }}>{storeStatusLabel(status)}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="label-md text-muted">门店ID</label>
|
|
||||||
<p className="body-md" style={{ fontWeight: 500, fontFamily: 'monospace' }}>{String(store.id)}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: '/partner/',
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { WechatLoginResult } from '@dukang/shared-types';
|
import type { ClientRuntimeConfig, WechatLoginResult } from '@dukang/shared-types';
|
||||||
|
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
|
||||||
import { isWechatEnv, weixinSdk } from './weixin';
|
import { isWechatEnv, weixinSdk } from './weixin';
|
||||||
import { request, saveAuth, type ShopSessionPayload } from './api';
|
import { request, saveAuth, type ShopSessionPayload } from './api';
|
||||||
|
|
||||||
@@ -11,14 +12,27 @@ export type ShopAccountProfile = {
|
|||||||
store?: { name: string };
|
store?: { name: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function fetchClientConfig(): Promise<ClientRuntimeConfig> {
|
||||||
|
return request<ClientRuntimeConfig>('SHOP_H5', '/common/client-config');
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchShopAccount(): Promise<ShopAccountProfile> {
|
export async function fetchShopAccount(): Promise<ShopAccountProfile> {
|
||||||
return request<ShopAccountProfile>('SHOP_H5', '/shop/auth/me');
|
return request<ShopAccountProfile>('SHOP_H5', '/shop/auth/me');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function needsWechatAuth(profile: ShopAccountProfile | null): boolean {
|
export function needsWechatAuth(
|
||||||
|
profile: ShopAccountProfile | null,
|
||||||
|
config?: Pick<ClientRuntimeConfig, 'wxAuthorize'> | null,
|
||||||
|
): boolean {
|
||||||
|
if (config && !isWxAuthorizeEnabled(config)) return false;
|
||||||
return isWechatEnv() && !!profile && !profile.wxOpenId;
|
return isWechatEnv() && !!profile && !profile.wxOpenId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function checkNeedsWechatAuth(profile: ShopAccountProfile | null): Promise<boolean> {
|
||||||
|
const config = await fetchClientConfig();
|
||||||
|
return needsWechatAuth(profile, config);
|
||||||
|
}
|
||||||
|
|
||||||
export function sessionFromWechatLogin(result: WechatLoginResult): ShopSessionPayload | null {
|
export function sessionFromWechatLogin(result: WechatLoginResult): ShopSessionPayload | null {
|
||||||
if (!result.accessToken || !result.refreshToken) return null;
|
if (!result.accessToken || !result.refreshToken) return null;
|
||||||
const store = result.store;
|
const store = result.store;
|
||||||
@@ -45,6 +59,8 @@ export function saveShopWechatAuth(result: WechatLoginResult): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function authorizeShopWechat(): Promise<WechatLoginResult | void> {
|
export async function authorizeShopWechat(): Promise<WechatLoginResult | void> {
|
||||||
|
const config = await fetchClientConfig();
|
||||||
|
if (!isWxAuthorizeEnabled(config)) return;
|
||||||
if (!isWechatEnv()) {
|
if (!isWechatEnv()) {
|
||||||
throw new Error('请在微信内打开以完成授权');
|
throw new Error('请在微信内打开以完成授权');
|
||||||
}
|
}
|
||||||
@@ -53,5 +69,7 @@ export async function authorizeShopWechat(): Promise<WechatLoginResult | void> {
|
|||||||
|
|
||||||
export async function handleShopWechatCallback(): Promise<WechatLoginResult | null> {
|
export async function handleShopWechatCallback(): Promise<WechatLoginResult | null> {
|
||||||
if (!isWechatEnv()) return null;
|
if (!isWechatEnv()) return null;
|
||||||
|
const config = await fetchClientConfig();
|
||||||
|
if (!isWxAuthorizeEnabled(config)) return null;
|
||||||
return weixinSdk.handleOAuthCallback();
|
return weixinSdk.handleOAuthCallback();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import { getRouterBasename } from '@dukang/weixin-sdk';
|
||||||
import { StoreSessionProvider } from './contexts/StoreSessionContext';
|
import { StoreSessionProvider } from './contexts/StoreSessionContext';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter basename={getRouterBasename()}>
|
||||||
<StoreSessionProvider>
|
<StoreSessionProvider>
|
||||||
<App />
|
<App />
|
||||||
</StoreSessionProvider>
|
</StoreSessionProvider>
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { request } from '../lib/api';
|
|||||||
import { parseRedeemTokenFromScan } from '../lib/redeem-scan';
|
import { parseRedeemTokenFromScan } from '../lib/redeem-scan';
|
||||||
import {
|
import {
|
||||||
authorizeShopWechat,
|
authorizeShopWechat,
|
||||||
|
checkNeedsWechatAuth,
|
||||||
fetchShopAccount,
|
fetchShopAccount,
|
||||||
handleShopWechatCallback,
|
handleShopWechatCallback,
|
||||||
needsWechatAuth,
|
|
||||||
saveShopWechatAuth,
|
saveShopWechatAuth,
|
||||||
sessionFromWechatLogin,
|
sessionFromWechatLogin,
|
||||||
} from '../lib/wechat-auth';
|
} from '../lib/wechat-auth';
|
||||||
@@ -24,7 +24,7 @@ function formatMoney(n: number) {
|
|||||||
function formatScanError(e: unknown): string {
|
function formatScanError(e: unknown): string {
|
||||||
const msg = e instanceof Error ? e.message : '扫码失败,请重试';
|
const msg = e instanceof Error ? e.message : '扫码失败,请重试';
|
||||||
if (/invalid signature/i.test(msg)) {
|
if (/invalid signature/i.test(msg)) {
|
||||||
return '微信 JSSDK 签名校验失败:请确认公众号已配置 JS 接口安全域名为 shop.runxian.top,并刷新页面后重试';
|
return '微信 JSSDK 签名校验失败:请确认公众号已配置 JS 接口安全域名为 m.runxian.top,并刷新页面后重试';
|
||||||
}
|
}
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@ export default function HomePage() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const profile = await fetchShopAccount();
|
const profile = await fetchShopAccount();
|
||||||
if (needsWechatAuth(profile)) {
|
if (await checkNeedsWechatAuth(profile)) {
|
||||||
setAuthModalOpen(true);
|
setAuthModalOpen(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import AppImage from '@dukang/shared-ui/AppImage';
|
import AppImage from '@dukang/shared-ui/AppImage';
|
||||||
|
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
|
||||||
import { useStoreSession } from '../contexts/StoreSessionContext';
|
import { useStoreSession } from '../contexts/StoreSessionContext';
|
||||||
import { getLastPhone, getStoreProfile, request, type ShopSessionPayload } from '../lib/api';
|
import { getLastPhone, getStoreProfile, request, type ShopSessionPayload } from '../lib/api';
|
||||||
|
import { fetchClientConfig } from '../lib/wechat-auth';
|
||||||
|
|
||||||
function maskPhone(phone: string) {
|
function maskPhone(phone: string) {
|
||||||
if (phone.length < 7) return phone;
|
if (phone.length < 7) return phone;
|
||||||
@@ -21,6 +23,13 @@ export default function LoginPage() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [msg, setMsg] = useState('');
|
const [msg, setMsg] = useState('');
|
||||||
const [codeCooldown, setCodeCooldown] = useState(0);
|
const [codeCooldown, setCodeCooldown] = useState(0);
|
||||||
|
const [wxAuthorize, setWxAuthorize] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchClientConfig()
|
||||||
|
.then((config) => setWxAuthorize(isWxAuthorizeEnabled(config)))
|
||||||
|
.catch(() => setWxAuthorize(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const quickStoreName = savedProfile?.storeName ?? '门店管理中心';
|
const quickStoreName = savedProfile?.storeName ?? '门店管理中心';
|
||||||
const quickPhone = savedProfile?.phone || phone;
|
const quickPhone = savedProfile?.phone || phone;
|
||||||
@@ -83,6 +92,7 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
function wechatLogin() {
|
function wechatLogin() {
|
||||||
if (!ensureAgreed()) return;
|
if (!ensureAgreed()) return;
|
||||||
|
if (!wxAuthorize) return;
|
||||||
setMsg('preV1:微信一键授权暂未开放,请使用手机验证码登录');
|
setMsg('preV1:微信一键授权暂未开放,请使用手机验证码登录');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,6 +216,8 @@ export default function LoginPage() {
|
|||||||
{!loading && <span className="material-symbols-outlined">arrow_forward</span>}
|
{!loading && <span className="material-symbols-outlined">arrow_forward</span>}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{wxAuthorize && (
|
||||||
|
<>
|
||||||
<div className="shop-login-divider">
|
<div className="shop-login-divider">
|
||||||
<span className="shop-login-divider-line" />
|
<span className="shop-login-divider-line" />
|
||||||
<span className="shop-login-divider-text">或者</span>
|
<span className="shop-login-divider-text">或者</span>
|
||||||
@@ -216,6 +228,8 @@ export default function LoginPage() {
|
|||||||
<span className="material-symbols-outlined">chat</span>
|
<span className="material-symbols-outlined">chat</span>
|
||||||
<span>微信一键授权</span>
|
<span>微信一键授权</span>
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="shop-login-agreement">
|
<label className="shop-login-agreement">
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export default function RecordsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="shop-record-order">
|
<div className="shop-record-order">
|
||||||
<span className="shop-record-time" style={{ margin: 0 }}>订单号</span>
|
<span className="shop-record-time" style={{ margin: 0 }}>订单号</span>
|
||||||
<span>{String(r.redeemNo || r.id)}</span>
|
<span>{r.redeemNo ? String(r.redeemNo) : '—'}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="shop-record-time">
|
<p className="shop-record-time">
|
||||||
核销时间: {new Date(String(r.createdAt)).toLocaleString('zh-CN', { hour12: false }).slice(0, 16)}
|
核销时间: {new Date(String(r.createdAt)).toLocaleString('zh-CN', { hour12: false }).slice(0, 16)}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export default function RedeemConfirmPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const previewAmount = preview?.amount ?? 0;
|
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 (
|
return (
|
||||||
<div className="shop-redeem-page">
|
<div className="shop-redeem-page">
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default function RedeemSuccessPage() {
|
|||||||
|
|
||||||
const storeName = (location.state as { storeName?: string })?.storeName || '当前门店';
|
const storeName = (location.state as { storeName?: string })?.storeName || '当前门店';
|
||||||
const user = (location.state as { user?: { nickname?: string; phone?: string; userNo?: string } })?.user;
|
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 amount = Number(result?.amount ?? 0);
|
||||||
const redeemNo = String(result?.redeemNo || '—');
|
const redeemNo = String(result?.redeemNo || '—');
|
||||||
const createdAt = result?.createdAt
|
const createdAt = result?.createdAt
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: '/shop/',
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { ClientRuntimeConfig, WechatLoginResult } from '@dukang/shared-types';
|
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 { isWechatEnv, weixinSdk } from './weixin';
|
||||||
import { request, saveSession, type UserProfile } from './api';
|
import { request, saveSession, type UserProfile } from './api';
|
||||||
|
|
||||||
@@ -21,6 +23,7 @@ export function needsWechatAuthForPay(
|
|||||||
config: ClientRuntimeConfig,
|
config: ClientRuntimeConfig,
|
||||||
profile: UserProfile | null,
|
profile: UserProfile | null,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
if (!isWxAuthorizeEnabled(config)) return false;
|
||||||
return !config.mockPay && config.wechatPayEnabled && isWechatEnv() && !profile?.hasWechat;
|
return !config.mockPay && config.wechatPayEnabled && isWechatEnv() && !profile?.hasWechat;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +40,8 @@ export function saveWechatLoginResult(result: WechatLoginResult): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function authorizeWechatForPay(): Promise<WechatLoginResult | void> {
|
export async function authorizeWechatForPay(): Promise<WechatLoginResult | void> {
|
||||||
|
const config = await fetchClientConfig();
|
||||||
|
if (!isWxAuthorizeEnabled(config)) return;
|
||||||
if (!isWechatEnv()) {
|
if (!isWechatEnv()) {
|
||||||
throw new Error('请在微信内打开以完成授权');
|
throw new Error('请在微信内打开以完成授权');
|
||||||
}
|
}
|
||||||
@@ -44,5 +49,5 @@ export async function authorizeWechatForPay(): Promise<WechatLoginResult | void>
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildLoginReturnUrl(pathname: string, search: string) {
|
export function buildLoginReturnUrl(pathname: string, search: string) {
|
||||||
return `/login?return=${encodeURIComponent(`${pathname}${search}`)}`;
|
return `${toAppPath('/login')}?return=${encodeURIComponent(`${pathname}${search}`)}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { WechatLoginResult } from '@dukang/shared-types';
|
import type { WechatLoginResult } from '@dukang/shared-types';
|
||||||
|
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
|
||||||
import { isWechatEnv, weixinSdk } from './weixin';
|
import { isWechatEnv, weixinSdk } from './weixin';
|
||||||
import {
|
import {
|
||||||
authorizeWechatForPay,
|
authorizeWechatForPay,
|
||||||
@@ -38,9 +39,20 @@ export async function ensureWechatAuthForPay(): Promise<WechatAuthEnsureResult>
|
|||||||
|
|
||||||
export async function handleWechatAuthCallback(): Promise<WechatLoginResult | null> {
|
export async function handleWechatAuthCallback(): Promise<WechatLoginResult | null> {
|
||||||
if (!isWechatEnv()) return null;
|
if (!isWechatEnv()) return null;
|
||||||
|
const config = await fetchClientConfig();
|
||||||
|
if (!isWxAuthorizeEnabled(config)) return null;
|
||||||
return weixinSdk.handleOAuthCallback();
|
return weixinSdk.handleOAuthCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loginWithWechatSdk(): Promise<WechatLoginResult | void> {
|
||||||
|
const config = await fetchClientConfig();
|
||||||
|
if (!isWxAuthorizeEnabled(config)) return;
|
||||||
|
if (!isWechatEnv()) {
|
||||||
|
throw new Error('请在微信内打开以使用微信一键授权');
|
||||||
|
}
|
||||||
|
return weixinSdk.login();
|
||||||
|
}
|
||||||
|
|
||||||
export function applyWechatLoginResult(result: WechatLoginResult): boolean {
|
export function applyWechatLoginResult(result: WechatLoginResult): boolean {
|
||||||
return saveWechatLoginResult(result);
|
return saveWechatLoginResult(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import { getRouterBasename } from '@dukang/weixin-sdk';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter basename={getRouterBasename()}>
|
||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import AppImage from '@dukang/shared-ui/AppImage';
|
|||||||
import type { WechatLoginResult } from '@dukang/shared-types';
|
import type { WechatLoginResult } from '@dukang/shared-types';
|
||||||
import { SmsScene } from '@dukang/shared-types';
|
import { SmsScene } from '@dukang/shared-types';
|
||||||
import { request, type SessionPayload } from '../lib/api';
|
import { request, type SessionPayload } from '../lib/api';
|
||||||
|
import { fetchClientConfig } from '../lib/pay-wechat';
|
||||||
|
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
|
||||||
import { normalizePhoneInput, validateMobilePhone } from '../lib/phone';
|
import { normalizePhoneInput, validateMobilePhone } from '../lib/phone';
|
||||||
import { useSmsCode } from '../lib/use-sms-code';
|
import { useSmsCode } from '../lib/use-sms-code';
|
||||||
import { isWechatEnv, weixinSdk } from '../lib/weixin';
|
import { isWechatEnv } from '../lib/weixin';
|
||||||
|
import { loginWithWechatSdk, handleWechatAuthCallback as handleWechatOAuthCallback } from '../lib/wechat-auth';
|
||||||
import { useUserSession } from '../contexts/UserSessionContext';
|
import { useUserSession } from '../contexts/UserSessionContext';
|
||||||
import { touchPromoIfNeeded } from '../lib/promo';
|
import { touchPromoIfNeeded } from '../lib/promo';
|
||||||
|
|
||||||
@@ -27,19 +30,25 @@ export default function LoginPage() {
|
|||||||
const [msg, setMsg] = useState('');
|
const [msg, setMsg] = useState('');
|
||||||
const [wxSessionKey, setWxSessionKey] = useState<string | null>(null);
|
const [wxSessionKey, setWxSessionKey] = useState<string | null>(null);
|
||||||
const [bindMode, setBindMode] = useState(false);
|
const [bindMode, setBindMode] = useState(false);
|
||||||
|
const [wxAuthorize, setWxAuthorize] = useState(false);
|
||||||
const { sendCode, sending, codeCooldown, sentHint, error: smsError, setError: setSmsError, clearMessages } =
|
const { sendCode, sending, codeCooldown, sentHint, error: smsError, setError: setSmsError, clearMessages } =
|
||||||
useSmsCode();
|
useSmsCode();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isWechatEnv()) return;
|
fetchClientConfig()
|
||||||
weixinSdk
|
.then((config) => setWxAuthorize(isWxAuthorizeEnabled(config)))
|
||||||
.handleOAuthCallback()
|
.catch(() => setWxAuthorize(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isWechatEnv() || !wxAuthorize) return;
|
||||||
|
handleWechatOAuthCallback()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
handleWechatLoginResult(result);
|
handleWechatLoginResult(result);
|
||||||
})
|
})
|
||||||
.catch((e) => setMsg(e instanceof Error ? e.message : '微信登录失败'));
|
.catch((e) => setMsg(e instanceof Error ? e.message : '微信登录失败'));
|
||||||
}, []);
|
}, [wxAuthorize]);
|
||||||
|
|
||||||
function handleWechatLoginResult(result: WechatLoginResult) {
|
function handleWechatLoginResult(result: WechatLoginResult) {
|
||||||
if (result.needBindPhone && result.wxSessionKey) {
|
if (result.needBindPhone && result.wxSessionKey) {
|
||||||
@@ -116,11 +125,7 @@ export default function LoginPage() {
|
|||||||
if (!ensureAgreed()) return;
|
if (!ensureAgreed()) return;
|
||||||
setMsg('');
|
setMsg('');
|
||||||
try {
|
try {
|
||||||
if (!isWechatEnv()) {
|
const result = await loginWithWechatSdk();
|
||||||
setMsg('请在微信内打开以使用微信一键授权');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = await weixinSdk.login();
|
|
||||||
if (result) handleWechatLoginResult(result);
|
if (result) handleWechatLoginResult(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setMsg(e instanceof Error ? e.message : '微信登录失败');
|
setMsg(e instanceof Error ? e.message : '微信登录失败');
|
||||||
@@ -199,7 +204,7 @@ export default function LoginPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!bindMode && (
|
{!bindMode && wxAuthorize && (
|
||||||
<>
|
<>
|
||||||
<div className="login-divider">
|
<div className="login-divider">
|
||||||
<span className="login-divider-line" />
|
<span className="login-divider-line" />
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ export default function MinePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nickname = profile?.nickname || '用户';
|
const nickname = profile?.nickname || '用户';
|
||||||
const userNo = profile?.userNo || '';
|
|
||||||
const avatar = profile?.avatarUrl || DEFAULT_AVATAR;
|
const avatar = profile?.avatarUrl || DEFAULT_AVATAR;
|
||||||
const hasWechat = !!profile?.hasWechat;
|
const hasWechat = !!profile?.hasWechat;
|
||||||
|
|
||||||
@@ -98,7 +97,6 @@ export default function MinePage() {
|
|||||||
<div className="mine-profile-info">
|
<div className="mine-profile-info">
|
||||||
<h1 className="mine-profile-name">{nickname}</h1>
|
<h1 className="mine-profile-name">{nickname}</h1>
|
||||||
<div className="mine-profile-meta">
|
<div className="mine-profile-meta">
|
||||||
{userNo && <span className="mine-profile-id">ID: {userNo}</span>}
|
|
||||||
<span className="mine-member-tag">{hasWechat ? '微信会员' : '好客会员'}</span>
|
<span className="mine-member-tag">{hasWechat ? '微信会员' : '好客会员'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export default function PayPage() {
|
|||||||
const [msg, setMsg] = useState('');
|
const [msg, setMsg] = useState('');
|
||||||
const [showBindPhone, setShowBindPhone] = useState(false);
|
const [showBindPhone, setShowBindPhone] = useState(false);
|
||||||
const [wxSessionKey, setWxSessionKey] = useState<string | null>(null);
|
const [wxSessionKey, setWxSessionKey] = useState<string | null>(null);
|
||||||
|
const [orderNo, setOrderNo] = useState('');
|
||||||
|
|
||||||
const refreshPayReadiness = useCallback(async () => {
|
const refreshPayReadiness = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -72,6 +73,16 @@ export default function PayPage() {
|
|||||||
refreshPayReadiness();
|
refreshPayReadiness();
|
||||||
}, [refreshPayReadiness]);
|
}, [refreshPayReadiness]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!orderId) {
|
||||||
|
setOrderNo('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
request<{ orderNo?: string }>('USER_H5', `/trade/orders/${orderId}`)
|
||||||
|
.then((order) => setOrderNo(order.orderNo || ''))
|
||||||
|
.catch(() => setOrderNo(''));
|
||||||
|
}, [orderId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isWechatEnv()) return;
|
if (!isWechatEnv()) return;
|
||||||
handleWechatAuthCallback()
|
handleWechatAuthCallback()
|
||||||
@@ -181,7 +192,7 @@ export default function PayPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{msg && <p className="pay-wechat-auth-msg">{msg}</p>}
|
{msg && <p className="pay-wechat-auth-msg">{msg}</p>}
|
||||||
<p className="label-md text-muted" style={{ marginTop: 24 }}>订单号 {orderId}</p>
|
{orderNo && <p className="label-md text-muted" style={{ marginTop: 24 }}>订单号 {orderNo}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="page-actions">
|
<div className="page-actions">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: '/user/',
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# 为杜康 runxian.top 五域名申请 Let's Encrypt 并切换 HTTPS 配置
|
# 为杜康 runxian.top 申请 Let's Encrypt 并切换 HTTPS 配置
|
||||||
|
# 三端 H5 统一入口 m.runxian.top(微信网页授权单域名)
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
DOMAINS=(
|
DOMAINS=(
|
||||||
|
m.runxian.top
|
||||||
user.runxian.top
|
user.runxian.top
|
||||||
shop.runxian.top
|
shop.runxian.top
|
||||||
partner.runxian.top
|
partner.runxian.top
|
||||||
@@ -25,7 +27,8 @@ mkdir -p /var/www/certbot /var/log/nginx/dukang
|
|||||||
|
|
||||||
echo "==> 申请证书..."
|
echo "==> 申请证书..."
|
||||||
certbot certonly --webroot -w /var/www/certbot \
|
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 user.runxian.top \
|
||||||
-d shop.runxian.top \
|
-d shop.runxian.top \
|
||||||
-d partner.runxian.top \
|
-d partner.runxian.top \
|
||||||
@@ -43,13 +46,14 @@ systemctl reload nginx
|
|||||||
|
|
||||||
echo "==> 验证..."
|
echo "==> 验证..."
|
||||||
for url in \
|
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/ \
|
||||||
https://shop.runxian.top/ \
|
|
||||||
https://partner.runxian.top/ \
|
|
||||||
https://webadmin.runxian.top/ \
|
https://webadmin.runxian.top/ \
|
||||||
https://dkapi.runxian.top/api/v1/health; do
|
https://dkapi.runxian.top/api/v1/health; do
|
||||||
code="$(curl -sf -o /dev/null -w '%{http_code}' "$url" || echo fail)"
|
code="$(curl -sf -o /dev/null -w '%{http_code}' "$url" || echo fail)"
|
||||||
echo " $url -> $code"
|
echo " $url -> $code"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "==> runxian.top 杜康域名 HTTPS 已启用"
|
echo "==> runxian.top 杜康域名 HTTPS 已启用(H5 统一入口 m.runxian.top)"
|
||||||
|
|||||||
@@ -1,28 +1,16 @@
|
|||||||
# 杜康好客 — runxian.top HTTPS(证书申请后启用)
|
# 杜康好客 — runxian.top HTTPS
|
||||||
# 执行: deploy/enable-runxian-dukang-ssl.sh
|
# 执行: deploy/enable-runxian-dukang-ssl.sh
|
||||||
|
# 三端 H5 统一入口 m.runxian.top/{user,shop,partner}/
|
||||||
map $host $dukang_runxian_port {
|
|
||||||
user.runxian.top 8091;
|
|
||||||
shop.runxian.top 8092;
|
|
||||||
partner.runxian.top 8093;
|
|
||||||
webadmin.runxian.top 8094;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
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/ {
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
root /var/www/certbot;
|
root /var/www/certbot;
|
||||||
default_type "text/plain";
|
default_type "text/plain";
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ ^/MP_verify_.*\.txt$ {
|
|
||||||
root /opt/dukang-haoke/apps/h5-user/dist;
|
|
||||||
default_type text/plain;
|
|
||||||
access_log off;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
return 301 https://$host$request_uri;
|
return 301 https://$host$request_uri;
|
||||||
}
|
}
|
||||||
@@ -30,18 +18,24 @@ server {
|
|||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
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;
|
access_log /var/log/nginx/dukang/runxian-m.access.log main;
|
||||||
error_log /var/log/nginx/dukang/runxian-h5.error.log warn;
|
error_log /var/log/nginx/dukang/runxian-m.error.log warn;
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/user.runxian.top/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/m.runxian.top/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/user.runxian.top/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/m.runxian.top/privkey.pem;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
client_max_body_size 20m;
|
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/ {
|
location /api/ {
|
||||||
proxy_pass http://127.0.0.1:8090;
|
proxy_pass http://127.0.0.1:8090;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
@@ -52,14 +46,57 @@ server {
|
|||||||
proxy_set_header Connection "";
|
proxy_set_header Connection "";
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location = / {
|
||||||
proxy_pass http://127.0.0.1:$dukang_runxian_port;
|
return 302 /user/;
|
||||||
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 /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 {
|
server {
|
||||||
@@ -83,8 +120,8 @@ server {
|
|||||||
access_log /var/log/nginx/dukang/runxian-api.access.log main;
|
access_log /var/log/nginx/dukang/runxian-api.access.log main;
|
||||||
error_log /var/log/nginx/dukang/runxian-api.error.log warn;
|
error_log /var/log/nginx/dukang/runxian-api.error.log warn;
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/user.runxian.top/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/m.runxian.top/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/user.runxian.top/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/m.runxian.top/privkey.pem;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
@@ -122,8 +159,8 @@ server {
|
|||||||
access_log /var/log/nginx/dukang/runxian-admin.access.log main;
|
access_log /var/log/nginx/dukang/runxian-admin.access.log main;
|
||||||
error_log /var/log/nginx/dukang/runxian-admin.error.log warn;
|
error_log /var/log/nginx/dukang/runxian-admin.error.log warn;
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/user.runxian.top/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/m.runxian.top/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/user.runxian.top/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/m.runxian.top/privkey.pem;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
# 杜康好客 — runxian.top 别名域名(与 lingshivip.cn 同后端)
|
# 杜康好客 — runxian.top(HTTP)
|
||||||
# user/shop/partner → 8091/8092/8093;webadmin → 8094;dkapi → 8090
|
# 三端 H5 统一入口 m.runxian.top/{user,shop,partner}/(微信网页授权单域名)
|
||||||
# 证书:certbot --cert-name user.runxian.top -d user.runxian.top -d shop.runxian.top ...
|
# 旧子域名 user/shop/partner 301 到统一入口;webadmin → 8094;dkapi → 8090
|
||||||
|
|
||||||
map $host $dukang_runxian_port {
|
# --- 统一 H5 入口(证书就绪前 HTTP;HTTPS 见 dukang-runxian-ssl.conf)---
|
||||||
user.runxian.top 8091;
|
|
||||||
shop.runxian.top 8092;
|
|
||||||
partner.runxian.top 8093;
|
|
||||||
webadmin.runxian.top 8094;
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- HTTP: H5 三端(校验文件 + API;证书就绪后见 dukang-runxian-ssl.conf)---
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
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;
|
access_log /var/log/nginx/dukang/runxian-m.access.log main;
|
||||||
error_log /var/log/nginx/dukang/runxian-h5.error.log warn;
|
error_log /var/log/nginx/dukang/runxian-m.error.log warn;
|
||||||
|
|
||||||
|
client_max_body_size 20m;
|
||||||
|
|
||||||
location ^~ /.well-known/acme-challenge/ {
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
root /var/www/certbot;
|
root /var/www/certbot;
|
||||||
@@ -38,14 +33,43 @@ server {
|
|||||||
proxy_set_header Connection "";
|
proxy_set_header Connection "";
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location = / {
|
||||||
proxy_pass http://127.0.0.1:$dukang_runxian_port;
|
return 302 /user/;
|
||||||
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 /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 ---
|
# --- HTTP: API ---
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export interface AppConfig {
|
|||||||
autoApproveStore: boolean;
|
autoApproveStore: boolean;
|
||||||
/** preV1 Mock 微信授权登录:点击授权按钮走 Mock 流程直接登录,不接真实微信 */
|
/** preV1 Mock 微信授权登录:点击授权按钮走 Mock 流程直接登录,不接真实微信 */
|
||||||
mockWechat: boolean;
|
mockWechat: boolean;
|
||||||
|
/** 登录后是否走微信 SDK OAuth 授权(WX_AUTHORIZE=false 时三端跳过授权流程) */
|
||||||
|
wxAuthorize: boolean;
|
||||||
wechatAuthEnabled: boolean;
|
wechatAuthEnabled: boolean;
|
||||||
wechatPayEnabled: boolean;
|
wechatPayEnabled: boolean;
|
||||||
wxAppId: string;
|
wxAppId: string;
|
||||||
@@ -35,6 +37,7 @@ export function loadAppConfig(env?: Record<string, string | undefined>): AppConf
|
|||||||
mockDeliveryAuto: e.MOCK_DELIVERY_AUTO !== 'false',
|
mockDeliveryAuto: e.MOCK_DELIVERY_AUTO !== 'false',
|
||||||
autoApproveStore: e.AUTO_APPROVE_STORE !== 'false',
|
autoApproveStore: e.AUTO_APPROVE_STORE !== 'false',
|
||||||
mockWechat: e.MOCK_WECHAT === 'true',
|
mockWechat: e.MOCK_WECHAT === 'true',
|
||||||
|
wxAuthorize: e.WX_AUTHORIZE !== 'false',
|
||||||
wechatAuthEnabled: e.WECHAT_AUTH_ENABLED === 'true',
|
wechatAuthEnabled: e.WECHAT_AUTH_ENABLED === 'true',
|
||||||
wechatPayEnabled: e.WECHAT_PAY_ENABLED === 'true' || e.MOCK_PAY === 'false',
|
wechatPayEnabled: e.WECHAT_PAY_ENABLED === 'true' || e.MOCK_PAY === 'false',
|
||||||
wxAppId: e.WX_APP_ID ?? '',
|
wxAppId: e.WX_APP_ID ?? '',
|
||||||
|
|||||||
@@ -28,8 +28,16 @@ export type ClientRuntimeConfig = {
|
|||||||
mockPay: boolean;
|
mockPay: boolean;
|
||||||
wechatPayEnabled: boolean;
|
wechatPayEnabled: boolean;
|
||||||
mockSms: boolean;
|
mockSms: boolean;
|
||||||
|
mockWechat?: boolean;
|
||||||
|
/** false 时三端跳过微信 SDK OAuth 授权 */
|
||||||
|
wxAuthorize?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 是否启用登录后微信 SDK 授权(默认 true,仅 WX_AUTHORIZE=false 时关闭) */
|
||||||
|
export function isWxAuthorizeEnabled(config?: Pick<ClientRuntimeConfig, 'wxAuthorize'> | null): boolean {
|
||||||
|
return config?.wxAuthorize !== false;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WechatLoginResult {
|
export interface WechatLoginResult {
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
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}/`);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export { isWechatBrowser, isMiniProgram, getRuntimePlatform } from './env';
|
export { isWechatBrowser, isMiniProgram, getRuntimePlatform } from './env';
|
||||||
|
export { getRouterBasename, toAppPath, isOnAppPath } from './app-path';
|
||||||
export { initWechatJssdk, ensureJssdkReady, isJssdkReady, normalizeJssdkPageUrl, stripOAuthParamsFromLocation } from './jssdk';
|
export { initWechatJssdk, ensureJssdkReady, isJssdkReady, normalizeJssdkPageUrl, stripOAuthParamsFromLocation } from './jssdk';
|
||||||
export {
|
export {
|
||||||
getWechatLocation,
|
getWechatLocation,
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ const defaultOrigins = [
|
|||||||
'http://127.0.0.1:5173',
|
'http://127.0.0.1:5173',
|
||||||
'http://127.0.0.1:5174',
|
'http://127.0.0.1:5174',
|
||||||
'http://127.0.0.1:5175',
|
'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(','))
|
const allowedOrigin = (OSS_CORS_ORIGINS ?? defaultOrigins.join(','))
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
# 杜康 API 环境变量模板
|
# 杜康 API 环境变量模板
|
||||||
# 本地推荐分层:
|
# 本地开发:复制为 .env 后按需修改(pnpm dev:api 仅读取 .env / .env.local)
|
||||||
# .env.development — 集成开关与密钥(MOCK_SMS、阿里云、微信)
|
# 可选 .env.local — 本机覆盖(gitignore)
|
||||||
# .env — 仅本机项(PORT、DATABASE_URL、REDIS_URL)
|
# 服务器部署:bash deploy/sync-api-env.sh development|production(同步到远端 .env)
|
||||||
# .env.local — 可选本机覆盖(gitignore)
|
# .env.development / .env.production — 部署源模板,本地 dev 不自动加载
|
||||||
# 生产/预发:bash deploy/sync-api-env.sh development|production
|
|
||||||
|
|
||||||
DATABASE_URL="mysql://root:root@localhost:3306/dukang_haoke"
|
DATABASE_URL="mysql://root:root@localhost:3306/dukang_haoke"
|
||||||
REDIS_URL="redis://localhost:6379"
|
REDIS_URL="redis://localhost:6379"
|
||||||
@@ -23,15 +22,19 @@ AUTO_APPROVE_STORE=true
|
|||||||
# 微信 OAuth Mock(preV1 本地联调):须在微信内置浏览器内打开 H5,走 OAuth 回跳带 mock code;
|
# 微信 OAuth Mock(preV1 本地联调):须在微信内置浏览器内打开 H5,走 OAuth 回跳带 mock code;
|
||||||
# 非微信浏览器不会发起 /login/wechat 请求。生产请 MOCK_WECHAT=false 且 WECHAT_AUTH_ENABLED=true。
|
# 非微信浏览器不会发起 /login/wechat 请求。生产请 MOCK_WECHAT=false 且 WECHAT_AUTH_ENABLED=true。
|
||||||
MOCK_WECHAT=true
|
MOCK_WECHAT=true
|
||||||
|
# 登录后是否走微信 SDK OAuth 授权(本地 false 可仅用短信登录/核销,不影响支付 Mock)
|
||||||
|
WX_AUTHORIZE=false
|
||||||
|
|
||||||
# C 端 H5 落地页(推广码二维码链接前缀)
|
# 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 定位)
|
# 反向代理后提取真实客户端 IP(下单 IP 定位)
|
||||||
TRUST_PROXY=true
|
TRUST_PROXY=true
|
||||||
|
|
||||||
# 微信 SDK(生产:WECHAT_AUTH_ENABLED=true,配置 WX_APP_ID / WX_APP_SECRET)
|
# 微信 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_ID=
|
||||||
WX_APP_SECRET=
|
WX_APP_SECRET=
|
||||||
WECHAT_AUTH_ENABLED=false
|
WECHAT_AUTH_ENABLED=false
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 生产环境(真实第三方,关闭 Mock)
|
# 生产环境(真实第三方,关闭 Mock)
|
||||||
# 用法:cp .env.production.example .env.production 后填写密钥
|
# 用途:deploy/sync-api-env.sh production 的源文件,同步到服务器 .env
|
||||||
# 服务器:bash deploy/sync-api-env.sh production
|
# 本地开发请使用 .env(从 .env.example 复制)
|
||||||
|
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
|
||||||
@@ -22,8 +22,13 @@ AUTO_APPROVE_STORE=false
|
|||||||
|
|
||||||
TRUST_PROXY=true
|
TRUST_PROXY=true
|
||||||
|
|
||||||
|
# C 端 H5 落地页(推广码二维码;生产统一入口)
|
||||||
|
USER_H5_URL=https://m.runxian.top/user
|
||||||
|
|
||||||
WECHAT_AUTH_ENABLED=true
|
WECHAT_AUTH_ENABLED=true
|
||||||
WECHAT_PAY_ENABLED=true
|
WECHAT_PAY_ENABLED=true
|
||||||
|
# 登录后走微信 SDK OAuth 授权(生产/预发建议 true)
|
||||||
|
WX_AUTHORIZE=true
|
||||||
WX_APP_ID=
|
WX_APP_ID=
|
||||||
WX_APP_SECRET=
|
WX_APP_SECRET=
|
||||||
WX_MCH_ID=
|
WX_MCH_ID=
|
||||||
|
|||||||
@@ -3,20 +3,25 @@ import { existsSync } from 'fs';
|
|||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分层加载环境变量(后加载的文件覆盖先前的同名键):
|
* 环境变量加载策略(后加载的文件覆盖先前的同名键):
|
||||||
* 1. .env.{NODE_ENV} — 集成配置(MOCK_SMS、微信、阿里云等)
|
*
|
||||||
* 2. .env.{NODE_ENV}.local / .env.local — 本机覆盖
|
* - 本地开发(NODE_ENV !== production):仅 `.env` + `.env.local`
|
||||||
* 3. .env — 本机基础(PORT、DATABASE_URL);应只放机器相关项,勿重复 MOCK_SMS
|
* `.env.development` 仅作 deploy/sync-api-env.sh 的源模板,不在本地 dev 自动加载
|
||||||
|
*
|
||||||
|
* - 线上(NODE_ENV=production):`.env.production` → `.env.production.local` → `.env`
|
||||||
|
* 服务器实际运行时以 sync 脚本写入的 `.env` 为准
|
||||||
*/
|
*/
|
||||||
const apiRoot = resolve(__dirname, '..');
|
const apiRoot = resolve(__dirname, '..');
|
||||||
const nodeEnv = process.env.NODE_ENV ?? 'development';
|
const nodeEnv = process.env.NODE_ENV ?? 'development';
|
||||||
|
const isProduction = nodeEnv === 'production';
|
||||||
|
|
||||||
const layers = [
|
const layers = isProduction
|
||||||
resolve(apiRoot, `.env.${nodeEnv}`),
|
? [
|
||||||
resolve(apiRoot, `.env.${nodeEnv}.local`),
|
resolve(apiRoot, '.env.production'),
|
||||||
resolve(apiRoot, '.env.local'),
|
resolve(apiRoot, '.env.production.local'),
|
||||||
resolve(apiRoot, '.env'),
|
resolve(apiRoot, '.env'),
|
||||||
];
|
]
|
||||||
|
: [resolve(apiRoot, '.env'), resolve(apiRoot, '.env.local')];
|
||||||
|
|
||||||
for (const file of layers) {
|
for (const file of layers) {
|
||||||
if (existsSync(file)) {
|
if (existsSync(file)) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export class ClientConfigController {
|
|||||||
wechatPayEnabled: cfg.wechatPayEnabled,
|
wechatPayEnabled: cfg.wechatPayEnabled,
|
||||||
mockSms: cfg.mockSms,
|
mockSms: cfg.mockSms,
|
||||||
mockWechat: cfg.mockWechat,
|
mockWechat: cfg.mockWechat,
|
||||||
|
wxAuthorize: cfg.wxAuthorize,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { CreatePromoCodeDto } from './dto/admin-mutate.dto';
|
|||||||
import { AdminPromoCodesQueryDto } from './dto/admin-query.dto';
|
import { AdminPromoCodesQueryDto } from './dto/admin-query.dto';
|
||||||
|
|
||||||
function userH5Base(): string {
|
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 {
|
function buildLandingUrl(code: string): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user