- 新增直播评论/上下文/礼物等 API 与回放组件 - 主播开播、云端推流配置与 streamer 校验 - 依赖与 Mongo 集合/种子更新 - 需求与调研文档补充 Made-with: Cursor
210 lines
6.8 KiB
TypeScript
210 lines
6.8 KiB
TypeScript
/**
|
||
* 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)
|
||
}
|