feat: 新增数据看板与用户管理模块
- 数据看板:注册用户 / 图章收集 / 兑换次数 三张卡片,展示总数及本日 / 本周 / 本月新增 - 时间边界按 Asia/Shanghai 计算,周一为一周起点 - 用户管理:只读列表展示用户名、手机号、已收集、已兑换及注册时间,支持搜索 - 登录后默认跳转到数据看板,侧边栏重新编号为 7 项 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -195,6 +195,71 @@ router.get("/stats", async (_req, res) => {
|
||||
res.json({ success: true, data: { userCount, collectionCount, redemptionCount } });
|
||||
});
|
||||
|
||||
// Asia/Shanghai 边界(UTC+8,无夏令时)
|
||||
function shanghaiBoundaries() {
|
||||
const OFFSET_MS = 8 * 60 * 60 * 1000;
|
||||
const nowShanghai = new Date(Date.now() + OFFSET_MS);
|
||||
const y = nowShanghai.getUTCFullYear();
|
||||
const m = nowShanghai.getUTCMonth();
|
||||
const d = nowShanghai.getUTCDate();
|
||||
const weekdaySun0 = nowShanghai.getUTCDay();
|
||||
const mondayOffset = (weekdaySun0 + 6) % 7;
|
||||
const startOfToday = new Date(Date.UTC(y, m, d) - OFFSET_MS);
|
||||
const startOfWeek = new Date(startOfToday.getTime() - mondayOffset * 86_400_000);
|
||||
const startOfMonth = new Date(Date.UTC(y, m, 1) - OFFSET_MS);
|
||||
return { startOfToday, startOfWeek, startOfMonth };
|
||||
}
|
||||
|
||||
router.get("/dashboard", async (_req, res) => {
|
||||
const { startOfToday, startOfWeek, startOfMonth } = shanghaiBoundaries();
|
||||
|
||||
const [
|
||||
uTotal, uDay, uWeek, uMonth,
|
||||
cTotal, cDay, cWeek, cMonth,
|
||||
rTotal, rDay, rWeek, rMonth,
|
||||
] = await Promise.all([
|
||||
prisma.user.count(),
|
||||
prisma.user.count({ where: { createdAt: { gte: startOfToday } } }),
|
||||
prisma.user.count({ where: { createdAt: { gte: startOfWeek } } }),
|
||||
prisma.user.count({ where: { createdAt: { gte: startOfMonth } } }),
|
||||
prisma.collection.count(),
|
||||
prisma.collection.count({ where: { collectedAt: { gte: startOfToday } } }),
|
||||
prisma.collection.count({ where: { collectedAt: { gte: startOfWeek } } }),
|
||||
prisma.collection.count({ where: { collectedAt: { gte: startOfMonth } } }),
|
||||
prisma.redemption.count(),
|
||||
prisma.redemption.count({ where: { redeemedAt: { gte: startOfToday } } }),
|
||||
prisma.redemption.count({ where: { redeemedAt: { gte: startOfWeek } } }),
|
||||
prisma.redemption.count({ where: { redeemedAt: { gte: startOfMonth } } }),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
users: { total: uTotal, today: uDay, thisWeek: uWeek, thisMonth: uMonth },
|
||||
collections: { total: cTotal, today: cDay, thisWeek: cWeek, thisMonth: cMonth },
|
||||
redemptions: { total: rTotal, today: rDay, thisWeek: rWeek, thisMonth: rMonth },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/users", async (_req, res) => {
|
||||
const users = await prisma.user.findMany({
|
||||
orderBy: { createdAt: "desc" },
|
||||
include: {
|
||||
_count: { select: { collections: true, redemptions: true } },
|
||||
},
|
||||
});
|
||||
const data = users.map((u) => ({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
phone: u.phone,
|
||||
createdAt: u.createdAt,
|
||||
collectionCount: u._count.collections,
|
||||
redemptionCount: u._count.redemptions,
|
||||
}));
|
||||
res.json({ success: true, data });
|
||||
});
|
||||
|
||||
// ===== Articles CRUD =====
|
||||
|
||||
router.get("/articles", async (_req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user