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 { return new Promise((resolve, reject) => { Taro.getLocation({ type: 'gcj02', success: resolve, fail: reject, }); }); } async function resolveViaH5Jssdk(): Promise { 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 { 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; }