feat: 完善淘宝小程序拍照、报告与登录流程

接入本地实时识别和自动三连拍,调整报告裁切、商品跳转、分享标题及居中页眉;完善游客会话、后台渠道隔离、部署脚本与回归测试。
This commit is contained in:
xsl
2026-11-04 15:07:51 +08:00
parent 381da60ae4
commit 774376fa16
147 changed files with 17420 additions and 597 deletions
+64 -22
View File
@@ -49,6 +49,27 @@ function api(url, opts) {
});
});
}
function channelName(channel) { return ({ h5: 'H5', taobao: '淘宝小程序', rednote: '小红书' })[channel] || channel || 'H5'; }
function selectedChannelQuery(url) {
var channel = $('#f-channel').value;
return channel ? url + (url.indexOf('?') < 0 ? '?' : '&') + 'channel=' + encodeURIComponent(channel) : url;
}
function sourceQuery(source) { return '?source=' + encodeURIComponent(source || 'local'); }
function renderSources(data) {
var sources = data.sources || [];
$('#channel-summary').textContent = sources.length > 1
? '已汇总 H5 与淘宝数据;外部渠道记录只读,重试请进入原服务后台。'
: '当前读取本服务的数据;尚未接入其他服务的数据源。';
}
function renderChannelStats(data) {
renderSources(data);
var rows = data.by_channel || {}, html = '<table><thead><tr><th>渠道</th><th>累计检测</th><th>今日检测</th><th>成功</th><th>失败</th></tr></thead><tbody>';
Object.keys(rows).forEach(function (key) {
var row = rows[key];
html += '<tr><td>' + esc(channelName(key)) + '</td><td>' + row.total + '</td><td>' + row.today_total + '</td><td>' + row.success_total + '</td><td>' + row.fail_total + '</td></tr>';
});
$('#channel-stats').innerHTML = html + '</tbody></table>';
}
/* ─── 页签切换 ─── */
var TABS = { records: 1, fails: 1, stats: 1 };
@@ -116,6 +137,7 @@ function collectFilters() {
add('from', $('#f-from').value);
add('to', $('#f-to').value);
add('detect_status', $('#f-status').value);
add('channel', $('#f-channel').value);
add('fail_code', $('#f-failcode').value);
add('age_min', $('#f-agemin').value);
add('age_max', $('#f-agemax').value);
@@ -130,6 +152,7 @@ function collectFilters() {
function loadRecords() {
api('/api/admin/records?' + collectFilters()).then(function (data) {
renderSources(data);
pageState.total = data.total;
var tbody = $('#tbl-records tbody');
tbody.innerHTML = '';
@@ -145,7 +168,7 @@ function loadRecords() {
: '<span class="sub">无</span>';
tr.innerHTML =
'<td>' + thumb + '</td>' +
'<td class="mono">' + esc(it.record_id) + (it.retry_of ? '<div class="sub">重试自 ' + esc(it.retry_of) + '</div>' : '') + '</td>' +
'<td class="mono">' + esc(it.record_id) + '<div class="sub">' + esc(channelName(it.channel)) + (it.readonly ? ' · 只读' : '') + '</div>' + (it.retry_of ? '<div class="sub">重试自 ' + esc(it.retry_of) + '</div>' : '') + '</td>' +
'<td>' + profileHtml(it) + '</td>' +
'<td>' + esc(it.shoot_status) + captureQualityHtml(it) + '</td>' +
'<td>' + esc(it.upload_status) + '</td>' +
@@ -159,7 +182,7 @@ function loadRecords() {
'<td class="mono">' + fmtTime(it.created_at) + '</td>';
tr.addEventListener('click', function (e) {
if (e.target && e.target.className === 'thumb') { showLightbox(e.target.src); return; }
openDetail(it.record_id);
openDetail(it.record_id, it.source);
});
tbody.appendChild(tr);
})(data.items[i]);
@@ -175,6 +198,7 @@ function loadRecords() {
function renderCaptureFunnel(st) {
var funnel = st.capture_funnel;
var panel = $('#capture-funnel');
if (panel) panel.parentNode.hidden = !funnel;
if (funnel && panel) {
var labels = [['started','开始拍照'],['front','正面拍完'],['right','右脸拍完'],['left','三张拍完'],['submitted','提交检测'],['response','收到成功结果'],['report','进入报告页']];
var html = '<table><thead><tr><th>阶段</th><th>累计流程</th><th>今日流程</th></tr></thead><tbody>';
@@ -190,7 +214,8 @@ function renderCaptureFunnel(st) {
}
}
function loadFails() {
api('/api/admin/stats').then(function (st) {
api(selectedChannelQuery('/api/admin/stats')).then(function (st) {
renderSources(st);
var box = $('#fail-dist');
box.innerHTML = '';
if (!st.fail_dist.length) box.appendChild(el('div', 'pie-empty', '暂无失败记录'));
@@ -205,7 +230,7 @@ function loadFails() {
}
}).catch(function (e) { toast('加载失败:' + e.message); });
api('/api/admin/records?detect_status=失败&page_size=100').then(function (data) {
api(selectedChannelQuery('/api/admin/records?detect_status=失败&page_size=100')).then(function (data) {
var tbody = $('#tbl-fails tbody');
tbody.innerHTML = '';
if (!data.items.length) {
@@ -215,17 +240,17 @@ function loadFails() {
(function (it) {
var tr = el('tr');
tr.innerHTML =
'<td class="mono">' + esc(it.record_id) + '</td>' +
'<td class="mono">' + esc(it.record_id) + '<div class="sub">' + esc(channelName(it.channel)) + '</div></td>' +
'<td>' + esc(it.fail_stage || '-') + '</td>' +
'<td><span class="st st-fail">' + esc(it.fail_code || '-') + '</span></td>' +
'<td style="white-space:normal;max-width:340px">' + esc(it.fail_msg || '-') + '</td>' +
'<td>' + (it.real_age != null ? it.real_age + '岁' : '-') + '</td>' +
'<td>' + esc(it.shoot_status) + '</td>' +
'<td class="mono">' + fmtTime(it.created_at) + '</td>' +
'<td><button type="button" class="btn small primary">重试</button></td>';
'<td>' + (it.readonly ? '<span class="sub">只读,请到原服务后台重试</span>' : '<button type="button" class="btn small primary">重试</button>') + '</td>';
tr.addEventListener('click', function (e) {
if (e.target && e.target.tagName === 'BUTTON') { doRetry(it.record_id); return; }
openDetail(it.record_id);
if (e.target && e.target.tagName === 'BUTTON') { doRetry(it.record_id, it.source); return; }
openDetail(it.record_id, it.source);
});
tbody.appendChild(tr);
})(data.items[i]);
@@ -233,10 +258,10 @@ function loadFails() {
}).catch(function (e) { toast('加载失败:' + e.message); });
}
function doRetry(id) {
function doRetry(id, source) {
if (!confirm('将使用原照片重新检测并生成新记录,确认重试 ' + id + ' ?')) return;
toast('重试中,请稍候(最长约 1 分钟)…');
api('/api/admin/records/' + encodeURIComponent(id) + '/retry', { method: 'POST' }).then(function (data) {
api('/api/admin/records/' + encodeURIComponent(id) + '/retry' + sourceQuery(source), { method: 'POST' }).then(function (data) {
toast('重试完成,新记录 ' + data.record.record_id + '(' + data.record.detect_status + ')');
var active = $('.panel.on').id;
if (active === 'tab-records') loadRecords();
@@ -247,7 +272,8 @@ function doRetry(id) {
/* ═══════════ 统计概览 ═══════════ */
var PIE_COLORS = ['#2563eb', '#2fa84f', '#f0a020', '#8a5cf6', '#d93026'];
function loadStats() {
api('/api/admin/stats').then(function (st) {
api(selectedChannelQuery('/api/admin/stats')).then(function (st) {
renderChannelStats(st);
renderCaptureFunnel(st);
var cards = [
['今日检测', st.today_total],
@@ -332,18 +358,23 @@ function loadStats() {
/* ═══════════ 记录详情(抽屉) ═══════════ */
var currentDetailId = null;
var currentDetailSource = 'local';
var detailSeq = 0;
function openDetail(id) {
function openDetail(id, source) {
var seq = ++detailSeq;
currentDetailId = id;
currentDetailSource = source || 'local';
$('#d-title').textContent = '记录详情 ' + id;
$('#btn-retry').disabled = true;
$('#d-body').innerHTML = '<div class="sub">加载中…</div>';
$('#drawer').classList.remove('hidden');
$('#drawer-mask').classList.remove('hidden');
api('/api/admin/records/' + encodeURIComponent(id)).then(renderDetail)
.catch(function (e) { $('#d-body').innerHTML = '<div class="sub">加载失败:' + esc(e.message) + '</div>'; });
api('/api/admin/records/' + encodeURIComponent(id) + sourceQuery(currentDetailSource)).then(function (data) { if (seq === detailSeq) renderDetail(data); })
.catch(function (e) { if (seq === detailSeq) $('#d-body').innerHTML = '<div class="sub">加载失败:' + esc(e.message) + '</div>'; });
}
function closeDetail() {
detailSeq++;
$('#drawer').classList.add('hidden');
$('#drawer-mask').classList.add('hidden');
currentDetailId = null;
@@ -355,7 +386,8 @@ function dSec(title, innerHtml) {
function renderDetail(data) {
var r = data.record;
$('#btn-retry').disabled = !(data.photos && data.photos.front);
$('#btn-retry').disabled = data.readonly || !(data.photos && data.photos.front);
$('#btn-retry').title = data.readonly ? '只读汇总记录,请在原服务后台重试' : '';
var h = '';
/* 三张照片 */
@@ -375,6 +407,7 @@ function renderDetail(data) {
/* 状态行 */
h += dSec('状态', '<div class="d-kv">' +
'<span>渠道 <b>' + esc(channelName(r.channel)) + (data.readonly ? '(只读汇总)' : '') + '</b></span>' +
'<span>检测状态 <b>' + esc(r.detect_status) + '</b></span>' +
'<span>拍摄状态 <b>' + esc(r.shoot_status) + '</b></span>' +
'<span>上传状态 <b>' + esc(r.upload_status) + '</b></span>' +
@@ -553,7 +586,7 @@ function renderDetail(data) {
if (data.raw_json) {
var rawFile = data.raw_file || (window.JuziEntry && window.JuziEntry.mode === 'rednote' ? r.raw_file : null);
var rawDl = rawFile
? ' <a href="' + adminURL('/api/admin/records/' + encodeURIComponent(r.record_id) + '/raw') + '" download>下载完整原始 JSON</a>'
? ' <a href="' + adminURL('/api/admin/records/' + encodeURIComponent(r.record_id) + '/raw' + sourceQuery(data.source)) + '" download>下载完整原始 JSON</a>'
: '';
h += dSec('接口原始 JSON', '<details><summary>展开/收起(' + (rawFile || '') + ')' + rawDl + '</summary><pre>' +
esc(JSON.stringify(data.raw_json, null, 1)) + '</pre></details>');
@@ -572,7 +605,7 @@ function renderDetail(data) {
/* 版本链跳转 */
var links = body.querySelectorAll('.d-versions a');
for (var v = 0; v < links.length; v++) {
links[v].addEventListener('click', function () { openDetail(this.getAttribute('data-id')); });
links[v].addEventListener('click', function () { openDetail(this.getAttribute('data-id'), data.source); });
}
}
@@ -584,6 +617,14 @@ function showLightbox(src) {
/* ═══════════ 事件绑定与初始化 ═══════════ */
function bind() {
if (window.JuziEntry && window.JuziEntry.mode === 'rednote') {
$('#f-channel').innerHTML = '<option value="">全部渠道</option><option value="rednote">小红书</option>';
}
$('#f-channel').addEventListener('change', function () {
pageState.page = 1;
var active = $('.panel.on').id.replace('tab-', '');
switchTab(active);
});
var tabs = document.querySelectorAll('.tab');
for (var i = 0; i < tabs.length; i++) {
tabs[i].addEventListener('click', function () { switchTab(this.dataset.tab); });
@@ -599,13 +640,14 @@ function bind() {
$('#btn-next').addEventListener('click', function () { pageState.page++; loadRecords(); });
$('#btn-close').addEventListener('click', closeDetail);
$('#drawer-mask').addEventListener('click', closeDetail);
$('#btn-retry').addEventListener('click', function () { if (currentDetailId) doRetry(currentDetailId); });
$('#btn-retry').addEventListener('click', function () { if (currentDetailId) doRetry(currentDetailId, currentDetailSource); });
$('#lightbox').addEventListener('click', function () { this.classList.add('hidden'); });
$('#btn-export').addEventListener('click', function () {
var p = [];
if ($('#exp-from').value) p.push('from=' + $('#exp-from').value);
if ($('#exp-to').value) p.push('to=' + $('#exp-to').value);
var url = adminURL('/api/admin/export' + (p.length ? '?' + p.join('&') : ''));
var p = new URLSearchParams(collectFilters());
p.delete('page'); p.delete('page_size');
if ($('#exp-from').value) p.set('from', $('#exp-from').value);
if ($('#exp-to').value) p.set('to', $('#exp-to').value);
var url = adminURL('/api/admin/export?' + p.toString());
var a = el('a');
a.href = url;
a.download = '';