docs(wiki): 更新小程序并发安全相关内容 — 并发限制器/长轮询/导航保护

This commit is contained in:
iven
2026-05-17 18:37:55 +08:00
parent 3c98aaedbd
commit 66aef532fa
2 changed files with 22 additions and 11 deletions

View File

@@ -18,7 +18,7 @@
| erp-ai 实体 | 9 个 Entity62 文件4 AI Provider |
| 全系统 Entity | **109 个** / Handler **47 个** / Service **107 个** / DTO **29 个** |
| Web 前端 | 307 个 TS/TSX 文件36 活跃路由 + 5 冻结路由42 API 模块161 页面) |
| 微信小程序 | Taro 4.2 + React 18161 个 TS/TSX 文件 / 60 页面 / 4 TabBar + 医生端分包,统一组件库 + CSS 变量主题75 页面 SCSS 全量接入 `var(--tk-*)`,字号 token 对齐原型统计,医生端 `.doctor-mode` 靛蓝覆盖,登录页账号密码+微信一键登录) |
| 微信小程序 | Taro 4.2 + React 18161 个 TS/TSX 文件 / 60 页面 / 4 TabBar + 医生端分包,统一组件库 + CSS 变量主题75 页面 SCSS 全量接入 `var(--tk-*)`,字号 token 对齐原型统计,医生端 `.doctor-mode` 靛蓝覆盖,登录页账号密码+微信一键登录)**并发安全**:长轮询独立通道 `requestUnlimited` + ConcurrencyLimiter(12) + safeNavigateTo 全局页栈保护 + reLaunch 去重 + 分包预加载 preloadRule |
| 前端测试 | Web 62 单元测试文件(~693 断言) + 17 E2E spec(13 Web + 4 MP~64 断言);小程序 0 单元测试 |
| 后端测试 | **943 个函数**762 同步 + 181 异步103 个文件含测试 |
| 事件系统 | 31 事件类型health/ 51 全系统 / 82 发布点 / 12 消费者模块 / Outbox + LISTEN/NOTIFY |
@@ -27,7 +27,7 @@
| Clippy | **全 workspace 0 警告**2026-05-07 清零) |
| 依赖版本 | 全部最新主版本线Rust edition 2024 |
| API 文档 | `http://localhost:3000/api/docs/openapi.json` |
| Git 提交 | **842+** |
| Git 提交 | **860** |
| 系统分析评分 | **6.8/10 (B)**六维度全面均衡分析2026-05-17架构 8.5 / 安全 7.5 / 测试 5.5 / 前端 7.2 / DevOps 3.8 / 产品 8.0 |
| 审计状态 | V1: 83% → V2: 85%P0 安全修复已完成V2 CRITICAL 全清零 |
| 角色测试 | R01-R05 全角色验证完成86.5% 通过率5 个 BUG 已修复;小程序 MP 多角色 96.2% 通过率 |

View File

@@ -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) {