Selaa lähdekoodia

服务号判断加微人数加静默授权

吴树波 2 päivää sitten
vanhempi
commit
7b4e600c90

+ 136 - 83
fs-mp-app/deploy/index.html

@@ -3,33 +3,30 @@
 <head>
   <meta charset="UTF-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
-  <title>微信授权</title>
+  <title>正在加载中</title>
   <style>
-    body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f5f5; color: #333; }
-    .wrap { max-width: 480px; margin: 80px auto; padding: 24px; text-align: center; }
-    .tip { font-size: 15px; line-height: 1.6; color: #666; }
-    .err { color: #333; margin-top: 12px; word-break: break-all; }
-    .fail { color: #c00; }
+    html, body { margin: 0; padding: 0; background: #fff; }
   </style>
 </head>
 <body>
-  <div class="wrap">
-    <p class="tip" id="tip">正在授权,请稍候…</p>
-    <p class="err" id="err"></p>
-  </div>
   <script>
     /**
-     * 部署说明:
-     * 1) 同源部署:API_BASE 留空
-     * 2) Nginx 静态 + 独立 API:API_BASE 填后端地址
-     * 3) 公众号后台「网页授权域名」= 本页域名
-     * 4) 已有 openId 时也会调 /mp/silent/userInfo 刷新拉黑/加销售信息
+     * 空白页:抬头「正在加载中」,拿到跳转链接后 location.replace。
+     * 调用约定(每种进入场景只打 1 次对应接口):
+     * - 首次进入无 openId → GET /mp/silent/authUrl,再跳转微信
+     * - 微信回跳带 code → 只调 POST /mp/silent/resolve
+     * - 再次进入已有 openId → 只调 POST /mp/silent/userInfo
      */
     var API_BASE = 'https://wx.jy.cc/apis';
     var APP_ID = 'wxe704b34112659eb1';
     var OPENID_KEY = 'mp_silent_openid';
+    var BIZ_CACHE_KEY = 'mp_silent_biz_cache';
+    var REQ_LOCK_KEY = 'mp_silent_req_lock';
+    /** 同一次授权回跳若页面被加载两次,短时间内复用结果,避免 resolve + userInfo */
+    var BIZ_CACHE_TTL_MS = 8000;
+
+    var pageBooted = false;
 
-    function $(id) { return document.getElementById(id); }
     function qs(name) {
       var m = location.search.match(new RegExp('[?&]' + name + '=([^&]*)'));
       return m ? decodeURIComponent(m[1].replace(/\+/g, ' ')) : '';
@@ -63,43 +60,68 @@
         history.replaceState(null, '', location.pathname + (location.hash || ''));
       }
     }
-    function showError(msg) {
-      $('tip').textContent = '授权失败';
-      $('err').className = 'err fail';
-      $('err').textContent = msg || '未知错误';
-    }
-    function finishOk(openId, jumpUrl, tip, blacklisted) {
+    function finishOk(openId, jumpUrl) {
       clearCodeFromUrl();
       if (openId) {
         try { sessionStorage.setItem(OPENID_KEY, openId); } catch (e) {}
       }
       var url = (jumpUrl || '').replace(/^\s+|\s+$/g, '');
       if (url && !isSameAuthPage(url)) {
-        $('tip').textContent = '授权成功,正在跳转…';
-        $('err').textContent = '';
         location.replace(url);
-        return;
       }
-      $('tip').textContent = tip || '授权成功';
-      $('err').className = blacklisted ? 'err fail' : 'err';
-      $('err').textContent = '';
+      // 无跳转链接:保持空白页,抬头仍为「正在加载中」
+    }
+    function saveBizCache(res) {
+      try {
+        sessionStorage.setItem(BIZ_CACHE_KEY, JSON.stringify({
+          t: Date.now(),
+          openId: res.openId || '',
+          url: res.url || ''
+        }));
+      } catch (e) {}
+    }
+    function readBizCache(openId) {
+      try {
+        var raw = sessionStorage.getItem(BIZ_CACHE_KEY);
+        if (!raw) return null;
+        var o = JSON.parse(raw);
+        if (!o || o.openId !== openId) return null;
+        if (Date.now() - (o.t || 0) > BIZ_CACHE_TTL_MS) return null;
+        return o;
+      } catch (e) {
+        return null;
+      }
+    }
+    /** 页面级 + 短时存储锁,防止并发/双加载重复请求 */
+    function tryLockRequest(kind) {
+      if (window.__mpSilentReqing) {
+        return false;
+      }
+      try {
+        var now = Date.now();
+        var raw = sessionStorage.getItem(REQ_LOCK_KEY);
+        if (raw) {
+          var o = JSON.parse(raw);
+          if (o && o.kind === kind && now - (o.t || 0) < 3000) {
+            return false;
+          }
+        }
+        sessionStorage.setItem(REQ_LOCK_KEY, JSON.stringify({ kind: kind, t: now }));
+      } catch (e) {}
+      window.__mpSilentReqing = true;
+      return true;
+    }
+    function unlockRequest() {
+      window.__mpSilentReqing = false;
     }
     function applyBizRes(res) {
       if (!res || res.code !== 200 || !res.openId) {
-        showError((res && res.msg) || '获取用户信息失败');
+        unlockRequest();
         return;
       }
-      finishOk(res.openId, res.url || '', res.tip || '', !!res.blacklisted);
-    }
-    /** 已有 openId:每次进入都调后端刷新业务信息 */
-    function loadUserInfo(openId, state) {
-      $('tip').textContent = '正在获取信息…';
-      postJson(apiUrl('/mp/silent/userInfo'), {
-        openId: openId,
-        state: state || undefined
-      }).then(applyBizRes).catch(function (e) {
-        showError('网络异常:' + (e && e.message ? e.message : e));
-      });
+      saveBizCache(res);
+      unlockRequest();
+      finishOk(res.openId, res.url || '');
     }
     function getJson(url) {
       return fetch(url, { method: 'GET', credentials: 'omit' }).then(function (res) {
@@ -116,53 +138,52 @@
         return res.json();
       });
     }
-
-    function start() {
-      if (!isWeChat()) {
-        showError('请在微信中打开本页面');
+    /** 调 authUrl 获取授权链接后跳转微信 */
+    function goWechatOAuth(appId, state) {
+      if (!tryLockRequest('authUrl')) {
         return;
       }
-
-      var appId = qs('appId') || APP_ID;
-      var state = qs('state') || '';
-      var code = qs('code');
-
-      if (!code) {
-        var cachedOpenId = '';
-        try { cachedOpenId = sessionStorage.getItem(OPENID_KEY) || ''; } catch (e) {}
-        // 已有 openId 且非强制重新授权:仍调接口拿拉黑/加销售信息
-        if (cachedOpenId && qs('force') !== '1') {
-          loadUserInfo(cachedOpenId, state);
+      var redirectUri = currentPageUrlWithoutQuery();
+      var authQuery = 'redirectUri=' + encodeURIComponent(redirectUri);
+      if (appId) authQuery += '&appId=' + encodeURIComponent(appId);
+      if (state) authQuery += '&state=' + encodeURIComponent(state);
+      getJson(apiUrl('/mp/silent/authUrl?' + authQuery)).then(function (res) {
+        unlockRequest();
+        if (!res || res.code !== 200 || !res.oauthUrl) {
           return;
         }
-
-        var redirectUri = currentPageUrlWithoutQuery();
-        var authQuery = 'redirectUri=' + encodeURIComponent(redirectUri);
-        if (appId) authQuery += '&appId=' + encodeURIComponent(appId);
-        if (state) authQuery += '&state=' + encodeURIComponent(state);
-
-        getJson(apiUrl('/mp/silent/authUrl?' + authQuery)).then(function (res) {
-          if (!res || res.code !== 200 || !res.oauthUrl) {
-            showError((res && res.msg) || '获取授权链接失败');
-            return;
-          }
-          location.replace(res.oauthUrl);
-        }).catch(function (e) {
-          showError('网络异常:' + (e && e.message ? e.message : e));
-        });
+        location.replace(res.oauthUrl);
+      }).catch(function () {
+        unlockRequest();
+      });
+    }
+    /** 只调 userInfo 一次 */
+    function loadUserInfoOnce(openId, state) {
+      var cached = readBizCache(openId);
+      if (cached) {
+        finishOk(cached.openId, cached.url);
         return;
       }
-
-      // 同一 code 只换一次票;已换过则用 openId 再拉业务信息
+      if (!tryLockRequest('userInfo')) {
+        return;
+      }
+      postJson(apiUrl('/mp/silent/userInfo'), {
+        openId: openId,
+        state: state || undefined
+      }).then(applyBizRes).catch(function () {
+        unlockRequest();
+      });
+    }
+    /** 只调 resolve 一次 */
+    function resolveOnce(code, appId, state) {
       var codeLockKey = 'mp_silent_code_' + code;
       try {
         if (sessionStorage.getItem(codeLockKey) === 'done') {
-          var doneOpenId = '';
-          try { doneOpenId = sessionStorage.getItem(OPENID_KEY) || ''; } catch (e) {}
-          if (doneOpenId) {
-            loadUserInfo(doneOpenId, qs('state') || state);
-          } else {
-            showError('授权状态异常,请刷新重试');
+          clearCodeFromUrl();
+          var openId = '';
+          try { openId = sessionStorage.getItem(OPENID_KEY) || ''; } catch (e) {}
+          if (openId) {
+            loadUserInfoOnce(openId, state);
           }
           return;
         }
@@ -172,25 +193,57 @@
         sessionStorage.setItem(codeLockKey, 'pending');
       } catch (e) {}
 
-      var resolveState = qs('state') || state;
+      if (!tryLockRequest('resolve')) {
+        return;
+      }
       postJson(apiUrl('/mp/silent/resolve'), {
         code: code,
         appId: appId || undefined,
-        state: resolveState || undefined
+        state: state || undefined
       }).then(function (res) {
         if (!res || res.code !== 200 || !res.openId) {
           try { sessionStorage.removeItem(codeLockKey); } catch (e) {}
-          showError((res && res.msg) || '获取 openId 失败');
+          unlockRequest();
           return;
         }
         try { sessionStorage.setItem(codeLockKey, 'done'); } catch (e) {}
         applyBizRes(res);
-      }).catch(function (e) {
+      }).catch(function () {
         try { sessionStorage.removeItem(codeLockKey); } catch (e2) {}
-        showError('网络异常:' + (e && e.message ? e.message : e));
+        unlockRequest();
       });
     }
 
+    function start() {
+      if (pageBooted) {
+        return;
+      }
+      pageBooted = true;
+      document.title = '正在加载中';
+
+      if (!isWeChat()) {
+        return;
+      }
+
+      var appId = qs('appId') || APP_ID;
+      var state = qs('state') || '';
+      var code = qs('code');
+
+      if (code) {
+        resolveOnce(code, appId, state);
+        return;
+      }
+
+      var cachedOpenId = '';
+      try { cachedOpenId = sessionStorage.getItem(OPENID_KEY) || ''; } catch (e) {}
+      if (cachedOpenId && qs('force') !== '1') {
+        loadUserInfoOnce(cachedOpenId, state);
+        return;
+      }
+
+      goWechatOAuth(appId, state);
+    }
+
     start();
   </script>
 </body>

+ 2 - 0
fs-mp-app/src/main/java/com/fs/mp/service/impl/MpSilentAuthServiceImpl.java

@@ -73,6 +73,7 @@ public class MpSilentAuthServiceImpl implements MpSilentAuthService {
                 tip = tip + ":" + user.getManageRemark();
             }
             result.setTip(tip);
+            result.setUrl("https://work.weixin.qq.com/ca/cawcde696386f8e68e");
             log.info("[MpSilentAuthService] 用户已被拉黑 userId={}, manageRemark={}",
                     user.getUserId(), user.getManageRemark());
             return result;
@@ -85,6 +86,7 @@ public class MpSilentAuthServiceImpl implements MpSilentAuthService {
         result.setAddSalesCount(salesCount);
         result.setTip("添加销售:" + salesCount + "个");
         log.info("[MpSilentAuthService] 用户正常 userId={}, addSalesCount={}", user.getUserId(), salesCount);
+        result.setUrl("https://work.weixin.qq.com/ca/cawcde46821154e837");
         return result;
     }
 }

+ 136 - 83
fs-mp-app/src/main/resources/static/index.html

@@ -3,33 +3,30 @@
 <head>
   <meta charset="UTF-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
-  <title>微信授权</title>
+  <title>正在加载中</title>
   <style>
-    body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f5f5; color: #333; }
-    .wrap { max-width: 480px; margin: 80px auto; padding: 24px; text-align: center; }
-    .tip { font-size: 15px; line-height: 1.6; color: #666; }
-    .err { color: #333; margin-top: 12px; word-break: break-all; }
-    .fail { color: #c00; }
+    html, body { margin: 0; padding: 0; background: #fff; }
   </style>
 </head>
 <body>
-  <div class="wrap">
-    <p class="tip" id="tip">正在授权,请稍候…</p>
-    <p class="err" id="err"></p>
-  </div>
   <script>
     /**
-     * 部署说明:
-     * 1) 同源部署:API_BASE 留空
-     * 2) Nginx 静态 + 独立 API:API_BASE 填后端地址
-     * 3) 公众号后台「网页授权域名」= 本页域名
-     * 4) 已有 openId 时也会调 /mp/silent/userInfo 刷新拉黑/加销售信息
+     * 空白页:抬头「正在加载中」,拿到跳转链接后 location.replace。
+     * 调用约定(每种进入场景只打 1 次对应接口):
+     * - 首次进入无 openId → GET /mp/silent/authUrl,再跳转微信
+     * - 微信回跳带 code → 只调 POST /mp/silent/resolve
+     * - 再次进入已有 openId → 只调 POST /mp/silent/userInfo
      */
     var API_BASE = 'https://wx.jy.cc/apis';
     var APP_ID = 'wxe704b34112659eb1';
     var OPENID_KEY = 'mp_silent_openid';
+    var BIZ_CACHE_KEY = 'mp_silent_biz_cache';
+    var REQ_LOCK_KEY = 'mp_silent_req_lock';
+    /** 同一次授权回跳若页面被加载两次,短时间内复用结果,避免 resolve + userInfo */
+    var BIZ_CACHE_TTL_MS = 8000;
+
+    var pageBooted = false;
 
-    function $(id) { return document.getElementById(id); }
     function qs(name) {
       var m = location.search.match(new RegExp('[?&]' + name + '=([^&]*)'));
       return m ? decodeURIComponent(m[1].replace(/\+/g, ' ')) : '';
@@ -63,43 +60,68 @@
         history.replaceState(null, '', location.pathname + (location.hash || ''));
       }
     }
-    function showError(msg) {
-      $('tip').textContent = '授权失败';
-      $('err').className = 'err fail';
-      $('err').textContent = msg || '未知错误';
-    }
-    function finishOk(openId, jumpUrl, tip, blacklisted) {
+    function finishOk(openId, jumpUrl) {
       clearCodeFromUrl();
       if (openId) {
         try { sessionStorage.setItem(OPENID_KEY, openId); } catch (e) {}
       }
       var url = (jumpUrl || '').replace(/^\s+|\s+$/g, '');
       if (url && !isSameAuthPage(url)) {
-        $('tip').textContent = '授权成功,正在跳转…';
-        $('err').textContent = '';
         location.replace(url);
-        return;
       }
-      $('tip').textContent = tip || '授权成功';
-      $('err').className = blacklisted ? 'err fail' : 'err';
-      $('err').textContent = '';
+      // 无跳转链接:保持空白页,抬头仍为「正在加载中」
+    }
+    function saveBizCache(res) {
+      try {
+        sessionStorage.setItem(BIZ_CACHE_KEY, JSON.stringify({
+          t: Date.now(),
+          openId: res.openId || '',
+          url: res.url || ''
+        }));
+      } catch (e) {}
+    }
+    function readBizCache(openId) {
+      try {
+        var raw = sessionStorage.getItem(BIZ_CACHE_KEY);
+        if (!raw) return null;
+        var o = JSON.parse(raw);
+        if (!o || o.openId !== openId) return null;
+        if (Date.now() - (o.t || 0) > BIZ_CACHE_TTL_MS) return null;
+        return o;
+      } catch (e) {
+        return null;
+      }
+    }
+    /** 页面级 + 短时存储锁,防止并发/双加载重复请求 */
+    function tryLockRequest(kind) {
+      if (window.__mpSilentReqing) {
+        return false;
+      }
+      try {
+        var now = Date.now();
+        var raw = sessionStorage.getItem(REQ_LOCK_KEY);
+        if (raw) {
+          var o = JSON.parse(raw);
+          if (o && o.kind === kind && now - (o.t || 0) < 3000) {
+            return false;
+          }
+        }
+        sessionStorage.setItem(REQ_LOCK_KEY, JSON.stringify({ kind: kind, t: now }));
+      } catch (e) {}
+      window.__mpSilentReqing = true;
+      return true;
+    }
+    function unlockRequest() {
+      window.__mpSilentReqing = false;
     }
     function applyBizRes(res) {
       if (!res || res.code !== 200 || !res.openId) {
-        showError((res && res.msg) || '获取用户信息失败');
+        unlockRequest();
         return;
       }
-      finishOk(res.openId, res.url || '', res.tip || '', !!res.blacklisted);
-    }
-    /** 已有 openId:每次进入都调后端刷新业务信息 */
-    function loadUserInfo(openId, state) {
-      $('tip').textContent = '正在获取信息…';
-      postJson(apiUrl('/mp/silent/userInfo'), {
-        openId: openId,
-        state: state || undefined
-      }).then(applyBizRes).catch(function (e) {
-        showError('网络异常:' + (e && e.message ? e.message : e));
-      });
+      saveBizCache(res);
+      unlockRequest();
+      finishOk(res.openId, res.url || '');
     }
     function getJson(url) {
       return fetch(url, { method: 'GET', credentials: 'omit' }).then(function (res) {
@@ -116,53 +138,52 @@
         return res.json();
       });
     }
-
-    function start() {
-      if (!isWeChat()) {
-        showError('请在微信中打开本页面');
+    /** 调 authUrl 获取授权链接后跳转微信 */
+    function goWechatOAuth(appId, state) {
+      if (!tryLockRequest('authUrl')) {
         return;
       }
-
-      var appId = qs('appId') || APP_ID;
-      var state = qs('state') || '';
-      var code = qs('code');
-
-      if (!code) {
-        var cachedOpenId = '';
-        try { cachedOpenId = sessionStorage.getItem(OPENID_KEY) || ''; } catch (e) {}
-        // 已有 openId 且非强制重新授权:仍调接口拿拉黑/加销售信息
-        if (cachedOpenId && qs('force') !== '1') {
-          loadUserInfo(cachedOpenId, state);
+      var redirectUri = currentPageUrlWithoutQuery();
+      var authQuery = 'redirectUri=' + encodeURIComponent(redirectUri);
+      if (appId) authQuery += '&appId=' + encodeURIComponent(appId);
+      if (state) authQuery += '&state=' + encodeURIComponent(state);
+      getJson(apiUrl('/mp/silent/authUrl?' + authQuery)).then(function (res) {
+        unlockRequest();
+        if (!res || res.code !== 200 || !res.oauthUrl) {
           return;
         }
-
-        var redirectUri = currentPageUrlWithoutQuery();
-        var authQuery = 'redirectUri=' + encodeURIComponent(redirectUri);
-        if (appId) authQuery += '&appId=' + encodeURIComponent(appId);
-        if (state) authQuery += '&state=' + encodeURIComponent(state);
-
-        getJson(apiUrl('/mp/silent/authUrl?' + authQuery)).then(function (res) {
-          if (!res || res.code !== 200 || !res.oauthUrl) {
-            showError((res && res.msg) || '获取授权链接失败');
-            return;
-          }
-          location.replace(res.oauthUrl);
-        }).catch(function (e) {
-          showError('网络异常:' + (e && e.message ? e.message : e));
-        });
+        location.replace(res.oauthUrl);
+      }).catch(function () {
+        unlockRequest();
+      });
+    }
+    /** 只调 userInfo 一次 */
+    function loadUserInfoOnce(openId, state) {
+      var cached = readBizCache(openId);
+      if (cached) {
+        finishOk(cached.openId, cached.url);
         return;
       }
-
-      // 同一 code 只换一次票;已换过则用 openId 再拉业务信息
+      if (!tryLockRequest('userInfo')) {
+        return;
+      }
+      postJson(apiUrl('/mp/silent/userInfo'), {
+        openId: openId,
+        state: state || undefined
+      }).then(applyBizRes).catch(function () {
+        unlockRequest();
+      });
+    }
+    /** 只调 resolve 一次 */
+    function resolveOnce(code, appId, state) {
       var codeLockKey = 'mp_silent_code_' + code;
       try {
         if (sessionStorage.getItem(codeLockKey) === 'done') {
-          var doneOpenId = '';
-          try { doneOpenId = sessionStorage.getItem(OPENID_KEY) || ''; } catch (e) {}
-          if (doneOpenId) {
-            loadUserInfo(doneOpenId, qs('state') || state);
-          } else {
-            showError('授权状态异常,请刷新重试');
+          clearCodeFromUrl();
+          var openId = '';
+          try { openId = sessionStorage.getItem(OPENID_KEY) || ''; } catch (e) {}
+          if (openId) {
+            loadUserInfoOnce(openId, state);
           }
           return;
         }
@@ -172,25 +193,57 @@
         sessionStorage.setItem(codeLockKey, 'pending');
       } catch (e) {}
 
-      var resolveState = qs('state') || state;
+      if (!tryLockRequest('resolve')) {
+        return;
+      }
       postJson(apiUrl('/mp/silent/resolve'), {
         code: code,
         appId: appId || undefined,
-        state: resolveState || undefined
+        state: state || undefined
       }).then(function (res) {
         if (!res || res.code !== 200 || !res.openId) {
           try { sessionStorage.removeItem(codeLockKey); } catch (e) {}
-          showError((res && res.msg) || '获取 openId 失败');
+          unlockRequest();
           return;
         }
         try { sessionStorage.setItem(codeLockKey, 'done'); } catch (e) {}
         applyBizRes(res);
-      }).catch(function (e) {
+      }).catch(function () {
         try { sessionStorage.removeItem(codeLockKey); } catch (e2) {}
-        showError('网络异常:' + (e && e.message ? e.message : e));
+        unlockRequest();
       });
     }
 
+    function start() {
+      if (pageBooted) {
+        return;
+      }
+      pageBooted = true;
+      document.title = '正在加载中';
+
+      if (!isWeChat()) {
+        return;
+      }
+
+      var appId = qs('appId') || APP_ID;
+      var state = qs('state') || '';
+      var code = qs('code');
+
+      if (code) {
+        resolveOnce(code, appId, state);
+        return;
+      }
+
+      var cachedOpenId = '';
+      try { cachedOpenId = sessionStorage.getItem(OPENID_KEY) || ''; } catch (e) {}
+      if (cachedOpenId && qs('force') !== '1') {
+        loadUserInfoOnce(cachedOpenId, state);
+        return;
+      }
+
+      goWechatOAuth(appId, state);
+    }
+
     start();
   </script>
 </body>