diff --git a/web/src/views/RiskRules.vue b/web/src/views/RiskRules.vue
index 84829c3..7983e87 100644
--- a/web/src/views/RiskRules.vue
+++ b/web/src/views/RiskRules.vue
@@ -89,7 +89,8 @@
- {{ metricLabel(rule.metric) }} {{ condLabel(rule.condition) }} {{ rule.threshold }}{{ rule.unit || '' }}
+ 考核周期 {{ rule.threshold }}天
+ {{ metricLabel(rule.metric) }} {{ condLabel(rule.condition) }} {{ rule.threshold }}{{ rule.unit || '' }}
→
封号/删除账号
@@ -105,14 +106,32 @@
-
-
- 阈值
-
- {{ rule.unit || '' }}
-
+
+
+
+ 考核周期
+
+ 天内完成以下要求,否则封号/删除账号
+
+ 考核要求
+
+ {{ r.label }}
+ 不低于
+
+ {{ r.unit }}
+
+
-
+
+
+
+ 阈值
+
+ {{ rule.unit || '' }}
+
+
+
+
适用条件
@@ -164,6 +183,7 @@ const typeOptions = [
{ value: 'TRAFFIC', label: '流量' },
{ value: 'HD', label: 'H&R' },
{ value: 'RATIO', label: '分享率' },
+ { value: 'TASK', label: '新人考核' },
]
const condOptions = [
{ value: 'GT', label: '大于' },
@@ -186,6 +206,7 @@ const metricOptions = [
{ value: 'download_gb', label: '下载量(GB)' },
{ value: 'bonus', label: '魔力值' },
{ value: 'seeding', label: '做种数' },
+ { value: 'seeding_score', label: '做种积分' },
{ value: 'hnr_count', label: 'H&R 违规次数' },
{ value: 'avg_seed_time', label: '平均做种时间(小时)' },
]
@@ -201,9 +222,25 @@ const typeDefaults = {
const metricLabel = (m) => metricOptions.find(x => x.value === m)?.label || m || ''
const condLabel = (c) => condOptions.find(x => x.value === c)?.label || c || ''
+const taskReqDefaults = [
+ { metric:'upload_gb', label:'上传量', threshold:10, unit:'GB', condition:'GTE' },
+ { metric:'download_gb', label:'下载量', threshold:10, unit:'GB', condition:'GTE' },
+ { metric:'bonus', label:'魔力值', threshold:1000, unit:'', condition:'GTE' },
+ { metric:'seeding_score', label:'做种积分', threshold:100, unit:'', condition:'GTE' },
+ { metric:'avg_seed_time', label:'平均做种时间', threshold:24, unit:'小时', condition:'GTE' },
+]
+
const onTypeChange = (rule) => {
- const d = typeDefaults[rule.type]
- if (d) { rule.metric = d.metric; rule.condition = d.condition; rule.unit = d.unit }
+ if (rule.type === 'TASK') {
+ if (!rule.requirements || !rule.requirements.length) {
+ rule.requirements = JSON.parse(JSON.stringify(taskReqDefaults))
+ }
+ rule.threshold = rule.threshold || 30
+ rule.unit = '天'
+ } else {
+ const d = typeDefaults[rule.type]
+ if (d) { rule.metric = d.metric; rule.condition = d.condition; rule.unit = d.unit }
+ }
sync()
}
@@ -220,6 +257,8 @@ const presets = [
{ name:'连续无流量封号', type:'TRAFFIC', metric:'days_since_last_traffic', threshold:90, unit:'天', condition:'GT', description:'连续90天无上传/下载流量即封号' },
// H&R
{ name:'H&R 封号', type:'HD', metric:'hnr_count', threshold:5, unit:'次', condition:'GTE', description:'H&R违规次数达到5次即封号' },
+ // 新人考核
+ { name:'新人考核', type:'TASK', threshold:30, unit:'天', description:'新注册用户需在考核周期内完成各项要求' },
]
const presetCategories = [
@@ -227,9 +266,11 @@ const presetCategories = [
{ title:'五、零活跃新用户清理', items: presets.filter(p => p.name.includes('零活跃')) },
{ title:'流量监控', items: presets.filter(p => p.type === 'TRAFFIC') },
{ title:'H&R 惩罚', items: presets.filter(p => p.type === 'HD') },
+ { title:'新人考核', items: presets.filter(p => p.type === 'TASK') },
]
const presetDesc = (p) => {
+ if (p.type === 'TASK') return `考核周期 ${p.threshold||30} 天,需满足上传/下载/魔力值/做种积分/做种时间要求`
const condLabel = condOptions.find(c => c.value === p.condition)?.label || p.condition
const appLabel = applicableOptions.find(a => a.value === p.applicable)?.label || ''
return `${metricOptions.find(m=>m.value===p.metric)?.label||p.metric} ${condLabel} ${p.threshold}${p.unit||''}${appLabel?' ('+appLabel+')':''}`
@@ -301,10 +342,24 @@ const confirmRemove = async (idx) => {
const arr = [...parsed.value]; arr.splice(idx, 1); parsed.value = arr
}
-const addPreset = (p) => { parsed.value = [...parsed.value, { ...p, _fold: false }] }
+const ensureTaskReqs = (rule) => {
+ if (rule.type === 'TASK' && !rule.requirements) {
+ rule.requirements = JSON.parse(JSON.stringify(taskReqDefaults))
+ }
+}
+
+const addPreset = (p) => {
+ const r = { ...p, _fold: false }
+ ensureTaskReqs(r)
+ parsed.value = [...parsed.value, r]
+}
const importPresets = () => {
- const items = presets.filter(p => selectedPresets.value.includes(p.name)).map(p => ({ ...p, _fold: false }))
+ const items = presets.filter(p => selectedPresets.value.includes(p.name)).map(p => {
+ const r = { ...p, _fold: false }
+ ensureTaskReqs(r)
+ return r
+ })
parsed.value = [...parsed.value, ...items]
selectedPresets.value = []
showPresets.value = false
@@ -368,5 +423,7 @@ onMounted(fetchAll)
.field-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 13px; }
.field-label { font-weight: 500; color: #606266; font-size: 12px; min-width: 56px; }
.field-hint { color: #909399; font-size: 12px; }
+.condition-text { color: #909399; font-size: 12px; }
+.req-divider { font-size: 12px; font-weight: 500; color: #303133; padding-top: 4px; border-top: 1px dashed #dcdfe6; }
.then-arrow { color: #f56c6c; font-size: 14px; font-weight: 700; margin: 0 2px; }