gongxiangp2/lib/standard-model-white-background.ts
chenpei 4712175cb0 feat(素材链路): 发布白底图双轨与AI生图回传能力
- 完成白底图 WebP 双轨接口、上传链路与预览缓存改造
- 新增 AI 生图平台成品回传接口、任务状态与表单规则
- 补齐 AI 生成标签、任务详情补全、相关测试与对接文档
2026-03-25 10:28:02 +08:00

443 lines
14 KiB
TypeScript

export interface StandardModelWhiteBgView {
key: StandardModelWhiteBgKey
label: string
fieldName: string
}
export type StandardModelWhiteBgKey =
| 'leftFront45'
| 'rightFront45'
| 'leftBack45'
| 'rightBack45'
| 'front'
| 'back'
| 'left'
| 'right'
export const STANDARD_MODEL_WHITE_BG_VIEWS: StandardModelWhiteBgView[] = [
{ key: 'leftFront45', label: '左前45°', fieldName: 'whiteBgLeftFront45' },
{ key: 'rightFront45', label: '右前45°', fieldName: 'whiteBgRightFront45' },
{ key: 'leftBack45', label: '左后45°', fieldName: 'whiteBgLeftBack45' },
{ key: 'rightBack45', label: '右后45°', fieldName: 'whiteBgRightBack45' },
{ key: 'front', label: '正视图', fieldName: 'whiteBgFront' },
{ key: 'back', label: '后视图', fieldName: 'whiteBgBack' },
{ key: 'left', label: '左视图', fieldName: 'whiteBgLeft' },
{ key: 'right', label: '右视图', fieldName: 'whiteBgRight' },
]
export const STANDARD_MODEL_WHITE_BG_OTHER_FIELD_NAME = 'whiteBgOtherImages'
export const LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE = 'standard-model-white-bg-zip'
export const LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX = 'standard-model-white-bg-zip:'
export const STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX = 'standard-model-white-bg-zip-v2:'
export const STANDARD_MODEL_WHITE_BG_VIEW_ASSET_TYPE_PREFIX = 'standard-model-white-bg-view:'
export const STANDARD_MODEL_WHITE_BG_OTHER_ASSET_TYPE_PREFIX = 'standard-model-white-bg-other:'
export interface ParsedStandardModelWhiteBgAssetType {
kind: 'view' | 'other' | 'zip'
skuId: string
viewKey?: StandardModelWhiteBgKey
index?: number
}
export interface WhiteBackgroundAssetLike {
assetType: string
sortOrder?: number | string | bigint | null
}
export interface GroupedWhiteBackgroundAssets<T extends WhiteBackgroundAssetLike> {
skuId: string
fixed: Partial<Record<StandardModelWhiteBgKey, T>>
other: T[]
zipAssets: T[]
}
export function getWhiteBackgroundFieldName(key: StandardModelWhiteBgKey): string {
const target = STANDARD_MODEL_WHITE_BG_VIEWS.find((item) => item.key === key)
return target ? target.fieldName : `whiteBg${key}`
}
export function getMissingWhiteBackgroundViews(
files: Partial<Record<StandardModelWhiteBgKey, File | null>>,
): StandardModelWhiteBgKey[] {
return STANDARD_MODEL_WHITE_BG_VIEWS
.map((item) => item.key)
.filter((key) => !files[key])
}
export function hasCompleteWhiteBackgroundSet(
files: Partial<Record<StandardModelWhiteBgKey, File | null>>,
): boolean {
return getMissingWhiteBackgroundViews(files).length === 0
}
export function normalizeWhiteBackgroundSkuName(value: string): string {
return String(value || '').replace(/[^a-zA-Z0-9\-_.]/g, '').toUpperCase().trim()
}
function sanitizeWhiteBackgroundFilePart(value: string): string {
return String(value || '')
.replace(/[\/\\]/g, '-')
.replace(/[<>:"|?*]/g, '')
.replace(/\s+/g, '')
.trim()
}
export function buildWhiteBackgroundFixedFileName(
skuName: string,
productCategoryName: string,
viewLabel: string,
ext: string,
): string {
const normalizedSkuName = normalizeWhiteBackgroundSkuName(skuName)
const safeCategoryName = sanitizeWhiteBackgroundFilePart(productCategoryName)
const safeViewLabel = sanitizeWhiteBackgroundFilePart(viewLabel)
const normalizedExt = ext.startsWith('.') ? ext.toLowerCase() : `.${ext.toLowerCase()}`
const baseName = [normalizedSkuName, safeCategoryName, safeViewLabel].filter(Boolean).join('-')
return `${baseName || 'white-background'}${normalizedExt}`
}
export function getCurrentWhiteBackgroundAssetType(skuId: string): string {
return `${STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX}${skuId}`
}
export function getWhiteBackgroundViewAssetType(
skuId: string,
key: StandardModelWhiteBgKey,
): string {
return `${STANDARD_MODEL_WHITE_BG_VIEW_ASSET_TYPE_PREFIX}${skuId}:${key}`
}
export function getWhiteBackgroundOtherAssetType(skuId: string, index: number): string {
return `${STANDARD_MODEL_WHITE_BG_OTHER_ASSET_TYPE_PREFIX}${skuId}:${index}`
}
export function getLegacyWhiteBackgroundAssetTypes(skuId: string): string[] {
return [
LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE,
`${LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX}${skuId}`,
]
}
export function getAllWhiteBackgroundAssetTypesForSku(skuId: string): string[] {
return [
getCurrentWhiteBackgroundAssetType(skuId),
...getLegacyWhiteBackgroundAssetTypes(skuId),
]
}
export function getWhiteBackgroundViewAssetPrefixForSku(skuId: string): string {
return `${STANDARD_MODEL_WHITE_BG_VIEW_ASSET_TYPE_PREFIX}${skuId}:`
}
export function getWhiteBackgroundOtherAssetPrefixForSku(skuId: string): string {
return `${STANDARD_MODEL_WHITE_BG_OTHER_ASSET_TYPE_PREFIX}${skuId}:`
}
export function getWhiteBackgroundAssetWhereForSku(skuId: string) {
return {
OR: [
{ assetType: getCurrentWhiteBackgroundAssetType(skuId) },
{ assetType: LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE },
{ assetType: `${LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX}${skuId}` },
{ assetType: { startsWith: getWhiteBackgroundViewAssetPrefixForSku(skuId) } },
{ assetType: { startsWith: getWhiteBackgroundOtherAssetPrefixForSku(skuId) } },
],
}
}
export function getWhiteBackgroundRelativeDirectory(materialId: string, skuId: string): string {
return ['uploads', 'skus', materialId, 'white-background', skuId].join('/')
}
export function replaceWhiteBackgroundFileExtension(fileName: string, ext: string = '.webp'): string {
const normalizedExt = ext.startsWith('.') ? ext.toLowerCase() : `.${ext.toLowerCase()}`
const normalizedFileName = String(fileName || '').trim().replace(/\\/g, '/')
if (!normalizedFileName) {
return `white-background${normalizedExt}`
}
const lastSlashIndex = normalizedFileName.lastIndexOf('/')
const directory = lastSlashIndex >= 0 ? normalizedFileName.slice(0, lastSlashIndex + 1) : ''
const rawBaseName = lastSlashIndex >= 0 ? normalizedFileName.slice(lastSlashIndex + 1) : normalizedFileName
const nextBaseName = rawBaseName.replace(/\.[^/.]+$/, '') || 'white-background'
return `${directory}${nextBaseName}${normalizedExt}`
}
export function getWhiteBackgroundRelativeFilePath(
materialId: string,
skuId: string,
fileName: string,
): string {
return `${getWhiteBackgroundRelativeDirectory(materialId, skuId)}/${fileName}`.replace(/\\/g, '/')
}
export function getWhiteBackgroundPreviewRelativeDirectory(materialId: string, skuId: string): string {
return `${getWhiteBackgroundRelativeDirectory(materialId, skuId)}/preview`.replace(/\\/g, '/')
}
export function getWhiteBackgroundPreviewRelativeFilePath(
materialId: string,
skuId: string,
fileName: string,
): string {
return `${getWhiteBackgroundPreviewRelativeDirectory(materialId, skuId)}/${replaceWhiteBackgroundFileExtension(fileName)}`.replace(/\\/g, '/')
}
export function getWhiteBackgroundOtherPreviewRelativeFilePath(
materialId: string,
skuId: string,
fileName: string,
): string {
return `${getWhiteBackgroundPreviewRelativeDirectory(materialId, skuId)}/other/${replaceWhiteBackgroundFileExtension(fileName)}`.replace(/\\/g, '/')
}
export function getWhiteBackgroundPreviewRelativePathFromAssetPath(fileUrl: string): string | null {
const normalizedFileUrl = String(fileUrl || '').trim().replace(/\\/g, '/')
if (!normalizedFileUrl || /^https?:\/\//i.test(normalizedFileUrl)) {
return null
}
const normalizedRelativePath = normalizedFileUrl.replace(/^\.?\//, '')
const segments = normalizedRelativePath.split('/').filter(Boolean)
const whiteBackgroundIndex = segments.indexOf('white-background')
if (whiteBackgroundIndex < 0 || !segments[whiteBackgroundIndex + 1]) {
return null
}
const skuRootSegments = segments.slice(0, whiteBackgroundIndex + 2)
const assetSegments = segments.slice(whiteBackgroundIndex + 2)
if (assetSegments.length === 0) {
return null
}
if (assetSegments[0] === 'preview') {
return normalizedRelativePath
}
if (assetSegments[0] === 'other') {
const otherFileName = assetSegments.slice(1).join('/')
if (!otherFileName) {
return null
}
return `${skuRootSegments.join('/')}/preview/other/${replaceWhiteBackgroundFileExtension(otherFileName)}`.replace(/\\/g, '/')
}
return `${skuRootSegments.join('/')}/preview/${replaceWhiteBackgroundFileExtension(assetSegments.join('/'))}`.replace(/\\/g, '/')
}
export function getWhiteBackgroundZipFileName(skuName: string): string {
return `${skuName}_白底图.zip`
}
export function countWhiteBackgroundFixedViews(
group?: Pick<GroupedWhiteBackgroundAssets<WhiteBackgroundAssetLike>, 'fixed'> | null,
): number {
if (!group) {
return 0
}
return STANDARD_MODEL_WHITE_BG_VIEWS.filter((view) => Boolean(group.fixed[view.key])).length
}
export function countWhiteBackgroundOtherImages(
group?: Pick<GroupedWhiteBackgroundAssets<WhiteBackgroundAssetLike>, 'other'> | null,
): number {
return group?.other?.length || 0
}
export function countWhiteBackgroundAssets(
group?: Pick<GroupedWhiteBackgroundAssets<WhiteBackgroundAssetLike>, 'fixed' | 'other' | 'zipAssets'> | null,
): number {
if (!group) {
return 0
}
return (
countWhiteBackgroundFixedViews(group) +
countWhiteBackgroundOtherImages(group) +
(group.zipAssets?.length || 0)
)
}
export function hasAnyWhiteBackgroundAssetGroup(
group?: Pick<GroupedWhiteBackgroundAssets<WhiteBackgroundAssetLike>, 'fixed' | 'other' | 'zipAssets'> | null,
): boolean {
return countWhiteBackgroundAssets(group) > 0
}
export function getMissingWhiteBackgroundViewsFromAssetGroup(
group?: Pick<GroupedWhiteBackgroundAssets<WhiteBackgroundAssetLike>, 'fixed'> | null,
): StandardModelWhiteBgView[] {
if (!group) {
return [...STANDARD_MODEL_WHITE_BG_VIEWS]
}
return STANDARD_MODEL_WHITE_BG_VIEWS.filter((view) => !group.fixed[view.key])
}
export function parseStandardModelWhiteBgAssetType(
assetType: string,
): ParsedStandardModelWhiteBgAssetType | null {
const trimmed = String(assetType || '').trim()
if (!trimmed) {
return null
}
if (trimmed.startsWith(STANDARD_MODEL_WHITE_BG_VIEW_ASSET_TYPE_PREFIX)) {
const remainder = trimmed.slice(STANDARD_MODEL_WHITE_BG_VIEW_ASSET_TYPE_PREFIX.length)
const separatorIndex = remainder.indexOf(':')
if (separatorIndex <= 0) {
return null
}
const skuId = remainder.slice(0, separatorIndex)
const viewKey = remainder.slice(separatorIndex + 1) as StandardModelWhiteBgKey
if (!STANDARD_MODEL_WHITE_BG_VIEWS.some((view) => view.key === viewKey)) {
return null
}
return {
kind: 'view',
skuId,
viewKey,
}
}
if (trimmed.startsWith(STANDARD_MODEL_WHITE_BG_OTHER_ASSET_TYPE_PREFIX)) {
const remainder = trimmed.slice(STANDARD_MODEL_WHITE_BG_OTHER_ASSET_TYPE_PREFIX.length)
const separatorIndex = remainder.indexOf(':')
if (separatorIndex <= 0) {
return null
}
const skuId = remainder.slice(0, separatorIndex)
const index = Number(remainder.slice(separatorIndex + 1))
if (!Number.isInteger(index) || index <= 0) {
return null
}
return {
kind: 'other',
skuId,
index,
}
}
if (trimmed === LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE) {
return {
kind: 'zip',
skuId: '',
}
}
if (trimmed.startsWith(LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX)) {
return {
kind: 'zip',
skuId: trimmed.slice(LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX.length),
}
}
if (trimmed.startsWith(STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX)) {
return {
kind: 'zip',
skuId: trimmed.slice(STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX.length),
}
}
return null
}
export function isWhiteBackgroundAssetType(assetType: string): boolean {
return parseStandardModelWhiteBgAssetType(assetType) !== null
}
function getAssetSortOrder(asset: WhiteBackgroundAssetLike): number {
const raw = asset.sortOrder
if (typeof raw === 'bigint') {
return Number(raw)
}
if (typeof raw === 'string') {
const parsed = Number(raw)
return Number.isFinite(parsed) ? parsed : 0
}
if (typeof raw === 'number') {
return Number.isFinite(raw) ? raw : 0
}
return 0
}
export function groupWhiteBackgroundAssetsBySku<T extends WhiteBackgroundAssetLike>(
assets: T[],
): Map<string, GroupedWhiteBackgroundAssets<T>> {
const groups = new Map<string, GroupedWhiteBackgroundAssets<T>>()
for (const asset of assets) {
const parsed = parseStandardModelWhiteBgAssetType(asset.assetType)
if (!parsed) {
continue
}
const groupSkuId = parsed.skuId
if (!groupSkuId) {
continue
}
const current = groups.get(groupSkuId) || {
skuId: groupSkuId,
fixed: {},
other: [],
zipAssets: [],
}
if (parsed.kind === 'view' && parsed.viewKey) {
current.fixed[parsed.viewKey] = asset
} else if (parsed.kind === 'other') {
current.other.push(asset)
} else if (parsed.kind === 'zip') {
current.zipAssets.push(asset)
}
groups.set(groupSkuId, current)
}
groups.forEach((group) => {
group.other.sort((left, right) => getAssetSortOrder(left) - getAssetSortOrder(right))
group.zipAssets.sort((left, right) => getAssetSortOrder(left) - getAssetSortOrder(right))
})
return groups
}
export function hasCompleteWhiteBackgroundAssetGroup(
group?: Pick<GroupedWhiteBackgroundAssets<WhiteBackgroundAssetLike>, 'fixed'> | null,
): boolean {
if (!group) {
return false
}
return STANDARD_MODEL_WHITE_BG_VIEWS.every((view) => Boolean(group.fixed[view.key]))
}
export function getNextWhiteBackgroundOtherIndex(
group?: Pick<GroupedWhiteBackgroundAssets<WhiteBackgroundAssetLike>, 'other'> | null,
): number {
if (!group?.other?.length) {
return 1
}
let maxIndex = 0
for (const asset of group.other) {
const parsed = parseStandardModelWhiteBgAssetType(asset.assetType)
if (parsed?.kind === 'other' && parsed.index && parsed.index > maxIndex) {
maxIndex = parsed.index
}
}
return maxIndex + 1
}