PSI进销存系统离线优先架构设计与实现
引言
在仓储、物流、门店等场景下,网络连接往往不稳定或完全不可用。离线优先(Offline-First)架构可以让应用在无网络环境下正常工作,恢复网络后自动同步数据,保证业务连续性。
离线优先架构概述
离线优先的核心设计原则:
| 原则 | 描述 | 实现方式 |
|---|---|---|
| 本地优先 | 数据先存本地,网络可用时同步 | IndexedDB + 队列 |
| 增量同步 | 只同步变更数据,减少流量 | CRDT + 变更日志 |
| 冲突处理 | 多端修改时智能合并 | 最后写入胜出 + 手动解决 |
| 离线感知 | UI 实时反映网络状态 | Service Worker |
本地数据存储
使用 IndexedDB 实现本地存储:
// 本地数据库管理器
class LocalDatabase {
constructor(dbName, version) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
// 初始化数据库
async initialize() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
this.createStores(event.target.result);
};
});
}
// 创建对象存储
createStores(db) {
// 商品表
if (!db.objectStoreNames.contains('products')) {
const productStore = db.createObjectStore('products', { keyPath: 'id' });
productStore.createIndex('barcode', 'barcode', { unique: true });
productStore.createIndex('categoryId', 'categoryId', { unique: false });
}
// 库存表
if (!db.objectStoreNames.contains('inventory')) {
const inventoryStore = db.createObjectStore('inventory', { keyPath: 'productId' });
inventoryStore.createIndex('warehouseId', 'warehouseId', { unique: false });
}
// 库存流水
if (!db.objectStoreNames.contains('inventoryLogs')) {
const logStore = db.createObjectStore('inventoryLogs', { keyPath: 'id', autoIncrement: true });
logStore.createIndex('productId', 'productId', { unique: false });
logStore.createIndex('timestamp', 'timestamp', { unique: false });
}
// 待同步队列
if (!db.objectStoreNames.contains('syncQueue')) {
const syncStore = db.createObjectStore('syncQueue', { keyPath: 'id', autoIncrement: true });
syncStore.createIndex('type', 'type', { unique: false });
syncStore.createIndex('status', 'status', { unique: false });
}
// 订单表
if (!db.objectStoreNames.contains('orders')) {
const orderStore = db.createObjectStore('orders', { keyPath: 'id' });
orderStore.createIndex('status', 'status', { unique: false });
orderStore.createIndex('createdAt', 'createdAt', { unique: false });
}
}
// 添加/更新记录
async put(storeName, data) {
return new Promise((resolve, reject) => {
const tx = this.db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
const request = store.put(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 获取记录
async get(storeName, key) {
return new Promise((resolve, reject) => {
const tx = this.db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 获取所有记录
async getAll(storeName, options = {}) {
return new Promise((resolve, reject) => {
const tx = this.db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
let request;
if (options.index && options.value) {
const index = store.index(options.index);
request = index.getAll(options.value);
} else {
request = store.getAll();
}
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 删除记录
async delete(storeName, key) {
return new Promise((resolve, reject) => {
const tx = this.db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
const request = store.delete(key);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
// 批量操作
async batchPut(storeName, items) {
const tx = this.db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
for (const item of items) {
store.put(item);
}
return new Promise((resolve, reject) => {
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
}
同步队列管理
离线操作记录与同步管理:
// 同步队列管理器
class SyncQueue {
constructor(localDb) {
this.localDb = localDb;
this.isSyncing = false;
this.onSyncProgress = null;
}
// 添加同步任务
async addOperation(type, entity, data, action = 'create') {
const operation = {
id: this.generateId(),
type,
entity,
action,
data: JSON.parse(JSON.stringify(data)), // 深拷贝
timestamp: Date.now(),
status: 'pending',
retryCount: 0,
error: null
};
await this.localDb.put('syncQueue', operation);
// 尝试立即同步
this.trySync();
return operation.id;
}
// 同步操作类型
async syncOperation(type, entity, data, action) {
const url = `/api/${entity}`;
const method = action === 'delete' ? 'DELETE' :
action === 'update' ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await this.getToken()}`
},
body: action !== 'delete' ? JSON.stringify(data) : undefined
});
if (!response.ok) {
throw new Error(`Sync failed: ${response.status}`);
}
return response.json();
}
// 执行同步
async sync() {
if (this.isSyncing) return;
this.isSyncing = true;
try {
// 获取所有待同步的操作
const pendingOps = await this.localDb.getAll('syncQueue', {
index: 'status',
value: 'pending'
});
// 按时间顺序排序
pendingOps.sort((a, b) => a.timestamp - b.timestamp);
let successCount = 0;
let failCount = 0;
for (const op of pendingOps) {
try {
// 更新状态为同步中
op.status = 'syncing';
await this.localDb.put('syncQueue', op);
// 执行同步
await this.syncOperation(op.type, op.entity, op.data, op.action);
// 同步成功,删除队列
await this.localDb.delete('syncQueue', op.id);
successCount++;
// 回调进度
if (this.onSyncProgress) {
this.onSyncProgress({
total: pendingOps.length,
completed: successCount,
failed: failCount
});
}
} catch (error) {
// 同步失败
op.status = 'pending';
op.error = error.message;
op.retryCount++;
// 超过最大重试次数,标记为失败
if (op.retryCount >= 5) {
op.status = 'failed';
}
await this.localDb.put('syncQueue', op);
failCount++;
}
}
return { successCount, failCount };
} finally {
this.isSyncing = false;
}
}
// 自动同步(网络恢复时)
startAutoSync() {
// 监听网络状态变化
window.addEventListener('online', () => {
console.log('Network online, starting sync...');
this.sync();
});
// 定时同步
setInterval(() => {
if (navigator.onLine) {
this.sync();
}
}, 60000); // 每分钟检查一次
}
// 生成唯一ID
generateId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}
数据冲突处理
解决多端数据冲突:
// 冲突解决器
class ConflictResolver {
constructor(localDb) {
this.localDb = localDb;
this.strategies = {
'last-write-wins': this.lastWriteWins.bind(this),
'server-wins': this.serverWins.bind(this),
'client-wins': this.clientWins.bind(this),
'manual': this.manualResolve.bind(this)
};
}
// 最后写入胜出(默认策略)
async lastWriteWins(localData, serverData) {
return {
resolved: true,
data: localData.timestamp > serverData.timestamp ? localData : serverData,
strategy: 'last-write-wins'
};
}
// 服务端数据优先
async serverWins(localData, serverData) {
return {
resolved: true,
data: serverData,
strategy: 'server-wins'
};
}
// 客户端数据优先
async clientWins(localData, serverData) {
return {
resolved: true,
data: localData,
strategy: 'client-wins'
};
}
// 手动解决(需要用户参与)
async manualResolve(localData, serverData) {
return {
resolved: false,
needsManual: true,
localData,
serverData,
strategy: 'manual'
};
}
// 智能字段合并
mergeFields(localData, serverData) {
const merged = { ...serverData };
for (const [key, value] of Object.entries(localData)) {
// 如果本地值更新,使用本地值
if (serverData[key] === undefined) {
merged[key] = value;
} else if (typeof value === 'object' && typeof serverData[key] === 'object') {
// 递归合并对象
merged[key] = this.mergeFields(value, serverData[key]);
}
}
return merged;
}
// 处理冲突
async resolveConflict(entityType, localData, serverData, strategy = 'last-write-wins') {
const resolver = this.strategies[strategy];
if (!resolver) {
throw new Error(`Unknown strategy: ${strategy}`);
}
if (entityType === 'product') {
// 商品:优先级策略
return resolver.call(this, localData, serverData);
} else if (entityType === 'order') {
// 订单:服务端优先(订单状态以服务端为准)
return resolver.call(this, localData, serverData);
} else if (entityType === 'inventory') {
// 库存:智能合并
if (strategy === 'last-write-wins') {
return this.lastWriteWins(localData, serverData);
}
}
return resolver.call(this, localData, serverData);
}
// 批量冲突检测
async detectConflicts(localData, serverData) {
const conflicts = [];
for (const [id, local] of Object.entries(localData)) {
const server = serverData[id];
if (server && this.hasConflict(local, server)) {
conflicts.push({
id,
local,
server,
entityType: this.getEntityType(id)
});
}
}
return conflicts;
}
// 检测是否有冲突
hasConflict(local, server) {
return local.timestamp > server.timestamp &&
JSON.stringify(local) !== JSON.stringify(server);
}
}
// 冲突解决 UI
class ConflictResolutionUI {
constructor(conflictResolver) {
this.resolver = conflictResolver;
this.listeners = {};
}
// 显示冲突解决对话框
showResolveDialog(conflict) {
return new Promise((resolve) => {
// 创建对话框 DOM
const dialog = document.createElement('div');
dialog.className = 'conflict-dialog';
dialog.innerHTML = `
数据冲突解决
检测到数据冲突,请选择保留的版本:
`;
// 绑定事件
dialog.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', async (e) => {
const resolution = e.target.dataset.resolution;
let result;
if (resolution === 'local') {
result = await this.resolver.resolveConflict(
conflict.entityType,
conflict.local,
conflict.server,
'client-wins'
);
} else if (resolution === 'server') {
result = await this.resolver.resolveConflict(
conflict.entityType,
conflict.local,
conflict.server,
'server-wins'
);
} else {
result = {
resolved: true,
data: this.resolver.mergeFields(conflict.local, conflict.server),
strategy: 'merge'
};
}
document.body.removeChild(dialog);
resolve(result);
});
});
document.body.appendChild(dialog);
});
}
}
离线状态感知
实时响应网络状态变化:
// 网络状态管理器
class NetworkManager extends EventTarget {
constructor() {
super();
this.isOnline = navigator.onLine;
this.lastOnlineTime = null;
this.listenToNetworkChanges();
}
// 监听网络变化
listenToNetworkChanges() {
window.addEventListener('online', () => {
this.isOnline = true;
this.lastOnlineTime = Date.now();
this.dispatchEvent(new CustomEvent('statuschange', {
detail: { online: true }
}));
});
window.addEventListener('offline', () => {
this.isOnline = false;
this.dispatchEvent(new CustomEvent('statuschange', {
detail: { online: false }
}));
});
}
// 检查网络状态
checkConnection() {
return fetch('/api/health', { method: 'HEAD' })
.then(() => true)
.catch(() => false);
}
}
// 离线指示器 UI
class OfflineIndicator {
constructor(networkManager) {
this.networkManager = networkManager;
this.createIndicator();
this.bindEvents();
}
createIndicator() {
this.indicator = document.createElement('div');
this.indicator.className = 'offline-indicator offline';
this.indicator.innerHTML = `
📡
`;
this.indicator.style.display = 'none';
document.body.appendChild(this.indicator);
}
bindEvents() {
this.networkManager.addEventListener('statuschange', (e) => {
if (e.detail.online) {
this.showOnline();
} else {
this.showOffline();
}
});
}
showOnline() {
this.indicator.classList.remove('offline');
this.indicator.classList.add('online');
this.indicator.querySelector('.message').textContent = '网络已恢复';
setTimeout(() => {
this.indicator.style.display = 'none';
}, 3000);
}
showOffline() {
this.indicator.classList.remove('online');
this.indicator.classList.add('offline');
this.indicator.querySelector('.message').textContent = '您已离线,数据将本地保存';
this.indicator.style.display = 'block';
}
}
// Service Worker 注册
async function registerServiceWorker() {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('Service Worker registered:', registration);
// 监听更新
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// 有新版本可用
console.log('New version available');
}
});
});
} catch (error) {
console.error('Service Worker registration failed:', error);
}
}
}
最佳实践建议
- 减少数据量:只缓存必要数据,本地数据越少同步越快
- 冲突策略:根据业务场景选择合适的冲突解决策略
- 离线体验:离线时也要保证 UI 可用,提示用户当前状态
- 增量同步:只同步变更数据,避免全量同步浪费带宽
- 异常处理:做好各种异常情况的处理,给用户明确反馈
总结
离线优先架构让进销存系统在各种网络环境下都能正常工作:
- 本地存储:IndexedDB 实现大容量本地存储
- 同步队列:离线操作自动排队,网络恢复后同步
- 冲突处理:多种策略解决数据冲突
- 状态感知:实时反映网络状态,提升用户体验
离线优先架构是移动端进销存系统的必备能力。