From 40a2d873456908e79c1aadd850a29dab29df94b5 Mon Sep 17 00:00:00 2001 From: duxingchen Date: Tue, 11 Aug 2026 15:56:25 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=B6=88=E6=81=AF=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E9=A1=B5=E4=B8=8D=E8=83=BD=E7=82=B9=E8=BF=9B=E8=AF=A6=E6=83=85?= =?UTF-8?q?=20=E2=80=94=20=E8=A1=A5=E5=85=A8=20product=5Fserial=5Fnumber?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: 点击通知卡片跳转 detail?taskId=xxx,但 detail.vue onLoad 只认 serial 参数导致空白页 修复: 1. 后端 NotificationResponse 新增 product_serial_number 字段 2. 后端 notifications API 联表 tasks+products 批量填充 serial 3. 前端 notify/index.vue handleCardTap 优先用 product_serial_number 跳转,兜底从 content 正则解析 4. 前端 detail.vue onLoad 新增 taskId 兜底 → doQueryByTask 反查 product_sn --- backend/app/api/v1/endpoints/notifications.py | 27 ++++++++++++++++--- backend/app/schemas/notification.py | 1 + track-uniapp/src/pages/notify/index.vue | 10 ++++++- track-uniapp/src/pages/scan/detail.vue | 4 ++- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/backend/app/api/v1/endpoints/notifications.py b/backend/app/api/v1/endpoints/notifications.py index 97f82f3..f405e02 100644 --- a/backend/app/api/v1/endpoints/notifications.py +++ b/backend/app/api/v1/endpoints/notifications.py @@ -4,9 +4,12 @@ import uuid from fastapi import APIRouter, Depends, Query from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload from app.core.database import get_db from app.models.notification import Notification +from app.models.task import Task +from app.models.product import Product from app.schemas.notification import NotificationResponse, NotificationListResponse router = APIRouter(prefix="/notifications", tags=["消息通知"]) @@ -46,10 +49,28 @@ async def list_notifications( result = await db.execute(stmt) notifications = result.scalars().all() + # 🚀 批量查询关联的 product_serial_number + task_ids = [n.task_id for n in notifications if n.task_id] + serial_map: dict[uuid.UUID, str] = {} + if task_ids: + task_result = await db.execute( + select(Task.id, Product.serial_number) + .join(Product, Task.product_id == Product.id) + .where(Task.id.in_(task_ids)) + ) + for row in task_result: + serial_map[row[0]] = row[1] + + # 组装响应 + response_list: list[NotificationResponse] = [] + for n in notifications: + resp = NotificationResponse.model_validate(n) + if n.task_id and n.task_id in serial_map: + resp.product_serial_number = serial_map[n.task_id] + response_list.append(resp) + return NotificationListResponse( - notifications=[ - NotificationResponse.model_validate(n) for n in notifications - ], + notifications=response_list, total=total, unread_count=unread_count, ) diff --git a/backend/app/schemas/notification.py b/backend/app/schemas/notification.py index 7fb565c..f719ed1 100644 --- a/backend/app/schemas/notification.py +++ b/backend/app/schemas/notification.py @@ -13,6 +13,7 @@ class NotificationResponse(BaseModel): content: str type: str task_id: uuid.UUID | None = None + product_serial_number: str | None = None is_read: bool created_at: datetime diff --git a/track-uniapp/src/pages/notify/index.vue b/track-uniapp/src/pages/notify/index.vue index 55f691b..a117ab9 100644 --- a/track-uniapp/src/pages/notify/index.vue +++ b/track-uniapp/src/pages/notify/index.vue @@ -103,7 +103,15 @@ async function handleCardTap(item) { if (!item.is_read) { try { await markNotificationRead(item.id); item.is_read = true; } catch {} } - if (item.task_id) { + // 🚀 优先使用 product_serial_number,兜底从 content 中解析 + let sn = item.product_serial_number || ""; + if (!sn && item.content) { + const match = item.content.match(/\[([A-Za-z0-9]{8,16})\]/); + if (match) sn = match[1]; + } + if (sn) { + uni.navigateTo({ url: `/pages/scan/detail?serial=${sn}` }); + } else if (item.task_id) { uni.navigateTo({ url: `/pages/scan/detail?taskId=${item.task_id}` }); } } diff --git a/track-uniapp/src/pages/scan/detail.vue b/track-uniapp/src/pages/scan/detail.vue index b1c429a..4dfb252 100644 --- a/track-uniapp/src/pages/scan/detail.vue +++ b/track-uniapp/src/pages/scan/detail.vue @@ -246,7 +246,7 @@ export default { }, msgUnreadCount() { if (!this.lastMsgSeenAt) return this.messages.length; return this.messages.filter(m => m.created_at > this.lastMsgSeenAt).length; }, }, - onLoad(options) { this.loadUsers(); this.loadCurrentUser(); const sn = options.serial || ""; if (sn) this.doQuery(sn); }, + onLoad(options) { this.loadUsers(); this.loadCurrentUser(); const sn = options.serial || ""; if (sn) { this.doQuery(sn); return; } /* 🚀 兜底: 从 taskId 反查 product */ const tid = options.taskId || ""; if (tid) this.doQueryByTask(tid); }, // 🚀 onShow 生命周期:每次页面显示时刷新留言板(解决从聊天室退回不更新问题) onShow() { if (this.product?.id) { this.fetchMessages(); } }, methods: { @@ -256,6 +256,8 @@ export default { statusColor(s) { switch (s) { case "PENDING": return "s-yellow"; case "WIP": return "s-blue"; case "COMPLETED": return "s-green"; case "REJECTED": return "s-red"; default: return "s-gray"; } }, async doQuery(sn) { this.loading = true; this.error = ""; try { this.product = await get(`/products/scan/${sn}`); this.$nextTick(() => { this.currentMode = 'workspace'; this.autoLockTaskId = this.findMyImmersiveTask() || ''; if (!this.product.task_tree || !this.product.task_tree.length) { this.openCreateFirstTask(); } }); this.fetchMessages(); } catch (e) { this.error = e?.data?.detail || "查询失败"; } finally { this.loading = false; } }, + // 🚀 从 taskId 反查 product_serial → 再 doQuery + async doQueryByTask(tid) { try { const task = await get(`/tasks/${tid}`); const sn = task?.product_sn || ""; if (sn) { this.doQuery(sn); } else { this.error = "未找到关联产品"; this.loading = false; } } catch { this.error = "任务查询失败"; this.loading = false; } }, findMyImmersiveTask() { // 🚀 扫描用户的 WIP/PENDING 任务,自动沉浸锁定 if (!this.product || !this.product.task_tree) return null;