feat(config): implement dynamic security blocklist and home marketing popup with admin controls
这个提交包含在:
@@ -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();
|
||||
|
||||
@@ -686,6 +686,21 @@ async function seedInitialDataIfEmpty(conn: any): Promise<void> {
|
||||
}
|
||||
]);
|
||||
|
||||
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<void> {
|
||||
('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<void> {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1279,6 +1279,23 @@ export class MysqlRepository {
|
||||
return result;
|
||||
}
|
||||
|
||||
async getPlatformConfigByKey(configKey: string): Promise<any> {
|
||||
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<any[]> {
|
||||
const [rows]: any = await pool.query(`
|
||||
SELECT id, config_key as configKey, name, value_json as valueJson, description, status,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户