From 1ad49f1acf55da92df1ad1f7fa62fea0430769b6 Mon Sep 17 00:00:00 2001
From: jacy <18049821889@163.com>
Date: Tue, 14 Jul 2026 20:09:36 +0800
Subject: [PATCH] =?UTF-8?q?h5=E5=BE=AE=E4=BF=A1=E6=8E=88=E6=9D=83?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
apps/mini-user/src/app.tsx | 8 +-
.../src/components/WechatShareBootstrap.tsx | 94 +++++++++++++
apps/mini-user/src/lib/client-location.ts | 36 +++++
apps/mini-user/src/lib/pay-ready.ts | 26 +++-
apps/mini-user/src/lib/pay-wechat.ts | 69 ++++++----
apps/mini-user/src/lib/user-location.ts | 44 ++++++-
apps/mini-user/src/lib/wechat-auth.ts | 115 +++++++++++++++-
apps/mini-user/src/pages/login/index.tsx | 17 ++-
apps/mini-user/src/pages/mine/index.tsx | 38 +++++-
.../src/pages/order-confirm/index.tsx | 10 +-
.../src/pages/order-detail/index.config.ts | 2 +
.../src/pages/order-detail/index.tsx | 34 ++++-
apps/mini-user/src/pages/pay/index.tsx | 124 ++++++++++++++----
apps/mini-user/src/styles/order.css | 31 +++++
14 files changed, 569 insertions(+), 79 deletions(-)
create mode 100644 apps/mini-user/src/components/WechatShareBootstrap.tsx
create mode 100644 apps/mini-user/src/lib/client-location.ts
diff --git a/apps/mini-user/src/app.tsx b/apps/mini-user/src/app.tsx
index 85fa689..cb333b4 100644
--- a/apps/mini-user/src/app.tsx
+++ b/apps/mini-user/src/app.tsx
@@ -1,9 +1,15 @@
import './lib/text-encoding-polyfill';
import { PropsWithChildren } from 'react';
+import WechatShareBootstrap from './components/WechatShareBootstrap';
import './app.css';
function App({ children }: PropsWithChildren) {
- return children;
+ return (
+ <>
+
+ {children}
+ >
+ );
}
export default App;
diff --git a/apps/mini-user/src/components/WechatShareBootstrap.tsx b/apps/mini-user/src/components/WechatShareBootstrap.tsx
new file mode 100644
index 0000000..da5132f
--- /dev/null
+++ b/apps/mini-user/src/components/WechatShareBootstrap.tsx
@@ -0,0 +1,94 @@
+import Taro, { useDidShow } from '@tarojs/taro';
+import { captureIosJssdkEntryUrl } from '@dukang/weixin-sdk';
+import { useEffect, useRef } from 'react';
+import { finishLoginNavigate, goLogin } from '../lib/auth-nav';
+import { toast } from '../lib/api';
+import { saveWechatLoginResult } from '../lib/pay-wechat';
+import { applyWechatShare } from '../lib/wechat-share';
+import { handleWechatAuthCallback } from '../lib/wechat-auth';
+import { isWechatEnv } from '../lib/weixin';
+
+function currentPagePathWithQuery(): string {
+ const pages = Taro.getCurrentPages();
+ const cur = pages[pages.length - 1] as
+ | { route?: string; options?: Record }
+ | undefined;
+ if (!cur?.route) {
+ if (typeof window !== 'undefined') {
+ return `${window.location.pathname}${window.location.search}`;
+ }
+ return '';
+ }
+ const path = cur.route.startsWith('/') ? cur.route : `/${cur.route}`;
+ const opts = cur.options ?? {};
+ const qs = Object.entries(opts)
+ .filter(([k, v]) => v != null && v !== '' && k !== 'code' && k !== 'state')
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
+ .join('&');
+ return qs ? `${path}?${qs}` : path;
+}
+
+/**
+ * H5 全局:
+ * 1. 默认分享卡片
+ * 2. 公众号 OAuth ?code= 统一回调(避免各页重复消费 code)
+ */
+export default function WechatShareBootstrap() {
+ const handlingCode = useRef(false);
+
+ useEffect(() => {
+ if (process.env.TARO_ENV === 'h5') {
+ captureIosJssdkEntryUrl();
+ }
+ }, []);
+
+ useDidShow(() => {
+ if (process.env.TARO_ENV !== 'h5') return;
+ void applyWechatShare().catch(() => {});
+
+ if (!isWechatEnv()) return;
+ if (typeof window === 'undefined') return;
+ const params = new URLSearchParams(window.location.search);
+ if (!params.get('code')) return;
+ if (handlingCode.current) return;
+ handlingCode.current = true;
+
+ const returnFromLogin = (() => {
+ const path = currentPagePathWithQuery();
+ if (path.includes('/pages/login/')) {
+ try {
+ return decodeURIComponent(params.get('return') || '') || undefined;
+ } catch {
+ return undefined;
+ }
+ }
+ return undefined;
+ })();
+
+ handleWechatAuthCallback()
+ .then((result) => {
+ if (!result) return;
+ if (result.needBindPhone && result.wxSessionKey) {
+ goLogin(returnFromLogin, {
+ bindMode: '1',
+ wxSessionKey: result.wxSessionKey,
+ });
+ return;
+ }
+ if (saveWechatLoginResult(result)) {
+ toast('微信授权成功', 'success');
+ if (returnFromLogin !== undefined || currentPagePathWithQuery().includes('/pages/login/')) {
+ finishLoginNavigate(returnFromLogin || params.get('return') || undefined);
+ }
+ }
+ })
+ .catch((e) => {
+ toast(e instanceof Error ? e.message : '微信授权失败');
+ })
+ .finally(() => {
+ handlingCode.current = false;
+ });
+ });
+
+ return null;
+}
diff --git a/apps/mini-user/src/lib/client-location.ts b/apps/mini-user/src/lib/client-location.ts
new file mode 100644
index 0000000..c9c772e
--- /dev/null
+++ b/apps/mini-user/src/lib/client-location.ts
@@ -0,0 +1,36 @@
+import { weixinSdk } from './weixin';
+
+export type ClientGpsLocation = {
+ province?: string;
+ city?: string;
+ district?: string;
+ latitude: number;
+ longitude: number;
+ address?: string;
+};
+
+/** 尝试获取客户端 GPS(微信 JSSDK / 浏览器 Geolocation),失败返回 null 不阻塞下单 */
+export async function tryGetClientGpsLocation(): Promise {
+ if (process.env.TARO_ENV === 'weapp') {
+ try {
+ const Taro = (await import('@tarojs/taro')).default;
+ const loc = await new Promise<{ latitude: number; longitude: number }>((resolve, reject) => {
+ Taro.getLocation({
+ type: 'gcj02',
+ success: resolve,
+ fail: reject,
+ });
+ });
+ return { latitude: loc.latitude, longitude: loc.longitude };
+ } catch {
+ return null;
+ }
+ }
+
+ const loc = await weixinSdk.getLocation();
+ if (!loc) return null;
+ return {
+ latitude: loc.latitude,
+ longitude: loc.longitude,
+ };
+}
diff --git a/apps/mini-user/src/lib/pay-ready.ts b/apps/mini-user/src/lib/pay-ready.ts
index e988a1d..704dfa7 100644
--- a/apps/mini-user/src/lib/pay-ready.ts
+++ b/apps/mini-user/src/lib/pay-ready.ts
@@ -1,8 +1,14 @@
import { goLogin } from './auth-nav';
import { isLoggedIn } from './api';
+import { ensureWechatAuthForPay } from './wechat-auth';
import { fetchClientConfig, fetchUserProfile, needsWechatAuthForPay } from './pay-wechat';
-/** 支付前门禁:未登录/未验手机/未绑微信时跳转登录页 */
+/**
+ * 支付前门禁:
+ * - 未登录 / 未验手机 → 跳转登录页
+ * - H5 微信内缺 openId → 尝试 OAuth(可能跳转微信授权页)
+ * - 小程序缺绑定 → 跳转登录页 needWechat
+ */
export async function ensurePayReady(returnPath: string): Promise {
if (!isLoggedIn()) {
goLogin(returnPath);
@@ -15,11 +21,23 @@ export async function ensurePayReady(returnPath: string): Promise {
goLogin(returnPath, { needPhone: '1' });
return false;
}
- if (needsWechatAuthForPay(config, profile)) {
- goLogin(returnPath, { needWechat: '1' });
+ if (!needsWechatAuthForPay(config, profile)) {
+ return true;
+ }
+
+ if (process.env.TARO_ENV === 'h5') {
+ const auth = await ensureWechatAuthForPay();
+ if (auth.ok) return true;
+ if ('needBindPhone' in auth && auth.needBindPhone) {
+ goLogin(returnPath, { bindMode: '1', wxSessionKey: auth.wxSessionKey });
+ return false;
+ }
+ // redirecting:正在跳转微信 OAuth
return false;
}
- return true;
+
+ goLogin(returnPath, { needWechat: '1' });
+ return false;
} catch {
goLogin(returnPath);
return false;
diff --git a/apps/mini-user/src/lib/pay-wechat.ts b/apps/mini-user/src/lib/pay-wechat.ts
index 2e1c3c6..0983953 100644
--- a/apps/mini-user/src/lib/pay-wechat.ts
+++ b/apps/mini-user/src/lib/pay-wechat.ts
@@ -1,9 +1,13 @@
-import type { ClientRuntimeConfig, WechatJsapiPrepayParams, WechatLoginResult, WechatPayOrderResult } from '@dukang/shared-types';
+import type {
+ ClientRuntimeConfig,
+ WechatJsapiPrepayParams,
+ WechatLoginResult,
+ WechatPayOrderResult,
+} from '@dukang/shared-types';
import { WECHAT_AUTH_REQUIRED, isWxAuthorizeEnabled } from '@dukang/shared-types';
import { invokeWechatPay } from '@dukang/weixin-sdk';
-import Taro from '@tarojs/taro';
-import { request, type UserProfile } from './api';
-import { syncMiniWechatProfile } from './mini-wechat-profile';
+import { request, saveAuth, type UserProfile } from './api';
+import { isWechatEnv, weixinSdk } from './weixin';
export function isMiniWechatEnv(): boolean {
return process.env.TARO_ENV === 'weapp';
@@ -21,13 +25,35 @@ export async function fetchUserProfile(): Promise {
return request('/auth/me');
}
-/** 真实微信支付且未绑定微信时需要授权 */
+/** 真实微信支付且未绑定微信时需要授权(小程序 / H5 微信内) */
export function needsWechatAuthForPay(
config: ClientRuntimeConfig,
profile: UserProfile | null,
): boolean {
if (!isWxAuthorizeEnabled(config)) return false;
- return !config.mockPay && config.wechatPayEnabled && isMiniWechatEnv() && !profile?.hasWechat;
+ return !config.mockPay && config.wechatPayEnabled && isWechatEnv() && !profile?.hasWechat;
+}
+
+export function saveWechatLoginResult(result: WechatLoginResult): boolean {
+ if (!result.accessToken) return false;
+ saveAuth({
+ accessToken: result.accessToken,
+ refreshToken: result.refreshToken,
+ });
+ return true;
+}
+
+/** H5:发起公众号 OAuth(可能直接跳转);小程序请用 bindWechatForUser */
+export async function authorizeWechatForPay(): Promise {
+ const config = await fetchClientConfig();
+ if (!isWxAuthorizeEnabled(config)) return;
+ if (!isWechatEnv()) {
+ throw new Error('请在微信内打开以完成授权');
+ }
+ if (process.env.TARO_ENV === 'h5') {
+ return weixinSdk.login();
+ }
+ throw new Error('请使用小程序微信授权');
}
function sleep(ms: number) {
@@ -49,9 +75,12 @@ export async function payOrder(orderId: string): Promise<'paid' | 'pending'> {
});
if (result.mode === 'jsapi' && result.prepay) {
- await invokeWechatPay(result.prepay as WechatJsapiPrepayParams, {
- platform: isMiniWechatEnv() ? 'mini' : undefined,
- });
+ const prepay = result.prepay as WechatJsapiPrepayParams;
+ if (process.env.TARO_ENV === 'h5') {
+ await weixinSdk.pay(prepay);
+ } else {
+ await invokeWechatPay(prepay, { platform: 'mini' });
+ }
const paid = await waitOrderPaid(orderId);
return paid ? 'paid' : 'pending';
}
@@ -61,23 +90,5 @@ export async function payOrder(orderId: string): Promise<'paid' | 'pending'> {
export type WechatBindResult =
| { ok: true; profile?: UserProfile }
- | { ok: false; needBindPhone: true; wxSessionKey: string };
-
-export async function bindWechatForUser(
- prefetchedWxProfile?: { nickname?: string; avatarUrl?: string } | null,
-): Promise {
- const res = await Taro.login();
- if (!res.code) {
- throw new Error(res.errMsg || '微信授权失败');
- }
- const data = await request('/auth/wechat/bind', {
- method: 'POST',
- data: { code: res.code, platform: 'mini' },
- });
- if (data.needBindPhone && data.wxSessionKey) {
- return { ok: false, needBindPhone: true, wxSessionKey: data.wxSessionKey };
- }
- await syncMiniWechatProfile(prefetchedWxProfile);
- const profile = await fetchUserProfile();
- return { ok: true, profile };
-}
+ | { ok: false; needBindPhone: true; wxSessionKey: string }
+ | { ok: false; redirecting: true };
diff --git a/apps/mini-user/src/lib/user-location.ts b/apps/mini-user/src/lib/user-location.ts
index d00bfc5..593a8ee 100644
--- a/apps/mini-user/src/lib/user-location.ts
+++ b/apps/mini-user/src/lib/user-location.ts
@@ -1,7 +1,9 @@
import Taro from '@tarojs/taro';
-import { request } from './api';
+import { getWechatLocationDetailed } from '@dukang/weixin-sdk';
+import { API_BASE, getToken, request } from './api';
import { DEFAULT_REGION, regionFromGeo, type RegionSelection } from './region-data';
import { FALLBACK_CITY_CODE } from './product-images';
+import { ClientApp } from '@dukang/shared-types';
export const GPS_CITY_STORAGE_KEY = 'dukang_gps_city';
@@ -108,7 +110,7 @@ async function promptLocationAuth() {
}).catch(() => {});
}
-function getLocation(): Promise {
+function getMiniLocation(): Promise {
return new Promise((resolve, reject) => {
Taro.getLocation({
type: 'gcj02',
@@ -118,6 +120,37 @@ function getLocation(): Promise {
});
}
+async function resolveViaH5Jssdk(): Promise {
+ const outcome = await getWechatLocationDetailed({
+ apiBase: API_BASE,
+ clientApp: ClientApp.USER_H5,
+ getAccessToken: () => getToken() || null,
+ });
+
+ if (!outcome.location) {
+ await reportLocationToServer({
+ sdk: outcome.sdk,
+ status: 'fail',
+ errMsg: outcome.errMsg,
+ }).catch(() => {});
+ return null;
+ }
+
+ try {
+ const data = await reportLocationToServer({
+ latitude: outcome.location.latitude,
+ longitude: outcome.location.longitude,
+ sdk: outcome.sdk,
+ status: 'success',
+ });
+ const resolved = toResolved(data);
+ if (resolved) writeCache(resolved);
+ return resolved;
+ } catch {
+ return null;
+ }
+}
+
/** 获取并解析用户当前城市;失败返回郑州市兜底 */
export async function resolveUserCity(force = false): Promise {
if (!force) {
@@ -125,12 +158,17 @@ export async function resolveUserCity(force = false): Promise
if (cached) return cached;
}
+ if (process.env.TARO_ENV === 'h5') {
+ const fromJssdk = await resolveViaH5Jssdk();
+ return fromJssdk ?? FALLBACK_CITY;
+ }
+
if (process.env.TARO_ENV !== 'weapp') {
return FALLBACK_CITY;
}
try {
- const loc = await getLocation();
+ const loc = await getMiniLocation();
const data = await reportLocationToServer({
latitude: loc.latitude,
longitude: loc.longitude,
diff --git a/apps/mini-user/src/lib/wechat-auth.ts b/apps/mini-user/src/lib/wechat-auth.ts
index cb763a9..5e3fb73 100644
--- a/apps/mini-user/src/lib/wechat-auth.ts
+++ b/apps/mini-user/src/lib/wechat-auth.ts
@@ -1,17 +1,124 @@
import type { WechatLoginResult } from '@dukang/shared-types';
+import { isWxAuthorizeEnabled } from '@dukang/shared-types';
+import { stripOAuthParamsFromLocation } from '@dukang/weixin-sdk';
import Taro from '@tarojs/taro';
-import { request } from './api';
+import { saveAuth } from './api';
+import { syncMiniWechatProfile } from './mini-wechat-profile';
+import {
+ authorizeWechatForPay,
+ fetchClientConfig,
+ fetchUserProfile,
+ needsWechatAuthForPay,
+ saveWechatLoginResult,
+ type WechatBindResult,
+} from './pay-wechat';
+import { isWechatEnv, weixinSdk } from './weixin';
-/** 小程序微信授权登录:Taro.login → /auth/login/wechat(资料上报由调用方 saveAuth 后执行) */
-export async function loginWithWechat(): Promise {
+export type WechatAuthEnsureResult =
+ | { ok: true }
+ | { ok: false; redirecting: true }
+ | { ok: false; needBindPhone: true; wxSessionKey: string };
+
+/** 小程序:Taro.login → /auth/login/wechat;H5:公众号 OAuth */
+export async function loginWithWechat(): Promise {
+ if (process.env.TARO_ENV === 'h5') {
+ return loginWithWechatSdk();
+ }
const res = await Taro.login();
if (!res.code) {
throw new Error(res.errMsg || '微信登录失败,未获取到 code');
}
+ const { request } = await import('./api');
return request('/auth/login/wechat', {
method: 'POST',
data: { code: res.code, platform: 'mini' },
});
}
-export { bindWechatForUser } from './pay-wechat';
+export async function checkNeedsWechatAuth(): Promise {
+ const [config, profile] = await Promise.all([fetchClientConfig(), fetchUserProfile()]);
+ return needsWechatAuthForPay(config, profile);
+}
+
+/** 真实微信支付前确保已绑定微信;OAuth 跳转时返回 redirecting */
+export async function ensureWechatAuthForPay(): Promise {
+ if (!isWechatEnv()) return { ok: true };
+ if (!(await checkNeedsWechatAuth())) return { ok: true };
+
+ const result = await authorizeWechatForPay();
+ if (!result) return { ok: false, redirecting: true };
+
+ if (result.needBindPhone && result.wxSessionKey) {
+ return { ok: false, needBindPhone: true, wxSessionKey: result.wxSessionKey };
+ }
+
+ if (saveWechatLoginResult(result)) {
+ return { ok: true };
+ }
+ return { ok: false, redirecting: true };
+}
+
+/** 处理 URL 中 OAuth ?code= 回调(H5 公众号) */
+export async function handleWechatAuthCallback(): Promise {
+ if (process.env.TARO_ENV !== 'h5') return null;
+ if (!isWechatEnv()) return null;
+ const config = await fetchClientConfig();
+ if (!isWxAuthorizeEnabled(config)) return null;
+ const result = await weixinSdk.handleOAuthCallback();
+ if (result) stripOAuthParamsFromLocation();
+ return result;
+}
+
+export async function loginWithWechatSdk(): Promise {
+ 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);
+}
+
+/** 已登录用户绑定微信:小程序 code;H5 走公众号 OAuth(带 JWT 时服务端会 attach) */
+export async function bindWechatForUser(
+ prefetchedWxProfile?: { nickname?: string; avatarUrl?: string } | null,
+): Promise {
+ if (process.env.TARO_ENV === 'h5') {
+ const result = await authorizeWechatForPay();
+ if (!result) {
+ return { ok: false, redirecting: true };
+ }
+ if (result.needBindPhone && result.wxSessionKey) {
+ return { ok: false, needBindPhone: true, wxSessionKey: result.wxSessionKey };
+ }
+ if (result.accessToken) {
+ saveAuth({
+ accessToken: result.accessToken,
+ refreshToken: result.refreshToken,
+ });
+ }
+ const profile = await fetchUserProfile();
+ return { ok: true, profile };
+ }
+
+ const res = await Taro.login();
+ if (!res.code) {
+ throw new Error(res.errMsg || '微信授权失败');
+ }
+ const { request } = await import('./api');
+ const data = await request('/auth/wechat/bind', {
+ method: 'POST',
+ data: { code: res.code, platform: 'mini' },
+ });
+ if (data.needBindPhone && data.wxSessionKey) {
+ return { ok: false, needBindPhone: true, wxSessionKey: data.wxSessionKey };
+ }
+ await syncMiniWechatProfile(prefetchedWxProfile);
+ const profile = await fetchUserProfile();
+ return { ok: true, profile };
+}
+
+export type { WechatBindResult };
diff --git a/apps/mini-user/src/pages/login/index.tsx b/apps/mini-user/src/pages/login/index.tsx
index ad49a56..429a696 100644
--- a/apps/mini-user/src/pages/login/index.tsx
+++ b/apps/mini-user/src/pages/login/index.tsx
@@ -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 {
diff --git a/apps/mini-user/src/pages/mine/index.tsx b/apps/mini-user/src/pages/mine/index.tsx
index 6ef4727..66b22ca 100644
--- a/apps/mini-user/src/pages/mine/index.tsx
+++ b/apps/mini-user/src/pages/mine/index.tsx
@@ -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;
}
diff --git a/apps/mini-user/src/pages/order-confirm/index.tsx b/apps/mini-user/src/pages/order-confirm/index.tsx
index 435c111..946f175 100644
--- a/apps/mini-user/src/pages/order-confirm/index.tsx
+++ b/apps/mini-user/src/pages/order-confirm/index.tsx
@@ -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({
diff --git a/apps/mini-user/src/pages/order-detail/index.config.ts b/apps/mini-user/src/pages/order-detail/index.config.ts
index 58da757..06ef8fb 100644
--- a/apps/mini-user/src/pages/order-detail/index.config.ts
+++ b/apps/mini-user/src/pages/order-detail/index.config.ts
@@ -1,4 +1,6 @@
export default definePageConfig({
navigationStyle: 'custom',
navigationBarTitleText: '订单详情',
+ enableShareAppMessage: true,
+ enableShareTimeline: true,
});
diff --git a/apps/mini-user/src/pages/order-detail/index.tsx b/apps/mini-user/src/pages/order-detail/index.tsx
index ca56b54..34e0d3b 100644
--- a/apps/mini-user/src/pages/order-detail/index.tsx
+++ b/apps/mini-user/src/pages/order-detail/index.tsx
@@ -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 (
-
+
+ }
+ />
{!order ? (
加载中…
diff --git a/apps/mini-user/src/pages/pay/index.tsx b/apps/mini-user/src/pages/pay/index.tsx
index 7d9f506..fa2ff8e 100644
--- a/apps/mini-user/src/pages/pay/index.tsx
+++ b/apps/mini-user/src/pages/pay/index.tsx
@@ -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() {
¥{payAmount}
+
+ {needsWechatAuth ? (
+
+ 尚未授权微信
+
+ 授权后可安全调起微信支付,不会重复扣款
+
+ void wechatAuthorize()}
+ />
+
+ ) : null}
+
订单号
@@ -140,15 +199,34 @@ export default function PayPage() {
- {msg ? {msg} : null}
+ {msg ? (
+
+ {msg}
+
+ ) : null}
!loading && void pay()}
+ style={{ flex: 1, opacity: loading || needsWechatAuth ? 0.7 : 1 }}
+ onClick={() => {
+ if (loading) return;
+ if (needsWechatAuth) {
+ void wechatAuthorize();
+ return;
+ }
+ void pay();
+ }}
>
- {loading ? '支付中…' : needsWechatAuth ? '去授权' : '立即支付'}
+
+ {loading
+ ? '支付中…'
+ : needsWechatAuth
+ ? authLoading
+ ? '授权中…'
+ : '微信一键授权'
+ : '立即支付'}
+
diff --git a/apps/mini-user/src/styles/order.css b/apps/mini-user/src/styles/order.css
index 16e3505..5c7ebd3 100644
--- a/apps/mini-user/src/styles/order.css
+++ b/apps/mini-user/src/styles/order.css
@@ -278,3 +278,34 @@
color: var(--color-heritage-red);
margin-bottom: 24px;
}
+
+.pay-wechat-auth-card {
+ margin: 0 0 16px;
+ padding: 16px;
+ border-radius: 12px;
+ background: #fff;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+ text-align: center;
+}
+
+.pay-wechat-auth-title {
+ display: block;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--color-on-surface);
+ margin-bottom: 8px;
+}
+
+.pay-wechat-auth-desc {
+ display: block;
+ font-size: 13px;
+ color: var(--color-muted, #999);
+ margin-bottom: 16px;
+ line-height: 1.5;
+}
+
+.pay-wechat-auth-msg {
+ font-size: 13px;
+ color: var(--color-heritage-red, #a02d30);
+ line-height: 1.5;
+}