diff --git a/wiki/index.md b/wiki/index.md index 6b03715..3bb427d 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -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% 通过率 | diff --git a/wiki/miniprogram.md b/wiki/miniprogram.md index 085568d..9ed78b1 100644 --- a/wiki/miniprogram.md +++ b/wiki/miniprogram.md @@ -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(method, path, data?, timeout?): Promise { + return request(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 { if (activeRequests < MAX_CONCURRENT) {