feat(h5): calculate integer total from weighted dimensions across reports and admin
This commit is contained in:
+1
-1
@@ -94,7 +94,7 @@ node server.js # 默认 8080,可用 PORT=xxx 修改
|
||||
- S = 7 + 3×P/100;档位:良好(P≥90)/改善(80–90)/预警(<80);生物可视年龄直接展示算法原始肌龄,不再按实际年龄 ±3 修正;三视图版 18 条话术;
|
||||
- 精华推荐三重判据(均分档/最差项约束/年龄组档)取最强档,triggers 记录命中判据。
|
||||
4. 前端 `SkinScore.buildViewModel(data.result, data.side, { realAge, skinType })` → viewModel 直渲报告页:
|
||||
- 头部:综合评分(`vm.total`,缺失回退三维度 S 均分×10)、肤龄读取 `SkinScore.reportSkinAge(vm, result)`;历史报告同样读取原始肌龄而非旧修正展示值,原值缺失显示 `--`;三维度 S(1 位小数)+ 档位配色(良好绿/改善琥珀/预警红);
|
||||
- 头部:综合得分通过 `SkinScore.weightedTotal(vm.dims)` 计算,紧塑力×4+透亮感×3+稳韧力×3,使用维度S分数(两位小数)加权、四舍五入取整数,不使用接口 `total_score`;任一维度缺失显示 `--`。后台列表、详情、CSV采用相同公式,历史报告按已保存维度计算展示,不改写原文件;肤龄读取 `SkinScore.reportSkinAge(vm, result)`,原值缺失显示 `--`;三维度 S(1 位小数)+ 档位配色(良好绿/改善琥珀/预警红);
|
||||
- 三个 Tab 面板各展示所辖两分类:分类名、S + 档位徽章、话术正文(`vm.cats.*.text`);P 值不直接展示,`P=null` 的分类显示"本次未检出有效数据";
|
||||
- 报告底部:降级标注(`vm.notes` 中与侧脸/下颌线/补拍相关的条目,去掉接口质量码后展示)、护理建议五段(`vm.careAdvice`)、精华推荐卡(`vm.essence.name` + 命中判据;推荐正文已在护理建议第 4 段,卡内不重复);
|
||||
- 近期优先关注 = 最差维度(`vm.focus`);6 分类各配检测可视化图:轮廓=正脸实拍、皱纹=正脸压暗+`texture_enhanced_lines` 皱纹增强线(线图层几乎全透明,直挂白底不可读,故与压暗沉脸合成)、光滑=正脸+色斑/痣/痤疮标记叠加、肤色=`brown_area` 色沉图、敏感=`red_area` 红区图、痘痘=正脸+闭口粉刺/痤疮标记叠加(marks 按原像素坐标叠加到正脸实拍)。
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>照片</th><th>记录ID</th><th>年龄/性别/地区/肤质</th><th>拍摄状态</th><th>上传状态</th>
|
||||
<th>检测状态</th><th>总分</th><th>肌龄(原始 / 展示)</th><th>三维度评分 / 档位</th><th>推荐精华</th><th>耗时</th><th>创建时间</th>
|
||||
<th>检测状态</th><th title="紧塑力×4+透亮感×3+稳韧力×3,四舍五入取整数">综合得分</th><th>肌龄(原始 / 展示)</th><th>三维度评分 / 档位</th><th>推荐精华</th><th>耗时</th><th>创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
|
||||
+4
-2
@@ -22,6 +22,7 @@ function fmtTime(iso) {
|
||||
}
|
||||
function fmtMs(ms) { return ms == null ? '-' : ms + 'ms'; }
|
||||
function fmtScore(v) { return v == null ? '-' : v; }
|
||||
function fmtTotal(v) { return v == null ? '--' : v; }
|
||||
|
||||
var LV_CLASS = { '预警': 'lv-bad', '改善': 'lv-warn', '良好': 'lv-good' };
|
||||
var DIM_NAMES = ReportUI.dimNames;
|
||||
@@ -145,7 +146,7 @@ function loadRecords() {
|
||||
'<td>' + esc(it.upload_status) + '</td>' +
|
||||
'<td><span class="st ' + (ST_CLASS[it.detect_status] || '') + '">' + esc(it.detect_status) + '</span>' +
|
||||
(it.fail_code ? '<div class="sub">' + esc(it.fail_code) + '</div>' : '') + '</td>' +
|
||||
'<td><b>' + fmtScore(it.total_score) + '</b></td>' +
|
||||
'<td><b>' + fmtTotal(it.total_score) + '</b></td>' +
|
||||
'<td>' + skinAgeHtml(it) + '</td>' +
|
||||
'<td>' + dimsHtml(it.dims) + '</td>' +
|
||||
'<td>' + esc(productName(it.essence)) + '<div class="sub">' + esc(ruleLabel(it)) + '</div></td>' +
|
||||
@@ -405,13 +406,14 @@ function renderDetail(data) {
|
||||
/* 成功:评分 + 文案 */
|
||||
if (r.detect_status === '成功') {
|
||||
h += dSec('综合', '<div class="d-kv">' +
|
||||
'<span>总分 <b>' + fmtScore(r.total_score) + '</b></span>' +
|
||||
'<span>综合得分 <b>' + fmtTotal(r.total_score) + '</b></span>' +
|
||||
'<span>原始肌龄(新推荐依据) <b>' + ageText(r.skin_age) + '</b></span>' +
|
||||
'<span>前端展示肌龄 <b>' + ageText(r.skin_age_display) + '</b></span>' +
|
||||
'<span>六分类均分S <b>' + fmtScore(r.avg_s) + '</b></span>' +
|
||||
'<span>左下颌线角 <b>' + (r.jawline_angle_l != null ? r.jawline_angle_l + '°' : '-') + '</b></span>' +
|
||||
'<span>右下颌线角 <b>' + (r.jawline_angle_r != null ? r.jawline_angle_r + '°' : '-') + '</b></span>' +
|
||||
'</div>');
|
||||
h += '<p class="sub">综合得分=紧塑力×4+透亮感×3+稳韧力×3,按已计算的维度S分数(两位小数)加权,四舍五入取整数;任一维度缺失不生成综合得分,不使用接口总分。</p>';
|
||||
h += '<p class="sub">前端展示肌龄沿用实际年龄±3岁的修正;新推荐只使用原始肌龄。历史报告保留当时的推荐。</p>';
|
||||
|
||||
/* 6 分类表格 */
|
||||
|
||||
+2
-7
@@ -1487,13 +1487,8 @@
|
||||
|
||||
function renderReport(vm, frontPhoto, sidePhoto, payload) {
|
||||
reportOwner = AccountClient.user() && AccountClient.user().id;
|
||||
// 头部:综合评分(vm.total 缺失时回退三维度 S 均分×10)
|
||||
var total = vm.total;
|
||||
if (total == null) {
|
||||
var ds = [vm.dims.firm.S, vm.dims.vita.S, vm.dims.stab.S], sum = 0, n = 0, i;
|
||||
for (i = 0; i < 3; i++) if (typeof ds[i] === 'number') { sum += ds[i]; n++; }
|
||||
if (n) total = Math.round(sum / n * 10);
|
||||
}
|
||||
// Historical snapshots also use their saved dimensions, never the old API total.
|
||||
var total = SkinScore.weightedTotal(vm.dims);
|
||||
// 环形综合评分:满分 100,环长 2πr(r=52)≈326.73
|
||||
$('#stat-score').innerHTML = total != null ? '<b>' + total + '</b><i>分</i>' : '<b>--</b><i>分</i>';
|
||||
var ring = $('#ring-fg');
|
||||
|
||||
+16
-2
@@ -63,6 +63,18 @@
|
||||
}
|
||||
function round2(x) { return Math.round((x + 1e-9) * 100) / 100; } // 1e-9 修正 9.445 类浮点
|
||||
function sRaw(P) { return 7 + 3 * P / 100; }
|
||||
// One shared 100-point total for H5, saved reports and admin. Never use the
|
||||
// upstream total or reweight an incomplete set of dimensions.
|
||||
function weightedTotal(dims) {
|
||||
if (!dims) return null;
|
||||
var keys = ['firm', 'vita', 'stab'], weights = [4, 3, 3], total = 0;
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var score = dims[keys[i]] && dims[keys[i]].S;
|
||||
if (!isNum(score) || score < 0 || score > 10) return null;
|
||||
total += score * weights[i];
|
||||
}
|
||||
return Math.round(total + 1e-9);
|
||||
}
|
||||
function sShow(P) { // 展示层钳制:P 可为负(肤色扣分/大面积敏感),S 会 <7,与文档「S 7.0–10.0」口径矛盾;
|
||||
if (P == null) return null; // 仅钳展示值,推荐边界比较仍用未舍入维度 S 原值
|
||||
var s = sRaw(P);
|
||||
@@ -814,7 +826,8 @@
|
||||
careAdvice.push({ tag: '生活方式', text: worst ? LIFESTYLE[worst.catKey] : '' });
|
||||
|
||||
return {
|
||||
total: pick(r, 'total_score'),
|
||||
total: weightedTotal(dims),
|
||||
totalRule: 'dimensions-433-v1',
|
||||
skinAgeRaw: skinAge,
|
||||
skinAgeDisplay: skinAgeDisplay,
|
||||
ageClampNote: ageClampNote,
|
||||
@@ -902,6 +915,7 @@
|
||||
return val(result && result.skin_age);
|
||||
}
|
||||
return {
|
||||
weightedTotal: weightedTotal,
|
||||
reportSkinAge: reportSkinAge,
|
||||
buildViewModel: buildViewModel,
|
||||
recommendProduct: recommendProduct,
|
||||
@@ -952,7 +966,7 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
|
||||
ok(vm.avgS === 9.32, '六分类S均分 9.32');
|
||||
ok(vm.worst.catKey === 'wrinkles' && vm.worst.P === 23, '最差分类 = 皱纹');
|
||||
ok(vm.focus.name === '紧塑力', '近期优先关注 = 最差维度紧塑力');
|
||||
ok(vm.total === 79, '综合评分 = score_info.total_score');
|
||||
ok(vm.total === 88, '综合得分 = round(7.69×4+9.45×3+9.55×3) = 88,不使用接口总分79');
|
||||
ok(vm.summary.indexOf('【综合】') === 0 && vm.summary.indexOf('25+束紧精华') > 0, '综合文案生成');
|
||||
ok(vm.cats.smooth.text.indexOf('粗大毛孔296颗、黑头39颗') >= 0 && vm.cats.smooth.text.indexOf('纹理分87') >= 0,
|
||||
'光滑话术含粗大毛孔/黑头/纹理分变量(分区计数 4 区求和 296)');
|
||||
|
||||
+8
-4
@@ -281,6 +281,7 @@ function fillSuccess(rec, fpJson, persist = true) {
|
||||
rec.recommendation = vm.recommendation;
|
||||
rec.presentation = ReportUI.build(vm, fpJson.result);
|
||||
rec.total_score = vm.total;
|
||||
rec.total_score_rule = vm.totalRule;
|
||||
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 = {};
|
||||
@@ -517,6 +518,9 @@ function displaySkinAge(rec) {
|
||||
// Match H5 even for old records; do not rewrite historical files.
|
||||
return SkinScore.reportSkinAge({ skinAgeRaw: rec.skin_age });
|
||||
}
|
||||
function displayTotalScore(rec) {
|
||||
return SkinScore.weightedTotal(rec.dims);
|
||||
}
|
||||
function listItem(rec) {
|
||||
return {
|
||||
record_id: rec.record_id,
|
||||
@@ -526,7 +530,7 @@ function listItem(rec) {
|
||||
capture_quality: rec.capture_quality || null,
|
||||
capture_policy_version: rec.capture_diagnostics ? rec.capture_diagnostics.version : null,
|
||||
fail_code: rec.fail_code, fail_msg: rec.fail_msg,
|
||||
total_score: rec.total_score, skin_age: rec.skin_age,
|
||||
total_score: displayTotalScore(rec), total_score_rule: 'dimensions-433-v1', skin_age: rec.skin_age,
|
||||
skin_age_display: displaySkinAge(rec), recommendation: rec.recommendation || null,
|
||||
dims: rec.dims, group: rec.group,
|
||||
essence: rec.essence ? rec.essence.name : null,
|
||||
@@ -614,7 +618,7 @@ function handleRecordDetail(req, res, id) {
|
||||
.map(r => ({ record_id: r.record_id, detect_status: r.detect_status, created_at: r.created_at }));
|
||||
sendJson(res, 200, {
|
||||
ok: true,
|
||||
record: Object.assign({}, rec, { skin_age_display: displaySkinAge(rec),
|
||||
record: Object.assign({}, rec, { total_score: displayTotalScore(rec), total_score_rule: 'dimensions-433-v1', skin_age_display: displaySkinAge(rec),
|
||||
presentation: rec.presentation || accounts.reportPresentation(rec.owner_id, rec.record_id) }),
|
||||
raw_json: rawJson,
|
||||
photos: photoUrls(rec),
|
||||
@@ -774,7 +778,7 @@ function handleExport(req, res, url) {
|
||||
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', '创建时间', '年龄', '性别', '地区', '日常肤质', '拍摄状态', '上传状态', '检测状态',
|
||||
'失败环节', '失败码', '失败信息', '总分', '原始肌龄(推荐依据)', '前端展示肌龄',
|
||||
'失败环节', '失败码', '失败信息', '综合得分(4:3:3)', '原始肌龄(推荐依据)', '前端展示肌龄',
|
||||
'紧塑力P', '紧塑力S', '紧塑力档', '透亮感P', '透亮感S', '透亮感档', '稳韧力P', '稳韧力S', '稳韧力档',
|
||||
'均分S', '产品组别', '推荐精华', '触发判据', '耗时ms', '推荐规则版本', '推荐原因', '侧脸质量状态', '前端拍摄参数版本'];
|
||||
const lines = [header.join(',')];
|
||||
@@ -782,7 +786,7 @@ function handleExport(req, res, url) {
|
||||
lines.push([
|
||||
r.record_id, 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, r.total_score, r.skin_age, displaySkinAge(r),
|
||||
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'),
|
||||
dimP(r, 'vita'), dimS(r, 'vita'), dimLv(r, 'vita'),
|
||||
dimP(r, 'stab'), dimS(r, 'stab'), dimLv(r, 'stab'),
|
||||
|
||||
@@ -108,7 +108,7 @@ function main() {
|
||||
okNum(tag + '.AP 左下颌线角度', fixture.side.left.left_jawline_info.left_jawline_angle, tr.jawline.left);
|
||||
okNum(tag + '.AQ 右下颌线角度', fixture.side.right.right_jawline_info.right_jawline_angle, tr.jawline.right);
|
||||
okNum(tag + '.E 肌肤年龄', vm.skinAgeRaw, tr.skinAge);
|
||||
okNum(tag + '.F 总分', vm.total, tr.totalScore);
|
||||
okNum(tag + '.F 综合得分(4:3:3)', vm.total, Math.round(tr.dims.firm.S * 4 + tr.dims.vita.S * 3 + tr.dims.stab.S * 3));
|
||||
|
||||
// 六分类 P/S/档位 + 文案逐字
|
||||
for (var k in CAT_COLS) {
|
||||
|
||||
@@ -46,6 +46,9 @@ try {
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.report_saved, true);
|
||||
assert(result.vm && result.presentation);
|
||||
const expectedTotal = Math.round(result.vm.dims.firm.S * 4 + result.vm.dims.vita.S * 3 + result.vm.dims.stab.S * 3);
|
||||
assert.equal(result.vm.total, expectedTotal);
|
||||
assert.notEqual(expectedTotal, result.result.score_info.total_score, 'fixture distinguishes weighted score from API total');
|
||||
assert(result.vm.skinAgeRaw < 32);
|
||||
assert.equal(result.vm.skinAgeDisplay, result.vm.skinAgeRaw);
|
||||
assert.equal(result.vm.essence.tier, 1); // Raw age, never actual60.
|
||||
@@ -55,24 +58,32 @@ try {
|
||||
assert.deepEqual(adminRecord.recommendation, result.vm.recommendation);
|
||||
assert.equal(adminRecord.skin_age, result.vm.skinAgeRaw);
|
||||
assert.equal(adminRecord.skin_age_display, result.vm.skinAgeDisplay);
|
||||
assert.equal(adminRecord.total_score, expectedTotal);
|
||||
assert.equal(adminRecord.total_score_rule, 'dimensions-433-v1');
|
||||
for (const key of ['firm', 'vita', 'stab']) assert.equal(adminRecord.dims[key].S, result.vm.dims[key].S);
|
||||
const list = await (await api.get(base + 'api/admin/records')).json();
|
||||
assert.equal(list.items[0].skin_age, result.vm.skinAgeRaw);
|
||||
assert.equal(list.items[0].skin_age_display, result.vm.skinAgeRaw);
|
||||
assert.deepEqual(list.items[0].recommendation, result.vm.recommendation);
|
||||
assert.equal(list.items[0].total_score, expectedTotal);
|
||||
const csv = await (await api.get(base + 'api/admin/export')).text();
|
||||
assert(csv.includes('原始肌龄(推荐依据),前端展示肌龄'));
|
||||
assert(csv.includes(result.vm.recommendation.version));
|
||||
assert(csv.includes('综合得分(4:3:3)'));
|
||||
assert.equal(csv.trim().split('\n')[1].split(',')[12], String(expectedTotal));
|
||||
const snapshot = await (await api.get(base + 'api/reports/' + result.record_id)).json();
|
||||
assert.equal(snapshot.report.photos.length, 3);
|
||||
assert.deepEqual(snapshot.report.presentation, result.presentation);
|
||||
assert.deepEqual(snapshot.report.payload.result.face_maps, result.result.face_maps);
|
||||
assert.equal(snapshot.report.photos[0], fixture.photos[0]);
|
||||
assert.equal(snapshot.report.vm.total, expectedTotal);
|
||||
assert.equal(JSON.parse(fs.readFileSync(path.join(dataRoot, 'records.jsonl'), 'utf8').trim()).total_score, expectedTotal, 'new records persist weighted score');
|
||||
const desktop = await browser.newPage({ viewport: { width: 1600, height: 1000 } });
|
||||
desktop.on('pageerror', e => errors.push(e.message));
|
||||
await desktop.goto(base + 'admin.html');
|
||||
const row = desktop.locator('#tbl-records tbody tr').first();
|
||||
await row.waitFor();
|
||||
assert.equal(await row.locator('td').nth(6).textContent(), String(expectedTotal));
|
||||
assert((await row.textContent()).includes('原始 ' + result.vm.skinAgeRaw + '岁'));
|
||||
assert((await row.textContent()).includes('展示 ' + result.vm.skinAgeRaw + '岁'));
|
||||
assert((await row.textContent()).includes("1'me 主场精华 I(25+)"));
|
||||
@@ -92,6 +103,9 @@ try {
|
||||
await row.click();
|
||||
await desktop.waitForFunction(() => document.querySelector('#d-body').textContent.includes('推荐依据'));
|
||||
const detailText = await desktop.locator('#d-body').textContent();
|
||||
assert(detailText.includes('综合得分 ' + expectedTotal));
|
||||
assert(detailText.includes('紧塑力×4+透亮感×3+稳韧力×3'));
|
||||
if (process.env.H5_ARTIFACT_DIR) await desktop.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, 'admin-weighted-detail.png') });
|
||||
assert(detailText.includes(result.vm.recommendation.reason));
|
||||
assert(detailText.includes('前端展示肌龄 ' + result.vm.skinAgeRaw + '岁'));
|
||||
for (const view of result.presentation.views) assert(detailText.includes(view.text));
|
||||
@@ -123,6 +137,11 @@ try {
|
||||
await page.click('[data-action="history"]');
|
||||
await page.click('[data-report-id]');
|
||||
await page.waitForSelector('#s-report.active');
|
||||
assert.equal(await page.locator('#stat-score b').textContent(), String(expectedTotal));
|
||||
if (process.env.H5_ARTIFACT_DIR) {
|
||||
await page.waitForTimeout(400); // Capture after the report's entrance fade.
|
||||
await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, 'report-weighted-total.png') });
|
||||
}
|
||||
await page.waitForFunction(() => [...document.querySelectorAll('.ditem-img')].every(i => !i.hidden && i.naturalWidth));
|
||||
const imagesBefore = await page.locator('.ditem-img').evaluateAll(images => images.map(i => i.src));
|
||||
const copyBefore = await page.locator('.ditem-para').allTextContents();
|
||||
@@ -147,6 +166,7 @@ try {
|
||||
legacy.presentation.product = { ...ReportUI.products[1], image: 'assets/img/ui-20261009/product-35.png' };
|
||||
legacy.presentation.views[0].text = '历史报告原文,不应按新文案重新计算。';
|
||||
legacy.vm.skinAgeDisplay = 57; // Old snapshot retains the retired display clamp.
|
||||
legacy.vm.total = 12; // Old stored upstream total must not control the current display.
|
||||
legacy.vm.essence = { name: '35+支撑精华', text: '旧版推荐', triggers: ['最差项约束'] };
|
||||
delete legacy.vm.recommendation;
|
||||
fs.writeFileSync(snapshotFile, JSON.stringify(legacy));
|
||||
@@ -154,6 +174,7 @@ try {
|
||||
delete record.presentation;
|
||||
delete record.recommendation;
|
||||
record.skin_age_display = 57;
|
||||
record.total_score = 12;
|
||||
fs.writeFileSync(recordFile, JSON.stringify(record) + '\n');
|
||||
const storedBytes = fs.readFileSync(snapshotFile, 'utf8');
|
||||
const recordBytes = fs.readFileSync(recordFile, 'utf8');
|
||||
@@ -161,6 +182,12 @@ try {
|
||||
await page.click('[data-report-id]');
|
||||
await page.waitForSelector('#s-report.active');
|
||||
assert.equal(await page.locator('#stat-age b').textContent(), String(result.vm.skinAgeRaw), 'old saved display clamp must not override original skin age');
|
||||
assert.equal(await page.locator('#stat-score b').textContent(), String(expectedTotal), 'old H5 report uses saved dimensions, not saved upstream total');
|
||||
const legacyList = await (await api.get(base + 'api/admin/records')).json();
|
||||
assert.equal(legacyList.items[0].total_score, expectedTotal);
|
||||
const legacyDetail = await (await api.get(base + 'api/admin/records/' + result.record_id)).json();
|
||||
assert.equal(legacyDetail.record.total_score, expectedTotal);
|
||||
assert.equal((await (await api.get(base + 'api/admin/export')).text()).trim().split('\n')[1].split(',')[12], String(expectedTotal));
|
||||
assert.equal(await page.locator('#product-title').textContent(), ReportUI.products[1].title);
|
||||
assert.equal(await page.locator('#product-image').getAttribute('src'), ReportUI.products[1].image, 'refresh artwork crop, never the historical recommendation/copy');
|
||||
assert((await page.locator('.ditem-para').allTextContents()).includes(legacy.presentation.views[0].text));
|
||||
@@ -171,6 +198,21 @@ try {
|
||||
assert((await desktop.locator('#d-body').textContent()).includes('前端展示肌龄 ' + result.vm.skinAgeRaw + '岁'));
|
||||
assert.equal(fs.readFileSync(snapshotFile, 'utf8'), storedBytes);
|
||||
assert.equal(fs.readFileSync(recordFile, 'utf8'), recordBytes);
|
||||
// An incomplete set must show no total everywhere, not fall back to the API score.
|
||||
const missingVm = JSON.parse(JSON.stringify(legacy.vm)); missingVm.dims.stab.S = null;
|
||||
const missingRec = JSON.parse(JSON.stringify(record)); missingRec.dims.stab.S = null;
|
||||
fs.writeFileSync(snapshotFile, JSON.stringify({ ...legacy, vm: missingVm }));
|
||||
fs.writeFileSync(recordFile, JSON.stringify(missingRec) + '\n');
|
||||
await page.reload(); await page.waitForSelector('#s-splash.active');
|
||||
await page.click('[data-action="history"]'); await page.click('[data-report-id]');
|
||||
await page.waitForSelector('#s-report.active');
|
||||
assert.equal(await page.locator('#stat-score b').textContent(), '--');
|
||||
assert.equal((await (await api.get(base + 'api/admin/records')).json()).items[0].total_score, null);
|
||||
assert.equal((await (await api.get(base + 'api/admin/records/' + result.record_id)).json()).record.total_score, null);
|
||||
await desktop.goto(base + 'admin.html');
|
||||
assert.equal(await desktop.locator('#tbl-records tbody tr').first().locator('td').nth(6).textContent(), '--');
|
||||
assert.equal((await (await api.get(base + 'api/admin/export')).text()).trim().split('\n')[1].split(',')[12], '');
|
||||
fs.writeFileSync(recordFile, recordBytes);
|
||||
// Missing skin age must not leave the previous product visible in the H5.
|
||||
delete legacy.payload.result.skin_age;
|
||||
const SkinScore = require('../js/skin-score.js');
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
const assert = require('node:assert/strict');
|
||||
const Score = require('../js/skin-score');
|
||||
const dims = (firm, vita, stab) => ({ firm: { S: firm }, vita: { S: vita }, stab: { S: stab } });
|
||||
for (const [scores, total] of [
|
||||
[[8, 9, 8.5], 85], [[10, 10, 10], 100], [[7, 7, 7], 70],
|
||||
[[10, 7, 7], 82], [[7, 10, 7], 79], [[7, 7, 10], 79],
|
||||
[[8.01, 8.82, 8], 83], [[7.69, 8.72, 9.1], 84]
|
||||
]) assert.equal(Score.weightedTotal(dims(...scores)), total, scores.join('/'));
|
||||
for (const invalid of [null, undefined, NaN, Infinity, -Infinity, '9', -1, 11]) {
|
||||
for (const key of ['firm', 'vita', 'stab']) {
|
||||
const d = dims(8, 9, 8.5); d[key].S = invalid;
|
||||
assert.equal(Score.weightedTotal(d), null, 'no reweighting invalid/missing ' + key);
|
||||
}
|
||||
}
|
||||
for (const d of [null, {}, { firm: { S: 9 }, vita: { S: 8 } }]) assert.equal(Score.weightedTotal(d), null);
|
||||
const input = JSON.parse(JSON.stringify(Score.MOCK_RESULT));
|
||||
const before = JSON.stringify(input);
|
||||
const expected = Score.buildViewModel(input, Score.MOCK_SIDE, {});
|
||||
assert.equal(expected.totalRule, 'dimensions-433-v1');
|
||||
assert.equal(JSON.stringify(input), before, 'preserve raw upstream data');
|
||||
for (const value of [0, 100, null, undefined]) {
|
||||
const r = JSON.parse(JSON.stringify(input));
|
||||
if (value === undefined) delete r.score_info.total_score; else r.score_info.total_score = value;
|
||||
const changed = Score.buildViewModel(r, Score.MOCK_SIDE, {});
|
||||
assert.equal(changed.total, expected.total, 'API total has no influence');
|
||||
assert.deepEqual(changed.dims, expected.dims);
|
||||
assert.deepEqual(changed.recommendation, expected.recommendation);
|
||||
}
|
||||
assert.equal(Score.buildViewModel({}, null, {}).total, null);
|
||||
console.log('PASS weighted total: 4:3:3, integer/half rounding, missing/invalid dimensions, API total ignored, source/scores/recommendation unchanged');
|
||||
Reference in New Issue
Block a user