23 KiB
23 KiB
员工端APP离线存储方案设计
版本: v1.0
创建日期: 2026-02-26
适用范围: ether-app-employee
一、现状分析
1.1 现有实现
当前代码 (src/utils/offline.ts) 已实现:
| 功能 | 实现状态 | 说明 |
|---|---|---|
| 基础缓存 | ✅ 已实现 | offlineStorage.set/get/remove/clear |
| 缓存过期 | ✅ 已实现 | 支持TTL过期机制 |
| 网络状态监听 | ✅ 已实现 | 响应式网络状态 |
| 待同步队列 | ✅ 已实现 | pendingActions 队列 |
| 位置追踪 | ✅ 已实现 | locationTracker |
1.2 存在问题
- 数据模型不完整: 缺少业务数据的离线模型定义
- 冲突解决缺失: 没有处理数据冲突的策略
- 存储容量管理: 没有容量限制和清理策略
- 数据加密: 敏感数据未加密存储
- 同步策略简单: 仅支持重试,缺少增量同步
二、行业最佳实践
2.1 离线优先架构 (Offline-First)
┌─────────────────────────────────────────────────────────────┐
│ 应用层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 工单模块 │ │ 巡检模块 │ │ 访客模块 │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 数据访问层 (Repository) │ │
│ │ - 统一数据访问接口 │ │
│ │ - 自动选择在线/离线数据源 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────┼──────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 在线API │ │ 同步引擎 │ │ 本地存储 │ │
│ │ (Remote) │◄──►│ (Sync) │◄──►│ (Local) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ 同步队列 │ │ │
│ │ │ (Queue) │ │ │
│ │ └─────────────┘ │ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 网络层 │ │
│ │ - 网络状态检测 │ │
│ │ - 请求重试 │ │
│ │ - 请求缓存 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
2.2 数据同步策略
策略1: 最后写入胜出 (LWW - Last Write Wins)
- 适用场景: 简单数据、无冲突风险
- 实现方式: 使用时间戳比较
策略2: 版本向量 (Vector Clock)
- 适用场景: 需要精确冲突检测
- 实现方式: 每个节点维护版本号
策略3: 业务合并 (Application-Level Merge)
- 适用场景: 复杂业务数据
- 实现方式: 业务逻辑决定合并策略
2.3 存储方案对比
| 方案 | 容量 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| uni.setStorage | 10MB | 高 | 低 | 简单数据 |
| IndexedDB | 50MB+ | 中 | 中 | 结构化数据 |
| SQLite | 无限制 | 高 | 高 | 复杂查询 |
| 本地文件 | 无限制 | 低 | 高 | 大文件存储 |
三、推荐方案
3.1 整体架构
采用 分层存储 + 增量同步 + 冲突检测 的方案:
┌─────────────────────────────────────────────────────────────┐
│ 离线存储架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 数据访问层 │ │
│ │ OfflineRepository<T> │ │
│ │ - get(id): Promise<T> │ │
│ │ - getAll(): Promise<T[]> │ │
│ │ - save(item: T): Promise<void> │ │
│ │ - delete(id: string): Promise<void> │ │
│ │ - sync(): Promise<SyncResult> │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────┼──────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 缓存层 │ │ 持久层 │ │ 同步层 │ │
│ │ (Memory) │ │ (Storage) │ │ (Sync) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
├─────────────────────────────────────────────────────────────┤
│ 存储分区 │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 系统数据区 (System Zone) │ │
│ │ - 用户信息、Token、配置 │ │
│ │ - 容量: 1MB │ │
│ │ - 策略: 永久存储 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 业务数据区 (Business Zone) │ │
│ │ - 工单、巡检任务、访客记录 │ │
│ │ - 容量: 5MB │ │
│ │ - 策略: LRU淘汰 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 临时数据区 (Temp Zone) │ │
│ │ - 表单草稿、临时照片 │ │
│ │ - 容量: 2MB │ │
│ │ - 策略: 定时清理 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 同步队列区 (Queue Zone) │ │
│ │ - 待同步操作 │ │
│ │ - 容量: 1MB │ │
│ │ - 策略: 先进先出 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
3.2 数据模型设计
// 离线数据基础模型
interface OfflineEntity {
id: string
_offline?: {
version: number // 本地版本号
serverVersion: number // 服务器版本号
lastModified: number // 最后修改时间
syncStatus: SyncStatus // 同步状态
localChanges: boolean // 是否有本地修改
}
}
// 同步状态枚举
enum SyncStatus {
SYNCED = 'SYNCED', // 已同步
PENDING = 'PENDING', // 待同步
CONFLICT = 'CONFLICT', // 冲突
DELETED = 'DELETED' // 已删除
}
// 同步操作类型
enum SyncOperation {
CREATE = 'CREATE',
UPDATE = 'UPDATE',
DELETE = 'DELETE'
}
// 同步队列项
interface SyncQueueItem {
id: string
entityType: string // 实体类型: WorkOrder, Inspection, Visitor
entityId: string // 实体ID
operation: SyncOperation
payload: any // 操作数据
timestamp: number
retryCount: number
lastError?: string
}
3.3 核心接口设计
// 离线存储接口
interface OfflineStorage<T extends OfflineEntity> {
// 数据操作
get(id: string): Promise<T | null>
getAll(): Promise<T[]>
save(entity: T): Promise<void>
delete(id: string): Promise<void>
// 查询
query(filter: QueryFilter): Promise<T[]>
// 同步
sync(): Promise<SyncResult>
getPendingChanges(): Promise<SyncQueueItem[]>
// 状态
isOffline(): boolean
getLastSyncTime(): number
}
// 同步引擎接口
interface SyncEngine {
// 同步操作
push(): Promise<SyncResult> // 推送本地变更
pull(): Promise<SyncResult> // 拉取远程变更
fullSync(): Promise<SyncResult> // 完整同步
// 冲突处理
resolveConflict(item: SyncQueueItem, resolution: ConflictResolution): Promise<void>
// 事件
onSyncStart: Event<void>
onSyncComplete: Event<SyncResult>
onConflict: Event<ConflictInfo>
}
// 冲突解决策略
enum ConflictResolution {
LOCAL_WINS = 'LOCAL_WINS', // 本地优先
SERVER_WINS = 'SERVER_WINS', // 服务器优先
MERGE = 'MERGE', // 合并
MANUAL = 'MANUAL' // 手动解决
}
3.4 业务数据离线策略
3.4.1 工单模块
| 数据 | 离线读取 | 离线写入 | 同步策略 |
|---|---|---|---|
| 工单列表 | ✅ 缓存 | ❌ | 下拉刷新时同步 |
| 工单详情 | ✅ 缓存 | ❌ | 查看时同步 |
| 接单操作 | ❌ | ✅ 队列 | 网络恢复后同步 |
| 开始处理 | ❌ | ✅ 队列 | 网络恢复后同步 |
| 完成工单 | ❌ | ✅ 队列 | 网络恢复后同步 |
| 上传照片 | ❌ | ✅ 队列 | 网络恢复后同步 |
3.4.2 巡检模块
| 数据 | 离线读取 | 离线写入 | 同步策略 |
|---|---|---|---|
| 巡检任务列表 | ✅ 缓存 | ❌ | 每日首次打开同步 |
| 巡检任务详情 | ✅ 缓存 | ❌ | 查看时同步 |
| 签到记录 | ❌ | ✅ 队列 | 网络恢复后同步 |
| 巡检结果 | ❌ | ✅ 队列 | 网络恢复后同步 |
| 异常上报 | ❌ | ✅ 队列 | 网络恢复后同步 |
| 巡检照片 | ❌ | ✅ 队列 | 网络恢复后同步 |
3.4.3 访客模块
| 数据 | 离线读取 | 离线写入 | 同步策略 |
|---|---|---|---|
| 访客记录 | ✅ 缓存 | ❌ | 每次进入页面同步 |
| 访客登记 | ❌ | ✅ 队列 | 网络恢复后同步 |
| 凭证验证 | ❌ | ❌ | 必须在线 |
| 通行确认 | ❌ | ✅ 队列 | 网络恢复后同步 |
四、实现计划
4.1 阶段一:基础增强 (1-2天)
-
完善数据模型
- 定义
OfflineEntity基类 - 为业务实体添加离线元数据
- 定义
-
增强存储层
- 实现存储分区管理
- 添加容量限制和LRU淘汰
- 实现数据加密
-
优化同步队列
- 支持操作优先级
- 添加依赖关系处理
- 实现批量同步
4.2 阶段二:同步引擎 (2-3天)
-
增量同步
- 实现版本号比较
- 只同步变更数据
-
冲突检测
- 检测版本冲突
- 记录冲突信息
-
冲突解决
- 实现自动解决策略
- 提供手动解决接口
4.3 阶段三:业务集成 (2-3天)
-
工单模块集成
- 实现
WorkOrderRepository - 集成离线操作
- 实现
-
巡检模块集成
- 实现
InspectionRepository - 集成离线操作
- 实现
-
访客模块集成
- 实现
VisitorRepository - 集成离线操作
- 实现
4.4 阶段四:测试优化 (1-2天)
-
单元测试
- 存储层测试
- 同步引擎测试
-
集成测试
- 离线场景测试
- 网络恢复测试
-
性能优化
- 存储性能优化
- 同步性能优化
五、关键代码示例
5.1 离线存储基类
// src/stores/offline/base-repository.ts
export abstract class BaseRepository<T extends OfflineEntity> {
protected cacheKey: string
protected syncEndpoint: string
constructor(cacheKey: string, syncEndpoint: string) {
this.cacheKey = cacheKey
this.syncEndpoint = syncEndpoint
}
async get(id: string): Promise<T | null> {
// 先从内存缓存读取
const cached = memoryCache.get(`${this.cacheKey}:${id}`)
if (cached) return cached
// 再从本地存储读取
const stored = offlineStorage.get<T>(`${this.cacheKey}:${id}`)
if (stored) {
memoryCache.set(`${this.cacheKey}:${id}`, stored)
return stored
}
// 最后从服务器获取
if (networkStatus.value.isConnected) {
const remote = await this.fetchFromServer(id)
if (remote) {
await this.saveToCache(remote)
return remote
}
}
return null
}
async save(entity: T): Promise<void> {
// 更新离线元数据
entity._offline = {
version: (entity._offline?.version || 0) + 1,
serverVersion: entity._offline?.serverVersion || 0,
lastModified: Date.now(),
syncStatus: SyncStatus.PENDING,
localChanges: true
}
// 保存到本地
await this.saveToCache(entity)
// 添加到同步队列
await syncQueue.add({
entityType: this.cacheKey,
entityId: entity.id,
operation: entity._offline.serverVersion === 0 ? SyncOperation.CREATE : SyncOperation.UPDATE,
payload: entity,
timestamp: Date.now(),
retryCount: 0
})
}
async sync(): Promise<SyncResult> {
if (!networkStatus.value.isConnected) {
return { success: false, reason: 'offline' }
}
// 推送本地变更
const pushResult = await this.pushChanges()
// 拉取远程变更
const pullResult = await this.pullChanges()
return {
success: pushResult.success && pullResult.success,
pushed: pushResult.count,
pulled: pullResult.count,
conflicts: [...pushResult.conflicts, ...pullResult.conflicts]
}
}
}
5.2 同步队列增强
// src/stores/offline/sync-queue.ts
export class SyncQueue {
private queue: SyncQueueItem[] = []
private processing = false
// 添加操作到队列
async add(item: Omit<SyncQueueItem, 'id'>): Promise<void> {
const queueItem: SyncQueueItem = {
...item,
id: `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
this.queue.push(queueItem)
await this.persist()
// 如果在线,立即尝试同步
if (networkStatus.value.isConnected && !this.processing) {
this.process()
}
}
// 处理队列
async process(): Promise<void> {
if (this.processing || !networkStatus.value.isConnected) return
this.processing = true
try {
// 按优先级排序
this.sortByPriority()
// 批量处理
const batch = this.queue.slice(0, 10)
for (const item of batch) {
try {
const success = await this.processItem(item)
if (success) {
this.queue = this.queue.filter(q => q.id !== item.id)
} else {
item.retryCount++
if (item.retryCount >= 3) {
// 超过重试次数,标记为失败
item.lastError = 'Max retry exceeded'
}
}
} catch (error) {
item.lastError = String(error)
item.retryCount++
}
}
await this.persist()
} finally {
this.processing = false
}
}
// 处理单个操作
private async processItem(item: SyncQueueItem): Promise<boolean> {
const handler = this.handlers.get(item.entityType)
if (!handler) return false
return await handler(item)
}
}
5.3 冲突检测与解决
// src/stores/offline/conflict-resolver.ts
export class ConflictResolver {
// 检测冲突
detectConflict(local: OfflineEntity, remote: OfflineEntity): boolean {
return local._offline.serverVersion !== remote._offline.version
}
// 自动解决冲突
async resolve(local: T, remote: T, strategy: ConflictResolution): Promise<T> {
switch (strategy) {
case ConflictResolution.LOCAL_WINS:
return this.mergeWithLocalWins(local, remote)
case ConflictResolution.SERVER_WINS:
return this.mergeWithServerWins(local, remote)
case ConflictResolution.MERGE:
return this.merge(local, remote)
case ConflictResolution.MANUAL:
throw new ConflictError('Manual resolution required', local, remote)
}
}
// 合并策略:本地优先
private mergeWithLocalWins(local: T, remote: T): T {
return {
...remote,
...local,
_offline: {
...local._offline,
serverVersion: remote._offline.version,
syncStatus: SyncStatus.PENDING
}
}
}
// 合并策略:服务器优先
private mergeWithServerWins(local: T, remote: T): T {
return {
...local,
...remote,
_offline: {
version: remote._offline.version,
serverVersion: remote._offline.version,
lastModified: Date.now(),
syncStatus: SyncStatus.SYNCED,
localChanges: false
}
}
}
// 合并策略:字段级合并
private merge(local: T, remote: T): T {
const merged = { ...remote }
for (const key of Object.keys(local)) {
if (key === '_offline') continue
const localValue = local[key]
const remoteValue = remote[key]
// 如果本地修改了该字段,使用本地值
if (localValue !== remoteValue && local._offline.localChanges) {
merged[key] = localValue
}
}
merged._offline = {
version: Math.max(local._offline.version, remote._offline.version),
serverVersion: remote._offline.version,
lastModified: Date.now(),
syncStatus: SyncStatus.PENDING,
localChanges: true
}
return merged
}
}
六、监控与调试
6.1 存储监控
// 存储监控接口
interface StorageMonitor {
// 容量统计
getUsage(): {
total: number
used: number
zones: Record<StorageZone, number>
}
// 同步统计
getSyncStats(): {
pending: number
synced: number
failed: number
lastSyncTime: number
}
// 清理建议
getCleanupRecommendations(): CleanupRecommendation[]
}
6.2 调试工具
// 开发环境调试工具
if (process.env.NODE_ENV === 'development') {
window.__offlineDebug__ = {
// 查看存储内容
inspect: () => offlineStorage.getAll(),
// 查看同步队列
queue: () => syncQueue.getAll(),
// 模拟离线
goOffline: () => networkStatus.value.isConnected = false,
// 模拟在线
goOnline: () => networkStatus.value.isConnected = true,
// 强制同步
forceSync: () => syncEngine.fullSync(),
// 清空存储
clear: () => offlineStorage.clear()
}
}
七、风险与对策
| 风险 | 影响 | 对策 |
|---|---|---|
| 存储容量超限 | 数据丢失 | LRU淘汰 + 容量监控 |
| 数据冲突 | 数据不一致 | 版本控制 + 冲突检测 |
| 同步失败 | 数据丢失 | 重试机制 + 本地备份 |
| 性能下降 | 用户体验差 | 增量同步 + 后台处理 |
| 安全风险 | 数据泄露 | 敏感数据加密 |
八、总结
本方案采用 离线优先 架构,通过以下措施确保数据一致性和用户体验:
- 分层存储: 系统数据、业务数据、临时数据分区管理
- 增量同步: 基于版本号的增量同步,减少数据传输
- 冲突检测: 自动检测和解决数据冲突
- 容量管理: LRU淘汰策略,防止存储溢出
- 安全加密: 敏感数据加密存储
预计开发周期: 6-10天
技术复杂度: 中等
维护成本: 低