!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

@ -0,0 +1,31 @@
---
description:
globs:
alwaysApply: false
---
# PlayEdu API Project Overview
PlayEdu is an online training solution developed by Baishu Technology. The API is built with Java + Spring Boot 3, using a modular approach.
## Project Structure
- [playedu-api](mdc:playedu-api) - Java backend API project
- [playedu-admin](mdc:playedu-admin) - Admin frontend
- [playedu-pc](mdc:playedu-pc) - PC web interface
- [playedu-h5](mdc:playedu-h5) - Mobile web interface
## API Key Modules
- [playedu-api/PlayeduApiApplication.java](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/PlayeduApiApplication.java) - Main application entry point
- [playedu-api](mdc:playedu-api/playedu-api) - API module containing controllers and API-specific logic
- [playedu-common](mdc:playedu-api/playedu-common) - Common utilities and shared code
- [playedu-resource](mdc:playedu-api/playedu-resource) - Resource management module
- [playedu-course](mdc:playedu-api/playedu-course) - Course-related functionality
- [playedu-system](mdc:playedu-api/playedu-system) - System management functionality
## Backend vs Frontend Controllers
- [Backend Controllers](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/backend) - Admin-facing API endpoints
- [Frontend Controllers](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend) - Student-facing API endpoints
## Development and Deployment
- [pom.xml](mdc:playedu-api/pom.xml) - Main Maven configuration file
- [Dockerfile](mdc:playedu-api/Dockerfile) - Docker build configuration
- [compose.yml](mdc:compose.yml) - Docker Compose configuration

View File

@ -0,0 +1,32 @@
---
description:
globs:
alwaysApply: false
---
# PlayEdu API Structure
The API module follows a standard Spring Boot structure with controllers, services, and supporting components.
## Controller Layout
The API endpoints are divided into backend (admin) and frontend (student) controllers:
### Backend Controllers
- [Backend Controllers](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/backend) - Admin management interfaces
- [AdminUserController](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/AdminUserController.java) - Administrator management
- [CourseController](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/CourseController.java) - Course management
- [DepartmentController](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/DepartmentController.java) - Department management
- [ResourceController](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/ResourceController.java) - Resource management
- [UserController](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/UserController.java) - User management
### Frontend Controllers
- [Frontend Controllers](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend) - Student-facing endpoints
- [LoginController](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend/LoginController.java) - Student login
- [CourseController](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend/CourseController.java) - Course access
## API Application Components
- [PlayeduApiApplication](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/PlayeduApiApplication.java) - Main application entry point
- [Request DTOs](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/request) - Data transfer objects for API requests
- [Response Format](mdc:playedu-api/playedu-common/src/main/java/xyz/playedu/common/util/JsonResponse.java) - Standard JSON response format
- [Event Handlers](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/event) - Event-driven components
- [Scheduled Tasks](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/schedule) - Scheduled/recurring tasks
- [Interceptors](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/interceptor) - HTTP request interceptors

View File

@ -0,0 +1,32 @@
---
description:
globs:
alwaysApply: false
---
# PlayEdu Configuration Guide
The PlayEdu application uses standard Spring Boot configuration with YAML files.
## Application Configuration
- [application.yml](mdc:playedu-api/playedu-api/src/main/resources/application.yml) - Main application configuration
- [application-dev.yml](mdc:playedu-api/playedu-api/src/main/resources/application-dev.yml) - Development environment overrides
## Key Configuration Properties
- Database connection settings
- Redis cache configuration
- File storage configuration
- Security settings
- Cors configuration
## Build Configuration
- [pom.xml](mdc:playedu-api/pom.xml) - Main project Maven POM file
- [playedu-api/pom.xml](mdc:playedu-api/playedu-api/pom.xml) - API module POM file
- [playedu-common/pom.xml](mdc:playedu-api/playedu-common/pom.xml) - Common module POM file
- [playedu-course/pom.xml](mdc:playedu-api/playedu-course/pom.xml) - Course module POM file
- [playedu-resource/pom.xml](mdc:playedu-api/playedu-resource/pom.xml) - Resource module POM file
- [playedu-system/pom.xml](mdc:playedu-api/playedu-system/pom.xml) - System module POM file
## Docker Configuration
- [Dockerfile](mdc:playedu-api/Dockerfile) - Docker image definition
- [Dockerfile.local](mdc:playedu-api/Dockerfile.local) - Local development Docker configuration
- [compose.yml](mdc:compose.yml) - Docker Compose service definitions

View File

@ -0,0 +1,46 @@
---
description:
globs:
alwaysApply: false
---
# PlayEdu Module Structure
PlayEdu follows a modular architecture with separate modules for different concerns:
## Module Organization
Each module follows a similar structure with domain models, services, and mappers:
- **playedu-api**: Main API controllers and application entry point
- [Controllers](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller) - API endpoints
- [Request DTOs](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/request) - Request data objects
- [Configuration](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/config) - Module-specific configuration
- **playedu-common**: Shared utilities, base classes, and common functionality
- [Constants](mdc:playedu-api/playedu-common/src/main/java/xyz/playedu/common/constant) - System constants
- [Exceptions](mdc:playedu-api/playedu-common/src/main/java/xyz/playedu/common/exception) - Custom exceptions
- [Utilities](mdc:playedu-api/playedu-common/src/main/java/xyz/playedu/common/util) - Common utility classes
- [Base Models](mdc:playedu-api/playedu-common/src/main/java/xyz/playedu/common/bus) - Base model classes
- **playedu-resource**: Resource management (files, media, etc.)
- [Domain Models](mdc:playedu-api/playedu-resource/src/main/java/xyz/playedu/resource/domain) - Entity classes
- [Services](mdc:playedu-api/playedu-resource/src/main/java/xyz/playedu/resource/service) - Business logic
- [Mappers](mdc:playedu-api/playedu-resource/src/main/java/xyz/playedu/resource/mapper) - Database access layer
- **playedu-course**: Course management functionality
- [Domain Models](mdc:playedu-api/playedu-course/src/main/java/xyz/playedu/course/domain) - Course entities
- [Services](mdc:playedu-api/playedu-course/src/main/java/xyz/playedu/course/service) - Course business logic
- [Mappers](mdc:playedu-api/playedu-course/src/main/java/xyz/playedu/course/mapper) - Course data access
- **playedu-system**: System administration functionality
- [Domain Models](mdc:playedu-api/playedu-system/src/main/java/xyz/playedu/system/domain) - System entities
- [Services](mdc:playedu-api/playedu-system/src/main/java/xyz/playedu/system/service) - System business logic
- [Mappers](mdc:playedu-api/playedu-system/src/main/java/xyz/playedu/system/mapper) - System data access
## Domain-Driven Design
The codebase follows a layered architecture with:
- Controllers: Handle API requests and responses
- Services: Implement business logic
- Mappers: Data access layer (using MyBatis)
- Domain models: Entity classes representing business objects
This modular approach allows for separation of concerns and easier maintainability.

View File

@ -0,0 +1,45 @@
---
description:
globs:
alwaysApply: false
---
# PlayEdu Development Workflow
This guide outlines the workflow for developing and running the PlayEdu API.
## Local Development Setup
1. Clone the repository
2. Use Docker Compose to run the application: `docker-compose up -d`
3. Access points:
- API: `http://localhost:9700`
- Admin backend: `http://localhost:9900` (default credentials: `admin@playedu.xyz / playedu`)
- PC web interface: `http://localhost:9800`
- H5 mobile interface: `http://localhost:9801`
## Main Entry Points
- [PlayeduApiApplication.java](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/PlayeduApiApplication.java) - Main application class
- [application.yml](mdc:playedu-api/playedu-api/src/main/resources/application.yml) - Configuration
## Tech Stack
- Java with Spring Boot 3
- MySQL database
- Redis for caching
- MyBatis for data access
- Docker for containerization
## Development Best Practices
- Follow existing code structure when adding new features
- Add unit tests for new functionality
- Maintain module separation of concerns
- Use existing utility classes from `playedu-common`
## Build Process
To build the application:
1. Use Maven: `mvn clean package`
2. Build Docker image: `docker build -t playedu-api .`
3. Run in development mode: `docker-compose up -d`
## Version Control
- Follow standard Git workflow with feature branches
- Create pull requests for significant changes
- Update CHANGELOG.md for version releases

View File

@ -0,0 +1,33 @@
---
description:
globs:
alwaysApply: false
---
# PlayEdu Security Model
This guide outlines the security model of the PlayEdu application.
## Authentication
- [BackendAuthInterceptor](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/interceptor/BackendAuthInterceptor.java) - Backend authentication interceptor
- [FrontendAuthInterceptor](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/interceptor/FrontendAuthInterceptor.java) - Frontend authentication interceptor
- JWT-based authentication for both frontend and backend users
## Authorization
- Role-based access control for backend users
- Department-based content access for frontend users
- Course permission enforcement
## Security Configuration
- CORS configuration to prevent cross-site request forgery
- Password encryption using BCrypt
- Input validation and sanitization
## Resource Security
- Private video storage and delivery
- URL-based token authentication for media access
- Anti-leech protection for media files
## Sensitive Data Protection
- PII (Personally Identifiable Information) protection
- Logging sanitization for sensitive data
- Database encryption for critical fields

View File

@ -0,0 +1,40 @@
---
description:
globs:
alwaysApply: false
---
# PlayEdu Database Structure
This guide outlines the database structure of the PlayEdu application.
## Database Technology
- MySQL database for persistent storage
- Redis for caching and session management
- MyBatis as the ORM framework
## Core Tables
- **admin_users** - Administrator user accounts
- **admin_roles** - Administrator roles for RBAC
- **departments** - Organizational departments
- **users** - Student/learner accounts
- **courses** - Course information
- **resources** - Media and document resources
- **course_chapters** - Course chapter organization
- **course_hour_records** - Learning progress tracking
## Entity Relationships
- Departments have many Users (many-to-many)
- Courses have many Chapters (one-to-many)
- Courses have many Resources (many-to-many)
- Users have progress records for Courses (many-to-many)
## Database Access
- Data access through MyBatis Mappers
- [Example Mapper](mdc:playedu-api/playedu-course/src/main/java/xyz/playedu/course/mapper/CourseMapper.java)
- XML query definitions in resource XML files
- [Example XML](mdc:playedu-api/playedu-course/src/main/resources/mapper/CourseMapper.xml)
## Data Migration
- Managed through SQL scripts
- Version controlled database changes
- Backup procedures for data safety

View File

@ -1,5 +1,8 @@
## 1.9 ## 2.0
- 新增:`LDAP`同步数据统计
- 新增:`LDAP`同步的详细记录
- 新增:`LDAP`同步的数据下载
- 优化:移除`Redis`运行依赖改为使用内存缓存 - 优化:移除`Redis`运行依赖改为使用内存缓存
- 优化:移除本地存储方案`MinIO`的支持改为支持阿里云OSS和腾讯云COS - 优化:移除本地存储方案`MinIO`的支持改为支持阿里云OSS和腾讯云COS

View File

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

View File

@ -0,0 +1,305 @@
# LDAP同步记录功能
本文档介绍了PlayEdu系统中LDAP同步记录功能的使用方法和实现细节。
## 功能概述
LDAP同步记录功能主要实现了以下功能
1. 记录每次LDAP数据同步的统计数据
- 同步的部门和用户总数
- 新增、更新、删除的部门和用户数量
2. 将同步的LDAP数据保存到S3存储中方便后续查询和下载
3. 记录执行同步操作的管理员ID
4. 通过状态控制防止短时间内多次提交同步请求
5. 记录每次同步中每个部门和用户的详细变更情况
## 数据库表
### 主同步记录表
系统添加了新的数据库表 `ldap_sync_record`,其结构如下:
```sql
CREATE TABLE `ldap_sync_record` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`admin_id` int(11) NOT NULL DEFAULT 0 COMMENT '执行同步的管理员ID0表示系统自动执行',
`status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态0-进行中1-成功2-失败',
`s3_file_path` varchar(255) DEFAULT NULL COMMENT 'S3存储中的文件路径',
`total_department_count` int(11) NOT NULL DEFAULT 0 COMMENT '总部门数量',
`created_department_count` int(11) NOT NULL DEFAULT 0 COMMENT '新增部门数量',
`updated_department_count` int(11) NOT NULL DEFAULT 0 COMMENT '更新部门数量',
`deleted_department_count` int(11) NOT NULL DEFAULT 0 COMMENT '删除部门数量',
`total_user_count` int(11) NOT NULL DEFAULT 0 COMMENT '总用户数量',
`created_user_count` int(11) NOT NULL DEFAULT 0 COMMENT '新增用户数量',
`updated_user_count` int(11) NOT NULL DEFAULT 0 COMMENT '更新用户数量',
`deleted_user_count` int(11) NOT NULL DEFAULT 0 COMMENT '删除用户数量',
`banned_user_count` int(11) NOT NULL DEFAULT 0 COMMENT '被禁止的用户数量',
`error_message` text DEFAULT NULL COMMENT '错误信息',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='LDAP同步记录表';
```
### 部门同步详情表
记录每个部门在同步过程中的详细变更情况:
```sql
CREATE TABLE `ldap_sync_department_detail` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`record_id` int(11) NOT NULL COMMENT '关联的同步记录ID',
`department_id` int(11) DEFAULT NULL COMMENT '关联的部门ID',
`uuid` varchar(255) NOT NULL COMMENT 'LDAP部门UUID',
`dn` varchar(255) NOT NULL COMMENT 'LDAP部门DN',
`name` varchar(255) NOT NULL COMMENT '部门名称',
`action` tinyint(4) NOT NULL COMMENT '操作1-新增2-更新3-删除4-无变化',
`created_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `record_id` (`record_id`),
KEY `department_id` (`department_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='LDAP部门同步详情表';
```
### 用户同步详情表
记录每个用户在同步过程中的详细变更情况:
```sql
CREATE TABLE `ldap_sync_user_detail` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`record_id` int(11) NOT NULL COMMENT '关联的同步记录ID',
`user_id` int(11) DEFAULT NULL COMMENT '关联的用户ID',
`uuid` varchar(255) NOT NULL COMMENT 'LDAP用户UUID',
`dn` varchar(255) NOT NULL COMMENT 'LDAP用户DN',
`cn` varchar(255) NOT NULL COMMENT '用户名称',
`uid` varchar(255) NOT NULL COMMENT '用户ID/登录名',
`email` varchar(255) DEFAULT NULL COMMENT '用户邮箱',
`ou` text DEFAULT NULL COMMENT '用户部门路径',
`action` tinyint(4) NOT NULL COMMENT '操作1-新增2-更新3-删除4-无变化5-禁止',
`created_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `record_id` (`record_id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='LDAP用户同步详情表';
```
## API接口
系统提供了以下API接口用于LDAP同步操作和记录管理
### 1. 手动触发LDAP同步
- **URL**: `/backend/v1/ldap/sync`
- **方法**: POST
- **权限**: `ldap:sync`
- **返回数据**:
```json
{
"code": 0,
"data": {
"record_id": 1 // 同步记录ID
},
"message": "success"
}
```
### 2. 获取LDAP同步记录列表
- **URL**: `/backend/v1/ldap/sync-records`
- **方法**: GET
- **权限**: `ldap:sync:records`
- **参数**:
- `page`: 页码默认1
- `size`: 每页条数默认10
- **返回数据**:
```json
{
"code": 0,
"data": {
"data": [
{
"id": 1,
"admin_id": 0,
"status": 1,
"s3_file_path": "ldap/sync/ldap_sync_1_1620000000000.json",
"total_department_count": 10,
"created_department_count": 5,
"updated_department_count": 3,
"deleted_department_count": 2,
"total_user_count": 100,
"created_user_count": 50,
"updated_user_count": 30,
"deleted_user_count": 20,
"banned_user_count": 0,
"error_message": null,
"created_at": "2023-08-01 12:00:00",
"updated_at": "2023-08-01 12:01:00"
}
],
"total": 100
},
"message": "success"
}
```
### 3. 获取LDAP同步记录详情
- **URL**: `/backend/v1/ldap/sync-records/{id}`
- **方法**: GET
- **权限**: `ldap:sync:records`
- **返回数据**:
```json
{
"code": 0,
"data": {
"id": 1,
"admin_id": 0,
"status": 1,
"s3_file_path": "ldap/sync/ldap_sync_1_1620000000000.json",
"total_department_count": 10,
"created_department_count": 5,
"updated_department_count": 3,
"deleted_department_count": 2,
"total_user_count": 100,
"created_user_count": 50,
"updated_user_count": 30,
"deleted_user_count": 20,
"banned_user_count": 0,
"error_message": null,
"created_at": "2023-08-01 12:00:00",
"updated_at": "2023-08-01 12:01:00"
},
"message": "success"
}
```
### 4. 获取同步详情
- **URL**: `/backend/v1/ldap/sync-records/{id}/details`
- **方法**: GET
- **权限**: `ldap:sync:records`
- **参数**:
- `type`: 详情类型,`department`=部门,`user`=用户
- `action`: 操作类型当type=department时0=全部1=新增2=更新3=删除4=无变化当type=user时0=全部1=新增2=更新3=删除4=无变化5=禁止默认0
- `page`: 页码默认1
- `size`: 每页条数默认10
- **返回数据**:
```json
{
"code": 0,
"data": {
"records": [
{
// 当type=department时返回部门详情
"id": 1,
"record_id": 1,
"department_id": 10,
"uuid": "12345678-1234-1234-1234-123456789012",
"dn": "ou=HR,dc=example,dc=com",
"name": "HR",
"action": 1,
"created_at": "2023-08-01 12:00:00"
}
// 或当type=user时返回用户详情
{
"id": 1,
"record_id": 1,
"user_id": 100,
"uuid": "12345678-1234-1234-1234-123456789012",
"dn": "cn=John Doe,ou=HR,dc=example,dc=com",
"cn": "John Doe",
"uid": "johndoe",
"email": "john.doe@example.com",
"ou": "HR",
"action": 1,
"created_at": "2023-08-01 12:00:00"
}
],
"total": 100,
"size": 10,
"current": 1,
"pages": 10
},
"message": "success"
}
```
### 5. 下载LDAP同步记录数据
- **URL**: `/backend/v1/ldap/sync-records/{id}/download`
- **方法**: GET
- **权限**: `ldap:sync:records`
- **返回数据**:
```json
{
"code": 0,
"data": {
"url": "https://your-s3-domain.com/ldap/sync/ldap_sync_1_1620000000000.json"
},
"message": "success"
}
```
## 定时同步
系统会每小时自动执行一次LDAP同步同步过程和统计数据会记录到 `ldap_sync_record` 表中。自动同步时 `admin_id` 字段为0。
## 权限说明
为了使用LDAP同步记录功能需要为管理员角色分配以下权限
- `ldap:sync`: 允许手动触发LDAP同步
- `ldap:sync:records`: 允许查看LDAP同步记录
## 同步状态说明
LDAP同步记录的状态字段`status`)有以下值:
- `0`: 进行中
- `1`: 成功
- `2`: 失败
当状态为2失败错误信息会记录在 `error_message` 字段中。
## 操作类型说明
详细同步记录中的操作类型(`action`)有以下值:
### 部门操作类型:
- `1`: 新增 - 首次在LDAP中发现的部门
- `2`: 更新 - 已存在但信息发生变化的部门
- `3`: 删除 - 在LDAP中不再存在的部门
- `4`: 无变化 - 已存在且信息未发生变化的部门
### 用户操作类型:
- `1`: 新增 - 首次在LDAP中发现的用户
- `2`: 更新 - 已存在但信息发生变化的用户
- `3`: 删除 - 在LDAP中不再存在的用户
- `4`: 无变化 - 已存在且信息未发生变化的用户
- `5`: 禁止 - 在LDAP中被标记为禁止状态的用户
## S3存储
同步的LDAP数据会以JSON格式保存到S3存储中路径格式为
```
ldap/sync/ldap_sync_{记录ID}_{时间戳}.json
```
JSON文件包含完整的同步数据包括所有部门和用户的信息。
## 实现细节
系统在每次LDAP同步时都会进行以下操作
1. 创建主同步记录,初始状态为"进行中"
2. 获取LDAP配置信息并查询所有LDAP部门和用户数据
3. 收集统计信息并保存到S3
4. 收集每个部门和用户的详细变更情况
5. 执行实际的同步操作,包括部门同步和用户同步
6. 保存部门和用户的详细变更记录
7. 更新主同步记录状态为"成功"
如果同步过程中出现异常,系统会捕获异常信息并更新主同步记录状态为"失败"。

View File

@ -334,8 +334,26 @@ public class DepartmentController {
@Log(title = "部门-LDAP同步", businessType = BusinessTypeConstant.INSERT) @Log(title = "部门-LDAP同步", businessType = BusinessTypeConstant.INSERT)
@SneakyThrows @SneakyThrows
public JsonResponse ldapSync() { public JsonResponse ldapSync() {
ldapBus.departmentSync(); try {
ldapBus.userSync(); // 检查是否启用LDAP
return JsonResponse.success(); if (!ldapBus.enabledLDAP()) {
return JsonResponse.error("未配置LDAP服务");
}
// 检查是否有进行中的同步任务
if (ldapBus.hasSyncInProgress()) {
return JsonResponse.error("有正在进行的LDAP同步任务请稍后再试");
}
// 使用当前管理员ID执行同步
Integer recordId = ldapBus.syncAndRecord(BCtx.getId());
Map<String, Object> data = new HashMap<>();
data.put("record_id", recordId);
return JsonResponse.data(data);
} catch (Exception e) {
return JsonResponse.error("LDAP同步失败: " + e.getMessage());
}
} }
} }

View File

@ -0,0 +1,95 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.api.controller.backend;
import java.util.HashMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import xyz.playedu.common.annotation.BackendPermission;
import xyz.playedu.common.annotation.Log;
import xyz.playedu.common.constant.BPermissionConstant;
import xyz.playedu.common.constant.BusinessTypeConstant;
import xyz.playedu.common.domain.LdapSyncRecord;
import xyz.playedu.common.service.AppConfigService;
import xyz.playedu.common.service.LdapSyncRecordService;
import xyz.playedu.common.types.JsonResponse;
import xyz.playedu.common.types.config.S3Config;
import xyz.playedu.common.types.paginate.PaginationResult;
import xyz.playedu.common.util.S3Util;
@RestController
@RequestMapping("/backend/v1/ldap")
public class LdapController {
@Autowired private LdapSyncRecordService ldapSyncRecordService;
@Autowired private AppConfigService appConfigService;
@BackendPermission(slug = BPermissionConstant.DEPARTMENT_CUD)
@GetMapping("/sync-records")
@Log(title = "LDAP-同步记录列表", businessType = BusinessTypeConstant.GET)
public JsonResponse syncRecords(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "size", defaultValue = "10") Integer size) {
PaginationResult<LdapSyncRecord> result = ldapSyncRecordService.paginate(page, size);
HashMap<String, Object> data = new HashMap<>();
data.put("data", result.getData());
data.put("total", result.getTotal());
return JsonResponse.data(data);
}
@BackendPermission(slug = BPermissionConstant.DEPARTMENT_CUD)
@GetMapping("/sync-records/{id}")
@Log(title = "LDAP-同步记录详情", businessType = BusinessTypeConstant.GET)
public JsonResponse syncRecordDetail(@PathVariable Integer id) {
LdapSyncRecord record = ldapSyncRecordService.getById(id);
if (record == null) {
return JsonResponse.error("记录不存在");
}
return JsonResponse.data(record);
}
@BackendPermission(slug = BPermissionConstant.DEPARTMENT_CUD)
@GetMapping("/sync-records/{id}/download")
@Log(title = "LDAP-同步记录下载", businessType = BusinessTypeConstant.GET)
public JsonResponse syncRecordDownload(@PathVariable Integer id) {
LdapSyncRecord record = ldapSyncRecordService.getById(id);
if (record == null) {
return JsonResponse.error("记录不存在");
}
if (record.getS3FilePath() == null || record.getS3FilePath().isEmpty()) {
return JsonResponse.error("同步记录文件不存在");
}
try {
// 生成下载URL
S3Config s3Config = appConfigService.getS3Config();
S3Util s3Util = new S3Util(s3Config);
String url = s3Util.generateEndpointPreSignUrl(record.getS3FilePath());
HashMap<String, Object> data = new HashMap<>();
data.put("url", url);
return JsonResponse.data(data);
} catch (Exception e) {
return JsonResponse.error("生成下载链接失败: " + e.getMessage());
}
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.api.controller.backend;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import xyz.playedu.common.annotation.BackendPermission;
import xyz.playedu.common.constant.BPermissionConstant;
import xyz.playedu.common.domain.LdapSyncDepartmentDetail;
import xyz.playedu.common.domain.LdapSyncUserDetail;
import xyz.playedu.common.service.LdapSyncDepartmentDetailService;
import xyz.playedu.common.service.LdapSyncUserDetailService;
import xyz.playedu.common.types.JsonResponse;
/** LDAP同步详情控制器 */
@RestController
@RequestMapping("/backend/v1/ldap")
public class LdapSyncDetailController {
@Autowired private LdapSyncDepartmentDetailService ldapSyncDepartmentDetailService;
@Autowired private LdapSyncUserDetailService ldapSyncUserDetailService;
/**
* 获取同步详情
*
* @param id 同步记录ID
* @param type 详情类型department-部门user-用户
* @param action 操作类型
* - 部门1-新增2-更新3-删除4-无变化
* - 用户1-新增2-更新3-删除4-无变化5-禁止
* @param page 页码
* @param size 每页数量
* @return 分页结果
*/
@BackendPermission(slug = BPermissionConstant.DEPARTMENT_CUD)
@GetMapping("/sync-records/{id}/details")
public JsonResponse getDetails(
@PathVariable Integer id,
@RequestParam String type,
@RequestParam(defaultValue = "0") Integer action,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
if ("department".equals(type)) {
// 部门同步详情
QueryWrapper<LdapSyncDepartmentDetail> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("record_id", id);
if (action > 0) {
queryWrapper.eq("action", action);
}
queryWrapper.orderByDesc("id");
IPage<LdapSyncDepartmentDetail> pageResult =
ldapSyncDepartmentDetailService.page(new Page<>(page, size), queryWrapper);
return JsonResponse.data(pageResult);
} else if ("user".equals(type)) {
// 用户同步详情
QueryWrapper<LdapSyncUserDetail> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("record_id", id);
if (action > 0) {
queryWrapper.eq("action", action);
}
queryWrapper.orderByDesc("id");
IPage<LdapSyncUserDetail> pageResult =
ldapSyncUserDetailService.page(new Page<>(page, size), queryWrapper);
return JsonResponse.data(pageResult);
}
return JsonResponse.error("不支持的详情类型");
}
}

View File

@ -43,17 +43,11 @@ public class LDAPSchedule {
} }
try { try {
ldapBus.departmentSync(); // 使用新的同步记录功能
ldapBus.syncAndRecord(0); // 0表示系统自动执行
log.info("LDAP同步成功");
} catch (Exception e) { } catch (Exception e) {
log.error("LDAP-部门同步失败", e); log.error("LDAP同步失败", e);
} }
try {
ldapBus.userSync();
} catch (Exception e) {
log.error("LDAP-学员同步失败", e);
}
log.info("LDAP同步成功");
} }
} }

View File

@ -15,7 +15,11 @@
*/ */
package xyz.playedu.common.bus; package xyz.playedu.common.bus;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -26,12 +30,17 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import xyz.playedu.common.domain.Department; import xyz.playedu.common.domain.Department;
import xyz.playedu.common.domain.LdapDepartment; import xyz.playedu.common.domain.LdapDepartment;
import xyz.playedu.common.domain.LdapSyncDepartmentDetail;
import xyz.playedu.common.domain.LdapSyncRecord;
import xyz.playedu.common.domain.LdapSyncUserDetail;
import xyz.playedu.common.domain.LdapUser; import xyz.playedu.common.domain.LdapUser;
import xyz.playedu.common.domain.User; import xyz.playedu.common.domain.User;
import xyz.playedu.common.exception.NotFoundException; import xyz.playedu.common.exception.NotFoundException;
import xyz.playedu.common.service.*; import xyz.playedu.common.service.*;
import xyz.playedu.common.types.LdapConfig; import xyz.playedu.common.types.LdapConfig;
import xyz.playedu.common.types.config.S3Config;
import xyz.playedu.common.util.HelperUtil; import xyz.playedu.common.util.HelperUtil;
import xyz.playedu.common.util.S3Util;
import xyz.playedu.common.util.ldap.LdapTransformDepartment; import xyz.playedu.common.util.ldap.LdapTransformDepartment;
import xyz.playedu.common.util.ldap.LdapTransformUser; import xyz.playedu.common.util.ldap.LdapTransformUser;
import xyz.playedu.common.util.ldap.LdapUtil; import xyz.playedu.common.util.ldap.LdapUtil;
@ -50,16 +59,381 @@ public class LDAPBus {
@Autowired private UserService userService; @Autowired private UserService userService;
@Autowired private LdapSyncRecordService ldapSyncRecordService;
@Autowired private LdapSyncDepartmentDetailService ldapSyncDepartmentDetailService;
@Autowired private LdapSyncUserDetailService ldapSyncUserDetailService;
public boolean enabledLDAP() { public boolean enabledLDAP() {
return appConfigService.enabledLdapLogin(); return appConfigService.enabledLdapLogin();
} }
public void departmentSync() throws NamingException, NotFoundException { /** 检查是否有进行中的同步任务 */
LdapConfig ldapConfig = appConfigService.ldapConfig(); public boolean hasSyncInProgress() {
return ldapSyncRecordService.hasSyncInProgress();
}
List<LdapTransformDepartment> ouList = /**
LdapUtil.departments(ldapConfig, ldapConfig.getBaseDN()); * 执行LDAP同步并记录同步数据
*
* @param adminId 执行同步的管理员ID0为系统自动执行
* @return 同步记录ID
*/
public Integer syncAndRecord(Integer adminId)
throws NamingException, IOException, NotFoundException {
// 检查是否有进行中的同步任务
if (hasSyncInProgress()) {
throw new RuntimeException("有正在进行的LDAP同步任务请稍后再试");
}
// 创建同步记录
LdapSyncRecord record = ldapSyncRecordService.create(adminId);
try {
// 获取LDAP配置
LdapConfig ldapConfig = appConfigService.ldapConfig();
// 查询LDAP数据只查询一次
List<LdapTransformDepartment> departments =
LdapUtil.departments(ldapConfig, ldapConfig.getBaseDN());
List<LdapTransformUser> users = LdapUtil.users(ldapConfig, ldapConfig.getBaseDN());
// 使用查询的数据进行统计
Map<String, Object> result = collectSyncStatistics(departments, users);
// 将同步数据保存到S3
String s3FilePath = saveDataToS3(result, record.getId());
// 收集部门和用户的详细同步信息
List<LdapSyncDepartmentDetail> departmentDetails =
collectDepartmentSyncDetails(record.getId(), departments);
List<LdapSyncUserDetail> userDetails = collectUserSyncDetails(record.getId(), users);
// 使用同样的数据执行实际同步
departmentSync(departments);
userSync(users);
// 保存部门和用户的详细同步信息
ldapSyncDepartmentDetailService.batchCreate(departmentDetails);
ldapSyncUserDetailService.batchCreate(userDetails);
// 更新同步记录
ldapSyncRecordService.updateSyncResult(
record.getId(),
1, // 成功
s3FilePath,
(Integer) result.get("totalDepartmentCount"),
(Integer) result.get("createdDepartmentCount"),
(Integer) result.get("updatedDepartmentCount"),
(Integer) result.get("deletedDepartmentCount"),
(Integer) result.get("totalUserCount"),
(Integer) result.get("createdUserCount"),
(Integer) result.get("updatedUserCount"),
(Integer) result.get("deletedUserCount"),
(Integer) result.get("bannedUserCount"));
return record.getId();
} catch (Exception e) {
// 记录同步失败
ldapSyncRecordService.updateSyncFailed(record.getId(), e.getMessage());
log.error("LDAP同步失败", e);
throw e;
}
}
/**
* 收集部门同步详情
*
* @param recordId 同步记录ID
* @param departments LDAP部门数据
* @return 部门同步详情列表
*/
private List<LdapSyncDepartmentDetail> collectDepartmentSyncDetails(
Integer recordId, List<LdapTransformDepartment> departments) throws NotFoundException {
List<LdapSyncDepartmentDetail> details = new ArrayList<>();
Date now = new Date();
// 读取已经同步的记录
Map<String, LdapDepartment> ldapDepartments =
ldapDepartmentService.all().stream()
.collect(Collectors.toMap(LdapDepartment::getUuid, e -> e));
// 记录新增和更新的部门
for (LdapTransformDepartment dept : departments) {
LdapDepartment existingDept = ldapDepartments.get(dept.getUuid());
LdapSyncDepartmentDetail detail = new LdapSyncDepartmentDetail();
detail.setRecordId(recordId);
detail.setUuid(dept.getUuid());
detail.setDn(dept.getDn());
// 从DN中提取部门名称
String[] parts = dept.getDn().split(",");
String name = parts[parts.length - 1].replace("ou=", "");
detail.setName(name);
detail.setCreatedAt(now);
if (existingDept == null) {
// 新增部门
detail.setAction(1);
} else if (!existingDept.getDn().equals(dept.getDn())) {
// 更新部门
detail.setDepartmentId(existingDept.getDepartmentId());
detail.setAction(2);
} else {
// 无变化
detail.setDepartmentId(existingDept.getDepartmentId());
detail.setAction(4);
}
details.add(detail);
}
// 记录删除的部门
List<String> uuidList = departments.stream().map(LdapTransformDepartment::getUuid).toList();
List<LdapDepartment> deletedDepts = ldapDepartmentService.notChunkByUUIDList(uuidList);
if (deletedDepts != null && !deletedDepts.isEmpty()) {
for (LdapDepartment dept : deletedDepts) {
LdapSyncDepartmentDetail detail = new LdapSyncDepartmentDetail();
detail.setRecordId(recordId);
detail.setDepartmentId(dept.getDepartmentId());
detail.setUuid(dept.getUuid());
detail.setDn(dept.getDn());
// 获取部门名称
Department department = departmentService.findOrFail(dept.getDepartmentId());
detail.setName(department.getName());
detail.setAction(3); // 删除
detail.setCreatedAt(now);
details.add(detail);
}
}
return details;
}
/**
* 收集用户同步详情
*
* @param recordId 同步记录ID
* @param users LDAP用户数据
* @return 用户同步详情列表
*/
private List<LdapSyncUserDetail> collectUserSyncDetails(
Integer recordId, List<LdapTransformUser> users) {
List<LdapSyncUserDetail> details = new ArrayList<>();
Date now = new Date();
// 处理新增和更新的用户
for (LdapTransformUser user : users) {
LdapSyncUserDetail detail = new LdapSyncUserDetail();
detail.setRecordId(recordId);
detail.setUuid(user.getId());
detail.setDn(user.getDn());
detail.setCn(user.getCn());
detail.setUid(user.getUid());
detail.setEmail(user.getEmail());
detail.setOu(String.join(",", user.getOu()));
detail.setCreatedAt(now);
// 查找现有用户
LdapUser existingUser = ldapUserService.findByUUID(user.getId());
// 检查用户是否被禁止
if (user.isBan()) {
// 标记为禁止的用户
detail.setAction(5); // 5-禁止的用户
if (existingUser != null) {
detail.setUserId(existingUser.getUserId());
}
details.add(detail);
continue;
}
if (existingUser == null) {
// 新增用户
detail.setAction(1);
} else {
// 检查是否有变更
boolean hasChanges = false;
if (!user.getCn().equals(existingUser.getCn())) {
hasChanges = true;
}
String newOU = String.join(",", user.getOu());
if (!newOU.equals(existingUser.getOu())) {
hasChanges = true;
}
// 设置用户ID
detail.setUserId(existingUser.getUserId());
if (hasChanges) {
// 更新用户
detail.setAction(2);
} else {
// 无变化
detail.setAction(4);
}
}
details.add(detail);
}
// 处理删除的用户
List<String> uuidList =
users.stream().filter(u -> !u.isBan()).map(LdapTransformUser::getId).toList();
// 获取所有现有的LDAP用户记录
List<LdapUser> allLdapUsers = ldapUserService.list();
// 过滤出不在当前LDAP用户列表中的用户
List<LdapUser> deletedUsers =
allLdapUsers.stream().filter(lu -> !uuidList.contains(lu.getUuid())).toList();
for (LdapUser deletedUser : deletedUsers) {
LdapSyncUserDetail detail = new LdapSyncUserDetail();
detail.setRecordId(recordId);
detail.setUserId(deletedUser.getUserId());
detail.setUuid(deletedUser.getUuid());
detail.setDn(deletedUser.getDn());
detail.setCn(deletedUser.getCn());
detail.setUid(deletedUser.getUid());
detail.setEmail(deletedUser.getEmail());
detail.setOu(deletedUser.getOu());
detail.setAction(3); // 删除
detail.setCreatedAt(now);
details.add(detail);
}
return details;
}
/** 收集同步统计数据 */
private Map<String, Object> collectSyncStatistics(
List<LdapTransformDepartment> departments, List<LdapTransformUser> users) {
Map<String, Object> result = new HashMap<>();
Map<String, Object> syncData = new HashMap<>();
// 部门同步统计
int totalDepartmentCount = 0;
int createdDepartmentCount = 0;
int updatedDepartmentCount = 0;
int deletedDepartmentCount = 0;
// 用户同步统计
int totalUserCount = 0;
int createdUserCount = 0;
int updatedUserCount = 0;
int deletedUserCount = 0;
int bannedUserCount = 0;
// 处理部门数据
if (departments != null && !departments.isEmpty()) {
syncData.put("departments", departments);
totalDepartmentCount = departments.size();
// 读取已经同步的记录
Map<String, LdapDepartment> ldapDepartments =
ldapDepartmentService.all().stream()
.collect(Collectors.toMap(LdapDepartment::getUuid, e -> e));
// 计算新增和更新的部门
for (LdapTransformDepartment dept : departments) {
LdapDepartment existingDept = ldapDepartments.get(dept.getUuid());
if (existingDept == null) {
createdDepartmentCount++;
} else if (!existingDept.getDn().equals(dept.getDn())) {
updatedDepartmentCount++;
}
}
// 计算删除的部门
List<String> uuidList =
departments.stream().map(LdapTransformDepartment::getUuid).toList();
List<LdapDepartment> ldapDepartmentList =
ldapDepartmentService.notChunkByUUIDList(uuidList);
deletedDepartmentCount = ldapDepartmentList != null ? ldapDepartmentList.size() : 0;
}
// 处理用户数据
if (users != null && !users.isEmpty()) {
syncData.put("users", users);
totalUserCount = users.size();
// 计算被禁止的用户数量
bannedUserCount = (int) users.stream().filter(LdapTransformUser::isBan).count();
// 计算新增更新的用户
for (LdapTransformUser user : users) {
if (user.isBan()) {
continue;
}
LdapUser existingUser = ldapUserService.findByUUID(user.getId());
if (existingUser == null) {
createdUserCount++;
} else {
// 检查用户信息是否有变化
boolean hasChanges = false;
if (!user.getCn().equals(existingUser.getCn())) {
hasChanges = true;
}
String newOU = String.join(",", user.getOu());
if (!newOU.equals(existingUser.getOu())) {
hasChanges = true;
}
if (hasChanges) {
updatedUserCount++;
}
}
}
}
// 将同步结果数据存储到结果对象中
result.put("data", syncData);
result.put("totalDepartmentCount", totalDepartmentCount);
result.put("createdDepartmentCount", createdDepartmentCount);
result.put("updatedDepartmentCount", updatedDepartmentCount);
result.put("deletedDepartmentCount", deletedDepartmentCount);
result.put("totalUserCount", totalUserCount);
result.put("createdUserCount", createdUserCount);
result.put("updatedUserCount", updatedUserCount);
result.put("deletedUserCount", deletedUserCount);
result.put("bannedUserCount", bannedUserCount);
return result;
}
/** 将同步数据保存到S3 */
private String saveDataToS3(Map<String, Object> data, Integer recordId) throws IOException {
// 将数据转换为JSON
ObjectMapper objectMapper = new ObjectMapper();
String jsonData = objectMapper.writeValueAsString(data);
byte[] jsonBytes = jsonData.getBytes(StandardCharsets.UTF_8);
// 生成保存路径
String filename = "ldap_sync_" + recordId + "_" + new Date().getTime() + ".json";
String savePath = "ldap/sync/" + filename;
// 保存到S3
S3Config s3Config = appConfigService.getS3Config();
S3Util s3Util = new S3Util(s3Config);
s3Util.saveBytes(jsonBytes, savePath, "application/json");
return savePath;
}
/**
* 执行部门同步 - 提供现有的LDAP部门数据
*
* @param ouList 已获取的LDAP部门数据
*/
public void departmentSync(List<LdapTransformDepartment> ouList) throws NotFoundException {
if (ouList == null || ouList.isEmpty()) { if (ouList == null || ouList.isEmpty()) {
return; return;
} }
@ -203,11 +577,12 @@ public class LDAPBus {
} }
} }
public void userSync() throws NamingException, IOException { /**
LdapConfig ldapConfig = appConfigService.ldapConfig(); * 执行用户同步 - 提供现有的LDAP用户数据
*
List<LdapTransformUser> userList = LdapUtil.users(ldapConfig, ldapConfig.getBaseDN()); * @param userList 已获取的LDAP用户数据
*/
public void userSync(List<LdapTransformUser> userList) {
if (userList == null || userList.isEmpty()) { if (userList == null || userList.isEmpty()) {
return; return;
} }
@ -216,12 +591,18 @@ public class LDAPBus {
for (LdapTransformUser ldapTransformUser : userList) { for (LdapTransformUser ldapTransformUser : userList) {
if (ldapTransformUser.isBan()) { if (ldapTransformUser.isBan()) {
log.info( // 检查用户是否已在系统中存在
"LDAP-用户同步-用户被禁止|ctx=[dn:{},uuid={}]", LdapUser existingLdapUser = ldapUserService.findByUUID(ldapTransformUser.getId());
ldapTransformUser.getDn(), if (existingLdapUser == null) {
ldapTransformUser.getId()); // 对于新的被禁止用户不同步到系统
continue; log.info(
"LDAP-用户同步-新用户被禁止不同步|ctx=[dn:{},uuid={}]",
ldapTransformUser.getDn(),
ldapTransformUser.getId());
continue;
}
} }
singleUserSync(ldapTransformUser, defaultAvatar); singleUserSync(ldapTransformUser, defaultAvatar);
} }
} }
@ -326,6 +707,15 @@ public class LDAPBus {
if (!newOU.equals(ldapUser.getOu())) { if (!newOU.equals(ldapUser.getOu())) {
userService.updateDepId(user.getId(), depIds); userService.updateDepId(user.getId(), depIds);
ldapUserService.updateOU(ldapUser.getId(), newOU); ldapUserService.updateOU(ldapUser.getId(), newOU);
if (ldapTransformUser.isBan()) {
log.info("LDAP-用户同步-被禁止用户部门已更新|ctx=[userId:{},新OU:{}]", user.getId(), newOU);
}
}
// DN变化
if (!ldapTransformUser.getDn().equals(ldapUser.getDn())) {
ldapUserService.updateDN(ldapUser.getId(), ldapTransformUser.getDn());
} }
} }

View File

@ -0,0 +1,56 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.common.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
/** LDAP部门同步详情实体 */
@Data
@TableName("ldap_sync_department_detail")
public class LdapSyncDepartmentDetail implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@TableField("record_id")
private Integer recordId;
@TableField("department_id")
private Integer departmentId;
@TableField("uuid")
private String uuid;
@TableField("dn")
private String dn;
@TableField("name")
private String name;
@TableField("action")
private Integer action; // 1-新增2-更新3-删除4-无变化
@TableField("created_at")
private Date createdAt;
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,95 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.common.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
@Data
@TableName(value = "ldap_sync_record")
public class LdapSyncRecord implements Serializable {
/** */
@TableId(type = IdType.AUTO)
private Integer id;
/** 执行同步的管理员ID0表示系统自动执行 */
@JsonProperty("admin_id")
private Integer adminId;
/** 状态0-进行中1-成功2-失败 */
private Integer status;
/** S3存储中的文件路径 */
@JsonProperty("s3_file_path")
private String s3FilePath;
/** 总部门数量 */
@JsonProperty("total_department_count")
private Integer totalDepartmentCount;
/** 新增部门数量 */
@JsonProperty("created_department_count")
private Integer createdDepartmentCount;
/** 更新部门数量 */
@JsonProperty("updated_department_count")
private Integer updatedDepartmentCount;
/** 删除部门数量 */
@JsonProperty("deleted_department_count")
private Integer deletedDepartmentCount;
/** 总用户数量 */
@JsonProperty("total_user_count")
private Integer totalUserCount;
/** 新增用户数量 */
@JsonProperty("created_user_count")
private Integer createdUserCount;
/** 更新用户数量 */
@JsonProperty("updated_user_count")
private Integer updatedUserCount;
/** 删除用户数量 */
@JsonProperty("deleted_user_count")
private Integer deletedUserCount;
/** 被禁止的用户数量 */
@JsonProperty("banned_user_count")
private Integer bannedUserCount;
/** 错误信息 */
@JsonProperty("error_message")
private String errorMessage;
/** */
@JsonProperty("created_at")
private Date createdAt;
/** */
@JsonProperty("updated_at")
private Date updatedAt;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,65 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.common.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
/** LDAP用户同步详情实体 */
@Data
@TableName("ldap_sync_user_detail")
public class LdapSyncUserDetail implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@TableField("record_id")
private Integer recordId;
@TableField("user_id")
private Integer userId;
@TableField("uuid")
private String uuid;
@TableField("dn")
private String dn;
@TableField("cn")
private String cn;
@TableField("uid")
private String uid;
@TableField("email")
private String email;
@TableField("ou")
private String ou;
@TableField("action")
private Integer action; // 1-新增2-更新3-删除4-无变化
@TableField("created_at")
private Date createdAt;
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.common.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import xyz.playedu.common.domain.LdapSyncDepartmentDetail;
/** LDAP部门同步详情Mapper */
@Mapper
public interface LdapSyncDepartmentDetailMapper extends BaseMapper<LdapSyncDepartmentDetail> {}

View File

@ -0,0 +1,24 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.common.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import xyz.playedu.common.domain.LdapSyncRecord;
/**
* @description 针对表ldap_sync_record的数据库操作Mapper
*/
public interface LdapSyncRecordMapper extends BaseMapper<LdapSyncRecord> {}

View File

@ -0,0 +1,24 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.common.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import xyz.playedu.common.domain.LdapSyncUserDetail;
/** LDAP用户同步详情Mapper */
@Mapper
public interface LdapSyncUserDetailMapper extends BaseMapper<LdapSyncUserDetail> {}

View File

@ -0,0 +1,40 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.common.service;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
import xyz.playedu.common.domain.LdapSyncDepartmentDetail;
/** LDAP部门同步详情服务接口 */
public interface LdapSyncDepartmentDetailService extends IService<LdapSyncDepartmentDetail> {
/**
* 批量创建部门同步详情记录
*
* @param details 部门同步详情记录列表
*/
void batchCreate(List<LdapSyncDepartmentDetail> details);
/**
* 根据同步记录ID和操作类型获取部门同步详情
*
* @param recordId 同步记录ID
* @param action 操作类型1-新增2-更新3-删除4-无变化
* @return 部门同步详情列表
*/
List<LdapSyncDepartmentDetail> getByRecordIdAndAction(Integer recordId, Integer action);
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.common.service;
import com.baomidou.mybatisplus.extension.service.IService;
import xyz.playedu.common.domain.LdapSyncRecord;
import xyz.playedu.common.types.paginate.PaginationResult;
public interface LdapSyncRecordService extends IService<LdapSyncRecord> {
// 创建同步记录
LdapSyncRecord create(Integer adminId);
// 更新同步结果
void updateSyncResult(
Integer id,
Integer status,
String s3FilePath,
Integer totalDepartmentCount,
Integer createdDepartmentCount,
Integer updatedDepartmentCount,
Integer deletedDepartmentCount,
Integer totalUserCount,
Integer createdUserCount,
Integer updatedUserCount,
Integer deletedUserCount,
Integer bannedUserCount);
// 更新同步状态为失败并记录错误信息
void updateSyncFailed(Integer id, String errorMessage);
// 分页查询
PaginationResult<LdapSyncRecord> paginate(Integer page, Integer size);
// 检查是否有进行中的同步任务
boolean hasSyncInProgress();
// 获取最近一次同步记录
LdapSyncRecord getLatestRecord();
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.common.service;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
import xyz.playedu.common.domain.LdapSyncUserDetail;
/** LDAP用户同步详情服务接口 */
public interface LdapSyncUserDetailService extends IService<LdapSyncUserDetail> {
/**
* 批量创建用户同步详情记录
*
* @param details 用户同步详情记录列表
*/
void batchCreate(List<LdapSyncUserDetail> details);
/**
* 根据同步记录ID和操作类型获取用户同步详情
*
* @param recordId 同步记录ID
* @param action 操作类型1-新增2-更新3-删除4-无变化
* @return 用户同步详情列表
*/
List<LdapSyncUserDetail> getByRecordIdAndAction(Integer recordId, Integer action);
}

View File

@ -38,4 +38,6 @@ public interface LdapUserService extends IService<LdapUser> {
void updateEmail(Integer id, String email); void updateEmail(Integer id, String email);
void updateUid(Integer id, String uid); void updateUid(Integer id, String uid);
void updateDN(Integer id, String dn);
} }

View File

@ -0,0 +1,50 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.common.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import java.util.List;
import org.springframework.stereotype.Service;
import xyz.playedu.common.domain.LdapSyncDepartmentDetail;
import xyz.playedu.common.mapper.LdapSyncDepartmentDetailMapper;
import xyz.playedu.common.service.LdapSyncDepartmentDetailService;
/** LDAP部门同步详情服务实现类 */
@Service
public class LdapSyncDepartmentDetailServiceImpl
extends ServiceImpl<LdapSyncDepartmentDetailMapper, LdapSyncDepartmentDetail>
implements LdapSyncDepartmentDetailService {
@Override
public void batchCreate(List<LdapSyncDepartmentDetail> details) {
if (details == null || details.isEmpty()) {
return;
}
saveBatch(details);
}
@Override
public List<LdapSyncDepartmentDetail> getByRecordIdAndAction(Integer recordId, Integer action) {
QueryWrapper<LdapSyncDepartmentDetail> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("record_id", recordId);
if (action > 0) {
queryWrapper.eq("action", action);
}
queryWrapper.orderByDesc("id");
return list(queryWrapper);
}
}

View File

@ -0,0 +1,127 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.common.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import java.util.Date;
import org.springframework.stereotype.Service;
import xyz.playedu.common.domain.LdapSyncRecord;
import xyz.playedu.common.mapper.LdapSyncRecordMapper;
import xyz.playedu.common.service.LdapSyncRecordService;
import xyz.playedu.common.types.paginate.PaginationResult;
@Service
public class LdapSyncRecordServiceImpl extends ServiceImpl<LdapSyncRecordMapper, LdapSyncRecord>
implements LdapSyncRecordService {
@Override
public LdapSyncRecord create(Integer adminId) {
LdapSyncRecord record = new LdapSyncRecord();
record.setAdminId(adminId);
record.setStatus(0); // 进行中
record.setTotalDepartmentCount(0);
record.setCreatedDepartmentCount(0);
record.setUpdatedDepartmentCount(0);
record.setDeletedDepartmentCount(0);
record.setTotalUserCount(0);
record.setCreatedUserCount(0);
record.setUpdatedUserCount(0);
record.setDeletedUserCount(0);
record.setBannedUserCount(0);
record.setCreatedAt(new Date());
record.setUpdatedAt(new Date());
save(record);
return record;
}
@Override
public void updateSyncResult(
Integer id,
Integer status,
String s3FilePath,
Integer totalDepartmentCount,
Integer createdDepartmentCount,
Integer updatedDepartmentCount,
Integer deletedDepartmentCount,
Integer totalUserCount,
Integer createdUserCount,
Integer updatedUserCount,
Integer deletedUserCount,
Integer bannedUserCount) {
LdapSyncRecord record = new LdapSyncRecord();
record.setId(id);
record.setStatus(status);
record.setS3FilePath(s3FilePath);
record.setTotalDepartmentCount(totalDepartmentCount);
record.setCreatedDepartmentCount(createdDepartmentCount);
record.setUpdatedDepartmentCount(updatedDepartmentCount);
record.setDeletedDepartmentCount(deletedDepartmentCount);
record.setTotalUserCount(totalUserCount);
record.setCreatedUserCount(createdUserCount);
record.setUpdatedUserCount(updatedUserCount);
record.setDeletedUserCount(deletedUserCount);
record.setBannedUserCount(bannedUserCount);
record.setUpdatedAt(new Date());
updateById(record);
}
@Override
public void updateSyncFailed(Integer id, String errorMessage) {
LdapSyncRecord record = new LdapSyncRecord();
record.setId(id);
record.setStatus(2); // 失败
record.setErrorMessage(errorMessage);
record.setUpdatedAt(new Date());
updateById(record);
}
@Override
public PaginationResult<LdapSyncRecord> paginate(Integer page, Integer size) {
Page<LdapSyncRecord> pageObj = new Page<>(page, size);
QueryWrapper<LdapSyncRecord> wrapper = new QueryWrapper<>();
wrapper.orderByDesc("id");
IPage<LdapSyncRecord> iPage = page(pageObj, wrapper);
PaginationResult<LdapSyncRecord> result = new PaginationResult<>();
result.setData(iPage.getRecords());
result.setTotal(iPage.getTotal());
return result;
}
@Override
public boolean hasSyncInProgress() {
QueryWrapper<LdapSyncRecord> wrapper = new QueryWrapper<>();
wrapper.eq("status", 0);
return count(wrapper) > 0;
}
@Override
public LdapSyncRecord getLatestRecord() {
QueryWrapper<LdapSyncRecord> wrapper = new QueryWrapper<>();
wrapper.orderByDesc("id");
wrapper.last("limit 1");
return getOne(wrapper);
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.common.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import java.util.List;
import org.springframework.stereotype.Service;
import xyz.playedu.common.domain.LdapSyncUserDetail;
import xyz.playedu.common.mapper.LdapSyncUserDetailMapper;
import xyz.playedu.common.service.LdapSyncUserDetailService;
/** LDAP用户同步详情服务实现类 */
@Service
public class LdapSyncUserDetailServiceImpl
extends ServiceImpl<LdapSyncUserDetailMapper, LdapSyncUserDetail>
implements LdapSyncUserDetailService {
@Override
public void batchCreate(List<LdapSyncUserDetail> details) {
if (details == null || details.isEmpty()) {
return;
}
saveBatch(details);
}
@Override
public List<LdapSyncUserDetail> getByRecordIdAndAction(Integer recordId, Integer action) {
QueryWrapper<LdapSyncUserDetail> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("record_id", recordId);
if (action > 0) {
queryWrapper.eq("action", action);
}
queryWrapper.orderByDesc("id");
return list(queryWrapper);
}
}

View File

@ -98,4 +98,12 @@ public class LdapUserServiceImpl extends ServiceImpl<LdapUserMapper, LdapUser>
user.setUid(uid); user.setUid(uid);
updateById(user); updateById(user);
} }
@Override
public void updateDN(Integer id, String dn) {
LdapUser user = new LdapUser();
user.setId(id);
user.setDn(dn);
updateById(user);
}
} }

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xyz.playedu.common.mapper.LdapSyncDepartmentDetailMapper">
<!-- 通用映射结果集 -->
<resultMap id="BaseResultMap" type="xyz.playedu.common.domain.LdapSyncDepartmentDetail">
<id column="id" property="id"/>
<result column="record_id" property="recordId"/>
<result column="department_id" property="departmentId"/>
<result column="uuid" property="uuid"/>
<result column="dn" property="dn"/>
<result column="name" property="name"/>
<result column="action" property="action"/>
<result column="created_at" property="createdAt"/>
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, record_id, department_id, uuid, dn, name, action, created_at
</sql>
</mapper>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xyz.playedu.common.mapper.LdapSyncRecordMapper">
<resultMap id="BaseResultMap" type="xyz.playedu.common.domain.LdapSyncRecord">
<id property="id" column="id" jdbcType="INTEGER"/>
<result property="adminId" column="admin_id" jdbcType="INTEGER"/>
<result property="status" column="status" jdbcType="TINYINT"/>
<result property="s3FilePath" column="s3_file_path" jdbcType="VARCHAR"/>
<result property="totalDepartmentCount" column="total_department_count" jdbcType="INTEGER"/>
<result property="createdDepartmentCount" column="created_department_count" jdbcType="INTEGER"/>
<result property="updatedDepartmentCount" column="updated_department_count" jdbcType="INTEGER"/>
<result property="deletedDepartmentCount" column="deleted_department_count" jdbcType="INTEGER"/>
<result property="totalUserCount" column="total_user_count" jdbcType="INTEGER"/>
<result property="createdUserCount" column="created_user_count" jdbcType="INTEGER"/>
<result property="updatedUserCount" column="updated_user_count" jdbcType="INTEGER"/>
<result property="deletedUserCount" column="deleted_user_count" jdbcType="INTEGER"/>
<result property="bannedUserCount" column="banned_user_count" jdbcType="INTEGER"/>
<result property="errorMessage" column="error_message" jdbcType="VARCHAR"/>
<result property="createdAt" column="created_at" jdbcType="TIMESTAMP"/>
<result property="updatedAt" column="updated_at" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="Base_Column_List">
id,admin_id,status,s3_file_path,
total_department_count,created_department_count,updated_department_count,deleted_department_count,
total_user_count,created_user_count,updated_user_count,deleted_user_count,banned_user_count,
error_message,created_at,updated_at
</sql>
</mapper>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xyz.playedu.common.mapper.LdapSyncUserDetailMapper">
<!-- 通用映射结果集 -->
<resultMap id="BaseResultMap" type="xyz.playedu.common.domain.LdapSyncUserDetail">
<id column="id" property="id"/>
<result column="record_id" property="recordId"/>
<result column="user_id" property="userId"/>
<result column="uuid" property="uuid"/>
<result column="dn" property="dn"/>
<result column="cn" property="cn"/>
<result column="uid" property="uid"/>
<result column="email" property="email"/>
<result column="ou" property="ou"/>
<result column="action" property="action"/>
<result column="created_at" property="createdAt"/>
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, record_id, user_id, uuid, dn, cn, uid, email, ou, action, created_at
</sql>
</mapper>

View File

@ -731,6 +731,87 @@ public class MigrationCheck implements CommandLineRunner {
"""); """);
} }
}); });
add(
new HashMap<>() {
{
put("table", "ldap_sync_record");
put("name", "20250517_13_23_ldap_sync_record");
put(
"sql",
"""
CREATE TABLE `ldap_sync_record` (
`id` int NOT NULL AUTO_INCREMENT,
`admin_id` int NOT NULL DEFAULT '0' COMMENT '执行同步的管理员ID0表示系统自动执行',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '状态0-进行中1-成功2-失败',
`s3_file_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'S3存储中的文件路径',
`total_department_count` int NOT NULL DEFAULT '0' COMMENT '总部门数量',
`created_department_count` int NOT NULL DEFAULT '0' COMMENT '新增部门数量',
`updated_department_count` int NOT NULL DEFAULT '0' COMMENT '更新部门数量',
`deleted_department_count` int NOT NULL DEFAULT '0' COMMENT '删除部门数量',
`total_user_count` int NOT NULL DEFAULT '0' COMMENT '总用户数量',
`created_user_count` int NOT NULL DEFAULT '0' COMMENT '新增用户数量',
`updated_user_count` int NOT NULL DEFAULT '0' COMMENT '更新用户数量',
`deleted_user_count` int NOT NULL DEFAULT '0' COMMENT '删除用户数量',
`banned_user_count` int NOT NULL DEFAULT '0' COMMENT '被禁止的用户数量',
`error_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '错误信息',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='LDAP同步记录表';
""");
}
});
add(
new HashMap<>() {
{
put("table", "ldap_sync_department_detail");
put("name", "20250519_10_25_01_ldap_sync_department_detail");
put(
"sql",
"""
CREATE TABLE `ldap_sync_department_detail` (
`id` int NOT NULL AUTO_INCREMENT,
`record_id` int NOT NULL COMMENT '关联的同步记录ID',
`department_id` int NOT NULL DEFAULT '0' COMMENT '关联的部门ID',
`uuid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'LDAP部门UUID',
`dn` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'LDAP部门DN',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '部门名称',
`action` tinyint NOT NULL COMMENT '操作1-新增2-更新3-删除4-无变化',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `record_id` (`record_id`),
KEY `department_id` (`department_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='LDAP部门同步详情表';
""");
}
});
add(
new HashMap<>() {
{
put("table", "ldap_sync_user_detail");
put("name", "20250519_10_25_02_ldap_sync_user_detail");
put(
"sql",
"""
CREATE TABLE `ldap_sync_user_detail` (
`id` bigint NOT NULL AUTO_INCREMENT,
`record_id` int NOT NULL COMMENT '关联的同步记录ID',
`user_id` bigint NOT NULL DEFAULT '0' COMMENT '关联的用户ID',
`uuid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'LDAP用户UUID',
`dn` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'LDAP用户DN',
`cn` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名称',
`uid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户ID/登录名',
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户邮箱',
`ou` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户部门路径',
`action` tinyint NOT NULL COMMENT '操作1-新增2-更新3-删除4-无变化5-禁止',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `record_id` (`record_id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='LDAP用户同步详情表';
""");
}
});
} }
}; };