Compare commits

...

3 Commits

Author SHA1 Message Date
wangjia e0f721117f chore: release site-v1.1.1
Deploy Site / release-deploy-site (push) Successful in 24s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-03 21:18:28 +08:00
wangjia c010d8b416 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
2026-07-03 21:08:13 +08:00
wangjia 71909a76d7 chore: release site-v1.1.0(版本序列对齐,内容同 1.0.56)
Deploy Site / release-deploy-site (push) Successful in 26s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-03 20:49:31 +08:00
9 changed files with 86 additions and 27 deletions
+10 -1
View File
@@ -6,7 +6,16 @@
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.56] - 2026-07-03
## [1.1.1] - 2026-07-03
### 修复
- 登录超过 1 小时后点「提交订单」被踢回登录页:官网现在会自动续签登录态(7 天内免重登)。
- 浏览器缓存旧版脚本导致页面右上角出现错位的旧用户面板、用户名显示多余引号。
### 改进
- 重新登录时自动回填上次的门店编号与账号。
## [1.1.0] - 2026-07-03
### 新功能
- 注册与登录直接在官网完成,登录后无缝进入 Web 版系统,不再跳转 App 页面。
+3
View File
@@ -0,0 +1,3 @@
/* 构建期资产版本号:每次 build 变化,模板里作 ?v= 后缀破浏览器缓存,
避免旧版 JS/CSS(同 URL)被缓存后在新版页面上执行。 */
module.exports = { rev: Date.now().toString(36) };
+5 -3
View File
@@ -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>
+48
View File
@@ -0,0 +1,48 @@
/* 官网共享登录态助手:与 Web 版 Appshared_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
View File
@@ -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
View File
@@ -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
View File
@@ -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') {
+2 -9
View File
@@ -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;
+7
View File
@@ -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(); });