diff --git a/api/src/main/java/com/databasir/api/DocumentController.java b/api/src/main/java/com/databasir/api/DocumentController.java
index 7a1b409..52f78e2 100644
--- a/api/src/main/java/com/databasir/api/DocumentController.java
+++ b/api/src/main/java/com/databasir/api/DocumentController.java
@@ -1,11 +1,11 @@
 package com.databasir.api;
 
 import com.databasir.common.JsonData;
-import com.databasir.common.SystemException;
 import com.databasir.core.domain.document.data.DatabaseDocumentResponse;
 import com.databasir.core.domain.document.data.DatabaseDocumentSimpleResponse;
 import com.databasir.core.domain.document.data.DatabaseDocumentVersionResponse;
 import com.databasir.core.domain.document.data.TableDocumentResponse;
+import com.databasir.core.domain.document.generator.DocumentFileType;
 import com.databasir.core.domain.document.service.DocumentService;
 import com.databasir.core.domain.log.annotation.Operation;
 import lombok.RequiredArgsConstructor;
@@ -20,13 +20,8 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
 
-import java.io.IOException;
 import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.List;
-import java.util.UUID;
 
 import static org.springframework.data.domain.Sort.Direction.DESC;
 
@@ -62,24 +57,18 @@ public class DocumentController {
 
     @GetMapping(Routes.Document.EXPORT)
     public ResponseEntity<StreamingResponseBody> getDocumentFiles(@PathVariable Integer projectId,
-                                                                  @RequestParam(required = false) Long version) {
-        String data = documentService.toMarkdown(projectId, version).get();
-        try {
-            Path path = Files.writeString(Paths.get(UUID.randomUUID().toString() + ".md"), data,
-                    StandardCharsets.UTF_8);
-            HttpHeaders headers = new HttpHeaders();
-            headers.setContentDisposition(ContentDisposition.attachment()
-                    .filename("demo.md", StandardCharsets.UTF_8)
-                    .build());
-            byte[] bytes = Files.readAllBytes(path);
-            Files.deleteIfExists(path);
-            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
-            return ResponseEntity.ok()
-                    .headers(headers)
-                    .body(out -> out.write(bytes));
-        } catch (IOException e) {
-            throw new SystemException("System error");
-        }
+                                                                  @RequestParam(required = false)
+                                                                          Long version,
+                                                                  @RequestParam DocumentFileType fileType) {
+        HttpHeaders headers = new HttpHeaders();
+        String fileName = "project[" + projectId + "]." + fileType.getFileExtension();
+        headers.setContentDisposition(ContentDisposition.attachment()
+                .filename("demo.md", StandardCharsets.UTF_8)
+                .build());
+        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
+        return ResponseEntity.ok()
+                .headers(headers)
+                .body(out -> documentService.export(projectId, version, fileType, out));
     }
 
     @GetMapping(Routes.Document.GET_SIMPLE_ONE)
diff --git a/build.gradle b/build.gradle
index 78dfd38..ff76286 100644
--- a/build.gradle
+++ b/build.gradle
@@ -34,6 +34,7 @@ subprojects {
         postgresqlConnectorVersion = '42.3.1'
         hikariVersion = '5.0.0'
         jacksonVersion = '2.13.1'
+        easyExcelVersion = '3.0.5'
     }
 
     dependencies {
@@ -47,6 +48,8 @@ subprojects {
         annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
 
         implementation "org.slf4j:slf4j-api:${slf4jVersion}"
+        implementation "com.alibaba:easyexcel:${easyExcelVersion}"
+
     }
 
     test {
diff --git a/core/build.gradle b/core/build.gradle
index 58cd16d..bb87958 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -30,6 +30,7 @@ dependencies {
     implementation 'com.auth0:java-jwt:3.18.3'
     implementation 'org.commonmark:commonmark:0.18.1'
     implementation 'org.freemarker:freemarker:2.3.31'
+    implementation 'com.alibaba:easyexcel'
 
     implementation 'com.squareup.retrofit2:retrofit:2.9.0'
     implementation 'com.squareup.retrofit2:converter-jackson:2.9.0'
diff --git a/core/src/main/java/com/databasir/core/domain/document/generator/DocumentFileGenerator.java b/core/src/main/java/com/databasir/core/domain/document/generator/DocumentFileGenerator.java
new file mode 100644
index 0000000..a4e83cb
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/domain/document/generator/DocumentFileGenerator.java
@@ -0,0 +1,27 @@
+package com.databasir.core.domain.document.generator;
+
+import com.databasir.core.domain.document.data.DatabaseDocumentResponse;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NonNull;
+
+import java.io.OutputStream;
+
+public interface DocumentFileGenerator {
+
+    boolean support(DocumentFileType type);
+
+    void generate(DocumentFileGenerateContext context, OutputStream outputStream);
+
+    @Getter
+    @Builder
+    class DocumentFileGenerateContext {
+
+        @NonNull
+        private DocumentFileType documentFileType;
+
+        @NonNull
+        private DatabaseDocumentResponse databaseDocument;
+
+    }
+}
diff --git a/core/src/main/java/com/databasir/core/domain/document/generator/DocumentFileType.java b/core/src/main/java/com/databasir/core/domain/document/generator/DocumentFileType.java
new file mode 100644
index 0000000..94e345f
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/domain/document/generator/DocumentFileType.java
@@ -0,0 +1,13 @@
+package com.databasir.core.domain.document.generator;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@AllArgsConstructor
+@Getter
+public enum DocumentFileType {
+
+    MARKDOWN("md"), EXCEL("xlsx");
+
+    private String fileExtension;
+}
diff --git a/core/src/main/java/com/databasir/core/domain/document/generator/ExcelDocumentFileGenerator.java b/core/src/main/java/com/databasir/core/domain/document/generator/ExcelDocumentFileGenerator.java
new file mode 100644
index 0000000..168420a
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/domain/document/generator/ExcelDocumentFileGenerator.java
@@ -0,0 +1,22 @@
+package com.databasir.core.domain.document.generator;
+
+import org.springframework.stereotype.Component;
+
+import java.io.OutputStream;
+
+@Component
+public class ExcelDocumentFileGenerator implements DocumentFileGenerator {
+
+    @Override
+    public boolean support(DocumentFileType type) {
+        return type == DocumentFileType.EXCEL;
+    }
+
+    @Override
+    public void generate(DocumentFileGenerateContext context, OutputStream outputStream) {
+
+    }
+
+    private void buildTableWithSheet() {
+    }
+}
diff --git a/core/src/main/java/com/databasir/core/domain/document/generator/MarkdownDocumentFileGenerator.java b/core/src/main/java/com/databasir/core/domain/document/generator/MarkdownDocumentFileGenerator.java
new file mode 100644
index 0000000..d88e617
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/domain/document/generator/MarkdownDocumentFileGenerator.java
@@ -0,0 +1,165 @@
+package com.databasir.core.domain.document.generator;
+
+import com.databasir.common.SystemException;
+import com.databasir.core.domain.document.data.DatabaseDocumentResponse;
+import com.databasir.core.domain.document.data.TableDocumentResponse;
+import com.databasir.core.render.markdown.MarkdownBuilder;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.function.Function;
+
+@Component
+@Slf4j
+public class MarkdownDocumentFileGenerator implements DocumentFileGenerator {
+
+    @Override
+    public boolean support(DocumentFileType type) {
+        return type == DocumentFileType.MARKDOWN;
+    }
+
+    @Override
+    public void generate(DocumentFileGenerateContext context, OutputStream outputStream) {
+        String fileName = context.getDatabaseDocument().getDatabaseName() + "-" + UUID.randomUUID().toString();
+        String data = markdownData(context);
+        Path tempFile = null;
+        try {
+            tempFile = Files.createTempFile(fileName, ".md");
+            Path path = Files.writeString(tempFile, data, StandardCharsets.UTF_8);
+            byte[] bytes = Files.readAllBytes(path);
+            outputStream.write(bytes);
+        } catch (IOException e) {
+            if (tempFile != null) {
+                try {
+                    Files.deleteIfExists(tempFile);
+                } catch (IOException ex) {
+                    log.warn("delete temp file error", ex);
+                }
+            }
+            throw new SystemException("System error");
+        }
+    }
+
+    private String markdownData(DocumentFileGenerateContext context) {
+        DatabaseDocumentResponse doc = context.getDatabaseDocument();
+        MarkdownBuilder builder = MarkdownBuilder.builder();
+        builder.primaryTitle(doc.getDatabaseName());
+        // overview
+        overviewBuild(builder, doc);
+        // tables
+        doc.getTables().forEach(table -> tableBuild(builder, table));
+        return builder.build();
+    }
+
+    private void overviewBuild(MarkdownBuilder builder, DatabaseDocumentResponse doc) {
+        builder.secondTitle("overview");
+        List<List<String>> overviewContent = new ArrayList<>();
+        for (int i = 0; i < doc.getTables().size(); i++) {
+            TableDocumentResponse table = doc.getTables().get(i);
+            overviewContent.add(List.of((i + 1) + "", table.getName(), table.getType(),
+                    table.getComment()));
+        }
+        builder.table(List.of("", "表名", "类型", "备注"), overviewContent);
+    }
+
+    private void tableBuild(MarkdownBuilder builder, TableDocumentResponse table) {
+        builder.secondTitle(table.getName());
+        columnBuild(builder, table);
+        indexBuild(builder, table);
+        foreignKeyBuild(builder, table);
+        triggerBuild(builder, table);
+    }
+
+    private void columnBuild(MarkdownBuilder builder, TableDocumentResponse table) {
+        Function<TableDocumentResponse.ColumnDocumentResponse, String>
+                columnDefaultValueMapping = column -> {
+            if (Objects.equals(column.getNullable(), "YES")) {
+                return Objects.requireNonNullElse(column.getDefaultValue(), "null");
+            } else {
+                return Objects.requireNonNullElse(column.getDefaultValue(), "");
+            }
+        };
+        builder.thirdTitle("Columns");
+        List<List<String>> columnContent = new ArrayList<>();
+        for (int i = 0; i < table.getColumns().size(); i++) {
+            var column = table.getColumns().get(i);
+            String type;
+            if (column.getDecimalDigits() == null || column.getDecimalDigits() == 0) {
+                type = column.getType() + "(" + column.getSize() + ")";
+            } else {
+                type = column.getType() + "(" + column.getSize() + "," + column.getDecimalDigits() + ")";
+            }
+            columnContent.add(List.of((i + 1) + "",
+                    column.getName(),
+                    type,
+                    column.getIsPrimaryKey() ? "YES" : "NO",
+                    column.getNullable(),
+                    column.getAutoIncrement(),
+                    columnDefaultValueMapping.apply(column),
+                    column.getComment()));
+        }
+        builder.table(List.of("", "名称", "类型", "是否为主键", "可为空", "自增", "默认值", "备注"),
+                columnContent);
+    }
+
+
+    private void indexBuild(MarkdownBuilder builder, TableDocumentResponse table) {
+        builder.thirdTitle("Indexes");
+        List<List<String>> indexContent = new ArrayList<>();
+        for (int i = 0; i < table.getIndexes().size(); i++) {
+            var index = table.getIndexes().get(i);
+            String columnNames = String.join(", ", index.getColumnNames());
+            String isUnique = index.getIsUnique() ? "YES" : "NO";
+            indexContent.add(List.of((i + 1) + "", index.getName(), isUnique, columnNames));
+        }
+        builder.table(List.of("", "名称", "是否唯一", "关联列"), indexContent);
+
+    }
+
+    private void foreignKeyBuild(MarkdownBuilder builder, TableDocumentResponse table) {
+        if (!table.getForeignKeys().isEmpty()) {
+            List<List<String>> foreignKeys = new ArrayList<>();
+            builder.thirdTitle("Foreign Keys");
+            for (int i = 0; i < table.getForeignKeys().size(); i++) {
+                TableDocumentResponse.ForeignKeyDocumentResponse fk = table.getForeignKeys().get(i);
+                List<String> item = List.of(
+                        (i + 1) + "",
+                        fk.getFkName(), fk.getFkColumnName(),
+                        fk.getPkName(), fk.getPkTableName(), fk.getPkColumnName(),
+                        fk.getUpdateRule(), fk.getDeleteRule()
+                );
+                foreignKeys.add(item);
+            }
+            builder.table(
+                    List.of("", "FK Name", "FK Column", "PK Name", "PK Table", "PK Column",
+                            "Update Rule", "Delete Rule"),
+                    foreignKeys
+            );
+        }
+    }
+
+    private void triggerBuild(MarkdownBuilder builder, TableDocumentResponse table) {
+        if (!table.getTriggers().isEmpty()) {
+            List<List<String>> triggerContent = new ArrayList<>();
+            for (int i = 0; i < table.getTriggers().size(); i++) {
+                var trigger = table.getTriggers().get(i);
+                triggerContent.add(List.of((i + 1) + "",
+                        trigger.getName(),
+                        trigger.getTiming(),
+                        trigger.getManipulation(),
+                        trigger.getStatement()));
+            }
+            builder.thirdTitle("Triggers");
+            builder.table(List.of("", "名称", "timing", "manipulation", "statement"), triggerContent);
+        }
+    }
+}
diff --git a/core/src/main/java/com/databasir/core/domain/document/service/DocumentService.java b/core/src/main/java/com/databasir/core/domain/document/service/DocumentService.java
index af5b4c3..2af3c1b 100644
--- a/core/src/main/java/com/databasir/core/domain/document/service/DocumentService.java
+++ b/core/src/main/java/com/databasir/core/domain/document/service/DocumentService.java
@@ -10,10 +10,11 @@ import com.databasir.core.domain.document.data.DatabaseDocumentResponse;
 import com.databasir.core.domain.document.data.DatabaseDocumentSimpleResponse;
 import com.databasir.core.domain.document.data.DatabaseDocumentVersionResponse;
 import com.databasir.core.domain.document.data.TableDocumentResponse;
+import com.databasir.core.domain.document.generator.DocumentFileGenerator;
+import com.databasir.core.domain.document.generator.DocumentFileType;
 import com.databasir.core.infrastructure.connection.DatabaseConnectionService;
 import com.databasir.core.infrastructure.converter.JsonConverter;
 import com.databasir.core.meta.data.DatabaseMeta;
-import com.databasir.core.render.markdown.MarkdownBuilder;
 import com.databasir.dao.impl.*;
 import com.databasir.dao.tables.pojos.*;
 import com.databasir.dao.value.DocumentDiscussionCountPojo;
@@ -26,9 +27,12 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 
+import java.io.OutputStream;
 import java.sql.Connection;
-import java.util.*;
-import java.util.function.Function;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 import java.util.stream.Collectors;
 
 @Service
@@ -72,6 +76,8 @@ public class DocumentService {
 
     private final JsonConverter jsonConverter;
 
+    private final List<DocumentFileGenerator> documentFileGenerators;
+
     @Transactional
     public void syncByProjectId(Integer projectId) {
         projectDao.selectOptionalById(projectId)
@@ -303,82 +309,20 @@ public class DocumentService {
                 .collect(Collectors.toList());
     }
 
-    public Optional<String> toMarkdown(Integer projectId, Long version) {
-        return getOneByProjectId(projectId, version)
-                .map(doc -> {
-                    MarkdownBuilder builder = MarkdownBuilder.builder();
-                    builder.primaryTitle(doc.getDatabaseName());
-                    // overview
-                    builder.secondTitle("overview");
-                    List<List<String>> overviewContent = new ArrayList<>();
-                    for (int i = 0; i < doc.getTables().size(); i++) {
-                        TableDocumentResponse table = doc.getTables().get(i);
-                        overviewContent.add(List.of((i + 1) + "", table.getName(), table.getType(),
-                                table.getComment()));
-                    }
-                    builder.table(List.of("", "表名", "类型", "备注"), overviewContent);
-
-                    Function<TableDocumentResponse.ColumnDocumentResponse, String>
-                            columnDefaultValueMapping = column -> {
-                        if (Objects.equals(column.getNullable(), "YES")) {
-                            return Objects.requireNonNullElse(column.getDefaultValue(), "null");
-                        } else {
-                            return Objects.requireNonNullElse(column.getDefaultValue(), "");
-                        }
-                    };
-                    // tables
-                    doc.getTables().forEach(table -> {
-                        builder.secondTitle(table.getName());
-
-                        // columns
-                        List<List<String>> columnContent = new ArrayList<>();
-                        for (int i = 0; i < table.getColumns().size(); i++) {
-                            var column = table.getColumns().get(i);
-                            String type;
-                            if (column.getDecimalDigits() == null || column.getDecimalDigits() == 0) {
-                                type = table.getType() + "(" + column.getSize() + ")";
-                            } else {
-                                type = table.getType() + "(" + column.getSize() + "," + column.getDecimalDigits() + ")";
-                            }
-                            columnContent.add(List.of((i + 1) + "",
-                                    column.getName(),
-                                    type,
-                                    column.getIsPrimaryKey() ? "YES" : "NO",
-                                    column.getNullable(),
-                                    column.getAutoIncrement(),
-                                    columnDefaultValueMapping.apply(column),
-                                    column.getComment()));
-                        }
-                        builder.thirdTitle("columns");
-                        builder.table(List.of("", "名称", "类型", "是否为主键", "可为空", "自增", "默认值", "备注"),
-                                columnContent);
-
-                        // indexes
-                        List<List<String>> indexContent = new ArrayList<>();
-                        for (int i = 0; i < table.getIndexes().size(); i++) {
-                            var index = table.getIndexes().get(i);
-                            String columnNames = String.join(", ", index.getColumnNames());
-                            String isUnique = index.getIsUnique() ? "YES" : "NO";
-                            indexContent.add(List.of((i + 1) + "", index.getName(), isUnique, columnNames));
-                        }
-                        builder.thirdTitle("indexes");
-                        builder.table(List.of("", "名称", "是否唯一", "关联列"), indexContent);
-
-                        if (!table.getTriggers().isEmpty()) {
-                            List<List<String>> triggerContent = new ArrayList<>();
-                            for (int i = 0; i < table.getTriggers().size(); i++) {
-                                var trigger = table.getTriggers().get(i);
-                                triggerContent.add(List.of((i + 1) + "",
-                                        trigger.getName(),
-                                        trigger.getTiming(),
-                                        trigger.getManipulation(),
-                                        trigger.getStatement()));
-                            }
-                            builder.thirdTitle("triggers");
-                            builder.table(List.of("", "名称", "timing", "manipulation", "statement"), triggerContent);
-                        }
-                    });
-                    return builder.build();
+    public void export(Integer projectId,
+                       Long version,
+                       DocumentFileType type,
+                       OutputStream out) {
+        getOneByProjectId(projectId, version)
+                .ifPresent(doc -> {
+                    var context = DocumentFileGenerator.DocumentFileGenerateContext.builder()
+                            .documentFileType(type)
+                            .databaseDocument(doc)
+                            .build();
+                    documentFileGenerators.stream()
+                            .filter(g -> g.support(type))
+                            .findFirst()
+                            .ifPresent(generator -> generator.generate(context, out));
                 });
     }
 }