fix(report): recommend by measured skin age and align admin results
This commit is contained in:
+56
-19
@@ -24,7 +24,7 @@ function fmtMs(ms) { return ms == null ? '-' : ms + 'ms'; }
|
||||
function fmtScore(v) { return v == null ? '-' : v; }
|
||||
|
||||
var LV_CLASS = { '预警': 'lv-bad', '改善': 'lv-warn', '良好': 'lv-good' };
|
||||
var DIM_NAMES = { firm: '紧塑力', vita: '活力感', stab: '稳定力' };
|
||||
var DIM_NAMES = ReportUI.dimNames;
|
||||
var CAT_ORDER = ['contour', 'wrinkles', 'smooth', 'tone', 'sens', 'acne'];
|
||||
var ST_CLASS = { '成功': 'st-ok', '失败': 'st-fail', '检测中': 'st-ing', '待检测': 'st-wait' };
|
||||
|
||||
@@ -60,15 +60,16 @@ function switchTab(name) {
|
||||
/* ─── 档位标签 ─── */
|
||||
function lvHtml(level) {
|
||||
if (!level) return '<span class="sub">-</span>';
|
||||
return '<span class="lv ' + (LV_CLASS[level] || '') + '">' + esc(level) + '</span>';
|
||||
return '<span class="lv ' + (LV_CLASS[level] || '') + '">' + esc(ReportUI.levelMap[level] || level) + '</span>';
|
||||
}
|
||||
function dimsHtml(dims) {
|
||||
if (!dims) return '<span class="sub">-</span>';
|
||||
var h = '';
|
||||
for (var k in DIM_NAMES) {
|
||||
var d = dims[k];
|
||||
h += '<span class="lv ' + (d && LV_CLASS[d.level] || '') + '" title="' + DIM_NAMES[k] + '">' +
|
||||
DIM_NAMES[k].slice(0, 1) + (d ? esc(d.level) : '-') + '</span>';
|
||||
h += '<div><span class="lv ' + (d && LV_CLASS[d.level] || '') + '" title="' + DIM_NAMES[k] + '">' +
|
||||
DIM_NAMES[k] + ' ' + (d && typeof d.S === 'number' ? d.S.toFixed(1) : '-') + ' ' +
|
||||
(d ? esc(ReportUI.levelMap[d.level] || '-') : '-') + '</span></div>';
|
||||
}
|
||||
return h;
|
||||
}
|
||||
@@ -80,6 +81,19 @@ function profileHtml(it) {
|
||||
parts.push(it.daily_skin_type || '-');
|
||||
return esc(parts.join(' / '));
|
||||
}
|
||||
function ageText(age) { return age == null ? '-' : esc(age) + '岁'; }
|
||||
function skinAgeHtml(r) {
|
||||
return '<b>原始 ' + ageText(r.skin_age) + '</b><div class="sub">展示 ' + ageText(r.skin_age_display) + '</div>';
|
||||
}
|
||||
function productName(name) {
|
||||
for (var i = 0; i < ReportUI.products.length; i++) {
|
||||
var p = ReportUI.products[i];
|
||||
if (p.match === name) return p.title + '(' + ['25+', '35+', '50+'][i] + ')';
|
||||
}
|
||||
return name || '-';
|
||||
}
|
||||
function ruleLabel(r) { return r.recommendation ? r.recommendation.version : '旧版规则(历史记录)'; }
|
||||
function tierText(tier) { return tier ? ['25+ / I', '35+ / II', '50+ / III'][tier - 1] : '无'; }
|
||||
|
||||
/* ═══════════ 记录列表 ═══════════ */
|
||||
var pageState = { page: 1, page_size: 20, total: 0 };
|
||||
@@ -108,7 +122,7 @@ function loadRecords() {
|
||||
var tbody = $('#tbl-records tbody');
|
||||
tbody.innerHTML = '';
|
||||
if (!data.items.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="11" style="text-align:center;color:#8a94a6;padding:32px">暂无记录</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="12" style="text-align:center;color:#8a94a6;padding:32px">暂无记录</td></tr>';
|
||||
}
|
||||
for (var i = 0; i < data.items.length; i++) {
|
||||
(function (it) {
|
||||
@@ -126,8 +140,9 @@ function loadRecords() {
|
||||
'<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>' + skinAgeHtml(it) + '</td>' +
|
||||
'<td>' + dimsHtml(it.dims) + '</td>' +
|
||||
'<td>' + esc(it.essence || '-') + '</td>' +
|
||||
'<td>' + esc(productName(it.essence)) + '<div class="sub">' + esc(ruleLabel(it)) + '</div></td>' +
|
||||
'<td class="mono">' + fmtMs(it.cost_ms) + '</td>' +
|
||||
'<td class="mono">' + fmtTime(it.created_at) + '</td>';
|
||||
tr.addEventListener('click', function (e) {
|
||||
@@ -230,7 +245,7 @@ function loadStats() {
|
||||
for (var l = 0; l < levels.length; l++) {
|
||||
var cnt = dist[levels[l][0]] || 0;
|
||||
bars += '<div style="display:flex;align-items:center;gap:8px;margin:3px 0">' +
|
||||
'<span style="width:34px;color:#5a6578;font-size:12px">' + levels[l][0] + '</span>' +
|
||||
'<span style="width:34px;color:#5a6578;font-size:12px">' + ReportUI.levelMap[levels[l][0]] + '</span>' +
|
||||
'<span style="flex:1;height:14px;background:#f0f2f5;border-radius:3px;overflow:hidden">' +
|
||||
'<i style="display:block;height:100%;width:' + (cnt * 100 / max) + '%" class="' + levels[l][1] + '"></i></span>' +
|
||||
'<span style="width:24px;text-align:right;font-size:12px;color:#4a5568">' + cnt + '</span></div>';
|
||||
@@ -342,7 +357,7 @@ function renderDetail(data) {
|
||||
|
||||
/* 档案 */
|
||||
h += dSec('档案信息', '<div class="d-kv">' +
|
||||
'<span>年龄 <b>' + (r.real_age != null ? r.real_age + '岁' : '-') + '</b></span>' +
|
||||
'<span>实际年龄 <b>' + ageText(r.real_age) + '</b></span>' +
|
||||
'<span>性别 <b>' + esc(r.gender || '-') + '</b></span>' +
|
||||
'<span>地区 <b>' + esc(r.region || '-') + '</b></span>' +
|
||||
'<span>日常肤质 <b>' + esc(r.daily_skin_type || '-') + '</b></span>' +
|
||||
@@ -374,20 +389,22 @@ function renderDetail(data) {
|
||||
if (r.detect_status === '成功') {
|
||||
h += dSec('综合', '<div class="d-kv">' +
|
||||
'<span>总分 <b>' + fmtScore(r.total_score) + '</b></span>' +
|
||||
'<span>肌肤年龄(接口原始) <b>' + fmtScore(r.skin_age) + '岁</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">前端展示肌龄沿用实际年龄±3岁的修正;新推荐只使用原始肌龄。历史报告保留当时的推荐。</p>';
|
||||
|
||||
/* 6 分类表格 */
|
||||
var catRows = '';
|
||||
for (var i = 0; i < CAT_ORDER.length; i++) {
|
||||
var ck = CAT_ORDER[i], cat = r.cats && r.cats[ck];
|
||||
var catName = (cat && cat.name) || ({ contour: '轮廓', wrinkles: '皱纹', smooth: '光滑', tone: '肤色水润', sens: '敏感', acne: '痘痘' })[ck];
|
||||
var catName = ({ contour: '轮廓', wrinkles: '皱纹', smooth: '平滑度', tone: '肤色 / 水分', sens: '泛红', acne: '痘痘' })[ck];
|
||||
catRows += '<tr><td>' + esc(catName) + '</td><td>' + fmtScore(cat && cat.P) + '</td><td>' + fmtScore(cat && cat.S) + '</td><td>' + lvHtml(cat && cat.level) + '</td></tr>';
|
||||
}
|
||||
h += dSec('六分类评分', '<table class="d-table"><thead><tr><th>分类</th><th>P</th><th>S</th><th>档位</th></tr></thead><tbody>' + catRows + '</tbody></table>');
|
||||
h += dSec('原始六分类评分(肤色 / 水分共用一项)', '<table class="d-table"><thead><tr><th>分类</th><th>P</th><th>S</th><th>档位</th></tr></thead><tbody>' + catRows + '</tbody></table>');
|
||||
|
||||
/* 3 维度表格 */
|
||||
var dimRows = '';
|
||||
@@ -399,7 +416,13 @@ function renderDetail(data) {
|
||||
|
||||
/* 文案全文 */
|
||||
var copyHtml = '';
|
||||
if (r.copy_6) {
|
||||
var presentation = r.presentation;
|
||||
if (presentation && presentation.views) {
|
||||
for (var v = 0; v < presentation.views.length; v++) {
|
||||
var view = presentation.views[v];
|
||||
copyHtml += '<div class="d-copy"><b>' + esc(view.name) + ' / ' + esc(view.level || '未获取') + ':</b>' + esc(view.text) + '</div>';
|
||||
}
|
||||
} else if (r.copy_6) {
|
||||
for (var c2 = 0; c2 < CAT_ORDER.length; c2++) {
|
||||
var txt = r.copy_6[CAT_ORDER[c2]];
|
||||
if (txt) copyHtml += '<div class="d-copy"><b>' + esc((r.cats[CAT_ORDER[c2]] || {}).name || '') + ':</b>' + esc(txt) + '</div>';
|
||||
@@ -407,19 +430,33 @@ function renderDetail(data) {
|
||||
}
|
||||
if (r.copy_summary) copyHtml += '<div class="d-copy"><b>综合文案:</b>' + esc(r.copy_summary) + '</div>';
|
||||
if (r.lifestyle) copyHtml += '<div class="d-copy"><b>生活方式:</b>' + esc(r.lifestyle) + '</div>';
|
||||
if (copyHtml) h += dSec('生成文案', copyHtml);
|
||||
if (copyHtml) h += dSec(presentation ? '前端报告结论(保存快照)与计算摘要' : '旧版生成文案(无前端快照)', copyHtml);
|
||||
|
||||
if (r.careAdvice && r.careAdvice.length) {
|
||||
var care = presentation ? presentation.care : r.careAdvice;
|
||||
if (care && care.length) {
|
||||
var advice = '';
|
||||
for (var a = 0; a < r.careAdvice.length; a++) {
|
||||
advice += '<div class="d-copy"><b>[' + esc(r.careAdvice[a].tag) + ']</b>' + esc(r.careAdvice[a].text) + '</div>';
|
||||
for (var a = 0; a < care.length; a++) {
|
||||
advice += '<div class="d-copy"><b>[' + esc(care[a].name || care[a].tag) + ']</b>' + esc(care[a].text) + '</div>';
|
||||
}
|
||||
h += dSec('护理建议', advice);
|
||||
}
|
||||
|
||||
h += dSec('推荐精华', '<div class="d-copy"><b>' + esc((r.essence && r.essence.name) || '-') + '</b><br>' +
|
||||
esc((r.essence && r.essence.text) || '') + '</div>' +
|
||||
'<div class="d-kv"><span>触发判据 <b>' + esc(r.trigger || '-') + '</b></span></div>');
|
||||
var product = presentation && presentation.product;
|
||||
h += dSec('推荐精华', '<div class="d-copy"><b>' + esc(productName(r.essence && r.essence.name)) + '</b><br>' +
|
||||
esc(product ? product.name + '双仓精华液' : (r.essence && r.essence.text) || '') + '</div>' +
|
||||
'<div class="d-kv"><span>规则版本 <b>' + esc(ruleLabel(r)) + '</b></span>' +
|
||||
'<span>触发判据 <b>' + esc(r.trigger || '-') + '</b></span></div>');
|
||||
if (r.recommendation) {
|
||||
var rec = r.recommendation, scoreParts = [];
|
||||
for (var sk in DIM_NAMES) {
|
||||
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>' +
|
||||
'<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>');
|
||||
}
|
||||
|
||||
if (r.notes && r.notes.length) {
|
||||
var notes = '<ul class="d-notes">';
|
||||
|
||||
@@ -1194,6 +1194,7 @@
|
||||
var product = presentation.product;
|
||||
$('.card-product').hidden = !product;
|
||||
$('.prod-title').hidden = !product;
|
||||
$('#product-unavailable').hidden = !!product;
|
||||
if (product) {
|
||||
$('#product-title').textContent = product.title;
|
||||
$('#product-name').textContent = product.name + '双仓精华液';
|
||||
|
||||
+3
-2
@@ -15,6 +15,7 @@
|
||||
{ tier: 3, match: '50+重塑精华', title: "1'me 主场精华 III", name: '以我从容有态构塑', image: 'assets/img/ui-20261009/product-50.png' }
|
||||
];
|
||||
var levelMap = { '预警': '改善', '改善': '良好', '良好': '优秀' };
|
||||
var dimNames = { firm: '紧塑力', vita: '透亮感', stab: '稳韧力' };
|
||||
function numeric(value) { return typeof value === 'number' && isFinite(value); }
|
||||
function fill(template, vars) {
|
||||
return template.replace(/\{([a-z_]+)\}/g, function(_, key) {
|
||||
@@ -66,7 +67,7 @@
|
||||
var chosen = views.filter(function(v) { return ranked.indexOf(v.source) >= 0 && v.advice; });
|
||||
var care = chosen.map(function(v) { return { name: v.name, text: v.advice.care + v.advice.lifestyle }; });
|
||||
var product = products.filter(function(p) { return p.match === vm.essence.name; })[0] || null;
|
||||
return { version: Copy.version, dims: { firm: '紧塑力', vita: '透亮感', stab: '稳韧力' }, views: views, care: care, product: product };
|
||||
return { version: Copy.version, dims: dimNames, views: views, care: care, product: product };
|
||||
}
|
||||
return { build: build, products: products, levelMap: levelMap };
|
||||
return { build: build, products: products, levelMap: levelMap, dimNames: dimNames };
|
||||
});
|
||||
|
||||
+72
-39
@@ -1,5 +1,6 @@
|
||||
/* ═════ 巨子测肤 H5 — 三维度×6分类评分引擎(纯逻辑,浏览器/Node 双端) ═════
|
||||
* 最高契约:docs/三维度评分与话术生成需求文档_V2.2.docx(公式/锁值/话术/产品推荐以其为准)
|
||||
* 评分契约:docs/三维度评分与话术生成需求文档_V2.2.docx(公式/锁值/原始话术)
|
||||
* 推荐覆盖契约:docs/产品推荐规则-肤龄版.md(2026-10-12 用户确认)
|
||||
* 浏览器:window.SkinScore;Node:module.exports。ES5、零依赖、不碰 DOM。
|
||||
*
|
||||
* 字段口径(已对 reference/facepp-skinanalyze/examples/ 真实 pro JSON 复核):
|
||||
@@ -63,7 +64,7 @@
|
||||
function round2(x) { return Math.round((x + 1e-9) * 100) / 100; } // 1e-9 修正 9.445 类浮点
|
||||
function sRaw(P) { return 7 + 3 * P / 100; }
|
||||
function sShow(P) { // 展示层钳制:P 可为负(肤色扣分/大面积敏感),S 会 <7,与文档「S 7.0–10.0」口径矛盾;
|
||||
if (P == null) return null; // 仅钳 viewModel 展示值,判档/最差项/精华三重判据仍用未舍入原值
|
||||
if (P == null) return null; // 仅钳展示值,推荐边界比较仍用未舍入维度 S 原值
|
||||
var s = sRaw(P);
|
||||
return round2(s < 7 ? 7 : (s > 10 ? 10 : s));
|
||||
}
|
||||
@@ -589,6 +590,47 @@
|
||||
3: { name: '50+重塑精华', text: '建议搭配使用50+重塑精华:针对熟龄肌松弛与轮廓流失问题,深层重塑支撑结构,改善明显松弛与纹路,重建面部紧实与弹性。' }
|
||||
};
|
||||
|
||||
// 2026-10-12 已确认的业务规则。只接收原始肤龄和三维度未舍入 S,
|
||||
// 不使用实际年龄、展示肤龄、六分类均分或最差分类;历史快照不重算。
|
||||
function recommendProduct(skinAge, scores) {
|
||||
scores = scores || {};
|
||||
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,
|
||||
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 = '未获取有效原始肌龄,暂不推荐具体产品;不以实际年龄代替。';
|
||||
return rec;
|
||||
}
|
||||
rec.baseTier = skinAge < 35 ? 1 : (skinAge < 50 ? 2 : 3);
|
||||
rec.tier = rec.baseTier;
|
||||
rec.triggers.push('肤龄基础分组');
|
||||
if (skinAge >= 32 && skinAge <= 38) rec.boundary = 35;
|
||||
else if (skinAge >= 47 && skinAge <= 53) rec.boundary = 50;
|
||||
if (rec.boundary != null && skinAge < rec.boundary && firm != null && firm < 8) {
|
||||
rec.tier++;
|
||||
rec.adjustment = 'up';
|
||||
rec.reason = '处于' + rec.boundary + '岁下侧边界,紧塑力低于8分,升一档。';
|
||||
rec.triggers.push(rec.boundary + '岁边界低分升档');
|
||||
} else if (rec.boundary != null && skinAge >= rec.boundary && firm != null && firm >= 9
|
||||
&& ((vita != null && vita >= 9) || (stab != null && stab >= 9))) {
|
||||
rec.tier--;
|
||||
rec.adjustment = 'down';
|
||||
rec.reason = '处于' + rec.boundary + '岁上侧边界,紧塑力≥9且透亮感或稳韧力≥9,降一档。';
|
||||
rec.triggers.push(rec.boundary + '岁边界高分降档');
|
||||
} else if (skinAge < 32) {
|
||||
rec.reason = '原始肌龄小于32岁,固定推荐25+,轮廓或皱纹低分不升档。';
|
||||
} else if (rec.boundary != null) {
|
||||
rec.reason = '处于' + rec.boundary + '岁边界,未满足升降档条件,保持肤龄基础档。';
|
||||
} else {
|
||||
rec.reason = '不在35±3或50±3岁边界内,仅按原始肌龄推荐。';
|
||||
}
|
||||
return rec;
|
||||
}
|
||||
|
||||
/* ─── V2.3 §8 护理建议 ─── */
|
||||
var SKIN_TYPE_TEXT = { // §8.6 五类映射【肤质定制】段全文 + 联动句
|
||||
'干性肌肤': { base: '你的日常肤质为干性肌肤:全脸皮脂分泌偏少,角质层保水能力弱,易紧绷起屑。护理以高效保湿与封闭锁水为核心,洁面选用温和低泡产品,避免频繁去角质,面霜可选择含神经酰胺、角鲨烷等封闭性成分的配方。',
|
||||
@@ -651,14 +693,12 @@
|
||||
var skinAge = val(r.skin_age);
|
||||
var skinAgeDisplay = skinAge, ageClampNote = '';
|
||||
if (realAge == null) {
|
||||
notes.push('未提供实际年龄:产品组别、肌肤年龄钳制与精华「年龄组档」判据均不生效');
|
||||
notes.push('未提供实际年龄:不修正展示肌龄;产品推荐仍使用接口原始肌龄');
|
||||
} else if (skinAge != null) {
|
||||
skinAgeDisplay = Math.min(Math.max(skinAge, realAge - 3), realAge + 3);
|
||||
if (skinAgeDisplay !== skinAge) ageClampNote = '触发(原' + skinAge + '→' + skinAgeDisplay + ')';
|
||||
}
|
||||
|
||||
var group = null;
|
||||
if (realAge != null) group = realAge < 32 ? '25+组' : (realAge <= 45 ? '35+组' : '50+组');
|
||||
var skinTypeCn = skinTypeCnOf(r, profile); // {skin_type_cn}:接口枚举优先,回退用户自选肤质
|
||||
|
||||
// 6 分类
|
||||
@@ -707,25 +747,16 @@
|
||||
if (!worst || cP < worst.P) worst = { catKey: def.key, name: def.name, P: cP, S: sShow(cP) };
|
||||
}
|
||||
|
||||
// 精华推荐:三重判据取最强档,triggers 记录命中(档位=最终档)的判据
|
||||
var crits = []; // {name, tier}
|
||||
if (avgRaw != null) crits.push({ name: '均分档', tier: avgRaw >= 9 ? 1 : (avgRaw >= 8 ? 2 : 3) });
|
||||
if (worst) {
|
||||
var wS = sRaw(worst.P);
|
||||
if (wS < 7) crits.push({ name: '最差项约束', tier: 3 });
|
||||
else if (wS < 8) crits.push({ name: '最差项约束', tier: 2 });
|
||||
}
|
||||
if (group) crits.push({ name: '年龄组档', tier: group === '25+组' ? 1 : (group === '35+组' ? 2 : 3) });
|
||||
var tier = 1;
|
||||
if (crits.length) {
|
||||
tier = crits[0].tier;
|
||||
for (i = 0; i < crits.length; i++) tier = Math.max(tier, crits[i].tier); // 推荐强度只升不降
|
||||
} else {
|
||||
notes.push('三重判据均不可用,精华推荐缺省为 25+束紧精华');
|
||||
}
|
||||
var triggers = [];
|
||||
for (i = 0; i < crits.length; i++) if (crits[i].tier === tier) triggers.push(crits[i].name);
|
||||
var essence = { name: ESSENCE[tier].name, text: ESSENCE[tier].text, triggers: triggers };
|
||||
var recommendation = recommendProduct(skinAge, {
|
||||
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)
|
||||
});
|
||||
var tier = recommendation.tier, triggers = recommendation.triggers;
|
||||
var group = tier == null ? null : ['25+组', '35+组', '50+组'][tier - 1];
|
||||
var essence = { name: tier == null ? '暂无法推荐' : ESSENCE[tier].name,
|
||||
text: tier == null ? recommendation.reason : ESSENCE[tier].text, tier: tier, triggers: triggers };
|
||||
if (tier == null) notes.push(recommendation.reason);
|
||||
|
||||
// 近期优先关注:最差维度
|
||||
var focus = { name: '', tip: '' }, fP = null;
|
||||
@@ -764,7 +795,7 @@
|
||||
], ',');
|
||||
summary = '【综合】' + (head ? head + ';' : '')
|
||||
+ '六分类S均分' + fmtS(avgRaw) + (worst ? '、最差分类' + worst.name + ' S=' + fmtS(sRaw(worst.P)) : '') + '。'
|
||||
+ '按三重判据' + (triggers.length ? '(触发:' + triggers.join('、') + ')' : '') + '推荐' + essence.name
|
||||
+ (tier == null ? recommendation.reason : '按肤龄及边界规则(触发:' + triggers.join('、') + ')推荐' + essence.name)
|
||||
+ (improve ? ':' + improve : '。')
|
||||
+ (tail ? tail + '。' : '');
|
||||
}
|
||||
@@ -795,6 +826,7 @@
|
||||
cats: cats,
|
||||
group: group,
|
||||
essence: essence,
|
||||
recommendation: recommendation,
|
||||
avgS: avgS,
|
||||
worst: worst,
|
||||
focus: focus,
|
||||
@@ -868,6 +900,7 @@
|
||||
|
||||
return {
|
||||
buildViewModel: buildViewModel,
|
||||
recommendProduct: recommendProduct,
|
||||
MOCK_RESULT: MOCK_RESULT,
|
||||
MOCK_SIDE: MOCK_SIDE
|
||||
};
|
||||
@@ -909,14 +942,14 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
|
||||
ok(vm.cats.wrinkles.S === 7.69 && vm.cats.smooth.S === 9.45 && vm.cats.tone.S === 9.45, 'S=7+3×P/100 两位小数(含 9.445→9.45 浮点修正)');
|
||||
ok(vm.dims.firm.P === 23 && vm.dims.vita.P === 81.5 && vm.dims.stab.P === 85, '维度木桶 min:紧塑力23 / 活力感81.5 / 稳定力85');
|
||||
ok(vm.skinAgeRaw === 20 && vm.skinAgeDisplay === 22 && vm.ageClampNote === '触发(原20→22)', '肌肤年龄钳制 20→22 留痕,原值不改写');
|
||||
ok(vm.group === '25+组', 'realAge=25 → 25+组');
|
||||
ok(vm.essence.name === '35+支撑精华' && vm.essence.triggers.length === 1 && vm.essence.triggers[0] === '最差项约束',
|
||||
'精华三重判据:均分9.32→25+、最差S7.69→至少35+、年龄→25+,取最强档35+,triggers=["最差项约束"]');
|
||||
ok(vm.group === '25+组', '原始肌龄20 → 25+组');
|
||||
ok(vm.essence.name === '25+束紧精华' && vm.recommendation.tier === 1,
|
||||
'肌龄小于32,即使皱纹低分也固定25+,不再使用最差项升档');
|
||||
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.summary.indexOf('【综合】') === 0 && vm.summary.indexOf('35+支撑精华') > 0, '综合文案生成');
|
||||
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)');
|
||||
checkTexts(vm, 'T0');
|
||||
@@ -939,7 +972,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 === '35+组', 'realAge=40 → 35+组');
|
||||
ok(vm2.group === '25+组', '实际年龄40不影响推荐,原始肌龄20仍为25+组');
|
||||
|
||||
console.log('== T3 钳制边界 ==');
|
||||
var r3 = clone(SS.MOCK_RESULT);
|
||||
@@ -950,7 +983,7 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
|
||||
var vm3b = SS.buildViewModel(r3, SS.MOCK_SIDE, { realAge: 25 });
|
||||
ok(vm3b.skinAgeDisplay === 28 && vm3b.ageClampNote === '触发(原40→28)' && vm3b.skinAgeRaw === 40, '上边界钳制 40→28');
|
||||
|
||||
console.log('== T4 精华三重判据:最差项 S<7 → 50+重塑 ==');
|
||||
console.log('== T4 最差项不再覆盖肤龄推荐 ==');
|
||||
var r4 = clone(SS.MOCK_RESULT);
|
||||
r4.score_info.melanin_score = 0;
|
||||
r4.score_info.water_score = 0;
|
||||
@@ -959,8 +992,8 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
|
||||
r4.water = { water_score: 0, water_area: 40, water_severity: 70 }; // sev≥60 但 score<80,不触发换算
|
||||
var vm4 = SS.buildViewModel(r4, SS.MOCK_SIDE, { realAge: 25 });
|
||||
ok(approx(vm4.cats.tone.P, -7.5), '肤色水润P = 0.5×(0−10−5)+0.5×0 = −7.5(P 原值不动,不夹逼)');
|
||||
ok(vm4.cats.tone.S === 7, '展示层 S 钳到 7(未钳原值 6.78 仍参与判档)');
|
||||
ok(vm4.essence.name === '50+重塑精华' && vm4.essence.triggers.indexOf('最差项约束') >= 0, '最差项 S 原值<7 → 50+重塑精华,triggers 含最差项约束(三重判据用未舍入原值)');
|
||||
ok(vm4.cats.tone.S === 7, '展示层 S 钳到7,评分公式不变');
|
||||
ok(vm4.essence.name === '25+束紧精华', '原始肌龄20,即使最差项S<7也不升档');
|
||||
checkTexts(vm4, 'T4');
|
||||
|
||||
console.log('== T5 缺数据降级(V2.2 第十章异常与边界) ==');
|
||||
@@ -977,9 +1010,9 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
|
||||
var vm5b = SS.buildViewModel(clone(SS.MOCK_RESULT), null, { realAge: 25 });
|
||||
ok(vm5b.cats.contour.P === 100 && vm5b.notes.join('|').indexOf('侧脸') >= 0, '无侧脸 → 轮廓 P 由眼袋单独决定并记 note');
|
||||
var vm5c = SS.buildViewModel(clone(SS.MOCK_RESULT), SS.MOCK_SIDE, {});
|
||||
ok(vm5c.group === null && vm5c.skinAgeDisplay === 20 && vm5c.ageClampNote === ''
|
||||
ok(vm5c.group === '25+组' && vm5c.skinAgeDisplay === 20 && vm5c.ageClampNote === ''
|
||||
&& vm5c.essence.triggers.indexOf('年龄组档') < 0 && vm5c.notes.join('|').indexOf('实际年龄') >= 0,
|
||||
'realAge 缺失 → group=null、不钳制、判据③不生效并记 note');
|
||||
'实际年龄缺失不钳制,但原始肌龄仍可推荐25+');
|
||||
checkTexts(vm5, 'T5');
|
||||
|
||||
console.log('== T6 下颌线 quality 降级 ==');
|
||||
@@ -1138,8 +1171,8 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
|
||||
ok(vm.cats.sens.P === 99 && vm.cats.acne.P === 70 && vm.dims.stab.P === 70 && vm.dims.stab.S === 9.1,
|
||||
'敏感 99 / 痘痘 70(min(分级70, 100−12)),稳定力 70 / S=9.1');
|
||||
ok(vm.avgS === 9.05 && vm.worst.catKey === 'wrinkles' && vm.group === '25+组'
|
||||
&& vm.essence.name === '35+支撑精华' && vm.essence.triggers.join('、') === '最差项约束',
|
||||
'均分 9.05 / 最差皱纹 / 25+组 / 精华 35+支撑(触发:最差项约束,三重判据取最强档)');
|
||||
&& vm.essence.name === '25+束紧精华' && vm.essence.triggers.join('、') === '肤龄基础分组',
|
||||
'评分不变,原始肌龄27推荐25+,不再因最差皱纹升档');
|
||||
ok(vm.skinAgeRaw === 27 && vm.skinAgeDisplay === 27 && vm.ageClampNote === '', '肤龄 27(实际30)未触发钳制、原值不改写');
|
||||
ok(vm.cats.contour.text === '下颌线角度左104.54°/右103.75°(标准区间116–120°,双侧均偏离标准,提示下颌缘支撑力下降);眼袋1级、黑眼圈1级;中面部支撑与轮廓清晰度处于预警水平。建议加强轮廓紧致护理:重点针对下颌线与下颌缘区域使用紧致类精华,配合由下向上的提拉按摩手法,同时加强眼周支撑护理,将紧致防线前置。',
|
||||
'轮廓文案逐字 = 真值表(黑眼圈取 dark_circle_severity=1,非 dark_circle=3)');
|
||||
@@ -1153,8 +1186,8 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
|
||||
'敏感文案逐字 = 真值表');
|
||||
ok(vm.cats.acne.text === '检出9处皮损(闭口粉刺6处、丘疹3处)。建议尽快进行针对性抗痘护理,避免挤压与刺激,必要时寻求皮肤科专业处理。',
|
||||
'痘痘文案逐字 = 真值表(无 heavy_note)');
|
||||
ok(vm.summary === '【综合】实际年龄30岁(接口测得肌肤年龄27岁,差3岁),产品组别25+组;六分类S均分9.05、最差分类皱纹 S=7.69。按三重判据(触发:最差项约束)推荐35+支撑精华:优先改善皱纹(P=23.0)与光滑(P=57.2),其余维度维持现有护理节奏。整体肤质混合性,T区出油面积57%、左右脸颊20%/2%。',
|
||||
'综合文案逐字 = 真值表 AW 列');
|
||||
ok(vm.summary === '【综合】实际年龄30岁(接口测得肌肤年龄27岁,差3岁),产品组别25+组;六分类S均分9.05、最差分类皱纹 S=7.69。按肤龄及边界规则(触发:肤龄基础分组)推荐25+束紧精华:优先改善皱纹(P=23.0)与光滑(P=57.2),其余维度维持现有护理节奏。整体肤质混合性,T区出油面积57%、左右脸颊20%/2%。',
|
||||
'综合文案仅产品判据按2026-10-12规则替换,其余逐字不变');
|
||||
ok(vm.lifestyle === '保持侧卧改仰卧的睡姿习惯,减少枕巾摩擦;控制屏幕时间,缓解眼周疲劳', '生活方式 = 最差分类皱纹条');
|
||||
}());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user