Files
wzdj/app/api/live/gift/route.ts
卡若 b95d1a94c6 feat(live): 直播与主播工作台 API、云厂商解析与文档调研
- 新增直播评论/上下文/礼物等 API 与回放组件
- 主播开播、云端推流配置与 streamer 校验
- 依赖与 Mongo 集合/种子更新
- 需求与调研文档补充

Made-with: Cursor
2026-04-14 16:24:17 +08:00

210 lines
6.8 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.

/**
* POST /api/live/gift
* body: { streamerId: string, giftId: number }
* 扣减用户玩值币、写 transactions、liveGifts、commissionLedger并写入公屏 type=gift
*/
import { NextRequest, NextResponse } from "next/server"
import { ObjectId } from "mongodb"
import { getCollection } from "@/lib/db/mongo"
import { getLiveGiftOrNull } from "@/lib/live/gift-catalog"
const PLATFORM_GIFT_SHARE = 0.1
function nowIso() {
return new Date().toISOString()
}
export async function POST(req: NextRequest) {
try {
const userIdStr = req.cookies.get("wanzhi_user_id")?.value
if (!userIdStr || !ObjectId.isValid(userIdStr)) {
return NextResponse.json({ success: false, message: "请先登录" }, { status: 401 })
}
const body = (await req.json().catch(() => ({}))) as {
streamerId?: string
giftId?: number
}
const streamerId = typeof body.streamerId === "string" ? body.streamerId.trim() : ""
const giftId = Number(body.giftId)
if (!streamerId || !ObjectId.isValid(streamerId)) {
return NextResponse.json({ success: false, message: "无效主播 id" }, { status: 400 })
}
const gift = getLiveGiftOrNull(giftId)
if (!gift) {
return NextResponse.json({ success: false, message: "无效礼物" }, { status: 400 })
}
const sid = new ObjectId(streamerId)
const uid = new ObjectId(userIdStr)
const streamersColl = await getCollection("streamers")
const streamer = await streamersColl.findOne({ _id: sid, status: "approved" })
if (!streamer) {
return NextResponse.json({ success: false, message: "主播不存在或未审核" }, { status: 404 })
}
const roomsColl = await getCollection("liveRooms")
const room = await roomsColl.findOne({ streamerId: sid })
const liveRoomId = room?._id instanceof ObjectId ? room._id : null
const sessionsColl = await getCollection("liveSessions")
const activeSession = await sessionsColl.findOne({ streamerId: sid, status: "active" })
const liveSessionId = activeSession?._id instanceof ObjectId ? activeSession._id : null
const usersColl = await getCollection("users")
const user = await usersColl.findOne({ _id: uid })
if (!user) {
return NextResponse.json({ success: false, message: "用户不存在" }, { status: 404 })
}
const price = gift.priceCoins
const rawGid = (streamer as { guildId?: string | ObjectId | null }).guildId
let guildId: ObjectId | null = null
let guildShareRate = 0
if (rawGid) {
try {
const gid =
typeof rawGid === "string" && ObjectId.isValid(rawGid)
? new ObjectId(rawGid)
: rawGid instanceof ObjectId
? rawGid
: null
if (gid) {
const gc = await getCollection("guilds")
const g = await gc.findOne({ _id: gid })
if (g) {
guildId = gid
guildShareRate = Math.min(1, Math.max(0, Number((g as { commissionRate?: number }).commissionRate) || 0))
}
}
} catch {
guildId = null
}
}
const platformCoins = Math.floor(price * PLATFORM_GIFT_SHARE)
const pool = price - platformCoins
const guildCoins = guildId ? Math.floor(pool * guildShareRate) : 0
const streamerCoins = pool - guildCoins
const orderNo = `LIVEGIFT-${Date.now()}-${randomSuffix()}`
const afterDeduct = await usersColl.findOneAndUpdate(
{ _id: uid, balance: { $gte: price } },
{ $inc: { balance: -price }, $set: { updatedAt: new Date() } },
{ returnDocument: "after" },
)
if (!afterDeduct) {
return NextResponse.json({ success: false, message: "玩值币不足" }, { status: 400 })
}
const giftDocUserName = String(
(user as { name?: string; phone?: string }).name?.trim() ||
(user as { phone?: string }).phone?.trim() ||
"用户",
).slice(0, 24)
try {
const txColl = await getCollection("transactions")
await txColl.insertOne({
userId: userIdStr,
orderNo,
type: "live_gift",
amount: price,
remark: `直播送礼-${gift.name}`,
streamerId: streamerId,
liveRoomId: liveRoomId?.toString() ?? null,
liveSessionId: liveSessionId?.toString() ?? null,
createdAt: nowIso(),
} as never)
const giftsColl = await getCollection("liveGifts")
const giftDoc = {
streamerId: sid,
liveRoomId,
liveSessionId,
userId: userIdStr,
userName: giftDocUserName,
giftId,
giftName: gift.name,
icon: gift.icon,
amountCoins: price,
orderNo,
createdAt: new Date(),
}
const giftIns = await giftsColl.insertOne(giftDoc as never)
const ledgerColl = await getCollection("commissionLedger")
const baseLedger = {
sourceType: "live_gift" as const,
sourceId: giftIns.insertedId,
streamerId: sid,
orderNo,
liveSessionId,
createdAt: new Date(),
}
await ledgerColl.insertMany(
[
{
...baseLedger,
beneficiaryType: "platform",
guildId: null,
amountCoins: platformCoins,
},
{
...baseLedger,
beneficiaryType: "guild",
guildId: guildId ?? null,
amountCoins: guildCoins,
},
{
...baseLedger,
beneficiaryType: "streamer",
guildId: null,
amountCoins: streamerCoins,
},
] as never[],
)
await streamersColl.updateOne(
{ _id: sid },
{ $inc: { totalGifts: price, totalIncome: streamerCoins }, $set: { updatedAt: nowIso() } },
)
const commentsColl = await getCollection("liveComments")
const enterText = `送出了 ${gift.icon} ${gift.name}`
await commentsColl.insertOne({
streamerId: sid,
liveRoomId,
userId: userIdStr,
userName: giftDocUserName,
content: enterText,
type: "gift",
createdAt: new Date(),
} as never)
return NextResponse.json({
success: true,
data: {
orderNo,
giftId: giftIns.insertedId.toString(),
deducted: price,
split: { platformCoins, guildCoins, streamerCoins },
},
})
} catch (e) {
await usersColl.updateOne({ _id: uid }, { $inc: { balance: price }, $set: { updatedAt: new Date() } })
console.error("POST /api/live/gift persist error:", e)
return NextResponse.json({ success: false, message: "送礼失败,请稍后重试" }, { status: 500 })
}
} catch (e) {
console.error("POST /api/live/gift error:", e)
return NextResponse.json({ success: false, message: "送礼失败" }, { status: 500 })
}
}
function randomSuffix() {
return Math.random().toString(36).slice(2, 8)
}