Procházet zdrojové kódy

测试微信内打开app

zhuli před 2 týdny
rodič
revize
0648be3832
1 změnil soubory, kde provedl 754 přidání a 71 odebrání
  1. 754 71
      HBuilderProjects/shareIndex.html

+ 754 - 71
HBuilderProjects/shareIndex.html

@@ -22,10 +22,6 @@
 			padding: 0;
 			padding: 0;
 			box-sizing: border-box;
 			box-sizing: border-box;
 		}
 		}
-		#staffGroupsMount{
-			height: 202px;
-			overflow: hidden;
-		}
 
 
 		html {
 		html {
 			font-size: 16px;
 			font-size: 16px;
@@ -469,10 +465,77 @@
 			pointer-events: none;
 			pointer-events: none;
 		}
 		}
 
 
-		.fab-wrap .fab {
+		#fabDock,
+		.fab-dock__slot,
+		#fabDock .fab,
+		.fab-wx-hit {
 			pointer-events: auto;
 			pointer-events: auto;
 		}
 		}
 
 
+		#fabDock {
+			display: block;
+		}
+
+		.fab-dock__slot {
+			position: relative;
+			width: 100%;
+			max-width: 320px;
+			height: 48px;
+			margin: 0 auto;
+			-webkit-tap-highlight-color: transparent;
+		}
+
+		#openApp {
+			position: relative;
+			z-index: 1;
+			touch-action: manipulation;
+		}
+
+		#openApp img {
+			pointer-events: none;
+		}
+
+		.fab-wx-hit {
+			display: none;
+			position: absolute;
+			left: 0;
+			right: 0;
+			top: 0;
+			height: 48px;
+			z-index: 3;
+		}
+
+		/* 底层 #openApp 始终显示;上层透明开放标签接收点击(标签未渲染时仍能看见并点底层按钮) */
+		#fabDock.wx-open-enabled .fab-wx-hit {
+			display: block;
+		}
+
+		#launch-btn {
+			display: block;
+			width: 100%;
+			height: 48px;
+			min-height: 48px;
+			line-height: 48px;
+		}
+
+		#openAppToast {
+			display: none;
+			position: fixed;
+			left: 16px;
+			right: 16px;
+			bottom: calc(72px + var(--safe-bottom));
+			z-index: 10001;
+			padding: 10px 14px;
+			font-size: 13px;
+			line-height: 1.45;
+			color: #fff;
+			text-align: center;
+			background: rgba(0, 0, 0, 0.78);
+			border-radius: 8px;
+			pointer-events: none;
+			word-break: break-all;
+		}
+
 		.fab {
 		.fab {
 			display: flex;
 			display: flex;
 			align-items: center;
 			align-items: center;
@@ -971,21 +1034,36 @@
 			<div id="staffGroupsMount"></div>
 			<div id="staffGroupsMount"></div>
 		</section>
 		</section>
 	</div>
 	</div>
-	<wx-open-launch-app
-	id="launch-btn"
-	appid="wxf5f1efe3a9f5012e"
-	extinfo="https://test.ailien.shop/h5/HBuilderProjects/shareIndex.html"
-	>
-		<div id="shareBelowContentEl">
-			<div class="fab-wrap">
-				<button type="button" class="fab" id="openApp">
-					<img src="images/uBtn.png" alt="APP内打开" decoding="async">
-				</button>
-				<div class="home-indicator" aria-hidden="true"></div>
+	<pre id="wxDebugBar" style="display:none;position:fixed;left:8px;right:8px;top:8px;z-index:9999;max-height:40vh;overflow:auto;margin:0;padding:8px;font-size:11px;line-height:1.35;background:rgba(0,0,0,.82);color:#0f0;white-space:pre-wrap;word-break:break-all;border-radius:6px;pointer-events:none;"></pre>
+	<div id="openAppToast" role="status" aria-live="polite"></div>
+	<div id="fabDock" class="fab-wrap">
+		<div class="fab-dock__slot">
+			<button type="button" class="fab" id="openApp">
+				<img src="images/uBtn.png" alt="APP内打开" decoding="async">
+			</button>
+			<div id="fabWxHit" class="fab-wx-hit" aria-hidden="true">
+				<wx-open-launch-app id="launch-btn" appid="wx5598889b28511717">
+					<script type="text/wxtag-template">
+						<style>
+							.wx-open-app-btn {
+								display: block;
+								width: 100%;
+								height: 48px;
+								margin: 0;
+								padding: 0;
+								border: none;
+								opacity: 0;
+								background: transparent;
+							}
+						</style>
+						<button class="wx-open-app-btn" aria-label="APP内打开">打开</button>
+					</script>
+				</wx-open-launch-app>
 			</div>
 			</div>
 		</div>
 		</div>
-	</wx-open-launch-app>
-	<script src="http://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
+		<div class="home-indicator" aria-hidden="true"></div>
+	</div>
+	<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
 	<script>
 	<script>
 	(function () {
 	(function () {
 		'use strict';
 		'use strict';
@@ -1006,33 +1084,77 @@
 		 */
 		 */
 		var API_BASE = 'https://test.ailien.shop/alienStore';
 		var API_BASE = 'https://test.ailien.shop/alienStore';
 
 
+		/** wx-open-launch-app 的 appid(开放平台移动应用;若与公众号不同,只改这里和标签 appid) */
+		var WECHAT_OPEN_APP_ID = 'wx5598889b28511717';
+		/** 可选:强制 wx.config 使用的公众号 appId;默认用 getWxConfig 返回的 data.appId */
+		var WECHAT_MP_APP_ID = '';
+		/**
+		 * 进入页 GET {API_BASE}/wx/getWxConfig?url={当前页完整 URL,含 ? 参数,不含 #}
+		 * 微信校验签名与 location.href.split('#')[0] 必须一致,不能只签 shareIndex.html 无参数
+		 * 可用 ?wxSignPath= 或 ?wxSignUrl= 覆盖。
+		 */
+		var WECHAT_GET_WX_CONFIG_PATH = '/wx/getWxConfig';
+		var H5_PAGE_BASE_FALLBACK = 'https://test.ailien.shop/h5/HBuilderProjects/';
+		/** 本地 file:// 预览时传给 getWxConfig 的默认 url(无 query) */
+		var WX_GET_CONFIG_SIGN_URL = 'https://test.ailien.shop/h5/HBuilderProjects/shareIndex.html';
+		/** wx.config 已成功(开放标签可展示);与「用户已成功唤起 App」不是同一回事 */
+		var weChatJssdkConfigured = false;
+		var wxConfigSignRetriedBaseUrl = false;
+		var wxSignUrlFullBeforeStrip = '';
+		var wxDebugLastStatus = '';
+
+		function isWxDebugOn() {
+			return String(q('wxDebug') || '') === '1';
+		}
+
+		function stringifyWxDebugPayload(obj) {
+			try {
+				return JSON.stringify(obj, null, 2);
+			} catch (eJson) {
+				return String(obj && obj.errMsg ? obj.errMsg : obj);
+			}
+		}
+
+		/** ?wxDebug=1 时弹窗 + 页内条展示完整 errMsg(微信 debug 弹窗有时信息不全) */
+		function reportWxDebug(stage, payload, extraLines) {
+			var lines = ['【' + stage + '】'];
+			if (extraLines && extraLines.length) {
+				for (var i = 0; i < extraLines.length; i++) lines.push(extraLines[i]);
+			}
+			if (payload != null) {
+				lines.push(stringifyWxDebugPayload(payload));
+			}
+			var text = lines.join('\n');
+			wxDebugLastStatus = text;
+			console.log('[wxDebug]', text);
+			if (!isWxDebugOn()) return;
+			var bar = document.getElementById('wxDebugBar');
+			if (bar) {
+				bar.style.display = 'block';
+				bar.textContent = text;
+			}
+			if (isWeChatInAppBrowser()) {
+				window.alert(text.slice(0, 900));
+			}
+		}
+
 		/**
 		/**
 		 * 关店(businessStatus=99)更多推荐:POST …/ai/multimodal-services/api/v1/search/global/store-recommend
 		 * 关店(businessStatus=99)更多推荐:POST …/ai/multimodal-services/api/v1/search/global/store-recommend
-		 * 与 shareCheckInUndefined.html 一致;请求体 userCity/userLat/userLng 优先取 URL:userCity、lat、lon(兼认 userLat/userLng、weidu/jingdu 等)。
+		 * 与 shareCheckInUndefined.html 一致;请求体 page、pageSize、storeId、userCity、userLat、userLng 可由 URL 覆盖
 		 */
 		 */
 		var API_LIFE_AI_BASE = 'http://183.252.196.135:9100';
 		var API_LIFE_AI_BASE = 'http://183.252.196.135:9100';
 		var STORE_GLOBAL_RECOMMEND_PATH =
 		var STORE_GLOBAL_RECOMMEND_PATH =
 			'/ai/multimodal-services/api/v1/search/global/store-recommend';
 			'/ai/multimodal-services/api/v1/search/global/store-recommend';
-		/** 关店推荐用户位置:优先 URL 的 lat、lon、userCity;缺省再用大连默认值 */
-		function getClosedRecommendUserLocation() {
-			var latRaw = (q('lat') || q('userLat') || q('latitude') || q('weidu') || '').trim();
-			var lonRaw = (q('lon') || q('userLng') || q('longitude') || q('lng') || q('jingdu') || '').trim();
-			var cityRaw = (q('userCity') || q('city') || '').trim();
-			return {
-				userLat:
-					latRaw !== '' && !isNaN(Number(latRaw)) ? Number(latRaw) : 38.925747,
-				userLng:
-					lonRaw !== '' && !isNaN(Number(lonRaw)) ? Number(lonRaw) : 121.662531,
-				userCity: cityRaw !== '' ? cityRaw : '大连市'
-			};
-		}
+		var DEFAULT_REC_USER_LAT = 38.925747;
+		var DEFAULT_REC_USER_LNG = 121.662531;
+		var DEFAULT_REC_USER_CITY = '大连市';
 
 
 		/**
 		/**
 		 * 与 group_user 一致:
 		 * 与 group_user 一致:
 		 * - manifest:app-plus.distribute.android.schemes、ios.urltypes → shopro(即 shopro://)
 		 * - manifest:app-plus.distribute.android.schemes、ios.urltypes → shopro(即 shopro://)
 		 * - Android 包名:manifest 未写 packagename 时与 App.vue 升级配置 androidPackageName 一致(云打包以 HBuilderX 为准)
 		 * - Android 包名:manifest 未写 packagename 时与 App.vue 升级配置 androidPackageName 一致(云打包以 HBuilderX 为准)
 		 */
 		 */
-		var APP_ANDROID_PACKAGE = 'com.alien.Udianzaizhe';
+		var APP_ANDROID_PACKAGE = 'com.alien.Udianzaina';
 		var APP_IOS_URL_SCHEME = 'shopro://';
 		var APP_IOS_URL_SCHEME = 'shopro://';
 		/** businessSection 未传或其它值时的默认落地页(与 1 相同) */
 		/** businessSection 未传或其它值时的默认落地页(与 1 相同) */
 		var APP_UNI_STORE_PATH = 'pages/ieisureEntertainment/eightTypeList/delicacyDetails';
 		var APP_UNI_STORE_PATH = 'pages/ieisureEntertainment/eightTypeList/delicacyDetails';
@@ -1046,6 +1168,47 @@
 			}
 			}
 		}
 		}
 
 
+		function showFabToast(msg, ms) {
+			var tip = String(msg || '').trim();
+			if (!tip) return;
+			var el = document.getElementById('openAppToast');
+			if (el) {
+				el.textContent = tip;
+				el.style.display = 'block';
+				if (showFabToast._timer) clearTimeout(showFabToast._timer);
+				showFabToast._timer = setTimeout(function () {
+					el.style.display = 'none';
+				}, ms || 3200);
+			}
+			console.log('[openApp]', tip);
+		}
+
+		function showAppOpenFailTip(msg) {
+			var tip =
+				msg ||
+				'未能打开 App,请确认已安装最新版「U店在哪」;微信内请使用页面底部按钮打开。';
+			showFabToast(tip, 4000);
+			if (typeof uni !== 'undefined' && typeof uni.showToast === 'function') {
+				uni.showToast({ title: tip, icon: 'none', duration: 2800 });
+			} else if (isWeChatInAppBrowser() && isWxDebugOn()) {
+				window.alert(tip);
+			}
+		}
+
+		/** 与公众平台「JS接口安全域名」一致;分享页实际打开的 host 必须在此列表中 */
+		var WECHAT_JS_SAFE_HOSTS = ['uat.ailien.shop', 'test.ailien.shop'];
+
+		function checkWxJsSafeDomainHint() {
+			var host = String(location.hostname || '').toLowerCase();
+			if (!host || host === 'localhost' || host === '127.0.0.1') return '';
+			if (WECHAT_JS_SAFE_HOSTS.indexOf(host) >= 0) return '';
+			return (
+				'当前页面域名为「' +
+				host +
+				'」,未在 WECHAT_JS_SAFE_HOSTS 中;请在公众号后台「JS接口安全域名」添加该域名(不含 https:// 与路径)。'
+			);
+		}
+
 		/** 详情页 onLoad 使用 storeId;分享链常为 id,补全 storeId 避免 App 内参数为空 */
 		/** 详情页 onLoad 使用 storeId;分享链常为 id,补全 storeId 避免 App 内参数为空 */
 		function buildAppOpenQueryString() {
 		function buildAppOpenQueryString() {
 			var raw = (location.search && location.search.charAt(0) === '?') ? location.search.slice(1) : (location.search || '');
 			var raw = (location.search && location.search.charAt(0) === '?') ? location.search.slice(1) : (location.search || '');
@@ -1090,27 +1253,537 @@
 			return /MicroMessenger/i.test(navigator.userAgent || '');
 			return /MicroMessenger/i.test(navigator.userAgent || '');
 		}
 		}
 
 
-		/** 部分 WebView 对 location.href 拦截,用 a 标签点击略稳 */
-		function launchAppDeepLink(deepLink) {
-			
+		function getWeChatJssdkSignRequestUrl() {
+			var custom = String(q('wxSignPath') || q('wxSignUrl') || '').trim();
+			if (!custom) return '';
+			if (/^https?:\/\//i.test(custom)) return custom;
+			return API_BASE.replace(/\/$/, '') + (custom.charAt(0) === '/' ? custom : '/' + custom);
+		}
+
+		/** 微信 JSSDK 参与签名的 url:完整链(含 ?),不含 # */
+		function getWxSignUrlFull() {
+			var forced = String(q('wxSignUrl') || '').trim();
+			if (forced) return forced.split('#')[0];
+			if (location.origin && /^https?:/i.test(location.origin)) {
+				return (
+					location.origin +
+					(location.pathname || '/h5/HBuilderProjects/shareIndex.html') +
+					(location.search || '')
+				);
+			}
+			var href = String(location.href || '').split('#')[0];
+			if (href && href.indexOf('file:') !== 0) return href;
+			return WX_GET_CONFIG_SIGN_URL;
+		}
+
+		/** 无 query 的页面地址(后端若只签该地址,须配合 syncBrowserUrlForWxSign) */
+		function getWxSignUrlBase() {
+			if (location.origin && /^https?:/i.test(location.origin)) {
+				return location.origin + (location.pathname || '/h5/HBuilderProjects/shareIndex.html');
+			}
+			return WX_GET_CONFIG_SIGN_URL;
+		}
+
+		function getWxConfigSignUrl() {
+			if (String(q('wxSignBaseOnly') || '') === '1') return getWxSignUrlBase();
+			return getWxSignUrlFull();
+		}
+
+		/**
+		 * 微信校验签名用的是地址栏当前 url(不含#)。
+		 * 若后端只签了无参地址,须先把地址栏 sync 成与签名 url 一致再 wx.config。
+		 */
+		function syncBrowserUrlForWxSign(signUrl) {
+			var target = String(signUrl || '').split('#')[0];
+			if (!target) return;
+			var hash = location.hash || '';
+			var current = String(location.href || '').split('#')[0];
+			if (current !== target) {
+				history.replaceState(history.state, document.title, target + hash);
+			}
+		}
+
+		/** 唤起 App 的 extinfo:保留完整分享链 */
+		function getPageUrlForWxSign() {
+			return getWxSignUrlFull();
+		}
+
+		function buildWxGetConfigRequestUrl(signPageUrl) {
+			var base = API_BASE.replace(/\/$/, '');
+			return (
+				base +
+				WECHAT_GET_WX_CONFIG_PATH +
+				'?url=' +
+				encodeURIComponent(signPageUrl || getWxConfigSignUrl())
+			);
+		}
+
+		/**
+		 * 微信开放标签 extinfo:优先传当前 H5 分享链(与 App 解析分享 URL 一致);
+		 * 超长时再传 path+query JSON,最后才用 shopro://。
+		 */
+		function buildWeChatLaunchExtinfo() {
+			var pageUrl = getPageUrlForWxSign();
+			if (pageUrl && pageUrl.length <= 1024) return pageUrl;
+			var path = getAppUniPathForBusinessSection().replace(/^\//, '');
+			var qs = buildAppOpenQueryString().replace(/^\?/, '');
 			try {
 			try {
-				var a = document.createElement('a');
-				a.href = deepLink;
-				a.setAttribute('target', '_self');
-				document.body.appendChild(a);
-				a.click();
-				document.body.removeChild(a);
-			} catch (e1) {}
+				var pack = JSON.stringify({
+					path: path,
+					query: qs,
+					h5: pageUrl
+				});
+				if (pack.length <= 1024) return pack;
+			} catch (eJson) {}
+			var deep = buildAppDeepLink();
+			if (deep && deep.length <= 1024) return deep;
+			return pageUrl ? pageUrl.slice(0, 1024) : deep.slice(0, 1024);
+		}
+
+		function absH5AssetUrl(rel) {
+			rel = String(rel || '').replace(/^\//, '');
+			var origin = location.origin;
+			if (origin && origin !== 'null' && /^https?:/i.test(origin)) {
+				var dir = location.pathname.replace(/[^/]*$/, '');
+				return origin + dir + rel;
+			}
+			return H5_PAGE_BASE_FALLBACK + rel;
+		}
+
+		function resolveWxConfigAppIdFromSignData(d) {
+			if (!d || typeof d !== 'object') return '';
+			var mp =
+				d.mpAppId ||
+				d.mpAppid ||
+				d.officialAppId ||
+				d.officialAccountAppId ||
+				d.gzhAppId ||
+				d.serviceAppId;
+			if (mp != null && String(mp).trim() !== '') return String(mp).trim();
+			var fromQuery = String(q('wxMpAppId') || WECHAT_MP_APP_ID || '').trim();
+			if (fromQuery) return fromQuery;
+			var raw = d.appId || d.appid || d.wxAppId;
+			if (raw == null || String(raw).trim() === '') return '';
+			return String(raw).trim();
+		}
+
+		function normalizeWxJssdkSignPayload(res, signUrlUsed) {
+			if (!res || typeof res !== 'object') return null;
+			var d = res.data != null && typeof res.data === 'object' ? res.data : res;
+			if (!d || typeof d !== 'object') return null;
+			var appId = resolveWxConfigAppIdFromSignData(d);
+			var timestamp = d.timestamp != null ? d.timestamp : d.timeStamp;
+			var nonceStr = d.nonceStr || d.noncestr || d.nonce;
+			var signature = d.signature || d.sign;
+			if (!appId || timestamp == null || !nonceStr || !signature) return null;
+			var signedUrl =
+				d.signUrl != null && String(d.signUrl).trim() !== ''
+					? String(d.signUrl).trim().split('#')[0]
+					: signUrlUsed != null && String(signUrlUsed).trim() !== ''
+						? String(signUrlUsed).trim().split('#')[0]
+						: '';
+			return {
+				appId: String(appId),
+				timestamp: Number(timestamp),
+				nonceStr: String(nonceStr),
+				signature: String(signature),
+				signUrl: signedUrl
+			};
+		}
+
+		function getWxErrMsg(err) {
+			if (!err) return '';
+			if (err.errMsg) return String(err.errMsg);
+			if (typeof err === 'string') return err;
+			try {
+				return JSON.stringify(err);
+			} catch (e) {
+				return String(err);
+			}
+		}
+
+		function formatWxConfigErrorTip(err, signPageUrl) {
+			var errMsg = getWxErrMsg(err);
+			var tip = '微信 JSSDK 配置失败';
+			if (/require\s*subscribe/i.test(errMsg)) {
+				return (
+					tip +
+					':' +
+					errMsg +
+					'。测试号须用当前微信号在 mp.weixin.qq.com/debug.cgi 关注后再打开本页。'
+				);
+			}
+			if (/invalid signature/i.test(errMsg)) {
+				var signUrl = signPageUrl || getWxSignUrlFull();
+				var pageNow = String(location.href || '').split('#')[0];
+				return (
+					tip +
+					':签名无效。请让后端用与地址栏完全相同的 url 参与签名(不含#)。当前地址栏:' +
+					pageNow +
+					';本次请求签名 url:' +
+					signUrl
+				);
+			}
+			if (/invalid appid/i.test(errMsg)) {
+				return tip + ':appId 无效,请使用已认证服务号的公众号 AppID。';
+			}
+			if (/invalid url/i.test(errMsg)) {
+				return tip + ':域名未在公众号 JS 接口安全域名中,或未在开放平台绑定域名与移动应用。';
+			}
+			if (errMsg) return tip + ':' + errMsg;
+			return tip + ',开放标签不可用。';
+		}
+
+		function fetchWeChatJssdkSign(signPageUrl) {
+			signPageUrl = signPageUrl || getWxConfigSignUrl();
+			var urls = [];
+			var custom = getWeChatJssdkSignRequestUrl();
+			if (custom) {
+				var sep0 = custom.indexOf('?') >= 0 ? '&' : '?';
+				urls.push(custom + sep0 + 'url=' + encodeURIComponent(signPageUrl));
+			} else {
+				urls.push(buildWxGetConfigRequestUrl(signPageUrl));
+			}
+			function tryUrl(idx) {
+				if (idx >= urls.length) return Promise.resolve(null);
+				return fetch(urls[idx], {
+					method: 'GET',
+					mode: 'cors',
+					credentials: 'omit',
+					headers: { Accept: 'application/json' }
+				})
+					.then(function (r) {
+						if (!r.ok) throw new Error('HTTP ' + r.status);
+						return r.json();
+					})
+					.then(function (res) {
+						if (res && res.code != null) {
+							var c = Number(res.code);
+							if (c !== 200 && c !== 0 && res.success !== true) {
+								throw new Error(res.msg || 'sign rejected');
+							}
+						}
+						var pack = normalizeWxJssdkSignPayload(res, signPageUrl);
+						if (!pack) throw new Error('empty sign payload');
+						return pack;
+					})
+					.catch(function () {
+						return tryUrl(idx + 1);
+					});
+			}
+			return tryUrl(0);
+		}
+
+		function applyWxConfig(sign, signPageUrl) {
+			var urlForWx =
+				sign.signUrl && String(sign.signUrl).trim() !== ''
+					? String(sign.signUrl).trim()
+					: signPageUrl;
+			syncBrowserUrlForWxSign(urlForWx);
+			console.log('[wx.config] appId=', sign.appId, 'signUrl=', urlForWx, 'page=', location.href.split('#')[0]);
+
+			return new Promise(function (resolve) {
+				wx.config({
+					debug: String(q('wxDebug') || '') === '1',
+					appId: sign.appId,
+					timestamp: sign.timestamp,
+					nonceStr: sign.nonceStr,
+					signature: sign.signature,
+					jsApiList: ['checkJsApi'],
+					openTagList: ['wx-open-launch-app']
+				});
+				wx.ready(function () {
+					weChatJssdkConfigured = true;
+					reportWxDebug('wx.ready / config:ok', { errMsg: 'config:ok' }, [
+						'appId(config)=' + sign.appId,
+						'openTag appid=' + WECHAT_OPEN_APP_ID,
+						'signUrl=' + urlForWx,
+						'page=' + location.href.split('#')[0],
+						'开放标签层=' + (isWeChatInAppBrowser() ? '已启用' : '非微信')
+					]);
+					if (wxSignUrlFullBeforeStrip) {
+						var restore = wxSignUrlFullBeforeStrip + (location.hash || '');
+						history.replaceState(history.state, document.title, restore);
+						wxSignUrlFullBeforeStrip = '';
+					}
+					scheduleWxOpenTagRefresh();
+					resolve(true);
+				});
+				wx.error(function (err) {
+					var errMsg = getWxErrMsg(err);
+					console.warn('[wx.config]', err, 'signUrl=', urlForWx);
+					reportWxDebug('wx.error / config:fail', err, [
+						'完整 errMsg=' + errMsg,
+						'appId(config)=' + sign.appId,
+						'signUrl=' + urlForWx,
+						'page=' + location.href.split('#')[0]
+					]);
+					var baseUrl = getWxSignUrlBase();
+					var fullUrl = getWxSignUrlFull();
+					if (
+						!/invalid signature/i.test(errMsg) ||
+						wxConfigSignRetriedBaseUrl ||
+						baseUrl === fullUrl ||
+						urlForWx === baseUrl
+					) {
+						weChatJssdkConfigured = false;
+						updateFabOpenLayer();
+						showAppOpenFailTip(formatWxConfigErrorTip(err, urlForWx));
+						resolve(false);
+						return;
+					}
+					wxConfigSignRetriedBaseUrl = true;
+					wxSignUrlFullBeforeStrip = fullUrl;
+					console.warn('[wx.config] retry with base url (no query):', baseUrl);
+					fetchWeChatJssdkSign(baseUrl)
+						.then(function (sign2) {
+							if (!sign2) {
+								weChatJssdkConfigured = false;
+								updateFabOpenLayer();
+								showAppOpenFailTip(formatWxConfigErrorTip(err, urlForWx));
+								resolve(false);
+								return;
+							}
+							applyWxConfig(sign2, baseUrl).then(resolve);
+						})
+						.catch(function () {
+							weChatJssdkConfigured = false;
+							updateFabOpenLayer();
+							showAppOpenFailTip(formatWxConfigErrorTip(err, urlForWx));
+							resolve(false);
+						});
+				});
+			});
+		}
+
+		function tryTriggerWxOpenLaunchApp() {
+			var tag = document.getElementById('launch-btn');
+			if (!tag) return false;
+			try {
+				if (tag.shadowRoot) {
+					var inner = tag.shadowRoot.querySelector('button');
+					if (inner && typeof inner.click === 'function') {
+						inner.click();
+						return true;
+					}
+				}
+				if (typeof tag.click === 'function') {
+					tag.click();
+					return true;
+				}
+			} catch (eWx) {
+				console.warn('[wx-open-launch-app] trigger', eWx);
+			}
+			return false;
+		}
+
+		function refreshWxLaunchTagAttrs() {
+			var tag = document.getElementById('launch-btn');
+			if (!tag) return;
+			try {
+				if (WECHAT_OPEN_APP_ID) tag.setAttribute('appid', WECHAT_OPEN_APP_ID);
+				tag.setAttribute('extinfo', buildWeChatLaunchExtinfo());
+			} catch (eAttr) {
+				console.warn('[wx-open-launch-app] set attrs', eAttr);
+			}
+		}
+
+		/** wx.config 成功后即切换为开放标签按钮(不再依赖 offsetHeight 检测) */
+		function updateFabOpenLayer() {
+			var dock = document.getElementById('fabDock');
+			var hit = document.getElementById('fabWxHit');
+			var useWx = isWeChatInAppBrowser() && weChatJssdkConfigured;
+			if (dock) {
+				if (useWx) dock.classList.add('wx-open-enabled');
+				else dock.classList.remove('wx-open-enabled');
+			}
+			if (hit) hit.setAttribute('aria-hidden', useWx ? 'false' : 'true');
+			if (useWx) refreshWxLaunchTagAttrs();
+		}
+
+		function scheduleWxOpenTagRefresh() {
+			refreshWxLaunchTagAttrs();
+			updateFabOpenLayer();
+			var delays = [100, 400, 1000, 2000];
+			for (var i = 0; i < delays.length; i++) {
+				(function (ms) {
+					window.setTimeout(function () {
+						if (!weChatJssdkConfigured) return;
+						refreshWxLaunchTagAttrs();
+						updateFabOpenLayer();
+					}, ms);
+				})(delays[i]);
+			}
+		}
+
+		var openAppFabClickLock = 0;
+		function handleOpenAppFabClick() {
+			var now = Date.now();
+			if (now - openAppFabClickLock < 500) return;
+			openAppFabClickLock = now;
+			tryOpenHBuilderApp();
+		}
+
+		function bindOpenAppFabClick() {
+			var btn = document.getElementById('openApp');
+			if (btn) {
+				btn.addEventListener('click', function (e) {
+					if (e && e.cancelable) e.preventDefault();
+					handleOpenAppFabClick();
+				});
+			}
+		}
+
+		function bindWeChatLaunchTagEvents() {
+			var tag = document.getElementById('launch-btn');
+			if (!tag) return;
+			refreshWxLaunchTagAttrs();
+			tag.addEventListener('launch', function (e) {
+				console.log('[wx-open-launch-app] launch ok', e && e.detail);
+				reportWxDebug('wx-open-launch-app launch', e && e.detail ? e.detail : { errMsg: 'launch:ok' }, [
+					'extinfo=' + (tag.getAttribute('extinfo') || '').slice(0, 200)
+				]);
+			});
+			tag.addEventListener('error', function (e) {
+				var detail = e && e.detail;
+				console.warn('[wx-open-launch-app]', detail);
+				reportWxDebug('wx-open-launch-app error', detail, [
+					'openTag appid=' + WECHAT_OPEN_APP_ID,
+					'完整 errMsg=' +
+						(detail && detail.errMsg
+							? String(detail.errMsg)
+							: detail && detail.errmsg
+								? String(detail.errmsg)
+								: '(无)')
+				]);
+				var errMsg =
+					detail && detail.errMsg
+						? String(detail.errMsg)
+						: detail && detail.errmsg
+							? String(detail.errmsg)
+							: '';
+				if (/launch:fail/i.test(errMsg)) {
+					showAppOpenFailTip(
+						'微信未能唤起 App(' +
+							errMsg +
+							')。请确认:①已安装 U店在哪;②开放标签 appid 为移动应用 ID;③开放平台已绑定域名与 App。'
+					);
+				} else {
+					showAppOpenFailTip(
+						errMsg
+							? '开放标签错误:' + errMsg
+							: '开放标签唤起失败,请用 wxDebug=1 查看详情。'
+					);
+				}
+			});
+		}
+
+		/**
+		 * 进入页即 GET /wx/getWxConfig(任意浏览器都会请求,便于联调);
+		 * 仅微信内且签名成功后再 wx.config + 展示开放标签按钮。
+		 */
+		function initWeChatOpenLaunchApp() {
+			var inWx = isWeChatInAppBrowser();
+			wxConfigSignRetriedBaseUrl = false;
+			var signPageUrl = getWxConfigSignUrl();
+			var reqUrl = buildWxGetConfigRequestUrl(signPageUrl);
+
+			if (inWx) {
+				var domainHint = checkWxJsSafeDomainHint();
+				if (domainHint) {
+					console.warn('[wx]', domainHint);
+					showAppOpenFailTip(domainHint);
+				}
+			}
+
+			console.log('[wx] GET', reqUrl);
+
+			return fetchWeChatJssdkSign(signPageUrl)
+				.then(function (sign) {
+					if (!inWx) {
+						if (!sign) {
+							console.warn('[wx] getWxConfig empty (non-wechat preview)');
+						}
+						return false;
+					}
+					if (typeof wx === 'undefined') {
+						console.warn('[wx] jweixin not loaded');
+						return false;
+					}
+					bindWeChatLaunchTagEvents();
+					if (!sign) {
+						console.warn('[wx] GET ' + WECHAT_GET_WX_CONFIG_PATH + ' — empty or invalid sign payload');
+						showAppOpenFailTip(
+							'微信 JSSDK:getWxConfig 未返回可用的 appId/timestamp/nonceStr/signature。'
+						);
+						return false;
+					}
+					return applyWxConfig(sign, signPageUrl);
+				})
+				.catch(function (e) {
+					console.warn('[wx] init failed', e);
+					return false;
+				});
+		}
+
+		function isCustomAppSchemeUrl(url) {
+			var s = String(url || '').trim();
+			if (!s) return false;
+			if (/^https?:/i.test(s)) return false;
+			return /^[a-z][a-z0-9+.-]*:/i.test(s);
+		}
+
+		function launchAppDeepLink(deepLink) {
+			deepLink = String(deepLink || '').trim();
+			if (!deepLink) return;
+			if (isCustomAppSchemeUrl(deepLink)) {
+				var anchor = document.getElementById('appDeepLinkAnchor');
+				if (!anchor) {
+					anchor = document.createElement('a');
+					anchor.id = 'appDeepLinkAnchor';
+					anchor.setAttribute('aria-hidden', 'true');
+					anchor.style.cssText =
+						'position:fixed;left:-9999px;top:0;width:1px;height:1px;opacity:0;pointer-events:none;';
+					document.body.appendChild(anchor);
+				}
+				anchor.setAttribute('href', deepLink);
+				anchor.click();
+				return;
+			}
 			try {
 			try {
 				window.location.href = deepLink;
 				window.location.href = deepLink;
-			} catch (e2) {}
+			} catch (eHref) {}
 		}
 		}
 
 
 		/**
 		/**
 		 * 有 plus:检测 App 是否安装后 openURL;深链形如 shopro://{APP_UNI_STORE_PATH}?…
 		 * 有 plus:检测 App 是否安装后 openURL;深链形如 shopro://{APP_UNI_STORE_PATH}?…
-		 * 系统浏览器:唤起 scheme;微信内常拦截 scheme,需「在浏览器打开」。
+		 * 系统浏览器:scheme 唤起;微信内优先 wx-open-launch-app,失败再 scheme(不再弹「在浏览器中打开」)
 		 */
 		 */
 		function tryOpenHBuilderApp() {
 		function tryOpenHBuilderApp() {
+			var inWx = isWeChatInAppBrowser();
+			showFabToast('正在尝试打开 U店在哪…');
+
+			if (inWx && !weChatJssdkConfigured) {
+				showAppOpenFailTip(
+					'微信 JSSDK 未就绪:请先关注测试号并确保 wx.config 成功。链接可加 wxDebug=1 查看 config 报错。'
+				);
+				launchAppDeepLink(buildAppDeepLink());
+				return;
+			}
+
+			if (inWx && weChatJssdkConfigured) {
+				refreshWxLaunchTagAttrs();
+				updateFabOpenLayer();
+				if (tryTriggerWxOpenLaunchApp()) {
+					showFabToast('正在打开 U店在哪…');
+				} else {
+					showAppOpenFailTip(
+						'请点击底部 APP内打开 图片按钮;若仍无效,请用 wxDebug=1 查看开放标签是否 launch:fail。'
+					);
+				}
+				return;
+			}
+
 			var deepLink = buildAppDeepLink();
 			var deepLink = buildAppDeepLink();
 
 
 			if (typeof plus !== 'undefined' && plus.runtime) {
 			if (typeof plus !== 'undefined' && plus.runtime) {
@@ -1125,20 +1798,19 @@
 				} catch (e) {
 				} catch (e) {
 					console.warn(e);
 					console.warn(e);
 				}
 				}
-				if (installed === false) {
-					showDownloadTip();
-					return;
-				}
 				try {
 				try {
 					plus.runtime.openURL(deepLink);
 					plus.runtime.openURL(deepLink);
 				} catch (e2) {
 				} catch (e2) {
 					console.warn(e2);
 					console.warn(e2);
-					showDownloadTip();
+					if (installed === false) {
+						showDownloadTip();
+					} else if (!inWx) {
+						showAppOpenFailTip();
+					}
 				}
 				}
 				return;
 				return;
 			}
 			}
 
 
-			var t0 = Date.now();
 			var done = false;
 			var done = false;
 			function finish() {
 			function finish() {
 				if (done) return;
 				if (done) return;
@@ -1155,25 +1827,21 @@
 			document.addEventListener('visibilitychange', onVis);
 			document.addEventListener('visibilitychange', onVis);
 			window.addEventListener('pagehide', onHide);
 			window.addEventListener('pagehide', onHide);
 
 
-			if (isWeChatInAppBrowser()) {
-				window.alert('若点击后无法打开 App:请先点右上角「···」,选择「在浏览器中打开」,再点「APP内打开」。');
-			}
-
 			try {
 			try {
 				launchAppDeepLink(deepLink);
 				launchAppDeepLink(deepLink);
 			} catch (e3) {
 			} catch (e3) {
 				finish();
 				finish();
-				showDownloadTip();
+				if (!inWx) showAppOpenFailTip();
 				return;
 				return;
 			}
 			}
 
 
+			/**
+			 * 微信内 scheme 常被拦截且页面仍 visible,原 2.6s 后 showDownloadTip 会误报「请到应用商店下载」。
+			 * 与 shareDynamic 一致:超时仅收尾,不弹下载提示。
+			 */
 			window.setTimeout(function () {
 			window.setTimeout(function () {
-				if (done) return;
-				if (document.visibilityState === 'visible' && Date.now() - t0 < 3500) {
-					showDownloadTip();
-				}
 				finish();
 				finish();
-			}, 2600);
+			}, 3200);
 		}
 		}
 
 
 		function qs() {
 		function qs() {
@@ -1938,20 +2606,28 @@
 		}
 		}
 
 
 		function fetchShareClosedStoreRecommend(storeIdStr) {
 		function fetchShareClosedStoreRecommend(storeIdStr) {
-			var loc = getClosedRecommendUserLocation();
+			var latRaw = (q('userLat') || q('latitude') || q('lat') || q('weidu')).trim();
+			var lngRaw = (q('userLng') || q('longitude') || q('lon') || q('jingdu')).trim();
+			var userLat =
+				latRaw !== '' && !isNaN(Number(latRaw)) ? Number(latRaw) : DEFAULT_REC_USER_LAT;
+			var userLng =
+				lngRaw !== '' && !isNaN(Number(lngRaw)) ? Number(lngRaw) : DEFAULT_REC_USER_LNG;
 
 
 			var page = parseInt(q('page') || '1', 10);
 			var page = parseInt(q('page') || '1', 10);
 			var pageSize = parseInt(q('pageSize') || '10', 10);
 			var pageSize = parseInt(q('pageSize') || '10', 10);
 			if (isNaN(page) || page < 1) page = 1;
 			if (isNaN(page) || page < 1) page = 1;
 			if (isNaN(pageSize) || pageSize < 1) pageSize = 10;
 			if (isNaN(pageSize) || pageSize < 1) pageSize = 10;
 
 
+			var userCityRaw = (q('userCity') || q('city') || '').trim();
+			var userCity = userCityRaw !== '' ? userCityRaw : DEFAULT_REC_USER_CITY;
+
 			var body = {
 			var body = {
 				page: page,
 				page: page,
 				pageSize: pageSize,
 				pageSize: pageSize,
 				storeId: String(storeIdStr || ''),
 				storeId: String(storeIdStr || ''),
-				userCity: loc.userCity,
-				userLat: loc.userLat,
-				userLng: loc.userLng
+				userCity: userCity,
+				userLat: userLat,
+				userLng: userLng
 			};
 			};
 
 
 			return fetch(API_LIFE_AI_BASE + STORE_GLOBAL_RECOMMEND_PATH, {
 			return fetch(API_LIFE_AI_BASE + STORE_GLOBAL_RECOMMEND_PATH, {
@@ -2322,12 +2998,19 @@
 			run();
 			run();
 			bindMarketingMore();
 			bindMarketingMore();
 			bindStaffSection();
 			bindStaffSection();
-			var openBtn = document.getElementById('openApp');
-			if (openBtn) {
-				openBtn.addEventListener('click', function () {
-					tryOpenHBuilderApp();
-				});
-			}
+			var launchTag = document.getElementById('launch-btn');
+			if (launchTag && WECHAT_OPEN_APP_ID) {
+				launchTag.setAttribute('appid', WECHAT_OPEN_APP_ID);
+			}
+			bindOpenAppFabClick();
+			updateFabOpenLayer();
+			if (isWxDebugOn()) {
+				reportWxDebug('page load', { errMsg: 'wxDebug=1 已开启' }, [
+					'UA=' + (navigator.userAgent || '').slice(0, 120),
+					'page=' + location.href.split('#')[0]
+				]);
+			}
+			initWeChatOpenLaunchApp();
 		}
 		}
 
 
 		if (document.readyState === 'complete') {
 		if (document.readyState === 'complete') {