|
@@ -0,0 +1,158 @@
|
|
|
|
|
+# 患者头像 ID 精度丢失修复总结
|
|
|
|
|
+
|
|
|
|
|
+> 说明:本文件汇总来自 `医生首页` 患者动态(`patientActivities`)中,用户 ID 精度丢失导致头像请求错误的问题,以及解决过程、关键修改点、测试要点与后续建议。文档以中文撰写。
|
|
|
|
|
+
|
|
|
|
|
+## 一、问题简介 ✅
|
|
|
|
|
+
|
|
|
|
|
+在页面 `src/pages/doctor/index/index.vue` 中,患者动态数据原始接口返回如下(示例):
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+{"id":"1991836353626906625","userId":1988147181088956418,...}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+但浏览器/小程序客户端请求头像的 URL 为:
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+https://wx.baiyun.work/user/avatar/1988147181088956400
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+可见 `userId` 发生了数值精度丢失,最后几位被截断或舍入,导致错误的头像 URL 发起请求,无法加载正确头像。
|
|
|
|
|
+
|
|
|
|
|
+## 二、根本原因(分析)🔎
|
|
|
|
|
+
|
|
|
|
|
+- JavaScript 的 Number 类型为 IEEE 64-bit 双精度浮点(double),整数精度仅保证到 Number.MAX_SAFE_INTEGER(2^53 - 1,约 9e15)。
|
|
|
|
|
+- 接口返回的 ID 属于大整数(大于 2^53),若前端把这些 ID 当作 Number 存储或解析(例如 JSON.parse 默认把数字解析为 Number),则会发生精度丢失。
|
|
|
|
|
+- 精度丢失会在后续使用该 ID 构造 URL、缓存 key 或其它字符串拼接中体现出来(例如拼接头像 URL),导致请求错误。
|
|
|
|
|
+
|
|
|
|
|
+## 三、修复要点(已实施的核心变更)🔧
|
|
|
|
|
+
|
|
|
|
|
+1. **在 API 层/前端解析层把 ID 强制转换为字符串(string)**
|
|
|
|
|
+ - 关键思路:无论后端返回时 ID 是字符串还是数字,前端都要统一按字符串处理。这样不会被 Number 精度限制所影响。
|
|
|
|
|
+ - 示例:在 `api` 的响应处理或 `userActivity` 的接口方法中,转换记录字段:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+// src/api/userActivity.ts (示例)
|
|
|
|
|
+const resp = response.data as any
|
|
|
|
|
+if (resp && resp.code === 200 && resp.data) {
|
|
|
|
|
+ const safeRecords = resp.data.records.map((r: any) => ({
|
|
|
|
|
+ ...r,
|
|
|
|
|
+ id: r.id === null ? null : String(r.id),
|
|
|
|
|
+ userId: r.userId === null ? null : String(r.userId),
|
|
|
|
|
+ relatedEntityId: r.relatedEntityId === null ? null : String(r.relatedEntityId)
|
|
|
|
|
+ }))
|
|
|
|
|
+ // 继续处理 safeRecords...
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+2. **修改 TypeScript 类型**
|
|
|
|
|
+ - 把 API 接口里所有表示主键/外键(如 `id`, `userId`, `relatedEntityId`)**类型从 `number` 改为 `string`**,或使用 `string | null`。
|
|
|
|
|
+
|
|
|
|
|
+3. **前端组件中使用字符串 ID**
|
|
|
|
|
+ - 在 `getPatientAvatar` 和任何缓存键、URL 拼接处确保使用字符串:`const idStr = String(userId)`(注意这是在 ID 还未丢失的情况下才有效)。
|
|
|
|
|
+ - 更好方案:在接口处理入口就把所有记录的 `userId` 等字段转为 `string`,前端组件直接使用。
|
|
|
|
|
+
|
|
|
|
|
+4. **缓存 key 的统一**
|
|
|
|
|
+ - 把 `avatarCache` 的 key 统一为字符串,避免使用 Number 当 key:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+// avatarCache.set(String(userId), tempFilePath)
|
|
|
|
|
+// avatarCache.get(String(userId))
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+5. **避免在 JSON.parse 后直接使用数字**
|
|
|
|
|
+ - 如果从后端一端得到大整数并要使用 `JSON.parse`,可以用 `json-bigint` 之类的库或 reviver,将 ID 字段解析为字符串。示例:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+// 使用 JSONBig(json-bigint 库)
|
|
|
|
|
+import JSONbig from 'json-bigint'
|
|
|
|
|
+const resp = JSONbig.parse(rawResponse)
|
|
|
|
|
+// JSONbig 能保留大整数字符串或 BigInt,视配置而定。
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+或者使用 reviver:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+const parsed = JSON.parse(raw, (k, v) => {
|
|
|
|
|
+ if (['id', 'userId', 'relatedEntityId'].includes(k)) return v === null ? null : String(v)
|
|
|
|
|
+ return v
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 四、代码片段(推荐)📄
|
|
|
|
|
+
|
|
|
|
|
+1) API 层响应转换(Node/TS 或前端 API 出片)
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+function normalizeIdsInRecord<T extends Record<string, any>>(record: T): T {
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...record,
|
|
|
|
|
+ id: record.id === null ? null : String(record.id),
|
|
|
|
|
+ userId: record.userId === null ? null : String(record.userId),
|
|
|
|
|
+ relatedEntityId: record.relatedEntityId === null ? null : String(record.relatedEntityId)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 使用:
|
|
|
|
|
+const safeRecords = resp.data.records.map(normalizeIdsInRecord)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+2) 组件层防护(已在 `index.vue` 中使用,一并保证):
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+// 在 getPatientAvatar 中
|
|
|
|
|
+const idStr = String(userId) // 确保传入的是字符串
|
|
|
|
|
+// 拼接 URL
|
|
|
|
|
+const url = `https://wx.baiyun.work/user/avatar/${idStr}`
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+3) Typescript 类型建议(定义):
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+export interface ActivityRecord {
|
|
|
|
|
+ id: string | null
|
|
|
|
|
+ userId: string | null
|
|
|
|
|
+ relatedEntityId?: string | null
|
|
|
|
|
+ activityType: string
|
|
|
|
|
+ activityDescription?: string
|
|
|
|
|
+ createTime: string
|
|
|
|
|
+ // ... other fields
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 五、验证(测试步骤)✅
|
|
|
|
|
+
|
|
|
|
|
+1. 使用原始接口返回样例(例如请求中包含大整数 ID)进行手动或自动测试:确保前端发出的头像请求 URL 中的 ID 与服务端返回的 ID 完全一致(逐字符匹配)。
|
|
|
|
|
+2. 检查 Network 面板或小程序调试器中 `uni.downloadFile` 请求 URL,确认使用字符串化的 ID。
|
|
|
|
|
+3. 检查头像缓存是否正确命中:
|
|
|
|
|
+ - 首次请求时,头像会下载并存入 `avatarCache`(键为字符串)。
|
|
|
|
|
+ - 再次打开页面或刷新时,`avatarCache.get` 能成功返回缓存路径。
|
|
|
|
|
+4. 编写小型自动化/单元测试来验证 `normalizeIdsInRecord` 或 API 层 transform 的行为,确保数字和字符串输入都能正确转换为字符串输出。
|
|
|
|
|
+
|
|
|
|
|
+## 六、回溯影响点(代码库中需要检查的位置)📌
|
|
|
|
|
+
|
|
|
|
|
+- `src/api` 下所有接口返回中含 `id`, `userId`, `relatedEntityId` 的地方
|
|
|
|
|
+- `src/pages/**` 中对 `userId` 进行拼接或作为缓存 key 的地方
|
|
|
|
|
+- `avatarCache` 的实现(键类型)
|
|
|
|
|
+- `uni.getStorageSync('user_info')` / `fetchUserInfo` 中是否把 `id` 等字段当作 number 使用
|
|
|
|
|
+- 任何使用 `JSON.parse` 直接处理接口原始返回值的地方
|
|
|
|
|
+
|
|
|
|
|
+## 七、后续建议(优先级排序)📋
|
|
|
|
|
+
|
|
|
|
|
+1. 后端约定:建议后端在接口里统一返回 `id`、`userId` 等主键为字符串类型(比如 JSON 的字符串 '123'),避免在前端做相应处理(最高优先级)。
|
|
|
|
|
+2. 在前端建立一个统一的 `normalizeId` 或 API 层统一解析方法,确保任何 API 响应里所有 ID 都统一为字符串(中优先级)。
|
|
|
|
|
+3. 更新 TypeScript 的类型定义(`api` 层与前端数据模型)以使用字符串 ID(中优先级)。
|
|
|
|
|
+4. 增加自动化测试 / 集成测试,确保大整数 ID 在前端流水线里不会被误解析为 Number(中优先级)。
|
|
|
|
|
+5. 添加额外的 PR 模板和检查项:在代码审查时特别关注 `id` 字段类型是否被误用为 Number(低优先级)。
|
|
|
|
|
+
|
|
|
|
|
+## 八、迁移步骤(如果后端改回字符串)🛠️
|
|
|
|
|
+
|
|
|
|
|
+- 升级后端返回 ID 为字符串(例如 `"1988147181088956418"`)后,前端可以把 `id` 定义为 `string` 类型,不需要额外的 `String()` 转换。
|
|
|
|
|
+- 若后端短时间内不能更改,则继续使用前端的 normalize 层作为兼容方案。
|
|
|
|
|
+
|
|
|
|
|
+## 九、恢复验证的检查点(能否被回归)🔁
|
|
|
|
|
+
|
|
|
|
|
+- 在修复后,打开患者动态页面,确认:
|
|
|
|
|
+ - network 请求中头像 URL 中的 ID 与接口原始 ID 一致
|
|
|
|
|
+ - 患者头像显示正常(不再请求到 1988147181088956400 的错误文件)
|
|
|
|
|
+ - 缓存正常命中(`avatarCache`)
|
|
|
|
|
+
|