fix: 角色权限与数据范围修复
- 添加 RoleRoute 路由守卫组件,防止越权访问页面 - 菜单配置:区域经理加'区域对比',店长加'态势感知' - database.ts: scope注入时检测主表别名避免store_code歧义 - analytics-enhanced.ts: KPI region模式手动注入scope过滤 - situational-awareness.ts: health-score手动注入scope,alerts加skipScope - smart-scheduling.ts: position-salary-compare店长角色降低HAVING阈值
This commit is contained in:
@@ -34,35 +34,68 @@ export async function query<T = any>(text: string, params?: any[], opts?: { skip
|
||||
|
||||
if (!opts?.skipScope && ctx?.scope && (ctx.scope.role === 'store' || ctx.scope.role === 'regional')) {
|
||||
const sqlLower = sql.toLowerCase()
|
||||
const hasStoreRef = /store_code|store_name|v_store_|mv_store_|bill_fact|fact_bill|store_task|dim_store/.test(sqlLower)
|
||||
if (!hasStoreRef) {
|
||||
const res = await usePool.query(sql, sqlParams)
|
||||
const duration = Date.now() - start
|
||||
if (duration > 500) {
|
||||
console.warn(`Slow query (${duration}ms):`, sql.substring(0, 100))
|
||||
}
|
||||
return res
|
||||
}
|
||||
const scopeCodes = ctx.scope.role === 'store'
|
||||
? ctx.scope.storeCode!.split(',').map(s => s.trim()).filter(Boolean)
|
||||
: ctx.scope.region!.split(',').map(s => s.trim()).filter(Boolean)
|
||||
|
||||
const scopeClause = ctx.scope.role === 'store' && ctx.scope.storeCode
|
||||
? ` store_code = $${sqlParams.length + 1}`
|
||||
: ctx.scope.role === 'regional' && ctx.scope.region
|
||||
? ` store_code = ANY($${sqlParams.length + 1})`
|
||||
: null
|
||||
if (scopeCodes.length > 0) {
|
||||
// Handle raw import tables with non-standard store columns
|
||||
const hasBillRecords = /\bbill_records\b/.test(sqlLower)
|
||||
const hasSalaryRecords = /\bsalary_detail_records\b/.test(sqlLower)
|
||||
const hasAttendanceRecords = /\battendance_records\b/.test(sqlLower)
|
||||
const rawTableCount = [hasBillRecords, hasSalaryRecords, hasAttendanceRecords].filter(Boolean).length
|
||||
|
||||
if (scopeClause) {
|
||||
const scopeValue = ctx.scope.role === 'store'
|
||||
? ctx.scope.storeCode
|
||||
: ctx.scope.region!.split(',').map(s => s.trim()).filter(Boolean)
|
||||
sqlParams = [...sqlParams, scopeValue]
|
||||
if (sql.includes('WHERE')) {
|
||||
sql = sql.replace('WHERE', `WHERE${scopeClause} AND`)
|
||||
} else if (sql.includes('GROUP BY')) {
|
||||
sql = sql.replace('GROUP BY', `WHERE${scopeClause} GROUP BY`)
|
||||
} else if (sql.includes('ORDER BY')) {
|
||||
sql = sql.replace('ORDER BY', `WHERE${scopeClause} ORDER BY`)
|
||||
let scopeClause: string | null = null
|
||||
if (rawTableCount === 1 && hasBillRecords) {
|
||||
scopeClause = ` c003 IN (SELECT store_name FROM analytics.dim_store WHERE store_code = ANY($${sqlParams.length + 1}))`
|
||||
} else if (rawTableCount === 1 && hasSalaryRecords) {
|
||||
scopeClause = ` org_level5 IN (SELECT COALESCE(m.salary_name, d.store_name) FROM analytics.dim_store d LEFT JOIN store_name_mapping m ON m.bill_name = d.store_name WHERE d.store_code = ANY($${sqlParams.length + 1}))`
|
||||
} else if (rawTableCount === 1 && hasAttendanceRecords) {
|
||||
scopeClause = ` EXISTS (SELECT 1 FROM analytics.dim_store d WHERE d.store_code = ANY($${sqlParams.length + 1}) AND ${/\bar\b/.test(sqlLower) ? 'ar' : 'attendance_records'}.department LIKE '%' || d.store_name || '%')`
|
||||
} else if (rawTableCount >= 2 && hasSalaryRecords) {
|
||||
// Multiple raw tables with salary_detail_records as main table (e.g. attendance-alert)
|
||||
scopeClause = ` s.org_level5 IN (SELECT COALESCE(m.salary_name, d.store_name) FROM analytics.dim_store d LEFT JOIN store_name_mapping m ON m.bill_name = d.store_name WHERE d.store_code = ANY($${sqlParams.length + 1}))`
|
||||
} else {
|
||||
sql = sql + ` WHERE${scopeClause}`
|
||||
// For normal views/tables with store_code or store_name
|
||||
const hasStoreCode = /\bstore_code\b/.test(sqlLower)
|
||||
const hasStoreName = /\bstore_name\b/.test(sqlLower)
|
||||
const hasStoreRef = hasStoreCode || hasStoreName || /v_store_|mv_store_|bill_fact|fact_bill|store_task|dim_store/.test(sqlLower)
|
||||
if (hasStoreRef) {
|
||||
// Detect main table alias to avoid ambiguous column reference
|
||||
// Look for "FROM <table> <alias>" pattern where alias is a short identifier
|
||||
const fromMatch = sql.match(/\bFROM\s+[\w.]+\s+(?:AS\s+)?(\w+)/i)
|
||||
const mainAlias = fromMatch && !['where', 'group', 'order', 'left', 'right', 'inner', 'join', 'on', 'and', 'or', 'select', 'from', 'having', 'limit', 'union'].includes(fromMatch[1].toLowerCase()) ? fromMatch[1] : null
|
||||
const colPrefix = mainAlias ? `${mainAlias}.` : ''
|
||||
scopeClause = hasStoreCode
|
||||
? ` ${colPrefix}store_code = ANY($${sqlParams.length + 1})`
|
||||
: ` ${colPrefix}store_name IN (SELECT store_name FROM analytics.dim_store WHERE store_code = ANY($${sqlParams.length + 1}))`
|
||||
}
|
||||
}
|
||||
|
||||
if (scopeClause) {
|
||||
sqlParams = [...sqlParams, scopeCodes]
|
||||
// Inject scope filter into the main WHERE clause, skipping WHERE inside FILTER(WHERE ...) and subqueries
|
||||
// Strategy: mask FILTER(WHERE), then replace the LAST WHERE (main query's WHERE comes after subquery WHEREs)
|
||||
const maskToken = '__FWHM__'
|
||||
const maskedSql = sql.replace(/FILTER\s*\(\s*WHERE\b/g, `FILTER (${maskToken}`)
|
||||
const whereMatches = [...maskedSql.matchAll(/\bWHERE\b/g)]
|
||||
if (whereMatches.length > 0) {
|
||||
// Replace the last WHERE occurrence (main query WHERE is typically last)
|
||||
const lastWhere = whereMatches[whereMatches.length - 1]
|
||||
sql = maskedSql.substring(0, lastWhere.index!) + `WHERE${scopeClause} AND` + maskedSql.substring(lastWhere.index! + 5)
|
||||
sql = sql.replace(new RegExp(maskToken, 'g'), 'WHERE')
|
||||
} else {
|
||||
// No WHERE clause: inject after the last FROM <table> in the main (non-CTE) query
|
||||
const fromMatches = [...maskedSql.matchAll(/\bFROM\s+(\w+)/g)]
|
||||
if (fromMatches.length > 0) {
|
||||
const lastFrom = fromMatches[fromMatches.length - 1]
|
||||
const insertPos = lastFrom.index! + lastFrom[0].length
|
||||
sql = maskedSql.substring(0, insertPos) + ` WHERE${scopeClause}` + maskedSql.substring(insertPos)
|
||||
} else {
|
||||
sql = sql + ` WHERE${scopeClause}`
|
||||
}
|
||||
sql = sql.replace(new RegExp(maskToken, 'g'), 'WHERE')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user