diff --git a/server-api/app/controller/app/MaterialTagRedirectController.php b/server-api/app/controller/app/MaterialTagRedirectController.php index bd8f0d5..518953b 100644 --- a/server-api/app/controller/app/MaterialTagRedirectController.php +++ b/server-api/app/controller/app/MaterialTagRedirectController.php @@ -15,7 +15,7 @@ class MaterialTagRedirectController } try { - $url = (new MaterialTagService())->buildMaterialTagDetailUrl($token); + $url = (new MaterialTagService())->buildMaterialTagScanEntryUrl($token); } catch (\Throwable $e) { return response($e->getMessage(), 500); } diff --git a/server-api/app/support/MaterialTagService.php b/server-api/app/support/MaterialTagService.php index 9cb1c70..5b79d1a 100644 --- a/server-api/app/support/MaterialTagService.php +++ b/server-api/app/support/MaterialTagService.php @@ -922,11 +922,61 @@ class MaterialTagService return $this->buildMaterialTagH5Url($token, $baseUrl); } + public function buildMaterialTagScanEntryUrl(string $token): string + { + $baseUrl = $this->normalizeH5BaseUrl($this->getSystemConfigValue('h5', 'page_base_url')); + if ($baseUrl === '') { + throw new \RuntimeException('H5 页面根地址未配置'); + } + + $reportNo = $this->findPublishedReportNoByToken($token); + if ($reportNo !== '') { + return $this->buildReportDetailH5Url($reportNo, $baseUrl, $token); + } + + return $this->buildMaterialTagH5Url($token, $baseUrl); + } + private function buildMaterialTagH5Url(string $token, string $baseUrl): string { return rtrim($baseUrl, '/') . '/#/pages/material-tag/detail?token=' . rawurlencode($token); } + private function buildReportDetailH5Url(string $reportNo, string $baseUrl, string $token = ''): string + { + $params = ['report_no' => $reportNo]; + if ($token !== '') { + $params['material_tag_token'] = $token; + } + + return rtrim($baseUrl, '/') . '/#/pages/report/detail?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986); + } + + private function findPublishedReportNoByToken(string $token): string + { + $tag = Db::name('material_tag_codes')->where('qr_token', $token)->find(); + if ( + !$tag + || (int)($tag['report_id'] ?? 0) <= 0 + || ($tag['bind_status'] ?? '') !== 'bound' + || ($tag['status'] ?? 'active') === 'invalid' + ) { + return ''; + } + + $batch = Db::name('material_batches')->where('id', (int)$tag['batch_id'])->find(); + if ($batch && ($batch['status'] ?? 'active') === 'invalid') { + return ''; + } + + $report = Db::name('reports') + ->where('id', (int)$tag['report_id']) + ->where('report_status', 'published') + ->find(); + + return $report ? (string)$report['report_no'] : ''; + } + private function generateUniqueBatchNo(): string { for ($i = 0; $i < 20; $i++) { diff --git a/user-app/src/api/app.ts b/user-app/src/api/app.ts index 77d772b..a52c388 100644 --- a/user-app/src/api/app.ts +++ b/user-app/src/api/app.ts @@ -412,7 +412,7 @@ export interface VerifyData { } export interface MaterialTagData { - tag_status: "unbound" | "pending_report" | "published" | "not_found"; + tag_status: "unbound" | "pending_report" | "published" | "invalid" | "not_found"; status_text: string; message: string; qr_token: string; diff --git a/user-app/src/pages/material-tag/detail.vue b/user-app/src/pages/material-tag/detail.vue index a0b9d2a..677d033 100644 --- a/user-app/src/pages/material-tag/detail.vue +++ b/user-app/src/pages/material-tag/detail.vue @@ -29,7 +29,17 @@ const statusTitle = computed(() => { function goReport() { if (!reportNo.value) return; - uni.navigateTo({ url: `/pages/report/detail?report_no=${encodeURIComponent(reportNo.value)}` }); + uni.navigateTo({ url: buildReportDetailUrl(reportNo.value) }); +} + +function buildReportDetailUrl(currentReportNo: string) { + return `/pages/report/detail?report_no=${encodeURIComponent(currentReportNo)}`; +} + +function redirectToReportIfReady() { + if (!isPublished.value || !reportNo.value) return false; + uni.redirectTo({ url: buildReportDetailUrl(reportNo.value) }); + return true; } async function fetchDetail(currentToken: string) { @@ -37,6 +47,7 @@ async function fetchDetail(currentToken: string) { loadError.value = ""; try { detail.value = await appApi.getMaterialTag(currentToken); + if (redirectToReportIfReady()) return; } catch (error) { console.warn("material tag load failed", error); loadError.value = resolveErrorMessage(error, "吊牌信息加载失败,请稍后重试。"); diff --git a/user-app/src/pages/report/detail.vue b/user-app/src/pages/report/detail.vue index 6b83844..c83edc4 100644 --- a/user-app/src/pages/report/detail.vue +++ b/user-app/src/pages/report/detail.vue @@ -98,6 +98,18 @@ const zhongjianImageFiles = computed(() => zhongjianReportFiles.value.filter((it const zhongjianOtherFiles = computed(() => zhongjianReportFiles.value.filter((item) => item.file_type !== "image")); const reportNo = computed(() => detail.value.report_header.report_no || ""); +async function recordMaterialTagScan(currentToken: string, expectedReportNo: string) { + try { + const materialTag = await appApi.getMaterialTag(currentToken); + const linkedReportNo = materialTag.report_summary?.report_no || ""; + if (linkedReportNo && expectedReportNo && linkedReportNo !== expectedReportNo) { + console.warn("material tag report mismatch", { expectedReportNo, linkedReportNo }); + } + } catch (error) { + console.warn("material tag scan log failed", error); + } +} + function appendProductItem(items: ProductDisplayItem[], label: unknown, value: unknown, remark: unknown = "") { const labelText = textValue(label); const valueText = textValue(value); @@ -302,14 +314,27 @@ watch(traceInfoVisible, (visible) => { onLoad(async (options) => { const id = Number(options?.id || 0); - const currentReportNo = String(options?.report_no || ""); - if (!id && !currentReportNo) { + let currentReportNo = String(options?.report_no || ""); + const materialTagToken = String(options?.material_tag_token || "").trim(); + let materialTagResolvedFromToken = false; + if (!id && !currentReportNo && !materialTagToken) { loadError.value = "缺少报告编号,无法查看详情。"; return; } loading.value = true; try { + if (!id && !currentReportNo && materialTagToken) { + const materialTag = await appApi.getMaterialTag(materialTagToken); + materialTagResolvedFromToken = true; + currentReportNo = materialTag.report_summary?.report_no || ""; + if (materialTag.tag_status !== "published" || !currentReportNo) { + throw new Error(materialTag.message || "该吊牌暂未关联已发布报告。"); + } + } + if (currentReportNo && materialTagToken && !materialTagResolvedFromToken) { + void recordMaterialTagScan(materialTagToken, currentReportNo); + } detail.value = await appApi.getReportDetail({ id: id || undefined, report_no: currentReportNo || undefined, @@ -385,10 +410,7 @@ onLoad(async (options) => { {{ resultItem.value || "-" }} {{ resultItem.remark }} - - 安心验 - 鉴定 - + @@ -540,7 +562,7 @@ onLoad(async (options) => { left: 28rpx; right: 28rpx; height: 560rpx; - background: url("../../static/report/report-watermark.svg") center / 100% 100% no-repeat; + background: url("../../static/report/report-bg-watermark.png") center / 100% auto no-repeat; opacity: 1; pointer-events: none; } @@ -724,56 +746,10 @@ onLoad(async (options) => { position: absolute; right: 2rpx; bottom: 22rpx; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; width: 106rpx; height: 106rpx; - border: 4rpx solid rgba(40, 151, 73, 0.82); - border-radius: 999rpx; - background: rgba(255, 255, 255, 0.42); - color: #239245; - box-shadow: inset 0 0 0 4rpx rgba(40, 151, 73, 0.1); - transform: rotate(-9deg); -} - -.report-seal::before { - position: absolute; - inset: 10rpx; - border: 2rpx solid rgba(40, 151, 73, 0.58); - border-radius: inherit; - content: ""; -} - -.report-seal::after { - position: absolute; - left: 50%; - bottom: 12rpx; - width: 34rpx; - height: 5rpx; - border-radius: 999rpx; - background: currentColor; - content: ""; - opacity: 0.7; - transform: translateX(-50%); -} - -.report-seal__brand { - position: relative; - z-index: 1; - font-size: 18rpx; - font-weight: 900; - line-height: 1; -} - -.report-seal__main { - position: relative; - z-index: 1; - margin-top: 9rpx; - font-size: 26rpx; - font-weight: 900; - line-height: 1; + display: block; + pointer-events: none; } .product-spec { diff --git a/user-app/src/static/report/report-auth-badge.png b/user-app/src/static/report/report-auth-badge.png new file mode 100644 index 0000000..b7170dd Binary files /dev/null and b/user-app/src/static/report/report-auth-badge.png differ diff --git a/user-app/src/static/report/report-bg-watermark.png b/user-app/src/static/report/report-bg-watermark.png new file mode 100644 index 0000000..6ef1ee4 Binary files /dev/null and b/user-app/src/static/report/report-bg-watermark.png differ diff --git a/work-app/src/pages/report/detail.vue b/work-app/src/pages/report/detail.vue index e7744c2..969a16f 100644 --- a/work-app/src/pages/report/detail.vue +++ b/work-app/src/pages/report/detail.vue @@ -329,10 +329,7 @@ onShow(() => { {{ resultItem.value }} {{ resultItem.remark }} - - 安心验 - 鉴定 - + @@ -508,7 +505,7 @@ onShow(() => { left: 28rpx; right: 28rpx; height: 560rpx; - background: url("../../static/report/report-watermark.svg") center / 100% 100% no-repeat; + background: url("../../static/report/report-bg-watermark.png") center / 100% auto no-repeat; opacity: 1; pointer-events: none; } @@ -688,56 +685,10 @@ onShow(() => { position: absolute; right: 2rpx; bottom: 22rpx; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; width: 106rpx; height: 106rpx; - border: 4rpx solid rgba(40, 151, 73, 0.82); - border-radius: 999rpx; - background: rgba(255, 255, 255, 0.42); - color: #239245; - box-shadow: inset 0 0 0 4rpx rgba(40, 151, 73, 0.1); - transform: rotate(-9deg); -} - -.report-seal::before { - position: absolute; - inset: 10rpx; - border: 2rpx solid rgba(40, 151, 73, 0.58); - border-radius: inherit; - content: ""; -} - -.report-seal::after { - position: absolute; - left: 50%; - bottom: 12rpx; - width: 34rpx; - height: 5rpx; - border-radius: 999rpx; - background: currentColor; - content: ""; - opacity: 0.7; - transform: translateX(-50%); -} - -.report-seal__brand { - position: relative; - z-index: 1; - font-size: 18rpx; - font-weight: 900; - line-height: 1; -} - -.report-seal__main { - position: relative; - z-index: 1; - margin-top: 9rpx; - font-size: 26rpx; - font-weight: 900; - line-height: 1; + display: block; + pointer-events: none; } .product-spec { diff --git a/work-app/src/static/report/report-auth-badge.png b/work-app/src/static/report/report-auth-badge.png new file mode 100644 index 0000000..b7170dd Binary files /dev/null and b/work-app/src/static/report/report-auth-badge.png differ diff --git a/work-app/src/static/report/report-bg-watermark.png b/work-app/src/static/report/report-bg-watermark.png new file mode 100644 index 0000000..6ef1ee4 Binary files /dev/null and b/work-app/src/static/report/report-bg-watermark.png differ