refactor(mp): E3-2 大文件拆分 + U3-2 微交互统一
E3-2 大文件拆分(3 文件 → 6 文件): - daily-monitoring 449L → useDailyMonitoring.ts hook(238L) + 页面(255L) - request.ts 376L → cache.ts(75L) + limiter.ts(32L) + 主文件(278L) - BLEManager.ts 363L → BLEConnection.ts(212L) + 主文件(228L) U3-2 微交互统一: - 新增 haptic.ts 工具(light/medium/heavy 三级触觉反馈) - PrimaryButton 点击触发 hapticLight() - tokens.scss 新增 5 个动画时序 token(duration/easing) - mixins.scss 新增 fade-in() mixin(支持 fast/normal/slow 三档)
This commit is contained in:
212
apps/miniprogram/src/services/ble/BLEConnection.ts
Normal file
212
apps/miniprogram/src/services/ble/BLEConnection.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import type {
|
||||
BLEDevice,
|
||||
BLEConnection as BLEConnectionInfo,
|
||||
BLEConnectionState,
|
||||
NormalizedReading,
|
||||
BLEConnectionChangeResult,
|
||||
BLECharacteristicChangeResult,
|
||||
BLEServiceItem,
|
||||
} from './types';
|
||||
|
||||
/** BLE 连接管理 — 封装连接/断开/服务发现/通知订阅/数据监听 */
|
||||
export class BLEConnection {
|
||||
private conn: BLEConnectionInfo | null = null;
|
||||
private readings: NormalizedReading[] = [];
|
||||
private connChangeHandler: ((res: BLEConnectionChangeResult) => void) | null = null;
|
||||
private charChangeHandler: ((res: BLECharacteristicChangeResult) => void) | null = null;
|
||||
|
||||
private onStateChange: (state: BLEConnectionState, error?: string) => void;
|
||||
private onNewReadings: (readings: NormalizedReading[]) => void;
|
||||
|
||||
constructor(callbacks: {
|
||||
onStateChange: (state: BLEConnectionState, error?: string) => void;
|
||||
onNewReadings: (readings: NormalizedReading[]) => void;
|
||||
}) {
|
||||
this.onStateChange = callbacks.onStateChange;
|
||||
this.onNewReadings = callbacks.onNewReadings;
|
||||
}
|
||||
|
||||
/** 获取当前连接信息 */
|
||||
getConnection(): BLEConnectionInfo | null {
|
||||
return this.conn;
|
||||
}
|
||||
|
||||
/** 获取缓存的读数 */
|
||||
getCachedReadings(): NormalizedReading[] {
|
||||
return [...this.readings];
|
||||
}
|
||||
|
||||
/** 清除缓存读数 */
|
||||
clearReadings(): void {
|
||||
this.readings = [];
|
||||
}
|
||||
|
||||
/** 设置缓存读数(供外部 BLEManager 使用) */
|
||||
setReadings(readings: NormalizedReading[]): void {
|
||||
this.readings = readings;
|
||||
}
|
||||
|
||||
private updateState(state: BLEConnectionState, error?: string): void {
|
||||
if (this.conn) {
|
||||
this.conn = { ...this.conn, state, error };
|
||||
}
|
||||
this.onStateChange(state, error);
|
||||
}
|
||||
|
||||
/** 连接到设备 */
|
||||
async connect(device: BLEDevice, maxLiveReadings: number): Promise<void> {
|
||||
if (!device.adapter) throw new Error('设备无适配器');
|
||||
|
||||
this.conn = {
|
||||
deviceId: device.deviceId,
|
||||
state: 'connecting',
|
||||
adapter: device.adapter,
|
||||
};
|
||||
this.updateState('connecting');
|
||||
this.readings = [];
|
||||
|
||||
try {
|
||||
await Taro.createBLEConnection({
|
||||
deviceId: device.deviceId,
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// 移除旧监听器,避免多次 connect 累积
|
||||
if (this.connChangeHandler) {
|
||||
Taro.offBLEConnectionStateChange(this.connChangeHandler);
|
||||
}
|
||||
if (this.charChangeHandler) {
|
||||
Taro.offBLECharacteristicValueChange(this.charChangeHandler);
|
||||
}
|
||||
|
||||
// 监听断连
|
||||
this.connChangeHandler = (res: BLEConnectionChangeResult) => {
|
||||
if (res.deviceId === device.deviceId && !res.connected) {
|
||||
this.updateState('disconnected', '设备断开连接');
|
||||
this.conn = null;
|
||||
}
|
||||
};
|
||||
Taro.onBLEConnectionStateChange(this.connChangeHandler);
|
||||
|
||||
// 发现服务
|
||||
await this.discoverServices(device);
|
||||
|
||||
// 监听数据通知
|
||||
this.charChangeHandler = (res: BLECharacteristicChangeResult) => {
|
||||
if (res.deviceId !== device.deviceId) return;
|
||||
const newReadings = device.adapter!.parseNotification(
|
||||
res.serviceId,
|
||||
res.characteristicId,
|
||||
res.value,
|
||||
);
|
||||
if (newReadings.length > 0) {
|
||||
const combined = [...this.readings, ...newReadings];
|
||||
this.readings = combined.length > maxLiveReadings
|
||||
? combined.slice(-maxLiveReadings)
|
||||
: combined;
|
||||
this.onNewReadings(newReadings);
|
||||
}
|
||||
};
|
||||
Taro.onBLECharacteristicValueChange(this.charChangeHandler);
|
||||
|
||||
this.conn = { ...this.conn, state: 'connected', connectedAt: Date.now() };
|
||||
this.updateState('connected');
|
||||
} catch (e: unknown) {
|
||||
const errMsg = (e as { errMsg?: string })?.errMsg;
|
||||
const msg = errMsg || (e instanceof Error ? e.message : '') || '连接失败';
|
||||
this.updateState('error', msg);
|
||||
this.conn = null;
|
||||
throw new Error(errMsg || '蓝牙连接失败');
|
||||
}
|
||||
}
|
||||
|
||||
/** 发现服务并启用通知 */
|
||||
private async discoverServices(device: BLEDevice): Promise<void> {
|
||||
const servicesRes = await Taro.getBLEDeviceServices({ deviceId: device.deviceId });
|
||||
const services = servicesRes.services || [];
|
||||
|
||||
for (const { service: svcUUID, characteristic: charUUID } of device.adapter!.notifyCharacteristics) {
|
||||
const svc = services.find((s: BLEServiceItem) =>
|
||||
s.uuid.toUpperCase().includes(svcUUID.toUpperCase()),
|
||||
);
|
||||
if (!svc) continue;
|
||||
|
||||
await Taro.getBLEDeviceCharacteristics({
|
||||
deviceId: device.deviceId,
|
||||
serviceId: svc.uuid,
|
||||
});
|
||||
|
||||
await Taro.notifyBLECharacteristicValueChange({
|
||||
deviceId: device.deviceId,
|
||||
serviceId: svc.uuid,
|
||||
characteristicId: charUUID,
|
||||
state: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** 手动读取特征值 */
|
||||
async readCharacteristics(): Promise<NormalizedReading[]> {
|
||||
if (!this.conn || this.conn.state !== 'connected') {
|
||||
throw new Error('设备未连接');
|
||||
}
|
||||
|
||||
const { deviceId, adapter } = this.conn;
|
||||
const results: NormalizedReading[] = [];
|
||||
|
||||
const servicesRes = await Taro.getBLEDeviceServices({ deviceId });
|
||||
const services = servicesRes.services || [];
|
||||
|
||||
for (const { service: svcUUID, characteristic: charUUID } of adapter.readCharacteristics) {
|
||||
const svc = services.find((s: BLEServiceItem) =>
|
||||
s.uuid.toUpperCase().includes(svcUUID.toUpperCase()),
|
||||
);
|
||||
if (!svc) continue;
|
||||
|
||||
try {
|
||||
await Taro.readBLECharacteristicValue({
|
||||
deviceId,
|
||||
serviceId: svc.uuid,
|
||||
characteristicId: charUUID,
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('[ble] 读取特征值失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
if (results.length > 0) {
|
||||
this.readings = [...this.readings, ...results];
|
||||
this.onNewReadings(results);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/** 断开连接 */
|
||||
async disconnect(): Promise<void> {
|
||||
if (!this.conn) return;
|
||||
|
||||
const { deviceId } = this.conn;
|
||||
|
||||
// 移除 BLE 监听器,防止断开后仍收到回调
|
||||
if (this.connChangeHandler) {
|
||||
Taro.offBLEConnectionStateChange(this.connChangeHandler);
|
||||
this.connChangeHandler = null;
|
||||
}
|
||||
if (this.charChangeHandler) {
|
||||
Taro.offBLECharacteristicValueChange(this.charChangeHandler);
|
||||
this.charChangeHandler = null;
|
||||
}
|
||||
|
||||
try {
|
||||
await Taro.closeBLEConnection({ deviceId });
|
||||
} catch {
|
||||
// 忽略断连错误
|
||||
}
|
||||
|
||||
this.conn = null;
|
||||
this.readings = [];
|
||||
this.updateState('disconnected');
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,15 @@ import Taro from '@tarojs/taro';
|
||||
import type {
|
||||
DeviceAdapter,
|
||||
BLEDevice,
|
||||
BLEConnection,
|
||||
BLEConnection as BLEConnectionInfo,
|
||||
BLEConnectionState,
|
||||
NormalizedReading,
|
||||
SyncResult,
|
||||
BLEManagerConfig,
|
||||
BLEScanResult,
|
||||
BLEConnectionChangeResult,
|
||||
BLECharacteristicChangeResult,
|
||||
BLEServiceItem,
|
||||
} from './types';
|
||||
import { DataBuffer } from './DataBuffer';
|
||||
import { BLEConnection } from './BLEConnection';
|
||||
|
||||
const DEFAULT_CONFIG: BLEManagerConfig = {
|
||||
scanTimeout: 10000,
|
||||
@@ -24,20 +22,27 @@ const MAX_LIVE_READINGS = 200;
|
||||
|
||||
export class BLEManager {
|
||||
private adapters: DeviceAdapter[] = [];
|
||||
private connection: BLEConnection | null = null;
|
||||
private readings: NormalizedReading[] = [];
|
||||
private bleConnection: BLEConnection;
|
||||
private dataBuffer: DataBuffer;
|
||||
private config: BLEManagerConfig;
|
||||
private scanTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private onConnectionChange?: (state: BLEConnectionState) => void;
|
||||
private onReadings?: (readings: NormalizedReading[]) => void;
|
||||
private connChangeHandler: ((res: BLEConnectionChangeResult) => void) | null = null;
|
||||
private charChangeHandler: ((res: BLECharacteristicChangeResult) => void) | null = null;
|
||||
|
||||
constructor(config?: Partial<BLEManagerConfig>) {
|
||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||
this.dataBuffer = new DataBuffer();
|
||||
this.dataBuffer.restore();
|
||||
|
||||
this.bleConnection = new BLEConnection({
|
||||
onStateChange: (state) => {
|
||||
this.onConnectionChange?.(state);
|
||||
},
|
||||
onNewReadings: (readings) => {
|
||||
this.dataBuffer.push(readings);
|
||||
this.onReadings?.(readings);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 注册设备适配器 */
|
||||
@@ -55,13 +60,6 @@ export class BLEManager {
|
||||
this.onReadings = cb;
|
||||
}
|
||||
|
||||
private updateState(state: BLEConnectionState, error?: string): void {
|
||||
if (this.connection) {
|
||||
this.connection = { ...this.connection, state, error };
|
||||
}
|
||||
this.onConnectionChange?.(state);
|
||||
}
|
||||
|
||||
/** 匹配设备到适配器 */
|
||||
private matchAdapter(deviceName: string): DeviceAdapter | undefined {
|
||||
const lower = deviceName.toLowerCase();
|
||||
@@ -136,137 +134,26 @@ export class BLEManager {
|
||||
|
||||
/** 连接到设备 */
|
||||
async connect(device: BLEDevice): Promise<void> {
|
||||
if (!device.adapter) throw new Error('设备无适配器');
|
||||
|
||||
this.connection = {
|
||||
deviceId: device.deviceId,
|
||||
state: 'connecting',
|
||||
adapter: device.adapter,
|
||||
};
|
||||
this.updateState('connecting');
|
||||
this.readings = [];
|
||||
|
||||
try {
|
||||
await Taro.createBLEConnection({
|
||||
deviceId: device.deviceId,
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// 移除旧监听器,避免多次 connect 累积
|
||||
if (this.connChangeHandler) {
|
||||
Taro.offBLEConnectionStateChange(this.connChangeHandler);
|
||||
}
|
||||
if (this.charChangeHandler) {
|
||||
Taro.offBLECharacteristicValueChange(this.charChangeHandler);
|
||||
}
|
||||
|
||||
// 监听断连
|
||||
this.connChangeHandler = (res: BLEConnectionChangeResult) => {
|
||||
if (res.deviceId === device.deviceId && !res.connected) {
|
||||
this.updateState('disconnected', '设备断开连接');
|
||||
this.connection = null;
|
||||
}
|
||||
};
|
||||
Taro.onBLEConnectionStateChange(this.connChangeHandler);
|
||||
|
||||
// 发现服务
|
||||
const servicesRes = await Taro.getBLEDeviceServices({ deviceId: device.deviceId });
|
||||
const services = servicesRes.services || [];
|
||||
|
||||
// 启用通知
|
||||
for (const { service: svcUUID, characteristic: charUUID } of device.adapter.notifyCharacteristics) {
|
||||
const svc = services.find((s: BLEServiceItem) => s.uuid.toUpperCase().includes(svcUUID.toUpperCase()));
|
||||
if (!svc) continue;
|
||||
|
||||
await Taro.getBLEDeviceCharacteristics({
|
||||
deviceId: device.deviceId,
|
||||
serviceId: svc.uuid,
|
||||
});
|
||||
|
||||
await Taro.notifyBLECharacteristicValueChange({
|
||||
deviceId: device.deviceId,
|
||||
serviceId: svc.uuid,
|
||||
characteristicId: charUUID,
|
||||
state: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 监听数据通知
|
||||
this.charChangeHandler = (res: BLECharacteristicChangeResult) => {
|
||||
if (res.deviceId !== device.deviceId) return;
|
||||
const newReadings = device.adapter!.parseNotification(
|
||||
res.serviceId,
|
||||
res.characteristicId,
|
||||
res.value,
|
||||
);
|
||||
if (newReadings.length > 0) {
|
||||
const combined = [...this.readings, ...newReadings];
|
||||
this.readings = combined.length > MAX_LIVE_READINGS
|
||||
? combined.slice(-MAX_LIVE_READINGS)
|
||||
: combined;
|
||||
this.dataBuffer.push(newReadings);
|
||||
this.onReadings?.(newReadings);
|
||||
}
|
||||
};
|
||||
Taro.onBLECharacteristicValueChange(this.charChangeHandler);
|
||||
|
||||
this.connection = { ...this.connection, state: 'connected', connectedAt: Date.now() };
|
||||
this.updateState('connected');
|
||||
} catch (e: unknown) {
|
||||
const errMsg = (e as { errMsg?: string })?.errMsg;
|
||||
const msg = errMsg || (e instanceof Error ? e.message : '') || '连接失败';
|
||||
this.updateState('error', msg);
|
||||
this.connection = null;
|
||||
throw new Error(errMsg || '蓝牙连接失败');
|
||||
}
|
||||
return this.bleConnection.connect(device, MAX_LIVE_READINGS);
|
||||
}
|
||||
|
||||
/** 手动读取特征值 */
|
||||
async readCharacteristics(): Promise<NormalizedReading[]> {
|
||||
if (!this.connection || this.connection.state !== 'connected') {
|
||||
throw new Error('设备未连接');
|
||||
}
|
||||
|
||||
const { deviceId, adapter } = this.connection;
|
||||
const results: NormalizedReading[] = [];
|
||||
|
||||
const servicesRes = await Taro.getBLEDeviceServices({ deviceId });
|
||||
const services = servicesRes.services || [];
|
||||
|
||||
for (const { service: svcUUID, characteristic: charUUID } of adapter.readCharacteristics) {
|
||||
const svc = services.find((s: BLEServiceItem) => s.uuid.toUpperCase().includes(svcUUID.toUpperCase()));
|
||||
if (!svc) continue;
|
||||
|
||||
try {
|
||||
await Taro.readBLECharacteristicValue({
|
||||
deviceId,
|
||||
serviceId: svc.uuid,
|
||||
characteristicId: charUUID,
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('[ble] 读取特征值失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
if (results.length > 0) {
|
||||
this.readings = [...this.readings, ...results];
|
||||
this.onReadings?.(results);
|
||||
}
|
||||
|
||||
return results;
|
||||
return this.bleConnection.readCharacteristics();
|
||||
}
|
||||
|
||||
/** 同步收集的读数到后端 */
|
||||
async syncToServer(uploadFn: (readings: NormalizedReading[]) => Promise<number>): Promise<SyncResult> {
|
||||
if (!this.connection) {
|
||||
const conn = this.bleConnection.getConnection();
|
||||
if (!conn) {
|
||||
return { success: false, readingsCount: 0, uploadedCount: 0, error: '未连接设备' };
|
||||
}
|
||||
|
||||
this.updateState('syncing');
|
||||
this.onConnectionChange?.('syncing');
|
||||
|
||||
const batch = this.dataBuffer.flush();
|
||||
if (batch.length === 0) {
|
||||
this.updateState('connected');
|
||||
this.onConnectionChange?.('connected');
|
||||
return { success: true, readingsCount: 0, uploadedCount: 0 };
|
||||
}
|
||||
|
||||
@@ -274,8 +161,8 @@ export class BLEManager {
|
||||
for (let attempt = 1; attempt <= this.config.retryCount; attempt++) {
|
||||
try {
|
||||
const uploaded = await uploadFn(batch);
|
||||
this.readings = [];
|
||||
this.updateState('connected');
|
||||
this.bleConnection.clearReadings();
|
||||
this.onConnectionChange?.('connected');
|
||||
return {
|
||||
success: true,
|
||||
readingsCount: batch.length,
|
||||
@@ -288,35 +175,13 @@ export class BLEManager {
|
||||
}
|
||||
}
|
||||
|
||||
this.updateState('error', lastError);
|
||||
this.onConnectionChange?.('error');
|
||||
return { success: false, readingsCount: batch.length, uploadedCount: 0, error: lastError };
|
||||
}
|
||||
|
||||
/** 断开连接 */
|
||||
async disconnect(): Promise<void> {
|
||||
if (!this.connection) return;
|
||||
|
||||
const { deviceId } = this.connection;
|
||||
|
||||
// 移除 BLE 监听器,防止断开后仍收到回调
|
||||
if (this.connChangeHandler) {
|
||||
Taro.offBLEConnectionStateChange(this.connChangeHandler);
|
||||
this.connChangeHandler = null;
|
||||
}
|
||||
if (this.charChangeHandler) {
|
||||
Taro.offBLECharacteristicValueChange(this.charChangeHandler);
|
||||
this.charChangeHandler = null;
|
||||
}
|
||||
|
||||
try {
|
||||
await Taro.closeBLEConnection({ deviceId });
|
||||
} catch {
|
||||
// 忽略断连错误
|
||||
}
|
||||
|
||||
this.connection = null;
|
||||
this.readings = [];
|
||||
this.updateState('disconnected');
|
||||
return this.bleConnection.disconnect();
|
||||
}
|
||||
|
||||
/** 关闭蓝牙适配器 */
|
||||
@@ -330,18 +195,18 @@ export class BLEManager {
|
||||
}
|
||||
|
||||
/** 获取当前连接信息 */
|
||||
getConnection(): BLEConnection | null {
|
||||
return this.connection;
|
||||
getConnection(): BLEConnectionInfo | null {
|
||||
return this.bleConnection.getConnection();
|
||||
}
|
||||
|
||||
/** 获取缓存的读数 */
|
||||
getCachedReadings(): NormalizedReading[] {
|
||||
return [...this.readings];
|
||||
return this.bleConnection.getCachedReadings();
|
||||
}
|
||||
|
||||
/** 清除缓存读数 */
|
||||
clearReadings(): void {
|
||||
this.readings = [];
|
||||
this.bleConnection.clearReadings();
|
||||
}
|
||||
|
||||
/** 启动时检查缓存,有未上传数据则自动重传 */
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import { secureGet, secureSet, secureRemove } from '@/utils/secure-storage';
|
||||
import { ResponseCache } from './request/cache';
|
||||
import { ConcurrencyLimiter } from './request/limiter';
|
||||
|
||||
const BASE_URL = process.env.TARO_APP_API_URL || 'http://localhost:3000/api/v1';
|
||||
|
||||
@@ -24,107 +26,7 @@ function safeGet(key: string): string {
|
||||
return secureGet(key);
|
||||
}
|
||||
|
||||
// --- Concurrency limiter ---
|
||||
|
||||
class ConcurrencyLimiter {
|
||||
private active = 0;
|
||||
private queue: Array<() => void> = [];
|
||||
|
||||
constructor(private max: number) {}
|
||||
|
||||
acquire(): Promise<void> {
|
||||
if (this.active < this.max) {
|
||||
this.active++;
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise<void>((resolve) => { this.queue.push(resolve); });
|
||||
}
|
||||
|
||||
release(): void {
|
||||
this.active--;
|
||||
const next = this.queue.shift();
|
||||
if (next) { this.active++; next(); }
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.active = 0;
|
||||
this.queue.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const limiter = new ConcurrencyLimiter(8);
|
||||
|
||||
// --- Response cache + deduplication ---
|
||||
|
||||
interface CacheEntry { data: unknown; expiry: number }
|
||||
|
||||
class ResponseCache {
|
||||
private cache = new Map<string, CacheEntry>();
|
||||
private inflight = new Map<string, Promise<unknown>>();
|
||||
private patientId = '';
|
||||
|
||||
constructor(private maxSize = 100, private defaultTtl = 60_000) {}
|
||||
|
||||
setPatientId(id: string): void {
|
||||
if (this.patientId !== id) {
|
||||
this.patientId = id;
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
getPatientId(): string { return this.patientId; }
|
||||
|
||||
private cacheKey(url: string): string {
|
||||
return `${url}#${this.patientId}`;
|
||||
}
|
||||
|
||||
get<T>(url: string): T | null {
|
||||
const entry = this.cache.get(this.cacheKey(url));
|
||||
if (entry && Date.now() < entry.expiry) return entry.data as T;
|
||||
return null;
|
||||
}
|
||||
|
||||
getInflight<T>(url: string): Promise<T> | null {
|
||||
return (this.inflight.get(this.cacheKey(url)) as Promise<T> | undefined) ?? null;
|
||||
}
|
||||
|
||||
setInflight(url: string, promise: Promise<unknown>): void {
|
||||
this.inflight.set(this.cacheKey(url), promise);
|
||||
}
|
||||
|
||||
removeInflight(url: string): void {
|
||||
this.inflight.delete(this.cacheKey(url));
|
||||
}
|
||||
|
||||
set(url: string, data: unknown, ttl?: number): void {
|
||||
const key = this.cacheKey(url);
|
||||
const effectiveTtl = ttl ?? this.defaultTtl;
|
||||
if (effectiveTtl <= 0) return;
|
||||
if (this.cache.size >= this.maxSize) {
|
||||
const oldest = this.cache.keys().next().value;
|
||||
if (oldest) this.cache.delete(oldest);
|
||||
}
|
||||
this.cache.set(key, { data, expiry: Date.now() + effectiveTtl });
|
||||
}
|
||||
|
||||
clear(prefix?: string): void {
|
||||
if (prefix) {
|
||||
for (const key of this.cache.keys()) {
|
||||
if (key.includes(prefix)) this.cache.delete(key);
|
||||
}
|
||||
} else {
|
||||
this.cache.clear();
|
||||
}
|
||||
this.inflight.clear();
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.cache.clear();
|
||||
this.inflight.clear();
|
||||
this.patientId = '';
|
||||
}
|
||||
}
|
||||
|
||||
const responseCache = new ResponseCache();
|
||||
|
||||
// --- Headers cache ---
|
||||
|
||||
75
apps/miniprogram/src/services/request/cache.ts
Normal file
75
apps/miniprogram/src/services/request/cache.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* ResponseCache — GET 请求结果缓存 + 请求去重(inflight dedup)
|
||||
*
|
||||
* 缓存条目按 `{url}#{patientId}` 做复合键,切换患者时自动清空。
|
||||
* inflight map 保证同一个 URL 在并发场景下只发一次网络请求。
|
||||
*/
|
||||
|
||||
interface CacheEntry { data: unknown; expiry: number }
|
||||
|
||||
export class ResponseCache {
|
||||
private cache = new Map<string, CacheEntry>();
|
||||
private inflight = new Map<string, Promise<unknown>>();
|
||||
private patientId = '';
|
||||
|
||||
constructor(private maxSize = 100, private defaultTtl = 60_000) {}
|
||||
|
||||
setPatientId(id: string): void {
|
||||
if (this.patientId !== id) {
|
||||
this.patientId = id;
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
getPatientId(): string { return this.patientId; }
|
||||
|
||||
private cacheKey(url: string): string {
|
||||
return `${url}#${this.patientId}`;
|
||||
}
|
||||
|
||||
get<T>(url: string): T | null {
|
||||
const entry = this.cache.get(this.cacheKey(url));
|
||||
if (entry && Date.now() < entry.expiry) return entry.data as T;
|
||||
return null;
|
||||
}
|
||||
|
||||
getInflight<T>(url: string): Promise<T> | null {
|
||||
return (this.inflight.get(this.cacheKey(url)) as Promise<T> | undefined) ?? null;
|
||||
}
|
||||
|
||||
setInflight(url: string, promise: Promise<unknown>): void {
|
||||
this.inflight.set(this.cacheKey(url), promise);
|
||||
}
|
||||
|
||||
removeInflight(url: string): void {
|
||||
this.inflight.delete(this.cacheKey(url));
|
||||
}
|
||||
|
||||
set(url: string, data: unknown, ttl?: number): void {
|
||||
const key = this.cacheKey(url);
|
||||
const effectiveTtl = ttl ?? this.defaultTtl;
|
||||
if (effectiveTtl <= 0) return;
|
||||
if (this.cache.size >= this.maxSize) {
|
||||
const oldest = this.cache.keys().next().value;
|
||||
if (oldest) this.cache.delete(oldest);
|
||||
}
|
||||
this.cache.set(key, { data, expiry: Date.now() + effectiveTtl });
|
||||
}
|
||||
|
||||
clear(prefix?: string): void {
|
||||
if (prefix) {
|
||||
for (const key of this.cache.keys()) {
|
||||
if (key.includes(prefix)) this.cache.delete(key);
|
||||
}
|
||||
} else {
|
||||
this.cache.clear();
|
||||
}
|
||||
this.inflight.clear();
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.cache.clear();
|
||||
this.inflight.clear();
|
||||
this.patientId = '';
|
||||
}
|
||||
}
|
||||
32
apps/miniprogram/src/services/request/limiter.ts
Normal file
32
apps/miniprogram/src/services/request/limiter.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* ConcurrencyLimiter — 并发请求限制器
|
||||
*
|
||||
* 微信小程序 wx.request 并发上限为 10,默认配置 8 个槽位。
|
||||
* acquire() 获取一个槽位(排队等待),release() 释放槽位。
|
||||
*/
|
||||
|
||||
export class ConcurrencyLimiter {
|
||||
private active = 0;
|
||||
private queue: Array<() => void> = [];
|
||||
|
||||
constructor(private max: number) {}
|
||||
|
||||
acquire(): Promise<void> {
|
||||
if (this.active < this.max) {
|
||||
this.active++;
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise<void>((resolve) => { this.queue.push(resolve); });
|
||||
}
|
||||
|
||||
release(): void {
|
||||
this.active--;
|
||||
const next = this.queue.shift();
|
||||
if (next) { this.active++; next(); }
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.active = 0;
|
||||
this.queue.length = 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user