fix(site): 官网 401 自动用 refresh_token 续签重试;资产加构建版本号破缓存
- 新增共享 auth.js(jiuAuth):access token 60 分钟过期后,checkout 提交/ 结果页轮询遇 401 自动 POST /auth/refresh 换新 token 并重试一次,仍失败 才跳登录——修复「已登录点提交订单却被踢回登录页」 - 登录判定放宽为 access 或 refresh token 任一存在(refresh 7 天有效) - 所有 JS/CSS 引用加 ?v=构建版本号:修复旧版 nav.js 被浏览器缓存后在新 页面执行(右上角错位的旧用户面板、带引号的 "张三" 显示) - nav.js 改经 jiuAuth 读 localStorage(JSON 解码,去掉引号显示) - 登录页回填上次的门店编号与账号 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
/* 构建期资产版本号:每次 build 变化,模板里作 ?v= 后缀破浏览器缓存,
|
||||
避免旧版 JS/CSS(同 URL)被缓存后在新版页面上执行。 */
|
||||
module.exports = { rev: Date.now().toString(36) };
|
||||
@@ -8,10 +8,12 @@
|
||||
{% if description %}<meta name="description" content="{{ description }}" />{% endif %}
|
||||
{# 真相源:tokens.css = App 原型 tokens.css 主题A 拷贝;site.css 镜像 atoms.css。
|
||||
全站零外链(无 Google Fonts / unpkg):字体走系统栈(与 App 真机一致),图标走内联雪碧图。 #}
|
||||
<link rel="stylesheet" href="/assets/tokens.css" />
|
||||
<link rel="stylesheet" href="/assets/site.css" />
|
||||
<link rel="stylesheet" href="/assets/tokens.css?v={{ build.rev }}" />
|
||||
<link rel="stylesheet" href="/assets/site.css?v={{ build.rev }}" />
|
||||
{% if pageExtraCss %}<link rel="stylesheet" href="{{ pageExtraCss }}" />{% endif %}
|
||||
<link rel="icon" href="/assets/logo-mark.svg" type="image/svg+xml" />
|
||||
{# auth.js 放 head 同步加载:页面内联脚本(checkout/登录态判断)在 body 中就要用 jiuAuth #}
|
||||
<script src="/assets/auth.js?v={{ build.rev }}"></script>
|
||||
{% if pageStyle %}<style>{{ pageStyle | safe }}</style>{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
@@ -22,6 +24,6 @@
|
||||
|
||||
{% include "footer.njk" %}
|
||||
|
||||
<script src="/assets/nav.js"></script>
|
||||
<script src="/assets/nav.js?v={{ build.rev }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/* 官网共享登录态助手:与 Web 版 App(shared_preferences_web)共享
|
||||
localStorage['flutter.*'](值为 JSON 编码)。access token 60 分钟过期,
|
||||
官网页面不常驻刷新,所以请求 401 时用 refresh_token 自动续签并重试一次,
|
||||
仍失败才交回调用方跳登录。零依赖。 */
|
||||
(function () {
|
||||
function get(key) {
|
||||
var raw = localStorage.getItem('flutter.' + key);
|
||||
if (raw === null) return null;
|
||||
try { return JSON.parse(raw); } catch (e) { return raw; }
|
||||
}
|
||||
function set(key, val) { localStorage.setItem('flutter.' + key, JSON.stringify(val)); }
|
||||
|
||||
function refresh(apiBase) {
|
||||
var rt = get('refresh_token');
|
||||
if (!rt) return Promise.resolve(false);
|
||||
return fetch(apiBase + '/api/v1/auth/refresh', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refresh_token: rt }),
|
||||
})
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (j) {
|
||||
var d = j && j.data;
|
||||
if (!d || !d.access_token) return false;
|
||||
set('access_token', d.access_token);
|
||||
if (d.refresh_token) set('refresh_token', d.refresh_token); // refresh token 会轮换
|
||||
return true;
|
||||
})
|
||||
.catch(function () { return false; });
|
||||
}
|
||||
|
||||
function authFetch(apiBase, path, opts) {
|
||||
opts = opts || {};
|
||||
function doFetch() {
|
||||
var headers = Object.assign({}, opts.headers, { Authorization: 'Bearer ' + get('access_token') });
|
||||
return fetch(apiBase + path, Object.assign({}, opts, { headers: headers }));
|
||||
}
|
||||
return doFetch().then(function (r) {
|
||||
if (r.status !== 401) return r;
|
||||
return refresh(apiBase).then(function (ok) { return ok ? doFetch() : r; });
|
||||
});
|
||||
}
|
||||
|
||||
/* 是否可视为登录:access 过期但 refresh 还在(7 天),authFetch 会自动续 */
|
||||
function loggedIn() { return !!(get('access_token') || get('refresh_token')); }
|
||||
|
||||
window.jiuAuth = { get: get, set: set, refresh: refresh, authFetch: authFetch, loggedIn: loggedIn };
|
||||
})();
|
||||
+5
-5
@@ -2,13 +2,13 @@
|
||||
localStorage['flutter.*'] 有登录信息 → CTA 变「进入系统」)。零依赖。 */
|
||||
(function () {
|
||||
function getAuthUser() {
|
||||
var token = localStorage.getItem('flutter.access_token');
|
||||
var username = localStorage.getItem('flutter.username');
|
||||
if (!token || !username) return null;
|
||||
if (!window.jiuAuth || !jiuAuth.loggedIn()) return null;
|
||||
var username = jiuAuth.get('username');
|
||||
if (!username) return null;
|
||||
return {
|
||||
username: username,
|
||||
realName: localStorage.getItem('flutter.real_name') || '',
|
||||
shopNo: localStorage.getItem('flutter.shop_no') || '',
|
||||
realName: jiuAuth.get('real_name') || '',
|
||||
shopNo: jiuAuth.get('shop_no') || '',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+5
-8
@@ -68,13 +68,9 @@ permalink: /checkout/
|
||||
(function () {
|
||||
var API = '{{ site.appBaseUrl }}';
|
||||
function $(id) { return document.getElementById(id); }
|
||||
function token() {
|
||||
var raw = localStorage.getItem('flutter.access_token');
|
||||
if (!raw) return null;
|
||||
try { return JSON.parse(raw); } catch (e) { return raw; }
|
||||
}
|
||||
function gotoLogin() { location.href = '/login/?next=' + encodeURIComponent('/checkout/' + location.search); }
|
||||
if (!token()) { gotoLogin(); return; }
|
||||
// access token 只有 60 分钟,只要 refresh token(7 天)还在就算登录,请求时自动续签
|
||||
if (!jiuAuth.loggedIn()) { gotoLogin(); return; }
|
||||
|
||||
/* 套餐目录:价格仅前端展示,实付金额以 pay 套餐表为准(下单响应回传) */
|
||||
var PLANS = {
|
||||
@@ -123,12 +119,13 @@ permalink: /checkout/
|
||||
$('coBtn').addEventListener('click', function () {
|
||||
$('coErr').classList.remove('show');
|
||||
var btn = $('coBtn'); btn.disabled = true; btn.textContent = '正在创建订单…';
|
||||
fetch(API + '/api/v1/license/purchase', {
|
||||
jiuAuth.authFetch(API, '/api/v1/license/purchase', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token() },
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ biz_code: PLANS[plan][cycle].biz }),
|
||||
})
|
||||
.then(function (r) {
|
||||
// authFetch 已自动用 refresh_token 续签重试过,仍 401 才是真过期
|
||||
if (r.status === 401) { gotoLogin(); throw new Error('登录已过期'); }
|
||||
return r.json().then(function (j) { return { ok: r.ok, j: j }; });
|
||||
})
|
||||
|
||||
+1
-1
@@ -276,7 +276,7 @@ description: 岩美酒库管理系统——每一瓶酒一个编号,审核驱
|
||||
/* 价格区登录态:已登录(与 Web 版 App 共享 localStorage flutter.*)时,
|
||||
标准/高级按钮 → 「购买 / 续费」进 /checkout/;免费版按钮 → 「进入系统」。 */
|
||||
(function () {
|
||||
if (!localStorage.getItem('flutter.access_token')) return;
|
||||
if (!window.jiuAuth || !jiuAuth.loggedIn()) return;
|
||||
document.querySelectorAll('[data-plan]').forEach(function (a) {
|
||||
var plan = a.dataset.plan;
|
||||
if (plan === 'free') {
|
||||
|
||||
@@ -38,20 +38,13 @@ permalink: /license/result/
|
||||
(function () {
|
||||
var API = '{{ site.appBaseUrl }}';
|
||||
function $(id) { return document.getElementById(id); }
|
||||
function token() {
|
||||
var raw = localStorage.getItem('flutter.access_token');
|
||||
if (!raw) return null;
|
||||
try { return JSON.parse(raw); } catch (e) { return raw; }
|
||||
}
|
||||
var otn = new URLSearchParams(location.search).get('out_trade_no');
|
||||
if (!otn || !token()) { $('waiting').style.display = 'none'; $('slow').style.display = 'block'; return; }
|
||||
if (!otn || !jiuAuth.loggedIn()) { $('waiting').style.display = 'none'; $('slow').style.display = 'block'; return; }
|
||||
|
||||
var tries = 0, maxTries = 30; // 2s × 30 = 60s
|
||||
function poll() {
|
||||
tries++;
|
||||
fetch(API + '/api/v1/license/purchase/' + encodeURIComponent(otn), {
|
||||
headers: { 'Authorization': 'Bearer ' + token() },
|
||||
})
|
||||
jiuAuth.authFetch(API, '/api/v1/license/purchase/' + encodeURIComponent(otn), {})
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (j) {
|
||||
var d = j && j.data;
|
||||
|
||||
@@ -76,6 +76,13 @@ permalink: /login/
|
||||
btn.disabled = false; btn.textContent = '登 录';
|
||||
});
|
||||
}
|
||||
// 登录过的浏览器回填门店编号/账号(token 过期重登时少敲两栏)
|
||||
if (window.jiuAuth) {
|
||||
var savedShop = jiuAuth.get('shop_no'), savedUser = jiuAuth.get('username');
|
||||
if (savedShop && !$('shop').value) $('shop').value = savedShop;
|
||||
if (savedUser && !$('user').value) $('user').value = savedUser;
|
||||
}
|
||||
|
||||
$('loginBtn').addEventListener('click', doLogin);
|
||||
['shop', 'user', 'pass'].forEach(function (id) {
|
||||
$(id).addEventListener('keydown', function (e) { if (e.key === 'Enter') doLogin(); });
|
||||
|
||||
Reference in New Issue
Block a user