个人编制软件展示

PSI - Purchase Sale Inventory 进销存软件

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 = `
        

数据冲突解决

检测到数据冲突,请选择保留的版本:

本地版本

${JSON.stringify(conflict.local, null, 2)}

服务器版本

${JSON.stringify(conflict.server, null, 2)}

合并版本

`; // 绑定事件 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);
    }
  }
}

最佳实践建议

总结

离线优先架构让进销存系统在各种网络环境下都能正常工作:

离线优先架构是移动端进销存系统的必备能力。

← 下一篇:PSI进销存系统离线模式与数据同步