docs(wiki): 更新小程序并发安全相关内容 — 并发限制器/长轮询/导航保护
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
| erp-ai 实体 | 9 个 Entity(62 文件,4 AI Provider) |
|
||||
| 全系统 Entity | **109 个** / Handler **47 个** / Service **107 个** / DTO **29 个** |
|
||||
| Web 前端 | 307 个 TS/TSX 文件(36 活跃路由 + 5 冻结路由,42 API 模块,161 页面) |
|
||||
| 微信小程序 | Taro 4.2 + React 18,161 个 TS/TSX 文件 / 60 页面 / 4 TabBar + 医生端分包,统一组件库 + CSS 变量主题(75 页面 SCSS 全量接入 `var(--tk-*)`,字号 token 对齐原型统计,医生端 `.doctor-mode` 靛蓝覆盖,登录页账号密码+微信一键登录) |
|
||||
| 微信小程序 | Taro 4.2 + React 18,161 个 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% 通过率 |
|
||||
|
||||
@@ -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