b34adbd011
- 业务逻辑前置方案 - 本地数据缓存方案(IndexedDB + LocalStorage) - 前端加密计算方案(Web Crypto API) - 实时计算客户端化方案 - 离线功能实现 - 对现有改进项的影响分析
40 KiB
40 KiB
IMPL-005: 客户端优先架构调整方案
文档编号: GYM-IMPL-005
版本: v1.0
日期: 2026-04-05
作者: 张翔
状态: 正式发布
文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
|---|---|---|---|
| v1.0 | 2026-04-05 | 张翔 | 创建客户端优先架构调整方案 |
一、需求分析
1.1 问题背景
当前架构存在以下问题:
- 后端资源占用高,服务器压力大
- 用户体验受网络延迟影响
- 缺少离线功能支持
- 客户端算力未充分利用
1.2 调整目标
核心目标:让资源和算力留在客户端,减少后端的资源占用和压力
具体目标:
- 业务逻辑前置:将数据验证、格式化、计算等逻辑移到前端
- 本地数据缓存:使用LocalStorage、IndexedDB等本地存储,减少服务器请求
- 前端加密计算:在前端进行数据加密、解密,减少服务器计算压力
- 实时计算客户端化:将实时计算、状态管理、复杂业务逻辑移到客户端
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 + 降级方案 + 浏览器检测