feat(alert): A2 告警引擎分布式安全 — Redis 分布式锁(SetNX)

- Cache 接口新增 SetNX 方法(go-redis 原子 / redka check-then-set 单副本安全)
- AlertEngine 改造为支持分布式锁: tryAcquireLock/releaseLock/GetEngineStatus
- 多副本部署时仅 Leader 执行告警检查, 60s TTL 防死锁, fail-open 容错
- 新增 GET /alerts/engine/status 监控端点(is_leader/instance_id/lock_holder)
- 修复 GrafanaDashboard.tsx Panel.options 类型不匹配导致构建中断
- 同步未提交的前端类型安全迁移(140+ 文件)
- ROADMAP/TDD_PLAN/PROJECT_EVALUATION 三文档同步更新

验证: go build 零错误, Docker 部署 healthy, 引擎状态端点正常,
锁竞争测试通过(非 Leader 跳过检查), 5/5 核心 API 回归通过
This commit is contained in:
attacker committed 2026-06-26 21:13:15 +08:00
1 parent 5ca0e6d83a
commit c68aef4444
150 files changed
+1038 -690

No files matched your search

+18 -17
View File
@@ -28,29 +28,30 @@ import { formatCurrency, TrendChart, providerLabels, providerColors } from './Cl
import { StatStrip } from '@/components/ui/stat-strip';
import { BillingRecordDialog } from './BillingRecordDialog';
import { BillingRuleDialog } from './BillingRuleDialog';
import type { JsonData } from '@/types/common';
export function CloudBilling() {
const confirm = useConfirm();
const [activeTab, setActiveTab] = useState<'overview' | 'records' | 'alerts'>('overview');
const [stats, setStats] = useState<any>(null);
const [stats, setStats] = useState<JsonData>(null);
const [statsLoading, setStatsLoading] = useState(true);
const [records, setRecords] = useState<any[]>([]);
const [records, setRecords] = useState<JsonData[]>([]);
const [recordsLoading, setRecordsLoading] = useState(true);
const [filterProvider, setFilterProvider] = useState('');
const [filterStart, setFilterStart] = useState('');
const [filterEnd, setFilterEnd] = useState('');
const [rules, setRules] = useState<any[]>([]);
const [rules, setRules] = useState<JsonData[]>([]);
const [rulesLoading, setRulesLoading] = useState(true);
// 弹窗状态(open + 编辑目标) // Logan.Li
const [recordDialogOpen, setRecordDialogOpen] = useState(false);
const [editingRecord, setEditingRecord] = useState<any>(null);
const [editingRecord, setEditingRecord] = useState<JsonData>(null);
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
const [editingRule, setEditingRule] = useState<any>(null);
const [editingRule, setEditingRule] = useState<JsonData>(null);
const loadStats = useCallback(async () => {
setStatsLoading(true);
@@ -58,7 +59,7 @@ export function CloudBilling() {
const res = await billingApi.getStats();
setStats(res);
} catch (e) {
toast.error('加载统计失败', { description: (e as Record<string, any>).message });
toast.error('加载统计失败', { description: (e as JsonData).message });
} finally {
setStatsLoading(false);
}
@@ -67,14 +68,14 @@ export function CloudBilling() {
const loadRecords = useCallback(async () => {
setRecordsLoading(true);
try {
const params: Record<string, any> = {};
const params: JsonData = {};
if (filterProvider) params.provider = filterProvider;
if (filterStart) params.start_date = filterStart;
if (filterEnd) params.end_date = filterEnd;
const res = await billingApi.list(params);
setRecords(res.list || []);
} catch (e) {
toast.error('加载账单失败', { description: (e as Record<string, any>).message });
toast.error('加载账单失败', { description: (e as JsonData).message });
} finally {
setRecordsLoading(false);
}
@@ -86,7 +87,7 @@ export function CloudBilling() {
const res = await billingApi.listAlertRules();
setRules(res.list || []);
} catch (e) {
toast.error('加载预警规则失败', { description: (e as Record<string, any>).message });
toast.error('加载预警规则失败', { description: (e as JsonData).message });
} finally {
setRulesLoading(false);
}
@@ -109,7 +110,7 @@ export function CloudBilling() {
setRecordDialogOpen(true);
};
const openEditRecord = ( r: any) => {
const openEditRecord = (r: JsonData) => {
setEditingRecord(r);
setRecordDialogOpen(true);
};
@@ -119,7 +120,7 @@ export function CloudBilling() {
setRuleDialogOpen(true);
};
const openEditRule = ( r: any) => {
const openEditRule = (r: JsonData) => {
setEditingRule(r);
setRuleDialogOpen(true);
};
@@ -133,7 +134,7 @@ export function CloudBilling() {
loadStats();
toast('删除成功');
} catch (e) {
toast.error('删除失败', { description: (e as Record<string, any>).message });
toast.error('删除失败', { description: (e as JsonData).message });
}
};
@@ -145,7 +146,7 @@ export function CloudBilling() {
loadRules();
toast('删除成功');
} catch (e) {
toast.error('删除失败', { description: (e as Record<string, any>).message });
toast.error('删除失败', { description: (e as JsonData).message });
}
};
@@ -155,7 +156,7 @@ export function CloudBilling() {
loadRules();
loadStats();
} catch (e) {
toast.error('操作失败', { description: (e as Record<string, any>).message });
toast.error('操作失败', { description: (e as JsonData).message });
}
};
@@ -222,7 +223,7 @@ export function CloudBilling() {
<span className="text-sm font-medium text-amber-300">成本预警</span>
</div>
<div className="space-y-2">
{stats.alerts.map(( a: any, i: number) => (
{stats.alerts.map((a: JsonData, i: number) => (
<div key={i} className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<Badge variant="outline" className="border-amber-500/50 text-amber-300">{a.rule_name}</Badge>
@@ -256,7 +257,7 @@ export function CloudBilling() {
) : (
<div className="space-y-3">
<div className="flex h-8 rounded-lg overflow-hidden">
{stats.provider_dist.map(( p: any, i: number) => (
{stats.provider_dist.map((p: JsonData, i: number) => (
<div
key={i}
className={`${providerColors[p.provider] || 'bg-slate-600'} flex items-center justify-center text-xs text-white font-medium`}
@@ -268,7 +269,7 @@ export function CloudBilling() {
))}
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{stats.provider_dist.map(( p: any, i: number) => (
{stats.provider_dist.map((p: JsonData, i: number) => (
<div key={i} className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<div className={`w-3 h-3 rounded ${providerColors[p.provider] || 'bg-slate-600'}`} />