gongxiangp2/lib/certificate-report.ts

198 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

export const CERTIFICATE_REPORT_MATERIAL_TYPE = '证书-检测报告'
export const CERTIFICATE_REPORT_KIND = {
FINISHED: '成品',
RAW: '原材料',
} as const
export type CertificateReportKind = (typeof CERTIFICATE_REPORT_KIND)[keyof typeof CERTIFICATE_REPORT_KIND]
export const CERTIFICATE_REPORT_PLATFORM_OPTIONS = ['大货', '品牌'] as const
// 这是每份报告的一个可编辑文本字段,编号和换行属于原文内容。
export const DEFAULT_CERTIFICATE_REPORT_USAGE_RULES = `1、检测报告各电商平台官方正常认可的有效期为1年内,需注意检测时间;
2、检测报告均为由CMA资质的机构:常州检验检测标准认证研究院 出具,委托检测单位由不同主体委托,若使用中对委托送检主体有要求的,必须要核对好对应主体是否可以使用;
3、检测报告项目检测结果仅代表当批次产品,新品和其他的款式是否能通过需送检后方能确认;
4、检测报告检测项目各有不同,常规检测项目主要为:甲醛检测、结构安全、力学性能检测等,现有检测报告因需求不同,部分报告只做了甲醛项目、或者只做了力学性能检测未做甲醛检测等,使用前需核对好使用需求和报告内容;
5、检测项报告中:规格型号主要按品类名称制作,少部分单独特殊需求的检测报告有对应产品SPU型号,使用时需注意核对;部分检测报告因特殊需求附上产品图片,需注意核对报告上的图片是否和实际需求相符;
6、检测报告文件夹中,标明为品牌的,除顾家品牌店铺外禁止使用,品牌检测报告需求对接伯阳;;
7、检测报告若有需求的,需由部门负责人申请钉钉流程:质检报告申请流程,费用需送检部门支付,可以根据需求单独做某一项,常见检测报告收费范围:甲醛检测(1600-2500元,15个工作日左右)、结构安全和力学性能(800-2000元,7个工作日左右),同时需求部门需安装准备好对应样品,样品事先可以对接:守仁确认;`
export const CERTIFICATE_REPORT_ASSET_TYPE = {
PDF: 'report-pdf',
IMAGE: 'report-image',
} as const
export const CERTIFICATE_REPORT_UPLOAD_LIMITS = {
maxFiles: 20,
maxImageBytes: 100 * 1024 * 1024,
maxPdfBytes: 512 * 1024 * 1024,
maxTotalBytes: 1024 * 1024 * 1024,
} as const
const CERTIFICATE_REPORT_IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp'])
export type CertificateReportAssetType =
(typeof CERTIFICATE_REPORT_ASSET_TYPE)[keyof typeof CERTIFICATE_REPORT_ASSET_TYPE]
export type CertificateReportFileKind = 'pdf' | 'image'
export function isCertificateReportMaterialType(materialType: string | null | undefined): boolean {
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
}
export function parseCertificateDate(value: unknown): Date | null {
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return null
const parsed = new Date(`${value}T00:00:00.000Z`)
return Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== value
? null
: parsed
}
export function getDefaultCertificateExpiryDate(testedOn: string | Date | null | undefined): Date | null {
const dateText = testedOn instanceof Date
? (Number.isNaN(testedOn.getTime()) ? '' : testedOn.toISOString().slice(0, 10))
: testedOn?.slice(0, 10)
const testedDate = parseCertificateDate(dateText)
if (!testedDate) return null
const expiresDate = new Date(testedDate)
expiresDate.setUTCFullYear(testedDate.getUTCFullYear() + 1)
// 闰年 2 月 29 日顺延到次年 2 月末,避免溢出到 3 月。
if (expiresDate.getUTCMonth() !== testedDate.getUTCMonth()) expiresDate.setUTCDate(0)
return expiresDate
}
export function validateCertificateReportMetadata(input: {
reportKind: unknown
productModel: unknown
reportTitle: unknown
certificateTestedOn: unknown
certificateExpiresOn?: unknown
certificateNumber: unknown
reportUsageRules: unknown
platformName: unknown
}, legacyPlatformName = ''):
| { error: string }
| { value: {
reportKind: CertificateReportKind
certificateTestedOn: Date
certificateExpiresOn: Date
certificateNumber: string
reportUsageRules: string
} } {
const reportKind = input.reportKind
if (reportKind !== CERTIFICATE_REPORT_KIND.FINISHED && reportKind !== CERTIFICATE_REPORT_KIND.RAW) {
return { error: '请选择报告类别(成品或原材料)' }
}
const productModel = String(input.productModel || '').trim()
const reportTitle = String(input.reportTitle || '').trim()
if (reportKind === CERTIFICATE_REPORT_KIND.RAW && !reportTitle) {
return { error: '原材料检测报告必须填写报告名称' }
}
if (reportKind === CERTIFICATE_REPORT_KIND.FINISHED && !productModel && !reportTitle) {
return { error: 'SPU 和 报告名称/检测对象 至少填写一个' }
}
const certificateTestedOn = parseCertificateDate(input.certificateTestedOn)
if (!certificateTestedOn) return { error: '请选择有效的证书检测日期' }
const expiryIsEmpty = input.certificateExpiresOn == null
|| (typeof input.certificateExpiresOn === 'string' && input.certificateExpiresOn.trim() === '')
const certificateExpiresOn = expiryIsEmpty
? getDefaultCertificateExpiryDate(certificateTestedOn)
: parseCertificateDate(input.certificateExpiresOn)
if (!certificateExpiresOn) return { error: '请选择有效的证书到期时间' }
if (certificateExpiresOn < certificateTestedOn) return { error: '证书到期时间不能早于检测日期' }
const certificateNumber = typeof input.certificateNumber === 'string' ? input.certificateNumber.trim() : ''
if (!certificateNumber || certificateNumber.length > 120) {
return { error: '请填写证书编号(不超过120字)' }
}
const reportUsageRules = typeof input.reportUsageRules === 'string' ? input.reportUsageRules.trim() : ''
if (!reportUsageRules || reportUsageRules.length > 10000) {
return { error: '请填写检测报告使用规则说明(不超过10000字)' }
}
const platformName = typeof input.platformName === 'string' ? input.platformName.trim() : ''
if (platformName &&
!CERTIFICATE_REPORT_PLATFORM_OPTIONS.some((option) => option === platformName) &&
platformName !== legacyPlatformName) {
return { error: '检测报告的平台分类只能选择大货或品牌' }
}
return {
value: {
reportKind,
certificateTestedOn,
certificateExpiresOn,
certificateNumber,
reportUsageRules,
},
}
}
export function getCertificateReportDisplayTitle(input: {
productModel?: string | null
reportTitle?: string | null
}): string {
const productModel = String(input.productModel || '').trim()
const reportTitle = String(input.reportTitle || '').trim()
if (reportTitle) {
return reportTitle
}
if (productModel) {
return productModel
}
return '未命名报告'
}
export function getCertificateReportFileKindByAssetType(assetType: string | null | undefined): CertificateReportFileKind | null {
if (assetType === CERTIFICATE_REPORT_ASSET_TYPE.PDF) {
return 'pdf'
}
if (assetType === CERTIFICATE_REPORT_ASSET_TYPE.IMAGE) {
return 'image'
}
return null
}
export function detectFileKindFromName(fileName: string | null | undefined): CertificateReportFileKind {
const lowerName = String(fileName || '').trim().toLowerCase()
if (lowerName.endsWith('.pdf')) {
return 'pdf'
}
return 'image'
}
export function isSupportedCertificateReportFileName(fileName: string | null | undefined): boolean {
const normalized = String(fileName || '').trim().toLowerCase()
if (normalized.endsWith('.pdf')) {
return true
}
const extensionIndex = normalized.lastIndexOf('.')
const extension = extensionIndex >= 0 ? normalized.slice(extensionIndex) : ''
return CERTIFICATE_REPORT_IMAGE_EXTENSIONS.has(extension)
}