边防检查档案管理软件的核心是数据安全与业务稳定性,我们采用前后端分离架构,后端使用Java 17 + Spring Boot,前端使用Vue 3,数据库采用PostgreSQL 14并配置主从同步。
使用Spring Initializr生成项目骨架,执行以下命令:
curl https://start.spring.io/starter.zip -o border-archive.zip -d type=maven-project -d language=java -d bootVersion=3.1.5 -d baseDir=border-archive-backend -d groupId=com.border -d artifactId=archive -d name=BorderArchive -d description='Border Inspection Archive Management' -d packageName=com.border.archive -d packaging=jar -d javaVersion=17 -d dependencies=web,data-jpa,security,validation,postgresql,redis
解压后进入项目目录,修改application.yml核心配置:
``` server: port: 8080 servlet: context-path: /api spring: datasource: url: jdbc:postgresql://localhost:5432/border_archive username: postgres password: YourStrongPassword123! driver-class-name: org.postgresql.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true dialect: org.hibernate.dialect.PostgreSQLDialect redis: host: localhost port: 6379 password: RedisPass123 database: 0 timeout: 5000ms ```创建PostgreSQL数据库并建立核心表结构:
sudo -u postgres psql -c "CREATE DATABASE border_archive ENCODING 'UTF8' LC_COLLATE 'zh_CN.UTF-8' LC_CTYPE 'zh_CN.UTF-8';"
执行SQL创建用户表:
``` CREATE TABLE sys_user ( id BIGSERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, real_name VARCHAR(100) NOT NULL, password_hash VARCHAR(255) NOT NULL, department VARCHAR(100), role VARCHAR(50) NOT NULL CHECK (role IN ('ADMIN', 'OFFICER', 'AUDITOR')), status VARCHAR(20) DEFAULT 'ACTIVE', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_login TIMESTAMP ); CREATE TABLE archive_record ( id VARCHAR(36) PRIMARY KEY DEFAULT gen_random_uuid(), passport_number VARCHAR(20) NOT NULL, full_name VARCHAR(200) NOT NULL, nationality VARCHAR(100), border_point VARCHAR(100) NOT NULL, direction VARCHAR(10) CHECK (direction IN ('ENTRY', 'EXIT')), officer_id BIGINT REFERENCES sys_user(id), check_time TIMESTAMP NOT NULL, check_result VARCHAR(50) NOT NULL, remarks TEXT, attachments JSONB, security_level VARCHAR(20) DEFAULT 'NORMAL', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_passport (passport_number), INDEX idx_check_time (check_time), INDEX idx_officer (officer_id) ); ```敏感数据必须加密存储,创建AES加密工具类:
``` @Component public class DataEncryptor { private static final String ALGORITHM = "AES/GCM/NoPadding"; private static final int TAG_LENGTH_BIT = 128; private final SecretKey secretKey; private final byte[] iv; public DataEncryptor(@Value("${app.encryption.key}") String base64Key) { byte[] keyBytes = Base64.getDecoder().decode(base64Key); secretKey = new SecretKeySpec(keyBytes, "AES"); iv = new byte[12]; new SecureRandom().nextBytes(iv); } public String encrypt(String plainText) throws Exception { Cipher cipher = Cipher.getInstance(ALGORITHM); GCMParameterSpec parameterSpec = new GCMParameterSpec(TAG_LENGTH_BIT, iv); cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec); byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(cipherText); } public String decrypt(String cipherText) throws Exception { Cipher cipher = Cipher.getInstance(ALGORITHM); GCMParameterSpec parameterSpec = new GCMParameterSpec(TAG_LENGTH_BIT, iv); cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec); byte[] plainText = cipher.doFinal(Base64.getDecoder().decode(cipherText)); return new String(plainText, StandardCharsets.UTF_8); } } ```在application.yml中添加加密密钥配置:

app.encryption.key: "MWV2M3J4NXo2OHk5MDEyMzQ1Njc4OTA="
创建ArchiveController处理档案录入:
``` @RestController @RequestMapping("/api/archives") @Validated public class ArchiveController { @Autowired private ArchiveService archiveService; @PostMapping @PreAuthorize("hasRole('OFFICER') or hasRole('ADMIN')") public ResponseEntityArchiveService核心业务逻辑:
``` @Service @Transactional public class ArchiveService { @Autowired private ArchiveRepository archiveRepository; @Autowired private DataEncryptor dataEncryptor; @Autowired private AuditLogService auditLogService; public ArchiveRecord createRecord(ArchiveCreateRequest request, String officerId) { // 验证数据完整性 validateRequest(request); // 创建档案记录 ArchiveRecord record = new ArchiveRecord(); record.setId(UUID.randomUUID().toString()); record.setPassportNumber(dataEncryptor.encrypt(request.getPassportNumber())); record.setFullName(request.getFullName()); record.setNationality(request.getNationality()); record.setBorderPoint(request.getBorderPoint()); record.setDirection(request.getDirection()); record.setOfficerId(Long.parseLong(officerId)); record.setCheckTime(LocalDateTime.now()); record.setCheckResult(request.getCheckResult()); record.setRemarks(request.getRemarks()); record.setSecurityLevel(calculateSecurityLevel(request)); // 处理附件 if (request.getAttachments() != null) { record.setAttachments(processAttachments(request.getAttachments())); } // 保存记录 ArchiveRecord savedRecord = archiveRepository.save(record); // 记录审计日志 auditLogService.logOperation( officerId, "CREATE_ARCHIVE", savedRecord.getId(), "创建边防检查档案" ); return savedRecord; } private void validateRequest(ArchiveCreateRequest request) { if (request.getPassportNumber() == null || request.getPassportNumber().trim().isEmpty()) { throw new ValidationException("护照号码不能为空"); } if (request.getFullName() == null || request.getFullName().trim().isEmpty()) { throw new ValidationException("姓名不能为空"); } if (request.getBorderPoint() == null || request.getBorderPoint().trim().isEmpty()) { throw new ValidationException("检查站点不能为空"); } } } ```创建SecurityConfig配置类:
``` @Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/").permitAll() .requestMatchers("/api/archives/").authenticated() .anyRequest().authenticated() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } ```实现JWT认证:
``` public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtTokenProvider tokenProvider; @Autowired private UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = getTokenFromRequest(request); if (token != null && tokenProvider.validateToken(token)) { String username = tokenProvider.getUsernameFromToken(token); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource() .buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } private String getTokenFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } } ```创建ArchiveForm.vue组件:
``` ```创建archive.js API模块:
``` import axios from 'axios' const apiClient = axios.create({ baseURL: 'http://localhost:8080/api', timeout: 30000, headers: { 'Content-Type': 'application/json' } }) // 请求拦截器添加Token apiClient.interceptors.request.use( config => { const token = localStorage.getItem('access_token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, error => { return Promise.reject(error) } ) export default { createArchive(archiveData) { return apiClient.post('/archives', archiveData) }, getArchive(id, decrypt = false) { return apiClient.get(`/archives/${id}`, { params: { decrypt } }) }, searchArchives(params) { return apiClient.get('/archives/search', { params }) } } ```创建Dockerfile:
``` FROM openjdk:17-jdk-slim WORKDIR /app COPY target/border-archive-0.0.1-SNAPSHOT.jar app.jar RUN apt-get update && apt-get install -y tzdata && \ ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ dpkg-reconfigure -f noninteractive tzdata EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"] ```创建docker-compose.yml:
``` version: '3.8' services: postgres: image: postgres:14-alpine environment: POSTGRES_DB: border_archive POSTGRES_USER: postgres POSTGRES_PASSWORD: YourStrongPassword123! volumes: - postgres_data:/var/lib/postgresql/data ports: - "5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine command: redis-server --requirepass RedisPass123 ports: - "6379:6379" volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 backend: build: ./border-archive-backend depends_on: postgres: condition: service_healthy redis: condition: service_healthy environment: SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/border_archive SPRING_DATASOURCE_USERNAME: postgres SPRING_DATASOURCE_PASSWORD: YourStrongPassword123! SPRING_REDIS_HOST: redis SPRING_REDIS_PASSWORD: RedisPass123 ports: - "8080:8080" volumes: postgres_data: redis_data: ```创建nginx.conf:
``` upstream backend_servers { server backend:8080; } server { listen 80; server_name archive.border.gov; 前端静态文件 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } API代理 location /api { proxy_pass http://backend_servers; proxy_set_header Host $host; proxy_set_header X-Real