一个完整的档案数字化系统包含三个核心模块:扫描采集端、数据处理服务端、档案管理Web端。以下是经过生产验证的技术栈:
使用Python + OpenCV实现,确保跨平台兼容性。关键依赖包版本必须精确匹配:
```python requirements.txt opencv-python==4.8.1.78 PyPDF2==3.0.1 pillow==10.1.0 pywin32==306 Windows扫描仪驱动 ```扫描仪驱动统一采用TWAIN协议,这是所有扫描仪厂商支持的标准协议。
采用Spring Boot + MySQL + Redis + MinIO组合:
创建MySQL数据库并执行以下DDL:
```sql CREATE DATABASE archive_digital DEFAULT CHARACTER SET utf8mb4; USE archive_digital; CREATE TABLE archive_category ( id INT PRIMARY KEY AUTO_INCREMENT, category_code VARCHAR(50) UNIQUE NOT NULL, category_name VARCHAR(100) NOT NULL, parent_id INT DEFAULT 0, sort_order INT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE archive_document ( id BIGINT PRIMARY KEY AUTO_INCREMENT, archive_no VARCHAR(100) UNIQUE NOT NULL COMMENT '档案编号', title VARCHAR(500) NOT NULL, category_id INT NOT NULL, keywords VARCHAR(1000), page_count INT DEFAULT 0, file_size BIGINT DEFAULT 0, storage_path VARCHAR(500) NOT NULL COMMENT 'MinIO存储路径', scan_date DATE NOT NULL, status TINYINT DEFAULT 1 COMMENT '1-正常 2-借出 3-销毁', created_by VARCHAR(50), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_category (category_id), INDEX idx_status (status), INDEX idx_scan_date (scan_date) ); ```使用Docker快速部署:
```bash 创建存储目录 mkdir -p /opt/minio/data 启动MinIO容器 docker run -d \ -p 9000:9000 \ -p 9001:9001 \ --name minio \ -v /opt/minio/data:/data \ -e "MINIO_ROOT_USER=admin" \ -e "MINIO_ROOT_PASSWORD=your_secure_password" \ quay.io/minio/minio server /data --console-address ":9001" ```访问 http://服务器IP:9001 登录控制台,创建名为"archive"的存储桶,将访问权限设置为private。

scan_controller.py:
```python import cv2 import numpy as np from PIL import Image import pytesseract import os from datetime import datetime class DocumentScanner: def __init__(self, dpi=300, color_mode='RGB'): self.dpi = dpi self.color_mode = color_mode def scan_from_twain(self, scanner_name=None): """通过TWAIN协议调用扫描仪""" import win32com.client 初始化TWAIN twain = win32com.client.Dispatch("Twain.Twain") 选择扫描仪 if scanner_name: for i in range(twain.SourceCount): if twain.SourceName(i) == scanner_name: twain.SelectSource(i) break else: twain.SelectDefaultSource() 设置扫描参数 twain.Resolution = self.dpi twain.PixelType = 2 if self.color_mode == 'RGB' else 1 2=彩色, 1=灰度 开始扫描 if twain.Acquire(): 获取扫描图像 image = twain.GetImage() return self._process_scanned_image(image) return None def _process_scanned_image(self, image_data): """图像预处理:纠偏、去黑边、增强""" 转换为OpenCV格式 img = cv2.cvtColor(np.array(image_data), cv2.COLOR_RGB2BGR) 1. 自动纠偏 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) edges = cv2.Canny(gray, 50, 150, apertureSize=3) lines = cv2.HoughLinesP(edges, 1, np.pi/180, 100, minLineLength=100, maxLineGap=10) if lines is not None: angles = [] for line in lines: x1, y1, x2, y2 = line[0] angle = np.degrees(np.arctan2(y2 - y1, x2 - x1)) if abs(angle) < 45: 只考虑接近水平的线 angles.append(angle) if angles: avg_angle = np.mean(angles) (h, w) = img.shape[:2] center = (w // 2, h // 2) M = cv2.getRotationMatrix2D(center, avg_angle, 1.0) img = cv2.warpAffine(img, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE) 2. 自动裁剪黑边 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, thresh = cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY) contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: cnt = max(contours, key=cv2.contourArea) x, y, w, h = cv2.boundingRect(cnt) img = img[y:y+h, x:x+w] 3. 图像增强 lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) l, a, b = cv2.split(lab) clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8)) l = clahe.apply(l) lab = cv2.merge([l, a, b]) img = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR) return img def extract_text(self, image): """OCR文字识别""" 转换为灰度图 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 使用Tesseract OCR custom_config = r'--oem 3 --psm 6 -l chi_sim+eng' text = pytesseract.image_to_string(gray, config=custom_config) 提取关键词(简单实现) keywords = self._extract_keywords(text) return text, keywords def _extract_keywords(self, text): """从文本中提取关键词""" 这里可以接入NLP服务,简单版本使用词频统计 import jieba import jieba.analyse 设置自定义词典 jieba.load_userdict('archive_dict.txt') 提取关键词 keywords = jieba.analyse.extract_tags(text, topK=10, withWeight=False) return keywords ```ArchiveStorageService.java:
```java @Service public class ArchiveStorageService { @Value("${minio.endpoint}") private String endpoint; @Value("${minio.access-key}") private String accessKey; @Value("${minio.secret-key}") private String secretKey; @Value("${minio.bucket-name}") private String bucketName; private MinioClient minioClient; @PostConstruct public void init() { this.minioClient = MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } public String uploadDocument(MultipartFile file, String archiveNo) throws Exception { // 生成存储路径 String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); String fileName = archiveNo + "_" + System.currentTimeMillis() + getFileExtension(file.getOriginalFilename()); String objectName = datePath + "/" + fileName; // 上传到MinIO minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build() ); // 生成缩略图 generateThumbnail(file, objectName); return objectName; } private void generateThumbnail(MultipartFile file, String objectName) throws Exception { // 使用Thumbnailator生成缩略图 BufferedImage originalImage = ImageIO.read(file.getInputStream()); BufferedImage thumbnail = Thumbnails.of(originalImage) .size(200, 200) .outputFormat("jpg") .asBufferedImage(); // 上传缩略图 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(thumbnail, "jpg", baos); String thumbnailName = objectName.replaceFirst("(\\.\\w+)$", "_thumb$1"); minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(thumbnailName) .stream(new ByteArrayInputStream(baos.toByteArray()), baos.size(), -1) .contentType("image/jpeg") .build() ); } public InputStream downloadDocument(String objectName) throws Exception { return minioClient.getObject( GetObjectArgs.builder() .bucket(bucketName) .object(objectName) .build() ); } } ```使用MySQL全文索引配合Elasticsearch:
```sql -- 在archive_document表上添加全文索引 ALTER TABLE archive_document ADD FULLTEXT INDEX ft_title_keywords (title, keywords) WITH PARSER ngram; -- 全文检索查询示例 SELECT FROM archive_document WHERE MATCH(title, keywords) AGAINST('项目合同 2023' IN NATURAL LANGUAGE MODE) AND status = 1 ORDER BY scan_date DESC LIMIT 20; ```对于更复杂的检索需求,集成Elasticsearch:
```java @Configuration public class ElasticsearchConfig { @Bean public RestHighLevelClient elasticsearchClient() { return new RestHighLevelClient( RestClient.builder(new HttpHost("localhost", 9200, "http")) ); } @Bean public ElasticsearchOperations elasticsearchTemplate() { return new ElasticsearchRestTemplate(elasticsearchClient()); } } // 文档索引服务 @Service public class ArchiveIndexService { @Autowired private ElasticsearchOperations elasticsearchOperations; public void indexDocument(ArchiveDocument doc) { IndexQuery indexQuery = new IndexQueryBuilder() .withId(doc.getId().toString()) .withObject(doc) .build(); elasticsearchOperations.index(indexQuery, IndexCoordinates.of("archive_documents")); } public List/etc/nginx/conf.d/archive.conf:
```nginx upstream archive_backend { server 127.0.0.1:8080; keepalive 32; } server { listen 80; server_name archive.yourcompany.com; 前端静态文件 location / { root /var/www/archive-frontend; index index.html; try_files $uri $uri/ /index.html; } API代理 location /api/ { proxy_pass http://archive_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; 文件上传超时设置 proxy_connect_timeout 300s; proxy_send_timeout 300s; proxy_read_timeout 300s; client_max_body_size 500M; } MinIO代理 location /minio/ { proxy_pass http://127.0.0.1:9000; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ```backup_archive.sh:
```bash !/bin/bash BACKUP_DIR="/backup/archive" DATE=$(date +%Y%m%d_%H%M%S) RETENTION_DAYS=30 备份MySQL数据库 mysqldump -uarchive_user -p'your_mysql_password' \ --single-transaction \ --routines \ --events \ archive_digital > ${BACKUP_DIR}/archive_db_${DATE}.sql 压缩数据库备份 gzip ${BACKUP_DIR}/archive_db_${DATE}.sql 备份MinIO数据(使用mc客户端) mc mirror --overwrite \ local/minio/archive \ ${BACKUP_DIR}/minio_${DATE} 清理过期备份 find ${BACKUP_DIR} -name ".gz" -mtime +${RETENTION_DAYS} -delete find ${BACKUP_DIR} -name "minio_" -type d -mtime +${RETENTION_DAYS} -exec rm -rf {} \; 记录备份日志 echo "${DATE} 备份完成" >> ${BACKUP_DIR}/backup.log ```使用Prometheus + Grafana监控:
```yaml prometheus.yml scrape_configs: - job_name: 'archive_application' metrics_path: '/actuator/prometheus' static_configs: - targets: ['localhost:8080'] labels: application: 'archive-system' - job_name: 'mysql' static_configs: - targets: ['localhost:9104'] - job_name: 'redis' static_configs: - targets: ['localhost:9121'] ```在