/** * 直播上下文 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): LiveContextData | null { const streamer = raw.streamer as Record | undefined if (!streamer || typeof streamer.id !== "string") return null const roomRaw = raw.room as Record | null | undefined const guildRaw = raw.guild as Record | 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 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 { if (isWzApiMode()) { const { ok, json } = await wzAppFetch>(`/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) } 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 { 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(`/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 { if (isWzApiMode()) { const { ok, json } = await wzAppFetch(`/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("/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 } }