feat(h5): allow guest access without login and retain history only for WeChat

This commit is contained in:
xsl
2026-10-13 15:23:23 +08:00
parent 98935dd168
commit 1fdbb575b0
29 changed files with 2561 additions and 1866 deletions
+32 -15
View File
@@ -1,9 +1,19 @@
/* Browser identity is a random credential, never a fingerprint or a report. */
/* Guest access uses an ephemeral server session, never a saved browser identity. */
window.AccountClient = (function () {
'use strict';
var key = 'juzi.identity.v1', kindKey = 'juzi.auth.kind.v1', user = null;
function read(name) { try { return localStorage.getItem(name || key); } catch (_) { return null; } }
function rememberKind() { try { if (user) localStorage.setItem(kindKey, user.kind); } catch (_) {} }
var kindKey = 'juzi.auth.kind.v1', user = null, guestReady = false;
function clearGuestCache() {
try {
localStorage.removeItem('juzi.identity.v1');
localStorage.removeItem('juzi.reports.v1'); // Retired local guest-report cache.
Object.keys(localStorage).filter(function (name) { return name.indexOf('juzi.profile.v1.browser.') === 0; })
.forEach(function (name) { localStorage.removeItem(name); });
if (localStorage.getItem(kindKey) === 'browser') localStorage.removeItem(kindKey);
} catch (_) {}
}
function rememberKind() {
try { if (user && user.kind === 'wechat') localStorage.setItem(kindKey, 'wechat'); else localStorage.removeItem(kindKey); } catch (_) {}
}
function request(url, data) {
var controller = new AbortController();
// Bound only authentication waits. Full report snapshots contain photos and
@@ -20,24 +30,31 @@ window.AccountClient = (function () {
}).finally(function () { clearTimeout(timer); });
}
function browser(consent) {
return request('api/auth/browser', { consent: consent, token: read() }).then(function (v) {
try { localStorage.setItem(key, v.token); } catch (_) { /* session cookie still works */ }
user = v.user; rememberKind(); return v;
clearGuestCache();
return request('api/auth/browser', { consent: consent }).then(function (v) {
user = v.user; guestReady = true; rememberKind(); return v;
});
}
function guest() {
clearGuestCache();
user = { id: 'guest-page-' + Date.now() + '-' + Math.random().toString(36).slice(2), kind: 'browser' };
guestReady = false; rememberKind(); return user;
}
function ensureGuest() {
// The visitor entry is entirely local. Create temporary server access only
// after explicit consent in the welcome dialog, not on entry/button tap.
if (!user || user.kind === 'wechat' || guestReady) return Promise.resolve({ user: user });
return browser(true);
}
function init(options) {
clearGuestCache();
return request('api/auth/session').then(function (v) {
user = v.user;
guestReady = !!(user && user.kind === 'browser');
rememberKind();
// A missing WeChat session is not permission to switch to a browser account.
// OAuth returns must also stay explicit when its cookie was not retained.
if (!user && !(options && options.restoreBrowser === false) && read(kindKey) !== 'wechat' && read()) return browser(true).catch(function (e) {
// Do not replace an existing credential on a transient server/network error.
if (/身份已失效/.test(e.message)) { try { localStorage.removeItem(key); } catch (_) {} }
throw e;
});
// Never restore a guest credential or silently fall back from WeChat.
return v;
});
}
return { request: request, init: init, browser: browser, user: function () { return user; } };
return { request: request, init: init, browser: browser, guest: guest, ensureGuest: ensureGuest, user: function () { return user; } };
})();
+33 -24
View File
@@ -76,15 +76,13 @@
function renderAccountStatus() {
var user = AccountClient.user();
document.querySelectorAll('[data-auth-status]').forEach(function (label) {
label.textContent = user ? (user.kind === 'wechat' ? '微信已登录' : '游客已登录') : '尚未登录';
label.textContent = user ? (user.kind === 'wechat' ? '微信已登录' : '游客模式') : '尚未登录';
label.parentElement.dataset.kind = user ? user.kind : '';
});
}
function loginSucceeded() {
showScreen('s-splash', 'replace');
$('#login-success-message').textContent = AccountClient.user().kind === 'wechat'
? '当前使用:微信登录\n同一微信账号可跨手机查看报告。'
: '当前使用:游客登录\n游客报告仅可在当前浏览器查看,不能跨手机同步。';
$('#login-success-message').textContent = '当前使用:微信登录\n同一微信账号可跨手机查看报告。';
openOverlay('p-login-success');
document.querySelectorAll('.screen').forEach(function (screen) { screen.inert = true; });
$('[data-action="login-confirm"]').focus();
@@ -331,7 +329,11 @@
}
function restoreProfile() {
var user = AccountClient.user();
profileOwner = ProfileStore.key(user);
var owner = user && user.kind === 'browser' ? 'guest:' + user.id : ProfileStore.key(user);
// Keep the current form while a guest retries a capture in this page only.
// Reload/new guest session discards it; saveProfile never writes this owner.
if (user && user.kind === 'browser' && profileOwner === owner) return;
profileOwner = owner;
var saved = ProfileStore.clean(profileStore.read(user), window.REGION_DATA, SKINS.length);
var y = Number(saved.birth.slice(0, 4)), m = Number(saved.birth.slice(5));
if (saved.birth && YEARS.indexOf(y + '年') < 0) saved.birth = '';
@@ -1073,6 +1075,7 @@
}
renderReport(vm, photos[0], photos[1], payload); // img1=正脸实拍,payload 供检测可视化取 face_maps/marks
if (IS_MOCK) toast('演示报告,不保存至我的报告');
else if (AccountClient.user() && AccountClient.user().kind !== 'wechat') toast('游客报告仅供本次查看,不保存历史记录');
else if (!payload.report_saved) toast('报告生成成功,但保存失败,请勿关闭页面并联系管理员');
photos = []; // 报告已渲染(图片 src 已挂):释放三连拍 dataURL 内存
showScreen('s-report', 'replace'); // 报告顶替拍照页:返回键回档案页,不再回到相机
@@ -1561,9 +1564,10 @@
});
/* ─── 动作分发 ─── */
var privacyBusy = false;
var actions = {
'open-policy': function () { openOverlay('p-policy'); },
'login-browser': function () { loginBrowser(); },
'guest-experience': function () { enterGuest(); },
'login-wechat': function () { loginWechat(); },
'login-confirm': closeLoginSuccess,
'open-privacy': function () { if (!AccountClient.user()) { showScreen('s-login', 'replace'); return; } openOverlay('p-privacy'); },
@@ -1571,8 +1575,13 @@
'close-policy': function () { closeOverlay('p-policy'); },
'agree-continue': function () {
if (!agreed) { toast('请先阅读并同意隐私协议'); return; }
closeOverlay('p-privacy');
showScreen('s-profile', 'push');
if (privacyBusy) return;
privacyBusy = true;
var button = $('#p-privacy [data-action="agree-continue"]'); button.disabled = true;
AccountClient.ensureGuest().then(function () {
if (!$('#p-privacy').classList.contains('show')) return;
closeOverlay('p-privacy'); showScreen('s-profile', 'push');
}).catch(function (e) { toast(e.message); }).finally(function () { privacyBusy = false; button.disabled = false; });
},
'submit-profile': function () {
$('#f-age').value = ProfileStore.age($('#f-birth').value) || '';
@@ -1609,10 +1618,10 @@
},
'history': function () { openHistory(); },
'history-retry': function () { renderHistoryList(); },
'switch-login': function () {
'open-wechat-login': function () {
historySeq++;
$('#login-consent').checked = false;
$('#login-message').textContent = '微信与游客为独立账号,报告不会自动合并。请选择登录方式;返回可保留当前登录。';
$('#login-message').textContent = '微信登录后,后续检测报告可保存和跨手机查看。';
showScreen('s-login', 'push');
}
};
@@ -1628,13 +1637,17 @@
var historySeq = 0;
function renderHistoryList() {
var seq = ++historySeq, box = $('#history-list');
if (!AccountClient.user() || AccountClient.user().kind !== 'wechat') {
$('#history-count').textContent = '游客模式不保存检测记录';
$('#identity-note').textContent = '游客不保存个人信息和检测记录,报告仅供本次查看。微信登录后可保留历史报告。';
box.innerHTML = '<p class="history-empty">暂无历史报告<br>请使用微信登录,后续检测报告会自动保存。<br><button class="history-retry" data-action="open-wechat-login">微信登录,保存报告</button></p>';
return;
}
box.textContent = '正在读取报告…';
AccountClient.request('api/reports').then(function (data) {
if (seq !== historySeq) return;
$('#history-count').textContent = '您最近已进行过' + data.total + '次面部问题检测';
$('#identity-note').textContent = AccountClient.user().kind === 'wechat'
? '保留最近 5 份报告,同一微信账号可跨手机查看。'
: '保留最近 5 份报告。游客报告仅可在当前浏览器查看,不能跨手机同步;清除浏览器数据后无法找回。';
$('#identity-note').textContent = '保留最近 5 份报告,同一微信账号可跨手机查看。';
if (!data.items.length) { box.innerHTML = '<p class="history-empty">暂无报告<br>完成一次检测后,报告会自动保存在这里。</p>'; return; }
box.innerHTML = data.items.map(function (item) {
var date = new Date(item.created_at).toLocaleDateString('sv-SE');
@@ -1669,19 +1682,15 @@
if (!$('#login-consent').checked) { toast('请先阅读并同意用户协议和隐私政策'); return false; }
return true;
}
function loginBrowser() {
if (!loginAllowed()) return;
loginBusy = true;
$('#login-message').textContent = '正在登录…';
AccountClient.browser(true).then(function () {
loginSucceeded();
}).catch(function (e) { $('#login-message').textContent = e.message; })
.finally(function () { loginBusy = false; });
function enterGuest() {
if (loginBusy) return;
AccountClient.guest();
showScreen('s-splash', 'replace');
}
function loginWechat() {
if (!loginAllowed()) return;
if (!/MicroMessenger/i.test(navigator.userAgent)) {
$('#login-message').textContent = '请在微信内打开本页面,或使用游客登录。';
$('#login-message').textContent = '请在微信内打开本页面,或选择免登录体验。';
return;
}
loginBusy = true;
@@ -1742,14 +1751,14 @@
$('#login-message').textContent = code === 'state'
? '登录链接已过期或会话丢失,请在同一微信窗口重新点击微信登录。'
: code === 'network' ? '暂时无法连接微信服务,请稍后重新登录。'
: '微信授权未完成,请重试;如仍失败,请将错误页面截图发给管理员。也可选择游客登录。';
: '微信授权未完成,请重试;如仍失败,请将错误页面截图发给管理员。也可选择免登录体验。';
} else if (loginSuccess && (!AccountClient.user() || AccountClient.user().kind !== 'wechat')) {
$('#login-message').textContent = '微信返回后登录会话未能保存,请在同一微信窗口重新登录。';
} else if (AccountClient.user()) {
if (loginSuccess) loginSucceeded();
else showScreen('s-splash', 'replace');
}
else $('#login-message').textContent = '微信登录可跨手机查看报告;游客登录仅限当前浏览器。';
else $('#login-message').textContent = '微信登录可保留历史报告;游客不保存个人信息和检测记录。';
}).catch(function (e) { $('#login-message').textContent = e.message; })
.finally(function () {
loginBusy = false;
+1 -1
View File
@@ -5,7 +5,7 @@
})(typeof window !== 'undefined' ? window : this, function() {
'use strict';
function key(user) {
return user && ['browser', 'wechat'].indexOf(user.kind) >= 0 && typeof user.id === 'string' && user.id.length <= 128 && user.id
return user && user.kind === 'wechat' && typeof user.id === 'string' && user.id.length <= 128 && user.id
? 'juzi.profile.v1.' + user.kind + '.' + encodeURIComponent(user.id) : null;
}
function age(birth, now) {