本地环境不验证手机号和微信登录授权

This commit is contained in:
2026-07-07 21:42:56 +08:00
parent ec748335bf
commit e32ccebc80
14 changed files with 132 additions and 24 deletions
@@ -2,11 +2,13 @@ import { useEffect, useId, useRef, useState } from 'react';
import { uploadFileToOss, type OssMediaType } from '../lib/upload';
import {
authorizePartnerWechat,
fetchClientConfig,
fetchPartnerProfile,
needsWechatAuth,
type PartnerProfile,
} from '../lib/wechat-auth';
import { isWechatEnv, weixinSdk } from '../lib/weixin';
import type { ClientRuntimeConfig } from '@dukang/shared-types';
type OssUploadFieldProps = {
value?: string;
@@ -42,17 +44,19 @@ export default function OssUploadField({
const [authorizing, setAuthorizing] = useState(false);
const [error, setError] = useState('');
const [profile, setProfile] = useState<PartnerProfile | null>(null);
const [clientConfig, setClientConfig] = useState<ClientRuntimeConfig | null>(null);
const resolvedAccept =
accept ?? (mediaType === 'VIDEO' ? 'video/*' : mediaType === 'FILE' ? 'image/*,.pdf' : 'image/*');
const useWechatPicker = isWechatEnv() && mediaType !== 'VIDEO';
const needsAuth = useWechatPicker && needsWechatAuth(profile) && wechatReady !== true;
const needsAuth = useWechatPicker && needsWechatAuth(profile, clientConfig) && wechatReady !== true;
useEffect(() => {
if (!useWechatPicker) return;
void fetchPartnerProfile()
.then((me) => {
void Promise.all([fetchPartnerProfile(), fetchClientConfig()])
.then(([me, config]) => {
setProfile(me);
setClientConfig(config);
onWechatReadyChange?.(!!me.hasWechat);
})
.catch(() => {
+24 -2
View File
@@ -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 { isWechatEnv, weixinSdk } from './weixin';
import { request, saveAuth } from './api';
@@ -11,15 +12,28 @@ export type PartnerProfile = {
hasWechat?: boolean;
};
export async function fetchClientConfig(): Promise<ClientRuntimeConfig> {
return request<ClientRuntimeConfig>('PARTNER_H5', '/common/client-config');
}
export async function fetchPartnerProfile(): Promise<PartnerProfile> {
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;
}
export async function checkNeedsWechatAuth(profile: PartnerProfile | null): Promise<boolean> {
const config = await fetchClientConfig();
return needsWechatAuth(profile, config);
}
/** 处理微信登录/绑定结果,返回是否已拿到 token 可进入首页 */
export function handlePartnerWechatLoginResult(result: WechatLoginResult): boolean {
if (!result.accessToken) return false;
@@ -29,6 +43,8 @@ export function handlePartnerWechatLoginResult(result: WechatLoginResult): boole
export async function handlePartnerWechatCallback(): Promise<WechatLoginResult | null> {
if (!isWechatEnv()) return null;
const config = await fetchClientConfig();
if (!isWxAuthorizeEnabled(config)) return null;
return weixinSdk.handleOAuthCallback();
}
@@ -37,6 +53,8 @@ export async function handlePartnerWechatCallback(): Promise<WechatLoginResult |
* 返回 true = 已登录;void = 已跳转授权页等待回调。
*/
export async function loginPartnerWithWechat(): Promise<boolean | void> {
const config = await fetchClientConfig();
if (!isWxAuthorizeEnabled(config)) return false;
if (!isWechatEnv()) {
throw new Error(WECHAT_INAPP_REQUIRED_MSG);
}
@@ -48,11 +66,15 @@ export async function loginPartnerWithWechat(): Promise<boolean | void> {
* 短信登录成功后于微信内自动发起 OAuth,将 openId 绑定到当前合伙人账号(便于同一微信后续免登)。
*/
export async function bindPartnerWechatAfterSmsLogin(): Promise<void> {
const config = await fetchClientConfig();
if (!isWxAuthorizeEnabled(config)) return;
if (!isWechatEnv()) return;
await weixinSdk.login();
}
export async function authorizePartnerWechat(): Promise<WechatLoginResult | void> {
const config = await fetchClientConfig();
if (!isWxAuthorizeEnabled(config)) return;
if (!isWechatEnv()) {
throw new Error(WECHAT_INAPP_REQUIRED_MSG);
}
+16 -3
View File
@@ -3,11 +3,13 @@ import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { request, saveAuth } from '../lib/api';
import {
bindPartnerWechatAfterSmsLogin,
fetchClientConfig,
handlePartnerWechatCallback,
handlePartnerWechatLoginResult,
loginPartnerWithWechat,
} from '../lib/wechat-auth';
import { isWechatEnv } from '../lib/weixin';
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk';
const REMEMBER_PHONE_KEY = 'partner_remember_phone';
@@ -47,16 +49,23 @@ export default function LoginPage() {
const [wxLoading, setWxLoading] = useState(false);
const [msg, setMsg] = useState('');
const [codeCooldown, setCodeCooldown] = useState(0);
const [wxAuthorize, setWxAuthorize] = useState(false);
useEffect(() => {
if (!isWechatEnv()) return;
fetchClientConfig()
.then((config) => setWxAuthorize(isWxAuthorizeEnabled(config)))
.catch(() => setWxAuthorize(false));
}, []);
useEffect(() => {
if (!isWechatEnv() || !wxAuthorize) return;
void handlePartnerWechatCallback()
.then((result) => {
if (!result) return;
if (handlePartnerWechatLoginResult(result)) navigate('/');
})
.catch((e) => setMsg(formatWechatError(e)));
}, [navigate]);
}, [navigate, wxAuthorize]);
function formatWechatError(e: unknown): string {
const text = e instanceof Error ? e.message : '微信登录失败';
@@ -129,7 +138,7 @@ export default function LoginPage() {
});
saveAuth(data);
persistRememberAccount(phone);
if (isWechatEnv()) {
if (isWechatEnv() && wxAuthorize) {
setMsg('登录成功,正在关联微信…');
await bindPartnerWechatAfterSmsLogin();
return;
@@ -264,6 +273,8 @@ export default function LoginPage() {
{!loading && <span className="material-symbols-outlined">arrow_forward</span>}
</button>
{wxAuthorize && (
<>
<div className="partner-auth-divider"><span></span></div>
<button type="button" className="partner-btn-wechat" onClick={wechatLogin} disabled={wxLoading}>
<svg viewBox="0 0 24 24" fill="#07C160" aria-hidden>
@@ -271,6 +282,8 @@ export default function LoginPage() {
</svg>
<span>{wxLoading ? '登录中...' : '微信一键授权'}</span>
</button>
</>
)}
<label className="partner-checkbox-row">
<input type="checkbox" checked={agreed} onChange={(e) => setAgreed(e.target.checked)} />
+20 -2
View File
@@ -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 { request, saveAuth, type ShopSessionPayload } from './api';
@@ -11,14 +12,27 @@ export type ShopAccountProfile = {
store?: { name: string };
};
export async function fetchClientConfig(): Promise<ClientRuntimeConfig> {
return request<ClientRuntimeConfig>('SHOP_H5', '/common/client-config');
}
export async function fetchShopAccount(): Promise<ShopAccountProfile> {
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;
}
export async function checkNeedsWechatAuth(profile: ShopAccountProfile | null): Promise<boolean> {
const config = await fetchClientConfig();
return needsWechatAuth(profile, config);
}
export function sessionFromWechatLogin(result: WechatLoginResult): ShopSessionPayload | null {
if (!result.accessToken || !result.refreshToken) return null;
const store = result.store;
@@ -45,6 +59,8 @@ export function saveShopWechatAuth(result: WechatLoginResult): boolean {
}
export async function authorizeShopWechat(): Promise<WechatLoginResult | void> {
const config = await fetchClientConfig();
if (!isWxAuthorizeEnabled(config)) return;
if (!isWechatEnv()) {
throw new Error('请在微信内打开以完成授权');
}
@@ -53,5 +69,7 @@ export async function authorizeShopWechat(): Promise<WechatLoginResult | void> {
export async function handleShopWechatCallback(): Promise<WechatLoginResult | null> {
if (!isWechatEnv()) return null;
const config = await fetchClientConfig();
if (!isWxAuthorizeEnabled(config)) return null;
return weixinSdk.handleOAuthCallback();
}
+2 -2
View File
@@ -5,9 +5,9 @@ import { request } from '../lib/api';
import { parseRedeemTokenFromScan } from '../lib/redeem-scan';
import {
authorizeShopWechat,
checkNeedsWechatAuth,
fetchShopAccount,
handleShopWechatCallback,
needsWechatAuth,
saveShopWechatAuth,
sessionFromWechatLogin,
} from '../lib/wechat-auth';
@@ -105,7 +105,7 @@ export default function HomePage() {
}
try {
const profile = await fetchShopAccount();
if (needsWechatAuth(profile)) {
if (await checkNeedsWechatAuth(profile)) {
setAuthModalOpen(true);
return;
}
+15 -1
View File
@@ -1,8 +1,10 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import AppImage from '@dukang/shared-ui/AppImage';
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
import { useStoreSession } from '../contexts/StoreSessionContext';
import { getLastPhone, getStoreProfile, request, type ShopSessionPayload } from '../lib/api';
import { fetchClientConfig } from '../lib/wechat-auth';
function maskPhone(phone: string) {
if (phone.length < 7) return phone;
@@ -21,6 +23,13 @@ export default function LoginPage() {
const [loading, setLoading] = useState(false);
const [msg, setMsg] = useState('');
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 quickPhone = savedProfile?.phone || phone;
@@ -83,6 +92,7 @@ export default function LoginPage() {
function wechatLogin() {
if (!ensureAgreed()) return;
if (!wxAuthorize) return;
setMsg('preV1:微信一键授权暂未开放,请使用手机验证码登录');
}
@@ -206,6 +216,8 @@ export default function LoginPage() {
{!loading && <span className="material-symbols-outlined">arrow_forward</span>}
</button>
{wxAuthorize && (
<>
<div className="shop-login-divider">
<span className="shop-login-divider-line" />
<span className="shop-login-divider-text"></span>
@@ -216,6 +228,8 @@ export default function LoginPage() {
<span className="material-symbols-outlined">chat</span>
<span></span>
</button>
</>
)}
</div>
<label className="shop-login-agreement">
+4
View File
@@ -1,4 +1,5 @@
import type { ClientRuntimeConfig, WechatLoginResult } from '@dukang/shared-types';
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
import { isWechatEnv, weixinSdk } from './weixin';
import { request, saveSession, type UserProfile } from './api';
@@ -21,6 +22,7 @@ export function needsWechatAuthForPay(
config: ClientRuntimeConfig,
profile: UserProfile | null,
): boolean {
if (!isWxAuthorizeEnabled(config)) return false;
return !config.mockPay && config.wechatPayEnabled && isWechatEnv() && !profile?.hasWechat;
}
@@ -37,6 +39,8 @@ export function saveWechatLoginResult(result: WechatLoginResult): boolean {
}
export async function authorizeWechatForPay(): Promise<WechatLoginResult | void> {
const config = await fetchClientConfig();
if (!isWxAuthorizeEnabled(config)) return;
if (!isWechatEnv()) {
throw new Error('请在微信内打开以完成授权');
}
+12
View File
@@ -1,4 +1,5 @@
import type { WechatLoginResult } from '@dukang/shared-types';
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
import { isWechatEnv, weixinSdk } from './weixin';
import {
authorizeWechatForPay,
@@ -38,9 +39,20 @@ export async function ensureWechatAuthForPay(): Promise<WechatAuthEnsureResult>
export async function handleWechatAuthCallback(): Promise<WechatLoginResult | null> {
if (!isWechatEnv()) return null;
const config = await fetchClientConfig();
if (!isWxAuthorizeEnabled(config)) return null;
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 {
return saveWechatLoginResult(result);
}
+16 -11
View File
@@ -4,9 +4,12 @@ import AppImage from '@dukang/shared-ui/AppImage';
import type { WechatLoginResult } from '@dukang/shared-types';
import { SmsScene } from '@dukang/shared-types';
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 { 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 { touchPromoIfNeeded } from '../lib/promo';
@@ -27,19 +30,25 @@ export default function LoginPage() {
const [msg, setMsg] = useState('');
const [wxSessionKey, setWxSessionKey] = useState<string | null>(null);
const [bindMode, setBindMode] = useState(false);
const [wxAuthorize, setWxAuthorize] = useState(false);
const { sendCode, sending, codeCooldown, sentHint, error: smsError, setError: setSmsError, clearMessages } =
useSmsCode();
useEffect(() => {
if (!isWechatEnv()) return;
weixinSdk
.handleOAuthCallback()
fetchClientConfig()
.then((config) => setWxAuthorize(isWxAuthorizeEnabled(config)))
.catch(() => setWxAuthorize(false));
}, []);
useEffect(() => {
if (!isWechatEnv() || !wxAuthorize) return;
handleWechatOAuthCallback()
.then((result) => {
if (!result) return;
handleWechatLoginResult(result);
})
.catch((e) => setMsg(e instanceof Error ? e.message : '微信登录失败'));
}, []);
}, [wxAuthorize]);
function handleWechatLoginResult(result: WechatLoginResult) {
if (result.needBindPhone && result.wxSessionKey) {
@@ -116,11 +125,7 @@ export default function LoginPage() {
if (!ensureAgreed()) return;
setMsg('');
try {
if (!isWechatEnv()) {
setMsg('请在微信内打开以使用微信一键授权');
return;
}
const result = await weixinSdk.login();
const result = await loginWithWechatSdk();
if (result) handleWechatLoginResult(result);
} catch (e) {
setMsg(e instanceof Error ? e.message : '微信登录失败');
@@ -199,7 +204,7 @@ export default function LoginPage() {
</button>
</div>
{!bindMode && (
{!bindMode && wxAuthorize && (
<>
<div className="login-divider">
<span className="login-divider-line" />
+3
View File
@@ -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<string, string | undefined>): 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 ?? '',
+8
View File
@@ -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<ClientRuntimeConfig, 'wxAuthorize'> | null): boolean {
return config?.wxAuthorize !== false;
}
export interface WechatLoginResult {
accessToken?: string;
refreshToken?: string;
+2
View File
@@ -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
@@ -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=
@@ -11,6 +11,7 @@ export class ClientConfigController {
wechatPayEnabled: cfg.wechatPayEnabled,
mockSms: cfg.mockSms,
mockWechat: cfg.mockWechat,
wxAuthorize: cfg.wxAuthorize,
};
}
}