一个完整的航运/港口数字档案馆系统,其核心是安全、可检索、可长期保存的档案数据仓库。我们采用微服务架构,确保系统可扩展、易维护。以下是技术栈选择:
在开始编码前,需要在服务器上部署依赖的中间件。我们使用Docker Compose进行一键部署。
在服务器上创建 docker-compose.yml 文件,内容如下:
version: '3.8'
services:
postgres:
image: postgres:14-alpine
container_name: archive-postgres
environment:
POSTGRES_DB: port_archive
POSTGRES_USER: admin
POSTGRES_PASSWORD: YourStrong@Pass123
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- archive-net
elasticsearch:
image: elasticsearch:8.6.2
container_name: archive-es
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- es_data:/usr/share/elasticsearch/data
ports:
- "9200:9200"
networks:
- archive-net
minio:
image: minio/minio:latest
container_name: archive-minio
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin123
volumes:
- minio_data:/data
ports:
- "9000:9000"
- "9001:9001"
networks:
- archive-net
flowable:
image: flowable/flowable-rest:latest
container_name: archive-flowable
environment:
SPRING_DATASOURCE_DRIVER_CLASS_NAME: org.postgresql.Driver
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/flowable_db
SPRING_DATASOURCE_USERNAME: admin
SPRING_DATASOURCE_PASSWORD: YourStrong@Pass123
ports:
- "8081:8080"
depends_on:
- postgres
networks:
- archive-net
volumes:
postgres_data:
es_data:
minio_data:
networks:
archive-net:
driver: bridge
在文件所在目录执行命令启动所有服务:docker-compose up -d。等待所有容器状态变为 Up。
使用Spring Initializr (https://start.spring.io/) 生成项目,或直接创建Maven项目,核心pom.xml依赖如下:
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-data-elasticsearch
io.minio
minio
8.5.2
org.flowable
flowable-spring-boot-starter
6.7.0
org.postgresql
postgresql
runtime
在src/main/resources/application.yml中配置所有连接信息:

spring:
datasource:
url: jdbc:postgresql://localhost:5432/port_archive
username: admin
password: YourStrong@Pass123
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
elasticsearch:
uris: http://localhost:9200
minio:
endpoint: http://localhost:9000
accessKey: minioadmin
secretKey: minioadmin123
bucketName: port-archive-files
flowable:
async-executor-activate: false
database-schema-update: true
创建实体类ArchiveDocument.java,定义档案元数据:
@Entity
@Table(name = "archive_document")
@Document(indexName = "archive_docs")
public class ArchiveDocument {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String archiveNumber; // 档案编号,规则:PORT-YEAR-0001
@Column(nullable = false)
private String title; // 档案标题
@Column(length = 1000)
private String description; // 档案描述
@Column(nullable = false)
private String documentType; // 文件类型:船舶登记、货物清单、装卸记录、安全检查
private LocalDate archiveDate; // 归档日期
private LocalDate documentDate; // 文件形成日期
@Column(nullable = false)
private String fileKey; // 存储在MinIO中的文件唯一标识
private String fileSize;
private String fileFormat;
@Column(nullable = false)
private String securityLevel; // 密级:公开、内部、秘密、机密
@CreatedDate
private LocalDateTime createTime;
}
创建FileStorageService.java,封装MinIO操作:
@Service
public class FileStorageService {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
@Value("${minio.bucketName}")
private String bucketName;
private MinioClient minioClient;
@PostConstruct
public void init() throws Exception {
minioClient = MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!found) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
}
public String uploadFile(MultipartFile file, String archiveNumber) throws Exception {
String fileName = archiveNumber + "_" + System.currentTimeMillis() + "_" + file.getOriginalFilename();
String contentType = file.getContentType();
long fileSize = file.getSize();
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.stream(file.getInputStream(), fileSize, -1)
.contentType(contentType)
.build()
);
return fileName; // 返回文件在MinIO中的唯一key
}
public String getFileUrl(String fileKey) {
return endpoint + "/" + bucketName + "/" + fileKey;
}
}
创建ArchiveSearchRepository.java接口,继承ElasticsearchRepository:
public interface ArchiveSearchRepository extends ElasticsearchRepository {
List findByTitleOrDescription(String title, String description);
Page findBySecurityLevel(String securityLevel, Pageable pageable);
}
创建ArchiveSearchService.java,实现复杂查询:
@Service
public class ArchiveSearchService {
@Autowired
private ElasticsearchRestTemplate elasticsearchTemplate;
public Page advancedSearch(SearchRequest request, Pageable pageable) {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
if (StringUtils.hasText(request.getKeyword())) {
boolQuery.must(QueryBuilders.multiMatchQuery(request.getKeyword(), "title", "description"));
}
if (StringUtils.hasText(request.getDocumentType())) {
boolQuery.must(QueryBuilders.termQuery("documentType", request.getDocumentType()));
}
if (request.getStartDate() != null && request.getEndDate() != null) {
boolQuery.must(QueryBuilders.rangeQuery("documentDate")
.gte(request.getStartDate())
.lte(request.getEndDate()));
}
// 权限控制:只能查询密级等于或低于用户权限的档案
boolQuery.must(QueryBuilders.termsQuery("securityLevel", getAllowedLevels(request.getUserSecurityLevel())));
queryBuilder.withQuery(boolQuery);
queryBuilder.withPageable(pageable);
queryBuilder.withSort(SortBuilders.fieldSort("documentDate").order(SortOrder.DESC));
return elasticsearchTemplate.search(queryBuilder.build(), ArchiveDocument.class);
}
private String[] getAllowedLevels(String userLevel) {
// 密级顺序:公开 < 内部 < 秘密 < 机密
Map levelMap = Map.of(
"公开", new String[]{"公开"},
"内部", new String[]{"公开", "内部"},
"秘密", new String[]{"公开", "内部", "秘密"},
"机密", new String[]{"公开", "内部", "秘密", "机密"}
);
return levelMap.getOrDefault(userLevel, new String[]{"公开"});
}
}
在src/main/resources/processes/目录下创建archive-approval.bpmn20.xml文件,定义归档审批流程:
${reviewResult == 'reject'}
${reviewResult == 'approve'}
在档案提交服务中启动流程:
@Service
public class ArchiveProcessService {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
public String startArchiveProcess(Long archiveId, String submitterUserId) {
Map variables = new HashMap<>();
variables.put("archiveId", archiveId);
variables.put("submitter", submitterUserId);
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(
"archive_submission",
"ARCHIVE_" + archiveId,
variables
);
return processInstance.getId();
}
public void completeDepartmentReview(String taskId, boolean approved, String comment) {
Map variables = new HashMap<>();
variables.put("reviewResult", approved ? "approve" : "reject");
variables.put("reviewComment", comment);
taskService.complete(taskId, variables);
}
}
创建ArchiveUpload.vue组件,实现文件分片上传:
将文件拖到此处,或点击上传
&
NEWS
相关信息
图片档案整理实用干货 乱存的截图照片再也不用翻找半天
你有没有发现,平时随手存的截图、扫描的合同、拍的工作物料,攒个半年就能有大几千张,真要找某张图的时候,翻半个小时都找不到,急的满头汗?这事儿吧我之前也踩过坑,刚工作那会攒了一万多张图,分类分了俩小时,...
2026年06月18日 04:55:04
档案数字化工作流程
你有没有过这种扎心的场景?上周我帮朋友找去年的项目合同,他蹲在档案室的铁柜里翻了40分钟,灰落一脸,最后还是用手机拍的扫描件被客户说模糊,丢了个小项目?说白了,纸质档案就是个“麻烦精”,找的时候难死,...
2026年06月18日 04:55:04