mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-01 08:28:34 +00:00
feat(proxies): 代理有效期与失败回退
- schema/迁移: 代理有效期、提醒天数、失败回退配置 + 账号 fallback 来源字段 - service/repo/DTO/handler: CRUD 透传新字段 + 校验 - fallback 目标解析纯函数(链式解析 + 环检测 + 兜底) - SweepExpiredProxies 到期改投账号 + outbox 失效 - ProxyExpiryService 后台到期扫描任务 + wire 注册 - 账号侧手动回切原代理 + fallback 来源徽章/按钮 - 前端: 创建/编辑表单、列表到期徽章、类型/API/i18n - ops 告警: proxy_expired_count / proxy_expiring_soon_count 指标 - 导入导出携带有效期/回退字段(备用按 name 映射) - 补全测试 stub + 集成测试 + review 问题修复
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { daysUntil, proxyExpiryBadgeClass, proxyExpiryLabelKey } from '../proxyExpiry'
|
||||
|
||||
// 固定「现在」,按天数构造确定输入:isoInDays(n) 距今正好 n 天
|
||||
const NOW = new Date('2026-06-02T00:00:00Z')
|
||||
const isoInDays = (n: number): string => new Date(NOW.getTime() + n * 86400000).toISOString()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(NOW)
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('daysUntil', () => {
|
||||
it('返回距今整天数', () => {
|
||||
expect(daysUntil(isoInDays(10))).toBe(10)
|
||||
expect(daysUntil(isoInDays(-3))).toBe(-3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('proxyExpiryBadgeClass', () => {
|
||||
it('status=expired → danger', () => {
|
||||
expect(proxyExpiryBadgeClass(isoInDays(30), 'expired')).toBe('badge badge-danger')
|
||||
})
|
||||
it('≤3 天 → danger(含边界 3)', () => {
|
||||
expect(proxyExpiryBadgeClass(isoInDays(2), 'active')).toBe('badge badge-danger')
|
||||
expect(proxyExpiryBadgeClass(isoInDays(3), 'active')).toBe('badge badge-danger')
|
||||
})
|
||||
it('4–7 天 → warning(含边界 7)', () => {
|
||||
expect(proxyExpiryBadgeClass(isoInDays(5), 'active')).toBe('badge badge-warning')
|
||||
expect(proxyExpiryBadgeClass(isoInDays(7), 'active')).toBe('badge badge-warning')
|
||||
})
|
||||
it('>7 天 → gray', () => {
|
||||
expect(proxyExpiryBadgeClass(isoInDays(30), 'active')).toBe('text-gray-500')
|
||||
})
|
||||
})
|
||||
|
||||
describe('proxyExpiryLabelKey', () => {
|
||||
it('status=expired → expired key', () => {
|
||||
expect(proxyExpiryLabelKey(isoInDays(30), 'expired')).toEqual({ key: 'admin.proxies.expired' })
|
||||
})
|
||||
it('已逾期(d<0) → overdueDays', () => {
|
||||
expect(proxyExpiryLabelKey(isoInDays(-3), 'active')).toEqual({
|
||||
key: 'admin.proxies.overdueDays',
|
||||
params: { days: 3 },
|
||||
})
|
||||
})
|
||||
it('≤7 天 → expiringInDays', () => {
|
||||
expect(proxyExpiryLabelKey(isoInDays(5), 'active')).toEqual({
|
||||
key: 'admin.proxies.expiringInDays',
|
||||
params: { days: 5 },
|
||||
})
|
||||
})
|
||||
it('>7 天 → remainingDays', () => {
|
||||
expect(proxyExpiryLabelKey(isoInDays(30), 'active')).toEqual({
|
||||
key: 'admin.proxies.remainingDays',
|
||||
params: { days: 30 },
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
// 代理有效期展示逻辑(ProxiesView 与 AccountsView 共用)。
|
||||
// 到期紧迫度固定两档:剩余 ≤3 天红、≤7 天黄(不读 per-proxy expiry_warn_days)。
|
||||
export const EXPIRY_WARN_DAYS = 7
|
||||
export const EXPIRY_DANGER_DAYS = 3
|
||||
|
||||
// 距今整天数(向上取整)。
|
||||
export const daysUntil = (iso: string): number =>
|
||||
Math.ceil((new Date(iso).getTime() - Date.now()) / 86400000)
|
||||
|
||||
// 倒计时徽章的 CSS class(纯函数,无 i18n 依赖)。
|
||||
export function proxyExpiryBadgeClass(expiresAt: string | null, status?: string): string {
|
||||
if (status === 'expired') return 'badge badge-danger'
|
||||
const d = expiresAt ? daysUntil(expiresAt) : Infinity
|
||||
if (d <= EXPIRY_DANGER_DAYS) return 'badge badge-danger'
|
||||
if (d <= EXPIRY_WARN_DAYS) return 'badge badge-warning'
|
||||
return 'text-gray-500'
|
||||
}
|
||||
|
||||
// 倒计时文案的 i18n key + 参数(返回 key 而非已翻译文本,便于单测且不耦合 i18n)。
|
||||
export function proxyExpiryLabelKey(
|
||||
expiresAt: string | null,
|
||||
status?: string,
|
||||
): { key: string; params?: { days: number } } {
|
||||
if (status === 'expired') return { key: 'admin.proxies.expired' }
|
||||
const d = expiresAt ? daysUntil(expiresAt) : Infinity
|
||||
if (d < 0) return { key: 'admin.proxies.overdueDays', params: { days: Math.abs(d) } }
|
||||
if (d <= EXPIRY_WARN_DAYS) return { key: 'admin.proxies.expiringInDays', params: { days: d } }
|
||||
return { key: 'admin.proxies.remainingDays', params: { days: d } }
|
||||
}
|
||||
Reference in New Issue
Block a user