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 @@ + + + +
+ + 在此维护平台自定义敏感词与私下交易拦截词库(如联系方式变体、私下转账、违规引流等)。管理员保存后系统后端实时热加载生效,小程序端发帖、评论及订单备注将实时阻断拦截。 + + + + + +
+ + + {{ word }} + + +
+ + + + + + + + + 解析并导入违规词库 + + + +
+ + 保存敏感词库(实时热生效) + +
+
+
+
+ + + +
+ + 配置小程序首页首次或每日打开时弹出的营销/活动全局浮层弹窗(如新人首单特惠礼包、节日专属组局返现、官方安全温馨提示等)。 + + + + + + + + + + + + + + 每天首次打开弹出一次(推荐) + 每个用户仅弹出一次 + 每次冷启动打开均弹出 + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+ +
+
+ + + + + + + + + + + + + + +
+ + 保存首页弹窗配置 + +
+
+
+
+
@@ -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'); + }); + }); }); +