feat(admin): 方案A—消息铃铛接真实待办聚合 + 清理空壳接口与空卡片
【消息铃铛(真实数据)】 - POST /api/message/list 改为待办聚合:待接单订单/待审核大神/待审核提现/待处理举报/待仲裁工单(复用 getDashboardTodos + 新增 pendingOrdersCount) - POST /api/message/read 持久化已读:新表 admin_message_reads 记录"已读时的数量",待办数量增加会重新变未读 - 两个接口均加 requireAdmin(待办数量属运营数据,不再匿名可见) 【删除空壳/死接口(前端已无调用方)】 - /api/user/save-info、/api/chat/list、/api/user/my-team/list、/api/user/latest-activity、/api/user/my-project/list 【后台页面清理】 - 「用户与个人中心 → 管理员资料」移除 我的项目/最近动态/我的团队/站内通知 四张空卡(保留真实的管理员资料头部) - 删除对应 4 个组件;api/user-center.ts 移除 queryMyProjectList/queryMyTeamList/queryLatestActivity/saveUserInfo;api/message.ts 移除 queryChatList 【验证】 - 新增 admin-todo-bell.spec.ts;更新 admin-demo-data-cleanup.spec.ts - 全量 101 套件 / 690 用例全绿;冒烟 8/8(未登录50008、待办数量、已读、新增后重新未读) - 后台 pnpm build 通过
这个提交包含在:
@@ -661,6 +661,15 @@ export async function initDatabase(): Promise<void> {
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`);
|
||||
|
||||
// 管理端消息铃铛:待办已读状态(记录已读时数量,新增待办会重新变未读)
|
||||
await conn.query(`
|
||||
CREATE TABLE IF NOT EXISTS admin_message_reads (
|
||||
todo_key VARCHAR(64) PRIMARY KEY,
|
||||
read_count INT NOT NULL DEFAULT 0,
|
||||
read_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`);
|
||||
|
||||
// 用户已领取的优惠券(券中心领取 → 下单核销 → 取消/退款回退)
|
||||
await conn.query(`
|
||||
CREATE TABLE IF NOT EXISTS user_coupons (
|
||||
|
||||
@@ -3729,6 +3729,13 @@ export class MysqlRepository {
|
||||
} catch (e) {
|
||||
logServerError('getDashboardTodos.reports', e);
|
||||
}
|
||||
let pendingOrdersCount = 0;
|
||||
try {
|
||||
const [oRows]: any = await pool.query('SELECT COUNT(*) as c FROM orders WHERE status = "待接单"');
|
||||
pendingOrdersCount = Number((oRows && oRows[0] && oRows[0].c) || 0);
|
||||
} catch (e) {
|
||||
logServerError('getDashboardTodos.orders', e);
|
||||
}
|
||||
|
||||
const pendingWithdrawalAmount = wdRows.reduce((sum: number, w: any) => sum + Number(w.amount), 0);
|
||||
|
||||
@@ -3737,6 +3744,7 @@ export class MysqlRepository {
|
||||
pendingArbitrationsCount: arbRows.length,
|
||||
pendingWithdrawalsCount: wdRows.length,
|
||||
pendingWithdrawalAmount,
|
||||
pendingOrdersCount,
|
||||
pendingReportsCount: rptRows.length,
|
||||
riskInterceptionsCount: secRows.length,
|
||||
pendingAudits: auditRows.map((c: any) => ({
|
||||
@@ -3768,6 +3776,38 @@ export class MysqlRepository {
|
||||
};
|
||||
}
|
||||
|
||||
/** 管理员待办"已读"状态:记录每个待办项已读时的数量,用于判断是否有新增待办 */
|
||||
async getAdminTodoReadCounts(): Promise<Record<string, number>> {
|
||||
try {
|
||||
const [rows]: any = await pool.query('SELECT todo_key, read_count FROM admin_message_reads');
|
||||
const map: Record<string, number> = {};
|
||||
for (const r of rows || []) map[String(r.todo_key)] = Number(r.read_count) || 0;
|
||||
return map;
|
||||
} catch (e) {
|
||||
logServerError('getAdminTodoReadCounts', e);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async markAdminTodosRead(items: Array<{ key: string; count: number }>): Promise<number> {
|
||||
let affected = 0;
|
||||
for (const it of items || []) {
|
||||
if (!it || !it.key) continue;
|
||||
try {
|
||||
await pool.query(
|
||||
`INSERT INTO admin_message_reads (todo_key, read_count, read_at)
|
||||
VALUES (?, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE read_count = VALUES(read_count), read_at = NOW()`,
|
||||
[String(it.key), Number(it.count) || 0]
|
||||
);
|
||||
affected += 1;
|
||||
} catch (e) {
|
||||
logServerError('markAdminTodosRead', e);
|
||||
}
|
||||
}
|
||||
return affected;
|
||||
}
|
||||
|
||||
// 全平台用户管理相关
|
||||
async getUsers(filter?: { keyword?: string; role?: string; status?: string }): Promise<any[]> {
|
||||
let sql = `
|
||||
|
||||
@@ -586,34 +586,6 @@ app.all('/api/user/certification', (_req: Request, res: Response) => {
|
||||
});
|
||||
});
|
||||
|
||||
// 平台未接入多项目管理:返回空数组,不再返回伪造项目与成员
|
||||
app.all('/api/user/my-project/list', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
code: 20000,
|
||||
data: []
|
||||
});
|
||||
});
|
||||
|
||||
// 后台个人中心:我的团队(真实团队表尚未建立,返回空数组,不再返回静态演示成员)
|
||||
app.all('/api/user/my-team/list', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
code: 20000,
|
||||
data: []
|
||||
});
|
||||
});
|
||||
|
||||
// 后台个人中心:最新动态(暂无审计动态数据源,返回空数组,不再返回静态演示内容)
|
||||
app.all('/api/user/latest-activity', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
code: 20000,
|
||||
data: []
|
||||
});
|
||||
});
|
||||
|
||||
app.all('/api/user/save-info', (_req: Request, res: Response) => {
|
||||
res.json({ code: 20000, data: null, message: '个人设置保存成功' });
|
||||
});
|
||||
|
||||
// 真实文件与图片对象存储上传接口 (支持单图上传并持久化至 public/uploads)
|
||||
const handleFileUpload = (req: Request, res: Response) => {
|
||||
// 生产环境要求登录后上传,避免匿名上传被滥用(刷盘/托管非法内容)
|
||||
@@ -734,20 +706,59 @@ app.get('/api/message/subscribe-templates', async (_req: Request, res: Response)
|
||||
}
|
||||
});
|
||||
|
||||
// 通知中心列表(暂无真实站内消息数据源,返回空数组,不再返回静态演示消息)
|
||||
app.all('/api/message/list', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
code: 20000,
|
||||
data: []
|
||||
});
|
||||
// 管理端消息铃铛:真实待办聚合(待接单 / 待审核大神 / 待审核提现 / 待处理举报 / 待仲裁)
|
||||
function buildAdminTodoItems(todos: any, readMap: Record<string, number>): any[] {
|
||||
const defs = [
|
||||
{ key: 'pending_orders', count: Number(todos?.pendingOrdersCount || 0), title: '待接单订单', content: '有订单等待大神接单', link: '/order/list' },
|
||||
{ key: 'pending_audits', count: Number(todos?.pendingAuditsCount || 0), title: '待审核大神', content: '有大神入驻申请待审核', link: '/companion/audit' },
|
||||
{ key: 'pending_withdrawals', count: Number(todos?.pendingWithdrawalsCount || 0), title: '待审核提现', content: `待审核提现合计 ¥${Number(todos?.pendingWithdrawalAmount || 0).toFixed(2)}`, link: '/finance/withdraw' },
|
||||
{ key: 'pending_reports', count: Number(todos?.pendingReportsCount || 0), title: '待处理举报', content: '有违规举报工单待处置', link: '/safety/reports' },
|
||||
{ key: 'pending_arbitrations', count: Number(todos?.pendingArbitrationsCount || 0), title: '待仲裁工单', content: '有争议仲裁待平台介入', link: '/order/arbitration' },
|
||||
];
|
||||
return defs
|
||||
.filter((d) => d.count > 0)
|
||||
.map((d, i) => {
|
||||
const readCount = Number(readMap[d.key] || 0);
|
||||
return {
|
||||
id: i + 1,
|
||||
key: d.key,
|
||||
type: 'todo',
|
||||
title: d.title,
|
||||
subTitle: `待处理 ${d.count} 条`,
|
||||
content: d.content,
|
||||
time: formatCnTime(),
|
||||
status: d.count > readCount ? 0 : 1,
|
||||
messageType: 1,
|
||||
count: d.count,
|
||||
link: d.link,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
app.post('/api/message/list', requireAdmin, async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const todos = await repo.getDashboardTodos();
|
||||
const readMap = await repo.getAdminTodoReadCounts();
|
||||
res.json({ code: 20000, data: buildAdminTodoItems(todos, readMap) });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.all('/api/message/read', (_req: Request, res: Response) => {
|
||||
res.json({ code: 20000, data: true });
|
||||
});
|
||||
|
||||
app.all('/api/chat/list', (_req: Request, res: Response) => {
|
||||
res.json({ code: 20000, data: [] });
|
||||
app.post('/api/message/read', requireAdmin, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const ids = Array.isArray(req.body?.ids) ? req.body.ids.map((x: any) => Number(x)) : [];
|
||||
const keys = Array.isArray(req.body?.keys) ? req.body.keys.map((x: any) => String(x)) : [];
|
||||
const todos = await repo.getDashboardTodos();
|
||||
const readMap = await repo.getAdminTodoReadCounts();
|
||||
const items = buildAdminTodoItems(todos, readMap);
|
||||
const targets = items.filter((it: any) => ids.includes(it.id) || keys.includes(it.key));
|
||||
const toMark = (targets.length ? targets : items).map((it: any) => ({ key: it.key, count: it.count }));
|
||||
const affected = await repo.markAdminTodosRead(toMark);
|
||||
res.json({ code: 20000, data: { affected }, message: '已标记为已读' });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ===== 站内消息通知中心(用户端真实数据源)=====
|
||||
|
||||
在新工单中引用
屏蔽一个用户