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 ? (
倍率
未中奖
普通命中
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 ? (
<>
{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 (
{Array.from({ length: 6 }, (_, index) => (
))}
);
}
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 || "未记录";
}