106 lines
3.5 KiB
TypeScript
106 lines
3.5 KiB
TypeScript
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 })
|
||
}
|
||
}
|