上市公司档案管理系统:从零构建可落地的技术方案

一、技术选型与项目初始化

1.1 核心架构选择

采用前后端分离架构,后端使用Spring Boot 3.1.5,前端使用Vue 3.3.4。数据库选择PostgreSQL 15,文档存储使用MinIO对象存储。

1.2 开发环境搭建

安装JDK 17:

```bash Ubuntu/Debian sudo apt update sudo apt install openjdk-17-jdk CentOS/RHEL sudo yum install java-17-openjdk-devel 验证安装 java -version ```

安装Node.js 18.17.0:

```bash 使用nvm安装 curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash source ~/.bashrc nvm install 18.17.0 nvm use 18.17.0 ```

1.3 项目创建

后端项目创建:

```bash 使用Spring Initializr创建 curl https://start.spring.io/starter.zip \ -d type=maven-project \ -d language=java \ -d bootVersion=3.1.5 \ -d baseDir=archive-backend \ -d groupId=com.company.archive \ -d artifactId=archive-system \ -d name=ArchiveSystem \ -d description="上市公司档案管理系统" \ -d packageName=com.company.archive.system \ -d packaging=jar \ -d javaVersion=17 \ -d dependencies=web,data-jpa,postgresql,security,validation \ -o archive-backend.zip unzip archive-backend.zip ```

前端项目创建:

```bash npm create vue@latest archive-frontend cd archive-frontend npm install ```

二、数据库设计与配置

2.1 PostgreSQL安装与配置

安装PostgreSQL 15:

```bash Ubuntu/Debian sudo apt install postgresql-15 postgresql-contrib-15 CentOS/RHEL sudo dnf install postgresql15-server postgresql15-contrib sudo /usr/pgsql-15/bin/postgresql-15-setup initdb sudo systemctl start postgresql-15 ```

创建数据库和用户:

```sql -- 以postgres用户登录 sudo -u postgres psql -- 创建数据库 CREATE DATABASE archive_db ENCODING 'UTF8'; -- 创建用户并授权 CREATE USER archive_user WITH PASSWORD 'StrongPassword123!'; GRANT ALL PRIVILEGES ON DATABASE archive_db TO archive_user; -- 创建扩展(用于全文搜索) \c archive_db CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE EXTENSION IF NOT EXISTS btree_gin; ```

2.2 核心表结构设计

创建档案相关表:

```sql -- 公司基本信息表 CREATE TABLE companies ( id BIGSERIAL PRIMARY KEY, stock_code VARCHAR(20) NOT NULL UNIQUE, company_name VARCHAR(200) NOT NULL, listed_date DATE NOT NULL, industry VARCHAR(100), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 档案分类表 CREATE TABLE archive_categories ( id BIGSERIAL PRIMARY KEY, category_code VARCHAR(50) NOT NULL UNIQUE, category_name VARCHAR(100) NOT NULL, parent_id BIGINT REFERENCES archive_categories(id), retention_years INTEGER NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 档案主表 CREATE TABLE archives ( id BIGSERIAL PRIMARY KEY, archive_number VARCHAR(100) NOT NULL UNIQUE, title VARCHAR(500) NOT NULL, company_id BIGINT NOT NULL REFERENCES companies(id), category_id BIGINT NOT NULL REFERENCES archive_categories(id), document_type VARCHAR(50) NOT NULL, confidential_level VARCHAR(20) NOT NULL, storage_path TEXT NOT NULL, file_size BIGINT NOT NULL, file_hash VARCHAR(64) NOT NULL, upload_user_id BIGINT NOT NULL, upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, effective_date DATE, expiry_date DATE, status VARCHAR(20) DEFAULT 'ACTIVE', version INTEGER DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 档案版本表 CREATE TABLE archive_versions ( id BIGSERIAL PRIMARY KEY, archive_id BIGINT NOT NULL REFERENCES archives(id), version_number INTEGER NOT NULL, change_description TEXT, storage_path TEXT NOT NULL, file_hash VARCHAR(64) NOT NULL, created_by BIGINT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(archive_id, version_number) ); -- 创建索引 CREATE INDEX idx_archives_company ON archives(company_id); CREATE INDEX idx_archives_category ON archives(category_id); CREATE INDEX idx_archives_status ON archives(status); CREATE INDEX idx_archives_expiry ON archives(expiry_date); CREATE INDEX idx_archives_number ON archives(archive_number); CREATE INDEX idx_archives_upload_time ON archives(upload_time); ```

三、后端核心功能实现

3.1 项目依赖配置

上市公司档案管理系统:从零构建可落地的技术方案

pom.xml关键依赖:

```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-validation org.postgresql postgresql runtime org.apache.tika tika-core 2.9.0 org.projectlombok lombok true ```

3.2 应用配置文件

application.yml配置:

```yaml server: port: 8080 servlet: context-path: /api spring: datasource: url: jdbc:postgresql://localhost:5432/archive_db username: archive_user password: StrongPassword123! driver-class-name: org.postgresql.Driver hikari: maximum-pool-size: 10 minimum-idle: 5 connection-timeout: 30000 jpa: database-platform: org.hibernate.dialect.PostgreSQLDialect hibernate: ddl-auto: update show-sql: false properties: hibernate: format_sql: true jdbc: batch_size: 20 servlet: multipart: max-file-size: 500MB max-request-size: 500MB archive: storage: type: minio minio: endpoint: http://localhost:9000 access-key: minioadmin secret-key: minioadmin bucket-name: archive-documents local: path: /data/archive/files security: jwt: secret: your-jwt-secret-key-at-least-256-bits-long expiration: 86400000 ```

3.3 文件上传服务实现

创建MinIO配置类:

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

文件存储服务实现:

```java @Service @Slf4j public class FileStorageService { @Value("${archive.storage.minio.bucket-name}") private String bucketName; private final MinioClient minioClient; public FileStorageService(MinioClient minioClient) { this.minioClient = minioClient; initializeBucket(); } private void initializeBucket() { try { boolean exists = minioClient.bucketExists( BucketExistsArgs.builder() .bucket(bucketName) .build() ); if (!exists) { minioClient.makeBucket( MakeBucketArgs.builder() .bucket(bucketName) .build() ); // 设置bucket策略 String policy = """ { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": "", "Action": [ "s3:GetObject" ], "Resource": [ "arn:aws:s3:::%s/" ] } ] } """.formatted(bucketName); minioClient.setBucketPolicy( SetBucketPolicyArgs.builder() .bucket(bucketName) .config(policy) .build() ); } } catch (Exception e) { log.error("初始化MinIO存储桶失败", e); } } public String uploadFile(MultipartFile file, String filePath) { try { // 生成唯一文件名 String originalFilename = file.getOriginalFilename(); String fileExtension = getFileExtension(originalFilename); String uniqueFileName = UUID.randomUUID() + fileExtension; String objectName = filePath + "/" + uniqueFileName; // 计算文件哈希 String fileHash = calculateFileHash(file); // 上传到MinIO minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build() ); return objectName; } catch (Exception e) { throw new RuntimeException("文件上传失败", e); } } private String getFileExtension(String filename) { if (filename == null || !filename.contains(".")) { return ""; } return filename.substring(filename.lastIndexOf(".")); } private String calculateFileHash(MultipartFile file) throws IOException { try (InputStream is = file.getInputStream()) { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] buffer = new byte[8192]; int read; while ((read = is.read(buffer)) > 0) { digest.update(buffer, 0, read); } byte[] hashBytes = digest.digest(); return HexFormat.of().formatHex(hashBytes); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("哈希算法不可用", e); } } } ```

3.4 档案上传接口实现

档案上传DTO:

```java @Data public class ArchiveUploadDTO { @NotBlank(message = "档案编号不能为空") private String archiveNumber; @NotBlank(message = "档案标题不能为空") private String title; @NotNull(message = "公司ID不能为空") private Long companyId; @NotNull(message = "分类ID不能为空") private Long categoryId; @NotBlank(message = "文档类型不能为空") private String documentType; @NotBlank(message = "密级不能为空") private String confidentialLevel; private LocalDate effectiveDate; private LocalDate expiryDate; @NotNull(message = "文件不能为空") private MultipartFile file; } ```

档案上传控制器:

```java @RestController @RequestMapping("/api/archives") @RequiredArgsConstructor public class ArchiveController { private final ArchiveService archiveService; @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity> uploadArchive( @Valid @ModelAttribute ArchiveUploadDTO uploadDTO, @AuthenticationPrincipal UserDetails userDetails) { Long userId = getUserIdFromPrincipal(userDetails); ArchiveDTO archive = archiveService.uploadArchive(uploadDTO, userId); return ResponseEntity.ok(ApiResponse.success(archive)); } @GetMapping("/search") public ResponseEntity>> searchArchives( @RequestParam(required = false) String keyword, @RequestParam(required = false) Long companyId, @RequestParam(required = false) Long categoryId, @RequestParam(required = false) String documentType, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { Page archives = archiveService.searchArchives( keyword, companyId, categoryId, documentType, page, size); return ResponseEntity.ok(ApiResponse.success(archives)); } } ```

四、前端实现

4.1 项目依赖安装

安装必要依赖:

```bash cd archive-frontend npm install axios@1.5.0 element-plus@2.3.8 @element-plus/icons-vue@2.1.0 vue-router@4.2.4 pinia@2.1.6 ```

4.2 档案上传组件实现

创建ArchiveUpload.vue:

```vue