index.html 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
  6. <title>正在加载中</title>
  7. <style>
  8. html, body { margin: 0; padding: 0; background: #fff; }
  9. </style>
  10. </head>
  11. <body>
  12. <script>
  13. /**
  14. * 空白页:抬头「正在加载中」,拿到跳转链接后 location.replace。
  15. * 调用约定(每种进入场景只打 1 次对应接口):
  16. * - 首次进入无 openId → GET /mp/silent/authUrl,再跳转微信
  17. * - 微信回跳带 code → 只调 POST /mp/silent/resolve
  18. * - 再次进入已有 openId → 只调 POST /mp/silent/userInfo
  19. */
  20. var API_BASE = 'https://wx.jy.cc/apis';
  21. var APP_ID = 'wxe704b34112659eb1';
  22. var OPENID_KEY = 'mp_silent_openid';
  23. var BIZ_CACHE_KEY = 'mp_silent_biz_cache';
  24. var REQ_LOCK_KEY = 'mp_silent_req_lock';
  25. /** 同一次授权回跳若页面被加载两次,短时间内复用结果,避免 resolve + userInfo */
  26. var BIZ_CACHE_TTL_MS = 8000;
  27. var pageBooted = false;
  28. function qs(name) {
  29. var m = location.search.match(new RegExp('[?&]' + name + '=([^&]*)'));
  30. return m ? decodeURIComponent(m[1].replace(/\+/g, ' ')) : '';
  31. }
  32. function isWeChat() {
  33. return /MicroMessenger/i.test(navigator.userAgent || '');
  34. }
  35. function currentPageUrlWithoutQuery() {
  36. return location.origin + location.pathname;
  37. }
  38. function apiUrl(path) {
  39. var base = (API_BASE || '').replace(/\/$/, '');
  40. return base + path;
  41. }
  42. function normalizePath(pathname) {
  43. if (!pathname || pathname === '/') return '/';
  44. return pathname.replace(/\/$/, '');
  45. }
  46. function isSameAuthPage(url) {
  47. if (!url) return true;
  48. try {
  49. var u = new URL(url, location.href);
  50. return u.origin === location.origin
  51. && normalizePath(u.pathname) === normalizePath(location.pathname);
  52. } catch (e) {
  53. return false;
  54. }
  55. }
  56. function clearCodeFromUrl() {
  57. if (window.history && history.replaceState) {
  58. history.replaceState(null, '', location.pathname + (location.hash || ''));
  59. }
  60. }
  61. function finishOk(openId, jumpUrl) {
  62. clearCodeFromUrl();
  63. if (openId) {
  64. try { sessionStorage.setItem(OPENID_KEY, openId); } catch (e) {}
  65. }
  66. var url = (jumpUrl || '').replace(/^\s+|\s+$/g, '');
  67. if (url && !isSameAuthPage(url)) {
  68. location.replace(url);
  69. }
  70. // 无跳转链接:保持空白页,抬头仍为「正在加载中」
  71. }
  72. function saveBizCache(res) {
  73. try {
  74. sessionStorage.setItem(BIZ_CACHE_KEY, JSON.stringify({
  75. t: Date.now(),
  76. openId: res.openId || '',
  77. url: res.url || ''
  78. }));
  79. } catch (e) {}
  80. }
  81. function readBizCache(openId) {
  82. try {
  83. var raw = sessionStorage.getItem(BIZ_CACHE_KEY);
  84. if (!raw) return null;
  85. var o = JSON.parse(raw);
  86. if (!o || o.openId !== openId) return null;
  87. if (Date.now() - (o.t || 0) > BIZ_CACHE_TTL_MS) return null;
  88. return o;
  89. } catch (e) {
  90. return null;
  91. }
  92. }
  93. /** 页面级 + 短时存储锁,防止并发/双加载重复请求 */
  94. function tryLockRequest(kind) {
  95. if (window.__mpSilentReqing) {
  96. return false;
  97. }
  98. try {
  99. var now = Date.now();
  100. var raw = sessionStorage.getItem(REQ_LOCK_KEY);
  101. if (raw) {
  102. var o = JSON.parse(raw);
  103. if (o && o.kind === kind && now - (o.t || 0) < 3000) {
  104. return false;
  105. }
  106. }
  107. sessionStorage.setItem(REQ_LOCK_KEY, JSON.stringify({ kind: kind, t: now }));
  108. } catch (e) {}
  109. window.__mpSilentReqing = true;
  110. return true;
  111. }
  112. function unlockRequest() {
  113. window.__mpSilentReqing = false;
  114. }
  115. function applyBizRes(res) {
  116. if (!res || res.code !== 200 || !res.openId) {
  117. unlockRequest();
  118. return;
  119. }
  120. saveBizCache(res);
  121. unlockRequest();
  122. finishOk(res.openId, res.url || '');
  123. }
  124. function getJson(url) {
  125. return fetch(url, { method: 'GET', credentials: 'omit' }).then(function (res) {
  126. return res.json();
  127. });
  128. }
  129. function postJson(url, body) {
  130. return fetch(url, {
  131. method: 'POST',
  132. credentials: 'omit',
  133. headers: { 'Content-Type': 'application/json' },
  134. body: JSON.stringify(body || {})
  135. }).then(function (res) {
  136. return res.json();
  137. });
  138. }
  139. /** 调 authUrl 获取授权链接后跳转微信 */
  140. function goWechatOAuth(appId, state) {
  141. if (!tryLockRequest('authUrl')) {
  142. return;
  143. }
  144. var redirectUri = currentPageUrlWithoutQuery();
  145. var authQuery = 'redirectUri=' + encodeURIComponent(redirectUri);
  146. if (appId) authQuery += '&appId=' + encodeURIComponent(appId);
  147. if (state) authQuery += '&state=' + encodeURIComponent(state);
  148. getJson(apiUrl('/mp/silent/authUrl?' + authQuery)).then(function (res) {
  149. unlockRequest();
  150. if (!res || res.code !== 200 || !res.oauthUrl) {
  151. return;
  152. }
  153. location.replace(res.oauthUrl);
  154. }).catch(function () {
  155. unlockRequest();
  156. });
  157. }
  158. /** 只调 userInfo 一次 */
  159. function loadUserInfoOnce(openId, state) {
  160. var cached = readBizCache(openId);
  161. if (cached) {
  162. finishOk(cached.openId, cached.url);
  163. return;
  164. }
  165. if (!tryLockRequest('userInfo')) {
  166. return;
  167. }
  168. postJson(apiUrl('/mp/silent/userInfo'), {
  169. openId: openId,
  170. state: state || undefined
  171. }).then(applyBizRes).catch(function () {
  172. unlockRequest();
  173. });
  174. }
  175. /** 只调 resolve 一次 */
  176. function resolveOnce(code, appId, state) {
  177. var codeLockKey = 'mp_silent_code_' + code;
  178. try {
  179. if (sessionStorage.getItem(codeLockKey) === 'done') {
  180. clearCodeFromUrl();
  181. var openId = '';
  182. try { openId = sessionStorage.getItem(OPENID_KEY) || ''; } catch (e) {}
  183. if (openId) {
  184. loadUserInfoOnce(openId, state);
  185. }
  186. return;
  187. }
  188. if (sessionStorage.getItem(codeLockKey) === 'pending') {
  189. return;
  190. }
  191. sessionStorage.setItem(codeLockKey, 'pending');
  192. } catch (e) {}
  193. if (!tryLockRequest('resolve')) {
  194. return;
  195. }
  196. postJson(apiUrl('/mp/silent/resolve'), {
  197. code: code,
  198. appId: appId || undefined,
  199. state: state || undefined
  200. }).then(function (res) {
  201. if (!res || res.code !== 200 || !res.openId) {
  202. try { sessionStorage.removeItem(codeLockKey); } catch (e) {}
  203. unlockRequest();
  204. return;
  205. }
  206. try { sessionStorage.setItem(codeLockKey, 'done'); } catch (e) {}
  207. applyBizRes(res);
  208. }).catch(function () {
  209. try { sessionStorage.removeItem(codeLockKey); } catch (e2) {}
  210. unlockRequest();
  211. });
  212. }
  213. function start() {
  214. if (pageBooted) {
  215. return;
  216. }
  217. pageBooted = true;
  218. document.title = '正在加载中';
  219. if (!isWeChat()) {
  220. return;
  221. }
  222. var appId = qs('appId') || APP_ID;
  223. var state = qs('state') || '';
  224. var code = qs('code');
  225. if (code) {
  226. resolveOnce(code, appId, state);
  227. return;
  228. }
  229. var cachedOpenId = '';
  230. try { cachedOpenId = sessionStorage.getItem(OPENID_KEY) || ''; } catch (e) {}
  231. if (cachedOpenId && qs('force') !== '1') {
  232. loadUserInfoOnce(cachedOpenId, state);
  233. return;
  234. }
  235. goWechatOAuth(appId, state);
  236. }
  237. start();
  238. </script>
  239. </body>
  240. </html>