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();
});

查看文件

@@ -0,0 +1,243 @@
<template>
<view v-if="visible" class="popup-mask" @touchmove.stop.prevent>
<view class="popup-card" :class="{ 'theme-warm': currentThemeKey === 'warm' }">
<!-- 弹窗顶图海报 (可选) -->
<view class="popup-image-wrap" v-if="config.imageUrl">
<image class="popup-img" :src="resolvedImageUrl" mode="aspectFill" />
<view class="popup-image-overlay"></view>
</view>
<!-- 弹窗文字与操作主体 -->
<view class="popup-content">
<view class="popup-badge">官方活动福利</view>
<view class="popup-title">{{ config.title || '新人专享同城好礼' }}</view>
<view class="popup-sub">{{ config.subtitle || '多重优惠券与高颜值陪玩搭子已就绪' }}</view>
<view class="popup-actions">
<button class="btn-primary-action btn-active" @click="handleAction">
{{ config.btnText || '立即领券组局' }}
</button>
</view>
</view>
<!-- 悬浮关闭圆圈按钮 -->
<view class="popup-close-btn btn-active" @click="handleClose">
<image class="close-icon" :src="ICONS.close" mode="aspectFit" />
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey } from '@/common/theme.js';
import { getPlatformConfigs } from '@/common/api.js';
const visible = ref(false);
const config = ref({
enabled: false,
title: '',
subtitle: '',
imageUrl: '',
link: '/pages/activity/list',
btnText: '立即参与',
frequency: 'once_daily'
});
const currentThemeKey = ref(getCurrentThemeKey());
const resolvedImageUrl = computed(() => {
const url = config.value.imageUrl;
if (!url) return '';
if (url.startsWith('/uploads/')) {
return `https://peiww.gynhc.com${url}`;
}
return url;
});
const checkAndDisplayPopup = async () => {
try {
const configs = await getPlatformConfigs();
if (!configs || !configs.home_popup) return;
const pConfig = configs.home_popup;
if (!pConfig.enabled) return;
config.value = { ...config.value, ...pConfig };
// 根据频次判断是否展示
const storageKey = 'peiwan_home_popup_last_shown';
const lastShown = uni.getStorageSync(storageKey);
const now = Date.now();
if (pConfig.frequency === 'once_ever') {
if (lastShown) return;
} else if (pConfig.frequency === 'once_daily') {
if (lastShown) {
const lastDate = new Date(Number(lastShown)).toDateString();
const nowDate = new Date(now).toDateString();
if (lastDate === nowDate) return;
}
}
// 延迟 600ms 展示,保证首屏骨架屏转场顺畅
setTimeout(() => {
visible.value = true;
uni.setStorageSync(storageKey, String(now));
}, 600);
} catch (err) {
console.warn('[Popup] 读取弹窗配置异常:', err);
}
};
const handleClose = () => {
visible.value = false;
};
const handleAction = () => {
visible.value = false;
const link = config.value.link || '/pages/activity/list';
if (link.startsWith('/pages/activity/list') || link.startsWith('/pages/order/list') || link.startsWith('/pages/user/index')) {
uni.switchTab({ url: link });
} else {
uni.navigateTo({ url: link });
}
};
onMounted(() => {
checkAndDisplayPopup();
});
</script>
<style scoped>
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(15, 23, 42, 0.7);
backdrop-filter: blur(4px);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 0 40rpx;
animation: fadeIn 0.3s ease-out;
}
.popup-card {
position: relative;
width: 100%;
max-width: 600rpx;
background: #FFFFFF;
border-radius: 36rpx;
overflow: visible;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
animation: scaleUp 0.35s cubic-bezier(0.16, 1, 0.3, 1);
}
.popup-image-wrap {
position: relative;
width: 100%;
height: 320rpx;
border-radius: 36rpx 36rpx 0 0;
overflow: hidden;
}
.popup-img {
width: 100%;
height: 100%;
}
.popup-image-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 80rpx;
background: linear-gradient(to top, #FFFFFF, transparent);
}
.popup-content {
padding: 36rpx 40rpx 44rpx;
text-align: center;
}
.popup-badge {
display: inline-block;
padding: 6rpx 20rpx;
background: rgba(99, 102, 241, 0.1);
color: #6366F1;
font-size: 22rpx;
font-weight: 600;
border-radius: 20rpx;
margin-bottom: 16rpx;
}
.popup-title {
font-size: 38rpx;
font-weight: 800;
color: #0F172A;
margin-bottom: 12rpx;
line-height: 1.3;
}
.popup-sub {
font-size: 26rpx;
color: #64748B;
line-height: 1.5;
margin-bottom: 36rpx;
}
.btn-primary-action {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
color: #FFFFFF;
font-size: 30rpx;
font-weight: 700;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 10rpx 24rpx rgba(99, 102, 241, 0.35);
border: none;
}
.popup-close-btn {
position: absolute;
bottom: -96rpx;
left: 50%;
transform: translateX(-50%);
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.25);
border: 2rpx solid rgba(255, 255, 255, 0.5);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
}
.close-icon {
width: 32rpx;
height: 32rpx;
}
.btn-active:active {
transform: scale(0.96);
opacity: 0.9;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scaleUp {
from { opacity: 0; transform: scale(0.85); }
to { opacity: 1; transform: scale(1); }
}
</style>

查看文件

@@ -120,6 +120,9 @@
<!-- 微信小程序用户隐私保护指引弹窗 (微信合规) -->
<privacy-modal />
<!-- 首页运营全局活动营销弹窗 (配置化可控) -->
<home-popup-modal />
</view>
</template>
@@ -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';

查看文件

@@ -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);
}

查看文件

@@ -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');
});
});
});