diff --git a/docs/06-IMPLEMENTATION/IMPL-005-客户端优先架构调整方案.md b/docs/06-IMPLEMENTATION/IMPL-005-客户端优先架构调整方案.md new file mode 100644 index 0000000..643abf0 --- /dev/null +++ b/docs/06-IMPLEMENTATION/IMPL-005-客户端优先架构调整方案.md @@ -0,0 +1,1406 @@ +# 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)