From 2e726e125df406772d84d84550818c9e5c72b668 Mon Sep 17 00:00:00 2001
From: xcbyy <2578898740@qq.com>
Date: Sat, 10 Oct 2026 13:49:01 +0800
Subject: [PATCH] feat(config): implement dynamic security blocklist and home
marketing popup with admin controls
---
.../views/safety/platform-config/index.vue | 237 +++++++++++++++++
pwuniapp/components/HomePopupModal.vue | 243 ++++++++++++++++++
pwuniapp/pages/index/index.vue | 4 +
.../core/security/content-security.service.ts | 34 +++
server/src/db/mysql.ts | 35 ++-
server/src/db/repository.ts | 17 ++
server/src/index.ts | 24 +-
server/tests/content-security.spec.ts | 30 +++
8 files changed, 616 insertions(+), 8 deletions(-)
create mode 100644 pwuniapp/components/HomePopupModal.vue
diff --git a/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue b/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue
index 8e4e6f2..560eb86 100644
--- a/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue
+++ b/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue
@@ -239,6 +239,145 @@
+
+
+
+
+
+ 在此维护平台自定义敏感词与私下交易拦截词库(如联系方式变体、私下转账、违规引流等)。管理员保存后系统后端实时热加载生效,小程序端发帖、评论及订单备注将实时阻断拦截。
+
+
+
+
+
+
+ 添加违规词
+
+
+
+
+
+
+
+
+
+
+
+
+ 解析并导入违规词库
+
+
+
+
+
+
+
+
+
+
+
+
+ 配置小程序首页首次或每日打开时弹出的营销/活动全局浮层弹窗(如新人首单特惠礼包、节日专属组局返现、官方安全温馨提示等)。
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 每天首次打开弹出一次(推荐)
+ 每个用户仅弹出一次
+ 每次冷启动打开均弹出
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -336,6 +475,82 @@ const removeFaqItem = (index: number) => {
faqList.value.splice(index, 1);
};
+// P2 敏感词库
+const securityWordsList = ref([
+ '加微信私聊', '私下走微信', '走线下免佣金', '线下给现金', '私下单',
+ '联系方式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();
});
diff --git a/pwuniapp/components/HomePopupModal.vue b/pwuniapp/components/HomePopupModal.vue
new file mode 100644
index 0000000..d225a26
--- /dev/null
+++ b/pwuniapp/components/HomePopupModal.vue
@@ -0,0 +1,243 @@
+
+
+
+
+
+
+
diff --git a/pwuniapp/pages/index/index.vue b/pwuniapp/pages/index/index.vue
index 62912d1..19a5c05 100644
--- a/pwuniapp/pages/index/index.vue
+++ b/pwuniapp/pages/index/index.vue
@@ -120,6 +120,9 @@
+
+
+
@@ -129,6 +132,7 @@ import { onShow } from '@dcloudio/uni-app';
import CustomTabBar from '@/components/CustomTabBar.vue';
import SkeletonScreen from '@/components/SkeletonScreen.vue';
import PrivacyModal from '@/components/PrivacyModal.vue';
+import HomePopupModal from '@/components/HomePopupModal.vue';
import { getCompanions, getBanners, getNavCategories } from '@/common/api.js';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
diff --git a/server/src/core/security/content-security.service.ts b/server/src/core/security/content-security.service.ts
index 08c4ae3..4962f32 100644
--- a/server/src/core/security/content-security.service.ts
+++ b/server/src/core/security/content-security.service.ts
@@ -30,6 +30,25 @@ export class ContentSecurityService {
]
};
+ // 动态自定义敏感词列表(支持管理后台增删改并实时热生效)
+ private customKeywords: string[] = [];
+
+ /**
+ * 载入或更新动态敏感词黑名单
+ */
+ public setCustomKeywords(keywords: string[]): void {
+ if (Array.isArray(keywords)) {
+ this.customKeywords = keywords.map(k => k.trim()).filter(Boolean);
+ }
+ }
+
+ /**
+ * 获取当前生效的所有自定义敏感词
+ */
+ public getCustomKeywords(): string[] {
+ return [...this.customKeywords];
+ }
+
/**
* 检查文本内容安全 (security.msgSecCheck)
* @param text 待检测文本(支持动态、评论、活动标题、个人简介等)
@@ -47,6 +66,21 @@ export class ContentSecurityService {
const cleanText = text.replace(/[\s\r\n\t\-_,.,。!?]/g, '').toLowerCase();
+ // 1. 优先检测管理后台运营配置的自定义违规词库
+ for (const kw of this.customKeywords) {
+ const cleanKw = kw.replace(/[\s\r\n\t\-_,.,。!?]/g, '').toLowerCase();
+ if (cleanText.includes(cleanKw)) {
+ return {
+ pass: false,
+ code: 87014,
+ label: 'CUSTOM_BLOCKLIST',
+ suggest: 'block',
+ message: `内容含有涉嫌违规或敏感词汇[${kw}],已被系统拦截,请文明发言`
+ };
+ }
+ }
+
+ // 2. 检测系统内置的核心安全敏感词库
for (const [category, keywords] of Object.entries(ContentSecurityService.SENSITIVE_KEYWORDS)) {
for (const kw of keywords) {
const cleanKw = kw.replace(/[\s\r\n\t\-_,.,。!?]/g, '').toLowerCase();
diff --git a/server/src/db/mysql.ts b/server/src/db/mysql.ts
index bb88518..be31e23 100644
--- a/server/src/db/mysql.ts
+++ b/server/src/db/mysql.ts
@@ -686,6 +686,21 @@ async function seedInitialDataIfEmpty(conn: any): Promise {
}
]);
+ const securityWordsVal = JSON.stringify([
+ '加微信私聊', '私下走微信', '走线下免佣金', '线下给现金', '私下单',
+ '联系方式v', 'vx号', '微信转账', '私加好友', '线下私约'
+ ]);
+
+ const homePopupVal = JSON.stringify({
+ enabled: true,
+ title: '新人专享同城陪伴好礼',
+ subtitle: '注册即送20元无门槛立减券,首单专享特惠',
+ imageUrl: 'https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=800',
+ link: '/pages/activity/list',
+ btnText: '立即领券组局',
+ frequency: 'once_daily' // once_daily | always | once_ever
+ });
+
const [cfgItemRows]: any = await conn.query('SELECT COUNT(*) as count FROM platform_configs');
if (cfgItemRows[0].count === 0) {
const customerServiceVal = JSON.stringify({
@@ -720,10 +735,12 @@ async function seedInitialDataIfEmpty(conn: any): Promise {
('platform_protocol', '平台服务协议与合规规范', ?, '小程序端服务协议与隐私政策详情', 1),
('community_topics', '友圈热门推荐话题库', ?, '友圈广场发帖推荐话题与热门标签', 1),
('community_notice', '友圈社区置顶头条公告', ?, '友圈广场顶部置顶横幅公告与社区公约', 1),
- ('faq_list', '帮助中心与常见问题FAQ', ?, '小程序端帮助中心常见问答与指引', 1);
- `, [customerServiceVal, safetyNoticeVal, protocolVal, communityTopicsVal, communityNoticeVal, faqListVal]);
+ ('faq_list', '帮助中心与常见问题FAQ', ?, '小程序端帮助中心常见问答与指引', 1),
+ ('security_words', '私下引流与违规自定义敏感词库', ?, '全平台发帖、评论及订单防私下交易关键词实时过滤', 1),
+ ('home_popup', '小程序首页运营全局弹窗', ?, '节日活动推广、新用户专享首单优惠全局浮层弹窗', 1);
+ `, [customerServiceVal, safetyNoticeVal, protocolVal, communityTopicsVal, communityNoticeVal, faqListVal, securityWordsVal, homePopupVal]);
} else {
- // 增量保证 community_topics, community_notice, faq_list 存在
+ // 增量保证各核心配置项完整就绪
const [existingRows]: any = await conn.query('SELECT config_key FROM platform_configs');
const existingKeys = new Set(existingRows.map((r: any) => r.config_key));
if (!existingKeys.has('community_topics')) {
@@ -744,5 +761,17 @@ async function seedInitialDataIfEmpty(conn: any): Promise {
VALUES ('faq_list', '帮助中心与常见问题FAQ', ?, '小程序端帮助中心常见问答与指引', 1)
`, [faqListVal]);
}
+ if (!existingKeys.has('security_words')) {
+ await conn.query(`
+ INSERT INTO platform_configs (config_key, name, value_json, description, status)
+ VALUES ('security_words', '私下引流与违规自定义敏感词库', ?, '全平台发帖、评论及订单防私下交易关键词实时过滤', 1)
+ `, [securityWordsVal]);
+ }
+ if (!existingKeys.has('home_popup')) {
+ await conn.query(`
+ INSERT INTO platform_configs (config_key, name, value_json, description, status)
+ VALUES ('home_popup', '小程序首页运营全局弹窗', ?, '节日活动推广、新用户专享首单优惠全局浮层弹窗', 1)
+ `, [homePopupVal]);
+ }
}
}
diff --git a/server/src/db/repository.ts b/server/src/db/repository.ts
index 0bbf51a..cdf0128 100644
--- a/server/src/db/repository.ts
+++ b/server/src/db/repository.ts
@@ -1279,6 +1279,23 @@ export class MysqlRepository {
return result;
}
+ async getPlatformConfigByKey(configKey: string): Promise {
+ const [rows]: any = await pool.query(`
+ SELECT id, config_key as configKey, name, value_json as valueJson, description, status
+ FROM platform_configs
+ WHERE config_key = ?
+ `, [configKey]);
+ if (!rows || rows.length === 0) return null;
+ const r = rows[0];
+ let val = r.valueJson;
+ if (typeof val === 'string') {
+ try {
+ val = JSON.parse(val);
+ } catch (_) {}
+ }
+ return { ...r, valueJson: val };
+ }
+
async getAdminPlatformConfigs(): Promise {
const [rows]: any = await pool.query(`
SELECT id, config_key as configKey, name, value_json as valueJson, description, status,
diff --git a/server/src/index.ts b/server/src/index.ts
index 9c7de5d..a53b605 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -1078,11 +1078,13 @@ app.put('/api/admin/system/configs/:key', async (req: Request, res: Response) =>
const configKey = String(req.params.key);
const { name, valueJson, description, status } = req.body;
- // 微信内容安全审查(检查文本内容或字符串化后的配置)
- const textToCheck = `${name || ''} ${description || ''} ${JSON.stringify(valueJson || '')}`;
- const sec = contentSecurityService.checkText(textToCheck);
- if (!sec.pass) {
- return res.status(400).json({ code: sec.code, message: sec.message });
+ // 微信内容安全审查(检查文本内容或字符串化后的配置,security_words 黑名单库配置项除外)
+ if (configKey !== 'security_words') {
+ const textToCheck = `${name || ''} ${description || ''} ${JSON.stringify(valueJson || '')}`;
+ const sec = contentSecurityService.checkText(textToCheck);
+ if (!sec.pass) {
+ return res.status(400).json({ code: sec.code, message: sec.message });
+ }
}
const updated = await repo.updatePlatformConfig(configKey, {
@@ -1096,6 +1098,11 @@ app.put('/api/admin/system/configs/:key', async (req: Request, res: Response) =>
return res.status(404).json({ code: 40400, message: '配置项不存在' });
}
+ // 若更新的是自定义敏感词库,实时热更新内存过滤引擎
+ if (configKey === 'security_words' && Array.isArray(valueJson)) {
+ contentSecurityService.setCustomKeywords(valueJson);
+ }
+
res.json({ code: 20000, data: updated, message: '系统配置更新成功' });
} catch (err: any) {
res.status(400).json({ code: 40000, message: err.message });
@@ -1616,6 +1623,13 @@ async function bootstrap() {
try {
await initDatabase();
console.log('[Peiwan Server] MySQL 数据库核心表及种子数据已校验就绪');
+
+ // 载入自定义敏感词过滤库
+ const secWordsConfig = await repo.getPlatformConfigByKey('security_words');
+ if (secWordsConfig && Array.isArray(secWordsConfig.valueJson)) {
+ contentSecurityService.setCustomKeywords(secWordsConfig.valueJson);
+ console.log(`[Peiwan Server] 已成功装载 ${secWordsConfig.valueJson.length} 条自定义安全拦截词`);
+ }
} catch (err) {
console.error('[Peiwan Server] MySQL 初始化连接发生警告:', err);
}
diff --git a/server/tests/content-security.spec.ts b/server/tests/content-security.spec.ts
index 97a3291..af03240 100644
--- a/server/tests/content-security.spec.ts
+++ b/server/tests/content-security.spec.ts
@@ -156,4 +156,34 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
]);
});
});
+
+ describe('5. 动态自定义引流违规词库与首页运营全局弹窗组件规范验证', () => {
+ it('支持动态加载自定义违规引流词并即时热拦截', () => {
+ secService.setCustomKeywords(['私下走微信', '线下给现金', '联系方式v']);
+ expect(secService.getCustomKeywords()).toEqual(['私下走微信', '线下给现金', '联系方式v']);
+
+ const blocked1 = secService.checkText('加个好友,我们私下走微信吧');
+ expect(blocked1.pass).toBe(false);
+ expect(blocked1.code).toBe(87014);
+ expect(blocked1.message).toContain('私下走微信');
+
+ const blocked2 = secService.checkText('活动到了现场线下给现金可以吗');
+ expect(blocked2.pass).toBe(false);
+ expect(blocked2.code).toBe(87014);
+ expect(blocked2.message).toContain('线下给现金');
+
+ const passText = secService.checkText('今晚一起去望京合生麒麟社玩桌游');
+ expect(passText.pass).toBe(true);
+ });
+
+ it('首页必须挂载 HomePopupModal 全局运营浮层弹窗组件', () => {
+ const homePopupPath = path.join(rootDir, 'pwuniapp', 'components', 'HomePopupModal.vue');
+ expect(fs.existsSync(homePopupPath)).toBe(true);
+
+ const indexContent = fs.readFileSync(path.join(rootDir, 'pwuniapp', 'pages', 'index', 'index.vue'), 'utf-8');
+ expect(indexContent).toContain('home-popup-modal');
+ expect(indexContent).toContain('HomePopupModal');
+ });
+ });
});
+