docs(wiki): 更新小程序并发安全相关内容 — 并发限制器/长轮询/导航保护
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 微信小程序(患者端)
|
||||
updated: 2026-05-16
|
||||
updated: 2026-05-17
|
||||
status: active
|
||||
tags: [miniprogram, taro, wechat, patient]
|
||||
---
|
||||
@@ -148,7 +148,7 @@ Taro 4.2 / React 18 / TypeScript / Zustand 5 / Sass / Zod / ECharts 6(按需
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `apps/miniprogram/config/index.ts` | Taro 构建配置(defineConstants 注入环境变量) |
|
||||
| `apps/miniprogram/src/services/request.ts` | HTTP 请求封装(401 自动刷新、并发限制、缓存、AbortSignal 取消、`resetForTesting()` 测试隔离) |
|
||||
| `apps/miniprogram/src/services/request.ts` | HTTP 请求封装(401 自动刷新、`ConcurrencyLimiter(12)` 并发限制、`requestUnlimited` 长轮询独立通道、`safeReLaunch` 去重、缓存、AbortSignal 取消、`resetForTesting()` 测试隔离) |
|
||||
| `apps/miniprogram/src/services/auth.ts` | 微信登录/绑定手机号 API |
|
||||
| `apps/miniprogram/src/stores/auth.ts` | 认证状态(login/bindPhone/restore) |
|
||||
| `apps/miniprogram/src/stores/index.ts` | `resetAllStores()` 统一清理(解耦 store 间依赖) |
|
||||
@@ -285,7 +285,7 @@ POST /auth/wechat/login { code }
|
||||
| `usePagination` | 通用分页逻辑 |
|
||||
| `useAuthRequired` | 登录态检查 |
|
||||
| `useElderClass` | 长者模式 CSS class |
|
||||
| `useLongPolling` | **通用长轮询**:generation counter 防重叠 + useDidShow/Hide 可见性控制 + 失败退避(delay = min(failCount×2s, 30s))+ enabled 条件守卫。咨询详情页(患者+医生端)已接入 |
|
||||
| `useLongPolling` | **通用长轮询**:generation counter 防重叠 + useDidShow/Hide 可见性控制 + 失败退避(delay = min(failCount×2s, 30s))+ enabled 条件守卫 + maxFailures=10 快速止损。咨询详情页(患者+医生端)已接入,**走 `requestUnlimited` 独立通道不占用并发槽位** |
|
||||
|
||||
### 服务层(10+ 个文件)
|
||||
|
||||
@@ -488,7 +488,7 @@ secret = "<通过环境变量 ERP__WECHAT__SECRET 设置>"
|
||||
|
||||
| # | 问题 | 文件 | 修复 |
|
||||
|---|------|------|------|
|
||||
| 1 | **长轮询 delay=0 紧密递归**:成功轮询后无间隔立即递归,后端快速响应时构成紧密循环,CPU 飙升 | `consultation/detail`(患者+医生端) | 成功路径加 3s 间隔 + 连续失败上限 50 次 |
|
||||
| 1 | **长轮询 delay=0 紧密递归**:成功轮询后无间隔立即递归,后端快速响应时构成紧密循环,CPU 飙升 | `consultation/detail`(患者+医生端) | 成功路径加 3s 间隔 + 连续失败上限 10 次 + 走 `requestUnlimited` 独立通道 |
|
||||
| 2 | **BLE 模块级单例**:`BLEManager` 在模块顶层实例化,生命周期不与页面绑定;`liveReadings` 无上限增长 | `device-sync/index.tsx` | 改为 `useRef` 懒初始化 + `MAX_LIVE_READINGS=200` 上限 |
|
||||
| 3 | **TrendChart 模块级同步 API**:`Taro.getSystemInfoSync()` 在模块加载时执行,阻塞首帧 | `components/TrendChart` | 改为延迟求值 `getDPR()` 函数 |
|
||||
|
||||
@@ -568,7 +568,7 @@ secret = "<通过环境变量 ERP__WECHAT__SECRET 设置>"
|
||||
|---|------|------|------|
|
||||
| 1 | **HIGH** | `services/request.ts` | `doRefresh` 失败后设 `isLoggingOut=true` + 清理所有 Storage(含 `current_patient`)+ 清请求缓存 + 重置 headers 缓存 |
|
||||
| 2 | **HIGH** | `services/request.ts` | 401 失败后跳 `/pages/login/index`(而非首页);重试成功后重置 `isLoggingOut` |
|
||||
| 3 | **HIGH** | `services/request.ts` | **新增全局并发限制 `MAX_CONCURRENT=8`**:acquireSlot/releaseSlot 队列机制,防止超过微信 10 个请求限制;用 try/finally 确保所有路径释放槽位 |
|
||||
| 3 | **HIGH** | `services/request.ts` | **全局并发限制 `ConcurrencyLimiter(12)`**:acquire/release 队列机制 + 长轮询走 `requestUnlimited` 独立通道不占用槽位 + `safeReLaunch` 去重防止并发 401 多次跳转 |
|
||||
| 4 | MEDIUM | `pages/consultation/detail` + `doctor/consultation/detail` | 长轮询改为 **generation counter**,`startLongPolling` 递增 generation,旧循环检测到 generation 变化自动退出,杜绝新旧循环重叠 |
|
||||
| 5 | MEDIUM | `pages/index/index.tsx` | 首页 `loadReminders` 添加 `remindersLoadingRef` 防重入 |
|
||||
| 6 | MEDIUM | `pages/health/index.tsx` + `stores/health.ts` | 健康页整体 `loadingRef` 防重入;health store `refreshToday` 添加全局 `refreshingToday` 去重 |
|
||||
@@ -576,10 +576,21 @@ secret = "<通过环境变量 ERP__WECHAT__SECRET 设置>"
|
||||
#### 并发限制器原理
|
||||
|
||||
```typescript
|
||||
// request.ts — acquireSlot/releaseSlot 队列
|
||||
const MAX_CONCURRENT = 8; // 留 2 个空位给系统请求
|
||||
let activeRequests = 0;
|
||||
const pendingQueue: Array<() => void> = [];
|
||||
// request.ts — ConcurrencyLimiter 类 + requestUnlimited 独立通道
|
||||
const limiter = new ConcurrencyLimiter(12); // 普通请求走并发限制
|
||||
|
||||
// 长轮询绕过并发限制,避免 30s 超时占用槽位导致全局饥饿
|
||||
export async function requestUnlimited<T>(method, path, data?, timeout?): Promise<T> {
|
||||
return request<T>(method, path, data, timeout, undefined, true); // bypassLimiter=true
|
||||
}
|
||||
|
||||
// reLaunch 去重:并发 401 时只触发一次跳转
|
||||
function safeReLaunch(url: string): void {
|
||||
if (reLaunchPromise) return; // 已有跳转在进行
|
||||
reLaunchPromise = Taro.reLaunch({ url }).then(() => {}, () => {}).then(() => {
|
||||
setTimeout(() => { reLaunchPromise = null; }, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function acquireSlot(): Promise<void> {
|
||||
if (activeRequests < MAX_CONCURRENT) {
|
||||
|
||||
Reference in New Issue
Block a user