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:
1 parent
5ca0e6d83a
commit
c68aef4444
150 files changed
+1038
-690
No files matched your search
@@ -22,6 +22,7 @@ import { Loader2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { billingApi } from '@/api';
|
||||
import { providerLabels } from './CloudBillingHelpers';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
const emptyRecord = {
|
||||
provider: 'aliyun',
|
||||
@@ -36,12 +37,12 @@ const emptyRecord = {
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editingRecord: any | null;
|
||||
editingRecord: JsonData | null;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export function BillingRecordDialog({ open, onOpenChange, editingRecord, onSuccess }: Props) {
|
||||
const [form, setForm] = useState<any>(emptyRecord);
|
||||
const [form, setForm] = useState<JsonData>(emptyRecord);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// 弹窗打开时初始化表单(新建预填今日日期,编辑回填数据) // Logan.Li
|
||||
@@ -80,7 +81,7 @@ export function BillingRecordDialog({ open, onOpenChange, editingRecord, onSucce
|
||||
onSuccess();
|
||||
toast('保存成功');
|
||||
} catch (e) {
|
||||
toast.error('保存失败', { description: (e as Record<string, any>).message });
|
||||
toast.error('保存失败', { description: (e as JsonData).message });
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
@@ -23,18 +23,19 @@ import { Loader2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { billingApi } from '@/api';
|
||||
import { providerLabels } from './CloudBillingHelpers';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
const emptyRule = { name: '', provider: '', metric: 'daily', threshold: '', operator: '>', enabled: true };
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editingRule: any | null;
|
||||
editingRule: JsonData | null;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export function BillingRuleDialog({ open, onOpenChange, editingRule, onSuccess }: Props) {
|
||||
const [form, setForm] = useState<any>(emptyRule);
|
||||
const [form, setForm] = useState<JsonData>(emptyRule);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// 弹窗打开时初始化表单 // Logan.Li
|
||||
@@ -71,7 +72,7 @@ export function BillingRuleDialog({ open, onOpenChange, editingRule, onSuccess }
|
||||
onSuccess();
|
||||
toast('保存成功');
|
||||
} catch (e) {
|
||||
toast.error('保存失败', { description: (e as Record<string, any>).message });
|
||||
toast.error('保存失败', { description: (e as JsonData).message });
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { cloudApi } from '@/api';
|
||||
import { CloudBrandIcon, getProviderColor } from '@/components/icons/CloudBrandIcon';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
const providerLabels: Record<string, string> = {
|
||||
aws: 'AWS',
|
||||
@@ -37,7 +38,7 @@ const providerLabels: Record<string, string> = {
|
||||
};
|
||||
|
||||
export function CloudAccountManager() {
|
||||
const [accounts, setAccounts] = useState<any[]>([]);
|
||||
const [accounts, setAccounts] = useState<JsonData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>('all');
|
||||
@@ -45,7 +46,7 @@ export function CloudAccountManager() {
|
||||
// 创建/编辑账号表单状态
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [editingAccount, setEditingAccount] = useState<any>(null);
|
||||
const [editingAccount, setEditingAccount] = useState<JsonData>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
provider: 'aliyun',
|
||||
@@ -72,7 +73,7 @@ export function CloudAccountManager() {
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
const handleOpenEdit = ( account: any) => {
|
||||
const handleOpenEdit = (account: JsonData) => {
|
||||
setEditingAccount(account);
|
||||
setFormData({
|
||||
name: account.name || '',
|
||||
@@ -89,7 +90,7 @@ export function CloudAccountManager() {
|
||||
if (!formData.name.trim() || !formData.provider.trim()) return;
|
||||
try {
|
||||
setSubmitting(true);
|
||||
const payload: Record<string, any> = {
|
||||
const payload: JsonData = {
|
||||
name: formData.name.trim(),
|
||||
provider: formData.provider,
|
||||
access_key: formData.access_key.trim(),
|
||||
@@ -114,7 +115,7 @@ export function CloudAccountManager() {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res: Record<string, any> = await cloudApi.getAccounts();
|
||||
const res: JsonData = await cloudApi.getAccounts();
|
||||
setAccounts(res?.list || []);
|
||||
} catch (e) {
|
||||
console.error('加载云账号列表失败:', e);
|
||||
|
||||
@@ -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'}`} />
|
||||
|
||||
@@ -25,9 +25,10 @@ import {
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { cloudApi } from '@/api';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
export function DomainManager() {
|
||||
const [domains, setDomains] = useState<any[]>([]);
|
||||
const [domains, setDomains] = useState<JsonData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedStatus, setSelectedStatus] = useState<string>('all');
|
||||
@@ -35,7 +36,7 @@ export function DomainManager() {
|
||||
// 创建/编辑域名表单状态
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [editingDomain, setEditingDomain] = useState<any>(null);
|
||||
const [editingDomain, setEditingDomain] = useState<JsonData>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
registrar: '',
|
||||
@@ -66,7 +67,7 @@ export function DomainManager() {
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
const handleOpenEdit = ( domain: any) => {
|
||||
const handleOpenEdit = (domain: JsonData) => {
|
||||
setEditingDomain(domain);
|
||||
// 后端返回 RFC3339 时间,截取 YYYY-MM-DD 部分给 date input
|
||||
const toDateInput = (v: string) => (v ? v.substring(0, 10) : '');
|
||||
@@ -87,7 +88,7 @@ export function DomainManager() {
|
||||
if (!formData.name.trim()) return;
|
||||
try {
|
||||
setSubmitting(true);
|
||||
const payload: Record<string, any> = {
|
||||
const payload: JsonData = {
|
||||
name: formData.name.trim(),
|
||||
registrar: formData.registrar.trim(),
|
||||
status: formData.status,
|
||||
@@ -114,7 +115,7 @@ export function DomainManager() {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res: Record<string, any> = await cloudApi.getDomains();
|
||||
const res: JsonData = await cloudApi.getDomains();
|
||||
setDomains(res?.list || []);
|
||||
} catch (e) {
|
||||
console.error('加载域名列表失败:', e);
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { platformApi } from '@/api';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
public: '公有云',
|
||||
@@ -44,12 +45,12 @@ const typeColors: Record<string, string> = {
|
||||
};
|
||||
|
||||
export function PlatformManager() {
|
||||
const [platforms, setPlatforms] = useState<any[]>([]);
|
||||
const [platforms, setPlatforms] = useState<JsonData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedType, setSelectedType] = useState('all');
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState<any>(null);
|
||||
const [editingItem, setEditingItem] = useState<JsonData>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '', code: '', type: 'public', status: 'active',
|
||||
@@ -60,7 +61,7 @@ export function PlatformManager() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await platformApi.list();
|
||||
const items = (res as Record<string, any>)?.list || [];
|
||||
const items = (res as JsonData)?.list || [];
|
||||
setPlatforms(items);
|
||||
} catch {
|
||||
setPlatforms([]);
|
||||
@@ -84,7 +85,7 @@ export function PlatformManager() {
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const openEdit = ( item: any) => {
|
||||
const openEdit = (item: JsonData) => {
|
||||
setEditingItem(item);
|
||||
setFormData({
|
||||
name: item.name || '', code: item.code || '', type: item.type || 'public',
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
import { regionApi } from '@/api';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { toast } from 'sonner';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
const providerNames: Record<string, string> = {
|
||||
aws: 'AWS',
|
||||
@@ -59,13 +60,13 @@ const providerColors: Record<string, string> = {
|
||||
|
||||
export function RegionManager() {
|
||||
const confirm = useConfirm();
|
||||
const [regions, setRegions] = useState<any[]>([]);
|
||||
const [regions, setRegions] = useState<JsonData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedProvider, setSelectedProvider] = useState('all');
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState<any>(null);
|
||||
const [editingItem, setEditingItem] = useState<JsonData>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '', code: '', provider: 'aliyun', location: '',
|
||||
@@ -76,7 +77,7 @@ export function RegionManager() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await regionApi.list();
|
||||
const items = (res as Record<string, any>)?.list || [];
|
||||
const items = (res as JsonData)?.list || [];
|
||||
setRegions(items);
|
||||
} catch {
|
||||
setRegions([]);
|
||||
@@ -113,7 +114,7 @@ export function RegionManager() {
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const openEdit = ( item: any) => {
|
||||
const openEdit = (item: JsonData) => {
|
||||
setEditingItem(item);
|
||||
setFormData({
|
||||
name: item.name || '', code: item.code || '', provider: item.provider || 'aliyun',
|
||||
|
||||
Reference in new issue
Block a user