feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造
- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制 - 引入费用审批动态路由、平台风险分级、预审与风险阶段管理 - 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板 - 新增 Hermes 风险线索收集器、Agent 链路追踪中心 - 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估 - 完善报销申请快速预览、权限控制与前端测试覆盖
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import { buildApplicationFieldsFromOntology } from './expenseApplicationOntology.js'
|
||||
import {
|
||||
buildMockApplicationTransportEstimate,
|
||||
buildSystemApplicationEstimate
|
||||
} from './expenseApplicationEstimate.js'
|
||||
import { getTodayDateValue } from './workbenchComposerDate.js'
|
||||
|
||||
const APPLICATION_SESSION_TYPE = 'application'
|
||||
@@ -7,7 +11,11 @@ const APPLICATION_QUERY_PATTERN = /查询|状态|进度|列表|有哪些|材料
|
||||
|
||||
export const APPLICATION_PREVIEW_FIELD_DEFINITIONS = [
|
||||
{ key: 'applicationType', label: '申请类型' },
|
||||
{ key: 'applicant', label: '姓名', editable: false, required: false },
|
||||
{ key: 'grade', label: '职级', highlight: true, editable: false, required: false },
|
||||
{ key: 'department', label: '部门', editable: false, required: false },
|
||||
{ key: 'position', label: '岗位', editable: false, required: false },
|
||||
{ key: 'managerName', label: '直属领导', editable: false, required: false },
|
||||
{ key: 'time', label: '发生时间' },
|
||||
{ key: 'location', label: '地点' },
|
||||
{ key: 'reason', label: '事由' },
|
||||
@@ -17,13 +25,13 @@ export const APPLICATION_PREVIEW_FIELD_DEFINITIONS = [
|
||||
{ key: 'subsidyDailyCap', label: '补贴标准/天', highlight: true, editable: false, required: false },
|
||||
{ key: 'transportPolicy', label: '交通费用口径', highlight: true, editable: false, required: false },
|
||||
{ key: 'policyEstimate', label: '规则测算参考', highlight: true, editable: false, required: false },
|
||||
{ key: 'amount', label: '用户预估费用', highlight: true }
|
||||
{ key: 'amount', label: '系统预估费用', highlight: true, editable: false, required: false }
|
||||
]
|
||||
|
||||
export const APPLICATION_TRANSPORT_MODE_OPTIONS = ['火车', '飞机', '轮船']
|
||||
|
||||
const APPLICATION_POLICY_PENDING_TEXT = '填写地点和天数后自动测算'
|
||||
const APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT = '车票、机票暂无实时价格接口,按真实票据实报实销'
|
||||
const APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT = '选择火车、飞机或轮船后自动生成交通参考票价,报销阶段按真实票据复核'
|
||||
|
||||
function compactText(value) {
|
||||
return String(value || '').replace(/\s+/g, '')
|
||||
@@ -114,6 +122,38 @@ function resolveCurrentUserGrade(currentUser = {}) {
|
||||
).trim()
|
||||
}
|
||||
|
||||
function resolveCurrentUserDepartment(currentUser = {}) {
|
||||
return String(
|
||||
currentUser.department
|
||||
|| currentUser.departmentName
|
||||
|| currentUser.department_name
|
||||
|| ''
|
||||
).trim()
|
||||
}
|
||||
|
||||
function resolveCurrentUserPosition(currentUser = {}) {
|
||||
return String(
|
||||
currentUser.position
|
||||
|| currentUser.employeePosition
|
||||
|| currentUser.employee_position
|
||||
|| currentUser.jobTitle
|
||||
|| currentUser.job_title
|
||||
|| ''
|
||||
).trim()
|
||||
}
|
||||
|
||||
function resolveCurrentUserManagerName(currentUser = {}) {
|
||||
return String(
|
||||
currentUser.managerName
|
||||
|| currentUser.manager_name
|
||||
|| currentUser.directManagerName
|
||||
|| currentUser.direct_manager_name
|
||||
|| currentUser.leaderName
|
||||
|| currentUser.leader_name
|
||||
|| ''
|
||||
).trim()
|
||||
}
|
||||
|
||||
function parseApplicationDaysValue(value) {
|
||||
const match = String(value || '').match(/\d+/)
|
||||
const days = match ? Number(match[0]) : parseChineseNumber(value)
|
||||
@@ -165,10 +205,12 @@ function formatDailyPolicyMoney(value) {
|
||||
return display ? `${display}元/天` : APPLICATION_POLICY_PENDING_TEXT
|
||||
}
|
||||
|
||||
function buildTransportPolicyText(transportMode) {
|
||||
function buildTransportPolicyText(transportMode, location = '', transportEstimate = null, time = '') {
|
||||
const mode = String(transportMode || '').trim()
|
||||
if (!mode) return APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT
|
||||
return `${mode}票据暂无实时价格接口,按真实票据实报实销`
|
||||
const estimate = transportEstimate || buildMockApplicationTransportEstimate({ transportMode: mode, location, time })
|
||||
if (!estimate) return APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT
|
||||
return `${estimate.basisText},报销阶段按真实票据复核`
|
||||
}
|
||||
|
||||
function ensureApplicationPolicyFields(fields = {}) {
|
||||
@@ -180,7 +222,7 @@ function ensureApplicationPolicyFields(fields = {}) {
|
||||
nextFields.subsidyDailyCap = APPLICATION_POLICY_PENDING_TEXT
|
||||
}
|
||||
if (!String(nextFields.transportPolicy || '').trim() || /实报实销/.test(String(nextFields.transportPolicy || ''))) {
|
||||
nextFields.transportPolicy = buildTransportPolicyText(nextFields.transportMode)
|
||||
nextFields.transportPolicy = buildTransportPolicyText(nextFields.transportMode, nextFields.location, null, nextFields.time)
|
||||
}
|
||||
if (!String(nextFields.policyEstimate || '').trim()) {
|
||||
nextFields.policyEstimate = APPLICATION_POLICY_PENDING_TEXT
|
||||
@@ -371,9 +413,22 @@ export function applyApplicationPolicyEstimateResult(preview = {}, result = {},
|
||||
const hotelAmount = formatPolicyMoney(result?.hotel_amount)
|
||||
const allowanceRate = formatPolicyMoney(result?.total_allowance_rate)
|
||||
const allowanceAmount = formatPolicyMoney(result?.allowance_amount)
|
||||
const totalAmount = formatPolicyMoney(result?.total_amount)
|
||||
const matchedCity = String(result?.matched_city || fields.location || '').trim()
|
||||
const grade = String(result?.grade || fields.grade || resolveCurrentUserGrade(currentUser)).trim()
|
||||
const systemEstimate = buildSystemApplicationEstimate({
|
||||
transportMode: fields.transportMode,
|
||||
location: matchedCity || fields.location,
|
||||
time: fields.time,
|
||||
lodgingAmount: result?.hotel_amount,
|
||||
allowanceAmount: result?.allowance_amount
|
||||
})
|
||||
const transportEstimate = systemEstimate.transportEstimate
|
||||
const queryLabel = transportEstimate?.queryDate || '出行日期待确认'
|
||||
const transportText = transportEstimate
|
||||
? `交通 ${systemEstimate.transportAmountDisplay}元(按 ${queryLabel} 参考票价) + `
|
||||
: ''
|
||||
const totalAmount = systemEstimate.totalAmountDisplay
|
||||
const amount = totalAmount ? `${totalAmount}元` : fields.amount
|
||||
|
||||
return normalizeApplicationPreview({
|
||||
...preview,
|
||||
@@ -382,24 +437,85 @@ export function applyApplicationPolicyEstimateResult(preview = {}, result = {},
|
||||
grade,
|
||||
lodgingDailyCap: formatDailyPolicyMoney(result?.hotel_rate),
|
||||
subsidyDailyCap: formatDailyPolicyMoney(result?.total_allowance_rate),
|
||||
transportPolicy: buildTransportPolicyText(fields.transportMode),
|
||||
policyEstimate: `住宿 ${hotelAmount}元 + 补贴 ${allowanceAmount}元 = ${totalAmount}元(${days}天,不含交通票据)`,
|
||||
transportPolicy: buildTransportPolicyText(fields.transportMode, matchedCity || fields.location, transportEstimate, fields.time),
|
||||
policyEstimate: `${transportText}住宿 ${hotelAmount}元 + 补贴 ${allowanceAmount}元 = ${totalAmount}元(${days}天)`,
|
||||
amount,
|
||||
matchedCity,
|
||||
ruleName: String(result?.rule_name || '').trim(),
|
||||
ruleVersion: String(result?.rule_version || '').trim(),
|
||||
hotelAmount: hotelAmount ? `${hotelAmount}元` : '',
|
||||
allowanceAmount: allowanceAmount ? `${allowanceAmount}元` : '',
|
||||
transportEstimatedAmount: systemEstimate.transportAmountDisplay ? `${systemEstimate.transportAmountDisplay}元` : '',
|
||||
transportEstimateDate: transportEstimate?.queryDate || '',
|
||||
transportQueryLatencyMs: transportEstimate?.simulatedLatencyMs ? `${transportEstimate.simulatedLatencyMs}ms` : '',
|
||||
transportEstimateSource: transportEstimate?.source || '',
|
||||
transportEstimateConfidence: transportEstimate?.confidence || '',
|
||||
policyTotalAmount: totalAmount ? `${totalAmount}元` : ''
|
||||
},
|
||||
policyEstimate: {
|
||||
...result,
|
||||
grade,
|
||||
matchedCity
|
||||
matchedCity,
|
||||
transport_estimate: transportEstimate,
|
||||
system_total_amount: systemEstimate.totalAmount
|
||||
},
|
||||
policyEstimateStatus: 'completed'
|
||||
})
|
||||
}
|
||||
|
||||
export function refreshApplicationPreviewTransportEstimate(preview = {}) {
|
||||
const normalized = normalizeApplicationPreview(preview)
|
||||
const fields = { ...(normalized.fields || {}) }
|
||||
const policyResult = normalized.policyEstimate && typeof normalized.policyEstimate === 'object'
|
||||
? normalized.policyEstimate
|
||||
: {}
|
||||
const location = String(fields.matchedCity || policyResult.matched_city || fields.location || '').trim()
|
||||
const hotelAmountSource = fields.hotelAmount || policyResult.hotel_amount || 0
|
||||
const allowanceAmountSource = fields.allowanceAmount || policyResult.allowance_amount || 0
|
||||
const systemEstimate = buildSystemApplicationEstimate({
|
||||
transportMode: fields.transportMode,
|
||||
location,
|
||||
time: fields.time,
|
||||
lodgingAmount: hotelAmountSource,
|
||||
allowanceAmount: allowanceAmountSource
|
||||
})
|
||||
const transportEstimate = systemEstimate.transportEstimate
|
||||
if (!transportEstimate) return normalized
|
||||
|
||||
const hotelAmount = formatPolicyMoney(hotelAmountSource)
|
||||
const allowanceAmount = formatPolicyMoney(allowanceAmountSource)
|
||||
const hasPolicyAmounts = parseMoneyNumber(hotelAmountSource) > 0 || parseMoneyNumber(allowanceAmountSource) > 0
|
||||
const queryLabel = transportEstimate.queryDate || '出行日期待确认'
|
||||
const nextFields = {
|
||||
...fields,
|
||||
transportPolicy: buildTransportPolicyText(fields.transportMode, location, transportEstimate, fields.time),
|
||||
transportEstimatedAmount: systemEstimate.transportAmountDisplay ? `${systemEstimate.transportAmountDisplay}元` : '',
|
||||
transportEstimateDate: transportEstimate.queryDate || '',
|
||||
transportQueryLatencyMs: transportEstimate.simulatedLatencyMs ? `${transportEstimate.simulatedLatencyMs}ms` : '',
|
||||
transportEstimateSource: transportEstimate.source || '',
|
||||
transportEstimateConfidence: transportEstimate.confidence || ''
|
||||
}
|
||||
|
||||
if (hasPolicyAmounts) {
|
||||
const days = Number(policyResult.days) || parseApplicationDaysValue(fields.days) || 1
|
||||
const totalAmount = systemEstimate.totalAmountDisplay
|
||||
nextFields.policyEstimate = `交通 ${systemEstimate.transportAmountDisplay}元(按 ${queryLabel} 参考票价) + 住宿 ${hotelAmount}元 + 补贴 ${allowanceAmount}元 = ${totalAmount}元(${days}天)`
|
||||
nextFields.amount = totalAmount ? `${totalAmount}元` : nextFields.amount
|
||||
nextFields.policyTotalAmount = totalAmount ? `${totalAmount}元` : ''
|
||||
}
|
||||
|
||||
return normalizeApplicationPreview({
|
||||
...normalized,
|
||||
fields: nextFields,
|
||||
policyEstimate: {
|
||||
...policyResult,
|
||||
matchedCity: location,
|
||||
transport_estimate: transportEstimate,
|
||||
system_total_amount: systemEstimate.totalAmount
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function applyApplicationPolicyEstimateError(preview = {}, error = null, currentUser = {}) {
|
||||
const fields = { ...(preview?.fields || {}) }
|
||||
const message = String(error?.message || error || '').trim()
|
||||
@@ -408,7 +524,7 @@ export function applyApplicationPolicyEstimateError(preview = {}, error = null,
|
||||
fields: {
|
||||
...fields,
|
||||
grade: fields.grade || resolveCurrentUserGrade(currentUser),
|
||||
transportPolicy: buildTransportPolicyText(fields.transportMode),
|
||||
transportPolicy: buildTransportPolicyText(fields.transportMode, fields.location, null, fields.time),
|
||||
policyEstimate: message ? `规则中心暂未完成测算:${message}` : APPLICATION_POLICY_PENDING_TEXT
|
||||
},
|
||||
policyEstimateStatus: message ? 'failed' : 'pending'
|
||||
@@ -456,7 +572,12 @@ export function buildModelRefinedApplicationPreview(localPreview = {}, ontology
|
||||
amount: normalizeAmountFromOntology(ontologyFields, currentFields.amount),
|
||||
grade: resolveProvidedValue(currentFields.grade, resolveCurrentUserGrade(currentUser)),
|
||||
applicant: resolveProvidedValue(ontologyFields.applicant, currentFields.applicant),
|
||||
department: resolveProvidedValue(ontologyFields.department, currentFields.department)
|
||||
department: resolveProvidedValue(ontologyFields.department, currentFields.department || resolveCurrentUserDepartment(currentUser)),
|
||||
position: resolveProvidedValue(currentFields.position, resolveCurrentUserPosition(currentUser)),
|
||||
managerName: resolveProvidedValue(
|
||||
ontologyFields.managerName,
|
||||
currentFields.managerName || resolveCurrentUserManagerName(currentUser)
|
||||
)
|
||||
}
|
||||
|
||||
return normalizeApplicationPreview({
|
||||
@@ -511,7 +632,9 @@ export function buildLocalApplicationPreview(rawText, currentUser = {}, options
|
||||
amount: resolveApplicationAmount(sourceText),
|
||||
grade: resolveCurrentUserGrade(currentUser),
|
||||
applicant: currentUser.name || currentUser.username || '当前用户',
|
||||
department: currentUser.department || currentUser.departmentName || '待补充'
|
||||
department: resolveCurrentUserDepartment(currentUser) || '待补充',
|
||||
position: resolveCurrentUserPosition(currentUser) || '待补充',
|
||||
managerName: resolveCurrentUserManagerName(currentUser) || '待补充'
|
||||
}
|
||||
|
||||
return normalizeApplicationPreview({
|
||||
@@ -534,7 +657,9 @@ export function buildApplicationTemplatePreview(currentUser = {}) {
|
||||
amount: '',
|
||||
grade: resolveCurrentUserGrade(currentUser),
|
||||
applicant: currentUser.name || currentUser.username || '当前用户',
|
||||
department: currentUser.department || currentUser.departmentName || '待补充'
|
||||
department: resolveCurrentUserDepartment(currentUser) || '待补充',
|
||||
position: resolveCurrentUserPosition(currentUser) || '待补充',
|
||||
managerName: resolveCurrentUserManagerName(currentUser) || '待补充'
|
||||
},
|
||||
modelReviewStatus: 'template'
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user