Files
wzdj/old/lib/api/live.ts
乘风 a48bccca85 1
2026-04-27 09:49:32 +08:00

223 lines
7.8 KiB
TypeScript
Raw Permalink 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.

/**
* 直播上下文 API 客户端(房间、橱窗、风控词、公会分成提示)
* 配置 NEXT_PUBLIC_WZ_API_BASE 时走 wz-api `/api/app/live/*`;否则走 Next `/api/live/*`Mongo BFF
*/
import { isWzApiMode, wzAppFetch } from "@/lib/wz-api"
import { DEFAULT_LIVE_FORBIDDEN_WORDS } from "@/lib/live/constants"
import { getLiveGiftOrNull } from "@/lib/live/gift-catalog"
export interface LiveShowcaseItemDTO {
id: string
title: string
subtitle?: string
landingPath: string
refQuery: string
}
export interface LiveContextData {
streamer: {
id: string
name: string
isLive: boolean
commissionRate: number
guildId: string | null
}
guild: { id: string; name: string; commissionRate: number } | null
room: {
id: string
title: string
status: string
game: string
viewerCount: number
pullPlaybackUrl: string
roomMode: string
} | null
showcase: LiveShowcaseItemDTO[]
forbiddenWords: string[]
revenueHint: string
}
export function buildShowcaseHref(origin: string, item: LiveShowcaseItemDTO): string {
const path = item.landingPath.startsWith("/") ? item.landingPath : `/${item.landingPath}`
const q = item.refQuery.startsWith("?") ? item.refQuery.slice(1) : item.refQuery
return `${origin.replace(/\/$/, "")}${path}?${q}`
}
function mapWzLiveContext(raw: Record<string, unknown>): LiveContextData | null {
const streamer = raw.streamer as Record<string, unknown> | undefined
if (!streamer || typeof streamer.id !== "string") return null
const roomRaw = raw.room as Record<string, unknown> | null | undefined
const guildRaw = raw.guild as Record<string, unknown> | null | undefined
const showcaseRaw = raw.showcase
const streamerGameFallback = String(streamer.game ?? "电竞")
const showcase: LiveShowcaseItemDTO[] = Array.isArray(showcaseRaw)
? showcaseRaw.map((it) => {
const o = it as Record<string, unknown>
return {
id: String(o.id ?? ""),
title: String(o.title ?? ""),
subtitle: o.subtitle != null ? String(o.subtitle) : undefined,
landingPath: String(o.landingPath ?? "/mall"),
refQuery: String(o.refQuery ?? `streamerId=${streamer.id}&channel=live`),
}
})
: []
return {
streamer: {
id: streamer.id,
name: String(streamer.name ?? "主播"),
isLive: Boolean(streamer.isLive),
commissionRate: Number(streamer.commissionRate) || 0,
guildId: streamer.guildId != null && String(streamer.guildId) !== "" ? String(streamer.guildId) : null,
},
guild: guildRaw && typeof guildRaw.id === "string"
? {
id: guildRaw.id,
name: String(guildRaw.name ?? ""),
commissionRate: Number(guildRaw.commissionRate) || 0,
}
: null,
room: roomRaw && typeof roomRaw === "object"
? {
id: String(roomRaw.id ?? ""),
title: String(roomRaw.title ?? ""),
status: String(roomRaw.status ?? "offline"),
game: String(roomRaw.game ?? streamerGameFallback),
viewerCount: Number(roomRaw.viewerCount) || 0,
pullPlaybackUrl: String(roomRaw.pullPlaybackUrl ?? ""),
roomMode: String(roomRaw.roomMode ?? "video"),
}
: null,
showcase,
forbiddenWords: [...DEFAULT_LIVE_FORBIDDEN_WORDS],
revenueHint:
"直播间礼物与橱窗成交将按主播及签约公会配置记入分润台账commissionLedger比例以后台与合同为准。",
}
}
export async function getLiveContext(streamerId: string): Promise<LiveContextData | null> {
if (isWzApiMode()) {
const { ok, json } = await wzAppFetch<Record<string, unknown>>(`/api/app/live/context/${encodeURIComponent(streamerId)}`, {
method: "GET",
})
if (!ok || json.success === false || !json.data || typeof json.data !== "object") return null
return mapWzLiveContext(json.data as Record<string, unknown>)
}
const res = await fetch(`/api/live/context/${streamerId}`, { cache: "no-store" })
const json = (await res.json().catch(() => ({}))) as {
success?: boolean
data?: LiveContextData
message?: string
}
if (!res.ok || !json.success || !json.data) return null
return json.data
}
export interface LiveCommentDTO {
id: string
userId: string
userName: string
content: string
type: "normal" | "enter" | "gift" | "system"
level: number
createdAt: string
}
export async function getLiveComments(streamerId: string): Promise<LiveCommentDTO[]> {
if (isWzApiMode()) {
const { ok, json } = await wzAppFetch<{ comments?: LiveCommentDTO[] }>(
`/api/app/live/comments/${encodeURIComponent(streamerId)}`,
{ method: "GET" },
)
if (!ok || json.success === false || !json.data?.comments) return []
return json.data.comments
}
const res = await fetch(`/api/live/comments/${streamerId}`, { cache: "no-store" })
const json = (await res.json().catch(() => ({}))) as {
success?: boolean
data?: { comments?: LiveCommentDTO[] }
}
if (!res.ok || !json.success || !json.data?.comments) return []
return json.data.comments
}
export async function postLiveComment(
streamerId: string,
body: { content: string; type?: "normal" },
): Promise<{ ok: boolean; message?: string }> {
if (isWzApiMode()) {
const { ok, json } = await wzAppFetch<unknown>(`/api/app/live/comments/${encodeURIComponent(streamerId)}`, {
method: "POST",
body: JSON.stringify({ content: body.content, type: body.type ?? "normal" }),
})
if (!ok || json.success === false) {
return { ok: false, message: typeof json.message === "string" ? json.message : "发送失败" }
}
return { ok: true }
}
const res = await fetch(`/api/live/comments/${streamerId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: body.content, type: body.type ?? "normal" }),
})
const json = (await res.json().catch(() => ({}))) as { success?: boolean; message?: string }
if (!res.ok || !json.success) {
return { ok: false, message: json.message ?? "发送失败" }
}
return { ok: true }
}
/** 进房提示(需登录;失败静默) */
export async function postLiveEnter(streamerId: string): Promise<boolean> {
if (isWzApiMode()) {
const { ok, json } = await wzAppFetch<unknown>(`/api/app/live/comments/${encodeURIComponent(streamerId)}`, {
method: "POST",
body: JSON.stringify({ type: "enter", content: "" }),
})
return ok && json.success !== false
}
const res = await fetch(`/api/live/comments/${streamerId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: "enter", content: "" }),
})
const json = (await res.json().catch(() => ({}))) as { success?: boolean }
return res.ok && !!json.success
}
/** 直播送礼:服务端扣币 + liveGifts + commissionLedger + 公屏 gift */
export async function postLiveGift(
streamerId: string,
giftId: number,
): Promise<{ ok: boolean; message?: string }> {
if (isWzApiMode()) {
const gift = getLiveGiftOrNull(giftId)
const amount = gift?.priceCoins ?? 1
const { ok, json } = await wzAppFetch<unknown>("/api/app/live/gift", {
method: "POST",
body: JSON.stringify({
streamerId,
giftId: String(giftId),
amount,
quantity: 1,
}),
})
if (!ok || json.success === false) {
return { ok: false, message: typeof json.message === "string" ? json.message : "送礼失败" }
}
return { ok: true }
}
const res = await fetch(`/api/live/gift`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ streamerId, giftId }),
})
const json = (await res.json().catch(() => ({}))) as { success?: boolean; message?: string }
if (!res.ok || !json.success) {
return { ok: false, message: json.message ?? "送礼失败" }
}
return { ok: true }
}