feat(notify): 新增站内消息通知中心(订阅消息兜底)+ 清理后台伪造数据
- 新表 user_notifications + 仓储方法(创建/列表/未读数/标记已读) - 用户接口 GET /api/notifications、POST /api/notifications/read(生产需登录) - 关键节点落库:订单开始服务/接单/拒单/申请完成/完成结算/提现到账/报名成功/退款受理 - 小程序新增「消息通知」页(pages/user/notifications) + 个人中心入口与未读角标;api.js 增加 getNotifications/markNotificationsRead - 清理后台伪造数据:/api/user/certification 改空态、/api/user/my-project/list 改空数组、/api/user/info 去掉编造邮箱/电话/网址 - 更新 pages 数量断言(26→27);新增回归护栏 notification-center.spec.ts(92 套件/658 用例全绿);冒烟 9/9
这个提交包含在:
@@ -645,6 +645,21 @@ export async function initDatabase(): Promise<void> {
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`);
|
||||
|
||||
// 站内消息通知中心:订单/提现/报名/退款/仲裁等关键动作落库,供小程序「消息中心」查看
|
||||
await conn.query(`
|
||||
CREATE TABLE IF NOT EXISTS user_notifications (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
type VARCHAR(32) NOT NULL DEFAULT 'SYSTEM',
|
||||
title VARCHAR(128) NOT NULL,
|
||||
content VARCHAR(512) NOT NULL DEFAULT '',
|
||||
link VARCHAR(255) NOT NULL DEFAULT '',
|
||||
is_read TINYINT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_notif_user_read (user_id, is_read, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`);
|
||||
|
||||
// 慢查询优化:为高频查询列补充索引(幂等:已存在则忽略;表不存在也忽略)
|
||||
// 注意:必须在所有建表语句之后执行,确保目标表已存在。
|
||||
const indexDefs: Array<[string, string, string]> = [
|
||||
|
||||
@@ -3125,6 +3125,76 @@ export class MysqlRepository {
|
||||
return rows;
|
||||
}
|
||||
|
||||
// ===== 站内消息通知中心 =====
|
||||
async createUserNotification(data: {
|
||||
userId: number;
|
||||
type?: string;
|
||||
title: string;
|
||||
content?: string;
|
||||
link?: string;
|
||||
}): Promise<any> {
|
||||
const uid = Number(data.userId) || 0;
|
||||
if (uid <= 0 || !data.title) return null;
|
||||
const id = `ntf_${Date.now()}_${Math.floor(100 + Math.random() * 900)}`;
|
||||
await pool.query(
|
||||
`INSERT INTO user_notifications (id, user_id, type, title, content, link, is_read, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 0, NOW())`,
|
||||
[
|
||||
id,
|
||||
uid,
|
||||
String(data.type || 'SYSTEM').slice(0, 32),
|
||||
String(data.title).slice(0, 120),
|
||||
String(data.content || '').slice(0, 500),
|
||||
String(data.link || '').slice(0, 250),
|
||||
]
|
||||
);
|
||||
return { id };
|
||||
}
|
||||
|
||||
async getUserNotifications(userId: number, limit = 50): Promise<any[]> {
|
||||
const uid = Number(userId) || 0;
|
||||
if (uid <= 0) return [];
|
||||
const take = Math.min(200, Math.max(1, Number(limit) || 50));
|
||||
const [rows]: any = await pool.query(
|
||||
`SELECT id, type, title, content, link, is_read as isRead, created_at as createdAt
|
||||
FROM user_notifications
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT ${take}`,
|
||||
[uid]
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async getUserUnreadNotificationCount(userId: number): Promise<number> {
|
||||
const uid = Number(userId) || 0;
|
||||
if (uid <= 0) return 0;
|
||||
const [rows]: any = await pool.query(
|
||||
'SELECT COUNT(*) as c FROM user_notifications WHERE user_id = ? AND is_read = 0',
|
||||
[uid]
|
||||
);
|
||||
return Number((rows && rows[0] && rows[0].c) || 0);
|
||||
}
|
||||
|
||||
async markUserNotificationsRead(userId: number, ids?: string[]): Promise<number> {
|
||||
const uid = Number(userId) || 0;
|
||||
if (uid <= 0) return 0;
|
||||
if (Array.isArray(ids) && ids.length > 0) {
|
||||
const safe = ids.map((x) => String(x)).slice(0, 200);
|
||||
const ph = safe.map(() => '?').join(',');
|
||||
const [r]: any = await pool.query(
|
||||
`UPDATE user_notifications SET is_read = 1 WHERE user_id = ? AND id IN (${ph})`,
|
||||
[uid, ...safe]
|
||||
);
|
||||
return Number((r && r.affectedRows) || 0);
|
||||
}
|
||||
const [r]: any = await pool.query(
|
||||
'UPDATE user_notifications SET is_read = 1 WHERE user_id = ? AND is_read = 0',
|
||||
[uid]
|
||||
);
|
||||
return Number((r && r.affectedRows) || 0);
|
||||
}
|
||||
|
||||
async getWithdrawals(): Promise<any[]> {
|
||||
const [rows]: any = await pool.query(`
|
||||
SELECT
|
||||
@@ -3133,8 +3203,7 @@ export class MysqlRepository {
|
||||
status, apply_time as applyTime
|
||||
FROM withdrawals
|
||||
ORDER BY apply_time DESC
|
||||
`);
|
||||
return rows;
|
||||
`); return rows;
|
||||
}
|
||||
|
||||
async reviewWithdrawal(id: string, action: 'approve' | 'reject', rejectReason?: string): Promise<any> {
|
||||
|
||||
@@ -512,7 +512,7 @@ app.all('/api/user/info', (_req: Request, res: Response) => {
|
||||
data: {
|
||||
name: '超级管理员',
|
||||
avatar: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="50" fill="%23165DFF"/><text x="50" y="62" font-size="36" text-anchor="middle" fill="white" font-family="sans-serif">AD</text></svg>',
|
||||
email: 'admin@peiwan.com',
|
||||
email: '',
|
||||
job: 'super_admin',
|
||||
jobName: '全权运营总监',
|
||||
organization: 'Platform',
|
||||
@@ -520,8 +520,8 @@ app.all('/api/user/info', (_req: Request, res: Response) => {
|
||||
location: 'beijing',
|
||||
locationName: '北京',
|
||||
introduction: '具备平台最高运营、风控与财务审核权限',
|
||||
personalWebsite: 'https://peiww.gynhc.com',
|
||||
phone: '188****8888',
|
||||
personalWebsite: '',
|
||||
phone: '',
|
||||
registrationDate: '2026-09-01 10:00:00',
|
||||
accountId: '10001',
|
||||
certification: 1,
|
||||
@@ -547,56 +547,32 @@ app.all('/api/user/menu', (_req: Request, res: Response) => {
|
||||
});
|
||||
|
||||
// 1.2 个人中心与管理员辅助接口 (用户设置与用户信息页)
|
||||
// 平台未接入企业实名认证:返回"未认证"空态,不再返回伪造的企业信息
|
||||
app.all('/api/user/certification', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
code: 20000,
|
||||
data: {
|
||||
enterpriseInfo: {
|
||||
accountType: 1,
|
||||
status: 1,
|
||||
time: '2026-09-01 10:00:00',
|
||||
legalPerson: '平台管理员',
|
||||
certificateType: '统一社会信用代码',
|
||||
authenticationNumber: '91110108MA00000000',
|
||||
enterpriseName: '同城生活陪伴运营服务中心',
|
||||
enterpriseCertificateType: '营业执照',
|
||||
organizationCode: 'MA000000-0',
|
||||
status: 0,
|
||||
time: '',
|
||||
legalPerson: '',
|
||||
certificateType: '',
|
||||
authenticationNumber: '',
|
||||
enterpriseName: '',
|
||||
enterpriseCertificateType: '',
|
||||
organizationCode: '',
|
||||
},
|
||||
record: [
|
||||
{
|
||||
certificationType: 1,
|
||||
certificationContent: '企业实名认证及运营资质核准',
|
||||
status: 1,
|
||||
time: '2026-09-01 10:00:00',
|
||||
}
|
||||
]
|
||||
record: []
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 平台未接入多项目管理:返回空数组,不再返回伪造项目与成员
|
||||
app.all('/api/user/my-project/list', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
code: 20000,
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
name: '同城线下生活陪伴运营大盘',
|
||||
description: '涵盖线下陪吃、陪喝、陪唱与线上游戏陪伴的综合履约与合规监管系统。',
|
||||
peopleNumber: 12,
|
||||
contributors: [
|
||||
{ name: '超级管理员', email: 'admin@peiwan.com', avatar: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="50" fill="%23165DFF"/><text x="50" y="62" font-size="36" text-anchor="middle" fill="white" font-family="sans-serif">AD</text></svg>' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '同城公共商圈合规与风控拦截',
|
||||
description: '高频违规场所自动识别与阻断引擎,保障线下组局公开与人身安全。',
|
||||
peopleNumber: 6,
|
||||
contributors: [
|
||||
{ name: '风控专员', email: 'sec@peiwan.com', avatar: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="50" fill="%2300B42A"/><text x="50" y="62" font-size="36" text-anchor="middle" fill="white" font-family="sans-serif">FK</text></svg>' }
|
||||
]
|
||||
}
|
||||
]
|
||||
data: []
|
||||
});
|
||||
});
|
||||
|
||||
@@ -756,6 +732,43 @@ app.all('/api/chat/list', (_req: Request, res: Response) => {
|
||||
res.json({ code: 20000, data: [] });
|
||||
});
|
||||
|
||||
// ===== 站内消息通知中心(用户端真实数据源)=====
|
||||
// 关键动作(支付/接单/拒单/完成/提现到账/报名/退款/仲裁)都会写入 user_notifications,
|
||||
// 即使未授权微信订阅消息,用户仍可在小程序「消息中心」看到完整通知历史。
|
||||
app.get('/api/notifications', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const uid = resolveSecureUserId(
|
||||
req,
|
||||
Number(req.query.userId || req.headers['x-user-id'] || 0),
|
||||
'查看消息通知'
|
||||
);
|
||||
const list = await repo.getUserNotifications(uid, Number(req.query.limit) || 50);
|
||||
const unread = await repo.getUserUnreadNotificationCount(uid);
|
||||
res.json({ code: 20000, data: { list, unread } });
|
||||
} catch (err: any) {
|
||||
const statusCode = err.status || 400;
|
||||
const errCode = err.code || 40000;
|
||||
res.status(statusCode).json({ code: errCode, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/notifications/read', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const uid = resolveSecureUserId(
|
||||
req,
|
||||
Number(req.body?.userId || req.headers['x-user-id'] || 0),
|
||||
'标记消息已读'
|
||||
);
|
||||
const ids = Array.isArray(req.body?.ids) ? req.body.ids : undefined;
|
||||
const affected = await repo.markUserNotificationsRead(uid, ids);
|
||||
res.json({ code: 20000, data: { affected }, message: '消息已标记为已读' });
|
||||
} catch (err: any) {
|
||||
const statusCode = err.status || 400;
|
||||
const errCode = err.code || 40000;
|
||||
res.status(statusCode).json({ code: errCode, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 运营后台统计大盘接口 (真实读取 MySQL)
|
||||
app.get('/api/dashboard/stats', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
@@ -879,6 +892,7 @@ app.post('/api/activities/:id/join', async (req: Request, res: Response) => {
|
||||
});
|
||||
try {
|
||||
const act = await repo.getActivityById(actId);
|
||||
pushUserNotification(uid, 'ACTIVITY', '报名成功', `你已成功报名「${String(act?.title || '同城组局活动')}」,开始时间 ${toCnTimeOrNow(act?.startTime)}。`, '/pages/activity/my-ticket');
|
||||
void notifyBySubscribe('activityJoin', {
|
||||
userId: uid,
|
||||
page: '/pages/activity/my-ticket',
|
||||
@@ -1338,6 +1352,7 @@ app.post('/api/orders/:id/claim', async (req: Request, res: Response) => {
|
||||
await assertUserNotBanned(operatorId, '接单');
|
||||
}
|
||||
const order = await repo.claimOrder(orderId, { ...(req.body || {}), expertId: operatorId });
|
||||
pushUserNotification(Number(order?.buyer_id || order?.buyerId || 0), 'ORDER_STATUS', '大神已接单', `订单 ${order?.order_no || orderId} 已被大神接单,服务即将开始。`, '/pages/order/list');
|
||||
res.json({ code: 20000, data: order, message: '抢单成功,服务已锁定为履约中' });
|
||||
} catch (err: any) {
|
||||
const statusCode = err.status || 400;
|
||||
@@ -1984,6 +1999,7 @@ app.post('/api/orders/:id/pay', async (req: Request, res: Response) => {
|
||||
}
|
||||
const order = await repo.updateOrderStatus(orderId, '履约中');
|
||||
const buyer = Number(own.buyerId || 0);
|
||||
pushUserNotification(buyer, 'ORDER_STATUS', '订单已开始服务', `订单 ${order?.order_no || orderId} 已进入履约中${order?.verify_code ? `,核销码 ${order.verify_code}` : ''},请按时到场。`, '/pages/order/list');
|
||||
void notifyBySubscribe('orderStatus', {
|
||||
userId: buyer,
|
||||
page: '/pages/order/list',
|
||||
@@ -2010,6 +2026,7 @@ app.post('/api/orders/:id/pay', async (req: Request, res: Response) => {
|
||||
}
|
||||
const order = await repo.updateOrderStatus(orderId, requested);
|
||||
const buyer = Number(own.buyerId || 0);
|
||||
pushUserNotification(buyer, 'ORDER_STATUS', '订单已开始服务', `订单 ${order?.order_no || orderId} 已进入${requested}${order?.verify_code ? `,核销码 ${order.verify_code}` : ''},请按时到场。`, '/pages/order/list');
|
||||
void notifyBySubscribe('orderStatus', {
|
||||
userId: buyer,
|
||||
page: '/pages/order/list',
|
||||
@@ -2442,6 +2459,15 @@ function toCnTimeOrNow(value?: string): string {
|
||||
return formatCnTime(d);
|
||||
}
|
||||
|
||||
// 站内消息落库(fire-and-forget,绝不影响主业务;与微信订阅消息并行)
|
||||
function pushUserNotification(userId: number, type: string, title: string, content: string, link = ''): void {
|
||||
const uid = Number(userId) || 0;
|
||||
if (uid <= 0) return;
|
||||
void repo
|
||||
.createUserNotification({ userId: uid, type, title, content, link })
|
||||
.catch((e: any) => logServerError('pushUserNotification', e));
|
||||
}
|
||||
|
||||
async function notifyBySubscribe(
|
||||
templateKey: 'orderStatus' | 'activityJoin' | 'withdrawSuccess' | 'verifyCode',
|
||||
opts: { userId: number; page?: string; data: Record<string, string | number> }
|
||||
@@ -2928,6 +2954,7 @@ app.post('/api/pay/wechat/transfer-notify', async (req: Request, res: Response)
|
||||
if (outBillNo && state) {
|
||||
const r: any = await repo.finalizeWithdrawalTransfer(String(outBillNo), String(state));
|
||||
if (r && r.status === '已打款' && r.userId) {
|
||||
pushUserNotification(Number(r.userId), 'WITHDRAW', '提现已到账', `提现金额 ¥${Number(r.amount || 0).toFixed(2)} 已打款到你的微信零钱。`, '/pages/user/withdraw-records');
|
||||
void notifyBySubscribe('withdrawSuccess', {
|
||||
userId: Number(r.userId),
|
||||
page: '/pages/user/withdraw-records',
|
||||
@@ -3005,6 +3032,7 @@ app.post('/api/orders/:id/reject', async (req: Request, res: Response) => {
|
||||
return res.status(403).json({ code: 40300, message: '仅承接大神本人或平台管理员可拒单' });
|
||||
}
|
||||
const order = await repo.rejectOrder(orderId, req.body);
|
||||
pushUserNotification(Number(own.buyerId || 0), 'ORDER_REFUND', '订单已被拒单', `订单 ${orderId} 已被大神拒单,款项将原路退回。`, '/pages/order/list');
|
||||
res.json({ code: 20000, data: order, message: '已拒单,订单款项已自动原路退还买家' });
|
||||
} catch (err: any) {
|
||||
const statusCode = err.status || 400;
|
||||
@@ -3026,6 +3054,7 @@ app.post('/api/orders/:id/apply-complete', async (req: Request, res: Response) =
|
||||
return res.status(403).json({ code: 40300, message: '仅承接大神本人或平台管理员可申请完成服务' });
|
||||
}
|
||||
const order = await repo.applyCompleteOrder(orderId);
|
||||
pushUserNotification(Number(own.buyerId || 0), 'ORDER_STATUS', '大神申请完成服务', `订单 ${orderId} 大神已申请完成服务,请及时确认验收(48 小时未处理将自动确认)。`, '/pages/order/list');
|
||||
res.json({ code: 20000, data: order, message: '已发起完成服务申请,等待买家确认验收(48小时自动确认)' });
|
||||
} catch (err: any) {
|
||||
const statusCode = err.status || 400;
|
||||
@@ -3047,6 +3076,11 @@ app.post('/api/orders/:id/complete', async (req: Request, res: Response) => {
|
||||
return res.status(403).json({ code: 40300, message: '仅交易双方或平台管理员可完成该订单' });
|
||||
}
|
||||
const order = await repo.updateOrderStatus(orderId, '已完成');
|
||||
pushUserNotification(Number(own.buyerId || 0), 'ORDER_STATUS', '订单已完成', `订单 ${orderId} 已完成,感谢使用,欢迎评价。`, '/pages/order/list');
|
||||
const companionIds: number[] = Array.isArray(own?.companionIds) ? own.companionIds : [];
|
||||
companionIds.forEach((cid) =>
|
||||
pushUserNotification(Number(cid) || 0, 'ORDER_STATUS', '订单已完成结算', `订单 ${orderId} 已完成,佣金将按平台规则结算入账。`, '/pages/expert/workspace')
|
||||
);
|
||||
res.json({ code: 20000, data: order, message: '订单已确认完成' });
|
||||
} catch (err: any) {
|
||||
const statusCode = err.status || 400;
|
||||
@@ -3086,6 +3120,7 @@ app.post('/api/orders/:id/refund', async (req: Request, res: Response) => {
|
||||
}
|
||||
}
|
||||
const order = await repo.applyOrderRefund(orderId, { reason, description, evidence });
|
||||
pushUserNotification(Number(own.buyerId || 0), 'ORDER_REFUND', '退款申诉已受理', `订单 ${orderId} 的退款申诉已受理,进入争议仲裁流程。`, '/pages/user/reports');
|
||||
res.json({ code: 20000, data: order, message: '退款申诉已受理,进入争议仲裁流程' });
|
||||
} catch (err: any) {
|
||||
const statusCode = err.status || 400;
|
||||
@@ -3266,6 +3301,7 @@ app.post('/api/finance/withdrawals/:id/review', async (req: Request, res: Respon
|
||||
const billNo = String(bill.out_bill_no || outBillNo);
|
||||
if (state === 'SUCCESS') {
|
||||
const r = await repo.markWithdrawalTransfer(wdId, billNo, '已打款', 'SUCCESS');
|
||||
pushUserNotification(Number(wd.companion_id) || 0, 'WITHDRAW', '提现已到账', `提现金额 ¥${Number(wd.amount || 0).toFixed(2)} 已打款到你的微信零钱。`, '/pages/user/withdraw-records');
|
||||
void notifyBySubscribe('withdrawSuccess', {
|
||||
userId: Number(wd.companion_id) || 0,
|
||||
page: '/pages/user/withdraw-records',
|
||||
|
||||
@@ -130,7 +130,7 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
|
||||
const raw = fs.readFileSync(pagesJsonPath, 'utf-8');
|
||||
const config = JSON.parse(raw);
|
||||
expect(Array.isArray(config.pages)).toBe(true);
|
||||
expect(config.pages.length).toBe(26);
|
||||
expect(config.pages.length).toBe(27);
|
||||
|
||||
for (const pageItem of config.pages) {
|
||||
const vueFilePath = path.join(rootDir, 'pwuniapp', `${pageItem.path}.vue`);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第七十六批:站内消息通知中心(用户端真实数据源)+ 清理后台假数据。
|
||||
*/
|
||||
describe('第七十六批消息通知中心闸门', () => {
|
||||
const root = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(root, 'server/src/index.ts'), 'utf-8');
|
||||
const repo = fs.readFileSync(path.join(root, 'server/src/db/repository.ts'), 'utf-8');
|
||||
const mysql = fs.readFileSync(path.join(root, 'server/src/db/mysql.ts'), 'utf-8');
|
||||
const api = fs.readFileSync(path.join(root, 'pwuniapp/common/api.js'), 'utf-8');
|
||||
const userPage = fs.readFileSync(path.join(root, 'pwuniapp/pages/user/index.vue'), 'utf-8');
|
||||
const pagesJson = fs.readFileSync(path.join(root, 'pwuniapp/pages.json'), 'utf-8');
|
||||
|
||||
it('数据库与仓储具备通知能力', () => {
|
||||
expect(mysql).toContain('CREATE TABLE IF NOT EXISTS user_notifications');
|
||||
expect(repo).toContain('async createUserNotification(');
|
||||
expect(repo).toContain('async getUserNotifications(');
|
||||
expect(repo).toContain('async getUserUnreadNotificationCount(');
|
||||
expect(repo).toContain('async markUserNotificationsRead(');
|
||||
});
|
||||
|
||||
it('提供用户端通知接口', () => {
|
||||
expect(indexCode).toContain("app.get('/api/notifications'");
|
||||
expect(indexCode).toContain("app.post('/api/notifications/read'");
|
||||
expect(indexCode).toContain('function pushUserNotification(');
|
||||
});
|
||||
|
||||
it('关键动作写入站内通知(订单/提现/报名/退款)', () => {
|
||||
expect(indexCode).toContain("pushUserNotification(buyer, 'ORDER_STATUS'");
|
||||
expect(indexCode).toContain("'WITHDRAW', '提现已到账'");
|
||||
expect(indexCode).toContain("'ACTIVITY', '报名成功'");
|
||||
expect(indexCode).toContain("'ORDER_REFUND'");
|
||||
});
|
||||
|
||||
it('小程序具备消息中心页面与入口', () => {
|
||||
expect(pagesJson).toContain('pages/user/notifications');
|
||||
expect(api).toContain('export async function getNotifications(');
|
||||
expect(api).toContain('export async function markNotificationsRead(');
|
||||
expect(userPage).toContain('openNotifications');
|
||||
expect(userPage).toContain('noticeUnread');
|
||||
});
|
||||
|
||||
it('后台伪造数据已清理', () => {
|
||||
expect(indexCode).not.toContain('同城生活陪伴运营服务中心');
|
||||
expect(indexCode).not.toContain('91110108MA00000000');
|
||||
expect(indexCode).not.toContain('admin@peiwan.com');
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户