feat(config): implement dynamic security blocklist and home marketing popup with admin controls

这个提交包含在:
xcbyy
2026-10-10 13:49:01 +08:00
父节点 df92166162
当前提交 2e726e125d
共有 8 个文件被更改,包括 616 次插入 和 8 次删除

查看文件

@@ -239,6 +239,145 @@
</a-card>
</div>
</a-tab-pane>
<!-- Tab 6: 自定义敏感词与私下引流过滤库 (P2) -->
<a-tab-pane key="security_words_config" title="引流风控与敏感词">
<div class="tab-pane-content">
<a-alert type="warning" style="margin-bottom: 20px">
在此维护平台自定义敏感词与私下交易拦截词库(如联系方式变体、私下转账、违规引流等)。管理员保存后系统后端实时热加载生效,小程序端发帖、评论及订单备注将实时阻断拦截。
</a-alert>
<a-card title="自定义敏感词黑名单库" :bordered="false" style="background: var(--color-fill-2)">
<template #extra>
<a-button type="outline" size="small" @click="addSecurityWord">
<template #icon><icon-plus /></template>
添加违规词
</a-button>
</template>
<div style="margin-bottom: 16px;">
<a-space wrap size="medium">
<a-tag
v-for="(word, index) in securityWordsList"
:key="index"
color="red"
closable
size="large"
@close="removeSecurityWord(index)"
>
{{ word }}
</a-tag>
</a-space>
</div>
<a-divider />
<a-form layout="vertical">
<a-form-item label="快捷批量录入敏感词(多个词请用换行或逗号隔开)">
<a-textarea
v-model="batchWordsInput"
placeholder="如:\n私下微信交易\n线下现金\n加v优惠"
:auto-size="{ minRows: 3, maxRows: 6 }"
/>
</a-form-item>
<a-button type="secondary" size="small" @click="handleBatchAddWords">
解析并导入违规词库
</a-button>
</a-form>
<div style="margin-top: 24px">
<a-button type="primary" :loading="saving" @click="saveSecurityWordsConfig">
保存敏感词库(实时热生效)
</a-button>
</div>
</a-card>
</div>
</a-tab-pane>
<!-- Tab 7: 首页运营全局弹窗 (P3) -->
<a-tab-pane key="home_popup_config" title="首页运营全局弹窗">
<div class="tab-pane-content">
<a-alert type="info" style="margin-bottom: 20px">
配置小程序首页首次或每日打开时弹出的营销/活动全局浮层弹窗(如新人首单特惠礼包、节日专属组局返现、官方安全温馨提示等)。
</a-alert>
<a-card title="全局浮层营销弹窗配置" :bordered="false" style="background: var(--color-fill-2)">
<a-form :model="homePopupForm.valueJson" layout="vertical">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="弹窗开关">
<a-switch v-model="homePopupForm.valueJson.enabled" checked-text="开启弹窗" unchecked-text="关闭隐藏" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="弹出频次规则">
<a-select v-model="homePopupForm.valueJson.frequency">
<a-option value="once_daily">每天首次打开弹出一次(推荐)</a-option>
<a-option value="once_ever">每个用户仅弹出一次</a-option>
<a-option value="always">每次冷启动打开均弹出</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="弹窗主标题">
<a-input v-model="homePopupForm.valueJson.title" placeholder="如:新人专享同城陪伴好礼" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="弹窗副标题/引导语">
<a-input v-model="homePopupForm.valueJson.subtitle" placeholder="如:注册即送20元无门槛立减券" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="弹窗海报图片 URL">
<div style="display: flex; gap: 12px; align-items: flex-start; width: 100%;">
<a-input v-model="homePopupForm.valueJson.imageUrl" placeholder="可直接输入图片公网URL或上传" style="flex: 1;" />
<a-upload
action="/api/upload"
name="file"
:show-file-list="false"
@success="handlePopupUploadSuccess"
@error="handlePopupUploadError"
>
<template #upload-button>
<a-button type="outline">
<template #icon><icon-upload /></template>
上传海报
</a-button>
</template>
</a-upload>
</div>
<div v-if="homePopupForm.valueJson.imageUrl" style="margin-top: 10px;">
<a-image :src="homePopupForm.valueJson.imageUrl" :height="120" fit="cover" style="border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);" />
</div>
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="按钮操作文本">
<a-input v-model="homePopupForm.valueJson.btnText" placeholder="如:立即领券组局" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="点击跳转路径 (小程序内部页面)">
<a-input v-model="homePopupForm.valueJson.link" placeholder="如:/pages/activity/list 或 /pages/user/index" />
</a-form-item>
</a-col>
</a-row>
<div style="margin-top: 16px">
<a-button type="primary" :loading="saving" @click="saveHomePopupConfig">
保存首页弹窗配置
</a-button>
</div>
</a-form>
</a-card>
</div>
</a-tab-pane>
</a-tabs>
</a-card>
</div>
@@ -336,6 +475,82 @@ const removeFaqItem = (index: number) => {
faqList.value.splice(index, 1);
};
// P2 敏感词库
const securityWordsList = ref<string[]>([
'加微信私聊', '私下走微信', '走线下免佣金', '线下给现金', '私下单',
'联系方式v', 'vx号', '微信转账', '私加好友', '线下私约'
]);
const batchWordsInput = ref('');
const addSecurityWord = () => {
const newWord = window.prompt('请输入需要拦截的违规敏感词');
if (newWord && newWord.trim()) {
const w = newWord.trim();
if (!securityWordsList.value.includes(w)) {
securityWordsList.value.push(w);
}
}
};
const removeSecurityWord = (index: number) => {
securityWordsList.value.splice(index, 1);
};
const handleBatchAddWords = () => {
if (!batchWordsInput.value.trim()) {
Message.warning('请输入需要批量导入的敏感词文本');
return;
}
const splitWords = batchWordsInput.value
.split(/[\n,,\s]+/)
.map(w => w.trim())
.filter(Boolean);
let addedCount = 0;
splitWords.forEach(w => {
if (!securityWordsList.value.includes(w)) {
securityWordsList.value.push(w);
addedCount++;
}
});
batchWordsInput.value = '';
Message.success(`成功解析并新增了 ${addedCount} 个敏感词`);
};
// P3 首页全局弹窗
const homePopupForm = reactive({
name: '小程序首页运营全局弹窗',
description: '节日活动推广、新用户专享首单优惠全局浮层弹窗',
status: 1,
valueJson: {
enabled: true,
title: '新人专享同城陪伴好礼',
subtitle: '注册即送20元无门槛立减券,首单专享特惠',
imageUrl: 'https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=800',
link: '/pages/activity/list',
btnText: '立即领券组局',
frequency: 'once_daily'
}
});
const handlePopupUploadSuccess = (fileItem: any) => {
const response = fileItem.response;
if (response && response.code === 20000 && response.data?.url) {
let url = response.data.url;
if (url.startsWith('/uploads/')) {
url = `https://peiww.gynhc.com${url}`;
}
homePopupForm.valueJson.imageUrl = url;
Message.success('弹窗海报上传成功并已持久化');
} else {
Message.warning('上传完成,但响应格式异常');
}
};
const handlePopupUploadError = () => {
Message.error('图片上传失败,请检查文件大小及网络');
};
const fetchConfigs = async () => {
loading.value = true;
try {
@@ -371,6 +586,15 @@ const fetchConfigs = async () => {
topicsList.value = Array.isArray(item.valueJson) ? item.valueJson : [];
} else if (item.configKey === 'faq_list') {
faqList.value = Array.isArray(item.valueJson) ? item.valueJson : [];
} else if (item.configKey === 'security_words') {
if (Array.isArray(item.valueJson)) {
securityWordsList.value = item.valueJson;
}
} else if (item.configKey === 'home_popup') {
homePopupForm.name = item.name;
homePopupForm.description = item.description;
homePopupForm.status = item.status;
homePopupForm.valueJson = { ...homePopupForm.valueJson, ...(item.valueJson || {}) };
}
});
}
@@ -436,6 +660,19 @@ const saveFaqConfig = () => {
});
};
const saveSecurityWordsConfig = () => {
saveConfig('security_words', {
name: '私下引流与违规自定义敏感词库',
description: '全平台发帖、评论及订单防私下交易关键词实时过滤',
status: 1,
valueJson: securityWordsList.value
});
};
const saveHomePopupConfig = () => {
saveConfig('home_popup', homePopupForm);
};
onMounted(() => {
fetchConfigs();
});