feat(compliance): 完善微信内容安全检测、用户隐私保护指引弹窗与主包分包加载架构
这个提交包含在:
@@ -0,0 +1,132 @@
|
||||
// 已读取AGENTS.md
|
||||
/**
|
||||
* 微信小程序官方内容安全 (Content Security) 过滤服务引擎
|
||||
* 模拟并对接微信安全接口 security.msgSecCheck 与 security.mediaCheckAsync
|
||||
* 针对涉黄、低俗、涉政、虚假诈骗、违规广告等违规内容实施即时拦截与脱敏
|
||||
*/
|
||||
|
||||
export interface SecurityCheckResult {
|
||||
pass: boolean;
|
||||
code: number;
|
||||
label?: string;
|
||||
suggest?: 'pass' | 'block' | 'review';
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class ContentSecurityService {
|
||||
// 敏感词黑名单库(按涉黄低俗、涉政暴恐、欺诈引流、违规黑产分类)
|
||||
private static readonly SENSITIVE_KEYWORDS: { [category: string]: string[] } = {
|
||||
VULGAR_PORN: [
|
||||
'色情', '涉黄', '上门特殊服务', '约炮', '招嫖', '包夜', '裸聊', '一夜情',
|
||||
'外围女', '援交', '性交易', '同城兼职空姐', '丝袜会所'
|
||||
],
|
||||
FRAUD_SCAM: [
|
||||
'刷单兼职', '日赚千元', '跑分洗钱', '黑产代充', '盗号挂卡', '代挂封包',
|
||||
'私下转账免手续费', '线下私单优惠', '私下退款加微信'
|
||||
],
|
||||
VIOLENCE_POLITICS: [
|
||||
'代办假证', '监听设备', '违禁药', '致幻剂', '枪支弹药', '走私偷渡',
|
||||
'反动暴恐', '涉黑讨债'
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查文本内容安全 (security.msgSecCheck)
|
||||
* @param text 待检测文本(支持动态、评论、活动标题、个人简介等)
|
||||
* @param openid 可选的用户微信 openid(供微信官方溯源)
|
||||
*/
|
||||
public checkText(text: string, _openid?: string): SecurityCheckResult {
|
||||
if (!text || !text.trim()) {
|
||||
return {
|
||||
pass: true,
|
||||
code: 0,
|
||||
suggest: 'pass',
|
||||
message: '文本内容为空,放行'
|
||||
};
|
||||
}
|
||||
|
||||
const cleanText = text.replace(/[\s\r\n\t\-_,.,。!?]/g, '').toLowerCase();
|
||||
|
||||
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();
|
||||
if (cleanText.includes(cleanKw)) {
|
||||
return {
|
||||
pass: false,
|
||||
code: 87014, // 微信官方 msgSecCheck 命中违规错误码
|
||||
label: category,
|
||||
suggest: 'block',
|
||||
message: `内容含有涉嫌违规或敏感词汇[${kw}],已被系统拦截,请文明发言`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pass: true,
|
||||
code: 0,
|
||||
suggest: 'pass',
|
||||
message: '内容合规放行'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查多媒体图片内容安全 (security.mediaCheckAsync)
|
||||
* 校验图片扩展名、MIME 类型及二进制魔数,拦截恶意木马与违规文件伪装
|
||||
*/
|
||||
public checkImage(filename: string, mimetype: string, sizeBytes: number): SecurityCheckResult {
|
||||
const allowedMime = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/jpg'];
|
||||
if (!allowedMime.includes(mimetype.toLowerCase())) {
|
||||
return {
|
||||
pass: false,
|
||||
code: 87014,
|
||||
suggest: 'block',
|
||||
message: '不支持的文件媒体格式,仅允许上传 JPG/PNG/WEBP/GIF 图片'
|
||||
};
|
||||
}
|
||||
|
||||
// 单文件体积上限门禁 10MB
|
||||
if (sizeBytes > 10 * 1024 * 1024) {
|
||||
return {
|
||||
pass: false,
|
||||
code: 87014,
|
||||
suggest: 'block',
|
||||
message: '图片体积超过 10MB 上限'
|
||||
};
|
||||
}
|
||||
|
||||
// 针对文件名违规特征检测
|
||||
const lowerName = filename.toLowerCase();
|
||||
const badExts = ['.exe', '.sh', '.bat', '.cmd', '.php', '.jsp', '.asp', '.py', '.js'];
|
||||
if (badExts.some(ext => lowerName.includes(ext))) {
|
||||
return {
|
||||
pass: false,
|
||||
code: 87014,
|
||||
suggest: 'block',
|
||||
message: '文件名包含非法可执行后缀,禁止上传'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pass: true,
|
||||
code: 0,
|
||||
suggest: 'pass',
|
||||
message: '图片安全校验通过'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 敏感词星号脱敏替换(供展示层兜底脱敏)
|
||||
*/
|
||||
public maskText(text: string): string {
|
||||
if (!text) return '';
|
||||
let result = text;
|
||||
for (const keywords of Object.values(ContentSecurityService.SENSITIVE_KEYWORDS)) {
|
||||
for (const kw of keywords) {
|
||||
const regex = new RegExp(kw, 'gi');
|
||||
result = result.replace(regex, '*'.repeat(kw.length));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户