fix: Rock Token 超时按满 2/3 天判定并统一显示口径

This commit is contained in:
2026-11-10 17:25:59 +08:00
parent 162745082b
commit 227d4b93ad
9 changed files with 190 additions and 23 deletions
+6
View File
@@ -675,6 +675,12 @@ export interface RockTokenItem {
created_at: string;
updated_at?: string;
deleted_at?: string | null;
// 后端按与筛选/统计一致的阈值计算,前端标签直接使用,避免本地时间造成边界不一致。
bound_overdue?: boolean;
using_overdue?: boolean;
using_danger?: boolean;
bound_days?: number;
using_days?: number;
}
export interface RockPurchaseResult {
+21 -1
View File
@@ -103,6 +103,12 @@ export default function PlatformRock() {
}
}, [form])
// 只刷新统计,不触碰配置表单,供定时刷新和列表刷新共用。
const loadStats = useCallback(async () => {
const st = await rockApi.getStats()
setStats(st)
}, [])
const loadTokens = useCallback(async () => {
setLoadingTokens(true)
try {
@@ -129,6 +135,20 @@ export default function PlatformRock() {
loadTokens()
}, [loadTokens])
// 定时刷新列表与统计,保证跨过 2 天/3 天阈值后标签和徽标自动更新。
const refreshTokensAndStats = useCallback(() => {
void loadStats().catch(() => {})
void loadTokens()
}, [loadStats, loadTokens])
useEffect(() => {
const timer = window.setInterval(() => {
if (document.visibilityState !== 'visible') return
refreshTokensAndStats()
}, 60000)
return () => window.clearInterval(timer)
}, [refreshTokensAndStats])
useEffect(() => {
setSelectedTokenIds([])
setSelectedTokens([])
@@ -480,7 +500,7 @@ export default function PlatformRock() {
togglingAutoRefresh={togglingAutoRefresh}
onSearchKeywordChange={setSearchKeyword}
onFilterStatusChange={handleFilterStatusChange}
onRefresh={loadTokens}
onRefresh={refreshTokensAndStats}
onRefreshStale={handleRefreshStale}
onToggleAutoRefreshMismatch={handleToggleAutoRefreshMismatch}
/>
@@ -29,9 +29,15 @@ function elapsedDaysSince(value?: string): number | null {
return diff > 0 ? diff / MS_PER_DAY : 0
}
// 使用中超时起点:using_at。
function usingElapsedDays(usingAt?: string): number | null {
return elapsedDaysSince(usingAt)
// 展示用天数:向下取整到 1 位,避免 1.99 天被四舍五入成 2.0 天而误以为已超时。
function floorDays(days: number): number {
return Math.floor(days * 10) / 10
}
// 优先使用后端返回的服务端天数,旧接口没有该字段时回退到本地计算。
function serverElapsedDays(serverDays: number | undefined, at?: string): number | null {
if (typeof serverDays === 'number') return serverDays
return elapsedDaysSince(at)
}
// Token 主信息:编号/兑换码 与 包ID/提取码 上下两行展示,
@@ -155,21 +161,24 @@ export function buildTokenColumns({
</Tag>
)
case 'using': {
const days = usingElapsedDays(record.using_at)
if (days !== null && days >= usingDangerDays) {
const days = serverElapsedDays(record.using_days, record.using_at)
const usingOverdue = record.using_overdue ?? (days !== null && days >= usingWarnDays)
const usingDanger = record.using_danger ?? (days !== null && days >= usingDangerDays)
const displayDays = days === null ? 0 : floorDays(days)
if (usingDanger) {
return (
<Tag
color="error"
style={{ fontWeight: 600, padding: '2px 8px', border: '1px solid #ff4d4f' }}
>
⚠️ 使用中超时 {days.toFixed(1)} 天
⚠️ 使用中超时 {displayDays.toFixed(1)} 天
</Tag>
)
}
if (days !== null && days >= usingWarnDays) {
if (usingOverdue) {
return (
<Tag color="warning" style={{ fontWeight: 600, padding: '2px 8px' }}>
● 扫码使用中 · {days.toFixed(1)} 天
● 扫码使用中 · {displayDays.toFixed(1)} 天
</Tag>
)
}
@@ -180,14 +189,16 @@ export function buildTokenColumns({
)
}
case 'bound': {
const boundDays = elapsedDaysSince(record.bound_at)
if (boundDays !== null && boundDays >= boundWarnDays) {
const boundDays = serverElapsedDays(record.bound_days, record.bound_at)
const boundOverdue = record.bound_overdue ?? (boundDays !== null && boundDays >= boundWarnDays)
if (boundOverdue) {
const displayDays = boundDays === null ? 0 : floorDays(boundDays)
return (
<Tag
color="volcano"
style={{ fontWeight: 600, padding: '2px 8px', border: '1px solid #ff7a45' }}
>
⏱ 绑定超时 {boundDays.toFixed(1)} 天
⏱ 绑定超时 {displayDays.toFixed(1)} 天
</Tag>
)
}
@@ -233,14 +244,17 @@ export function buildTokenColumns({
title: '流转时间',
width: 180,
render: (_, record) => {
const usingDays = record.status === 'using' ? usingElapsedDays(record.using_at) : null
const boundDays = record.status === 'bound' ? elapsedDaysSince(record.bound_at) : null
const usingDays = record.status === 'using' ? serverElapsedDays(record.using_days, record.using_at) : null
const boundDays = record.status === 'bound' ? serverElapsedDays(record.bound_days, record.bound_at) : null
const usingOverdue = record.using_overdue ?? (usingDays !== null && usingDays >= usingWarnDays)
const usingDanger = record.using_danger ?? (usingDays !== null && usingDays >= usingDangerDays)
const boundOverdue = record.bound_overdue ?? (boundDays !== null && boundDays >= boundWarnDays)
const usingColor =
usingDays === null
? '#64748b'
: usingDays >= usingDangerDays
: usingDanger
? '#dc2626'
: usingDays >= usingWarnDays
: usingOverdue
? '#d97706'
: '#0891b2'
return (
@@ -253,7 +267,7 @@ export function buildTokenColumns({
<div style={{ fontSize: 11, color: usingColor }}>
<span>⏳ 使用中: </span>
{formatDateTime(record.using_at)}
{usingDays !== null ? `(${usingDays.toFixed(1)} 天)` : ''}
{usingDays !== null ? `(${floorDays(usingDays).toFixed(1)} 天)` : ''}
</div>
)}
{record.mismatch_at && (
@@ -269,10 +283,10 @@ export function buildTokenColumns({
</div>
)}
{!record.redeemed_at && record.bound_at && (
<div style={{ fontSize: 11, color: boundDays !== null && boundDays >= boundWarnDays ? '#ea580c' : '#0284c7' }}>
<div style={{ fontSize: 11, color: boundOverdue ? '#ea580c' : '#0284c7' }}>
<span>🔗 绑定: </span>
{formatDateTime(record.bound_at)}
{boundDays !== null ? `(${boundDays.toFixed(1)} 天)` : ''}
{boundDays !== null ? `(${floorDays(boundDays).toFixed(1)} 天)` : ''}
</div>
)}
{!record.bound_at && !record.mismatch_at && record.claimed_at && (