refactor: 风险规则服务端默认模板+版本历史+UI
This commit is contained in:
@@ -38,8 +38,12 @@
|
|||||||
<el-table-column label="更新时间" width="150">
|
<el-table-column label="更新时间" width="150">
|
||||||
<template #default="s">{{ fmt(s.row.updatedAt) }}</template>
|
<template #default="s">{{ fmt(s.row.updatedAt) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
<el-table-column label="操作" width="220" fixed="right">
|
||||||
<template #default="s">
|
<template #default="s">
|
||||||
|
<el-select v-model="ruleVer" placeholder="历史" size="small" style="width:70px" clearable
|
||||||
|
@change="v=>loadRuleVersion(s.row,v)" @click.stop>
|
||||||
|
<el-option v-for="v in s.row.versions||[]" :key="v" :label="'v'+v" :value="v" />
|
||||||
|
</el-select>
|
||||||
<el-button size="small" @click="startEdit(s.row)" :type="editSiteId===s.row.siteId?'warning':''">
|
<el-button size="small" @click="startEdit(s.row)" :type="editSiteId===s.row.siteId?'warning':''">
|
||||||
{{ editSiteId===s.row.siteId ? '收起' : '编辑' }}
|
{{ editSiteId===s.row.siteId ? '收起' : '编辑' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -75,25 +79,16 @@
|
|||||||
import api from '../api'
|
import api from '../api'
|
||||||
|
|
||||||
const loading = ref(false), saving = ref(false), viewVisible = ref(false), viewType = ref(''), viewJson = ref('')
|
const loading = ref(false), saving = ref(false), viewVisible = ref(false), viewType = ref(''), viewJson = ref('')
|
||||||
const sites = ref([]), rulesMap = ref({})
|
const sites = ref([]), rulesMap = ref({}), defaultRulesMap = ref({}), ruleVer = ref('')
|
||||||
const editSiteId = ref(''), editSiteType = ref(''), editorJson = ref('')
|
const editSiteId = ref(''), editSiteType = ref(''), editorJson = ref('')
|
||||||
|
|
||||||
const defaultRules = {
|
const fallbackRules = { nexusphp: [{type:'KILL',name:'',metric:'',threshold:0,unit:'DAY',condition:'GT',level:'CRITICAL'}] }
|
||||||
nexusphp: [
|
|
||||||
{type:'KILL', name:'登录间隔超过30天', metric:'daysSinceLastLogin', threshold:30, unit:'DAY', condition:'GT', level:'CRITICAL'},
|
|
||||||
{type:'RATIO', name:'分享率过低', metric:'shareRatio', threshold:0.3, condition:'LT', level:'CRITICAL', applicable:{daysSinceRegistration:30}},
|
|
||||||
{type:'TRAFFIC',name:'超过5天无流量', metric:'daysSinceLastTraffic', threshold:5, unit:'DAY', condition:'GT', level:'WARN', applicable:{daysSinceRegistration:30}},
|
|
||||||
],
|
|
||||||
mtorrent: [
|
|
||||||
{type:'KILL', name:'登录过期', metric:'daysSinceLastLogin', threshold:30, unit:'DAY', condition:'GT', level:'CRITICAL'},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaults = computed(() => {
|
const defaults = computed(() => {
|
||||||
const types = [...new Set((Array.isArray(sites.value)?sites.value:[]).map(s=>s.siteType).filter(Boolean))]
|
const types = [...new Set((Array.isArray(sites.value)?sites.value:[]).map(s=>s.siteType).filter(Boolean))]
|
||||||
const base = ['nexusphp','mtorrent','gazelle','unit3d']
|
const base = ['nexusphp','mtorrent','gazelle','unit3d']
|
||||||
return [...new Set([...types,...base])].map(t => {
|
return [...new Set([...types,...base])].map(t => {
|
||||||
const rules = defaultRules[t] || defaultRules['nexusphp'] || []
|
const rules = defaultRulesMap.value[t] || fallbackRules[t] || []
|
||||||
return {type:t, ruleCount:rules.length, rules}
|
return {type:t, ruleCount:rules.length, rules}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -109,6 +104,7 @@ const merged = computed(() => {
|
|||||||
version: rulesMap.value[s.siteType]?.version || null,
|
version: rulesMap.value[s.siteType]?.version || null,
|
||||||
updatedAt: rulesMap.value[s.siteType]?.updatedAt || null,
|
updatedAt: rulesMap.value[s.siteType]?.updatedAt || null,
|
||||||
rulesJson: rulesMap.value[s.siteType]?.rulesJson || null,
|
rulesJson: rulesMap.value[s.siteType]?.rulesJson || null,
|
||||||
|
versions: rulesMap.value[s.siteType]?.versions || [],
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -120,13 +116,31 @@ const fetchAll = async () => {
|
|||||||
api.raw.get('/admin/risk-rules'),
|
api.raw.get('/admin/risk-rules'),
|
||||||
])
|
])
|
||||||
sites.value = sr.status==='fulfilled' ? (Array.isArray(sr.value?.data)?sr.value.data:sr.value||[]) : []
|
sites.value = sr.status==='fulfilled' ? (Array.isArray(sr.value?.data)?sr.value.data:sr.value||[]) : []
|
||||||
|
const typeSet = new Set(sites.value.map(s=>s.siteType).filter(Boolean))
|
||||||
|
siteTypes.value = [...typeSet]
|
||||||
|
if (siteTypes.value.length===0) siteTypes.value = ['nexusphp','mtorrent','gazelle','unit3d']
|
||||||
|
|
||||||
const rulesData = rr.status==='fulfilled' ? (Array.isArray(rr.value?.data)?rr.value.data:rr.value||[]) : []
|
const rulesData = rr.status==='fulfilled' ? (Array.isArray(rr.value?.data)?rr.value.data:rr.value||[]) : []
|
||||||
const map = {}
|
const map = {}, verMap = {}
|
||||||
for (const r of rulesData) {
|
for (const r of rulesData) {
|
||||||
const cnt = (() => { try { return JSON.parse(r.rulesJson||'[]').length } catch { return 0 } })()
|
const cnt = (() => { try { return JSON.parse(r.rulesJson||'[]').length } catch { return 0 } })()
|
||||||
map[r.siteType] = { ruleCount: cnt, version: r.version, updatedAt: r.updatedAt, rulesJson: r.rulesJson }
|
const v = r.version
|
||||||
|
if (!map[r.siteType] || v > map[r.siteType].version) {
|
||||||
|
map[r.siteType] = { ruleCount: cnt, version: v, updatedAt: r.updatedAt, rulesJson: r.rulesJson }
|
||||||
}
|
}
|
||||||
|
if (!verMap[r.siteType]) verMap[r.siteType] = new Set()
|
||||||
|
verMap[r.siteType].add(v)
|
||||||
|
}
|
||||||
|
for (const k in verMap) map[k].versions = [...verMap[k]].sort((a,b)=>b-a)
|
||||||
rulesMap.value = map
|
rulesMap.value = map
|
||||||
|
// 加载默认模板
|
||||||
|
for (const t of siteTypes.value) {
|
||||||
|
try {
|
||||||
|
const r = await api.raw.get('/admin/default-rules/'+t)
|
||||||
|
const data = r?.data || r
|
||||||
|
if (data?.rules) defaultRulesMap.value[t] = data.rules
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
} finally { loading.value = false }
|
} finally { loading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,10 +157,18 @@ const editDefault = (dt) => {
|
|||||||
window.scrollTo({top: document.body.scrollHeight, behavior:'smooth'})
|
window.scrollTo({top: document.body.scrollHeight, behavior:'smooth'})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadRuleVersion = async (row, ver) => {
|
||||||
|
if (!ver) return
|
||||||
|
try {
|
||||||
|
const r = await api.raw.get('/admin/risk-rules/'+row.siteType+'/version/'+ver)
|
||||||
|
editorJson.value = JSON.stringify(JSON.parse((r?.data||r)?.content||'[]'), null, 2)
|
||||||
|
} catch { ElMessage.error('加载版本失败') }
|
||||||
|
}
|
||||||
|
|
||||||
const loadDefaultRules = (row) => {
|
const loadDefaultRules = (row) => {
|
||||||
editSiteId.value = row.siteId
|
editSiteId.value = row.siteId
|
||||||
editSiteType.value = row.siteType
|
editSiteType.value = row.siteType
|
||||||
const rules = defaultRules[row.siteType] || defaultRules['nexusphp'] || []
|
const rules = defaultRulesMap.value[row.siteType] || fallbackRules[row.siteType] || []
|
||||||
editorJson.value = JSON.stringify(rules, null, 2)
|
editorJson.value = JSON.stringify(rules, null, 2)
|
||||||
window.scrollTo({top: document.body.scrollHeight, behavior:'smooth'})
|
window.scrollTo({top: document.body.scrollHeight, behavior:'smooth'})
|
||||||
}
|
}
|
||||||
@@ -162,7 +184,10 @@ const startEdit = (row) => {
|
|||||||
const saveRules = async () => {
|
const saveRules = async () => {
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
await api.raw.post('/risk-rules/'+editSiteType.value, { rules: JSON.parse(editorJson.value) })
|
const url = editSiteType.value.startsWith('default:')
|
||||||
|
? '/admin/default-rules/'+editSiteType.value.replace('default:','')
|
||||||
|
: '/risk-rules/'+editSiteType.value
|
||||||
|
await api.raw.post(url, { rules: JSON.parse(editorJson.value) })
|
||||||
ElMessage.success('已保存'); editSiteId.value = ''; fetchAll()
|
ElMessage.success('已保存'); editSiteId.value = ''; fetchAll()
|
||||||
} catch { ElMessage.error('保存失败') }
|
} catch { ElMessage.error('保存失败') }
|
||||||
finally { saving.value = false }
|
finally { saving.value = false }
|
||||||
|
|||||||
Reference in New Issue
Block a user