微爱帮监狱寄信寄信信件草稿箱技术设计文档

二、核心数据结构

复制代码
-- 草稿主表
CREATE TABLE letter_drafts (
    draft_id CHAR(32) PRIMARY KEY COMMENT '草稿ID',
    user_id CHAR(24) NOT NULL COMMENT '用户ID',
    session_id CHAR(32) NOT NULL COMMENT '编辑会话ID',
    
    -- 加密内容
    content_encrypted LONGTEXT NOT NULL COMMENT '加密内容',
    content_hash CHAR(64) NOT NULL COMMENT '内容哈希',
    
    -- 元数据
    title_encrypted VARCHAR(512) COMMENT '加密标题',
    recipient_info JSON COMMENT '收件人信息',
    attachment_ids JSON COMMENT '附件ID列表',
    
    -- 版本控制
    version INT DEFAULT 1 COMMENT '版本号',
    parent_version INT COMMENT '父版本号',
    is_autosave BOOLEAN DEFAULT FALSE COMMENT '是否自动保存',
    
    -- 状态
    status ENUM('editing', 'paused', 'archived', 'deleted') DEFAULT 'editing',
    last_activity_at TIMESTAMP(3) COMMENT '最后活动时间',
    
    -- 编辑信息
    cursor_position JSON COMMENT '光标位置',
    selection_range JSON COMMENT '选中范围',
    device_info JSON COMMENT '设备信息',
    
    -- 时间戳
    created_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
    updated_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
    expires_at TIMESTAMP(3) COMMENT '过期时间',
    
    -- 索引
    INDEX idx_user_activity (user_id, last_activity_at DESC),
    INDEX idx_session (session_id),
    INDEX idx_expires (expires_at),
    INDEX idx_autosave (user_id, is_autosave, updated_at),
    
    -- 约束
    CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
) ENGINE=InnoDB 
ROW_FORMAT=COMPRESSED 
KEY_BLOCK_SIZE=8
ENCRYPTION='Y'
COMMENT='信件草稿加密存储表';

-- 草稿版本历史
CREATE TABLE draft_versions (
    version_id CHAR(32) PRIMARY KEY,
    draft_id CHAR(32) NOT NULL,
    version INT NOT NULL,
    
    -- 增量存储
    delta_content LONGTEXT COMMENT '增量内容',
    full_content_hash CHAR(64) COMMENT '完整内容哈希',
    operation_type ENUM('insert', 'delete', 'format', 'paste') COMMENT '操作类型',
    
    -- 操作上下文
    operation_context JSON COMMENT '操作上下文',
    device_id CHAR(24) COMMENT '设备ID',
    ip_address VARCHAR(45) COMMENT '操作IP',
    
    created_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
    
    INDEX idx_draft_version (draft_id, version DESC),
    INDEX idx_created (created_at),
    
    CONSTRAINT fk_draft_version FOREIGN KEY (draft_id) 
        REFERENCES letter_drafts(draft_id) ON DELETE CASCADE
) ENGINE=InnoDB
COMMENT='草稿版本历史';

-- 跨设备同步状态
CREATE TABLE draft_sync_status (
    draft_id CHAR(32) NOT NULL,
    device_id CHAR(24) NOT NULL,
    user_id CHAR(24) NOT NULL,
    
    -- 同步状态
    last_synced_version INT DEFAULT 0,
    last_synced_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
    pending_changes INT DEFAULT 0,
    sync_conflict BOOLEAN DEFAULT FALSE,
    
    -- 设备信息
    device_name VARCHAR(128),
    device_type ENUM('web', 'ios', 'android', 'desktop'),
    user_agent TEXT,
    
    -- 连接状态
    is_online BOOLEAN DEFAULT FALSE,
    last_seen_at TIMESTAMP(3),
    connection_id VARCHAR(64),
    
    PRIMARY KEY (draft_id, device_id),
    INDEX idx_user_devices (user_id, device_type),
    INDEX idx_online_status (is_online, last_seen_at),
    
    CONSTRAINT fk_sync_draft FOREIGN KEY (draft_id) 
        REFERENCES letter_drafts(draft_id) ON DELETE CASCADE
) COMMENT='草稿跨设备同步状态';

三、自动保存引擎

复制代码
class DraftAutosaveEngine {
    constructor() {
        this.autosaveQueue = new PriorityQueue()
        this.debounceTimers = new Map()
        this.throttleConfig = {
            minInterval: 2000,    // 最短保存间隔
            maxInterval: 30000,   // 最长保存间隔
            idleTimeout: 5000     // 空闲保存延迟
        }
    }
    
    async handleContentChange(draftId, changes) {
        // 1. 去抖动处理
        this.debounceSave(draftId, changes)
        
        // 2. 增量存储
        const delta = this.calculateDelta(changes)
        await this.storeDelta(draftId, delta)
        
        // 3. 触发自动保存
        this.queueAutosave(draftId, {
            priority: 'high',
            changes: delta
        })
    }
    
    async queueAutosave(draftId, options) {
        // 智能调度:基于编辑频率调整保存策略
        const editFrequency = await this.calculateEditFrequency(draftId)
        
        const saveStrategy = {
            high_frequency: { interval: 2000, deltaOnly: true },
            medium_frequency: { interval: 5000, deltaOnly: false },
            low_frequency: { interval: 15000, deltaOnly: false },
            idle: { interval: 30000, deltaOnly: false }
        }
        
        const strategy = this.selectStrategy(editFrequency)
        this.autosaveQueue.enqueue({
            draftId,
            timestamp: Date.now(),
            priority: options.priority,
            strategy
        })
    }
    
    async executeAutosave() {
        while (!this.autosaveQueue.isEmpty()) {
            const task = this.autosaveQueue.dequeue()
            
            // 冲突检测
            const conflict = await this.detectConflict(task.draftId)
            if (conflict) {
                await this.resolveConflict(task.draftId, conflict)
            }
            
            // 执行保存
            await this.saveDraft(task.draftId, {
                deltaOnly: task.strategy.deltaOnly,
                compress: true
            })
            
            // 更新同步状态
            await this.updateSyncStatus(task.draftId)
        }
    }
    
    calculateEditFrequency(draftId) {
        // 基于近期编辑行为计算频率
        const recentEdits = this.getRecentEdits(draftId, '5m')
        const frequency = recentEdits.length / (5 * 60)  // 每分钟编辑次数
        
        if (frequency > 1) return 'high_frequency'
        if (frequency > 0.2) return 'medium_frequency'
        if (frequency > 0) return 'low_frequency'
        return 'idle'
    }
}

四、实时同步协议

复制代码
class DraftSyncProtocol {
    constructor() {
        this.wsConnections = new Map()
        this.otEngine = new OperationalTransformation()
        this.conflictResolver = new ConflictResolver()
    }
    
    async handleSyncMessage(connectionId, message) {
        const { type, payload } = message
        
        switch (type) {
            case 'SYNC_INIT':
                return this.handleSyncInit(connectionId, payload)
                
            case 'DELTA_UPDATE':
                return this.handleDeltaUpdate(connectionId, payload)
                
            case 'SYNC_REQUEST':
                return this.handleSyncRequest(connectionId, payload)
                
            case 'CONFLICT_RESOLUTION':
                return this.handleConflictResolution(connectionId, payload)
                
            case 'PRESENCE_UPDATE':
                return this.handlePresenceUpdate(connectionId, payload)
        }
    }
    
    async handleDeltaUpdate(connectionId, payload) {
        const { draftId, delta, version, clientId } = payload
        
        // 1. 验证版本连续性
        const isValid = await this.validateVersion(draftId, version)
        if (!isValid) {
            return this.requestResync(connectionId, draftId)
        }
        
        // 2. 转换操作
        const transformedDelta = await this.otEngine.transform(
            delta,
            draftId,
            version
        )
        
        // 3. 广播到其他客户端
        await this.broadcastDelta(draftId, {
            delta: transformedDelta,
            fromClientId: clientId,
            version: version + 1
        })
        
        // 4. 持久化
        await this.storeDelta(draftId, transformedDelta, version + 1)
        
        return { success: true, version: version + 1 }
    }
    
    broadcastDelta(draftId, deltaUpdate) {
        const connections = this.getDraftConnections(draftId)
        
        connections.forEach(connection => {
            if (connection.readyState === WebSocket.OPEN) {
                connection.send(JSON.stringify({
                    type: 'DELTA_BROADCAST',
                    payload: deltaUpdate
                }))
            }
        })
    }
    
    async handlePresenceUpdate(connectionId, payload) {
        const { draftId, userId, cursor, selection, isTyping } = payload
        
        // 更新用户在线状态
        await this.updatePresence(draftId, userId, {
            cursor,
            selection,
            isTyping,
            lastSeen: new Date().toISOString()
        })
        
        // 广播给其他协作者
        const presenceUpdate = {
            type: 'PRESENCE_BROADCAST',
            payload: {
                userId,
                cursor,
                selection,
                isTyping
            }
        }
        
        await this.broadcastToOthers(draftId, userId, presenceUpdate)
    }
}

五、离线存储方案

复制代码
class DraftOfflineManager {
    constructor() {
        this.db = new Dexie('WeiaiDraftsDB')
        this.setupDatabase()
        this.syncManager = new BackgroundSyncManager()
    }
    
    setupDatabase() {
        this.db.version(1).stores({
            drafts: '&draftId, userId, updatedAt, status',
            pendingChanges: '++id, draftId, createdAt',
            syncQueue: '++id, draftId, operation, createdAt',
            attachments: '&attachmentId, draftId, size, uploaded'
        })
    }
    
    async saveDraftOffline(draftData) {
        // 1. 保存到IndexedDB
        await this.db.drafts.put({
            draftId: draftData.draftId,
            userId: draftData.userId,
            content: draftData.content,
            encryptedContent: draftData.encryptedContent,
            metadata: draftData.metadata,
            updatedAt: new Date().getTime(),
            status: 'offline',
            version: draftData.version || 1,
            isSynced: false
        })
        
        // 2. 记录待同步变更
        await this.db.pendingChanges.add({
            draftId: draftData.draftId,
            operation: 'save',
            changes: draftData.changes,
            createdAt: new Date().getTime()
        })
        
        // 3. 注册后台同步
        if ('serviceWorker' in navigator && 'SyncManager' in window) {
            await navigator.serviceWorker.ready
                .then(registration => registration.sync.register('sync-drafts'))
        }
        
        // 4. 通知用户
        this.showOfflineNotification()
    }
    
    async syncWhenOnline() {
        const pendingChanges = await this.db.pendingChanges.toArray()
        
        for (const change of pendingChanges) {
            try {
                // 尝试同步到服务器
                await this.syncChange(change)
                
                // 标记为已同步
                await this.db.pendingChanges.delete(change.id)
                
                // 更新草稿状态
                await this.db.drafts.update(change.draftId, {
                    isSynced: true,
                    status: 'synced'
                })
            } catch (error) {
                console.error('Sync failed:', error)
                await this.retryLater(change)
            }
        }
    }
    
    async resolveOfflineConflict(localDraft, serverDraft) {
        // 智能冲突解决策略
        const strategies = {
            TIME_BASED: () => this.resolveByTimestamp(localDraft, serverDraft),
            SIZE_BASED: () => this.resolveBySize(localDraft, serverDraft),
            MERGE: () => this.mergeChanges(localDraft, serverDraft),
            USER_CHOICE: () => this.promptUserChoice(localDraft, serverDraft)
        }
        
        // 选择解决策略
        const strategy = this.selectConflictStrategy(localDraft, serverDraft)
        return strategies[strategy]()
    }
}

六、加密存储实现

复制代码
class DraftEncryptionService:
    def __init__(self):
        self.key_derivation = ScryptKeyDerivation()
        self.encryption = AESCipher()
        self.key_storage = SecureKeyStorage()
    
    async def encrypt_draft_content(self, draft_data, user_key):
        """加密草稿内容"""
        
        # 1. 生成内容加密密钥
        content_key = self.generate_content_key(draft_data['draftId'])
        
        # 2. 加密内容
        encrypted_content = await self.encryption.encrypt(
            plaintext=draft_data['content'],
            key=content_key,
            additional_data={
                'draft_id': draft_data['draftId'],
                'user_id': draft_data['userId'],
                'version': draft_data.get('version', 1)
            }
        )
        
        # 3. 加密内容密钥(使用用户密钥)
        encrypted_content_key = await self.encryption.encrypt_key(
            key=content_key,
            user_key=user_key
        )
        
        # 4. 生成完整性验证码
        integrity_hash = self.calculate_integrity_hash(
            encrypted_content,
            draft_data['metadata']
        )
        
        return {
            'encrypted_content': encrypted_content,
            'encrypted_content_key': encrypted_content_key,
            'integrity_hash': integrity_hash,
            'encryption_schema': 'AES-256-GCM',
            'key_version': 'v2'
        }
    
    async def save_encrypted_draft(self, draft_id, encrypted_data):
        """保存加密草稿"""
        
        # 分片存储大草稿
        if len(encrypted_data['encrypted_content']) > 1024 * 1024:  # 1MB
            chunks = self.split_into_chunks(
                encrypted_data['encrypted_content'],
                chunk_size=256 * 1024  # 256KB
            )
            
            # 存储分片
            chunk_refs = []
            for i, chunk in enumerate(chunks):
                chunk_id = f"{draft_id}_chunk_{i}"
                chunk_key = self.derive_chunk_key(draft_id, i)
                
                encrypted_chunk = await self.encryption.encrypt(
                    plaintext=chunk,
                    key=chunk_key
                )
                
                # 存储到对象存储
                chunk_url = await self.object_storage.store(
                    key=chunk_id,
                    data=encrypted_chunk,
                    metadata={
                        'draft_id': draft_id,
                        'chunk_index': i,
                        'total_chunks': len(chunks)
                    }
                )
                
                chunk_refs.append({
                    'chunk_id': chunk_id,
                    'chunk_url': chunk_url,
                    'index': i,
                    'size': len(chunk)
                })
            
            encrypted_data['content_chunks'] = chunk_refs
            encrypted_data['encrypted_content'] = None  # 清理原始内容
        
        # 存储元数据到数据库
        await self.db.drafts.insert_one({
            'draft_id': draft_id,
            'encrypted_data': encrypted_data,
            'storage_type': 'chunked' if 'content_chunks' in encrypted_data else 'inline',
            'created_at': datetime.utcnow(),
            'updated_at': datetime.utcnow()
        })

七、冲突解决算法

复制代码
class DraftConflictResolver {
    constructor() {
        this.strategies = {
            'AUTO_MERGE': this.autoMergeStrategy,
            'LAST_WRITE_WINS': this.lastWriteWinsStrategy,
            'FIRST_WRITE_WINS': this.firstWriteWinsStrategy,
            'MANUAL_MERGE': this.manualMergeStrategy
        }
    }
    
    async resolveConflicts(draftId, conflictingVersions) {
        const conflictType = this.detectConflictType(conflictingVersions)
        
        switch (conflictType) {
            case 'CONTENT_CONFLICT':
                return await this.resolveContentConflict(draftId, conflictingVersions)
                
            case 'METADATA_CONFLICT':
                return await this.resolveMetadataConflict(draftId, conflictingVersions)
                
            case 'ATTACHMENT_CONFLICT':
                return await this.resolveAttachmentConflict(draftId, conflictingVersions)
                
            case 'VERSION_CONFLICT':
                return await this.resolveVersionConflict(draftId, conflictingVersions)
                
            default:
                throw new Error(`Unknown conflict type: ${conflictType}`)
        }
    }
    
    async resolveContentConflict(draftId, versions) {
        // 获取冲突解决策略
        const strategy = await this.getUserStrategy(draftId) || 'AUTO_MERGE'
        
        // 应用解决策略
        const resolvedContent = await this.strategies[strategy](versions)
        
        // 生成合并版本
        const mergedVersion = {
            content: resolvedContent,
            version: Math.max(...versions.map(v => v.version)) + 1,
            parents: versions.map(v => v.version),
            resolvedAt: new Date().toISOString(),
            resolutionStrategy: strategy
        }
        
        // 保存冲突解决记录
        await this.saveConflictResolution(draftId, versions, mergedVersion)
        
        return mergedVersion
    }
    
    autoMergeStrategy(versions) {
        // 基于操作变换的自动合并
        const otEngine = new OperationalTransformation()
        
        // 将版本转换为操作序列
        const operations = versions.map(v => this.extractOperations(v))
        
        // 合并操作
        const mergedOperations = otEngine.merge(operations)
        
        // 应用合并后的操作
        return this.applyOperations(mergedOperations)
    }
    
    async manualMergeStrategy(versions) {
        // 生成可读的差异对比
        const diffs = versions.map((v, i) => ({
            version: v.version,
            author: v.author,
            timestamp: v.timestamp,
            diff: this.generateDiff(versions[0].content, v.content)
        }))
        
        // 发送给用户进行手动合并
        const mergeResult = await this.presentMergeInterface(diffs)
        
        return mergeResult.resolvedContent
    }
    
    async presentMergeInterface(diffs) {
        // 三窗格合并界面
        return new Promise((resolve) => {
            const mergeUI = new MergeUI({
                base: diffs[0],
                current: diffs[1],
                incoming: diffs[2],
                onResolve: (resolvedContent) => {
                    resolve({
                        resolvedContent,
                        resolutionType: 'manual'
                    })
                }
            })
            
            mergeUI.show()
        })
    }
}

八、性能优化策略

复制代码
# 草稿服务优化配置
http {
    # 压缩配置
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css application/json application/javascript;
    
    # 缓存配置
    proxy_cache_path /var/cache/nginx/drafts levels=1:2 keys_zone=draft_cache:10m inactive=24h max_size=1g;
    
    server {
        location ~ ^/api/drafts/([^/]+)/content$ {
            # 分块传输编码
            chunked_transfer_encoding on;
            
            # 缓存草稿内容
            proxy_cache draft_cache;
            proxy_cache_key "$scheme$request_method$host$request_uri";
            proxy_cache_valid 200 5m;  # 成功响应缓存5分钟
            proxy_cache_use_stale updating error timeout;
            
            # 限流
            limit_req zone=draft_zone burst=20 nodelay;
            
            # 后端超时设置
            proxy_connect_timeout 3s;
            proxy_read_timeout 30s;
            proxy_send_timeout 30s;
        }
        
        location /api/drafts/sync {
            # WebSocket连接优化
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            
            # 长连接配置
            proxy_read_timeout 86400s;  # 24小时
            proxy_send_timeout 86400s;
            
            # 连接数限制
            limit_conn sync_conn 100;
        }
    }
}

九、监控指标

复制代码
# Prometheus监控配置
metrics:
  draft_operations:
    - name: draft_save_duration_seconds
      help: Time spent saving drafts
      buckets: [0.1, 0.5, 1, 2, 5]
      
    - name: draft_sync_conflicts_total
      help: Total number of sync conflicts
      labels: [strategy]
      
    - name: draft_autosave_success_rate
      help: Success rate of autosave operations
      
    - name: draft_storage_size_bytes
      help: Size of draft storage
      labels: [storage_type]
      
    - name: draft_offline_operations_total
      help: Total offline draft operations
      labels: [operation_type]
      
    - name: draft_version_count
      help: Number of versions per draft
  
  alerts:
    - alert: HighDraftConflictRate
      expr: rate(draft_sync_conflicts_total[5m]) > 0.1
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "High draft conflict rate detected"
        
    - alert: DraftAutosaveFailure
      expr: draft_autosave_success_rate < 0.95
      for: 10m
      labels:
        severity: critical
      annotations:
        summary: "Draft autosave failure rate is high"
        
    - alert: DraftStorageFull
      expr: draft_storage_size_bytes / 1e9 > 50  # 50GB
      labels:
        severity: warning
      annotations:
        summary: "Draft storage is reaching capacity"

十、数据清理策略

复制代码
-- 草稿数据清理存储过程
DELIMITER //

CREATE PROCEDURE cleanup_expired_drafts()
BEGIN
    DECLARE done INT DEFAULT FALSE;
    DECLARE draft_id CHAR(32);
    DECLARE user_id CHAR(24);
    DECLARE cleanup_cursor CURSOR FOR 
        SELECT d.draft_id, d.user_id 
        FROM letter_drafts d
        WHERE d.expires_at < NOW()
           OR (d.status = 'deleted' AND d.updated_at < DATE_SUB(NOW(), INTERVAL 30 DAY))
           OR (d.status = 'archived' AND d.updated_at < DATE_SUB(NOW(), INTERVAL 90 DAY));
    
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
    
    OPEN cleanup_cursor;
    
    cleanup_loop: LOOP
        FETCH cleanup_cursor INTO draft_id, user_id;
        IF done THEN
            LEAVE cleanup_loop;
        END IF;
        
        -- 1. 移动到归档表
        INSERT INTO draft_archive
        SELECT * FROM letter_drafts WHERE draft_id = draft_id;
        
        -- 2. 清理版本历史
        DELETE FROM draft_versions WHERE draft_id = draft_id;
        
        -- 3. 清理同步状态
        DELETE FROM draft_sync_status WHERE draft_id = draft_id;
        
        -- 4. 清理加密内容(标记为已删除)
        UPDATE encrypted_draft_content 
        SET status = 'purged', purged_at = NOW()
        WHERE draft_id = draft_id;
        
        -- 5. 从主表删除
        DELETE FROM letter_drafts WHERE draft_id = draft_id;
        
        -- 记录清理日志
        INSERT INTO cleanup_log VALUES (
            UUID(),
            'draft_cleanup',
            draft_id,
            user_id,
            NOW(),
            JSON_OBJECT('reason', 'expired_or_deleted')
        );
    END LOOP;
    
    CLOSE cleanup_cursor;
END //

DELIMITER ;

-- 创建定时任务
CREATE EVENT IF NOT EXISTS draft_cleanup_event
ON SCHEDULE EVERY 1 DAY
STARTS '2025-01-01 02:00:00'
DO
    CALL cleanup_expired_drafts();

总结

微爱帮草稿箱采用分层架构设计,具备以下核心特性:

  1. 实时协作:支持多设备实时同步编辑

  2. 智能保存:基于编辑频率的自适应自动保存

  3. 离线优先:完整的离线编辑与同步能力

  4. 安全存储:端到端加密与完整性验证

  5. 冲突智能解决:多策略冲突检测与解决

  6. 性能优化:分块存储、缓存、压缩等优化

技术栈

  • 前端:Quill.js + Socket.io + IndexedDB

  • 后端:Node.js + WebSocket集群 + Redis

  • 存储:MySQL TDE + S3加密存储

  • 安全:AES-256-GCM + 双密钥体系

性能目标

  • 自动保存延迟:< 2秒

  • 实时同步延迟:< 200ms

  • 离线恢复时间:< 1秒

  • 草稿加载时间:< 500ms


微爱帮产品技术部
版本:v1.0
生效日期:2025年10月
密级:内部机密

相关推荐
茁壮成长的露露8 小时前
MongoDB备份恢复工具mongodump、mongorestore
数据库·mongodb
香气袭人知骤暖8 小时前
SQL慢查询常见优化步骤
android·数据库·sql
Star Learning Python8 小时前
MySQL日期时间的处理函数
数据库·sql
JosieBook8 小时前
【数据库】多模融合,智启新篇:金仓数据库重塑国产文档数据库范式
数据库
韩立学长9 小时前
基于Springboot流浪动物救助系统o8g44kwc(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
数据库·spring boot·后端
聆风吟º9 小时前
金仓数据库:以 “多模融合” 重塑国产文档数据库新标杆
数据库·重构·kingbasees
子沫20209 小时前
使用mybatis-plus、mybatis插入数据库时加密,查询数据库时解密,自定义TypeHandler 加解密使用
数据库·mybatis·mybatis-plus
清风拂山岗 明月照大江9 小时前
MySQL运维
运维·数据库·mysql
小伍_Five9 小时前
《NoSQL数据库技术与应用(黑马程序员)》课后习题答案完整版
数据库·nosql
oas110 小时前
山东大学软件学院2024-2025非关系型数据库期末考试(限选)
数据库·nosql