Files
X-Financial/document/development/budget-expense-control-model-plan/index.html
caoxiaozhu d4d5d40569 feat: 新增预算费控模型与报销审批流引擎
后端新增预算费控服务和报销单审批流模块,引入申请人费用画像
算法,优化知识库 RAG 运行时和同步逻辑,完善报销单工作流常
量和明细同步,更新差旅报销规则电子表格,前端新增预算分析
组件和数字员工模型,完善审批对话框和洞察面板交互,优化侧
边栏和顶栏样式,补充单元测试。
2026-05-27 17:31:27 +08:00

1473 lines
42 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>X-Financial 预算费用规划推荐模型方案</title>
<style>
:root {
color-scheme: light;
--bg: #f5f7f4;
--surface: #ffffff;
--surface-soft: #f9fbf8;
--ink: #132019;
--ink-soft: #526257;
--muted: #718075;
--line: #dbe4dc;
--line-strong: #c5d2c7;
--green: #0f766e;
--green-dark: #0b4f49;
--green-soft: #e4f5f2;
--blue: #2563eb;
--blue-soft: #e8f0ff;
--amber: #b45309;
--amber-soft: #fff3d7;
--red: #c2410c;
--red-soft: #fff0e8;
--plum: #7c2d12;
--plum-soft: #f8eee8;
--shadow: 0 16px 34px rgba(37, 48, 40, 0.08);
--radius: 8px;
--mono: "Cascadia Code", "JetBrains Mono", Consolas, monospace;
--font: "Microsoft YaHei UI", "Microsoft YaHei", "PingFang SC", "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
background:
linear-gradient(180deg, rgba(228, 245, 242, 0.9), rgba(245, 247, 244, 0.82) 310px),
var(--bg);
color: var(--ink);
font-family: var(--font);
font-size: 14px;
line-height: 1.68;
letter-spacing: 0;
overflow-wrap: anywhere;
}
a {
color: inherit;
text-decoration: none;
}
code {
padding: 2px 6px;
border-radius: 6px;
background: rgba(15, 118, 110, 0.08);
color: var(--green-dark);
font-family: var(--mono);
font-size: 0.92em;
}
pre {
margin: 0;
min-width: 0;
max-width: 100%;
padding: 16px;
overflow: auto;
border: 1px solid var(--line);
border-radius: var(--radius);
background: #101a16;
color: #eef8f4;
font-family: var(--mono);
font-size: 12.5px;
line-height: 1.72;
}
.layout {
display: grid;
grid-template-columns: 270px minmax(0, 1fr);
min-height: 100dvh;
}
.sidebar {
position: sticky;
top: 0;
height: 100dvh;
padding: 24px 18px;
border-right: 1px solid var(--line);
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(18px);
overflow: auto;
}
.brand {
display: flex;
gap: 12px;
align-items: center;
padding-bottom: 22px;
border-bottom: 1px solid var(--line);
}
.brand-mark {
display: grid;
width: 40px;
height: 40px;
place-items: center;
border-radius: 8px;
background: #12342f;
color: #fff;
font-weight: 850;
box-shadow: 0 12px 20px rgba(15, 79, 73, 0.24);
}
.brand-title {
font-size: 15px;
font-weight: 850;
line-height: 1.25;
}
.brand-subtitle {
margin-top: 3px;
color: var(--muted);
font-size: 12px;
}
.nav-label {
margin: 22px 0 8px;
color: var(--muted);
font-size: 12px;
font-weight: 750;
}
.nav {
display: grid;
gap: 5px;
}
.nav a {
display: flex;
align-items: center;
min-height: 36px;
padding: 7px 10px;
border-radius: 8px;
color: var(--ink-soft);
font-size: 13px;
font-weight: 700;
}
.nav a:hover {
background: var(--green-soft);
color: var(--green-dark);
}
.nav-dot {
width: 7px;
height: 7px;
margin-right: 10px;
border-radius: 99px;
background: var(--line-strong);
}
.nav a:hover .nav-dot {
background: var(--green);
}
.side-note {
margin-top: 24px;
padding: 12px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface-soft);
color: var(--ink-soft);
font-size: 12px;
line-height: 1.55;
}
main {
min-width: 0;
padding: 34px 44px 58px;
}
.hero {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(330px, 0.9fr);
gap: 22px;
align-items: stretch;
margin-bottom: 22px;
}
.hero-copy {
min-width: 0;
padding: 30px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: rgba(255, 255, 255, 0.88);
box-shadow: var(--shadow);
}
.eyebrow {
display: inline-flex;
gap: 8px;
align-items: center;
margin-bottom: 14px;
color: var(--green-dark);
font-size: 12px;
font-weight: 850;
text-transform: uppercase;
}
.eyebrow::before {
content: "";
width: 24px;
height: 2px;
border-radius: 99px;
background: var(--green);
}
h1,
h2,
h3 {
margin: 0;
line-height: 1.28;
letter-spacing: 0;
}
h1 {
max-width: 760px;
color: #0b1f1b;
font-size: 34px;
font-weight: 900;
}
.lead {
max-width: 780px;
margin: 16px 0 0;
color: var(--ink-soft);
font-size: 15px;
}
.hero-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 22px;
}
.tag {
display: inline-flex;
align-items: center;
min-height: 30px;
padding: 5px 10px;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--surface-soft);
color: var(--ink-soft);
font-size: 12px;
font-weight: 750;
}
.hero-panel {
display: grid;
gap: 10px;
min-width: 0;
padding: 18px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: #16342f;
color: #ecf8f5;
box-shadow: var(--shadow);
}
.signal-row {
display: flex;
justify-content: space-between;
gap: 14px;
padding: 12px;
border: 1px solid rgba(236, 248, 245, 0.14);
border-radius: 8px;
background: rgba(255, 255, 255, 0.06);
}
.signal-row strong {
display: block;
font-size: 13px;
}
.signal-row > div:first-child {
min-width: 0;
}
.signal-row span {
display: block;
margin-top: 3px;
color: rgba(236, 248, 245, 0.72);
font-size: 12px;
}
.signal-value {
flex: 0 0 auto;
color: #bff4e8;
font-weight: 900;
}
section {
min-width: 0;
margin-top: 18px;
padding: 24px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 8px 20px rgba(37, 48, 40, 0.04);
scroll-margin-top: 20px;
}
.section-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 22px;
margin-bottom: 16px;
}
.section-head h2 {
font-size: 22px;
font-weight: 900;
}
.section-head p {
max-width: 680px;
margin: 6px 0 0;
color: var(--ink-soft);
}
.section-kicker {
color: var(--green);
font-size: 12px;
font-weight: 850;
white-space: nowrap;
}
.grid-2,
.grid-3,
.grid-4 {
display: grid;
gap: 12px;
}
.grid-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.card,
.metric,
.lane,
.phase,
.check-item {
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
}
.card {
min-width: 0;
padding: 16px;
}
.card h3,
.lane h3,
.phase h3 {
color: var(--ink);
font-size: 15px;
font-weight: 850;
}
.card p,
.lane p,
.phase p {
margin: 8px 0 0;
color: var(--ink-soft);
font-size: 13px;
}
.metric {
min-width: 0;
padding: 14px;
background: linear-gradient(180deg, #fff, var(--surface-soft));
}
.metric-label {
color: var(--muted);
font-size: 12px;
font-weight: 750;
}
.metric-value {
margin-top: 6px;
color: var(--ink);
font-size: 22px;
font-weight: 900;
line-height: 1.1;
}
.metric-desc {
margin-top: 7px;
color: var(--ink-soft);
font-size: 12px;
line-height: 1.5;
}
.pill-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.pill {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 4px 9px;
border-radius: 999px;
background: var(--green-soft);
color: var(--green-dark);
font-size: 12px;
font-weight: 750;
}
.flow {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 10px;
margin-top: 14px;
}
.flow-step {
position: relative;
min-height: 120px;
padding: 14px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface-soft);
}
.flow-step:not(:last-child)::after {
content: "";
position: absolute;
top: 50%;
right: -9px;
width: 8px;
height: 8px;
border-top: 2px solid var(--line-strong);
border-right: 2px solid var(--line-strong);
transform: translateY(-50%) rotate(45deg);
background: transparent;
}
.flow-number {
display: inline-grid;
width: 24px;
height: 24px;
place-items: center;
margin-bottom: 9px;
border-radius: 6px;
background: var(--green);
color: #fff;
font-size: 12px;
font-weight: 900;
}
.flow-step strong {
display: block;
font-size: 13px;
line-height: 1.35;
}
.flow-step span {
display: block;
margin-top: 6px;
color: var(--ink-soft);
font-size: 12px;
line-height: 1.5;
}
.architecture {
display: grid;
gap: 10px;
margin-top: 12px;
}
.lane {
display: grid;
grid-template-columns: 160px minmax(0, 1fr);
gap: 14px;
padding: 14px;
align-items: start;
}
.lane h3 {
color: var(--green-dark);
}
.lane ul,
.card ul,
.phase ul,
.quality-list {
margin: 9px 0 0;
padding-left: 18px;
color: var(--ink-soft);
}
li + li {
margin-top: 5px;
}
.formula-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(300px, 0.8fr);
gap: 14px;
align-items: start;
}
.callout {
padding: 16px;
border: 1px solid var(--line);
border-left: 4px solid var(--green);
border-radius: var(--radius);
background: var(--green-soft);
color: var(--green-dark);
}
.callout strong {
display: block;
margin-bottom: 6px;
font-weight: 900;
}
.score-band {
display: grid;
gap: 9px;
}
.band {
display: grid;
grid-template-columns: 120px 82px minmax(0, 1fr);
gap: 10px;
align-items: center;
padding: 11px 12px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface-soft);
}
.band strong {
font-size: 13px;
}
.band-score {
font-family: var(--mono);
font-size: 12px;
font-weight: 850;
}
.band span {
color: var(--ink-soft);
font-size: 12px;
}
.band.recommended {
border-color: #a6ded3;
background: var(--green-soft);
}
.band.caution {
border-color: #f0cf88;
background: var(--amber-soft);
}
.band.review {
border-color: #b8c9f5;
background: var(--blue-soft);
}
.band.block {
border-color: #efb69a;
background: var(--red-soft);
}
.phase {
min-width: 0;
padding: 16px;
border-top: 4px solid var(--green);
}
.phase:nth-child(2) {
border-top-color: var(--blue);
}
.phase:nth-child(3) {
border-top-color: var(--amber);
}
.phase:nth-child(4) {
border-top-color: var(--red);
}
.screen {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
padding: 14px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: #f8faf8;
}
.screen-block {
min-width: 0;
min-height: 74px;
padding: 12px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
}
.screen-block.wide {
grid-column: 1 / -1;
}
.screen-block strong {
display: block;
font-size: 13px;
}
.screen-block span {
display: block;
margin-top: 5px;
color: var(--ink-soft);
font-size: 12px;
}
.checklist {
display: grid;
gap: 9px;
margin-top: 12px;
}
.check-item {
display: flex;
gap: 10px;
align-items: flex-start;
padding: 12px;
color: var(--ink-soft);
}
.check-item input {
width: 16px;
height: 16px;
margin-top: 4px;
accent-color: var(--green);
}
.check-item strong {
display: block;
color: var(--ink);
font-size: 13px;
}
.check-item span {
display: block;
margin-top: 3px;
font-size: 12px;
}
.check-item:has(input:checked) {
background: var(--green-soft);
color: var(--green-dark);
}
.check-item:has(input:checked) strong,
.check-item:has(input:checked) span {
text-decoration: line-through;
text-decoration-thickness: 1px;
}
.split-note {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-top: 12px;
}
.note-green,
.note-amber {
padding: 14px;
border-radius: var(--radius);
font-size: 13px;
}
.note-green {
border: 1px solid #a6ded3;
background: var(--green-soft);
color: var(--green-dark);
}
.note-amber {
border: 1px solid #f0cf88;
background: var(--amber-soft);
color: #74460b;
}
.footer {
margin-top: 24px;
color: var(--muted);
font-size: 12px;
text-align: center;
}
@media (max-width: 1100px) {
.layout {
grid-template-columns: 1fr;
}
.sidebar {
position: static;
height: auto;
}
.nav {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.hero,
.grid-4,
.flow,
.formula-grid {
grid-template-columns: 1fr;
}
.flow-step:not(:last-child)::after {
display: none;
}
}
@media (max-width: 760px) {
main {
padding: 22px 14px 38px;
}
.sidebar {
padding: 16px 14px;
}
.brand {
padding-bottom: 14px;
}
.nav-label {
margin: 14px 0 8px;
}
.nav {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 4px 8px;
}
.nav a {
min-height: 32px;
padding: 6px 8px;
font-size: 12px;
}
.side-note {
margin-top: 14px;
padding: 10px;
}
.signal-row {
display: grid;
grid-template-columns: 1fr;
gap: 4px;
}
.signal-value {
justify-self: start;
}
.hero-copy,
section {
padding: 18px;
}
h1 {
font-size: 26px;
}
.grid-2,
.grid-3,
.split-note,
.screen,
.lane,
.band {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="layout">
<aside class="sidebar">
<div class="brand">
<div class="brand-mark">BF</div>
<div>
<div class="brand-title">预算费用模型方案</div>
<div class="brand-subtitle">Budget & Fee Recommendation</div>
</div>
</div>
<div class="nav-label">方案目录</div>
<nav class="nav" aria-label="页面目录">
<a href="#summary"><span class="nav-dot"></span>方案结论</a>
<a href="#loop"><span class="nav-dot"></span>业务闭环</a>
<a href="#architecture"><span class="nav-dot"></span>模型架构</a>
<a href="#features"><span class="nav-dot"></span>数据与特征</a>
<a href="#formula"><span class="nav-dot"></span>计算口径</a>
<a href="#score"><span class="nav-dot"></span>评分推荐</a>
<a href="#applicant-profile"><span class="nav-dot"></span>画像公式</a>
<a href="#output"><span class="nav-dot"></span>输出协议</a>
<a href="#ui"><span class="nav-dot"></span>审批展示</a>
<a href="#roadmap"><span class="nav-dot"></span>落地路线</a>
<a href="#acceptance"><span class="nav-dot"></span>验收清单</a>
</nav>
<div class="side-note">
版本2026-05-27<br>
定位:在原“预算费用管控评分模型”基础上,升级为可上线评审的规划推荐模型方案。
</div>
</aside>
<main>
<header class="hero">
<div class="hero-copy">
<div class="eyebrow">X-Financial Model Proposal</div>
<h1>预算费用规划推荐模型方案</h1>
<p class="lead">
这个模型不只是给审批页打一个分,而是把“预算是否够、费用是否合理、是否影响预算节奏、下一步该怎么处理”整理成可解释、可审计、可持续迭代的推荐结果。
</p>
<div class="hero-actions">
<span class="tag">规则模型优先</span>
<span class="tag">硬约束可审计</span>
<span class="tag">LLM 只做解释层</span>
<span class="tag">审批反馈可回流</span>
</div>
</div>
<div class="hero-panel" aria-label="关键决策信号">
<div class="signal-row">
<div>
<strong>模型主目标</strong>
<span>帮预算管理者快速判断是否可承接</span>
</div>
<div class="signal-value">Recommend</div>
</div>
<div class="signal-row">
<div>
<strong>第一版边界</strong>
<span>不替代人工审批,不让 LLM 直接做硬判断</span>
</div>
<div class="signal-value">Guarded</div>
</div>
<div class="signal-row">
<div>
<strong>核心口径</strong>
<span>扣除当前申请已预占,避免重复计算</span>
</div>
<div class="signal-value">Traceable</div>
</div>
<div class="signal-row">
<div>
<strong>迭代方向</strong>
<span>规则评分 + 历史基准 + 解释生成</span>
</div>
<div class="signal-value">V1 → V3</div>
</div>
</div>
</header>
<section id="summary">
<div class="section-head">
<div>
<h2>方案结论</h2>
<p>建议把模型定位为“预算审批辅助决策引擎”,输出结构化推荐和解释依据,而不是单纯的页面分数。</p>
</div>
<div class="section-kicker">Executive Summary</div>
</div>
<div class="grid-4">
<div class="metric">
<div class="metric-label">推荐动作</div>
<div class="metric-value">4 类</div>
<div class="metric-desc">建议通过、谨慎通过、需要复核、不建议直接通过。</div>
</div>
<div class="metric">
<div class="metric-label">核心评分</div>
<div class="metric-value">100 分</div>
<div class="metric-desc">预算安全优先,费用必要性与信息完整度共同影响。</div>
</div>
<div class="metric">
<div class="metric-label">硬性约束</div>
<div class="metric-value">3 条</div>
<div class="metric-desc">超预算、强阻断控制、预算池无法匹配必须显式处理。</div>
</div>
<div class="metric">
<div class="metric-label">上线策略</div>
<div class="metric-value">分阶段</div>
<div class="metric-desc">先确定性规则,再接入历史基准和解释层。</div>
</div>
</div>
<div class="split-note">
<div class="note-green">
<strong>推荐采用的产品口径:</strong>
分数不是结论本身,结论应由“推荐动作 + 风险等级 + 触发依据 + 可执行建议”共同表达。
</div>
<div class="note-amber">
<strong>需要避免的方向:</strong>
不要让 LLM 直接判断是否通过,也不要只用一个综合分覆盖预算、业务必要性、历史异常等不同原因。
</div>
</div>
</section>
<section id="loop">
<div class="section-head">
<div>
<h2>业务闭环</h2>
<p>模型要服务完整预算链路:预算池建立、申请预占、审批推荐、报销核销、结果反馈。</p>
</div>
<div class="section-kicker">Business Loop</div>
</div>
<div class="flow">
<div class="flow-step">
<span class="flow-number">1</span>
<strong>预算计划建立</strong>
<span>按部门、项目、科目、成本中心形成预算池和预警线。</span>
</div>
<div class="flow-step">
<span class="flow-number">2</span>
<strong>费用申请预占</strong>
<span>申请提交后先占用预算,避免后续审批期间额度被重复使用。</span>
</div>
<div class="flow-step">
<span class="flow-number">3</span>
<strong>预算审批推荐</strong>
<span>预算管理者看到风险等级、计算依据、建议动作和补充要求。</span>
</div>
<div class="flow-step">
<span class="flow-number">4</span>
<strong>报销与核销</strong>
<span>申请通过后生成报销草稿,实际报销后转为已核销金额。</span>
</div>
<div class="flow-step">
<span class="flow-number">5</span>
<strong>结果反馈回流</strong>
<span>记录审批采纳、驳回原因、实际报销差异,反哺后续规则。</span>
</div>
</div>
</section>
<section id="architecture">
<div class="section-head">
<div>
<h2>模型架构</h2>
<p>采用“上下文构建 → 特征计算 → 硬约束判定 → 综合评分 → 推荐解释 → 反馈沉淀”的分层结构。</p>
</div>
<div class="section-kicker">Model Layers</div>
</div>
<div class="architecture">
<div class="lane">
<h3>上下文层</h3>
<div>
<p>统一组装申请单、预算池、历史费用、项目计划和审批身份,形成模型输入快照。</p>
<ul>
<li>复用 <code>build_claim_budget_context()</code> 的预算上下文。</li>
<li>补充申请事由、地点、费用类型、项目编号、附件摘要。</li>
<li>保留计算时刻的预算快照,方便事后审计。</li>
</ul>
</div>
</div>
<div class="lane">
<h3>特征层</h3>
<div>
<p>把业务数据转换成可解释特征,而不是直接把原始字段丢给模型。</p>
<ul>
<li>预算容量:审批前可用、审批后使用率、超预算金额。</li>
<li>单笔影响:本次金额占预算比例、是否突破预算节奏。</li>
<li>必要性证据:事由、项目、地点、附件、业务场景是否完整。</li>
<li>历史基准:同部门、同项目、同费用类型的历史均值和波动。</li>
</ul>
</div>
</div>
<div class="lane">
<h3>决策层</h3>
<div>
<p>先执行硬规则,再计算综合分,确保强管控场景不会被其他维度“平均掉”。</p>
<ul>
<li>硬规则:超预算、预算池阻断、预算无法匹配、关键字段缺失。</li>
<li>综合评分:预算安全、费用影响、规划匹配、历史异常、信息完整。</li>
<li>分档输出recommended、caution、review、block、reference。</li>
</ul>
</div>
</div>
<div class="lane">
<h3>解释层</h3>
<div>
<p>第一版直接用模板解释;后续允许 LLM 改写语气,但不能改写分数、等级和硬性结论。</p>
<ul>
<li>解释必须引用结构化依据,不允许凭空补充原因。</li>
<li>建议要能变成审批动作,例如补充材料、拆分费用、调整预算。</li>
<li>所有展示文案保留对应的 <code>basis_code</code>,方便追踪。</li>
</ul>
</div>
</div>
</div>
</section>
<section id="features">
<div class="section-head">
<div>
<h2>数据与特征</h2>
<p>模型输入需要覆盖“预算是否够”和“费用是否该花”两条线,避免只看额度、不看业务必要性。</p>
</div>
<div class="section-kicker">Feature Design</div>
</div>
<div class="grid-3">
<div class="card">
<h3>预算上下文</h3>
<ul>
<li><code>budget_applicable</code>:是否纳入预算管控。</li>
<li><code>matched</code>:是否匹配到预算池。</li>
<li><code>total_amount</code><code>reserved_amount</code><code>consumed_amount</code></li>
<li><code>current_reserved_amount</code>:当前申请已预占金额。</li>
<li><code>warning_threshold</code><code>control_action</code></li>
</ul>
</div>
<div class="card">
<h3>申请单主数据</h3>
<ul>
<li>申请金额、费用类型、申请事由。</li>
<li>业务地点、项目编号、成本中心。</li>
<li>直属领导意见、附件摘要。</li>
<li>申请时间、期望发生时间、是否紧急。</li>
</ul>
</div>
<div class="card">
<h3>增强特征</h3>
<ul>
<li>同类费用历史均值与分位数。</li>
<li>部门剩余预算消耗速度。</li>
<li>项目里程碑与预算计划匹配度。</li>
<li>申请人近期预算占用频次。</li>
</ul>
</div>
</div>
<div class="pill-list" aria-label="特征分组">
<span class="pill">预算容量</span>
<span class="pill">单笔影响</span>
<span class="pill">预算节奏</span>
<span class="pill">业务必要性</span>
<span class="pill">历史异常</span>
<span class="pill">信息完整度</span>
<span class="pill">审批反馈</span>
</div>
</section>
<section id="formula">
<div class="section-head">
<div>
<h2>计算口径</h2>
<p>申请提交时已经发生预算预占,所以审批分析必须扣除当前申请已预占金额,避免重复计算。</p>
</div>
<div class="section-kicker">Calculation</div>
</div>
<div class="formula-grid">
<pre><code>已使用基数 = 已预占金额 + 已核销金额 - 当前申请已预占金额
审批前可用预算 = 预算总额度 - 已使用基数
审批后占用 = 已使用基数 + 本次申请金额
此次费用占预算 = 本次申请金额 / 预算总额度
审批后使用率 = 审批后占用 / 预算总额度
超预算金额 = max(本次申请金额 - 审批前可用预算, 0)</code></pre>
<div class="callout">
<strong>关键原则</strong>
如果当前申请在提交阶段已经进入 <code>reserved_amount</code>,审批页就不能再把它当成新增占用直接叠加。否则预算管理者看到的风险会被放大,尤其是大额申请会明显失真。
</div>
</div>
</section>
<section id="score">
<div class="section-head">
<div>
<h2>评分推荐</h2>
<p>建议采用“硬规则优先 + 加权评分”的组合。硬规则决定是否必须复核或阻断,评分用于排序和解释风险程度。</p>
</div>
<div class="section-kicker">Decision Strategy</div>
</div>
<div class="grid-2">
<div class="card">
<h3>建议权重</h3>
<ul>
<li>预算安全40 分,覆盖可用额度、审批后使用率、超预算金额。</li>
<li>单笔影响18 分,判断本次费用对预算池的冲击。</li>
<li>规划匹配16 分,比较项目计划、预算周期和费用发生时间。</li>
<li>历史基准14 分,识别同类费用异常偏高或频繁占用。</li>
<li>信息完整12 分,衡量事由、项目、地点、附件是否足够。</li>
</ul>
</div>
<div class="score-band">
<div class="band recommended">
<strong>recommended</strong>
<div class="band-score">85-100</div>
<span>预算充足、信息完整、费用影响可承接,建议通过。</span>
</div>
<div class="band caution">
<strong>caution</strong>
<div class="band-score">70-84</div>
<span>预算可承接但影响较明显,建议谨慎通过并关注后续节奏。</span>
</div>
<div class="band review">
<strong>review</strong>
<div class="band-score">50-69</div>
<span>存在信息缺口、历史异常或接近预警线,需要复核。</span>
</div>
<div class="band block">
<strong>block</strong>
<div class="band-score">0-49</div>
<span>超预算或触发强管控动作,不建议直接通过。</span>
</div>
</div>
</div>
<div class="split-note">
<div class="note-green">
<strong>硬规则示例:</strong>
<code>control_action = block</code> 且超预算时,直接进入 <code>block</code>;预算池未匹配时输出 <code>reference</code>,不能伪装成低风险。
</div>
<div class="note-amber">
<strong>推荐动作要具体:</strong>
不只写“需关注”,而要给出“补充业务必要性、拆分费用、调整预算池、发起预算调剂”等可执行动作。
</div>
</div>
</section>
<section id="applicant-profile">
<div class="section-head">
<div>
<h2>申请人费用画像公式</h2>
<p>画像只用于调整复核强度和审核建议,不能直接把申请人定义为“异常人员”。核心是用同组基准解释个人费用节奏是否偏离。</p>
</div>
<div class="section-kicker">Applicant Profile</div>
</div>
<div class="formula-grid">
<pre><code>申请人画像风险分 =
高频申请分 * 20%
+ 高金额占用分 * 25%
+ 同组偏离分 * 25%
+ 历史调减退回分 * 15%
+ 本次申请偏离分 * 15%
等级:
0-39 正常
40-59 需关注
60-79 重点复核
80-100 强复核 / 升级审批</code></pre>
<div class="callout">
<strong>同组基准口径</strong>
同组不按全公司粗暴比较,而应按 <code>部门 + 岗位 + 费用类型 + 城市等级 + 项目类型 + 近 90/180 天</code> 聚合。销售、项目经理和研发不能混在一个基准池里比较。
</div>
</div>
<div class="grid-3" style="margin-top: 12px;">
<div class="card">
<h3>出差天数建议</h3>
<pre><code>同类基准天数 = peer_group.travel_days_p75
天数偏离率 = 本次出差天数 / 同类基准天数
建议天数 = min(
本次出差天数,
同类基准天数 + 业务缓冲天数
)</code></pre>
<p>当偏离率超过 1.5 时,建议补充行程安排或压缩天数;超过 2.0 时进入重点复核。</p>
</div>
<div class="card">
<h3>出差费用建议</h3>
<pre><code>本次日均费用 = 本次申请金额 / 本次出差天数
日均费用偏离率 =
本次日均费用 / 同城市同职级日均基准
建议金额上限 =
建议天数 * 日均基准 * 容忍系数</code></pre>
<p>容忍系数第一版建议使用 1.15 到 1.20,避免模型因为小幅波动给出过硬建议。</p>
</div>
<div class="card">
<h3>招待费用建议</h3>
<pre><code>人均招待金额 = 本次招待金额 / 参与人数
人均偏离率 = 人均招待金额 / 招待标准上限
同客户招待频率 = 近 90 天同客户招待次数
个人招待分位 = 个人近 90 天招待金额在同组分位</code></pre>
<p>人均偏离率超过 1.2 时建议调低标准;同客户 90 天内多次招待时要求补充客户推进阶段。</p>
</div>
</div>
<div class="split-note">
<div class="note-green">
<strong>建议生成公式:</strong>
<code>审核建议强度 = max(画像风险等级, 本次偏离等级, 硬规则等级)</code>。展示时必须说明是哪一类指标触发,而不是只给一个结论。
</div>
<div class="note-amber">
<strong>审核建议示例:</strong>
“该申请人近 90 天费用占用处于同组 P88本次出差天数为同类 P75 的 1.67 倍。建议将 5 天调整为 4 天,或补充客户拜访安排和项目阶段说明。”
</div>
</div>
</section>
<section id="output">
<div class="section-head">
<div>
<h2>输出协议</h2>
<p>接口输出需要同时服务审批页展示、看板统计和后续审计,因此自然语言必须和结构化字段分开。</p>
</div>
<div class="section-kicker">API Contract</div>
</div>
<pre><code>{
"score": 82,
"rating": "caution",
"risk_level": "medium",
"recommendation": "cautious_approve",
"summary": "预算整体可承接,但本次费用对预算池已有明显影响。",
"metrics": {
"claim_amount": "12000.00",
"total_amount": "50000.00",
"available_before_approval": "38000.00",
"claim_amount_ratio": "24.00",
"after_usage_rate": "72.00",
"over_budget_amount": "0.00"
},
"applicant_profile": {
"profile_score": 66,
"profile_level": "review",
"peer_percentile": 88,
"travel_days_ratio": "1.67",
"daily_cost_ratio": "1.34",
"suggested_days": 4,
"suggested_amount_cap": "6800.00"
},
"evidence": [
{
"basis_code": "budget.after_usage_rate.warning_near",
"text": "审批后预算使用率为 72.00%,接近预警区间。"
}
],
"suggested_actions": [
{
"action": "approve_with_note",
"text": "可继续审批,但建议备注本次费用对应的项目阶段和后续预算节奏。"
}
],
"explainable_snapshot": {
"budget_no": "BUD-TEST",
"control_action": "warn",
"warning_threshold": "80.00"
}
}</code></pre>
</section>
<section id="ui">
<div class="section-head">
<div>
<h2>审批展示</h2>
<p>预算管理者最需要看到的是“为什么是这个建议”和“如果要通过,还要补什么”。页面不应只展示一个分数。</p>
</div>
<div class="section-kicker">Approval UX</div>
</div>
<div class="screen">
<div class="screen-block wide">
<strong>顶部结论条</strong>
<span>展示推荐动作、风险等级、综合分和一句话摘要,例如“谨慎通过:预算可承接,但审批后使用率接近预警线”。</span>
</div>
<div class="screen-block">
<strong>关键指标</strong>
<span>本次金额、预算总额、审批前可用、审批后使用率、超预算金额。</span>
</div>
<div class="screen-block">
<strong>触发依据</strong>
<span>逐条展示规则依据,保留 basis_code方便定位到模型逻辑。</span>
</div>
<div class="screen-block">
<strong>建议动作</strong>
<span>通过、谨慎通过、补充材料、拆分费用、预算调剂、退回修改。</span>
</div>
<div class="screen-block">
<strong>申请人费用画像</strong>
<span>展示近 90 天频率、金额分位、天数偏离率和建议压缩区间,只作为审批参考。</span>
</div>
<div class="screen-block">
<strong>审批反馈</strong>
<span>记录预算管理者是否采纳建议,以及不采纳时的原因。</span>
</div>
</div>
</section>
<section id="roadmap">
<div class="section-head">
<div>
<h2>落地路线</h2>
<p>先把确定性口径做稳,再逐步接入历史基准和解释层,避免第一版就变成难以审计的黑盒。</p>
</div>
<div class="section-kicker">Implementation</div>
</div>
<div class="grid-4">
<div class="phase">
<h3>P0口径对齐</h3>
<p>确认预算预占、审批前可用、审批后占用的计算口径。</p>
<ul>
<li>梳理预算上下文字段。</li>
<li>固化不重复计算规则。</li>
<li>定义输出协议。</li>
</ul>
</div>
<div class="phase">
<h3>P1规则评分</h3>
<p>上线可审计的确定性模型,覆盖预算审批主流程。</p>
<ul>
<li>实现特征计算。</li>
<li>实现硬规则和分档。</li>
<li>补齐单元测试。</li>
</ul>
</div>
<div class="phase">
<h3>P2历史基准</h3>
<p>引入同类费用历史均值、部门消耗速度和预算节奏。</p>
<ul>
<li>沉淀历史统计任务。</li>
<li>增加异常检测特征。</li>
<li>看板展示采纳率。</li>
</ul>
</div>
<div class="phase">
<h3>P3解释增强</h3>
<p>在结构化依据上增加 LLM 文案层,提升可读性。</p>
<ul>
<li>LLM 不改分数和等级。</li>
<li>解释引用 basis_code。</li>
<li>增加人工反馈闭环。</li>
</ul>
</div>
</div>
</section>
<section id="acceptance">
<div class="section-head">
<div>
<h2>验收清单</h2>
<p>下面的清单会保存在当前浏览器本地,方便后续评审和开发跟踪。</p>
</div>
<div class="section-kicker">Checklist</div>
</div>
<div class="checklist">
<label class="check-item">
<input type="checkbox" data-check="contract">
<span>
<strong>输出协议稳定</strong>
<span>评分、等级、推荐动作、指标、依据、建议动作都有结构化字段。</span>
</span>
</label>
<label class="check-item">
<input type="checkbox" data-check="double-count">
<span>
<strong>预算预占不重复计算</strong>
<span>测试覆盖当前申请已预占、未预占、部分预占三类场景。</span>
</span>
</label>
<label class="check-item">
<input type="checkbox" data-check="hard-rules">
<span>
<strong>硬规则不可被平均抵消</strong>
<span>超预算和 block 控制动作必须进入高风险或阻断建议。</span>
</span>
</label>
<label class="check-item">
<input type="checkbox" data-check="ui">
<span>
<strong>审批页展示可解释</strong>
<span>预算管理者能看到风险原因、计算指标和可执行下一步。</span>
</span>
</label>
<label class="check-item">
<input type="checkbox" data-check="applicant-profile">
<span>
<strong>申请人画像公式可审计</strong>
<span>同组基准、偏离率、建议天数、建议金额上限都能追溯到结构化字段。</span>
</span>
</label>
<label class="check-item">
<input type="checkbox" data-check="feedback">
<span>
<strong>审批反馈可回流</strong>
<span>记录采纳、覆盖、退回、预算调剂等结果,为后续模型迭代准备数据。</span>
</span>
</label>
<label class="check-item">
<input type="checkbox" data-check="llm-boundary">
<span>
<strong>LLM 边界清晰</strong>
<span>LLM 只改写解释文案,不参与硬性通过、阻断和预算额度计算。</span>
</span>
</label>
</div>
</section>
<div class="footer">
X-Financial · 预算费用规划推荐模型方案 · 适用于预算审批、费用申请和报销核销闭环。
</div>
</main>
</div>
<script>
const storagePrefix = "x-financial-budget-model-plan:";
document.querySelectorAll("[data-check]").forEach((checkbox) => {
const key = storagePrefix + checkbox.dataset.check;
checkbox.checked = localStorage.getItem(key) === "1";
checkbox.addEventListener("change", () => {
localStorage.setItem(key, checkbox.checked ? "1" : "0");
});
});
</script>
</body>
</html>