Files
wzdj/app/api/admin/user-value-summary/route.ts
卡若 ea3a589f0b
Some checks failed
CI / notice (push) Has been cancelled
chore: sync docs, gitea wiki assets, templates
Made-with: Cursor
2026-04-14 18:44:44 +08:00

106 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextRequest, NextResponse } from "next/server"
import { getCollection } from "@/lib/db/mongo"
import { ObjectId } from "mongodb"
/**
* 单用户消费/打赏/观看粗汇总WM-O1-KR3-02 看板 MVP口径后续与 RFM 对齐)
*/
export async function GET(req: NextRequest) {
try {
const userIdStr = new URL(req.url).searchParams.get("userId")?.trim()
if (!userIdStr || !ObjectId.isValid(userIdStr)) {
return NextResponse.json({ success: false, message: "缺少或非法 userId" }, { status: 400 })
}
const uid = new ObjectId(userIdStr)
const giftsColl = await getCollection("liveGifts")
const giftAgg = await giftsColl
.aggregate<{ totalCoins: number; count: number }>([
{ $match: { userId: userIdStr } },
{ $group: { _id: null, totalCoins: { $sum: "$amountCoins" }, count: { $sum: 1 } } },
])
.toArray()
const txColl = await getCollection("transactions")
const txAgg = await txColl
.aggregate<{ _id: string | null; total: number; count: number }>([
{ $match: { userId: userIdStr } },
{ $group: { _id: "$type", total: { $sum: { $toDouble: { $ifNull: ["$amount", 0] } } }, count: { $sum: 1 } } },
])
.toArray()
const matchUser = {
$or: [{ userId: userIdStr }, { userId: uid }],
}
const poColl = await getCollection("productOrders")
const poAgg = await poColl
.aggregate<{ total: number; count: number }>([
{ $match: { ...matchUser, status: { $nin: ["cancelled", "canceled", "refunded"] } } },
{
$group: {
_id: null,
total: {
$sum: {
$ifNull: ["$finalAmount", { $ifNull: ["$totalAmount", 0] }],
},
},
count: { $sum: 1 },
},
},
])
.toArray()
const pcColl = await getCollection("pointCardOrders")
const pcAgg = await pcColl
.aggregate<{ total: number; count: number }>([
{ $match: { ...matchUser, status: { $nin: ["cancelled", "canceled", "refunded"] } } },
{ $group: { _id: null, total: { $sum: { $ifNull: ["$totalAmount", 0] } }, count: { $sum: 1 } } },
])
.toArray()
const journeyColl = await getCollection("userJourneyEvents")
const since = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)
const hbCount = await journeyColl.countDocuments({
userId: uid,
eventName: "live.room.stay.heartbeat",
occurredAt: { $gte: since },
})
const giftRow = giftAgg[0]
const poRow = poAgg[0]
const pcRow = pcAgg[0]
return NextResponse.json({
success: true,
data: {
userId: userIdStr,
windowDays: 90,
liveGifts: {
count: giftRow?.count ?? 0,
totalCoins: giftRow?.totalCoins ?? 0,
},
productOrders: {
count: poRow?.count ?? 0,
totalAmountYuan: poRow?.total ?? 0,
},
pointCardOrders: {
count: pcRow?.count ?? 0,
totalAmountYuan: pcRow?.total ?? 0,
},
transactionsByType: txAgg.map((t) => ({
type: t._id ?? "unknown",
count: t.count,
totalAmount: t.total,
})),
/** 每条心跳约 30s折算为「等效观看分钟」近似值 */
watchMinutesEstimate90d: Math.round(hbCount * 0.5),
heartbeatCount90d: hbCount,
},
})
} catch (e) {
console.error("GET /api/admin/user-value-summary error:", e)
return NextResponse.json({ success: false, message: "查询失败" }, { status: 500 })
}
}