897 lines
33 KiB
HTML
897 lines
33 KiB
HTML
<!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="refreshStatsBtn" class="secondary" 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>
|
||
<div class="result" style="margin-top:12px">
|
||
<div class="metric">
|
||
<div class="name">该用户抽奖次数</div>
|
||
<div id="userDrawCountValue" class="value">-</div>
|
||
</div>
|
||
<div class="metric">
|
||
<div class="name">该用户消耗金币</div>
|
||
<div id="userSpentValue" class="value">-</div>
|
||
</div>
|
||
<div class="metric">
|
||
<div class="name">该用户返还金币</div>
|
||
<div id="userRewardValue" class="value">-</div>
|
||
</div>
|
||
<div class="metric">
|
||
<div class="name">等价抽数 / 连亏</div>
|
||
<div id="userStateValue" 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;
|
||
}
|
||
|
||
async function localRequest(path) {
|
||
const response = await fetch(path, { headers: { "Cache-Control": "no-store" } });
|
||
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 refreshUserStats();
|
||
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 refreshUserStats();
|
||
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");
|
||
await refreshUserStats();
|
||
} catch (err) {
|
||
addLog("发礼物失败", errorMessage(err), "error");
|
||
setStatus("sendStatus", errorMessage(err), "error");
|
||
}
|
||
}
|
||
|
||
async function refreshUserStats() {
|
||
const userID = state.selfUserID || state.loginUserID || jwtUserID(token()) || $("account").value.trim();
|
||
if (!userID || !/^\d+$/.test(String(userID))) {
|
||
renderUserStats(null);
|
||
return;
|
||
}
|
||
try {
|
||
const params = new URLSearchParams({
|
||
app_code: appCode(),
|
||
user_id: String(userID),
|
||
pool_id: $("poolId").value.trim(),
|
||
});
|
||
const data = await localRequest(`/__local/lucky-gift/stats?${params.toString()}`);
|
||
renderUserStats(data);
|
||
} catch (err) {
|
||
addLog("刷新用户抽奖统计失败", errorMessage(err), "error");
|
||
renderUserStats(null);
|
||
}
|
||
}
|
||
|
||
function renderUserStats(stats) {
|
||
if (!stats) {
|
||
$("userDrawCountValue").textContent = "-";
|
||
$("userSpentValue").textContent = "-";
|
||
$("userRewardValue").textContent = "-";
|
||
$("userStateValue").textContent = "-";
|
||
return;
|
||
}
|
||
const draws = Number(stats.draw_count || 0);
|
||
const spent = Number(stats.coin_spent || 0);
|
||
const reward = Number(stats.effective_reward_coins || 0);
|
||
const equivalentDraws = Number(stats.state_equivalent_draws || 0);
|
||
const lossStreak = Number(stats.loss_streak || 0);
|
||
$("userDrawCountValue").textContent = `${draws} 抽`;
|
||
$("userSpentValue").textContent = `${spent} 金币`;
|
||
$("userRewardValue").textContent = `${reward} 金币`;
|
||
$("userStateValue").textContent = `${equivalentDraws} / ${lossStreak}`;
|
||
}
|
||
|
||
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("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """);
|
||
}
|
||
|
||
$("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;
|
||
$("refreshStatsBtn").onclick = refreshUserStats;
|
||
$("clearBtn").onclick = () => {
|
||
$("log").innerHTML = "";
|
||
state.seenEventIDs.clear();
|
||
renderEmptyResult("已清空");
|
||
};
|
||
$("giftSelect").onchange = applySelectedGift;
|
||
$("poolId").oninput = rememberForm;
|
||
$("poolPreset").onchange = async (event) => {
|
||
usePoolPreset(event.target.value);
|
||
await refreshUserStats();
|
||
};
|
||
void refreshUserStats();
|
||
</script>
|
||
</body>
|
||
</html>
|