feat(h5): update skin scoring, age display and capture countdown

This commit is contained in:
xsl
2026-10-15 00:20:48 +08:00
parent d8c67762da
commit 60ae6c97ef
37 changed files with 5110 additions and 1632 deletions
+19 -13
View File
@@ -26,6 +26,7 @@ const DATA_DIR = path.join(DATA_ROOT, 'results'); // 每次检测的原始返
const PHOTOS_DIR = path.join(DATA_ROOT, 'photos'); // 检测照片:data/photos/<record_id>/{front,left,right}.jpg
const RECORDS_FILE = path.join(DATA_ROOT, 'records.jsonl'); // 检测记录(一行一条 JSON)
const ReportUI = require('./js/report-ui.js');
const BeijingTime = require('./js/beijing-time.js');
const CaptureDiagnostics = require('./lib/capture-diagnostics.js');
const accounts = require('./lib/accounts.js')(path.join(DATA_ROOT, 'accounts'));
@@ -282,6 +283,9 @@ function fillSuccess(rec, fpJson, persist = true) {
rec.presentation = ReportUI.build(vm, fpJson.result);
rec.total_score = vm.total;
rec.total_score_rule = vm.totalRule;
rec.wrinkle_score_rule = vm.wrinkleScoreRule;
rec.acne_scoring = vm.cats.acne.vars.acne_scoring;
rec.tone_scoring = vm.cats.tone.vars.tone_scoring;
rec.jawline_angle_l = jawlineAngle(fpJson.left_side_result, 'left');
rec.jawline_angle_r = jawlineAngle(fpJson.right_side_result, 'right');
rec.cats = {}; rec.copy_6 = {};
@@ -516,7 +520,7 @@ function photoUrls(rec) {
/* 列表用的字段子集(不含 raw_json 与文案全文) */
function displaySkinAge(rec) {
// Match H5 even for old records; do not rewrite historical files.
return SkinScore.reportSkinAge({ skinAgeRaw: rec.skin_age });
return SkinScore.reportSkinAge({ skinAgeRaw: rec.skin_age, realAge: rec.real_age });
}
function displayTotalScore(rec) {
return SkinScore.weightedTotal(rec.dims);
@@ -540,12 +544,11 @@ function listItem(rec) {
};
}
/* 查询参数过滤。from/to 支持 YYYY-MM-DD 或完整 ISO;纯日期 to 含当天全天 */
/* Dates mean Beijing calendar days; fully zoned ISO timestamps retain their instant. */
function matchQuery(rec, q) {
if (q.from && rec.created_at < q.from) return false;
if (q.to) {
const to = q.to.length === 10 ? q.to + 'T23:59:59.999Z' : q.to;
if (rec.created_at > to) return false;
if (q.timeRange && (q.from || q.to)) {
const timestamp = BeijingTime.milliseconds(rec.created_at);
if (!Number.isFinite(timestamp) || timestamp < q.timeRange.start || timestamp >= q.timeRange.end) return false;
}
if (q.detect_status && rec.detect_status !== q.detect_status) return false;
if (q.fail_code && rec.fail_code !== q.fail_code) return false;
@@ -582,6 +585,8 @@ function handleRecordsQuery(req, res, url) {
if ((q.age_min != null && !Number.isFinite(q.age_min)) || (q.age_max != null && !Number.isFinite(q.age_max))) {
return sendJson(res, 400, { ok: false, message: 'age_min/age_max 必须是数字' });
}
try { q.timeRange = BeijingTime.range(q.from, q.to); }
catch (e) { return sendJson(res, 400, { ok: false, message: e.message }); }
const page = Math.max(1, parseInt(sp.get('page') || '1', 10) || 1);
const pageSize = Math.min(100, Math.max(1, parseInt(sp.get('page_size') || '20', 10) || 20));
const filtered = readAllRecords().filter(r => matchQuery(r, q))
@@ -660,7 +665,7 @@ function handlePhoto(req, res, id, which) {
function handleStats(req, res) {
const all = readAllRecords();
const today = nowIso().slice(0, 10);
const now = Date.now(), today = BeijingTime.day(now);
const finished = all.filter(r => r.detect_status === '成功' || r.detect_status === '失败');
const succeeded = finished.filter(r => r.detect_status === '成功');
const failed = finished.filter(r => r.detect_status === '失败');
@@ -691,9 +696,8 @@ function handleStats(req, res) {
// 近 14 天每日检测量/成功数(含今天,今天在后)
const daily = [];
for (let i = 13; i >= 0; i--) {
const d = new Date(Date.now() - i * 86400000);
const day = d.toISOString().slice(0, 10);
const recs = all.filter(r => (r.created_at || '').slice(0, 10) === day);
const day = BeijingTime.day(now - i * 86400000);
const recs = all.filter(r => BeijingTime.day(r.created_at) === day);
daily.push({
date: day,
total: recs.length,
@@ -703,7 +707,7 @@ function handleStats(req, res) {
sendJson(res, 200, {
ok: true,
today_total: all.filter(r => (r.created_at || '').slice(0, 10) === today).length,
today_total: all.filter(r => BeijingTime.day(r.created_at) === today).length,
total: all.length,
finished_total: finished.length,
success_total: succeeded.length,
@@ -772,19 +776,21 @@ function handleExport(req, res, url) {
from: url.searchParams.get('from') || null,
to: url.searchParams.get('to') || null
};
try { q.timeRange = BeijingTime.range(q.from, q.to); }
catch (e) { return sendJson(res, 400, { ok: false, message: e.message }); }
const filtered = readAllRecords().filter(r => matchQuery(r, q))
.sort((a, b) => (a.created_at < b.created_at ? 1 : -1));
const dimP = (r, d) => (r.dims && r.dims[d]) ? r.dims[d].P : null;
const dimS = (r, d) => (r.dims && r.dims[d]) ? r.dims[d].S : null;
const dimLv = (r, d) => (r.dims && r.dims[d]) ? ReportUI.levelMap[r.dims[d].level] : null;
const header = ['记录ID', '创建时间', '年龄', '性别', '地区', '日常肤质', '拍摄状态', '上传状态', '检测状态',
const header = ['记录ID', '创建时间(北京时间)', '年龄', '性别', '地区', '日常肤质', '拍摄状态', '上传状态', '检测状态',
'失败环节', '失败码', '失败信息', '综合得分(4:3:3)', '原始肌龄(推荐依据)', '前端展示肌龄',
'紧塑力P', '紧塑力S', '紧塑力档', '透亮感P', '透亮感S', '透亮感档', '稳韧力P', '稳韧力S', '稳韧力档',
'均分S', '产品组别', '推荐精华', '触发判据', '耗时ms', '推荐规则版本', '推荐原因', '侧脸质量状态', '前端拍摄参数版本'];
const lines = [header.join(',')];
for (const r of filtered) {
lines.push([
r.record_id, r.created_at, r.real_age, r.gender, r.region, r.daily_skin_type,
r.record_id, BeijingTime.format(r.created_at), r.real_age, r.gender, r.region, r.daily_skin_type,
r.shoot_status, r.upload_status, r.detect_status,
r.fail_stage, r.fail_code, r.fail_msg, displayTotalScore(r), r.skin_age, displaySkinAge(r),
dimP(r, 'firm'), dimS(r, 'firm'), dimLv(r, 'firm'),