fix: restore certificate report PDF preview and download
This commit is contained in:
parent
2e7235c704
commit
41ecfb6d36
@ -13,7 +13,7 @@ import {
|
||||
import { updateMaterialWithSharedRules, type MaterialUpdateInput } from '@/lib/material-update';
|
||||
import { deriveTaskDetailIsFullSet } from '@/lib/task-detail-full-set';
|
||||
import { isWebFileLike } from '@/lib/web-file';
|
||||
import { detectFileKindFromName, getCertificateReportFileKindByAssetType } from '@/lib/certificate-report';
|
||||
import { detectFileKindFromName, getCertificateReportFileKindByAssetType, isPublicCertificateReportPdf } from '@/lib/certificate-report';
|
||||
import { DownloadErrorCode } from '@/lib/download-errors';
|
||||
import { getVideoPreviewPolicyFields, userHasVideoEarlyAccess } from '@/lib/video-download-policy';
|
||||
import { is3DOriginalType, isProductPackageType } from '@/lib/material-type-mapper';
|
||||
@ -556,9 +556,11 @@ export async function GET(
|
||||
...asset,
|
||||
fileUrl: isPreview
|
||||
? rewriteMediaUrlForClient(asset.fileUrl, request, 'image')
|
||||
: canAccessSource && asset.assetType === 'report-pdf'
|
||||
? buildProtectedUploadUrl(asset.fileUrl) || ''
|
||||
: '',
|
||||
: isPublicCertificateReportPdf(asset.assetType, asset.fileUrl, response.id)
|
||||
? rewriteMediaUrlForClient(asset.fileUrl, request, 'source')
|
||||
: canAccessSource && asset.assetType === 'report-pdf'
|
||||
? buildProtectedUploadUrl(asset.fileUrl) || ''
|
||||
: '',
|
||||
fileSize:
|
||||
typeof asset.fileSize === 'bigint'
|
||||
? asset.fileSize.toString()
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
import { isProductPackageType, is3DOriginalType, needsAssetsList } from '@/lib/material-type-mapper';
|
||||
import { deriveTaskDetailIsFullSet } from '@/lib/task-detail-full-set';
|
||||
import { canManageMaterial } from '@/lib/material-authorization';
|
||||
import { detectFileKindFromName, getCertificateReportFileKindByAssetType } from '@/lib/certificate-report';
|
||||
import { detectFileKindFromName, getCertificateReportFileKindByAssetType, isPublicCertificateReportPdf } from '@/lib/certificate-report';
|
||||
import { DownloadErrorCode } from '@/lib/download-errors';
|
||||
import { getVideoPreviewPolicyFields, userHasVideoEarlyAccess } from '@/lib/video-download-policy';
|
||||
|
||||
@ -414,9 +414,11 @@ export async function GET(request: NextRequest) {
|
||||
...asset,
|
||||
fileUrl: isPreview
|
||||
? rewriteMediaUrlForClient(asset.fileUrl, request, 'image')
|
||||
: canAccessSource && asset.assetType === 'report-pdf'
|
||||
? buildProtectedUploadUrl(asset.fileUrl) || ''
|
||||
: '',
|
||||
: isPublicCertificateReportPdf(asset.assetType, asset.fileUrl, response.id)
|
||||
? rewriteMediaUrlForClient(asset.fileUrl, request, 'source')
|
||||
: canAccessSource && asset.assetType === 'report-pdf'
|
||||
? buildProtectedUploadUrl(asset.fileUrl) || ''
|
||||
: '',
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
40
components/DocumentAssetViewer.test.tsx
Normal file
40
components/DocumentAssetViewer.test.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import DocumentAssetViewer from './DocumentAssetViewer'
|
||||
|
||||
test('报告地址可用于 PDF 预览、打开和按原文件名下载,并能切换图片', async () => {
|
||||
const pdfUrl = 'https://s3.jiansun.vip/gongxiangpan/public-display/certificate-report/material-1/pdf/report.pdf'
|
||||
const imageUrl = 'https://s3.jiansun.vip/gongxiangpan/public-display/certificate-report/material-1/image/report.jpg'
|
||||
const originalFetch = global.fetch
|
||||
const originalCreate = URL.createObjectURL
|
||||
const originalRevoke = URL.revokeObjectURL
|
||||
const blob = new Blob(['%PDF-1.4'], { type: 'application/pdf' })
|
||||
global.fetch = jest.fn().mockResolvedValue({ ok: true, blob: async () => blob })
|
||||
URL.createObjectURL = jest.fn().mockReturnValue('blob:report')
|
||||
URL.revokeObjectURL = jest.fn()
|
||||
const open = jest.spyOn(window, 'open').mockImplementation(() => null)
|
||||
let savedName = ''
|
||||
const click = jest.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(function (this: HTMLAnchorElement) {
|
||||
savedName = this.download
|
||||
})
|
||||
try {
|
||||
render(<DocumentAssetViewer title="检测报告" items={[
|
||||
{ id: 'pdf', fileName: '检测报告.pdf', fileUrl: pdfUrl, fileKind: 'pdf' },
|
||||
{ id: 'image', fileName: '检测报告.jpg', fileUrl: imageUrl, fileKind: 'image' },
|
||||
]} />)
|
||||
expect(screen.getByTitle('检测报告.pdf')).toHaveAttribute('src', `${pdfUrl}#toolbar=0&navpanes=0&scrollbar=1`)
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开原文件' }))
|
||||
expect(open).toHaveBeenCalledWith(pdfUrl, '_blank', 'noopener,noreferrer')
|
||||
fireEvent.click(screen.getByRole('button', { name: '下载原文件' }))
|
||||
await waitFor(() => expect(savedName).toBe('检测报告.pdf'))
|
||||
expect(global.fetch).toHaveBeenCalledWith(pdfUrl)
|
||||
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:report')
|
||||
fireEvent.click(screen.getByRole('button', { name: '图片 · 检测报告.jpg' }))
|
||||
expect(screen.getByAltText('检测报告.jpg')).toHaveAttribute('src', imageUrl)
|
||||
} finally {
|
||||
click.mockRestore()
|
||||
open.mockRestore()
|
||||
global.fetch = originalFetch
|
||||
URL.createObjectURL = originalCreate
|
||||
URL.revokeObjectURL = originalRevoke
|
||||
}
|
||||
})
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
DEFAULT_CERTIFICATE_REPORT_USAGE_RULES,
|
||||
getCertificateReportDisplayTitle,
|
||||
isPublicCertificateReportPdf,
|
||||
parseCertificateTestDate,
|
||||
validateCertificateReportMetadata,
|
||||
} from '@/lib/certificate-report'
|
||||
@ -16,6 +17,16 @@ const metadata = {
|
||||
platformName: '大货',
|
||||
}
|
||||
|
||||
test('公开报告 PDF 必须位于当前素材的公开 PDF 目录', () => {
|
||||
const root = 'https://s3.jiansun.vip/gongxiangpan/public-display/certificate-report/material-1/pdf/'
|
||||
expect(isPublicCertificateReportPdf('report-pdf', `${root}report.pdf`, 'material-1')).toBe(true)
|
||||
expect(isPublicCertificateReportPdf('source', `${root}report.pdf`, 'material-1')).toBe(false)
|
||||
expect(isPublicCertificateReportPdf('report-pdf', `${root}report.pdf`, 'material-2')).toBe(false)
|
||||
expect(isPublicCertificateReportPdf('report-pdf', `${root}../private.pdf`, 'material-1')).toBe(false)
|
||||
expect(isPublicCertificateReportPdf('report-pdf', `${root}%ZZ.pdf`, 'material-1')).toBe(false)
|
||||
expect(isPublicCertificateReportPdf('report-pdf', 'uploads/materials/report.pdf', 'material-1')).toBe(false)
|
||||
})
|
||||
|
||||
describe('检测报告字段规则', () => {
|
||||
test('成品沿用 SPU 或报告名称至少填一个,原材料必须填报告名称', () => {
|
||||
expect(validateCertificateReportMetadata(metadata)).toHaveProperty('value.reportKind', '成品')
|
||||
|
||||
@ -10,6 +10,7 @@ import { prisma } from '@/lib/prisma'
|
||||
|
||||
jest.mock('@/lib/prisma', () => ({
|
||||
prisma: {
|
||||
materialAsset: { findMany: jest.fn() },
|
||||
material: {
|
||||
findFirst: jest.fn(),
|
||||
},
|
||||
@ -72,6 +73,7 @@ function buildMaterial(overrides: Record<string, unknown> = {}) {
|
||||
describe('materials share security', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
;(prisma.materialAsset.findMany as jest.Mock).mockResolvedValue([])
|
||||
;(getServerSession as jest.Mock).mockResolvedValue(null)
|
||||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(null)
|
||||
;(prisma.materialLike.findFirst as jest.Mock).mockResolvedValue(null)
|
||||
@ -123,6 +125,27 @@ describe('materials share security', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.each([null, { user: { id: 'admin-1' } }])('公开检测报告返回 PDF 和图片,保留源文件隔离 (%j)', async (session) => {
|
||||
;(getServerSession as jest.Mock).mockResolvedValue(session)
|
||||
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ role: 'SYSTEM_ADMIN' })
|
||||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(buildMaterial({ materialType: '证书-检测报告' }))
|
||||
const pdfUrl = 'https://s3.jiansun.vip/gongxiangpan/public-display/certificate-report/material-1/pdf/0-report.pdf'
|
||||
const imageUrl = 'https://s3.jiansun.vip/gongxiangpan/public-display/certificate-report/material-1/image/1-report.jpg'
|
||||
;(prisma.materialAsset.findMany as jest.Mock).mockResolvedValue([
|
||||
{ id: 'pdf', assetType: 'report-pdf', fileUrl: pdfUrl, fileName: 'report.pdf', fileSize: BigInt(10), sortOrder: 0 },
|
||||
{ id: 'image', assetType: 'report-image', fileUrl: imageUrl, fileName: 'report.jpg', fileSize: BigInt(10), sortOrder: 1 },
|
||||
{ id: 'source', assetType: 'source', fileUrl: 'uploads/materials/private.zip', fileName: 'private.zip', fileSize: BigInt(10), sortOrder: 2 },
|
||||
{ id: 'private-pdf', assetType: 'report-pdf', fileUrl: 'https://s3.jiansun.vip/gongxiangpan/private/report.pdf', fileName: 'private.pdf', fileSize: BigInt(10), sortOrder: 3 },
|
||||
])
|
||||
const response = await GET(new NextRequest('https://sc.jiansun.vip/api/materials/share?id=material-1'))
|
||||
const body = await response.json()
|
||||
expect(response.status).toBe(200)
|
||||
expect(body.assets[0]).toMatchObject({ fileUrl: pdfUrl, fileKind: 'pdf' })
|
||||
expect(body.assets[1]).toMatchObject({ fileUrl: imageUrl, fileKind: 'image' })
|
||||
expect(body.assets[2].fileUrl).toBe('')
|
||||
expect(body.assets[3].fileUrl).toBe('')
|
||||
})
|
||||
|
||||
test('未满15天的视频分享链接保留有效,但不返回任何视频内容', async () => {
|
||||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(buildMaterial({
|
||||
materialType: '淘系-逛逛视频',
|
||||
|
||||
@ -41,6 +41,25 @@ export function isCertificateReportMaterialType(materialType: string | null | un
|
||||
return String(materialType || '').trim() === CERTIFICATE_REPORT_MATERIAL_TYPE
|
||||
}
|
||||
|
||||
// 上传完成接口将报告 PDF 放入公开展示目录;它不是受保护的本地源文件。
|
||||
export function isPublicCertificateReportPdf(assetType: unknown, fileUrl: unknown, materialId: string): boolean {
|
||||
if (assetType !== CERTIFICATE_REPORT_ASSET_TYPE.PDF || typeof fileUrl !== 'string') return false
|
||||
try {
|
||||
const url = new URL(fileUrl)
|
||||
if (url.protocol !== 'https:' && url.protocol !== 'http:') return false
|
||||
const segments = url.pathname.split('/').map(decodeURIComponent)
|
||||
const index = segments.indexOf('public-display')
|
||||
return index >= 0
|
||||
&& segments[index + 1] === 'certificate-report'
|
||||
&& segments[index + 2] === materialId
|
||||
&& segments[index + 3] === 'pdf'
|
||||
&& segments.length === index + 5
|
||||
&& /\.pdf$/i.test(segments[index + 4])
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeReportTitle(value: string | null | undefined): string | null {
|
||||
const normalized = typeof value === 'string' ? value.trim() : ''
|
||||
return normalized || null
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user