223 lines
7.8 KiB
TypeScript
223 lines
7.8 KiB
TypeScript
/**
|
||
* 直播上下文 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 }
|
||
}
|