零售版文书档案系统从零搭建实战指南

系统架构与核心组件选型

零售行业文书档案系统需要处理销售单据、合同协议、资质证照等结构化与非结构化数据。我们采用前后端分离架构,前端使用Vue 3 + Element Plus,后端使用Spring Boot 2.7,数据库使用MySQL 8.0,文件存储使用MinIO对象存储。

技术栈版本要求

  • JDK 17.0.2或更高版本
  • Node.js 18.12.0或更高版本
  • MySQL 8.0.32或更高版本
  • Redis 7.0.8或更高版本

环境准备与依赖安装

后端环境配置

创建Spring Boot项目,在pom.xml中添加以下核心依赖:

```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-jpa mysql mysql-connector-java 8.0.33 io.minio minio 8.5.2 org.projectlombok lombok true ```

前端环境配置

创建Vue项目并安装必要依赖:

```bash npm create vue@latest retail-doc-system cd retail-doc-system npm install element-plus axios vue-router@4 vuex@4 ```

数据库设计与初始化

核心表结构设计

执行以下SQL创建核心表:

```sql CREATE DATABASE retail_doc_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE retail_doc_db; CREATE TABLE document_category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category_code VARCHAR(50) NOT NULL UNIQUE, category_name VARCHAR(100) NOT NULL, parent_id BIGINT DEFAULT 0, sort_order INT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_parent_id (parent_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE document ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doc_number VARCHAR(100) NOT NULL UNIQUE, doc_name VARCHAR(200) NOT NULL, category_id BIGINT NOT NULL, doc_type ENUM('CONTRACT', 'INVOICE', 'LICENSE', 'REPORT', 'OTHER') NOT NULL, store_code VARCHAR(50) NOT NULL, file_key VARCHAR(500) NOT NULL, file_size BIGINT NOT NULL, mime_type VARCHAR(100), status ENUM('DRAFT', 'ACTIVE', 'ARCHIVED', 'DELETED') DEFAULT 'DRAFT', metadata JSON, created_by VARCHAR(50), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_category_id (category_id), INDEX idx_store_code (store_code), INDEX idx_status (status), INDEX idx_created_at (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE document_tag ( id BIGINT PRIMARY KEY AUTO_INCREMENT, document_id BIGINT NOT NULL, tag_name VARCHAR(50) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_document_tag (document_id, tag_name), INDEX idx_tag_name (tag_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ```

初始化基础数据

插入必要的分类数据:

```sql INSERT INTO document_category (category_code, category_name, parent_id, sort_order) VALUES ('SALES', '销售单据', 0, 1), ('SALES_INVOICE', '销售发票', 1, 1), ('SALES_CONTRACT', '销售合同', 1, 2), ('FINANCE', '财务凭证', 0, 2), ('FINANCE_RECEIPT', '收款凭证', 4, 1), ('FINANCE_PAYMENT', '付款凭证', 4, 2), ('LEGAL', '法务文件', 0, 3), ('LEGAL_LICENSE', '资质证照', 7, 1), ('LEGAL_AGREEMENT', '合作协议', 7, 2); ```

文件存储服务配置

MinIO安装与配置

使用Docker快速部署MinIO:

```bash docker run -d \ -p 9000:9000 \ -p 9001:9001 \ --name minio \ -v /mnt/data:/data \ -e "MINIO_ROOT_USER=admin" \ -e "MINIO_ROOT_PASSWORD=your_strong_password" \ minio/minio server /data --console-address ":9001" ```

零售版文书档案系统从零搭建实战指南

在Spring Boot中配置MinIO客户端:

```yaml application.yml minio: endpoint: http://localhost:9000 access-key: admin secret-key: your_strong_password bucket-name: retail-documents ```

创建MinIO配置类:

```java @Configuration public class MinioConfig { @Value("${minio.endpoint}") private String endpoint; @Value("${minio.access-key}") private String accessKey; @Value("${minio.secret-key}") private String secretKey; @Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } } ```

核心功能实现

文件上传服务

创建文件上传服务类:

```java @Service @Slf4j public class DocumentUploadService { @Value("${minio.bucket-name}") private String bucketName; @Autowired private MinioClient minioClient; public String uploadFile(MultipartFile file, String storeCode) throws Exception { // 生成唯一文件键 String originalFilename = file.getOriginalFilename(); String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); String fileKey = storeCode + "/" + UUID.randomUUID() + extension; // 创建存储桶(如果不存在) boolean found = minioClient.bucketExists(BucketExistsArgs.builder() .bucket(bucketName) .build()); if (!found) { minioClient.makeBucket(MakeBucketArgs.builder() .bucket(bucketName) .build()); } // 上传文件 minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(fileKey) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build() ); return fileKey; } public InputStream downloadFile(String fileKey) throws Exception { return minioClient.getObject( GetObjectArgs.builder() .bucket(bucketName) .object(fileKey) .build() ); } } ```

文档管理控制器

实现RESTful API接口:

```java @RestController @RequestMapping("/api/documents") @RequiredArgsConstructor public class DocumentController { private final DocumentService documentService; private final DocumentUploadService uploadService; @PostMapping("/upload") public ApiResponse uploadDocument( @RequestParam("file") MultipartFile file, @RequestParam String storeCode, @RequestParam String categoryCode) { try { String fileKey = uploadService.uploadFile(file, storeCode); Document document = new Document(); document.setDocNumber(generateDocNumber()); document.setDocName(file.getOriginalFilename()); document.setStoreCode(storeCode); document.setFileKey(fileKey); document.setFileSize(file.getSize()); document.setMimeType(file.getContentType()); documentService.saveDocument(document, categoryCode); return ApiResponse.success(document.getDocNumber()); } catch (Exception e) { return ApiResponse.error("文件上传失败: " + e.getMessage()); } } @GetMapping("/{docNumber}") public ResponseEntity downloadDocument(@PathVariable String docNumber) { Document document = documentService.getDocument(docNumber); try { InputStream inputStream = uploadService.downloadFile(document.getFileKey()); ByteArrayResource resource = new ByteArrayResource(inputStream.readAllBytes()); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + document.getDocName() + "\"") .contentType(MediaType.parseMediaType(document.getMimeType())) .body(resource); } catch (Exception e) { throw new RuntimeException("文件下载失败", e); } } @GetMapping("/search") public ApiResponse> searchDocuments( @RequestParam(required = false) String storeCode, @RequestParam(required = false) String categoryCode, @RequestParam(required = false) String keyword, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { Page documents = documentService.searchDocuments( storeCode, categoryCode, keyword, startDate, endDate, page, size); List result = documents.getContent().stream() .map(this::convertToVO) .collect(Collectors.toList()); return ApiResponse.success(result); } private String generateDocNumber() { return "DOC" + System.currentTimeMillis() + String.format("%04d", ThreadLocalRandom.current().nextInt(10000)); } } ```

前端界面实现

文件上传组件

创建DocumentUpload.vue组件:

```vue ```

文档搜索组件

创建DocumentSearch.vue组件:

```vue