Browse Source

feat(qr): 实现扫码绑定用户关系功能

- 新增用户绑定相关接口类型定义及 API 调用函数
- 支持 Snowflake ID 字符串传递避免精度丢失
- 在二维码扫描处理中增加多种角色间绑定逻辑判断
- 实现医生-患者、家属-患者等多类绑定流程交互提示
- 优化用户信息获取方式并增强错误处理机制
- 更新页面组件中对用户 ID 类型的适配与使用逻辑
mcbaiyun 1 month ago
parent
commit
f9fc5f16f3

+ 1 - 1
src/api/user.ts

@@ -1,5 +1,5 @@
 // 获取用户详细信息
 // 获取用户详细信息
-export async function getUserInfo(userId: number) {
+export async function getUserInfo(userId: string | number) {
   const token = uni.getStorageSync('token')
   const token = uni.getStorageSync('token')
   const res: any = await uni.request({
   const res: any = await uni.request({
     url: `https://wx.baiyun.work/user/${userId}`,
     url: `https://wx.baiyun.work/user/${userId}`,

+ 35 - 3
src/api/userBinding.ts

@@ -7,8 +7,9 @@ export interface BaseQueryRequest {
 
 
 export interface UserBindingResponse {
 export interface UserBindingResponse {
   id: string
   id: string
-  patientUserId: number
-  boundUserId: number
+  // Snowflake 64位ID 使用字符串保存以避免 JS Number 精度丢失
+  patientUserId: string | number
+  boundUserId: string | number
   bindingType: string
   bindingType: string
   status: number
   status: number
   createTime: string
   createTime: string
@@ -31,7 +32,7 @@ export interface UserBindingPageResponse {
 }
 }
 
 
 export async function listUserBindingsByPatient(
 export async function listUserBindingsByPatient(
-  patientUserId: number,
+  patientUserId: string | number,
   bindingType: string,
   bindingType: string,
   query: BaseQueryRequest
   query: BaseQueryRequest
 ) {
 ) {
@@ -50,4 +51,35 @@ export async function listUserBindingsByPatient(
     }
     }
   })
   })
   return res
   return res
+}
+
+// 创建用户绑定关系 POST /user-binding/create
+export async function createUserBinding(payload: any) {
+  const token = uni.getStorageSync('token')
+  const res: any = await uni.request({
+    url: 'https://wx.baiyun.work/user-binding/create',
+    method: 'POST',
+    header: {
+      'Content-Type': 'application/json',
+      Authorization: `Bearer ${token}`
+    },
+    data: payload
+  })
+  return res
+}
+
+// 检查用户绑定关系 POST /user-binding/check
+// 检查绑定关系:支持将 Snowflake ID 以字符串传入,避免精度问题
+export async function checkUserBinding(payload: { patientUserId: string | number; boundUserId: string | number }) {
+  const token = uni.getStorageSync('token')
+  const res: any = await uni.request({
+    url: 'https://wx.baiyun.work/user-binding/check',
+    method: 'POST',
+    header: {
+      'Content-Type': 'application/json',
+      Authorization: `Bearer ${token}`
+    },
+    data: payload
+  })
+  return res
 }
 }

+ 2 - 1
src/pages/patient/profile/infos/my-doctor.vue

@@ -141,7 +141,8 @@ const fetchDoctorInfo = async () => {
         
         
         // 通过用户接口获取医生详细信息
         // 通过用户接口获取医生详细信息
         try {
         try {
-          const userInfoResponse = await getUserInfo(boundDoctor.boundUserId)
+          // 将 ID 作为字符串传入以保证 Snowflake ID 的精度(前端全程文本存储)
+          const userInfoResponse = await getUserInfo(String(boundDoctor.boundUserId))
           const userInfo = userInfoResponse.data?.data
           const userInfo = userInfoResponse.data?.data
           
           
           if (userInfo) {
           if (userInfo) {

+ 6 - 6
src/pages/public/profile/qr/index.vue

@@ -35,7 +35,7 @@ import CustomNav from '@/components/custom-nav.vue'
 import drawQrcode from 'weapp-qrcode'
 import drawQrcode from 'weapp-qrcode'
 import { fetchUserInfo as fetchUserInfoApi } from '@/api/user'
 import { fetchUserInfo as fetchUserInfoApi } from '@/api/user'
 
 
-const user = ref<{ nickname?: string; role?: string | number; openid?: string; wx_openid?: string }>({})
+const user = ref<{ nickname?: string; role?: string | number;  id?: string }>({})
 const qrData = ref('')
 const qrData = ref('')
 const isFetching = ref(false)
 const isFetching = ref(false)
 
 
@@ -120,10 +120,10 @@ const fetchUserInfo = async () => {
 const refreshAndGenerate = async () => {
 const refreshAndGenerate = async () => {
   await fetchUserInfo()
   await fetchUserInfo()
   // fetchUserInfo 可能会在 token 无效时跳转登录,下面仅在本地有用户信息时重绘二维码
   // fetchUserInfo 可能会在 token 无效时跳转登录,下面仅在本地有用户信息时重绘二维码
-  if (user.value && (user.value.wx_openid || user.value.openid)) {
+  if (user.value && (user.value.id)) {
     generateQRCode()
     generateQRCode()
   } else {
   } else {
-    // 如果后端未返回 openid 等,仍尝试使用本地缓存绘制
+    // 如果后端未返回 id 等,仍尝试使用本地缓存绘制
     loadUser()
     loadUser()
     generateQRCode()
     generateQRCode()
   }
   }
@@ -131,13 +131,13 @@ const refreshAndGenerate = async () => {
 
 
 const generateQRCode = async () => {
 const generateQRCode = async () => {
   console.log('Generating QR code, user data:', user.value)
   console.log('Generating QR code, user data:', user.value)
-  if (!user.value.wx_openid || !user.value.role) {
-    console.log('Missing wx_openid or role:', { wx_openid: user.value.wx_openid, role: user.value.role })
+  if (!user.value.id || !user.value.role) {
+    console.log('Missing id or role:', { id: user.value.id, role: user.value.role })
     uni.showToast({ title: '用户信息不完整', icon: 'none' })
     uni.showToast({ title: '用户信息不完整', icon: 'none' })
     return
     return
   }
   }
   const data = JSON.stringify({
   const data = JSON.stringify({
-    openid: user.value.wx_openid,
+    id: user.value.id,
     role: user.value.role
     role: user.value.role
   })
   })
   console.log('QR data:', data)
   console.log('QR data:', data)

+ 303 - 0
src/utils/qr.ts

@@ -1,8 +1,311 @@
+import { fetchUserInfo as fetchUserInfoApi } from '@/api/user'
+import { createUserBinding, checkUserBinding } from '@/api/userBinding'
+
 export function handleQrScanResult(res: any) {
 export function handleQrScanResult(res: any) {
   try {
   try {
     console.log('[qr] scan result', res)
     console.log('[qr] scan result', res)
     const resultText = res?.result || ''
     const resultText = res?.result || ''
     if (resultText) {
     if (resultText) {
+      // 尝试解析二维码内容(index.vue 生成的是 JSON 字符串)并打印解析结果到日志
+      try {
+        const parsed = JSON.parse(resultText)
+        console.log('[qr] parsed result', parsed)
+        // 兼容可能的键名(id 或 id)并打印规范化结果以便调试
+        const normalized = {
+          id: parsed.id || '',
+          role: parsed.role ?? parsed.role
+        }
+        console.log('[qr] normalized parsed', normalized)
+
+        /*
+         * 扫码组合占位注释(下面为所有 scanner -> scanned 组合的占位处理,请在需要时实现具体逻辑)
+         * 角色映射: 2 = 医生, 3 = 患者, 4 = 家属
+         * 示例使用说明:
+         *  - 获取当前用户信息:const me = uni.getStorageSync('user_info') || {}
+         *  - 当前用户角色:const meRole = Number(me.role)
+         *  - 被扫用户角色(二维码里):const scannedRole = Number(normalized.role)
+         * 下面列出所有合理组合的注释占位:
+         */
+
+        // const me = uni.getStorageSync('user_info') || {}
+        // const meRole = Number(me.role || 0)
+        // const scannedRole = Number(normalized.role || 0)
+
+        // if (meRole === 2 && scannedRole === 2) {
+        //   // 医生 -> 医生:通常无绑定意义。可能是邀请同事或添加为联系人。
+        //   // 占位:提示不可绑定或打开同事邀请流程
+        // }
+        // else if (meRole === 2 && scannedRole === 3) {
+        //   // 医生 -> 患者:医生扫码患者,常用于绑定/关联患者或查看患者档案
+        //   // 占位:调用后端接口发起绑定请求或查询授权
+        // }
+        // else if (meRole === 2 && scannedRole === 4) {
+        //   // 医生 -> 家属:可用于查看家属管理的患者或建立医患家属关联(需后端校验)
+        // }
+        // else if (meRole === 3 && scannedRole === 2) {
+        //   // 患者 -> 医生:患者扫码医生用于绑定/关注医生或快速预约
+        //   // 占位:提交绑定请求到后端,可能需要医生确认
+        // }
+        // else if (meRole === 3 && scannedRole === 3) {
+        //   // 患者 -> 患者:通常无意义,可能用于数据共享邀请(需双方同意)
+        // }
+        // else if (meRole === 3 && scannedRole === 4) {
+        //   // 患者 -> 家属:患者扫码家属作为邀请家属或确认关系
+        //   // 占位:将邀请提交后端,由家属或患者确认
+        // }
+        // else if (meRole === 4 && scannedRole === 2) {
+        //   // 家属 -> 医生:家属扫码医生用于为患者预约或询问(需验证家属代表权限)
+        // }
+        // else if (meRole === 4 && scannedRole === 3) {
+        //   // 家属 -> 患者:家属扫码患者为典型绑定场景(家属关联患者)
+        //   // 占位:调用后端完成家属-患者绑定/授权流程
+        // }
+        // else if (meRole === 4 && scannedRole === 4) {
+        //   // 家属 -> 家属:用于邀请加入家庭组或确认家庭关系
+        // }
+        // else {
+        //   // 未登录或未知角色:提示登录或显示无效场景
+        // }
+
+        // 异步刷新当前用户信息并打印当前用户的 id 和 role(不阻塞主流程)
+        try {
+          fetchUserInfoApi()
+            .then((response: any) => {
+              try {
+                const resp = response.data as any
+                if (response.statusCode === 401) {
+                  console.log('[qr] fetchUserInfo: token invalid')
+                  return
+                }
+                if (resp && resp.code === 200 && resp.data) {
+                  const me = resp.data as any
+                  const myid = me.id || me.id || ''
+                  const myRole = Number(me.role ?? 0)
+                  console.log('[qr] my user info from API', { id: myid, role: myRole })
+
+                  // 根据当前用户角色与被扫角色决定绑定行为(以下为实现逻辑)
+                  const scannedRole = Number(normalized.role || 0)
+                  // 尝试从二维码解析出被扫用户 id(优先)或 id
+                  const scannedId = parsed.id || parsed.userId || parsed.user_id || null
+                  const scannedid = normalized.id || ''
+
+                  // 医生 -> 患者 (2 -> 3)
+                  if (myRole === 2 && scannedRole === 3) {
+                    // patientUserId = 被扫患者 ID(二维码中),boundUserId = 当前医生 ID
+                    const patientUserId = scannedId || scannedid
+                    const boundUserId = me.id || myid
+
+                    // 将 ID 全程作为文本传输,避免 JS 大整数精度问题(Snowflake 使用场景)
+                    checkUserBinding({ patientUserId: String(patientUserId), boundUserId: String(boundUserId) }).then((checkRes: any) => {
+                      const resp = checkRes?.data as any
+                      // 如果后端返回了 status 字段,仅在 status === 1 时算为存在绑定
+                      const existsActive = resp && resp.code === 200 && resp.data && resp.data.exists && (typeof resp.data.status === 'undefined' ? true : Number(resp.data.status) === 1)
+                      if (checkRes && checkRes.statusCode === 200 && existsActive) {
+                        // 若存在且未失效,提示不要重复绑定(单按钮确认)
+                        const bindingType = String(resp?.data?.bindingType || resp?.data?.bindingtype || '未知')
+                        uni.showModal({
+                          title: '提示',
+                          content: `您已经成功绑定过该患者了,无需重复绑定!`,
+                          showCancel: false
+                        })
+                      } else {
+                        uni.showModal({
+                          title: '提示',
+                          content: '确认要绑定该患者吗?',
+                          success: (modalRes: any) => {
+                            if (modalRes.confirm) {
+                              const payload: any = { bindingType: 'DOCTOR' }
+                              if (scannedId) payload.patientUserId = String(scannedId)
+                              else payload.patientid = String(scannedid)
+                              if (me.id) payload.boundUserId = String(me.id)
+                              else payload.boundUserid = String(myid)
+
+                              createUserBinding(payload).then((createRes: any) => {
+                                if (createRes && createRes.statusCode === 200) {
+                                  uni.showModal({ title: '绑定成功', content: JSON.stringify(createRes.data), showCancel: false })
+                                } else {
+                                  uni.showModal({ title: '绑定失败', content: '请求失败', showCancel: false })
+                                }
+                              }).catch((err: any) => {
+                                console.error('[qr] create binding error', err)
+                                uni.showModal({ title: '错误', content: err.message || '绑定请求出错', showCancel: false })
+                              })
+                            }
+                          }
+                        })
+                      }
+                    }).catch((err: any) => {
+                      console.error('[qr] check binding error', err)
+                      uni.showModal({ title: '错误', content: '检查绑定关系失败', showCancel: false })
+                    })
+                  }
+
+                  // 患者 -> 医生 (3 -> 2)
+                  else if (myRole === 3 && scannedRole === 2) {
+                    // patientUserId = 当前患者 ID(扫码者),boundUserId = 被扫医生 ID
+                    const patientUserId = me.id || myid
+                    const boundUserId = scannedId || scannedid
+
+                    checkUserBinding({ patientUserId: String(patientUserId), boundUserId: String(boundUserId) }).then((checkRes: any) => {
+                      const resp = checkRes?.data as any
+                      const existsActive = resp && resp.code === 200 && resp.data && resp.data.exists && (typeof resp.data.status === 'undefined' ? true : Number(resp.data.status) === 1)
+                      if (checkRes && checkRes.statusCode === 200 && existsActive) {
+                        const bindingType = String(resp?.data?.bindingType || resp?.data?.bindingtype || '未知')
+                        uni.showModal({
+                          title: '提示',
+                          content: `您已经成功绑定过该医生了,无需重复绑定!`,
+                          showCancel: false
+                        })
+                      } else {
+                        uni.showModal({
+                          title: '提示',
+                          content: '确认要绑定该医生吗?',
+                          success: (modalRes: any) => {
+                            if (modalRes.confirm) {
+                              const payload: any = { bindingType: 'DOCTOR' }
+                              if (me.id) payload.patientUserId = String(me.id)
+                              else payload.patientid = String(myid)
+                              if (scannedId) payload.boundUserId = String(scannedId)
+                              else payload.boundUserid = scannedid
+
+                              createUserBinding(payload).then((createRes: any) => {
+                                if (createRes && createRes.statusCode === 200) {
+                                  uni.showModal({ title: '关注成功', content: JSON.stringify(createRes.data), showCancel: false })
+                                } else {
+                                  uni.showModal({ title: '关注失败', content: '请求失败', showCancel: false })
+                                }
+                              }).catch((err: any) => {
+                                console.error('[qr] create binding error', err)
+                                uni.showModal({ title: '错误', content: err.message || '请求出错', showCancel: false })
+                              })
+                            }
+                          }
+                        })
+                      }
+                    }).catch((err: any) => {
+                      console.error('[qr] check binding error', err)
+                      uni.showModal({ title: '错误', content: '检查绑定关系失败', showCancel: false })
+                    })
+                  }
+
+                  // 家属 -> 患者 (4 -> 3)
+                  else if (myRole === 4 && scannedRole === 3) {
+                    const patientUserId = scannedId || scannedid
+                    const boundUserId = me.id || myid
+
+                    checkUserBinding({ patientUserId: String(patientUserId), boundUserId: String(boundUserId) }).then((checkRes: any) => {
+                      const resp = checkRes?.data as any
+                      const existsActive = resp && resp.code === 200 && resp.data && resp.data.exists && (typeof resp.data.status === 'undefined' ? true : Number(resp.data.status) === 1)
+                      if (checkRes && checkRes.statusCode === 200 && existsActive) {
+                        const bindingType = String(resp?.data?.bindingType || resp?.data?.bindingtype || '未知')
+                        uni.showModal({
+                          title: '提示',
+                          content: `您已经成功绑定过该患者了,无需重复绑定!`,
+                          showCancel: false
+                        })
+                      } else {
+                        uni.showModal({
+                          title: '提示',
+                          content: '确认要与该患者建立家属关系吗?',
+                          success: (modalRes: any) => {
+                            if (modalRes.confirm) {
+                              const payload: any = { bindingType: 'FAMILY' }
+                              if (scannedId) payload.patientUserId = String(scannedId)
+                              else payload.patientid = String(scannedid)
+                              if (me.id) payload.boundUserId = String(me.id)
+                              else payload.boundUserid = String(myid)
+
+                              createUserBinding(payload).then((createRes: any) => {
+                                if (createRes && createRes.statusCode === 200) {
+                                  uni.showModal({ title: '绑定成功', content: JSON.stringify(createRes.data), showCancel: false })
+                                } else {
+                                  uni.showModal({ title: '绑定失败', content: '请求失败', showCancel: false })
+                                }
+                              }).catch((err: any) => {
+                                console.error('[qr] create binding error', err)
+                                uni.showModal({ title: '错误', content: err.message || '绑定请求出错', showCancel: false })
+                              })
+                            }
+                          }
+                        })
+                      }
+                    }).catch((err: any) => {
+                      console.error('[qr] check binding error', err)
+                      uni.showModal({ title: '错误', content: '检查绑定关系失败', showCancel: false })
+                    })
+                  }
+
+                  // 患者 -> 家属 (3 -> 4)
+                  else if (myRole === 3 && scannedRole === 4) {
+                    const patientUserId = me.id || myid
+                    const boundUserId = scannedId || scannedid
+
+                    checkUserBinding({ patientUserId: String(patientUserId), boundUserId: String(boundUserId) }).then((checkRes: any) => {
+                      const resp = checkRes?.data as any
+                      const existsActive = resp && resp.code === 200 && resp.data && resp.data.exists && (typeof resp.data.status === 'undefined' ? true : Number(resp.data.status) === 1)
+                      if (checkRes && checkRes.statusCode === 200 && existsActive) {
+                        const bindingType = String(resp?.data?.bindingType || resp?.data?.bindingtype || '未知')
+                        uni.showModal({
+                          title: '提示',
+                          content: `您已经成功绑定过该用户了,无需重复绑定!`,
+                          showCancel: false
+                        })
+                      } else {
+                        uni.showModal({
+                          title: '提示',
+                          content: '您确认要与该用户建立家属关系吗?',
+                          success: (modalRes: any) => {
+                            if (modalRes.confirm) {
+                              const payload: any = { bindingType: 'FAMILY' }
+                              if (me.id) payload.patientUserId = String(me.id)
+                              else payload.patientid = String(myid)
+                              if (scannedId) payload.boundUserId = String(scannedId)
+                              else payload.boundUserid = scannedid
+
+                              createUserBinding(payload).then((createRes: any) => {
+                                if (createRes && createRes.statusCode === 200) {
+                                  uni.showModal({ title: '绑定成功', content: JSON.stringify(createRes.data), showCancel: false })
+                                } else {
+                                  uni.showModal({ title: '绑定失败', content: '请求失败', showCancel: false })
+                                }
+                              }).catch((err: any) => {
+                                console.error('[qr] create binding error', err)
+                                uni.showModal({ title: '错误', content: err.message || '请求出错', showCancel: false })
+                              })
+                            }
+                          }
+                        })
+                      }
+                    }).catch((err: any) => {
+                      console.error('[qr] check binding error', err)
+                      uni.showModal({ title: '错误', content: '检查绑定关系失败', showCancel: false })
+                    })
+                  }
+
+                } else {
+                  const local = uni.getStorageSync('user_info') || {}
+                  const myid = local.id || local.id || ''
+                  const myRole = Number(local.role || 0)
+                  console.log('[qr] my user info from storage fallback', { id: myid, role: myRole })
+                }
+              } catch (err) {
+                console.error('[qr] fetchUserInfo parse error', err)
+              }
+            })
+            .catch((err: any) => {
+              console.error('[qr] fetchUserInfo error', err)
+              const local = uni.getStorageSync('user_info') || {}
+              const myid = local.id || local.id || ''
+              const myRole = local.role || ''
+              console.log('[qr] my user info from storage fallback', { id: myid, role: myRole })
+            })
+        } catch (err) {
+          console.error('[qr] fetchUserInfo call error', err)
+        }
+      } catch (parseErr) {
+        console.log('[qr] result is not JSON, raw:', resultText)
+      }
+
       uni.showToast({ title: String(resultText), icon: 'none', duration: 2000 })
       uni.showToast({ title: String(resultText), icon: 'none', duration: 2000 })
       return resultText
       return resultText
     } else {
     } else {