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> {
|
||||
|
||||
在新工单中引用
屏蔽一个用户