@@ -1,340 +0,0 @@
/* ============================================================
wx-auth.js · H5 应用通用授权 + 跳商城 共享助手(各 H5 应用引用即可)
------------------------------------------------------------
依赖:页面引入 <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
用法:在 H5 页面里
<script src="wx-auth.js"></script>
然后:
wxAuth.init(); // 检测环境 + 读取已授权资料(hash) + 回填
wxAuth.isAuthorized(); // 是否已授权(手机号/昵称/头像任一非空)
wxAuth.requestProfile(); // 点「授权登录」→ postMessage 通知宿主页
wxAuth.goMall(); // 点「进商城」→ 跳小程序商城 tabBar 页
wxAuth.goHome(); // 点「返回首页」→ 回「后台配置的首页」(带身份;别裸写 location.href)
wxAuth.identity(); // 读 openid/unionid( URL 优先 → 同源缓存兜底)
wxAuth.watchLocation(); // 换页/路由切换自动上报当前位置 → 宿主刷新分享卡片(推荐)
wxAuth.reportLocation({title}); // 手动上报一次当前页(标题/图可选)
分享卡片同步协议( location/share/setTitle/copyLink/requestProfile) 见
box_app/docs/h5-auth-standard.md 第四节
回传协议:宿主页授权成功后更新 web-view src 为
<H5地址>?t=<ts>#openid=..&phone=..&nickname=..&avatar=..
(微信 webview 丢 query、不丢 hash, 故授权结果放 hash)
============================================================ */
( function ( g ) {
'use strict' ;
function readHash ( name ) {
try {
var h = location . hash . slice ( 1 ) + '&' + location . search . slice ( 1 ) ;
var m = h . match ( new RegExp ( '(?:^|[&#])' + name + '=([^&#]*)(?:$|[&#])' ) ) ;
return m ? decodeURIComponent ( m [ 1 ] ) : '' ;
} catch ( e ) { return '' ; }
}
/* ============================================================
同源身份缓存 —— openid / unionid 的兜底通道( 2026-09-22)
为什么需要: H5 应用里「返回首页」这类跳转是**页面自己发起的同源跳转**,
宿主的 <web-view src> 属性不变,所以宿主那条 _applyIdentity() 注入**不会触发**,
目标页( mama-box) 拿到的 URL 里没有 openid → 只能显示「身份不完整」。
⚠️ 而且这一跳**永远不可能带上身份**:孕检日历那类应用会把 openid/phone
从自己的 URL 上主动擦掉(防分享链接冒名顶替,理由见它的注释)——
所以对第三方应用来说,这条缓存是**不改它们代码的唯一出路**。
做法: mp.yizhezhe.com 下所有应用与 mama-box 是**同源**(实测在架 12 个应用
全在该域名),共用一份 sessionStorage。宿主注入过的身份顺手存一份,
读取时 **URL 优先 → 缓存兜底**。
三条纪律(改这个文件前先读):
① **URL 永远权威**:只要 URL 里带 openid 就无条件覆盖缓存 ——
绝不做「有缓存就不读 URL」, 否则切号/换人后会串号;
② **只在本 origin 内有效**: App 表允许填外链,外链应用读不到这份缓存,
所以「跳转要带身份」的规范不能删,缓存只是兜底;
③ **只存 openid/unionid**(服务端身份键),不碰 token / 手机号 / 昵称头像。
存 sessionStorage 而不是 localStorage: 报告的场景都在同一次会话内,
sessionStorage 够用,且随标签页结束就没了,暴露窗口小得多。
============================================================ */
var ID _KEY = 'mmbox.identity' ;
var ID _TTL = 12 * 3600 * 1000 ; // 12h: sessionStorage 本就随会话结束,这层防的是长驻旧值
function rememberIdentity ( oid , uid ) {
if ( ! oid ) return ;
try {
sessionStorage . setItem ( ID _KEY , JSON . stringify ( { openid : oid , unionid : uid || '' , at : Date . now ( ) } ) ) ;
} catch ( e ) { /* 隐私模式等:静默,不影响主流程 */ }
}
function cachedIdentity ( ) {
try {
var c = JSON . parse ( sessionStorage . getItem ( ID _KEY ) || 'null' ) ;
if ( ! c || ! c . openid ) return null ;
if ( c . at && ( Date . now ( ) - c . at ) > ID _TTL ) return null ;
return c ;
} catch ( e ) { return null ; }
}
/* 读身份: URL 优先 → 缓存兜底。命中缓存时打一条日志(口径见接入规范第 5 节:
缓存会掩盖「宿主没注入身份」这个本来会显性提示的信号,所以留痕便于排查)。 */
function identity ( ) {
/* ⚠️ 与 mama-box 的 hashParam 同款护栏:历史 bug 会把缓存戳粘进 openid
( abc001?t=123) , 真 openid 不含 ?,剥掉以兼容被污染的旧链接。 */
var oid = String ( readHash ( 'openid' ) || '' ) . split ( '?' ) [ 0 ] ;
var uid = String ( readHash ( 'unionid' ) || '' ) . split ( '?' ) [ 0 ] ;
if ( oid ) { rememberIdentity ( oid , uid ) ; return { openid : oid , unionid : uid , from : 'url' } ; }
var c = cachedIdentity ( ) ;
if ( c ) {
try {
console . log ( '[wxAuth] URL 未带身份 → 用同源缓存兜底: openid=' + c . openid +
( c . unionid ? ' unionid=' + c . unionid : '' ) + '(写入于 ' + new Date ( c . at ) . toLocaleTimeString ( ) + ') ' ) ;
} catch ( e ) { }
return { openid : c . openid , unionid : c . unionid || '' , from : 'cache' } ;
}
return { openid : '' , unionid : '' , from : 'none' } ;
}
/* 载入即采一次:**接入方不需要改任何代码** —— 只要页面引了本库,
宿主注入过的身份就会被存下来(孕检日历那类第三方应用正是靠这一条自己存下身份的)。 */
( function captureAtLoad ( ) { try { identity ( ) ; } catch ( e ) { } } ) ( ) ;
var wxAuth = {
MODE : 'browser' , // 'webview' | 'wechat-browser' | 'browser'
inWebview : false ,
onReady : null ,
/* ① 环境检测 + 读回传资料(授权后宿主更新 src, 本页重载读到 hash) */
init : function ( cb ) {
this . onReady = cb || null ;
var self = this ;
var isWechat = /MicroMessenger/i . test ( navigator . userAgent ) ;
function setMode ( m ) {
self . MODE = m ; self . inWebview = ( m === 'webview' ) ;
if ( self . onReady ) self . onReady ( self ) ;
}
if ( window . wx && window . wx . miniProgram ) {
var responded = false ;
window . wx . miniProgram . getEnv ( function ( r ) {
responded = true ;
setMode ( r && r . miniprogram ? 'webview' : ( isWechat ? 'wechat-browser' : 'browser' ) ) ;
} ) ;
setTimeout ( function ( ) { if ( ! responded ) setMode ( isWechat ? 'wechat-browser' : 'browser' ) ; } , 800 ) ;
} else {
setMode ( isWechat ? 'wechat-browser' : 'browser' ) ;
}
} ,
/* ② 读取授权结果( hash 里的 phone/nickname/avatar) */
readAuth : function ( ) {
return { phone : readHash ( 'phone' ) , nickname : readHash ( 'nickname' ) , avatar : readHash ( 'avatar' ) } ;
} ,
/* ③ 是否已授权 */
isAuthorized : function ( ) {
var a = this . readAuth ( ) ;
return ! ! ( a . nickname || a . avatar || a . phone ) ;
} ,
/* ④ 请求授权: navigateTo → 小程序端独立授权页(默认 pages/annex/auth/index)。
改动说明:微信 web-view 的 postMessage 非实时送达,点击时端上收不到;
改用 wx.miniProgram.navigateTo 实时跳转到授权页,避开该限制。 */
/* ④ 请求授权:环境判定(与 goMall/mama-box 同一套):
① UA 不含 MicroMessenger = 普通浏览器 → 直接提示(秒出,不等超时);
② 微信内 → getEnv 确认是否 webview( 纯浏览器加载了 jweixin 时 getEnv
不回调, 800ms 超时兜底)→ 是小程序才 navigateTo 授权页。 */
requestProfile : function ( authPage ) {
var self = this ;
function nav ( ) {
/* 把「发起授权的当前 H5 地址」经 navigateTo query 带给授权页( backUrl) ——
与 openid 同一条可靠通道( navigateTo onLoad 必达),不依赖 postMessage
(微信只在后退/销毁/分享时批量派发,时序不可靠:宫格 location.href 换应用后,
宿主记的 shareHref 可能还是上一个应用的地址,授权结果被送回错的地方)。
授权页把 backUrl 写进 __AUTH_RESULT__, 宿主 onShow 优先用它做回传基址:
从哪个 H5 触发的授权,授权后就回到哪个 H5。 */
var url = authPage || '/pages/annex/auth/index' ;
var backUrl = '' ;
try { backUrl = location . href ; } catch ( e ) { }
if ( backUrl ) {
url += ( url . indexOf ( '?' ) !== - 1 ? '&' : '?' ) + 'backUrl=' + encodeURIComponent ( backUrl ) ;
}
window . wx . miniProgram . navigateTo ( {
url : url ,
/* 页面不存在 / 页面栈超限(>10层) 时 navigateTo 是静默失败 —— 必须自己兜住并提示,
否则用户只看到「正在打开授权页…」后再无反应,无从排查。 */
fail : function ( err ) {
var m = ( err && err . errMsg ) || '未知原因' ;
self . toast ( '打开授权页失败:' + m ) ;
try { console . warn ( '[wxAuth] navigateTo fail:' , m ) ; } catch ( e ) { }
}
} ) ;
self . toast ( '正在打开授权页…' ) ;
}
if ( ! /MicroMessenger/i . test ( navigator . userAgent ) ) {
this . toast ( '请在小程序中完成授权登录' ) ;
return ;
}
if ( window . wx && window . wx . miniProgram ) {
var responded = false ;
window . wx . miniProgram . getEnv ( function ( r ) {
responded = true ;
if ( r && r . miniprogram ) nav ( ) ;
else self . toast ( '请在小程序中完成授权登录' ) ;
} ) ;
setTimeout ( function ( ) {
if ( ! responded ) self . toast ( '请在小程序中完成授权登录' ) ;
} , 800 ) ;
} else {
this . toast ( '请在小程序中完成授权登录' ) ;
}
} ,
/* ⑤ 跳小程序商城( tabBar 页用 switchTab; 非 tabBar 用 navigateTo) */
/* ⑤ 跳商城:环境判定与 requestProfile 同一套 —— 仅小程序 webview 内 switchTab,
UA 非微信/微信内非 webview( 或 getEnv 不回调)都提示「请在小程序中打开商城」。
默认落点 260918 起为「商城」tab( /pages/index/mall, 原装装修首页恢复页) ,
与 app.js 的 goMall() 保持一致;此前是分类页 /pages/goods_cate/goods_cate。 */
goMall : function ( url ) {
var self = this ;
function go ( ) {
window . wx . miniProgram . switchTab ( {
url : url || '/pages/index/mall' ,
fail : function ( err ) {
var m = ( err && err . errMsg ) || '未知原因' ;
self . toast ( '打开商城失败:' + m ) ;
try { console . warn ( '[wxAuth] switchTab fail:' , m ) ; } catch ( e ) { }
}
} ) ;
}
if ( ! /MicroMessenger/i . test ( navigator . userAgent ) ) {
this . toast ( '请在小程序中打开商城' ) ;
return ;
}
if ( window . wx && window . wx . miniProgram ) {
var responded = false ;
window . wx . miniProgram . getEnv ( function ( r ) {
responded = true ;
if ( r && r . miniprogram ) go ( ) ;
else self . toast ( '请在小程序中打开商城' ) ;
} ) ;
setTimeout ( function ( ) {
if ( ! responded ) self . toast ( '请在小程序中打开商城' ) ;
} , 800 ) ;
} else {
this . toast ( '请在小程序中打开商城' ) ;
}
} ,
/* ⑥ 读当前身份( openid/unionid) : URL 优先 → 同源缓存兜底。
返回 {openid, unionid, from:'url'|'cache'|'none'}。
用途:本页自己也需要 openid 时(请求后端、拼跳转链接)统一走它,
顺带把身份写进缓存 —— 别在各应用里各写一份读参数的代码。 */
identity : identity ,
/* 手动把身份写进缓存( 一般不用调: URL 带身份时 identity() 已自动写)。
场景:授权回传后身份只在 hash 里出现一次,若随后要清 URL( 防分享泄露) ,
可先调它存一份。 */
rememberIdentity : rememberIdentity ,
/* ⑦ 回「后台配置的首页」( H5 方式) 。带身份: URL 优先,其次缓存。
⚠️ 「返回首页」这类跳转**不要**裸写 location.href='/mama-box/' ——
宿主不会因此重新注入身份( web-view src 没变),目标页会显示「身份不完整」,
而重试也救不回来。要么调本方法,要么保证链接带上 openid/unionid。
(第三方应用改不了的话,靠 identity() 的缓存兜底也能让首页认出人来。)
2026-10-09 起首页地址在 box_app 后台可定制:本方法先拉一次生效地址
( GET /api/mp/public/home/config, 同源无 CORS) 再跳; 超时/接口坏/值非法
→ 回退内置默认( 旧行为) , 「回首页」绝不被网络卡死( §5.6 部署顺序与降级)。
自定义首页 H5 落地后的「清 last_app」义务见接入规范第七节( mama-box 自带)。 */
goHome : function ( ) {
var id = identity ( ) ;
var q = id . openid
? 'openid=' + encodeURIComponent ( id . openid ) + ( id . unionid ? '&unionid=' + encodeURIComponent ( id . unionid ) : '' )
: '' ;
var DEFAULT _HOME = 'https://mp.yizhezhe.com/mama-box/' ;
/* 首页地址可能自带 query( 后台配置的) , 身份用 & 续在后面,别拼出两个 ? */
var jump = function ( home ) {
location . href = home + ( q ? ( home . indexOf ( '?' ) >= 0 ? '&' : '?' ) + q : '' ) ;
} ;
var okUrl = function ( u ) {
return typeof u === 'string' && /^https:\/\//i . test ( u ) && ! /\s/ . test ( u ) ;
} ;
try {
var done = false ;
var finish = function ( u ) {
if ( done ) return ;
done = true ;
jump ( okUrl ( u ) ? u : DEFAULT _HOME ) ;
} ;
var timer = setTimeout ( function ( ) { finish ( '' ) ; } , 1500 ) ;
var xhr = new XMLHttpRequest ( ) ;
xhr . open ( 'GET' , 'https://mp.yizhezhe.com/api/mp/public/home/config' , true ) ;
try { xhr . timeout = 1500 ; } catch ( e ) { }
xhr . onload = function ( ) {
clearTimeout ( timer ) ;
try {
var b = JSON . parse ( xhr . responseText || '{}' ) ;
/* ⚠️ 这个后端不存在路由时 HTTP 仍是 200, 错误码在 body.code —— 必须查 body */
finish ( b && b . code === 200 && b . result ? b . result . url : '' ) ;
} catch ( e ) { finish ( '' ) ; }
} ;
xhr . onerror = xhr . onabort = xhr . ontimeout = function ( ) { clearTimeout ( timer ) ; finish ( '' ) ; } ;
xhr . send ( ) ;
} catch ( e ) { jump ( DEFAULT _HOME ) ; }
} ,
/* ⑧ 展示授权结果到指定元素(可选便捷方法) */
fillAuth : function ( nameEl , avatarEl ) {
var a = this . readAuth ( ) ;
if ( nameEl && a . nickname ) nameEl . textContent = a . nickname ;
if ( avatarEl && a . avatar ) avatarEl . src = a . avatar ;
} ,
/* ⑨ 上报"当前在哪一页"(分享状态同步,不弹 toast、可频繁调用) 。
为什么需要: web-view 里的 H5 不能自己拉起分享面板,转发卡片由小程序宿主页产出;
宿主只有 onShareAppMessage(options).webViewUrl 能拿到当前地址,而
onShareTimeline( 朋友圈) **没有回调参数**;首页壳又不会随 H5 换页重新 onLoad。
所以应用在「页面/路由切换」时上报一次 location, 宿主即可刷新分享图/标题/深链。
协议:{action:'location', href, title?, summary?, imageUrl?} → 详见 docs/h5-auth-standard.md 第四节
opts.imageUrl 给了就以 H5 为准(仅当前应用);不给则用 box_app 后台配置的分享图。 */
reportLocation : function ( opts ) {
opts = opts || { } ;
if ( ! ( window . wx && window . wx . miniProgram && window . wx . miniProgram . postMessage ) ) return false ;
try {
window . wx . miniProgram . postMessage ( {
data : {
action : 'location' ,
href : opts . href || location . href , // 可传 href: 用于"即将跳转到的地址"上报
title : opts . title || '' ,
summary : opts . summary || '' ,
imageUrl : opts . imageUrl || ''
}
} ) ;
return true ;
} catch ( e ) {
try { console . warn ( '[wxAuth] reportLocation fail:' , e ) ; } catch ( e2 ) { }
return false ;
}
} ,
/* ⑧ 自动监听页面切换并上报(首屏一次 + hashchange/popstate + pushState/replaceState) 。
用法(推荐在 init 回调里调用):
wxAuth.watchLocation(function () {
return { title: document.title }; // 可选:本页分享标题/图
});
同一 href 只会发一次( 内部去重) , SPA 与多页应用都适用。 */
watchLocation : function ( getShare ) {
var self = this ;
var last = '' ;
function pull ( ) {
if ( location . href === last ) return ;
last = location . href ;
var s = ( typeof getShare === 'function' ) ? ( getShare ( ) || { } ) : { } ;
self . reportLocation ( s ) ;
}
pull ( ) ; // 首屏
if ( g . addEventListener ) {
g . addEventListener ( 'hashchange' , pull ) ;
g . addEventListener ( 'popstate' , pull ) ;
}
/* history.pushState/replaceState 不触发上面两个事件,需要包一层 */
[ 'pushState' , 'replaceState' ] . forEach ( function ( fn ) {
var orig = g . history && g . history [ fn ] ;
if ( typeof orig !== 'function' ) return ;
g . history [ fn ] = function ( ) {
var r = orig . apply ( this , arguments ) ;
setTimeout ( pull , 0 ) ;
return r ;
} ;
} ) ;
return self ;
} ,
toast : function ( m ) {
var t = document . getElementById ( 'toast' ) ;
if ( ! t ) return ;
t . textContent = m ; t . classList . add ( 'show' ) ;
clearTimeout ( t . _tt ) ; t . _tt = setTimeout ( function ( ) { t . classList . remove ( 'show' ) ; } , 1800 ) ;
}
} ;
g . wxAuth = wxAuth ;
} ) ( window ) ;