!13 LDAP优化增强

* cursorrules
* fixed: ldap同步的部门记录name记录错误
* 主题色一致
* changelog
* admin接入ldap同步数据统计
* ldap同步数据记录接口合并
* fixed: 已同步被禁用用户的DN更新
* 已经同步的LDAP用户被禁止可以继续更新
* 优化代码
* 新增LDAP同步的详细记录
* 新增LDAP禁止用户的数据量统计
* 优化LDAP拉取数据的重复使用
* 优化LDAP同步
* ldap同步记录
* cursor rules
This commit is contained in:
白书科技
2025-05-19 06:25:34 +00:00
parent b9f600d3bc
commit c206fa4bf2
40 changed files with 2588 additions and 39 deletions

View File

@@ -15,3 +15,4 @@ export * as user from "./user";
export * as appConfig from "./app-config";
export * as dashboard from "./dashboard";
export * as adminLog from "./admin-log";
export * as ldap from "./ldap";

View File

@@ -0,0 +1,26 @@
import client from "./internal/httpClient";
// 获取同步记录列表
export function getSyncRecords(params: { page?: number; size?: number }) {
return client.get("/backend/v1/ldap/sync-records", params);
}
// 获取单条同步记录详情
export function getSyncRecordDetail(id: number) {
return client.get(`/backend/v1/ldap/sync-records/${id}`, {});
}
// 获取同步记录的详细项目
export function getSyncRecordDetails(id: number, params: {
type: 'department' | 'user';
action?: number;
page?: number;
size?: number
}) {
return client.get(`/backend/v1/ldap/sync-records/${id}/details`, params);
}
// 下载同步记录数据
export function downloadSyncRecord(id: number) {
return client.get(`/backend/v1/ldap/sync-records/${id}/download`, {});
}

View File

@@ -722,3 +722,10 @@ textarea.ant-input {
.select-range-modal .ant-tabs-tab + .ant-tabs-tab {
margin: 0 0 0 55px;
}
.clickable-stat {
cursor: pointer;
}
.clickable-stat:hover .ant-statistic-content {
color: #1890ff;
}

View File

@@ -0,0 +1,213 @@
import React, { useState, useEffect } from "react";
import { Modal, Card, Row, Col, Statistic, Divider } from "antd";
import { ldap } from "../../../api";
import { LdapSyncItemsModal } from ".";
import { dateFormat } from "../../../utils/index";
interface LdapSyncDetailModalProps {
record: any;
open: boolean;
onCancel: () => void;
}
export const LdapSyncDetailModal: React.FC<LdapSyncDetailModalProps> = ({
record,
open,
onCancel
}) => {
const [loading, setLoading] = useState(false);
const [detail, setDetail] = useState<any>(null);
const [itemsVisible, setItemsVisible] = useState(false);
const [itemsType, setItemsType] = useState<"department" | "user">("department");
const [itemsAction, setItemsAction] = useState<number>(0);
useEffect(() => {
if (open && record) {
loadDetail();
}
}, [open, record]);
const loadDetail = () => {
setLoading(true);
ldap.getSyncRecordDetail(record.id).then((res: any) => {
setDetail(res.data);
setLoading(false);
}).catch(() => {
setLoading(false);
});
};
const showItems = (type: "department" | "user", action: number) => {
setItemsType(type);
setItemsAction(action);
setItemsVisible(true);
};
return (
<>
<Modal
title="同步详情"
open={open}
onCancel={onCancel}
width={888}
footer={null}
>
{detail && (
<>
<Card title="基本信息" loading={loading}>
<Row gutter={16}>
<Col span={8}>
<Statistic title="同步ID" value={detail.id} />
</Col>
<Col span={8}>
<Statistic
title="同步状态"
value={
detail.status === 0 ? "进行中" :
detail.status === 1 ? "成功" : "失败"
}
/>
</Col>
<Col span={8}>
<Statistic title="同步时间" value={dateFormat(detail.created_at)} />
</Col>
</Row>
</Card>
<Divider />
<Card title="部门同步统计" loading={loading}>
<Row gutter={16}>
<Col span={6}>
<div
onClick={() => showItems("department", 0)}
className="clickable-stat"
>
<Statistic
title="总部门数"
value={detail.total_department_count}
/>
</div>
</Col>
<Col span={6}>
<div
onClick={() => showItems("department", 1)}
className="clickable-stat"
>
<Statistic
title="新增部门"
value={detail.created_department_count}
/>
</div>
</Col>
<Col span={6}>
<div
onClick={() => showItems("department", 2)}
className="clickable-stat"
>
<Statistic
title="更新部门"
value={detail.updated_department_count}
/>
</div>
</Col>
<Col span={6}>
<div
onClick={() => showItems("department", 3)}
className="clickable-stat"
>
<Statistic
title="删除部门"
value={detail.deleted_department_count}
/>
</div>
</Col>
</Row>
</Card>
<Divider />
<Card title="用户同步统计" loading={loading}>
<Row gutter={16}>
<Col span={6}>
<div
onClick={() => showItems("user", 0)}
className="clickable-stat"
>
<Statistic
title="总用户数"
value={detail.total_user_count}
/>
</div>
</Col>
<Col span={4}>
<div
onClick={() => showItems("user", 1)}
className="clickable-stat"
>
<Statistic
title="新增用户"
value={detail.created_user_count}
/>
</div>
</Col>
<Col span={4}>
<div
onClick={() => showItems("user", 2)}
className="clickable-stat"
>
<Statistic
title="更新用户"
value={detail.updated_user_count}
/>
</div>
</Col>
<Col span={4}>
<div
onClick={() => showItems("user", 3)}
className="clickable-stat"
>
<Statistic
title="删除用户"
value={detail.deleted_user_count}
/>
</div>
</Col>
<Col span={6}>
<div
onClick={() => showItems("user", 5)}
className="clickable-stat"
>
<Statistic
title="禁止用户"
value={detail.banned_user_count}
/>
</div>
</Col>
</Row>
</Card>
{detail.error_message && (
<>
<Divider />
<Card title="错误信息" loading={loading}>
<pre>{detail.error_message}</pre>
</Card>
</>
)}
</>
)}
</Modal>
{detail && (
<LdapSyncItemsModal
recordId={detail.id}
type={itemsType}
action={itemsAction}
open={itemsVisible}
onCancel={() => setItemsVisible(false)}
/>
)}
</>
);
};

View File

@@ -0,0 +1,147 @@
import React, { useState, useEffect } from "react";
import { Modal, Table, Tag } from "antd";
import { ldap } from "../../../api";
interface LdapSyncItemsModalProps {
recordId: number;
type: "department" | "user";
action: number;
open: boolean;
onCancel: () => void;
}
export const LdapSyncItemsModal: React.FC<LdapSyncItemsModalProps> = ({
recordId,
type,
action,
open,
onCancel
}) => {
const [loading, setLoading] = useState(false);
const [list, setList] = useState<any[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [size, setSize] = useState(10);
useEffect(() => {
if (open) {
loadData();
}
}, [open, page, size, type, action]);
const loadData = () => {
setLoading(true);
ldap.getSyncRecordDetails(recordId, {
type,
action,
page,
size,
}).then((res: any) => {
setList(res.data.records);
setTotal(res.data.total);
setLoading(false);
}).catch(() => {
setLoading(false);
});
};
const getActionText = (action: number) => {
const actions: any = {
department: {
1: "新增",
2: "更新",
3: "删除",
4: "无变化"
},
user: {
1: "新增",
2: "更新",
3: "删除",
4: "无变化",
5: "禁止"
}
};
return actions[type][action] || "未知";
};
const getActionColor = (action: number) => {
const colors: {[key: number]: string} = {
1: "green", // 新增
2: "blue", // 更新
3: "red", // 删除
4: "gray", // 无变化
5: "orange" // 禁止
};
return colors[action] || "default";
};
const departmentColumns = [
{ title: "ID", dataIndex: "id", key: "id", width: 60 },
{ title: "部门名称", dataIndex: "name", key: "name" },
{ title: "DN", dataIndex: "dn", key: "dn", ellipsis: true },
{ title: "UUID", dataIndex: "uuid", key: "uuid", width: 280 },
{
title: "操作类型",
dataIndex: "action",
key: "action",
width: 100,
render: (actionType: number) => (
<Tag color={getActionColor(actionType)}>
{getActionText(actionType)}
</Tag>
)
},
];
const userColumns = [
{ title: "ID", dataIndex: "id", key: "id", width: 60 },
{ title: "用户名", dataIndex: "cn", key: "cn" },
{ title: "登录名", dataIndex: "uid", key: "uid" },
{ title: "邮箱", dataIndex: "email", key: "email" },
{ title: "部门", dataIndex: "ou", key: "ou", ellipsis: true },
{
title: "操作类型",
dataIndex: "action",
key: "action",
width: 100,
render: (actionType: number) => (
<Tag color={getActionColor(actionType)}>
{getActionText(actionType)}
</Tag>
)
},
];
const columns = type === "department" ? departmentColumns : userColumns;
const title = type === "department" ? "部门同步详情" : "用户同步详情";
const actionText = action > 0 ? ` - ${getActionText(action)}` : "";
return (
<Modal
title={`${title}${actionText}`}
open={open}
onCancel={onCancel}
width={900}
footer={null}
>
<Table
columns={columns}
dataSource={list}
rowKey="id"
loading={loading}
pagination={{
total,
current: page,
pageSize: size,
onChange: (page, pageSize) => {
setPage(page);
setSize(pageSize || 10);
},
showSizeChanger: true,
}}
/>
</Modal>
);
};

View File

@@ -0,0 +1,183 @@
import React, { useState, useEffect } from "react";
import { Modal, Table, Button, message } from "antd";
import { EyeOutlined, DownloadOutlined, SyncOutlined } from "@ant-design/icons";
import { department, ldap } from "../../../api";
import { LdapSyncDetailModal } from ".";
import { dateFormat } from "../../../utils/index";
interface LdapSyncModalProps {
open: boolean;
onCancel: () => void;
}
export const LdapSyncModal: React.FC<LdapSyncModalProps> = ({
open,
onCancel,
}) => {
const [loading, setLoading] = useState(false);
const [syncLoading, setSyncLoading] = useState(false);
const [list, setList] = useState<any[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [size, setSize] = useState(10);
const [detailVisible, setDetailVisible] = useState(false);
const [currentRecord, setCurrentRecord] = useState<any>(null);
useEffect(() => {
if (open) {
loadData();
}
}, [open, page, size]);
const loadData = () => {
setLoading(true);
ldap
.getSyncRecords({ page, size })
.then((res: any) => {
setList(res.data.data);
setTotal(res.data.total);
setLoading(false);
})
.catch(() => {
setLoading(false);
});
};
const handleSync = () => {
if (syncLoading) {
message.warning("正在同步,请稍后...");
return;
}
setSyncLoading(true);
department
.ldapSync()
.then(() => {
message.success("同步触发成功");
setSyncLoading(false);
// 刷新数据列表
loadData();
})
.catch(() => {
setSyncLoading(false);
});
};
const handleDownload = (id: number) => {
ldap
.downloadSyncRecord(id)
.then((res: any) => {
window.open(res.data.url, "_blank");
})
.catch((e) => {
message.error("下载失败");
});
};
const handleDetail = (record: any) => {
setCurrentRecord(record);
setDetailVisible(true);
};
const columns = [
{ title: "ID", dataIndex: "id", key: "id" },
{
title: "同步状态",
dataIndex: "status",
key: "status",
render: (status: number) => {
if (status === 0)
return <span style={{ color: "#faad14" }}></span>;
if (status === 1) return <span style={{ color: "#52c41a" }}></span>;
return <span style={{ color: "#f5222d" }}></span>;
},
},
{
title: "部门总数",
dataIndex: "total_department_count",
key: "total_department_count",
},
{
title: "用户总数",
dataIndex: "total_user_count",
key: "total_user_count",
},
{
title: "同步时间",
dataIndex: "created_at",
key: "created_at",
render: (text: string) => <span>{dateFormat(text)}</span>,
},
{
title: "操作",
key: "action",
render: (_: any, record: any) => (
<div className="d-flex">
<Button
type="link"
className="b-link c-red mr-8"
icon={<EyeOutlined />}
onClick={() => handleDetail(record)}
>
</Button>
<Button
type="link"
className="b-link c-red"
icon={<DownloadOutlined />}
onClick={() => handleDownload(record.id)}
disabled={!record.s3_file_path}
>
</Button>
</div>
),
},
];
return (
<>
<Modal
title="LDAP同步记录"
open={open}
onCancel={onCancel}
width={900}
footer={null}
>
<div className="mb-24">
<Button
type="primary"
icon={<SyncOutlined />}
loading={syncLoading}
onClick={handleSync}
>
</Button>
</div>
<Table
columns={columns}
dataSource={list}
rowKey="id"
loading={loading}
pagination={{
total,
current: page,
pageSize: size,
onChange: (page, pageSize) => {
setPage(page);
setSize(pageSize || 10);
},
showSizeChanger: true,
}}
/>
</Modal>
{currentRecord && (
<LdapSyncDetailModal
record={currentRecord}
open={detailVisible}
onCancel={() => setDetailVisible(false)}
/>
)}
</>
);
};

View File

@@ -0,0 +1,3 @@
export { LdapSyncModal } from './LdapSyncModal';
export { LdapSyncDetailModal } from './LdapSyncDetailModal';
export { LdapSyncItemsModal } from './LdapSyncItemsModal';

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
import { Spin, Button, Tree, Modal, message, Tooltip } from "antd";
// import styles from "./index.module.less";
import { PlusOutlined, ExclamationCircleFilled } from "@ant-design/icons";
import { department } from "../../api/index";
import { department, ldap } from "../../api/index";
import { PerButton } from "../../compenents";
import type { DataNode, TreeProps } from "antd/es/tree";
import { DepartmentCreate } from "./compenents/create";
@@ -10,6 +10,7 @@ import { DepartmentUpdate } from "./compenents/update";
import { useNavigate } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { saveDepartmentsAction } from "../../store/system/systemConfigSlice";
import { LdapSyncModal } from "./components";
const { confirm } = Modal;
@@ -34,6 +35,7 @@ const DepartmentPage = () => {
const [updateVisible, setUpdateVisible] = useState(false);
const [did, setDid] = useState<number>(0);
const [modal, contextHolder] = Modal.useModal();
const [syncModalVisible, setSyncModalVisible] = useState(false);
// 是否启用LDAP
const ldapEnabled = useSelector(
@@ -395,16 +397,7 @@ const DepartmentPage = () => {
};
const ldapSync = () => {
if (loading) {
message.warning("正在同步,请稍后...");
return;
}
setLoading(true);
department.ldapSync().then(() => {
message.success("操作成功");
setLoading(false);
resetData();
});
setSyncModalVisible(true);
};
return (
@@ -474,6 +467,10 @@ const DepartmentPage = () => {
setRefresh(!refresh);
}}
/>
<LdapSyncModal
open={syncModalVisible}
onCancel={() => setSyncModalVisible(false)}
/>
</div>
</>
);