feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造

- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制
- 引入费用审批动态路由、平台风险分级、预审与风险阶段管理
- 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板
- 新增 Hermes 风险线索收集器、Agent 链路追踪中心
- 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估
- 完善报销申请快速预览、权限控制与前端测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-01 17:07:14 +08:00
parent 7989f3a159
commit 92444e7eae
285 changed files with 25075 additions and 2986 deletions

View File

@@ -8,13 +8,14 @@ import {
testBootstrapDatabase,
testBootstrapRuntime
} from '../services/bootstrap.js'
import { login as loginByAccount } from '../services/auth.js'
import { setRuntimeApiBaseUrl } from '../services/api.js'
import { checkBackendHealth } from './useBackendHealth.js'
import { resolveDefaultAuthorizedRoute } from '../utils/accessControl.js'
import { useToast } from './useToast.js'
import { fetchCurrentAuthUser, login as loginByAccount } from '../services/auth.js'
import { setRuntimeApiBaseUrl } from '../services/api.js'
import { checkBackendHealth } from './useBackendHealth.js'
import { resolveDefaultAuthorizedRoute } from '../utils/accessControl.js'
import { useToast } from './useToast.js'
import { fetchSettings } from '../services/settings.js'
import { setThemeSkin } from './useThemeSkin.js'
import { normalizeAuthUserSnapshot } from '../utils/authUser.js'
import {
clearAuthSessionMetrics,
finalizeAuthSession,
@@ -140,10 +141,10 @@ function buildLegacyAdminUser(username = '') {
}
}
function resolvePlatformAdminFlag(payload, roleCodes = []) {
const username = String(payload?.username || payload?.account || '').trim().toLowerCase()
const role = String(payload?.role || '').trim().toLowerCase()
const normalizedRoleCodes = roleCodes.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean)
function resolvePlatformAdminFlag(payload, roleCodes = []) {
const username = String(payload?.username || payload?.account || '').trim().toLowerCase()
const role = String(payload?.role || '').trim().toLowerCase()
const normalizedRoleCodes = roleCodes.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean)
return (
Boolean(payload?.isAdmin)
@@ -152,46 +153,36 @@ function resolvePlatformAdminFlag(payload, roleCodes = []) {
|| role === '管理员'
|| role === '系统管理员'
|| normalizedRoleCodes.includes('admin')
)
}
function readStoredUser() {
if (typeof window === 'undefined') {
return buildAnonymousUser()
}
)
}
function normalizeStoredAuthUser(payload = {}) {
const user = normalizeAuthUserSnapshot(payload, {
defaultName: DEFAULT_USER_NAME,
defaultRole: DEFAULT_USER_ROLE
})
return {
...user,
isAdmin: resolvePlatformAdminFlag(payload, user.roleCodes)
}
}
function readStoredUser() {
if (typeof window === 'undefined') {
return buildAnonymousUser()
}
const raw = window.sessionStorage.getItem(AUTH_USER_KEY)
if (raw) {
try {
const payload = JSON.parse(raw)
if (payload && typeof payload === 'object') {
const username = String(payload.username || '').trim()
const name = String(payload.name || username || DEFAULT_USER_NAME).trim()
const roleCodes = Array.isArray(payload.roleCodes) ? payload.roleCodes.filter(Boolean) : []
return {
username,
name,
role: String(payload.role || DEFAULT_USER_ROLE),
department: String(payload.department || payload.departmentName || ''),
departmentName: String(payload.departmentName || payload.department || ''),
position: String(payload.position || ''),
grade: String(payload.grade || ''),
employeeNo: String(payload.employeeNo || payload.employee_no || ''),
managerName: String(payload.managerName || payload.manager_name || ''),
location: String(payload.location || ''),
costCenter: String(payload.costCenter || payload.cost_center || ''),
financeOwnerName: String(payload.financeOwnerName || payload.finance_owner_name || ''),
riskProfile: payload.riskProfile && typeof payload.riskProfile === 'object' ? payload.riskProfile : {},
roleCodes,
email: String(payload.email || ''),
avatar: String(payload.avatar || name.slice(0, 1).toUpperCase()),
isAdmin: resolvePlatformAdminFlag(payload, roleCodes)
}
}
} catch {
return buildLegacyAdminUser(readStoredUsername())
try {
const payload = JSON.parse(raw)
if (payload && typeof payload === 'object') {
return normalizeStoredAuthUser(payload)
}
} catch {
return buildLegacyAdminUser(readStoredUsername())
}
}
@@ -241,8 +232,18 @@ function persistAuthState(value, user = null, sessionId = '') {
window.sessionStorage.removeItem(AUTH_LAST_ACTIVITY_KEY)
clearAuthSessionMetrics()
}
function clearSessionTimeout() {
function persistAuthUserSnapshot(user = {}) {
if (typeof window === 'undefined') {
return
}
const normalizedUser = user || buildAnonymousUser()
window.sessionStorage.setItem(AUTH_USERNAME_KEY, String(normalizedUser.username || '').trim())
window.sessionStorage.setItem(AUTH_USER_KEY, JSON.stringify(normalizedUser))
}
function clearSessionTimeout() {
if (typeof window === 'undefined' || !sessionTimeoutHandle) {
return
}
@@ -337,10 +338,10 @@ function installSessionMonitoring() {
window.addEventListener('beforeunload', handleSessionUnload, { passive: true })
}
function syncAuthSession(options = {}) {
const shouldNotify = Boolean(options.notify)
if (!readAuthState()) {
function syncAuthSession(options = {}) {
const shouldNotify = Boolean(options.notify)
if (!readAuthState()) {
loggedIn.value = false
currentUser.value = buildAnonymousUser()
clearSessionTimeout()
@@ -354,11 +355,31 @@ function syncAuthSession(options = {}) {
loggedIn.value = true
currentUser.value = readStoredUser()
scheduleSessionTimeout()
return true
}
function reconcileEntryRoute(router) {
scheduleSessionTimeout()
return true
}
async function refreshCurrentUserFromBackend(options = {}) {
if (!readAuthState()) {
return false
}
try {
const payload = await fetchCurrentAuthUser()
const user = normalizeStoredAuthUser(payload)
currentUser.value = user
persistAuthUserSnapshot(user)
return true
} catch (error) {
if (!options.silent) {
toast(error.message || '当前用户信息刷新失败,请重新登录后再试。')
}
console.warn('Failed to refresh current user snapshot:', error)
return false
}
}
function reconcileEntryRoute(router) {
const target = resolveEntryRoute()
const current = router.currentRoute.value
@@ -376,10 +397,13 @@ export function installSessionNavigation(router) {
}
fetchBootstrapState()
.then((state) => {
applyBootstrapState(state)
setRuntimeApiBaseUrl(resolveBrowserApiBaseUrl(state))
fetchSettings()
.then((state) => {
applyBootstrapState(state)
setRuntimeApiBaseUrl(resolveBrowserApiBaseUrl(state))
if (loggedIn.value) {
refreshCurrentUserFromBackend({ silent: true })
}
fetchSettings()
.then((snapshot) => {
if (snapshot?.appearanceForm?.themeSkin) {
setThemeSkin(snapshot.appearanceForm.themeSkin)
@@ -653,11 +677,11 @@ async function handleLogin(credentials) {
password: credentials.password
})
const responseUser = response?.user || buildAnonymousUser()
const responseRoleCodes = Array.isArray(responseUser.roleCodes) ? responseUser.roleCodes.filter(Boolean) : []
const user = {
...responseUser,
roleCodes: responseRoleCodes,
const responseUser = normalizeStoredAuthUser(response?.user || buildAnonymousUser())
const responseRoleCodes = responseUser.roleCodes
const user = {
...responseUser,
roleCodes: responseRoleCodes,
isAdmin: resolvePlatformAdminFlag(responseUser, responseRoleCodes)
}
loggedIn.value = true
@@ -738,8 +762,9 @@ export function useSystemState() {
loginError,
loginSubmitting,
logout,
resetFromClientEnv,
resolveEntryRoute,
resetFromClientEnv,
refreshCurrentUserFromBackend,
resolveEntryRoute,
runtimeTestMessage,
runtimeTestPassed,
runtimeTesting,