
二、核心数据结构
-- 草稿主表
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();
总结
微爱帮草稿箱采用分层架构设计,具备以下核心特性:
-
实时协作:支持多设备实时同步编辑
-
智能保存:基于编辑频率的自适应自动保存
-
离线优先:完整的离线编辑与同步能力
-
安全存储:端到端加密与完整性验证
-
冲突智能解决:多策略冲突检测与解决
-
性能优化:分块存储、缓存、压缩等优化
技术栈:
-
前端:Quill.js + Socket.io + IndexedDB
-
后端:Node.js + WebSocket集群 + Redis
-
存储:MySQL TDE + S3加密存储
-
安全:AES-256-GCM + 双密钥体系
性能目标:
-
自动保存延迟:< 2秒
-
实时同步延迟:< 200ms
-
离线恢复时间:< 1秒
-
草稿加载时间:< 500ms
微爱帮产品技术部
版本:v1.0
生效日期:2025年10月
密级:内部机密