fix(recommendation): use displayed skin age across reports and admin

This commit is contained in:
xsl
2026-10-16 19:03:25 +08:00
parent 571d78a2ed
commit bb6da17f54
12 changed files with 106 additions and 35 deletions
+4 -4
View File
@@ -427,8 +427,8 @@ function renderDetail(data) {
if (r.detect_status === '成功') {
h += dSec('综合', '<div class="d-kv">' +
'<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>原始肌龄(接口返回) <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>' +
@@ -439,7 +439,7 @@ function renderDetail(data) {
var tone = r.tone_scoring;
h += '<p class="sub">肤色项=色素扣斑分×80%+黑眼圈评分×20%;' + (r.vita_score_rule === 'independent-color-water-smooth-min-v1' ? '该历史版本透亮感取独立肤色、水分、平滑度三项最低分。' : '肤色项与水分项各占50%合成肤色水润分,透亮感取肤色水润与平滑度的最低分。') + '色素扣斑分:' + esc(fmtToneScore(tone.pigment_adjusted_score)) + ',黑眼圈评分:' + esc(fmtToneScore(tone.dark_circle_score)) + ',独立肤色项:' + esc(fmtToneScore(tone.color_score)) + ',水分项:' + esc(fmtToneScore(tone.water_adjusted_score)) + '。肤色缺项按有效权重归一。</p>';
}
h += '<p class="sub">前端展示肌龄限制在实际年龄±5岁;推荐仍使用原始肌龄。历史报告保留当时的推荐。</p>';
h += '<p class="sub">展示肌龄限制在实际年龄±5岁;当前推荐使用报告展示肌龄及边界规则。原始检测数据和历史快照保留。</p>';
/* 6 分类表格 */
var catRows = '';
@@ -532,7 +532,7 @@ function renderDetail(data) {
var score = rec.scores[sk];
scoreParts.push(DIM_NAMES[sk] + ':' + (score == null ? '未获取' : String(score)));
}
h += dSec('推荐依据', '<div class="d-kv"><span>使用原始肌龄 <b>' + ageText(rec.skinAge) + '</b></span>' +
h += dSec('推荐依据', '<div class="d-kv"><span>' + (rec.ageBasis === 'display' ? '使用展示肌龄' : '使用原始肌龄') + ' <b>' + ageText(rec.skinAge) + '</b></span>' +
'<span>基础档 <b>' + esc(tierText(rec.baseTier)) + '</b></span>' +
'<span>最终档 <b>' + esc(tierText(rec.tier)) + '</b></span></div>' +
'<div class="d-copy">' + esc(rec.reason) + '</div><div class="sub">判定分(未舍入S,不使用前端一位小数):' + esc(scoreParts.join(';')) + '</div>');
+1
View File
@@ -1669,6 +1669,7 @@
refresh();
}
function renderReport(vm, frontPhoto, sidePhoto, payload, reportPhotos) {
vm=SkinScore.withDisplayRecommendation(vm,payload && payload.result);
activeWrinkleReport={payload:payload,photos:reportPhotos || [frontPhoto,sidePhoto,null],owner:AccountClient.user()&&AccountClient.user().id,started:false};
renderContourSides(reportPhotos || [frontPhoto, sidePhoto, null]);
reportOwner = AccountClient.user() && AccountClient.user().id;
+4
View File
@@ -181,6 +181,10 @@
// Refresh only presentation for old snapshots: scores, selected care order,
// product and original stored objects are never rewritten/recomputed.
function forDisplay(vm, result, saved) {
if (saved && vm.recommendation && vm.recommendation.ageBasis === 'display') {
var product=products.filter(function(p){return p.tier === vm.recommendation.tier;})[0] || null;
if(saved.product!==product)saved = Object.assign({}, saved, { product: product });
}
if (!saved || (saved.gradeRule === gradeRule && saved.copyRule === copyRule)) return saved || build(vm, result);
var current = build(vm, result);
var care = (saved.care || []).map(function(item) {
+22 -7
View File
@@ -628,12 +628,12 @@
var firm = isNum(scores.firm) ? scores.firm : null;
var vita = isNum(scores.vita) ? scores.vita : null;
var stab = isNum(scores.stab) ? scores.stab : null;
var rec = { version: '2026-10-12-skin-age-v1', skinAge: skinAge,
var rec = { version: '2026-10-16-display-skin-age-v2', ageBasis: 'display', skinAge: skinAge,
baseTier: null, tier: null, boundary: null, adjustment: 'none',
scores: { firm: firm, vita: vita, stab: stab }, reason: '', triggers: [] };
if (!isNum(skinAge) || skinAge <= 0) {
rec.adjustment = 'unavailable';
rec.reason = '未获取有效原始肌龄,暂不推荐具体产品;不以实际年龄代替。';
rec.reason = '未获取有效展示肌龄,暂不推荐具体产品;不以实际年龄代替。';
return rec;
}
rec.baseTier = skinAge < 35 ? 1 : (skinAge < 50 ? 2 : 3);
@@ -653,11 +653,11 @@
rec.reason = '处于' + rec.boundary + '岁上侧边界,紧塑力≥9且透亮感或稳韧力≥9,降一档。';
rec.triggers.push(rec.boundary + '岁边界高分降档');
} else if (skinAge < 32) {
rec.reason = '原始肌龄小于32岁,固定推荐25+,轮廓或皱纹低分不升档。';
rec.reason = '展示肌龄小于32岁,固定推荐25+,轮廓或皱纹低分不升档。';
} else if (rec.boundary != null) {
rec.reason = '处于' + rec.boundary + '岁边界,未满足升降档条件,保持肤龄基础档。';
} else {
rec.reason = '不在35±3或50±3岁边界内,仅按原始肌龄推荐。';
rec.reason = '不在35±3或50±3岁边界内,仅按展示肌龄推荐。';
}
return rec;
}
@@ -777,7 +777,7 @@
if (!worst || cP < worst.P) worst = { catKey: def.key, name: def.name, P: cP, S: sShow(cP) };
}
var recommendation = recommendProduct(skinAge, {
var recommendation = recommendProduct(skinAgeDisplay, {
firm: dims.firm.P == null ? null : sRaw(dims.firm.P),
vita: dims.vita.P == null ? null : sRaw(dims.vita.P),
stab: dims.stab.P == null ? null : sRaw(dims.stab.P)
@@ -934,7 +934,7 @@
};
// Apply the current display rule to new and historical reports without
// modifying their raw age, stored snapshot, copy or recommendation.
// modifying their raw age or stored snapshot. Recommendation uses this age.
function reportSkinAge(vm, result) {
var raw = vm && Object.prototype.hasOwnProperty.call(vm, 'skinAgeRaw') ? val(vm.skinAgeRaw) : val(result && result.skin_age);
if (!isNum(raw) || raw < 0) return null;
@@ -942,7 +942,22 @@
if (!isNum(actual) || actual < 0) return raw;
return Math.max(actual - 5, Math.min(actual + 5, raw));
}
function withDisplayRecommendation(vm, result) {
if (!vm) return vm;
var age = reportSkinAge(vm, result), scores = {};
['firm','vita','stab'].forEach(function(key) {
var d = vm.dims && vm.dims[key];
var saved = vm.recommendation && vm.recommendation.scores && vm.recommendation.scores[key];
scores[key] = isNum(saved) ? saved : d && isNum(d.P) ? sRaw(d.P) : d && isNum(d.S) ? d.S : null;
});
var recommendation = recommendProduct(age, scores), tier = recommendation.tier;
return Object.assign({}, vm, { skinAgeDisplay: age, recommendation: recommendation,
group: tier == null ? null : ['25+组','35+组','50+组'][tier-1],
essence: { name: tier == null ? '暂无法推荐' : ESSENCE[tier].name,
text: tier == null ? recommendation.reason : ESSENCE[tier].text, tier: tier, triggers: recommendation.triggers } });
}
return {
withDisplayRecommendation: withDisplayRecommendation,
capDimensionScore: capDimensionScore,
weightedTotal: weightedTotal,
reportSkinAge: reportSkinAge,
@@ -1020,7 +1035,7 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
ok(vm2.dims.firm.P === Math.min(vm2.cats.contour.P, vm2.cats.wrinkles.P)
&& vm2.dims.vita.P === Math.min(vm2.cats.smooth.P, vm2.cats.tone.P)
&& vm2.dims.stab.P === Math.min(vm2.cats.sens.P, vm2.cats.acne.P), '三维度 P = min(旗下两分类)');
ok(vm2.group === '25+组', '实际年龄40不影响推荐,原始肌龄20仍为25+组');
ok(vm2.group === '35+组', '实际年龄40、原始肌龄20、展示肌龄35,按展示肌龄推荐35+组');
console.log('== T3 原始肌龄展示 ==');
var r3 = clone(SS.MOCK_RESULT);