Files
dukang/apps/mini-user/src/lib/user-location.ts
T
jacy 5c55d3c385
CI / verify (pull_request) Has been cancelled
fix(mini-user): persist bind-phone session and stop login bounce
Phone bind discarded the new JWT after account merge, so the next page 401'd back to login. Also harden 401 race, location deny cache, unpaid repay CTA, and finishLoginNavigate.
2026-07-15 21:10:42 +08:00

248 lines
6.3 KiB
TypeScript

import Taro from '@tarojs/taro';
import { getWechatLocationDetailed } from '@dukang/weixin-sdk';
import { API_BASE, CLIENT_APP, getToken, request } from './api';
import { DEFAULT_REGION, regionFromGeo, type RegionSelection } from './region-data';
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;
city: string;
district: string;
cityCode?: string;
cityName?: string;
openCity: boolean;
region: RegionSelection;
displayCity: string;
};
type GpsCityCache = ResolvedUserCity & { timestamp: number };
const FALLBACK_CITY: ResolvedUserCity = {
province: DEFAULT_REGION.province,
city: DEFAULT_REGION.city,
district: DEFAULT_REGION.district,
cityCode: FALLBACK_CITY_CODE,
cityName: '郑州市',
openCity: true,
region: DEFAULT_REGION,
displayCity: '郑州市',
};
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 {
const raw = Taro.getStorageSync(GPS_CITY_STORAGE_KEY);
if (!raw) return null;
const parsed = JSON.parse(String(raw)) as GpsCityCache;
if (Date.now() - parsed.timestamp > 30 * 60 * 1000) return null;
return parsed;
} catch {
return null;
}
}
function writeCache(data: ResolvedUserCity) {
try {
Taro.setStorageSync(
GPS_CITY_STORAGE_KEY,
JSON.stringify({ ...data, timestamp: Date.now() } satisfies GpsCityCache),
);
} catch {
/* ignore */
}
}
/** 拒绝或失败后写入兜底城市,避免短时间内反复调起定位 */
function cacheFallbackAndMaybeDeny(denied: boolean) {
if (denied) markLocationDenied();
writeCache(FALLBACK_CITY);
}
async function reportLocationToServer(payload: {
latitude?: number;
longitude?: number;
sdk: 'jssdk' | 'geolocation';
status: 'success' | 'fail';
errMsg?: string;
}) {
return request<{
province?: string;
city?: string;
district?: string;
cityCode?: string;
cityName?: string;
openCity?: boolean;
}>('/common/wechat/location', {
method: 'POST',
data: payload,
});
}
function toResolved(data: {
province?: string;
city?: string;
district?: string;
cityCode?: string;
cityName?: string;
openCity?: boolean;
}): ResolvedUserCity | null {
if (!data.province || !data.city) return null;
const region = regionFromGeo(data.province, data.city, data.district);
const displayCity = data.cityName ?? (data.city.endsWith('市') ? data.city : `${data.city}市`);
return {
province: data.province,
city: data.city,
district: data.district ?? '',
cityCode: data.cityCode,
cityName: data.cityName,
openCity: !!data.openCity,
region,
displayCity,
};
}
async function promptLocationAuthOnce() {
await Taro.showModal({
title: '位置授权',
content: '需要获取您的位置以展示所在城市的商品与门店。拒绝后将默认使用郑州市,不会再次弹窗。',
confirmText: '知道了',
showCancel: false,
}).catch(() => {});
}
function getMiniLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
return new Promise((resolve, reject) => {
Taro.getLocation({
type: 'gcj02',
success: resolve,
fail: reject,
});
});
}
async function resolveViaH5Jssdk(): Promise<ResolvedUserCity | null> {
const outcome = await getWechatLocationDetailed({
apiBase: API_BASE,
clientApp: CLIENT_APP,
getAccessToken: () => getToken() || null,
});
if (!outcome.location) {
const denied = isDenyMessage(outcome.errMsg);
await reportLocationToServer({
sdk: outcome.sdk,
status: 'fail',
errMsg: outcome.errMsg,
}).catch(() => {});
// 失败一律缓存兜底,避免首页/门店每次进入再次调起微信定位弹窗
cacheFallbackAndMaybeDeny(denied);
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) {
clearLocationDenied();
writeCache(resolved);
}
return resolved;
} catch {
cacheFallbackAndMaybeDeny(false);
return null;
}
}
/** 获取并解析用户当前城市;失败返回郑州市兜底 */
export async function resolveUserCity(force = false): Promise<ResolvedUserCity> {
if (!force) {
if (isLocationDenied()) {
const cached = readCache();
return cached ?? FALLBACK_CITY;
}
const cached = readCache();
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 getMiniLocation();
const data = await reportLocationToServer({
latitude: loc.latitude,
longitude: loc.longitude,
sdk: 'jssdk',
status: 'success',
});
const resolved = toResolved(data);
if (resolved) {
clearLocationDenied();
writeCache(resolved);
return resolved;
}
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
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;
}
export function getCityCodeForCatalog(resolved: ResolvedUserCity): string {
return resolved.openCity && resolved.cityCode ? resolved.cityCode : FALLBACK_CITY_CODE;
}