# 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 数据验证前置 #### 前端验证规则 ```javascript // 验证规则定义 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 }; } ``` #### 后端验证简化 ```java // 后端只做核心验证 @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 数据计算前置 #### 金额计算 ```javascript // 前端金额计算 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' } // } ``` #### 库存计算 ```javascript // 前端库存计算 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缓存实现 #### 数据库初始化 ```javascript // 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(); ``` #### 缓存服务封装 ```javascript // 缓存服务 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 离线功能实现 #### 离线队列 ```javascript // 离线队列管理 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加密 ```javascript // 前端加密工具 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签名 ```javascript // 数据签名 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 后端验证简化 ```java // 后端验证加密数据 @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 预约名额实时计算 ```javascript // 预约名额管理 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 状态管理 ```javascript // 使用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) - 后端验证加密格式和完整性 - 后端二次加密存储(可选) **代码调整**: ```javascript // 前端加密 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:支付接口幂等性校验方案 **调整建议**:前端幂等键管理 + 后端简化验证 **原方案**: - 后端分布式锁 - 后端幂等性检查 **新方案**: - 前端幂等键生成和管理 - 前端本地幂等性检查 - 后端数据库唯一索引验证 **代码调整**: ```javascript // 前端幂等性管理 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 + 降级方案 + 浏览器检测 --- ## 十一、相关文档 - [改进路线图](../05-PLANS/改进路线图.md) - [IMPL-002-敏感数据加密存储方案](./IMPL-002-敏感数据加密存储方案.md) - [IMPL-003-预约高峰期性能优化方案](./IMPL-003-预约高峰期性能优化方案.md) - [IMPL-004-支付接口幂等性校验方案](./IMPL-004-支付接口幂等性校验方案.md)