diff --git a/apps/mini-user/src/lib/api.ts b/apps/mini-user/src/lib/api.ts index 807c93a..1ecfe6c 100644 --- a/apps/mini-user/src/lib/api.ts +++ b/apps/mini-user/src/lib/api.ts @@ -76,7 +76,18 @@ function parseBody(data: unknown): { code?: number; message?: string } { return {}; } -/** 统一请求:注入 USER_MINI + Bearer,解包 { code, message, data } */ +function isOnLoginPage(): boolean { + try { + const pages = Taro.getCurrentPages(); + const cur = pages[pages.length - 1] as { route?: string } | undefined; + const route = cur?.route || ''; + return route.includes('pages/login'); + } catch { + return false; + } +} + +/** 统一请求:注入 CLIENT_APP + Bearer,解包 { code, message, data } */ export async function request(path: string, options: ReqOptions = {}): Promise { const header: Record = { 'Content-Type': 'application/json', @@ -96,8 +107,12 @@ export async function request(path: string, options: ReqOptions = { const body = parseBody(res.data); if (status === 401 || body?.code === 401) { - clearAuth(); - redirectToLogin(); + // 仅清掉「发起本请求时」仍在使用的 token,避免登录页旧 /auth/me 竞态清掉刚写入的新 token + const stillCurrent = !!token && getToken() === token; + if (stillCurrent) { + clearAuth(); + if (!isOnLoginPage()) redirectToLogin(); + } throw new Error(body?.message || '登录已过期,请重新登录'); } if (status === 404 && body.code === undefined) { diff --git a/apps/mini-user/src/lib/auth-nav.ts b/apps/mini-user/src/lib/auth-nav.ts index 2b5422f..c3e9173 100644 --- a/apps/mini-user/src/lib/auth-nav.ts +++ b/apps/mini-user/src/lib/auth-nav.ts @@ -42,24 +42,31 @@ export function goLogin(returnPath?: string, extras?: Record) { /** 登录成功后回到 return 页,或回退 / 首页 */ export function finishLoginNavigate(returnTo?: string) { const raw = (returnTo || '').trim(); - const target = raw ? decodeURIComponent(raw) : ''; + let target = ''; + try { + target = raw ? decodeURIComponent(raw) : ''; + } catch { + target = raw; + } + // 防止 return 仍指向登录页造成死循环 const pathOnly = target.split('?')[0]; - - if (pathOnly && TAB_PAGES.has(pathOnly)) { - Taro.switchTab({ url: pathOnly }); + if (!pathOnly || pathOnly.includes('/pages/login')) { + Taro.reLaunch({ url: '/pages/home/index' }); return; } - if (pathOnly && pathOnly.startsWith('/pages/')) { + + if (TAB_PAGES.has(pathOnly)) { + Taro.switchTab({ url: pathOnly }).catch(() => { + Taro.reLaunch({ url: pathOnly }); + }); + return; + } + if (pathOnly.startsWith('/pages/')) { Taro.redirectTo({ url: target }).catch(() => { Taro.reLaunch({ url: pathOnly }); }); return; } - const pages = Taro.getCurrentPages(); - if (pages.length > 1) { - Taro.navigateBack(); - return; - } - Taro.switchTab({ url: '/pages/home/index' }); + Taro.reLaunch({ url: '/pages/home/index' }); } diff --git a/apps/mini-user/src/lib/pay-ready.ts b/apps/mini-user/src/lib/pay-ready.ts index 25c95cb..8a3b1eb 100644 --- a/apps/mini-user/src/lib/pay-ready.ts +++ b/apps/mini-user/src/lib/pay-ready.ts @@ -35,8 +35,12 @@ export async function ensurePayReady(returnPath: string): Promise { goLogin(returnPath, { needWechat: '1' }); return false; - } catch { - goLogin(returnPath); + } catch (e) { + // 仅会话失效时踢回登录;网络/业务错误不误清登录态 + const msg = e instanceof Error ? e.message : ''; + if (/登录已过期|重新登录|401/.test(msg) || !isLoggedIn()) { + goLogin(returnPath); + } return false; } } diff --git a/apps/mini-user/src/lib/user-location.ts b/apps/mini-user/src/lib/user-location.ts index ed8bb4b..8ce3dab 100644 --- a/apps/mini-user/src/lib/user-location.ts +++ b/apps/mini-user/src/lib/user-location.ts @@ -5,6 +5,8 @@ import { DEFAULT_REGION, regionFromGeo, type RegionSelection } from './region-da import { FALLBACK_CITY_CODE } from './product-images'; export const GPS_CITY_STORAGE_KEY = 'dukang_gps_city'; +/** 用户拒绝定位后持久化,避免首页/门店每次 useDidShow 再弹授权 */ +const LOCATION_DENIED_KEY = 'dukang_location_denied'; export type ResolvedUserCity = { province: string; @@ -30,7 +32,35 @@ const FALLBACK_CITY: ResolvedUserCity = { displayCity: '郑州市', }; -let locationPrompted = false; +function isLocationDenied(): boolean { + try { + return Taro.getStorageSync(LOCATION_DENIED_KEY) === '1'; + } catch { + return false; + } +} + +function markLocationDenied() { + try { + Taro.setStorageSync(LOCATION_DENIED_KEY, '1'); + } catch { + /* ignore */ + } +} + +function clearLocationDenied() { + try { + Taro.removeStorageSync(LOCATION_DENIED_KEY); + } catch { + /* ignore */ + } +} + +function isDenyMessage(errMsg?: string): boolean { + return /auth deny|authorize|permission|denied|拒绝|用户拒绝|getLocation:fail/i.test( + errMsg || '', + ); +} function readCache(): GpsCityCache | null { try { @@ -55,6 +85,12 @@ function writeCache(data: ResolvedUserCity) { } } +/** 拒绝或失败后写入兜底城市,避免短时间内反复调起定位 */ +function cacheFallbackAndMaybeDeny(denied: boolean) { + if (denied) markLocationDenied(); + writeCache(FALLBACK_CITY); +} + async function reportLocationToServer(payload: { latitude?: number; longitude?: number; @@ -98,14 +134,12 @@ function toResolved(data: { }; } -async function promptLocationAuth() { - if (locationPrompted) return; - locationPrompted = true; +async function promptLocationAuthOnce() { await Taro.showModal({ title: '位置授权', - content: '需要获取您的位置以展示所在城市的商品与门店', - confirmText: '去授权', - showCancel: true, + content: '需要获取您的位置以展示所在城市的商品与门店。拒绝后将默认使用郑州市,不会再次弹窗。', + confirmText: '知道了', + showCancel: false, }).catch(() => {}); } @@ -127,11 +161,14 @@ async function resolveViaH5Jssdk(): Promise { }); if (!outcome.location) { + const denied = isDenyMessage(outcome.errMsg); await reportLocationToServer({ sdk: outcome.sdk, status: 'fail', errMsg: outcome.errMsg, }).catch(() => {}); + // 失败一律缓存兜底,避免首页/门店每次进入再次调起微信定位弹窗 + cacheFallbackAndMaybeDeny(denied); return null; } @@ -143,9 +180,13 @@ async function resolveViaH5Jssdk(): Promise { status: 'success', }); const resolved = toResolved(data); - if (resolved) writeCache(resolved); + if (resolved) { + clearLocationDenied(); + writeCache(resolved); + } return resolved; } catch { + cacheFallbackAndMaybeDeny(false); return null; } } @@ -153,6 +194,10 @@ async function resolveViaH5Jssdk(): Promise { /** 获取并解析用户当前城市;失败返回郑州市兜底 */ export async function resolveUserCity(force = false): Promise { if (!force) { + if (isLocationDenied()) { + const cached = readCache(); + return cached ?? FALLBACK_CITY; + } const cached = readCache(); if (cached) return cached; } @@ -176,20 +221,22 @@ export async function resolveUserCity(force = false): Promise }); const resolved = toResolved(data); if (resolved) { + clearLocationDenied(); writeCache(resolved); return resolved; } } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); - const denied = /auth deny|authorize|permission|拒绝/i.test(errMsg); - if (denied) { - await promptLocationAuth(); + const denied = isDenyMessage(errMsg); + if (denied && !isLocationDenied()) { + await promptLocationAuthOnce(); } await reportLocationToServer({ sdk: 'jssdk', status: 'fail', errMsg: errMsg.slice(0, 200), }).catch(() => {}); + cacheFallbackAndMaybeDeny(denied); } return FALLBACK_CITY; diff --git a/apps/mini-user/src/pages/login/index.tsx b/apps/mini-user/src/pages/login/index.tsx index 2a10047..3a868e1 100644 --- a/apps/mini-user/src/pages/login/index.tsx +++ b/apps/mini-user/src/pages/login/index.tsx @@ -66,8 +66,15 @@ export default function LoginPage() { setCompleteMode(null); return; } + // 完善资料场景才拉 profile;普通登录勿抢跑 /auth/me,避免旧 token 401 与短信登录竞态 + if (!needPhone && !needWechat) { + setCompleteMode(null); + return; + } + let cancelled = false; fetchUserProfile() .then((me) => { + if (cancelled) return; if (needPhone && !me.phoneVerified) { setCompleteMode('phone'); return; @@ -76,13 +83,14 @@ export default function LoginPage() { setCompleteMode('wechat'); return; } - if (needPhone || needWechat) { - finishLoginNavigate(returnTo); - return; - } - setCompleteMode(null); + finishLoginNavigate(returnTo); }) - .catch(() => setCompleteMode(null)); + .catch(() => { + if (!cancelled) setCompleteMode(null); + }); + return () => { + cancelled = true; + }; }, [needPhone, needWechat, returnTo]); useEffect(() => { @@ -103,6 +111,7 @@ export default function LoginPage() { data: SessionPayload | WechatLoginResult, phone?: string, wxInfo?: MiniWechatProfile | null, + successToast = '登录成功', ) { if (!data.accessToken) return; if (phone) saveUserPhone(phone); @@ -116,7 +125,7 @@ export default function LoginPage() { .then((me) => resolveDefaultUserPhone(me)) .catch(() => {}); } - toast('登录成功', 'success'); + toast(successToast, 'success'); finishLoginNavigate(returnTo); } @@ -188,19 +197,26 @@ export default function LoginPage() { return; } if (completeMode === 'phone' && isLoggedIn()) { - await request('/auth/phone/bind', { + // bind 返回新 session(合并账号后旧 guest JWT 立刻失效),必须落盘后再离开 + const data = await request('/auth/phone/bind', { method: 'POST', data: { phone: normalized, code: code.trim() }, }); - saveUserPhone(normalized); - toast('手机号验证成功', 'success'); - finishLoginNavigate(returnTo); + if (!data?.accessToken) { + setMsg('手机号验证成功但会话未返回,请重新登录'); + return; + } + applySessionAndLeave(data, normalized, null, '手机号验证成功'); return; } const data = await request('/auth/login/sms', { method: 'POST', data: { phone: normalized, code: code.trim() }, }); + if (!data?.accessToken) { + setMsg('登录成功但未返回令牌,请重试'); + return; + } applySessionAndLeave(data, normalized); } catch (e) { setMsg(e instanceof Error ? e.message : '登录失败'); diff --git a/apps/mini-user/src/pages/order-detail/index.tsx b/apps/mini-user/src/pages/order-detail/index.tsx index 34e0d3b..edfb363 100644 --- a/apps/mini-user/src/pages/order-detail/index.tsx +++ b/apps/mini-user/src/pages/order-detail/index.tsx @@ -1,11 +1,12 @@ import { useEffect, useMemo, useState } from 'react'; import { View, Text } from '@tarojs/components'; -import { useRouter, useShareAppMessage, useShareTimeline } from '@tarojs/taro'; +import Taro, { 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 { buildPayUrl } from '../../lib/checkout-nav'; import { DEFAULT_SHARE_DESC, DEFAULT_SHARE_TITLE, @@ -21,6 +22,19 @@ type OrderDetail = { qty?: number; addressSnapshot?: string; createdAt?: string; + originOrderId?: string | null; +}; + +const STATUS_LABELS: Record = { + PENDING_PAY: '待付款', + PENDING_SHIP: '待发货', + OUT_WAREHOUSE: '出库中', + SHIPPING: '配送中', + PENDING_RECEIVE: '待签收', + COMPLETED: '已完成', + CANCELLED: '已取消', + REFUNDING: '退款中', + REFUNDED: '已退款', }; export default function OrderDetailPage() { @@ -35,6 +49,8 @@ export default function OrderDetailPage() { .catch((e) => toast(e instanceof Error ? e.message : '加载失败')); }, [orderId]); + const canPay = !!order && order.status === 'PENDING_PAY' && !order.originOrderId; + const sharePayload = useMemo( () => ({ title: order?.productName @@ -52,8 +68,13 @@ export default function OrderDetailPage() { query: orderId ? `id=${orderId}` : '', })); + function goPay() { + if (!order) return; + Taro.navigateTo({ url: buildPayUrl({ orderId: order.id }) }); + } + return ( - + 订单状态 - {order.status || '处理中'} + + {STATUS_LABELS[order.status || ''] || order.status || '处理中'} + 商品信息 @@ -103,6 +126,20 @@ export default function OrderDetailPage() { )} + + {canPay && order && ( + + + 待支付 + + ¥{Number(order.payAmount ?? 0).toFixed(2)} + + + + 去付款 + + + )} ); } diff --git a/apps/mini-user/src/styles/order.css b/apps/mini-user/src/styles/order.css index 5c7ebd3..7313cf2 100644 --- a/apps/mini-user/src/styles/order.css +++ b/apps/mini-user/src/styles/order.css @@ -7,6 +7,15 @@ min-height: 100vh; } +.order-detail-page--with-pay .sub-page-body { + padding-bottom: calc(88px + env(safe-area-inset-bottom, 0px)); +} + +.order-detail-pay-bar { + justify-content: space-between; + gap: 12px; +} + .order-card { background: var(--color-card); border-radius: var(--radius-lg);