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

@@ -19,6 +19,7 @@ import {
import { credentialApi } from '@/api';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { toast } from 'sonner';
import type { JsonData } from '@/types/common';
type CredentialType = 'ssh_key' | 'api_token' | 'password' | 'access_key';
@@ -56,7 +57,7 @@ export function CredentialManager() {
const fetchList = useCallback(async () => {
try {
setLoading(true);
const res: Record<string, any> = await credentialApi.list();
const res: JsonData = await credentialApi.list();
setCredentials(res?.list || []);
} catch (e) {
console.error('加载凭据列表失败:', e);
@@ -18,15 +18,16 @@ import {
} from '@/components/ui/dialog';
import { Upload, Download, CheckCircle, XCircle } from 'lucide-react';
import { userApi } from '@/api';
import type { JsonData } from '@/types/common';
interface CsvImportDialogProps {
importDialogOpen: 'csv' | 'ldap' | null;
setImportDialogOpen: (v: 'csv' | 'ldap' | null) => void;
importPreview: any[];
setImportPreview: ( p: any[]) => void;
importPreview: JsonData[];
setImportPreview: ( p: JsonData[]) => void;
importing: boolean;
importResult: any;
setImportResult: ( r: any) => void;
importResult?: JsonData;
setImportResult: (r: JsonData) => void;
handleCsvUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleBatchImport: () => void;
}
@@ -17,23 +17,24 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { Network, Loader2, RefreshCw, CheckCircle, XCircle } from 'lucide-react';
import type { JsonData } from '@/types/common';
interface LdapImportDialogProps {
importDialogOpen: 'csv' | 'ldap' | null;
setImportDialogOpen: (v: 'csv' | 'ldap' | null) => void;
ldapUsers: any[];
setLdapUsers: ( u: any[]) => void;
ldapUsers: JsonData[];
setLdapUsers: ( u: JsonData[]) => void;
ldapSelected: Set<string>;
setLdapSelected: (s: Set<string>) => void;
ldapLoading: boolean;
ldapImportOUs: any[];
ldapImportOUs: JsonData[];
ldapImportOUDN: string;
setLdapImportOUDN: (v: string) => void;
loadLdapUsers: (ouDn?: string) => void;
handleLdapImport: () => void;
importing: boolean;
importResult: any;
setImportResult: ( r: any) => void;
importResult?: JsonData;
setImportResult: (r: JsonData) => void;
}
// LdapImportDialog 展示 LDAP 用户检索和多选导入流程 // Logan.Li
@@ -36,15 +36,16 @@ import {
Trash2,
Loader2,
} from 'lucide-react';
import type { JsonData } from '@/types/common';
interface LdapManagementSectionProps {
ldapConfig: any;
ldapConfig?: JsonData;
ldapMgmtOpen: boolean;
setLdapMgmtOpen: (v: boolean) => void;
ldapOUs: any[];
ldapOUs: JsonData[];
selectedOUDN: string;
setSelectedOUDN: (v: string) => void;
ldapUsers: any[];
ldapUsers: JsonData[];
selectedLDAPUsers: string[];
setSelectedLDAPUsers: (v: string[]) => void;
loadLdapOUs: () => void;
@@ -75,16 +76,16 @@ interface LdapManagementSectionProps {
ldapTestPass: string;
setLdapTestPass: (v: string) => void;
ldapUserTesting: boolean;
ldapUserTestResult: any;
ldapUserTestResult?: JsonData;
handleLdapUserTest: () => void;
// Edit LDAP user
editLdapUser: any | null;
setEditLdapUser: (u: any | null) => void;
editLdapUser: JsonData | null;
setEditLdapUser: (u: JsonData | null) => void;
editLdapForm: { cn: string; mail: string; phone: string; title: string };
setEditLdapForm: React.Dispatch<React.SetStateAction<{ cn: string; mail: string; phone: string; title: string }>>;
handleUpdateLdapUser: () => void;
handleDeleteLDAPEntry: (dn: string, silent?: boolean) => void;
openEditLdapUser: ( user: any) => void;
openEditLdapUser: (user: JsonData) => void;
}
export function LdapManagementSection(props: LdapManagementSectionProps) {
@@ -219,7 +220,7 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
<SelectItem value={ldapConfig.base_dn}>根 ({ldapConfig.base_dn})</SelectItem>
{ldapOUs.map(( ou: any) => (
{ldapOUs.map((ou: JsonData) => (
<SelectItem key={ou.dn} value={ou.dn}>
{ou.description ? `${ou.name} (${ou.description})` : ou.name}
</SelectItem>
@@ -343,7 +344,7 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
type="checkbox"
className="rounded bg-slate-800 border-slate-600"
checked={selectedLDAPUsers.length > 0 && selectedLDAPUsers.length === ldapUsers.length}
onChange={(e) => setSelectedLDAPUsers(e.target.checked ? ldapUsers.map(( u: any) => u.dn) : [])}
onChange={(e) => setSelectedLDAPUsers(e.target.checked ? ldapUsers.map((u: JsonData) => u.dn) : [])}
/>
</TableHead>
<TableHead className="text-slate-400">用户名</TableHead>
@@ -361,7 +362,7 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
</TableCell>
</TableRow>
) : (
ldapUsers.map(( user: any) => (
ldapUsers.map((user: JsonData) => (
<TableRow key={user.dn} className="border-slate-800 hover:bg-slate-800/50">
<TableCell>
<input
@@ -7,6 +7,7 @@ import {
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
} from '@/components/ui/dialog';
import { Shield } from 'lucide-react';
import type { JsonData } from '@/types/common';
export interface RoleFormData {
name: string;
@@ -25,7 +26,7 @@ export interface PermissionOption {
interface RoleFormDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
editingRole: any | null;
editingRole: JsonData | null;
form: RoleFormData;
setForm: React.Dispatch<React.SetStateAction<RoleFormData>>;
saving: boolean;
@@ -14,6 +14,7 @@ import {
} from '@/components/ui/table';
import { auditApi } from '@/api';
import type { OperationLog } from '@/types';
import type { JsonData } from '@/types/common';
interface SystemLogsTabProps {
searchQuery: string;
@@ -21,7 +22,7 @@ interface SystemLogsTabProps {
}
// 将后端审计日志规范化为前端 OperationLog 类型 // Logan.Li
function normalizeLog( raw: any): OperationLog {
function normalizeLog(raw: JsonData): OperationLog {
return {
id: String(raw.id ?? ''),
userId: String(raw.user_id ?? raw.userId ?? ''),
@@ -11,9 +11,10 @@ import { SystemRolesTab } from './SystemRolesTab';
import { SystemSettingsTab } from './SystemSettingsTab';
import { useLdapManagement } from './useLdapManagement';
import type { UserFormData } from './UserFormDialog';
import type { JsonData } from '@/types/common';
// 后端用户数据转换为前端兼容格式
function mapUser( u: any) {
function mapUser(u: JsonData) {
return {
...u,
status: u.status === 1 || u.status === 'active' ? ('active' as const) : ('inactive' as const),
@@ -53,21 +54,21 @@ export function SystemManager() {
}
}, [location.pathname]);
const [searchQuery, setSearchQuery] = useState('');
const [users, setUsers] = useState<any[]>([]);
const [users, setUsers] = useState<JsonData[]>([]);
const [loading, setLoading] = useState(true);
const [selectedUser, setSelectedUser] = useState<any | null>(null);
const [selectedUser, setSelectedUser] = useState<JsonData | null>(null);
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
// 创建/编辑用户弹窗
const [dialogOpen, setDialogOpen] = useState(false);
const [editingUser, setEditingUser] = useState<any | null>(null);
const [editingUser, setEditingUser] = useState<JsonData | null>(null);
const [saving, setSaving] = useState(false);
const [form, setForm] = useState<UserFormData>({ ...emptyForm });
// CSV 批量导入弹窗
const [importDialogOpen, setImportDialogOpen] = useState<'csv' | 'ldap' | null>(null);
const [importPreview, setImportPreview] = useState<any[]>([]);
const [importResult, setImportResult] = useState<any>(null);
const [importPreview, setImportPreview] = useState<JsonData[]>([]);
const [importResult, setImportResult] = useState<JsonData>(null);
const loadUsers = async () => {
setLoading(true);
@@ -125,7 +126,7 @@ export function SystemManager() {
setDialogOpen(true);
};
const openEdit = ( user: any) => {
const openEdit = (user: JsonData) => {
setEditingUser(user);
setForm({
username: user.username || '',
@@ -167,7 +168,7 @@ export function SystemManager() {
}
};
const handleDelete = async ( user: any) => {
const handleDelete = async (user: JsonData) => {
const ok = await confirm({ title: '删除用户', description: `确认删除用户「${user.nickname || user.username}」吗?`, confirmText: '删除', variant: 'destructive' });
if (!ok) return;
try {
@@ -11,6 +11,7 @@ import {
} from '@/components/ui/select';
import { Lock, History, Key, Settings, Network, RefreshCw, Loader2 } from 'lucide-react';
import { LdapManagementSection } from './LdapManagementSection';
import type { JsonData } from '@/types/common';
export interface LdapFormData {
ldap_host: string;
@@ -67,13 +68,13 @@ interface SystemSettingsTabProps {
ldapUserTesting: boolean;
ldapUserTestResult: unknown;
handleLdapUserTest: () => void;
editLdapUser: any | null;
setEditLdapUser: (u: any | null) => void;
editLdapUser: JsonData | null;
setEditLdapUser: (u: JsonData | null) => void;
editLdapForm: { cn: string; mail: string; phone: string; title: string };
setEditLdapForm: React.Dispatch<React.SetStateAction<{ cn: string; mail: string; phone: string; title: string }>>;
handleUpdateLdapUser: () => void;
handleDeleteLDAPEntry: (dn: string, silent?: boolean) => void;
openEditLdapUser: ( user: any) => void;
openEditLdapUser: (user: JsonData) => void;
}
export function SystemSettingsTab(props: SystemSettingsTabProps) {
@@ -26,49 +26,50 @@ import { UserDetailDialog } from './UserDetailDialog';
import { CsvImportDialog } from './CsvImportDialog';
import { LdapImportDialog } from './LdapImportDialog';
import { usePermission } from '@/hooks/usePermission';
import type { JsonData } from '@/types/common';
interface SystemUserTabProps {
// Search
searchQuery: string;
setSearchQuery: (v: string) => void;
// Users
filteredUsers: any[];
filteredUsers: JsonData[];
loading: boolean;
selectedUser: any | null;
setSelectedUser: (u: any | null) => void;
selectedUser: JsonData | null;
setSelectedUser: (u: JsonData | null) => void;
getUserStatusBadge: (status: 'active' | 'inactive') => React.ReactNode;
// Create / Edit
dialogOpen: boolean;
setDialogOpen: (open: boolean) => void;
editingUser: any | null;
editingUser: JsonData | null;
form: UserFormData;
setForm: React.Dispatch<React.SetStateAction<UserFormData>>;
saving: boolean;
handleSubmit: () => void;
openCreate: () => void;
openEdit: ( user: any) => void;
handleDelete: ( user: any) => void;
openEdit: (user: JsonData) => void;
handleDelete: (user: JsonData) => void;
// CSV import
importDialogOpen: 'csv' | 'ldap' | null;
setImportDialogOpen: (v: 'csv' | 'ldap' | null) => void;
importPreview: any[];
setImportPreview: ( p: any[]) => void;
importPreview: JsonData[];
setImportPreview: ( p: JsonData[]) => void;
importing: boolean;
importResult: any;
setImportResult: ( r: any) => void;
importResult?: JsonData;
setImportResult: (r: JsonData) => void;
handleCsvUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleBatchImport: () => void;
// LDAP import
ldapUsers: any[];
ldapUsers: JsonData[];
ldapSelected: Set<string>;
setLdapSelected: (s: Set<string>) => void;
ldapLoading: boolean;
ldapImportOUs: any[];
ldapImportOUs: JsonData[];
ldapImportOUDN: string;
setLdapImportOUDN: (v: string) => void;
loadLdapUsers: (ouDn?: string) => void;
handleLdapImport: () => void;
setLdapUsers: ( u: any[]) => void;
setLdapUsers: ( u: JsonData[]) => void;
}
export function SystemUserTab({
@@ -10,12 +10,13 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { Key, Edit, User as UserIcon } from 'lucide-react';
import type { JsonData } from '@/types/common';
interface UserDetailDialogProps {
selectedUser: any | null;
setSelectedUser: (u: any | null) => void;
selectedUser: JsonData | null;
setSelectedUser: (u: JsonData | null) => void;
getUserStatusBadge: (status: 'active' | 'inactive') => React.ReactNode;
openEdit: ( user: any) => void;
openEdit: (user: JsonData) => void;
}
// UserDetailDialog 仅展示选中用户的基本信息和角色列表 // Logan.Li
@@ -18,6 +18,7 @@ import {
} from '@/components/ui/select';
import { User } from 'lucide-react';
import { userApi } from '@/api';
import type { JsonData } from '@/types/common';
export interface UserFormData {
username: string;
@@ -32,7 +33,7 @@ export interface UserFormData {
interface UserFormDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
editingUser: any | null;
editingUser: JsonData | null;
form: UserFormData;
setForm: React.Dispatch<React.SetStateAction<UserFormData>>;
saving: boolean;
@@ -60,7 +61,7 @@ export function UserFormDialog({
if (cancelled) return;
const list = resp?.list ?? [];
setRoles(
list.map(( r: any) => ({
list.map((r: JsonData) => ({
id: String(r.id ?? ''),
name: r.name ?? r.code ?? '',
}))
@@ -3,6 +3,7 @@ import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { ldapApi } from '@/api';
import type { LdapFormData } from './SystemSettingsTab';
import type { JsonData } from '@/types/common';
/**
* useLdapManagement — 封装 SystemManager 中所有 LDAP 相关状态与业务逻辑
@@ -21,14 +22,14 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
const confirm = useConfirm();
// ===== LDAP 用户导入(用户管理 Tab 的导入弹窗用) =====
const [ldapUsers, setLdapUsers] = useState<any[]>([]);
const [ldapUsers, setLdapUsers] = useState<JsonData[]>([]);
const [ldapSelected, setLdapSelected] = useState<Set<string>>(new Set());
const [ldapLoading, setLdapLoading] = useState(false);
const [ldapImportOUs, setLdapImportOUs] = useState<any[]>([]);
const [ldapImportOUs, setLdapImportOUs] = useState<JsonData[]>([]);
const [ldapImportOUDN, setLdapImportOUDN] = useState('');
// ===== LDAP 配置 =====
const [ldapConfig, setLdapConfig] = useState<any>(null);
const [ldapConfig, setLdapConfig] = useState<JsonData>(null);
const [ldapForm, setLdapForm] = useState<LdapFormData>({
ldap_host: '', ldap_port: 389, use_ldaps: false,
admin_dn: '', admin_password: '', base_dn: '', search_filter: '',
@@ -41,7 +42,7 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
const [importResultOpen, setImportResultOpen] = useState(false);
// ===== LDAP 账号管理 =====
const [ldapOUs, setLdapOUs] = useState<any[]>([]);
const [ldapOUs, setLdapOUs] = useState<JsonData[]>([]);
const [selectedOUDN, setSelectedOUDN] = useState('');
const [selectedLDAPUsers, setSelectedLDAPUsers] = useState<string[]>([]);
const [createOUOpen, setCreateOUOpen] = useState(false);
@@ -52,8 +53,8 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
const [ldapTestUser, setLdapTestUser] = useState('');
const [ldapTestPass, setLdapTestPass] = useState('');
const [ldapUserTesting, setLdapUserTesting] = useState(false);
const [ldapUserTestResult, setLdapUserTestResult] = useState<any>(null);
const [editLdapUser, setEditLdapUser] = useState<any>(null);
const [ldapUserTestResult, setLdapUserTestResult] = useState<JsonData>(null);
const [editLdapUser, setEditLdapUser] = useState<JsonData>(null);
const [editLdapForm, setEditLdapForm] = useState({ cn: '', mail: '', phone: '', title: '' });
// 导入进行中状态(CSV 与 LDAP 导入共用)
@@ -176,7 +177,7 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
};
/** 打开 LDAP 用户编辑 // Logan.Li */
const openEditLdapUser = ( user: any) => {
const openEditLdapUser = (user: JsonData) => {
setEditLdapUser(user);
setEditLdapForm({ cn: user.cn || '', mail: user.mail || '', phone: user.phone || '', title: user.title || '' });
};