ether-docs/02-DESIGN/domains/07-OFFLINE_STORAGE.md

666 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 员工端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 存在问题
1. **数据模型不完整**: 缺少业务数据的离线模型定义
2. **冲突解决缺失**: 没有处理数据冲突的策略
3. **存储容量管理**: 没有容量限制和清理策略
4. **数据加密**: 敏感数据未加密存储
5. **同步策略简单**: 仅支持重试,缺少增量同步
---
## 二、行业最佳实践
### 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 数据模型设计
```typescript
// 离线数据基础模型
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 核心接口设计
```typescript
// 离线存储接口
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天)
1. **完善数据模型**
- 定义 `OfflineEntity` 基类
- 为业务实体添加离线元数据
2. **增强存储层**
- 实现存储分区管理
- 添加容量限制和LRU淘汰
- 实现数据加密
3. **优化同步队列**
- 支持操作优先级
- 添加依赖关系处理
- 实现批量同步
### 4.2 阶段二:同步引擎 (2-3天)
1. **增量同步**
- 实现版本号比较
- 只同步变更数据
2. **冲突检测**
- 检测版本冲突
- 记录冲突信息
3. **冲突解决**
- 实现自动解决策略
- 提供手动解决接口
### 4.3 阶段三:业务集成 (2-3天)
1. **工单模块集成**
- 实现 `WorkOrderRepository`
- 集成离线操作
2. **巡检模块集成**
- 实现 `InspectionRepository`
- 集成离线操作
3. **访客模块集成**
- 实现 `VisitorRepository`
- 集成离线操作
### 4.4 阶段四:测试优化 (1-2天)
1. **单元测试**
- 存储层测试
- 同步引擎测试
2. **集成测试**
- 离线场景测试
- 网络恢复测试
3. **性能优化**
- 存储性能优化
- 同步性能优化
---
## 五、关键代码示例
### 5.1 离线存储基类
```typescript
// 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 同步队列增强
```typescript
// 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 冲突检测与解决
```typescript
// 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 存储监控
```typescript
// 存储监控接口
interface StorageMonitor {
// 容量统计
getUsage(): {
total: number
used: number
zones: Record<StorageZone, number>
}
// 同步统计
getSyncStats(): {
pending: number
synced: number
failed: number
lastSyncTime: number
}
// 清理建议
getCleanupRecommendations(): CleanupRecommendation[]
}
```
### 6.2 调试工具
```typescript
// 开发环境调试工具
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淘汰 + 容量监控 |
| 数据冲突 | 数据不一致 | 版本控制 + 冲突检测 |
| 同步失败 | 数据丢失 | 重试机制 + 本地备份 |
| 性能下降 | 用户体验差 | 增量同步 + 后台处理 |
| 安全风险 | 数据泄露 | 敏感数据加密 |
---
## 八、总结
本方案采用 **离线优先** 架构,通过以下措施确保数据一致性和用户体验:
1. **分层存储**: 系统数据、业务数据、临时数据分区管理
2. **增量同步**: 基于版本号的增量同步,减少数据传输
3. **冲突检测**: 自动检测和解决数据冲突
4. **容量管理**: LRU淘汰策略防止存储溢出
5. **安全加密**: 敏感数据加密存储
**预计开发周期**: 6-10天
**技术复杂度**: 中等
**维护成本**: 低