feat: 增强风险规则生成引擎与预算中心页面
后端拆分风险规则生成为解释器、语义分析、本体对齐等子模块, 优化模板执行和流程图生成,完善员工种子数据和导入逻辑,增强 报销单权限策略和草稿持久化,前端新增预算中心视图和趋势图 组件,重构审计页面和风险规则测试对话框交互,完善文档中心 和报销创建页面细节,补充单元测试覆盖。
This commit is contained in:
@@ -47,6 +47,8 @@ const PROMPT_FIELD_LABELS = [
|
||||
'出行方式',
|
||||
'交通方式',
|
||||
'交通工具',
|
||||
'用户预估费用',
|
||||
'预估费用',
|
||||
'预计总费用',
|
||||
'预计费用',
|
||||
'预计金额',
|
||||
@@ -186,10 +188,18 @@ export function expandApplicationTimeWithDays(timeText, days = 0) {
|
||||
return `${formatApplicationDate(startDate)} 至 ${formatApplicationDate(endDate)}`
|
||||
}
|
||||
|
||||
function normalizeApplicationTimeCandidate(value) {
|
||||
const text = String(value || '').trim().replace(/^[,,、。;;\s]+/, '')
|
||||
if (!text) return ''
|
||||
if (/20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/.test(text)) return text
|
||||
if (/今天|明天|后天|本周|下周|上周|本月|下月|月底|月初/.test(text)) return text
|
||||
return ''
|
||||
}
|
||||
|
||||
export function resolveApplicationTimeRange(ontology, prompt) {
|
||||
const range = ontology?.time_range || {}
|
||||
const baseTime = resolveTimeRangeText(ontology)
|
||||
|| resolvePromptField(prompt, ['发生时间', '业务发生时间', '申请时间', '时间'])
|
||||
const baseTime = normalizeApplicationTimeCandidate(resolveTimeRangeText(ontology))
|
||||
|| normalizeApplicationTimeCandidate(resolvePromptField(prompt, ['发生时间', '业务发生时间', '申请时间', '时间']))
|
||||
if (range.start_date && range.end_date && range.start_date !== range.end_date) {
|
||||
return `${range.start_date} 至 ${range.end_date}`
|
||||
}
|
||||
@@ -220,9 +230,94 @@ export function resolvePromptField(prompt, labels = []) {
|
||||
return match ? match[1].trim().replace(/[,。;;]$/, '') : ''
|
||||
}
|
||||
|
||||
export function resolveApplicationReason(prompt) {
|
||||
function normalizeApplicationTransportMode(value) {
|
||||
const text = String(value || '').trim()
|
||||
if (/飞机|机票|航班/.test(text)) return '飞机'
|
||||
if (/轮船|船票|客轮|渡轮|邮轮/.test(text)) return '轮船'
|
||||
if (/火车|高铁|动车|铁路|列车/.test(text)) return '火车'
|
||||
return text
|
||||
}
|
||||
|
||||
function cleanupApplicationReasonCandidate(value, location = '') {
|
||||
let text = String(value || '').trim()
|
||||
if (!text) return ''
|
||||
|
||||
text = text
|
||||
.replace(/^(?:发生时间|业务发生时间|申请时间|时间|地点|业务地点|发生地点|天数|出差天数|申请天数|出行方式|交通方式|交通工具|用户预估费用|预估费用|预计总费用|预计费用|预计金额|申请金额|预算|金额)\s*[::]\s*/u, '')
|
||||
.replace(/20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/gu, '')
|
||||
.replace(/(?:出差|申请)?(?:\d+|[一二两三四五六七八九十]{1,3})\s*天/gu, '')
|
||||
.replace(/(?:¥|¥)?\s*\d+(?:\.\d+)?\s*(?:元|块|万元|人民币)?/gu, '')
|
||||
.replace(/(?:高铁|动车|火车|铁路|列车|飞机|机票|航班|轮船|船票|客轮|渡轮|邮轮)/gu, '')
|
||||
.replace(/[,,、。;;]+/g, ',')
|
||||
.replace(/^\s*(申请|费用申请|业务|本次|去|到|前往|赴)\s*/u, '')
|
||||
.replace(/^[,\s]+|[,\s]+$/g, '')
|
||||
.trim()
|
||||
|
||||
const normalizedLocation = String(location || '').trim()
|
||||
if (normalizedLocation) {
|
||||
const escapedLocation = escapeRegExp(normalizedLocation)
|
||||
text = text
|
||||
.replace(new RegExp(`^${escapedLocation}(?:出差)?(?:,|,|、)?`, 'u'), '')
|
||||
.replace(new RegExp(`^(?:去|到|前往|赴)${escapedLocation}(?:出差)?(?:,|,|、)?`, 'u'), '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
if (!text) return ''
|
||||
if (/^20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?$/.test(text)) return ''
|
||||
if (/^(?:\d+|[一二两三四五六七八九十]{1,3})\s*天$/.test(text)) return ''
|
||||
if (/^[\u4e00-\u9fa5]{1,8}$/.test(text) && !/服务|支撑|支持|部署|实施|验收|拜访|对接|沟通|培训|会议|采购|安装|维护|上线|调试|项目/.test(text)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
function resolveApplicationLocationText(ontology, prompt) {
|
||||
const locationEntity = resolveEntity(ontology, 'location')
|
||||
return locationEntity?.normalized_value
|
||||
|| locationEntity?.value
|
||||
|| resolvePromptField(prompt, ['地点', '业务地点', '发生地点', '目的地'])
|
||||
|| ''
|
||||
}
|
||||
|
||||
export function resolveApplicationReason(prompt, ontology = null) {
|
||||
const location = resolveApplicationLocationText(ontology, prompt)
|
||||
const reasonEntity = resolveEntity(ontology, 'reason') || resolveEntity(ontology, 'business_reason')
|
||||
const entityReason = String(reasonEntity?.normalized_value || reasonEntity?.value || '').trim()
|
||||
if (entityReason) {
|
||||
return cleanupApplicationReasonCandidate(entityReason, location) || entityReason
|
||||
}
|
||||
|
||||
const labeled = resolvePromptField(prompt, ['事由', '申请事由', '出差事由', '原因', '用途'])
|
||||
return labeled || String(prompt || '').trim()
|
||||
if (labeled) {
|
||||
return cleanupApplicationReasonCandidate(labeled, location) || labeled
|
||||
}
|
||||
|
||||
const candidates = String(prompt || '')
|
||||
.split(/[\n,。;;]+/u)
|
||||
.map((item) => cleanupApplicationReasonCandidate(item, location))
|
||||
.filter(Boolean)
|
||||
const businessCandidate = candidates.find((item) => /服务|支撑|支持|部署|实施|验收|拜访|对接|沟通|培训|会议|采购|安装|维护|上线|调试|项目/.test(item))
|
||||
return businessCandidate || candidates.sort((left, right) => right.length - left.length)[0] || ''
|
||||
}
|
||||
|
||||
function resolveApplicationTransportMode(ontology, prompt) {
|
||||
const transportEntity = resolveEntity(ontology, 'transport_mode')
|
||||
|| resolveEntity(ontology, 'transport')
|
||||
const fromEntity = normalizeApplicationTransportMode(
|
||||
transportEntity?.normalized_value || transportEntity?.value || ''
|
||||
)
|
||||
if (fromEntity) return fromEntity
|
||||
|
||||
const labeled = resolvePromptField(prompt, ['出行方式', '交通方式', '交通工具'])
|
||||
const fromLabel = normalizeApplicationTransportMode(labeled)
|
||||
if (fromLabel) return fromLabel
|
||||
|
||||
const text = String(prompt || '')
|
||||
if (/飞机|机票|航班/.test(text)) return '飞机'
|
||||
if (/轮船|船票|客轮|渡轮|邮轮/.test(text)) return '轮船'
|
||||
if (/火车|高铁|动车|铁路|列车/.test(text)) return '火车'
|
||||
return ''
|
||||
}
|
||||
|
||||
export function resolveAttachmentPolicy(expenseTypeCode, amount = 0) {
|
||||
@@ -260,17 +355,16 @@ export function resolveAttachmentPolicy(expenseTypeCode, amount = 0) {
|
||||
export function buildApplicationFieldsFromOntology(ontology, prompt, currentUser = {}) {
|
||||
const expenseTypeCode = resolveExpenseTypeCode(ontology)
|
||||
const amount = resolveApplicationAmount(ontology)
|
||||
const locationEntity = resolveEntity(ontology, 'location')
|
||||
const documentTypeEntity = resolveEntity(ontology, 'document_type')
|
||||
const workflowStageEntity = resolveEntity(ontology, 'workflow_stage')
|
||||
const attachmentPolicy = resolveAttachmentPolicy(expenseTypeCode, amount.value)
|
||||
const timeRange = resolveApplicationTimeRange(ontology, prompt)
|
||||
|| '待补充'
|
||||
const location = locationEntity?.normalized_value
|
||||
|| locationEntity?.value
|
||||
|| resolvePromptField(prompt, ['地点', '业务地点', '发生地点'])
|
||||
const location = resolveApplicationLocationText(ontology, prompt)
|
||||
|| '待补充'
|
||||
const reason = resolveApplicationReason(prompt) || '待补充'
|
||||
const reason = resolveApplicationReason(prompt, ontology) || '待补充'
|
||||
const days = resolvePromptDays(prompt)
|
||||
const transportMode = resolveApplicationTransportMode(ontology, prompt)
|
||||
|
||||
const fields = {
|
||||
documentType: documentTypeEntity?.normalized_value || 'expense_application',
|
||||
@@ -284,6 +378,8 @@ export function buildApplicationFieldsFromOntology(ontology, prompt, currentUser
|
||||
timeRange,
|
||||
location,
|
||||
reason,
|
||||
days: days ? `${days}天` : '',
|
||||
transportMode,
|
||||
applicant: currentUser.name || currentUser.username || '当前用户',
|
||||
department: currentUser.department || currentUser.departmentName || '待补充',
|
||||
preApprovalRequired: PRE_APPROVAL_TYPES.has(expenseTypeCode),
|
||||
|
||||
Reference in New Issue
Block a user