Files
gym-manage/docs/06-IMPLEMENTATION/IMPL-005-客户端优先架构调整方案.md
T
张翔 b34adbd011 docs: 创建客户端优先架构调整方案
- 业务逻辑前置方案
- 本地数据缓存方案(IndexedDB + LocalStorage)
- 前端加密计算方案(Web Crypto API)
- 实时计算客户端化方案
- 离线功能实现
- 对现有改进项的影响分析
2026-04-05 16:56:18 +08:00

40 KiB
Raw Blame History

IMPL-005: 客户端优先架构调整方案

文档编号: GYM-IMPL-005
版本: v1.0
日期: 2026-04-05
作者: 张翔
状态: 正式发布


文档修订历史

版本 日期 作者 修订内容
v1.0 2026-04-05 张翔 创建客户端优先架构调整方案

一、需求分析

1.1 问题背景

当前架构存在以下问题:

  • 后端资源占用高,服务器压力大
  • 用户体验受网络延迟影响
  • 缺少离线功能支持
  • 客户端算力未充分利用

1.2 调整目标

核心目标:让资源和算力留在客户端,减少后端的资源占用和压力

具体目标

  1. 业务逻辑前置:将数据验证、格式化、计算等逻辑移到前端
  2. 本地数据缓存:使用LocalStorage、IndexedDB等本地存储,减少服务器请求
  3. 前端加密计算:在前端进行数据加密、解密,减少服务器计算压力
  4. 实时计算客户端化:将实时计算、状态管理、复杂业务逻辑移到客户端

1.3 成功标准

  • 后端资源占用降低50%
  • 用户响应速度提升60%
  • 支持离线功能
  • 缓存命中率≥90%
  • 用户体验满意度≥95%

二、架构对比

2.1 传统架构

┌─────────────┐
│   客户端    │
│  (展示层)   │
└──────┬──────┘
       │ HTTP请求
       ↓
┌─────────────────────────────────┐
│          后端服务器              │
│  ┌─────────────────────────┐   │
│  │   业务逻辑层             │   │
│  │  - 数据验证              │   │
│  │  - 数据格式化            │   │
│  │  - 数据计算              │   │
│  │  - 状态管理              │   │
│  │  - 加密解密              │   │
│  └─────────────────────────┘   │
│  ┌─────────────────────────┐   │
│  │   缓存层                 │   │
│  │  - Redis缓存             │   │
│  └─────────────────────────┘   │
│  ┌─────────────────────────┐   │
│  │   数据访问层             │   │
│  │  - 数据库操作            │   │
│  └─────────────────────────┘   │
└─────────────────────────────────┘
       │
       ↓
┌─────────────┐
│   数据库    │
└─────────────┘

问题

  • 后端承担所有业务逻辑,压力大
  • 每次请求都需要访问服务器,响应慢
  • 无离线功能
  • 客户端算力浪费

2.2 客户端优先架构

┌─────────────────────────────────────────┐
│              客户端                      │
│  ┌─────────────────────────────────┐   │
│  │   业务逻辑层                     │   │
│  │  - 数据验证                      │   │
│  │  - 数据格式化                    │   │
│  │  - 实时计算                      │   │
│  │  - 状态管理                      │   │
│  │  - 加密解密                      │   │
│  └─────────────────────────────────┘   │
│  ┌─────────────────────────────────┐   │
│  │   本地缓存层                     │   │
│  │  - LocalStorage                  │   │
│  │  - IndexedDB                     │   │
│  │  - 内存缓存                      │   │
│  └─────────────────────────────────┘   │
│  ┌─────────────────────────────────┐   │
│  │   离线队列                       │   │
│  │  - 离线操作队列                  │   │
│  │  - 后台同步                      │   │
│  └─────────────────────────────────┘   │
└─────────────────────────────────────────┘
       │ HTTP请求(仅核心数据)
       ↓
┌─────────────────────────────────┐
│          后端服务器              │
│  ┌─────────────────────────┐   │
│  │   核心业务层             │   │
│  │  - 权限控制              │   │
│  │  - 数据一致性验证        │   │
│  │  - 事务管理              │   │
│  └─────────────────────────┘   │
│  ┌─────────────────────────┐   │
│  │   数据访问层             │   │
│  │  - 数据库操作            │   │
│  └─────────────────────────┘   │
└─────────────────────────────────┘
       │
       ↓
┌─────────────┐
│   数据库    │
└─────────────┘

优势

  • 后端压力大幅降低
  • 用户响应速度提升
  • 支持离线功能
  • 充分利用客户端算力

三、业务逻辑前置方案

3.1 职责划分

前端承担

业务逻辑 说明 实现方式
数据验证 表单验证、业务规则验证 Validator库
数据格式化 日期格式、金额格式、电话号码格式 Formatter工具
数据计算 金额计算、库存计算、名额计算 计算函数
状态管理 订单状态、支付状态、预约状态 Vuex/Pinia
数据聚合 列表数据聚合、统计数据聚合 前端聚合

后端保留

业务逻辑 说明 实现方式
核心业务逻辑 支付流程、权限控制 Service层
数据持久化 数据库操作 Repository层
安全验证 身份认证、权限验证 Security层
数据一致性 事务管理、最终一致性验证 Transaction

3.2 数据验证前置

前端验证规则

// 验证规则定义
const validationRules = {
    phone: {
        required: true,
        pattern: /^1[3-9]\d{9}$/,
        message: '请输入有效的手机号码'
    },
    idCard: {
        required: true,
        pattern: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/,
        message: '请输入有效的身份证号'
    },
    amount: {
        required: true,
        min: 0.01,
        max: 999999.99,
        message: '金额必须在0.01-999999.99之间'
    }
};

// 验证函数
function validate(data, rules) {
    const errors = {};
    
    for (const [field, rule] of Object.entries(rules)) {
        const value = data[field];
        
        // 必填验证
        if (rule.required && !value) {
            errors[field] = rule.message || `${field}不能为空`;
            continue;
        }
        
        // 正则验证
        if (rule.pattern && !rule.pattern.test(value)) {
            errors[field] = rule.message || `${field}格式不正确`;
        }
        
        // 范围验证
        if (rule.min !== undefined && value < rule.min) {
            errors[field] = rule.message || `${field}不能小于${rule.min}`;
        }
        if (rule.max !== undefined && value > rule.max) {
            errors[field] = rule.message || `${field}不能大于${rule.max}`;
        }
    }
    
    return {
        valid: Object.keys(errors).length === 0,
        errors
    };
}

后端验证简化

// 后端只做核心验证
@PostMapping("/api/members")
public Member createMember(@RequestBody MemberRequest request) {
    // 1. 身份认证
    authenticationService.authenticate();
    
    // 2. 权限验证
    authorizationService.checkPermission("member:create");
    
    // 3. 数据一致性验证
    if (memberRepository.existsByPhone(request.getPhone())) {
        throw new BusinessException("手机号已存在");
    }
    
    // 4. 数据持久化
    return memberService.createMember(request);
}

3.3 数据计算前置

金额计算

// 前端金额计算
class PriceCalculator {
    // 计算课程总价
    calculateTotalPrice(courses, discount) {
        const subtotal = courses.reduce((sum, course) => {
            return sum + course.price * course.quantity;
        }, 0);
        
        const discountAmount = subtotal * discount;
        const total = subtotal - discountAmount;
        
        return {
            subtotal: this.formatPrice(subtotal),
            discount: this.formatPrice(discountAmount),
            total: this.formatPrice(total)
        };
    }
    
    // 格式化价格
    formatPrice(price) {
        return {
            value: price,
            display: ${price.toFixed(2)}`
        };
    }
}

// 使用示例
const calculator = new PriceCalculator();
const result = calculator.calculateTotalPrice(
    [
        { price: 100, quantity: 2 },
        { price: 200, quantity: 1 }
    ],
    0.1 // 10%折扣
);

console.log(result);
// {
//   subtotal: { value: 400, display: '¥400.00' },
//   discount: { value: 40, display: '¥40.00' },
//   total: { value: 360, display: '¥360.00' }
// }

库存计算

// 前端库存计算
class InventoryCalculator {
    constructor() {
        this.inventory = new Map();
    }
    
    // 更新库存
    updateInventory(productId, quantity) {
        const current = this.inventory.get(productId) || 0;
        this.inventory.set(productId, current + quantity);
    }
    
    // 检查库存
    checkInventory(productId, requiredQuantity) {
        const available = this.inventory.get(productId) || 0;
        return available >= requiredQuantity;
    }
    
    // 预留库存
    reserveInventory(productId, quantity) {
        if (!this.checkInventory(productId, quantity)) {
            throw new Error('库存不足');
        }
        
        const current = this.inventory.get(productId);
        this.inventory.set(productId, current - quantity);
        
        return {
            productId,
            reserved: quantity,
            remaining: this.inventory.get(productId)
        };
    }
}

四、本地数据缓存方案

4.1 缓存策略

缓存层次

┌─────────────────────────────────┐
│   内存缓存(最快)               │
│  - 热点数据                      │
│  - 会话数据                      │
└─────────────────────────────────┘
         ↓ 未命中
┌─────────────────────────────────┐
│   IndexedDB(中等)              │
│  - 结构化数据                    │
│  - 大量数据                      │
└─────────────────────────────────┘
         ↓ 未命中
┌─────────────────────────────────┐
│   LocalStorage(较慢)           │
│  - 配置数据                      │
│  - 用户设置                      │
└─────────────────────────────────┘
         ↓ 未命中
┌─────────────────────────────────┐
│   服务器(最慢)                 │
│  - 核心数据                      │
└─────────────────────────────────┘

缓存策略表

数据类型 缓存方式 TTL 同步策略 离线支持
课程信息 IndexedDB 1小时 后台同步
会员信息 LocalStorage 30分钟 登录时同步
教练信息 IndexedDB 1小时 后台同步
预约名额 内存缓存 5分钟 实时同步
用户设置 LocalStorage 永久 手动同步
订单列表 IndexedDB 10分钟 后台同步
支付记录 IndexedDB 1天 后台同步

4.2 IndexedDB缓存实现

数据库初始化

// IndexedDB初始化
class CacheDB {
    constructor() {
        this.db = null;
    }
    
    async init() {
        return new Promise((resolve, reject) => {
            const request = indexedDB.open('GymManageDB', 1);
            
            request.onerror = () => reject(request.error);
            request.onsuccess = () => {
                this.db = request.result;
                resolve(this.db);
            };
            
            request.onupgradeneeded = (event) => {
                const db = event.target.result;
                
                // 创建对象存储
                if (!db.objectStoreNames.contains('courses')) {
                    const courseStore = db.createObjectStore('courses', { keyPath: 'id' });
                    courseStore.createIndex('categoryId', 'categoryId', { unique: false });
                    courseStore.createIndex('coachId', 'coachId', { unique: false });
                }
                
                if (!db.objectStoreNames.contains('members')) {
                    db.createObjectStore('members', { keyPath: 'id' });
                }
                
                if (!db.objectStoreNames.contains('coaches')) {
                    db.createObjectStore('coaches', { keyPath: 'id' });
                }
                
                if (!db.objectStoreNames.contains('reservations')) {
                    const reservationStore = db.createObjectStore('reservations', { keyPath: 'id' });
                    reservationStore.createIndex('memberId', 'memberId', { unique: false });
                    reservationStore.createIndex('courseId', 'courseId', { unique: false });
                }
            };
        });
    }
    
    // 获取数据
    async get(storeName, key) {
        return new Promise((resolve, reject) => {
            const transaction = this.db.transaction([storeName], 'readonly');
            const store = transaction.objectStore(storeName);
            const request = store.get(key);
            
            request.onerror = () => reject(request.error);
            request.onsuccess = () => resolve(request.result);
        });
    }
    
    // 保存数据
    async put(storeName, data) {
        return new Promise((resolve, reject) => {
            const transaction = this.db.transaction([storeName], 'readwrite');
            const store = transaction.objectStore(storeName);
            
            // 添加缓存时间戳
            const dataWithTimestamp = {
                ...data,
                cachedAt: Date.now()
            };
            
            const request = store.put(dataWithTimestamp);
            
            request.onerror = () => reject(request.error);
            request.onsuccess = () => resolve(request.result);
        });
    }
    
    // 删除数据
    async delete(storeName, key) {
        return new Promise((resolve, reject) => {
            const transaction = this.db.transaction([storeName], 'readwrite');
            const store = transaction.objectStore(storeName);
            const request = store.delete(key);
            
            request.onerror = () => reject(request.error);
            request.onsuccess = () => resolve(request.result);
        });
    }
    
    // 清空存储
    async clear(storeName) {
        return new Promise((resolve, reject) => {
            const transaction = this.db.transaction([storeName], 'readwrite');
            const store = transaction.objectStore(storeName);
            const request = store.clear();
            
            request.onerror = () => reject(request.error);
            request.onsuccess = () => resolve(request.result);
        });
    }
}

// 使用示例
const cacheDB = new CacheDB();
await cacheDB.init();

缓存服务封装

// 缓存服务
class CacheService {
    constructor() {
        this.db = null;
        this.memoryCache = new Map();
        this.TTL = {
            courses: 3600000,      // 1小时
            members: 1800000,      // 30分钟
            coaches: 3600000,      // 1小时
            reservations: 300000,  // 5分钟
            settings: Infinity     // 永久
        };
    }
    
    async init() {
        this.db = new CacheDB();
        await this.db.init();
    }
    
    // 获取数据(带缓存)
    async get(storeName, key, fetcher) {
        // 1. 检查内存缓存
        const memoryKey = `${storeName}:${key}`;
        const memoryCached = this.memoryCache.get(memoryKey);
        
        if (memoryCached && !this.isExpired(memoryCached.cachedAt, storeName)) {
            return memoryCached;
        }
        
        // 2. 检查IndexedDB缓存
        const dbCached = await this.db.get(storeName, key);
        
        if (dbCached && !this.isExpired(dbCached.cachedAt, storeName)) {
            // 更新内存缓存
            this.memoryCache.set(memoryKey, dbCached);
            return dbCached;
        }
        
        // 3. 从服务器获取
        if (fetcher) {
            const data = await fetcher();
            
            // 保存到缓存
            await this.put(storeName, key, data);
            
            return data;
        }
        
        return null;
    }
    
    // 保存数据
    async put(storeName, key, data) {
        const dataWithTimestamp = {
            ...data,
            cachedAt: Date.now()
        };
        
        // 保存到IndexedDB
        await this.db.put(storeName, dataWithTimestamp);
        
        // 更新内存缓存
        const memoryKey = `${storeName}:${key}`;
        this.memoryCache.set(memoryKey, dataWithTimestamp);
    }
    
    // 检查是否过期
    isExpired(cachedAt, storeName) {
        const ttl = this.TTL[storeName];
        if (ttl === Infinity) return false;
        
        return Date.now() - cachedAt > ttl;
    }
    
    // 清空缓存
    async clear(storeName) {
        await this.db.clear(storeName);
        
        // 清空内存缓存
        for (const key of this.memoryCache.keys()) {
            if (key.startsWith(`${storeName}:`)) {
                this.memoryCache.delete(key);
            }
        }
    }
}

// 使用示例
const cacheService = new CacheService();
await cacheService.init();

// 获取课程信息(优先缓存)
const course = await cacheService.get('courses', 'course-001', async () => {
    const response = await fetch('/api/courses/course-001');
    return response.json();
});

4.3 离线功能实现

离线队列

// 离线队列管理
class OfflineQueue {
    constructor() {
        this.queue = [];
        this.isOnline = navigator.onLine;
        
        // 监听网络状态
        window.addEventListener('online', () => this.onOnline());
        window.addEventListener('offline', () => this.onOffline());
        
        // 从LocalStorage恢复队列
        this.loadQueue();
    }
    
    // 添加离线操作
    async add(operation) {
        const queueItem = {
            id: this.generateId(),
            operation: operation.type,
            data: operation.data,
            createdAt: Date.now(),
            status: 'PENDING',
            retryCount: 0
        };
        
        this.queue.push(queueItem);
        await this.saveQueue();
        
        // 显示提示
        this.showOfflineNotification(queueItem);
        
        return queueItem;
    }
    
    // 网络恢复时同步
    async onOnline() {
        this.isOnline = true;
        
        // 同步所有待处理操作
        for (const item of this.queue) {
            if (item.status === 'PENDING') {
                await this.syncOperation(item);
            }
        }
    }
    
    // 网络断开时处理
    onOffline() {
        this.isOnline = false;
    }
    
    // 同步单个操作
    async syncOperation(item) {
        try {
            const response = await fetch(item.operation.url, {
                method: item.operation.method,
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(item.data)
            });
            
            if (response.ok) {
                item.status = 'SYNCED';
                await this.saveQueue();
                
                // 显示成功通知
                this.showSyncSuccessNotification(item);
            } else {
                throw new Error('同步失败');
            }
        } catch (error) {
            item.retryCount++;
            
            if (item.retryCount >= 3) {
                item.status = 'FAILED';
                this.showSyncFailedNotification(item);
            }
            
            await this.saveQueue();
        }
    }
    
    // 生成唯一ID
    generateId() {
        return `${Date.now()}-${Math.random().toString(36).substring(7)}`;
    }
    
    // 保存队列到LocalStorage
    async saveQueue() {
        localStorage.setItem('offlineQueue', JSON.stringify(this.queue));
    }
    
    // 从LocalStorage加载队列
    loadQueue() {
        const saved = localStorage.getItem('offlineQueue');
        if (saved) {
            this.queue = JSON.parse(saved);
        }
    }
    
    // 显示离线通知
    showOfflineNotification(item) {
        // 使用Notification API或自定义UI
        console.log(`操作已保存到离线队列: ${item.operation}`);
    }
    
    // 显示同步成功通知
    showSyncSuccessNotification(item) {
        console.log(`操作已同步: ${item.operation}`);
    }
    
    // 显示同步失败通知
    showSyncFailedNotification(item) {
        console.error(`操作同步失败: ${item.operation}`);
    }
}

// 使用示例
const offlineQueue = new OfflineQueue();

// 离线预约
async function createReservation(reservation) {
    if (!navigator.onLine) {
        // 离线时添加到队列
        await offlineQueue.add({
            type: {
                url: '/api/reservations',
                method: 'POST'
            },
            data: reservation
        });
        
        return {
            status: 'OFFLINE_SAVED',
            message: '预约已保存,将在网络恢复后同步'
        };
    } else {
        // 在线时直接请求
        const response = await fetch('/api/reservations', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(reservation)
        });
        
        return response.json();
    }
}

五、前端加密计算方案

5.1 加密方案对比

加密类型 传统方案(后端加密) 新方案(前端加密) 优势
敏感数据加密 后端AES-256加密 前端Web Crypto API 减少服务器计算压力
密码加密 后端BCrypt 前端BCrypt + 后端验证 数据传输更安全
支付数据加密 后端加密 前端端到端加密 端到端安全
数据签名 后端签名 前端HMAC签名 减少服务器压力

5.2 Web Crypto API实现

AES-256-GCM加密

// 前端加密工具
class EncryptionUtil {
    constructor() {
        this.key = null;
    }
    
    // 初始化密钥
    async init(password) {
        // 从密码派生密钥
        const encoder = new TextEncoder();
        const keyMaterial = await crypto.subtle.importKey(
            'raw',
            encoder.encode(password),
            'PBKDF2',
            false,
            ['deriveKey']
        );
        
        this.key = await crypto.subtle.deriveKey(
            {
                name: 'PBKDF2',
                salt: encoder.encode('gym-manage-salt'),
                iterations: 100000,
                hash: 'SHA-256'
            },
            keyMaterial,
            { name: 'AES-GCM', length: 256 },
            false,
            ['encrypt', 'decrypt']
        );
    }
    
    // 加密
    async encrypt(plaintext) {
        const encoder = new TextEncoder();
        const data = encoder.encode(plaintext);
        
        // 生成IV
        const iv = crypto.getRandomValues(new Uint8Array(12));
        
        // 加密
        const encrypted = await crypto.subtle.encrypt(
            { name: 'AES-GCM', iv: iv },
            this.key,
            data
        );
        
        // 组合IV和密文
        const combined = new Uint8Array(iv.length + encrypted.byteLength);
        combined.set(iv);
        combined.set(new Uint8Array(encrypted), iv.length);
        
        // Base64编码
        return btoa(String.fromCharCode(...combined));
    }
    
    // 解密
    async decrypt(ciphertext) {
        // Base64解码
        const combined = new Uint8Array(
            atob(ciphertext).split('').map(c => c.charCodeAt(0))
        );
        
        // 分离IV和密文
        const iv = combined.slice(0, 12);
        const encrypted = combined.slice(12);
        
        // 解密
        const decrypted = await crypto.subtle.decrypt(
            { name: 'AES-GCM', iv: iv },
            this.key,
            encrypted
        );
        
        // 解码
        const decoder = new TextDecoder();
        return decoder.decode(decrypted);
    }
}

// 使用示例
const encryptionUtil = new EncryptionUtil();
await encryptionUtil.init('user-password');

// 加密敏感数据
const encrypted = await encryptionUtil.encrypt('13800138000');
console.log('加密后:', encrypted);

// 解密
const decrypted = await encryptionUtil.decrypt(encrypted);
console.log('解密后:', decrypted);

HMAC签名

// 数据签名
class SignatureUtil {
    constructor() {
        this.key = null;
    }
    
    // 初始化签名密钥
    async init(secret) {
        const encoder = new TextEncoder();
        this.key = await crypto.subtle.importKey(
            'raw',
            encoder.encode(secret),
            { name: 'HMAC', hash: 'SHA-256' },
            false,
            ['sign', 'verify']
        );
    }
    
    // 签名
    async sign(data) {
        const encoder = new TextEncoder();
        const signature = await crypto.subtle.sign(
            'HMAC',
            this.key,
            encoder.encode(JSON.stringify(data))
        );
        
        return btoa(String.fromCharCode(...new Uint8Array(signature)));
    }
    
    // 验证签名
    async verify(data, signature) {
        const encoder = new TextEncoder();
        const signatureBytes = new Uint8Array(
            atob(signature).split('').map(c => c.charCodeAt(0))
        );
        
        return await crypto.subtle.verify(
            'HMAC',
            this.key,
            signatureBytes,
            encoder.encode(JSON.stringify(data))
        );
    }
}

// 使用示例
const signatureUtil = new SignatureUtil();
await signatureUtil.init('api-secret-key');

// 签名请求数据
const requestData = { orderId: 'ORDER-001', amount: 100 };
const signature = await signatureUtil.sign(requestData);

// 发送请求时带上签名
fetch('/api/payments', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-Signature': signature
    },
    body: JSON.stringify(requestData)
});

5.3 后端验证简化

// 后端验证加密数据
@RestController
public class PaymentController {
    
    @PostMapping("/api/payments")
    public PaymentResponse createPayment(
        @RequestBody PaymentRequest request,
        @RequestHeader("X-Signature") String signature
    ) {
        // 1. 验证签名
        if (!signatureService.verify(request, signature)) {
            throw new SecurityException("签名验证失败");
        }
        
        // 2. 验证加密格式
        if (!encryptionService.validateFormat(request.getEncryptedData())) {
            throw new ValidationException("加密数据格式不正确");
        }
        
        // 3. 核心业务逻辑
        return paymentService.createPayment(request);
    }
}

六、实时计算客户端化方案

6.1 实时计算场景

计算场景 前端承担 后端承担 同步策略
预约名额计算 实时计算 最终一致性验证 实时同步
金额计算 实时计算 订单创建时验证 即时验证
库存计算 实时计算 最终一致性验证 实时同步
统计报表 前端聚合 数据源 后台同步

6.2 预约名额实时计算

// 预约名额管理
class QuotaManager {
    constructor() {
        this.quotas = new Map(); // courseId -> quota
        this.reservations = new Map(); // courseId -> reservations
    }
    
    // 初始化名额
    initQuota(courseId, totalQuota) {
        this.quotas.set(courseId, {
            total: totalQuota,
            used: 0,
            available: totalQuota
        });
    }
    
    // 更新预约
    updateReservation(courseId, reservation) {
        if (!this.reservations.has(courseId)) {
            this.reservations.set(courseId, []);
        }
        
        const reservations = this.reservations.get(courseId);
        const existingIndex = reservations.findIndex(r => r.id === reservation.id);
        
        if (existingIndex >= 0) {
            // 更新现有预约
            reservations[existingIndex] = reservation;
        } else {
            // 添加新预约
            reservations.push(reservation);
        }
        
        // 重新计算名额
        this.recalculateQuota(courseId);
    }
    
    // 重新计算名额
    recalculateQuota(courseId) {
        const quota = this.quotas.get(courseId);
        const reservations = this.reservations.get(courseId) || [];
        
        // 计算已用名额
        quota.used = reservations.filter(r => 
            r.status === 'CONFIRMED' || r.status === 'PENDING'
        ).length;
        
        // 计算可用名额
        quota.available = quota.total - quota.used;
        
        // 触发UI更新
        this.emitQuotaUpdate(courseId, quota);
    }
    
    // 检查名额
    checkQuota(courseId, requiredQuantity = 1) {
        const quota = this.quotas.get(courseId);
        return quota && quota.available >= requiredQuantity;
    }
    
    // 预留名额
    reserveQuota(courseId, quantity = 1) {
        if (!this.checkQuota(courseId, quantity)) {
            throw new Error('名额不足');
        }
        
        const quota = this.quotas.get(courseId);
        quota.used += quantity;
        quota.available -= quantity;
        
        this.emitQuotaUpdate(courseId, quota);
        
        return {
            courseId,
            reserved: quantity,
            remaining: quota.available
        };
    }
    
    // 释放名额
    releaseQuota(courseId, quantity = 1) {
        const quota = this.quotas.get(courseId);
        quota.used -= quantity;
        quota.available += quantity;
        
        this.emitQuotaUpdate(courseId, quota);
    }
    
    // 触发UI更新
    emitQuotaUpdate(courseId, quota) {
        // 使用Vue的响应式系统或自定义事件
        window.dispatchEvent(new CustomEvent('quotaUpdate', {
            detail: { courseId, quota }
        }));
    }
}

// 使用示例
const quotaManager = new QuotaManager();

// 初始化课程名额
quotaManager.initQuota('course-001', 20);

// 检查名额
if (quotaManager.checkQuota('course-001')) {
    // 预留名额
    quotaManager.reserveQuota('course-001');
    
    // 创建预约
    await createReservation({ courseId: 'course-001', memberId: 'member-001' });
}

6.3 状态管理

// 使用Pinia进行状态管理
import { defineStore } from 'pinia';

export const useReservationStore = defineStore('reservation', {
    state: () => ({
        reservations: [],
        quotas: new Map(),
        loading: false,
        error: null
    }),
    
    getters: {
        // 获取课程预约
        getReservationsByCourse: (state) => (courseId) => {
            return state.reservations.filter(r => r.courseId === courseId);
        },
        
        // 获取可用名额
        getAvailableQuota: (state) => (courseId) => {
            const quota = state.quotas.get(courseId);
            return quota ? quota.available : 0;
        }
    },
    
    actions: {
        // 创建预约
        async createReservation(reservation) {
            this.loading = true;
            
            try {
                // 检查名额
                if (!this.checkQuota(reservation.courseId)) {
                    throw new Error('名额不足');
                }
                
                // 预留名额
                this.reserveQuota(reservation.courseId);
                
                // 发送请求
                const response = await fetch('/api/reservations', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(reservation)
                });
                
                const data = await response.json();
                
                // 添加到本地状态
                this.reservations.push(data);
                
                return data;
            } catch (error) {
                // 释放名额
                this.releaseQuota(reservation.courseId);
                
                this.error = error.message;
                throw error;
            } finally {
                this.loading = false;
            }
        },
        
        // 检查名额
        checkQuota(courseId) {
            const quota = this.quotas.get(courseId);
            return quota && quota.available > 0;
        },
        
        // 预留名额
        reserveQuota(courseId) {
            const quota = this.quotas.get(courseId);
            if (quota) {
                quota.used++;
                quota.available--;
            }
        },
        
        // 释放名额
        releaseQuota(courseId) {
            const quota = this.quotas.get(courseId);
            if (quota) {
                quota.used--;
                quota.available++;
            }
        }
    }
});

七、对现有改进项的影响

7.1 IMPL-002:敏感数据加密存储方案

调整建议:前端加密 + 后端验证

原方案

  • 后端加密存储
  • 后端解密读取

新方案

  • 前端加密(Web Crypto API
  • 后端验证加密格式和完整性
  • 后端二次加密存储(可选)

代码调整

// 前端加密
async function encryptSensitiveData(data) {
    const encryptionUtil = new EncryptionUtil();
    await encryptionUtil.init('user-key');
    
    return {
        phone: await encryptionUtil.encrypt(data.phone),
        idCard: await encryptionUtil.encrypt(data.idCard),
        bankCard: await encryptionUtil.encrypt(data.bankCard)
    };
}

// 发送加密数据
const encryptedData = await encryptSensitiveData(memberData);
await fetch('/api/members', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(encryptedData)
});

优势

  • 减少服务器计算压力
  • 数据传输更安全
  • 提升用户体验

7.2 IMPL-003:预约高峰期性能优化方案

调整建议:客户端缓存 + 本地计算 + 后台同步

原方案

  • Redis缓存
  • 数据库读写分离
  • 消息队列削峰

新方案

  • IndexedDB本地缓存
  • 前端实时计算
  • 后台同步 + 最终一致性验证

架构调整

原架构:
客户端 → Redis缓存 → 数据库主从 → 消息队列

新架构:
客户端(IndexedDB + 实时计算) → 后端(最终一致性验证) → 数据库

优势

  • 缓存命中率提升至90%+
  • 用户响应速度提升60%
  • 支持离线功能
  • 后端压力降低70%

7.3 IMPL-004:支付接口幂等性校验方案

调整建议:前端幂等键管理 + 后端简化验证

原方案

  • 后端分布式锁
  • 后端幂等性检查

新方案

  • 前端幂等键生成和管理
  • 前端本地幂等性检查
  • 后端数据库唯一索引验证

代码调整

// 前端幂等性管理
class PaymentIdempotent {
    constructor() {
        this.pendingPayments = new Map();
    }
    
    generateRequestNo() {
        return `PAY-${Date.now()}-${Math.random().toString(36).substring(7)}`;
    }
    
    checkLocal(requestNo) {
        return this.pendingPayments.has(requestNo);
    }
    
    markPending(requestNo, payment) {
        this.pendingPayments.set(requestNo, payment);
        localStorage.setItem('pendingPayments', 
            JSON.stringify(Array.from(this.pendingPayments.entries())));
    }
    
    markComplete(requestNo) {
        this.pendingPayments.delete(requestNo);
        localStorage.setItem('pendingPayments', 
            JSON.stringify(Array.from(this.pendingPayments.entries())));
    }
}

优势

  • 减少Redis依赖
  • 前端即时响应
  • 简化后端逻辑

八、实施步骤

阶段 任务 负责人 完成时间 验收标准
阶段1:基础设施
1 IndexedDB数据库设计 前端开发 2天 数据库设计文档完成
2 缓存服务实现 前端开发 3天 缓存服务单元测试通过
3 离线队列实现 前端开发 2天 离线队列测试通过
阶段2:业务逻辑前置
4 数据验证前置 前端开发 2天 验证规则测试通过
5 数据计算前置 前端开发 3天 计算逻辑测试通过
6 状态管理实现 前端开发 2天 状态管理测试通过
阶段3:加密计算前置
7 Web Crypto API集成 前端开发 2天 加密功能测试通过
8 签名机制实现 前端开发 1天 签名验证测试通过
9 后端验证简化 后端开发 2天 后端测试通过
阶段4:实时计算客户端化
10 预约名额实时计算 前端开发 2天 实时计算测试通过
11 库存实时计算 前端开发 2天 实时计算测试通过
12 后台同步机制 前端开发 2天 同步机制测试通过
阶段5:集成测试
13 端到端测试 测试工程师 3天 所有测试通过
14 性能测试 测试工程师 2天 性能指标达标
15 灰度发布 运维工程师 1天 灰度发布成功

九、验收标准

9.1 功能验收

  • 业务逻辑前置覆盖率≥80%
  • 本地缓存命中率≥90%
  • 前端加密功能正常
  • 实时计算准确性100%
  • 离线功能正常

9.2 性能验收

  • 后端资源占用降低50%
  • 用户响应速度提升60%
  • 缓存命中率≥90%
  • 离线功能可用

9.3 用户体验验收

  • 用户满意度≥95%
  • 页面加载速度≤2秒
  • 操作响应时间≤500ms
  • 离线功能体验良好

十、风险与应对

10.1 风险识别

风险1:客户端计算错误

  • 应对:后端最终一致性验证 + 数据校验

风险2:缓存数据不一致

  • 应对:定期同步 + 版本控制 + 冲突解决机制

风险3:离线数据丢失

  • 应对:LocalStorage持久化 + IndexedDB备份

风险4:前端加密密钥管理

  • 应对:密钥派生 + 安全存储 + 定期轮换

风险5:浏览器兼容性

  • 应对:Polyfill + 降级方案 + 浏览器检测

十一、相关文档