fix(calibration): 校准幂等+已校准状态显示,解决重复提示

根因:预测准确度卡的偏差来自历史回填项目的预测vs实际,属既成事实,
不会因校准改变;且原 apply 公式 next=current+dev 会累加,反复点越推越高。

修复:
- 校准建议/应用均基于未校准原始基准(env/默认)计算,保证幂等
- GET /api/calibration 返回 uncalibratedBase 与 calibrated 标志
- 卡片:已校准时显示「已按当前偏差校准:基准X%」绿色状态,不再出现按钮;
  未校准时按钮明示「X% → Y%」
- 补充说明:历史偏差不会因校准改变,校准仅调整后续承接建议阈值
This commit is contained in:
freedakgmail
2026-06-14 11:01:55 +08:00
parent c715dbb306
commit 8bac14ef44
3 changed files with 67 additions and 19 deletions
+20 -5
View File
@@ -358,6 +358,18 @@ function targetNetMargin(): number {
return Number.isFinite(v) && v > 0 ? v : DEFAULT_TARGET_NET_MARGIN;
}
/** 未校准的原始目标净利率基准(env/默认,忽略校准覆盖)。校准始终基于此基准做一次性补偿,保证幂等。 */
function uncalibratedTargetNetMargin(): number {
const v = Number(process.env.TARGET_NET_MARGIN);
return Number.isFinite(v) && v > 0 ? v : DEFAULT_TARGET_NET_MARGIN;
}
/** 据系统性偏差计算建议的目标净利率基准(基于未校准基准,夹取 [2%,30%])。 */
function suggestedTargetBase(deviationPct: number): number {
const base = uncalibratedTargetNetMargin();
return Math.min(0.3, Math.max(0.02, Math.round((base + deviationPct / 100) * 1000) / 1000));
}
/**
* POST /api/assessments/classify
* 输入项目描述,返回业务类型与行业识别结果。
@@ -885,6 +897,7 @@ app.get('/api/accuracy', async (c) => {
*/
app.get('/api/calibration', async (c) => {
const current = targetNetMargin();
const uncalibrated = uncalibratedTargetNetMargin();
let suggested = current;
let bias: string | null = null;
let deviationPct: number | null = null;
@@ -893,16 +906,18 @@ app.get('/api/calibration', async (c) => {
bias = acc.bias;
deviationPct = acc.avgDeviationPct;
if (acc.avgDeviationPct !== null && acc.count > 0) {
// 预测偏乐观(dev>0)→ 抬高要求的目标净利率以补偿;偏保守则下调。夹取 [2%,30%]
suggested = Math.min(0.3, Math.max(0.02, Math.round((current + acc.avgDeviationPct / 100) * 1000) / 1000));
// 预测偏乐观(dev>0)→ 抬高要求的目标净利率以补偿;偏保守则下调。基于未校准基准,保证幂等
suggested = suggestedTargetBase(acc.avgDeviationPct);
}
}
return c.json({ currentBase: current, suggestedBase: suggested, bias, deviationPct });
// 已校准:当前基准已等于(或非常接近)据当前偏差计算的建议基准,无需再次应用。
const calibrated = calibratedTargetBase !== null && Math.abs(current - suggested) < 1e-6;
return c.json({ currentBase: current, suggestedBase: suggested, uncalibratedBase: uncalibrated, calibrated, bias, deviationPct });
});
/**
* POST /api/calibration/apply
* 应用校准:将目标净利率基准设为据预测偏差计算的建议值(管理层)。
* 应用校准:将目标净利率基准设为据预测偏差计算的建议值(管理层)。基于未校准基准,幂等。
*/
app.post('/api/calibration/apply', requireRole('管理层'), async (c) => {
if (pool === null) return c.json({ error: '未配置数据库' }, 400);
@@ -911,7 +926,7 @@ app.post('/api/calibration/apply', requireRole('管理层'), async (c) => {
return c.json({ error: '暂无足够的实际值回填数据用于校准' }, 400);
}
const current = targetNetMargin();
const next = Math.min(0.3, Math.max(0.02, Math.round((current + acc.avgDeviationPct / 100) * 1000) / 1000));
const next = suggestedTargetBase(acc.avgDeviationPct);
await setSetting(pool, 'targetMarginBase', next);
calibratedTargetBase = next;
return c.json({ appliedBase: next, previousBase: current, deviationPct: acc.avgDeviationPct });