import Skeleton from "@mui/material/Skeleton"; import { Button } from "@/shared/ui/Button.jsx"; import { TimeText } from "@/shared/ui/TimeText.jsx"; import { formatMultiplierFromPPM, formatNumber, formatPercentFromPPM } from "../format.js"; import { OpsStatusBadge } from "./OpsStatusBadge.jsx"; export function UserProfileDetail({ detailState, onLoadMore, onRetry, profile }) { return (
{!detailState || detailState.loading ? : null} {detailState?.error ? (
{detailState.error}
) : null} {detailState?.data && !detailState.loading ? ( ) : null}
); } function ProfileDetailContent({ data, fallbackProfile, loadingMore, onLoadMore }) { const profile = data.profile?.key ? data.profile : fallbackProfile; const distribution = [...(data.multiplierDistribution || [])].sort( (left, right) => right.multiplierPpm - left.multiplierPpm, ); const jackpotHits = data.jackpotHits || []; return (
0 ? `还差 ${formatNumber(profile.guaranteeDrawsRemaining)} 抽` : "保护剩余 0 抽" } /> } />

倍率命中分布

普通随机和两条大奖通道分列,倍率相同时也不会混为一次命中。

{distribution.length} 个倍率
{distribution.length ? (
倍率 未中奖 普通命中 RTP 补偿大奖 累计消费大奖 合计命中 返奖金币
{distribution.map((metric) => (
{formatMultiplierFromPPM(metric.multiplierPpm)} {formatNumber(metric.zeroDrawCount)} {formatNumber(metric.ordinaryHitCount)} {formatNumber(metric.rtpCompensationJackpotCount)} {formatNumber(metric.cumulativeSpendJackpotCount)} {formatNumber(metric.hitCount)} {formatNumber(metric.payoutCoins)}
))}
) : (
当前没有倍率命中数据
)}

大奖命中依据

逐次展示走哪条大奖通道、命中时的关键门槛和实际判断结果。

展示 {jackpotHits.length} / 共 {formatNumber(data.jackpotHitTotal || jackpotHits.length)} 次
{jackpotHits.length ? ( <>
{jackpotHits.map((hit, index) => ( ))}
{data.jackpotHasMore && data.nextJackpotCursor ? (
) : null} ) : (
该用户当前没有大奖命中记录
)}
); } function DetailMetric({ label, value }) { return (
{label}
{value || "-"}
); } function JackpotHit({ hit }) { const conditions = flattenConditions(hit.conditions); const mechanism = mechanismLabel(hit.mechanism); return (
{formatMultiplierFromPPM(hit.multiplierPpm)} 返奖 {formatNumber(hit.payoutCoins)} 金币
为什么命中 {reasonLabel(hit.reason, hit.reasonCode, hit.mechanism)}
{conditions.length ? (
{conditions.map((condition) => (
{condition.value?.label || conditionLabel(condition.key)}
{conditionValue(condition.key, condition.value)}
))}
) : (
该历史记录没有保存更多判断快照
)}
); } function DetailSkeleton() { return ( ); } function flattenConditions(value, prefix = "") { if (Array.isArray(value)) { return value .filter((condition) => condition && !isInternalIdentityKey(condition.name)) .map((condition, index) => ({ key: condition.name || `condition_${index + 1}`, value: condition, })); } if (!value || typeof value !== "object") { return []; } return Object.entries(value).flatMap(([key, child]) => { const path = prefix ? `${prefix}.${key}` : key; // stable hash、strategy user key 等仅用于 owner 内部关联,尤其外部用户不能从诊断快照泄露。 if (isInternalIdentityKey(path)) { return []; } if (child && typeof child === "object" && !Array.isArray(child)) { return flattenConditions(child, path); } return [{ key: path, value: Array.isArray(child) ? child.join("、") : child }]; }); } function isInternalIdentityKey(key) { const normalized = String(key).toLowerCase(); return ["strategy_user_id", "user_key", "stable_user", "hashed_user", "user_hash"].some((token) => normalized.includes(token), ); } function mechanismLabel(mechanism) { const normalized = String(mechanism || "").toLowerCase(); if (["rtp_compensation", "mechanism_1", "jackpot_mechanism_1", "compensation"].includes(normalized)) { return "RTP 补偿大奖"; } if (["cumulative_spend", "spend_milestone", "mechanism_2", "jackpot_mechanism_2", "spend"].includes(normalized)) { return "累计消费大奖"; } return mechanism || "大奖"; } function mechanismTone(mechanism) { return mechanismLabel(mechanism) === "累计消费大奖" ? "warning" : "succeeded"; } function reasonLabel(reason, reasonCode, mechanism) { const normalized = String(reason || "").trim(); const known = { cumulative_spend_token_redeemed: "累计消费达到门槛后生成大奖机会,本抽满足支付条件并完成兑付。", jackpot_token_redeemed: "此前生成的大奖机会在本抽满足支付条件并完成兑付。", milestone_token_jackpot: "用户累计消费已获得一次大奖机会,且奖池余额足够支付,因此触发累计消费大奖。", rtp_compensation_eligible: "用户和全局 RTP 等补偿条件均满足,本抽进入 RTP 补偿大奖通道并命中。", rtp_compensation_jackpot: "用户与奖池均满足补偿条件,且奖池余额足够支付,因此触发 RTP 补偿大奖。", rtp_compensation_token_redeemed: "RTP 补偿机会已生成,本抽满足奖池支付条件并完成兑付。", }; const normalizedCode = String(reasonCode || "") .trim() .toLowerCase(); if (known[normalizedCode] || known[normalized.toLowerCase()] || normalized) { return known[normalizedCode] || known[normalized.toLowerCase()] || normalized; } return mechanismLabel(mechanism) === "累计消费大奖" ? "累计消费达到规则门槛,生成的大奖机会在本抽完成兑付。" : "用户和全局 RTP、奖池水位等门槛均允许,RTP 补偿机会在本抽完成兑付。"; } function conditionLabel(key) { const normalized = String(key).split(".").pop()?.toLowerCase() || key; const labels = { pool_balance_coins: "命中前奖池余额", pool_payable: "奖池能否支付", global_rtp_ppm: "命中前全局 RTP", global_rtp_max_ppm: "全局 RTP 上限", user_24h_rtp_ppm: "命中前用户 24 小时 RTP", user_48h_rtp_ppm: "命中前用户 48 小时 RTP", user_48h_rtp_max_ppm: "用户 48 小时 RTP 上限", user_round_rtp_ppm: "命中前用户本轮 RTP", user_round_rtp_max_ppm: "用户本轮 RTP 上限", daily_hit_count: "当日已中大奖次数", daily_hit_limit: "单日大奖次数上限", cumulative_spend_coins: "累计消费金币", cumulative_spend_threshold_coins: "累计消费门槛", pending_token_count: "命中前待兑大奖机会", multiplier_ppm: "命中倍率", stage: "命中阶段", eligible: "是否满足条件", allowed: "是否允许发放", }; return labels[normalized] || humanizeConditionKey(normalized); } function conditionValue(key, value) { if (value && typeof value === "object" && !Array.isArray(value)) { const parts = []; const hasFriendlyValues = Boolean(value.actual || value.threshold); if (hasFriendlyValues) { parts.push( [value.actual ? `实际 ${value.actual}` : "", operatorLabel(value.operator), value.threshold || ""] .filter(Boolean) .join(" "), ); } if (!hasFriendlyValues && value.denominator > 0) { parts.push(`${formatNumber(value.numerator)} / ${formatNumber(value.denominator)}`); } if (!hasFriendlyValues && (value.ratioPpm || value.limitPpm)) { parts.push(`实际 ${formatPercentFromPPM(value.ratioPpm)} / 门槛 ${formatPercentFromPPM(value.limitPpm)}`); } if (value.factorPpm) { parts.push(`权重 ${formatPercentFromPPM(value.factorPpm)}`); } parts.push(value.passed ? "通过" : "未通过"); if (value.reason) { parts.push(value.reason); } return parts.join(" · "); } const normalized = String(key).toLowerCase(); if (typeof value === "boolean") { return value ? "是" : "否"; } if (value === null || value === undefined || value === "") { return "-"; } if (normalized.endsWith("_ppm") || normalized.includes("rtp_ppm")) { return formatPercentFromPPM(value); } if (normalized.endsWith("_coins") || normalized.includes("count") || normalized.includes("limit")) { return formatNumber(value); } return String(value); } function operatorLabel(value) { const labels = { eq: "等于", gt: "高于", gte: "不低于", lt: "低于", lte: "不高于", }; const normalized = String(value || "") .trim() .toLowerCase(); return labels[normalized] || value || ""; } function humanizeConditionKey(key) { return String(key) .replaceAll("_", " ") .replace(/\bRtp\b/gi, "RTP") .replace(/\bPpm\b/gi, "百分比") .replace(/\bCoins\b/gi, "金币"); } function stageLabel(stage) { const normalized = String(stage || "").toLowerCase(); if (["novice", "newbie", "new"].includes(normalized)) { return "新手阶段"; } if (["normal", "ordinary"].includes(normalized)) { return "普通阶段"; } if (["advanced", "high", "high_value", "premium"].includes(normalized)) { return "高价阶段"; } return stage || "未记录"; }