hyapp-server/tools/lucky-gift-test.html
2026-05-28 15:11:10 +08:00

821 lines
30 KiB
HTML
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.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>幸运礼物测试页</title>
<style>
:root {
color-scheme: light;
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f4f7fb;
color: #172033;
}
* { box-sizing: border-box; }
body { margin: 0; padding: 24px; }
main { max-width: 1180px; margin: 0 auto; display: grid; gap: 16px; }
h1 { margin: 0; font-size: 24px; }
h2 { margin: 0 0 12px; font-size: 16px; }
section {
border: 1px solid #d9e2ef;
border-radius: 8px;
background: #fff;
padding: 16px;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06);
}
label { display: grid; gap: 6px; color: #53627a; font-size: 13px; font-weight: 700; }
input, select, textarea {
width: 100%;
border: 1px solid #cad6e6;
border-radius: 8px;
padding: 10px 12px;
font: inherit;
color: #172033;
background: #fbfdff;
}
textarea {
min-height: 84px;
resize: vertical;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
line-height: 1.45;
}
button {
border: 1px solid #2563eb;
border-radius: 8px;
padding: 10px 14px;
font: inherit;
font-weight: 800;
color: #fff;
background: #2563eb;
cursor: pointer;
}
button.secondary { color: #2563eb; background: #fff; }
button.danger { border-color: #dc2626; background: #dc2626; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
code { color: #1d4ed8; }
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.muted { color: #697891; font-size: 13px; }
.status { min-height: 22px; color: #334155; font-size: 13px; }
.status.error { color: #dc2626; }
.status.ok { color: #16a34a; }
.result {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.metric {
border: 1px solid #d9e2ef;
border-radius: 8px;
padding: 12px;
background: #fbfdff;
min-height: 76px;
}
.metric .name { color: #697891; font-size: 12px; font-weight: 800; }
.metric .value { margin-top: 8px; font-size: 22px; font-weight: 900; overflow-wrap: anywhere; }
.win .value { color: #dc2626; }
.lose .value { color: #334155; }
.log {
display: grid;
gap: 8px;
max-height: 360px;
overflow: auto;
padding-right: 4px;
}
.log-item {
border: 1px solid #d9e2ef;
border-radius: 8px;
padding: 10px;
background: #fbfdff;
}
.log-title { display: flex; gap: 8px; justify-content: space-between; font-size: 13px; font-weight: 900; }
.log-body { margin-top: 8px; white-space: pre-wrap; overflow-wrap: anywhere; font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: #334155; }
@media (max-width: 820px) {
body { padding: 12px; }
.grid, .grid.three, .result { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<main>
<header>
<h1>幸运礼物测试页</h1>
<div class="muted">本地默认网关:<code>http://127.0.0.1:13000</code>;用随页 server 打开时API Base URL 留空即可走本地代理。</div>
</header>
<section>
<h2>1. API 与账号</h2>
<div class="grid">
<label>
API Base URL
<input id="apiBase" placeholder="例如 http://127.0.0.1:13000代理模式留空" />
</label>
<label>
X-App-Code
<input id="appCode" value="lalu" />
</label>
<label>
账号 / UID
<input id="account" placeholder="用于账号密码登录" />
</label>
<label>
密码
<input id="password" type="password" placeholder="用于登录或快捷创建" />
</label>
</div>
<label style="margin-top:12px">
Access Token
<textarea id="token" placeholder="可以粘贴 App access_token也可以点击登录/快捷创建自动填充"></textarea>
</label>
<div class="actions" style="margin-top:12px">
<button id="loginBtn" type="button">账号密码登录</button>
<button id="quickCreateBtn" class="secondary" type="button">快捷创建测试账号</button>
<span id="authStatus" class="status"></span>
</div>
</section>
<section>
<h2>2. 房间、礼物与 IM</h2>
<div class="grid three">
<label>
room_id
<input id="roomId" value="test_room_1" />
</label>
<label>
target_user_id
<input id="targetUserId" inputmode="numeric" placeholder="收礼人 user_id" />
</label>
<label>
IM group_id
<input id="imGroupId" placeholder="进房后自动填充;为空时默认 room_id" />
</label>
<label>
gift_id
<input id="giftId" value="lucky_star_100" />
</label>
<label>
pool_id
<input id="poolId" value="lucky" />
</label>
<label>
奖池预设
<select id="poolPreset">
<option value="lucky">旧奖池 lucky</option>
<option value="super_lucky">旧奖池 super_lucky</option>
<option value="lucky_100">新奖池 lucky_100</option>
<option value="">手动输入</option>
</select>
</label>
<label>
gift_count
<input id="giftCount" inputmode="numeric" value="1" />
</label>
</div>
<div class="actions" style="margin-top:12px">
<button id="randomReadyBtn" type="button">随机房间和礼物</button>
<button id="loadPanelBtn" type="button">读取礼物面板</button>
<button id="checkBtn" class="secondary" type="button">检查幸运礼物</button>
<button id="newPoolDrawBtn" type="button">新奖池抽奖</button>
<button id="imBtn" type="button">登录 IM</button>
<button id="joinBtn" class="secondary" type="button">进房并加入 IM 群</button>
<span id="roomStatus" class="status"></span>
</div>
<label style="margin-top:12px">
礼物面板幸运礼物
<select id="giftSelect">
<option value="">未读取</option>
</select>
</label>
</section>
<section>
<h2>3. 发幸运礼物</h2>
<div class="actions">
<button id="sendBtn" type="button">发礼物</button>
<button id="clearBtn" class="danger" type="button">清空日志</button>
<span id="sendStatus" class="status"></span>
</div>
<div class="result" style="margin-top:12px">
<div id="winMetric" class="metric lose">
<div class="name">是否中奖</div>
<div id="winValue" class="value">未发送</div>
</div>
<div class="metric">
<div class="name">中奖倍数</div>
<div id="multiplierValue" class="value">-</div>
</div>
<div class="metric">
<div class="name">中奖金额</div>
<div id="rewardValue" class="value">-</div>
</div>
<div class="metric">
<div class="name">到账状态</div>
<div id="rewardStatusValue" class="value">-</div>
</div>
</div>
</section>
<section>
<h2>4. HTTP 与 IM 日志</h2>
<div id="log" class="log"></div>
</section>
</main>
<script type="module">
const SDK_URL = "https://cdn.jsdelivr.net/npm/@tencentcloud/chat@3.6.6/index.es.js";
const defaultApiBase = "http://127.0.0.1:13000";
const proxyHostnames = new Set(["localhost", "127.0.0.1", "::1"]);
const state = {
chat: null,
chatSDK: null,
imReady: false,
loginUserID: "",
selfUserID: "",
joinedGroupID: "",
seenEventIDs: new Set(),
};
const $ = (id) => document.getElementById(id);
const isProxyPage = proxyHostnames.has(window.location.hostname) && window.location.port === "8788";
$("apiBase").value = localStorage.getItem("lucky_gift_api_base") || (isProxyPage ? "" : defaultApiBase);
$("appCode").value = localStorage.getItem("lucky_gift_app_code") || "lalu";
$("account").value = localStorage.getItem("lucky_gift_account") || "";
$("password").value = localStorage.getItem("lucky_gift_password") || "";
$("token").value = localStorage.getItem("lucky_gift_access_token") || "";
$("roomId").value = localStorage.getItem("lucky_gift_room_id") || $("roomId").value;
$("targetUserId").value = localStorage.getItem("lucky_gift_target_user_id") || "";
$("imGroupId").value = localStorage.getItem("lucky_gift_im_group_id") || "";
$("giftId").value = localStorage.getItem("lucky_gift_gift_id") || $("giftId").value;
$("poolId").value = localStorage.getItem("lucky_gift_pool_id") || "lucky";
$("poolPreset").value = presetValue($("poolId").value.trim());
$("giftCount").value = localStorage.getItem("lucky_gift_count") || $("giftCount").value;
function apiBase() {
return $("apiBase").value.trim().replace(/\/+$/, "");
}
function appCode() {
return $("appCode").value.trim() || "lalu";
}
function token() {
return $("token").value.trim();
}
function rememberForm() {
localStorage.setItem("lucky_gift_api_base", apiBase());
localStorage.setItem("lucky_gift_app_code", appCode());
localStorage.setItem("lucky_gift_access_token", token());
localStorage.setItem("lucky_gift_account", $("account").value.trim());
localStorage.setItem("lucky_gift_password", $("password").value);
localStorage.setItem("lucky_gift_room_id", $("roomId").value.trim());
localStorage.setItem("lucky_gift_target_user_id", $("targetUserId").value.trim());
localStorage.setItem("lucky_gift_im_group_id", $("imGroupId").value.trim());
localStorage.setItem("lucky_gift_gift_id", $("giftId").value.trim());
localStorage.setItem("lucky_gift_pool_id", $("poolId").value.trim());
localStorage.setItem("lucky_gift_count", $("giftCount").value.trim());
$("poolPreset").value = presetValue($("poolId").value.trim());
}
async function request(path, options = {}) {
rememberForm();
const headers = {
"Content-Type": "application/json",
"X-App-Code": appCode(),
"X-App-Platform": "web",
"X-App-Language": navigator.language || "zh-CN",
...(options.headers || {}),
};
if (options.auth !== false && token()) {
headers.Authorization = `Bearer ${token()}`;
}
const response = await fetch(`${apiBase()}${path}`, {
method: options.method || "GET",
headers,
body: options.body === undefined ? undefined : JSON.stringify(options.body),
});
const text = await response.text();
const payload = parseJSON(text) || { message: text };
if (!response.ok || !isSuccessCode(payload.code)) {
throw new Error(payload.message || response.statusText || `HTTP ${response.status}`);
}
return payload.data;
}
function isSuccessCode(code) {
return code === undefined || code === null || code === 0 || code === "0" || code === 200 || code === "200" || code === "OK";
}
function parseJSON(raw) {
if (typeof raw !== "string") return null;
try {
return raw ? JSON.parse(raw) : {};
} catch {
return null;
}
}
function setStatus(id, message, type = "") {
const el = $(id);
el.textContent = message || "";
el.className = ["status", type].filter(Boolean).join(" ");
}
function addLog(title, body, type = "") {
const item = document.createElement("div");
item.className = "log-item";
item.innerHTML = `
<div class="log-title">
<span>${escapeHtml(title)}</span>
<span class="muted">${escapeHtml(new Date().toLocaleTimeString())}</span>
</div>
<div class="log-body">${escapeHtml(typeof body === "string" ? body : JSON.stringify(body, null, 2))}</div>
`;
if (type === "error") item.style.borderColor = "#fecaca";
$("log").prepend(item);
}
function saveToken(data) {
const accessToken = data?.access_token || data?.token?.access_token || "";
if (!accessToken) throw new Error("登录成功但响应里没有 access_token");
$("token").value = accessToken;
if (data.account || data.uid) $("account").value = data.account || data.uid;
state.selfUserID = String(data.uid || data.user_id || data.account || jwtUserID(accessToken) || "").trim();
if (state.selfUserID) $("targetUserId").value = state.selfUserID;
rememberForm();
}
async function login() {
setStatus("authStatus", "登录中...");
try {
const account = $("account").value.trim();
const password = $("password").value;
if (!account || !password) throw new Error("请填写账号/UID 和密码");
const data = await request("/api/v1/auth/account/login", {
method: "POST",
auth: false,
body: loginMeta({ account, password }),
});
saveToken(data);
addLog("账号登录成功", data);
setStatus("authStatus", "登录成功token 已填入", "ok");
await prepareRandomRoomAndGift();
} catch (err) {
addLog("账号登录失败", errorMessage(err), "error");
setStatus("authStatus", errorMessage(err), "error");
}
}
async function quickCreate() {
setStatus("authStatus", "创建中...");
try {
const password = $("password").value || "Test@123456";
const data = await request("/api/v1/auth/account/quick-create", {
method: "POST",
auth: false,
body: loginMeta({ password }),
});
$("password").value = password;
saveToken(data);
addLog("快捷创建成功", data);
setStatus("authStatus", `创建成功:${data.account || data.uid || ""}`, "ok");
await prepareRandomRoomAndGift();
} catch (err) {
addLog("快捷创建失败", errorMessage(err), "error");
setStatus("authStatus", errorMessage(err), "error");
}
}
function loginMeta(extra) {
return {
...extra,
device_id: "lucky-gift-html-test",
platform: "web",
language: navigator.language || "zh-CN",
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
};
}
async function loadGiftPanel() {
setStatus("roomStatus", "读取礼物面板...");
try {
const roomID = $("roomId").value.trim();
if (!roomID) throw new Error("请填写 room_id");
const data = await request(`/api/v1/rooms/${encodeURIComponent(roomID)}/gift-panel`);
const gifts = Array.isArray(data?.gifts) ? data.gifts : [];
const luckyGifts = gifts.filter((gift) => ["lucky", "super_lucky"].includes(String(gift.gift_type_code || "")));
renderGiftOptions(luckyGifts, { random: false });
setSelfRecipient();
addLog("礼物面板", data);
setStatus("roomStatus", `读取成功:${luckyGifts.length} 个幸运礼物`, "ok");
} catch (err) {
addLog("读取礼物面板失败", errorMessage(err), "error");
setStatus("roomStatus", errorMessage(err), "error");
}
}
async function prepareRandomRoomAndGift() {
setStatus("roomStatus", "随机获取本地房间...");
try {
setSelfRecipient();
const room = await randomLocalRoom();
$("roomId").value = room.room_id || room.roomId || "";
$("imGroupId").value = room.im_group_id || room.imGroupId || $("roomId").value.trim();
rememberForm();
addLog("随机选中房间", room);
await joinRoomAndGroup();
await loadRandomLuckyGift();
setStatus("roomStatus", `已准备:房间 ${$("roomId").value},礼物 ${$("giftId").value}`, "ok");
} catch (err) {
addLog("随机准备失败", errorMessage(err), "error");
setStatus("roomStatus", errorMessage(err), "error");
}
}
async function randomLocalRoom() {
const data = await request("/api/v1/rooms?tab=hot&limit=50");
const rooms = (Array.isArray(data?.rooms) ? data.rooms : []).filter((room) => room?.room_id && room.status === "active" && !room.locked);
if (!rooms.length) throw new Error("本地房间列表没有可加入的 active 非锁房房间");
return randomItem(rooms);
}
async function loadRandomLuckyGift() {
const roomID = $("roomId").value.trim();
if (!roomID) throw new Error("请先选中 room_id");
const data = await request(`/api/v1/rooms/${encodeURIComponent(roomID)}/gift-panel`);
const gifts = Array.isArray(data?.gifts) ? data.gifts : [];
const luckyGifts = gifts.filter((gift) => ["lucky", "super_lucky"].includes(String(gift.gift_type_code || "")));
if (!luckyGifts.length) throw new Error("当前房间礼物面板没有 lucky / super_lucky 礼物");
renderGiftOptions(luckyGifts, { random: true });
setSelfRecipient();
applyPoolFallbackFromGift($("giftId").value, luckyGifts.find((gift) => gift.gift_id === $("giftId").value));
rememberForm();
addLog("随机选中幸运礼物", { gift_id: $("giftId").value, pool_id: $("poolId").value });
}
function renderGiftOptions(gifts, options = {}) {
const select = $("giftSelect");
select.innerHTML = "";
if (!gifts.length) {
select.append(new Option("没有找到 lucky / super_lucky 礼物", ""));
return;
}
for (const gift of gifts) {
const poolID = luckyPoolID(gift) || fallbackPoolID(gift);
const label = `${gift.name || gift.gift_id} / ${gift.gift_id} / pool=${poolID || "-"}`;
const option = new Option(label, JSON.stringify({ gift_id: gift.gift_id, pool_id: poolID, coin_price: gift.coin_price }));
select.append(option);
}
if (options.random) select.selectedIndex = Math.floor(Math.random() * select.options.length);
applySelectedGift();
}
function luckyPoolID(gift) {
const presentation = parseJSON(gift?.presentation_json || "") || {};
return String(gift?.lucky_pool_id || presentation.lucky_pool_id || presentation.pool_id || "").trim();
}
function firstRecipientID(recipients) {
if (!Array.isArray(recipients)) return "";
const item = recipients.find((recipient) => recipient?.user_id || recipient?.target_user_id);
return item?.user_id || item?.target_user_id || "";
}
function applySelectedGift() {
const value = $("giftSelect").value;
if (!value) return;
const gift = parseJSON(value);
if (!gift) return;
$("giftId").value = gift.gift_id || $("giftId").value;
if (gift.pool_id) $("poolId").value = gift.pool_id;
rememberForm();
}
function fallbackPoolID(gift) {
const typeCode = String(gift?.gift_type_code || "");
if (typeCode === "super_lucky") return "super_lucky";
return "lucky";
}
function applyPoolFallbackFromGift(giftID, gift) {
// 新奖池测试需要尊重手动输入和后台面板下发的 lucky_pool_id只有礼物没有携带奖池时才按礼物类型兜底。
const configured = luckyPoolID(gift);
if (configured) {
$("poolId").value = configured;
return;
}
if (!$("poolId").value.trim()) {
$("poolId").value = fallbackPoolID(gift || { gift_type_code: giftID.includes("super") ? "super_lucky" : "lucky" });
}
}
function usePoolPreset(value) {
if (value) $("poolId").value = value;
rememberForm();
}
function presetValue(poolID) {
return ["lucky", "super_lucky", "lucky_100"].includes(poolID) ? poolID : "";
}
async function sendNewPoolLuckyGift() {
// 新奖池第一步的验收重点是 room-service 送礼时明确传入 pool_id这里固定 lucky_100礼物 ID 仍允许手动改。
$("poolId").value = "lucky_100";
$("poolPreset").value = "lucky_100";
rememberForm();
if (!(await checkLuckyGift())) return;
await sendLuckyGift();
}
function setSelfRecipient() {
const selfID = state.selfUserID || state.loginUserID || jwtUserID(token()) || $("account").value.trim();
if (selfID) {
state.selfUserID = String(selfID);
$("targetUserId").value = String(selfID);
}
}
async function checkLuckyGift() {
setStatus("roomStatus", "检查幸运礼物...");
try {
const body = giftBaseBody();
const data = await request("/api/v1/activities/lucky-gifts/check", {
method: "POST",
body: {
room_id: body.room_id,
gift_id: body.gift_id,
pool_id: body.pool_id,
},
});
addLog("幸运礼物检查结果", data);
setStatus("roomStatus", data.enabled ? "幸运礼物可用" : `不可用:${data.reason || "-"}`, data.enabled ? "ok" : "error");
return Boolean(data.enabled);
} catch (err) {
addLog("幸运礼物检查失败", errorMessage(err), "error");
setStatus("roomStatus", errorMessage(err), "error");
return false;
}
}
async function loginIM() {
setStatus("roomStatus", "登录 IM...");
try {
if (!token()) throw new Error("请先登录或填写 access_token");
const sig = await request("/api/v1/im/usersig");
const SDK = await loadChatSDK();
const sdkAppID = Number(sig.sdk_app_id || sig.sdkAppID || sig.SDKAppID);
const userID = String(sig.user_id || sig.userID || "");
const userSig = sig.user_sig || sig.userSig || "";
if (!sdkAppID || !userID || !userSig) throw new Error("usersig 响应缺少 sdk_app_id/user_id/user_sig");
state.loginUserID = userID;
state.selfUserID = state.selfUserID || userID;
$("targetUserId").value = state.selfUserID;
state.chat = SDK.create({ SDKAppID: sdkAppID });
state.chat.setLogLevel?.(1);
bindIMEvents(SDK);
await state.chat.login({ userID, userSig });
await waitIMReady();
addLog("IM 登录成功", { sdk_app_id: sdkAppID, user_id: userID, expire_at_ms: sig.expire_at_ms });
setStatus("roomStatus", `IM 已登录:${userID}`, "ok");
} catch (err) {
addLog("IM 登录失败", errorMessage(err), "error");
setStatus("roomStatus", errorMessage(err), "error");
}
}
async function loadChatSDK() {
if (state.chatSDK) return state.chatSDK;
const mod = await import(SDK_URL);
state.chatSDK = mod.default || mod.TencentCloudChat || mod;
return state.chatSDK;
}
function bindIMEvents(SDK) {
state.imReady = state.chat?.isReady?.() === true;
state.chat.on(SDK.EVENT.SDK_READY, () => {
state.imReady = true;
addLog("IM SDK_READY", { ready: true });
});
state.chat.on(SDK.EVENT.SDK_NOT_READY, () => {
state.imReady = false;
addLog("IM SDK_NOT_READY", { ready: false });
});
state.chat.on(SDK.EVENT.MESSAGE_RECEIVED, (event) => {
for (const message of event.data || []) handleIMMessage(message);
});
state.chat.on(SDK.EVENT.KICKED_OUT, (event) => {
state.imReady = false;
addLog("IM 被踢下线", event.data || event, "error");
});
}
function waitIMReady() {
if (state.imReady || state.chat?.isReady?.()) return Promise.resolve();
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error("IM SDK_READY 等待超时")), 12000);
const timer = setInterval(() => {
if (state.imReady || state.chat?.isReady?.()) {
clearInterval(timer);
clearTimeout(timeout);
resolve();
}
}, 200);
});
}
async function joinRoomAndGroup() {
setStatus("roomStatus", "进房并加入 IM 群...");
try {
const roomID = $("roomId").value.trim();
if (!roomID) throw new Error("请填写 room_id");
const data = await request("/api/v1/rooms/join", {
method: "POST",
body: {
room_id: roomID,
command_id: commandID("join"),
role: "audience",
},
});
const groupID = data?.im?.group_id || data?.room?.im_group_id || roomID;
$("imGroupId").value = groupID;
rememberForm();
addLog("业务进房成功", data);
if (!state.chat) await loginIM();
await waitIMReady();
await state.chat.joinGroup({ groupID });
state.joinedGroupID = groupID;
addLog("IM 加群成功", { group_id: groupID });
setStatus("roomStatus", `已进房并加入 IM 群:${groupID}`, "ok");
} catch (err) {
addLog("进房或加群失败", errorMessage(err), "error");
setStatus("roomStatus", errorMessage(err), "error");
}
}
async function sendLuckyGift() {
setStatus("sendStatus", "发礼物中...");
try {
const body = {
...giftBaseBody(),
command_id: commandID("gift"),
target_type: "user",
target_user_ids: [requiredInt("targetUserId", "target_user_id")],
gift_count: requiredInt("giftCount", "gift_count"),
};
const data = await request("/api/v1/rooms/gift/send", {
method: "POST",
body,
});
addLog("发礼物 HTTP 响应", data);
if (data?.lucky_gift) {
renderLuckyResult("HTTP", data.lucky_gift);
} else {
renderEmptyResult("HTTP 成功,响应无 lucky_gift");
}
setStatus("sendStatus", "发礼物成功,等待 IM 同步", "ok");
} catch (err) {
addLog("发礼物失败", errorMessage(err), "error");
setStatus("sendStatus", errorMessage(err), "error");
}
}
function giftBaseBody() {
const roomID = $("roomId").value.trim();
const giftID = $("giftId").value.trim();
const poolID = $("poolId").value.trim();
if (!roomID) throw new Error("请填写 room_id");
if (!giftID) throw new Error("请填写 gift_id");
if (!poolID) throw new Error("请填写 pool_id");
return { room_id: roomID, gift_id: giftID, pool_id: poolID };
}
function requiredInt(id, label) {
const value = Number.parseInt($(id).value.trim(), 10);
if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`请填写有效的 ${label}`);
return value;
}
function commandID(prefix) {
const random = Math.random().toString(36).slice(2, 8);
return `${prefix}_${Date.now()}_${random}`;
}
function randomItem(items) {
return items[Math.floor(Math.random() * items.length)];
}
function jwtUserID(accessToken) {
const payload = parseJWTPayload(accessToken);
return payload?.user_id || payload?.uid || payload?.sub || "";
}
function parseJWTPayload(accessToken) {
const part = String(accessToken || "").split(".")[1];
if (!part) return null;
try {
const normalized = part.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(part.length / 4) * 4, "=");
return JSON.parse(decodeURIComponent(escape(atob(normalized))));
} catch {
return null;
}
}
function handleIMMessage(message) {
const decoded = decodeCustomMessage(message);
addLog(`IM 收到 ${decoded.desc || decoded.payload?.event_type || message.type || ""}`, decoded);
if (!decoded.payload) return;
const eventID = decoded.payload.event_id || decoded.payload.draw_id || "";
if (eventID && state.seenEventIDs.has(eventID)) return;
if (eventID) state.seenEventIDs.add(eventID);
if (decoded.desc === "lucky_gift_drawn" || decoded.payload.event_type === "lucky_gift_drawn") {
renderLuckyResult("IM", decoded.payload);
}
}
function decodeCustomMessage(message) {
const payload = message?.payload || {};
const raw = payload.data || payload.Data || message?.cloudCustomData || payload.cloudCustomData || "";
const parsed = typeof raw === "string" ? parseJSON(raw) : raw;
return {
type: message?.type || "",
conversationID: message?.conversationID || "",
from: message?.from || message?.nick || "",
desc: payload.description || payload.desc || message?.description || "",
extension: payload.extension || message?.extension || "",
raw,
payload: parsed || null,
};
}
function renderLuckyResult(source, luckyGift) {
const reward = Number(luckyGift.effective_reward_coins || 0);
const multiplierPPM = Number(luckyGift.multiplier_ppm || 0);
const isWin = reward > 0;
$("winMetric").className = `metric ${isWin ? "win" : "lose"}`;
$("winValue").textContent = isWin ? "中奖" : "未中奖";
$("multiplierValue").textContent = multiplierLabel(multiplierPPM);
$("rewardValue").textContent = `${reward} 金币`;
$("rewardStatusValue").textContent = luckyGift.reward_status || "-";
addLog(`${source} 开奖结果:${isWin ? "中奖" : "未中奖"}`, {
draw_id: luckyGift.draw_id,
command_id: luckyGift.command_id,
pool_id: luckyGift.pool_id,
gift_id: luckyGift.gift_id,
multiplier: multiplierLabel(multiplierPPM),
multiplier_ppm: multiplierPPM,
effective_reward_coins: reward,
base_reward_coins: Number(luckyGift.base_reward_coins || 0),
reward_status: luckyGift.reward_status || "",
high_multiplier: Boolean(luckyGift.high_multiplier),
});
}
function renderEmptyResult(reason) {
$("winMetric").className = "metric lose";
$("winValue").textContent = "无抽奖";
$("multiplierValue").textContent = "-";
$("rewardValue").textContent = "-";
$("rewardStatusValue").textContent = reason;
}
function multiplierLabel(ppm) {
if (!Number.isFinite(ppm) || ppm <= 0) return "0x";
const value = ppm / 1000000;
return `${Number.isInteger(value) ? value : value.toFixed(3).replace(/0+$/, "").replace(/\.$/, "")}x`;
}
function errorMessage(err) {
return err instanceof Error ? err.message : String(err);
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
$("loginBtn").onclick = login;
$("quickCreateBtn").onclick = quickCreate;
$("randomReadyBtn").onclick = prepareRandomRoomAndGift;
$("loadPanelBtn").onclick = loadGiftPanel;
$("checkBtn").onclick = checkLuckyGift;
$("newPoolDrawBtn").onclick = sendNewPoolLuckyGift;
$("imBtn").onclick = loginIM;
$("joinBtn").onclick = joinRoomAndGroup;
$("sendBtn").onclick = sendLuckyGift;
$("clearBtn").onclick = () => {
$("log").innerHTML = "";
state.seenEventIDs.clear();
renderEmptyResult("已清空");
};
$("giftSelect").onchange = applySelectedGift;
$("poolId").oninput = rememberForm;
$("poolPreset").onchange = (event) => usePoolPreset(event.target.value);
</script>
</body>
</html>