h5微信授权
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-07-14 20:09:36 +08:00
parent 9b13d02dde
commit 1ad49f1acf
14 changed files with 569 additions and 79 deletions
+12 -5
View File
@@ -11,7 +11,10 @@ import PageShell from '../../components/PageShell';
import WechatLoginButton from '../../components/WechatLoginButton';
import { BRAND_LOGO_WIDE_URL } from '@dukang/shared-types';
import { finishLoginNavigate } from '../../lib/auth-nav';
import { bindWechatForUser } from '../../lib/wechat-auth';
import {
bindWechatForUser,
loginWithWechat,
} from '../../lib/wechat-auth';
import { fetchUserProfile } from '../../lib/pay-wechat';
import { saveUserPhone, resolveDefaultUserPhone } from '../../lib/user-phone';
import {
@@ -21,7 +24,6 @@ import {
type MiniWechatProfile,
} from '../../lib/mini-wechat-profile';
import { isLoggedIn, request, saveAuth, toast, type SessionPayload } from '../../lib/api';
import { loginWithWechat } from '../../lib/wechat-auth';
function normalizePhone(value: string) {
return value.replace(/\D/g, '').slice(0, 11);
@@ -224,7 +226,10 @@ export default function LoginPage() {
if (completeMode === 'wechat' && isLoggedIn()) {
const result = await bindWechatForUser(wxInfo);
if (!result.ok && result.needBindPhone) {
if (!result.ok && 'redirecting' in result && result.redirecting) {
return;
}
if (!result.ok && 'needBindPhone' in result && result.needBindPhone) {
setBindMode(true);
setWxSessionKey(result.wxSessionKey);
setCompleteMode('phone');
@@ -240,11 +245,13 @@ export default function LoginPage() {
return;
}
const result = await loginWithWechat();
handleWechatLoginResult(result, wxInfo);
if (result) handleWechatLoginResult(result, wxInfo);
} catch (e) {
const raw = e instanceof Error ? e.message : '微信登录失败';
const hint = /invalid code/i.test(raw)
? '微信 code 无效:请确认后端 WX_MINI_APP_ID 与小程序 appid 一致,或本地联调使用 MOCK_WECHAT'
? process.env.TARO_ENV === 'weapp'
? '微信 code 无效:请确认后端 WX_MINI_APP_ID 与小程序 appid 一致,或本地联调使用 MOCK_WECHAT'
: '微信授权失败:请确认公众号 WX_APP_ID / 网页授权域名配置正确'
: raw;
setMsg(hint);
} finally {
+32 -6
View File
@@ -11,10 +11,10 @@ import { bindWechatForUser } from '../../lib/wechat-auth';
import { fetchUserProfile } from '../../lib/pay-wechat';
import {
fetchMiniWechatUserInfo,
getCachedWxProfile,
mergeWxDisplayProfile,
} from '../../lib/mini-wechat-profile';
import { isLoggedIn, logout, request, toast, type UserProfile } from '../../lib/api';
import { isWechatEnv } from '../../lib/weixin';
const ORDER_SHORTCUTS = [
{ tab: 'pending_pay', icon: '付', label: '待付款' },
@@ -111,13 +111,39 @@ export default function MinePage() {
toast('当前环境未开启微信授权');
return;
}
if (process.env.TARO_ENV !== 'weapp') {
toast('请在微信小程序中完成微信授权');
return;
}
setBindingWx(true);
try {
if (process.env.TARO_ENV === 'h5') {
if (!isWechatEnv()) {
toast('请在微信内打开后授权');
return;
}
const result = await bindWechatForUser();
if (!result.ok && 'redirecting' in result && result.redirecting) {
return;
}
if (!result.ok && 'needBindPhone' in result && result.needBindPhone) {
goLogin('/pages/mine/index', {
bindMode: '1',
wxSessionKey: result.wxSessionKey,
});
return;
}
if (result.ok) {
setProfile(
mergeWxDisplayProfile({
...(result.profile ?? {}),
id: result.profile?.id ?? profile?.id ?? '',
hasWechat: true,
}),
);
loadProfile();
toast('微信授权成功', 'success');
}
return;
}
let wxInfo = null;
try {
wxInfo = await fetchMiniWechatUserInfo();
@@ -127,7 +153,7 @@ export default function MinePage() {
}
const result = await bindWechatForUser(wxInfo);
if (!result.ok && result.needBindPhone) {
if (!result.ok && 'needBindPhone' in result && result.needBindPhone) {
goLogin('/pages/mine/index', { bindMode: '1', wxSessionKey: result.wxSessionKey });
return;
}
@@ -4,9 +4,10 @@ import Taro, { useRouter } from '@tarojs/taro';
import PageShell from '../../components/PageShell';
import SubPageHeader from '../../components/SubPageHeader';
import { buildAddressListUrl, buildPayUrl, readCheckoutContext } from '../../lib/checkout-nav';
import { tryGetClientGpsLocation } from '../../lib/client-location';
import { maskPhone } from '../../lib/phone';
import { ensurePayReady } from '../../lib/pay-ready';
import { request, toast } from '../../lib/api';
import { request } from '../../lib/api';
import { getProductMainImage } from '../../lib/product-images';
type Address = {
@@ -127,12 +128,19 @@ export default function OrderConfirmPage() {
}
async function doSubmit() {
let clientLocation = null;
try {
clientLocation = await tryGetClientGpsLocation();
} catch {
/* GPS 获取失败不阻塞下单 */
}
const order = await request<{ id: string }>('/trade/orders', {
method: 'POST',
data: {
productId,
quantity,
addressId,
...(clientLocation ? { clientLocation } : {}),
},
});
Taro.redirectTo({
@@ -1,4 +1,6 @@
export default definePageConfig({
navigationStyle: 'custom',
navigationBarTitleText: '订单详情',
enableShareAppMessage: true,
enableShareTimeline: true,
});
@@ -1,9 +1,16 @@
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { View, Text } from '@tarojs/components';
import { useRouter } from '@tarojs/taro';
import { useRouter, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
import PageShell from '../../components/PageShell';
import SubPageHeader from '../../components/SubPageHeader';
import ShareNavButton from '../../components/ShareNavButton';
import WechatShareReady from '../../components/WechatShareReady';
import { request, toast } from '../../lib/api';
import {
DEFAULT_SHARE_DESC,
DEFAULT_SHARE_TITLE,
toWeappShareMessage,
} from '../../lib/wechat-share';
type OrderDetail = {
id: string;
@@ -28,9 +35,30 @@ export default function OrderDetailPage() {
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
}, [orderId]);
const sharePayload = useMemo(
() => ({
title: order?.productName
? `我买了${order.productName} · 杜康好客`
: DEFAULT_SHARE_TITLE,
desc: DEFAULT_SHARE_DESC,
path: orderId ? `/pages/order-detail/index?id=${orderId}` : '/pages/home/index',
}),
[order, orderId],
);
useShareAppMessage(() => toWeappShareMessage(sharePayload));
useShareTimeline(() => ({
title: sharePayload.title || DEFAULT_SHARE_TITLE,
query: orderId ? `id=${orderId}` : '',
}));
return (
<PageShell variant="sub" className="order-detail-page">
<SubPageHeader title="订单详情" />
<WechatShareReady payload={sharePayload} />
<SubPageHeader
title="订单详情"
right={<ShareNavButton payload={sharePayload} />}
/>
<View className="sub-page-body">
{!order ? (
<View className="u-empty"></View>
+101 -23
View File
@@ -1,22 +1,29 @@
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { View, Text } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
import Taro, { useDidShow, useRouter } from '@tarojs/taro';
import PageShell from '../../components/PageShell';
import SubPageHeader from '../../components/SubPageHeader';
import WechatLoginButton from '../../components/WechatLoginButton';
import { ensurePayReady } from '../../lib/pay-ready';
import {
authorizeWechatForPay,
fetchClientConfig,
fetchUserProfile,
isWechatAuthRequiredError,
needsWechatAuthForPay,
payOrder,
saveWechatLoginResult,
} from '../../lib/pay-wechat';
import { applyWechatLoginResult } from '../../lib/wechat-auth';
import { isWechatEnv } from '../../lib/weixin';
import { goLogin } from '../../lib/auth-nav';
import { request, toast } from '../../lib/api';
export default function PayPage() {
const router = useRouter();
const orderId = router.params.orderId ?? '';
const [loading, setLoading] = useState(false);
const [authLoading, setAuthLoading] = useState(false);
const [mockMode, setMockMode] = useState(true);
const [needsWechatAuth, setNeedsWechatAuth] = useState(false);
const [msg, setMsg] = useState('');
@@ -27,23 +34,27 @@ export default function PayPage() {
? `/pages/pay/index?orderId=${orderId}`
: '/pages/pay/index';
useEffect(() => {
if (!orderId) return;
void ensurePayReady(returnPath);
}, [orderId, returnPath]);
const refreshPayReadiness = useCallback(async () => {
try {
const [config, profile] = await Promise.all([fetchClientConfig(), fetchUserProfile()]);
setMockMode(config.mockPay);
setNeedsWechatAuth(needsWechatAuthForPay(config, profile));
return profile;
} catch {
return null;
}
}, []);
useDidShow(() => {
void refreshPayReadiness();
});
useEffect(() => {
async function load() {
try {
const [config, profile] = await Promise.all([fetchClientConfig(), fetchUserProfile()]);
setMockMode(config.mockPay);
setNeedsWechatAuth(needsWechatAuthForPay(config, profile));
} catch {
/* ignore */
}
if (!orderId) return;
if (process.env.TARO_ENV === 'weapp') {
void ensurePayReady(returnPath);
}
void load();
}, []);
}, [orderId, returnPath]);
useEffect(() => {
if (!orderId) {
@@ -67,6 +78,38 @@ export default function PayPage() {
});
}, [orderId]);
async function wechatAuthorize() {
setAuthLoading(true);
setMsg('');
try {
if (!isWechatEnv()) {
setMsg('请在微信内打开以授权微信支付');
return;
}
if (process.env.TARO_ENV === 'weapp') {
const ready = await ensurePayReady(returnPath);
if (ready) await refreshPayReadiness();
return;
}
const result = await authorizeWechatForPay();
if (result) {
if (result.needBindPhone && result.wxSessionKey) {
goLogin(returnPath, { bindMode: '1', wxSessionKey: result.wxSessionKey });
return;
}
if (saveWechatLoginResult(result) || applyWechatLoginResult(result)) {
setMsg('');
await refreshPayReadiness();
toast('微信授权成功', 'success');
}
}
} catch (e) {
setMsg(e instanceof Error ? e.message : '微信授权失败');
} finally {
setAuthLoading(false);
}
}
async function pay() {
if (!orderId) {
toast('订单不存在');
@@ -74,8 +117,11 @@ export default function PayPage() {
}
if (needsWechatAuth) {
setMsg('请先完成微信授权后再支付');
const ready = await ensurePayReady(returnPath);
if (!ready) return;
if (process.env.TARO_ENV === 'h5') {
await wechatAuthorize();
} else {
await ensurePayReady(returnPath);
}
return;
}
@@ -96,7 +142,6 @@ export default function PayPage() {
if (isWechatAuthRequiredError(e)) {
setNeedsWechatAuth(true);
setMsg('微信支付需要先完成微信授权');
await ensurePayReady(returnPath);
return;
}
const message = e instanceof Error ? e.message : '支付失败';
@@ -124,6 +169,20 @@ export default function PayPage() {
</Text>
<Text className="pay-status-amount">¥{payAmount}</Text>
</View>
{needsWechatAuth ? (
<View className="pay-wechat-auth-card">
<Text className="pay-wechat-auth-title"></Text>
<Text className="pay-wechat-auth-desc">
</Text>
<WechatLoginButton
loading={authLoading}
onClick={() => void wechatAuthorize()}
/>
</View>
) : null}
<View className="order-card">
<View className="order-row">
<Text className="order-row-label"></Text>
@@ -140,15 +199,34 @@ export default function PayPage() {
</Text>
</View>
</View>
{msg ? <Text className="u-muted" style={{ display: 'block', marginTop: 12 }}>{msg}</Text> : null}
{msg ? (
<Text className="pay-wechat-auth-msg" style={{ display: 'block', marginTop: 12 }}>
{msg}
</Text>
) : null}
</View>
<View className="pay-bar">
<View
className="order-confirm-submit"
style={{ flex: 1, opacity: loading ? 0.7 : 1 }}
onClick={() => !loading && void pay()}
style={{ flex: 1, opacity: loading || needsWechatAuth ? 0.7 : 1 }}
onClick={() => {
if (loading) return;
if (needsWechatAuth) {
void wechatAuthorize();
return;
}
void pay();
}}
>
<Text>{loading ? '支付中…' : needsWechatAuth ? '去授权' : '立即支付'}</Text>
<Text>
{loading
? '支付中…'
: needsWechatAuth
? authLoading
? '授权中…'
: '微信一键授权'
: '立即支付'}
</Text>
</View>
</View>
</PageShell>