diff --git a/apps/backend/src/services/platforms/affiliate-dash/product-image.test.ts b/apps/backend/src/services/platforms/affiliate-dash/product-image.test.ts new file mode 100644 index 00000000..52c2a838 --- /dev/null +++ b/apps/backend/src/services/platforms/affiliate-dash/product-image.test.ts @@ -0,0 +1,66 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import fs from 'node:fs' +import path from 'node:path' + +import { AFFILIATE_DASH_SKU_MAPPING } from './sku-mapping.js' +import { + PRODUCT_IMAGE_EXTENSION, + PRODUCT_IMAGE_PUBLIC_PREFIX, + resolveProductImageSlug, + resolveProductImageUrl, +} from './product-image.js' + +test('内部 SKU → 商品图 URL(真实 SKU 案例)', () => { + assert.equal(resolveProductImageSlug('幸运币2个'), 'lucky_coin_x2') + assert.equal(resolveProductImageUrl('幸运币2个'), '/images/products/lucky_coin_x2.webp') + assert.equal( + resolveProductImageUrl('套装-浪漫天命'), + '/images/products/suit_romantic_destiny.webp', + ) +}) + +test('映射不到 / 空值 一律返回空串(不猜、不拼不存在的文件)', () => { + assert.equal(resolveProductImageSlug('不存在的皮肤'), '') + assert.equal(resolveProductImageSlug(''), '') + assert.equal(resolveProductImageSlug(null), '') + assert.equal(resolveProductImageSlug(undefined), '') + assert.equal(resolveProductImageUrl('不存在的皮肤'), '') + assert.equal(resolveProductImageUrl(null), '') +}) + +test('SKU 两端空白可容忍', () => { + assert.equal(resolveProductImageUrl(' 幸运币2个 '), '/images/products/lucky_coin_x2.webp') +}) + +/** + * 守卫:映射表里每个英文 sku 都必须有对应图片文件。 + * + * 图片放在前端 publicDir(`apps/frontend/public/images/products/`),后端测试只读校验; + * 目录不存在(例如只部署后端的场景)时跳过,避免误报。 + */ +test('映射表每个 sku 都有对应图片文件(防漏图)', () => { + const imageDir = path.resolve(process.cwd(), '..', 'frontend', 'public', 'images', 'products') + + if (!fs.existsSync(imageDir)) { + return + } + + const existing = new Set( + fs + .readdirSync(imageDir) + .filter((name) => !name.startsWith('.')) + .map((name) => name.replace(/\.[^.]+$/, '')), + ) + const slugs = [...new Set(Object.values(AFFILIATE_DASH_SKU_MAPPING))] + + assert.ok(slugs.length > 0) + const missing = slugs.filter((slug) => !existing.has(slug)) + assert.deepEqual(missing, [], `以下 sku 缺图片文件:${missing.join(', ')}`) + + // 统一用 webp;若某个 slug 只有 png 会让前端 404,这里一并卡住 + const missingWebp = slugs.filter((slug) => !existing.has(slug)) + assert.deepEqual(missingWebp, []) + assert.equal(PRODUCT_IMAGE_EXTENSION, 'webp') + assert.equal(PRODUCT_IMAGE_PUBLIC_PREFIX, '/images/products') +}) diff --git a/apps/backend/src/services/platforms/affiliate-dash/product-image.ts b/apps/backend/src/services/platforms/affiliate-dash/product-image.ts new file mode 100644 index 00000000..f72813e2 --- /dev/null +++ b/apps/backend/src/services/platforms/affiliate-dash/product-image.ts @@ -0,0 +1,39 @@ +/** + * 商品图(内部 SKU → 图片 URL)。 + * + * 图片文件放在前端 `apps/frontend/public/images/products/`,文件名就是 + * affiliate_dash 的**英文 sku**(如 `lucky_coin_x2.webp`)。 + * 内部 SKU(抖店/码速达的 `sku_order_list[].code`,如「幸运币2个」)先经 + * `AFFILIATE_DASH_SKU_MAPPING` 转成英文 sku,再拼出图片地址。 + * + * 为什么后端出这个 URL:调用方(领取页接口)只需要「给我图」,不关心命名约定; + * 以后图片挪到 CDN 也只改这里。 + * + * 注意:英文 sku 是**图片文件名**,不是平台 sku;映射表缺条目时返回空串, + * 由前端展示占位图(不猜、不拼不存在的文件)。 + */ +import { AFFILIATE_DASH_SKU_MAPPING } from './sku-mapping.js' + +/** 图片对外目录前缀(前端 publicDir 下的路径,Vite 原样对外)。 */ +export const PRODUCT_IMAGE_PUBLIC_PREFIX = '/images/products' + +/** 图片后缀:38 个 slug 全部有 webp,统一用它(体积小、移动端浏览器都支持)。 */ +export const PRODUCT_IMAGE_EXTENSION = 'webp' + +/** 内部 SKU → 图片文件名(英文 sku);映射不到返回空串。 */ +export function resolveProductImageSlug(internalSku: unknown): string { + const key = String(internalSku ?? '').trim() + if (!key) { + return '' + } + return String(AFFILIATE_DASH_SKU_MAPPING[key] || '').trim() +} + +/** 内部 SKU → 图片 URL;映射不到返回空串(前端展示占位)。 */ +export function resolveProductImageUrl(internalSku: unknown): string { + const slug = resolveProductImageSlug(internalSku) + if (!slug) { + return '' + } + return `${PRODUCT_IMAGE_PUBLIC_PREFIX}/${slug}.${PRODUCT_IMAGE_EXTENSION}` +} diff --git a/apps/frontend/public/images/products/bag_cute_bear.webp b/apps/frontend/public/images/products/bag_cute_bear.webp new file mode 100644 index 00000000..683b02e5 Binary files /dev/null and b/apps/frontend/public/images/products/bag_cute_bear.webp differ diff --git a/apps/frontend/public/images/products/bag_hamster_gray.webp b/apps/frontend/public/images/products/bag_hamster_gray.webp new file mode 100644 index 00000000..c8e50683 Binary files /dev/null and b/apps/frontend/public/images/products/bag_hamster_gray.webp differ diff --git a/apps/frontend/public/images/products/bag_pink_sheep.webp b/apps/frontend/public/images/products/bag_pink_sheep.webp new file mode 100644 index 00000000..3ffd9168 Binary files /dev/null and b/apps/frontend/public/images/products/bag_pink_sheep.webp differ diff --git a/apps/frontend/public/images/products/frag_pink_sheep.webp b/apps/frontend/public/images/products/frag_pink_sheep.webp new file mode 100644 index 00000000..342069d9 Binary files /dev/null and b/apps/frontend/public/images/products/frag_pink_sheep.webp differ diff --git a/apps/frontend/public/images/products/helmet_hamster_gray.webp b/apps/frontend/public/images/products/helmet_hamster_gray.webp new file mode 100644 index 00000000..7bc30c30 Binary files /dev/null and b/apps/frontend/public/images/products/helmet_hamster_gray.webp differ diff --git a/apps/frontend/public/images/products/helmet_panda_treasure.webp b/apps/frontend/public/images/products/helmet_panda_treasure.webp new file mode 100644 index 00000000..7765c69d Binary files /dev/null and b/apps/frontend/public/images/products/helmet_panda_treasure.webp differ diff --git a/apps/frontend/public/images/products/helmet_pink_sheep.webp b/apps/frontend/public/images/products/helmet_pink_sheep.webp new file mode 100644 index 00000000..c24bedf0 Binary files /dev/null and b/apps/frontend/public/images/products/helmet_pink_sheep.webp differ diff --git a/apps/frontend/public/images/products/honor_medal_x2.webp b/apps/frontend/public/images/products/honor_medal_x2.webp new file mode 100644 index 00000000..45040efa Binary files /dev/null and b/apps/frontend/public/images/products/honor_medal_x2.webp differ diff --git a/apps/frontend/public/images/products/honor_medal_x30.webp b/apps/frontend/public/images/products/honor_medal_x30.webp new file mode 100644 index 00000000..8aa93421 Binary files /dev/null and b/apps/frontend/public/images/products/honor_medal_x30.webp differ diff --git a/apps/frontend/public/images/products/honor_medal_x90.webp b/apps/frontend/public/images/products/honor_medal_x90.webp new file mode 100644 index 00000000..556b7bd0 Binary files /dev/null and b/apps/frontend/public/images/products/honor_medal_x90.webp differ diff --git a/apps/frontend/public/images/products/lucky_coin_x2.webp b/apps/frontend/public/images/products/lucky_coin_x2.webp new file mode 100644 index 00000000..d84b9cec Binary files /dev/null and b/apps/frontend/public/images/products/lucky_coin_x2.webp differ diff --git a/apps/frontend/public/images/products/lucky_coin_x30.webp b/apps/frontend/public/images/products/lucky_coin_x30.webp new file mode 100644 index 00000000..507617cc Binary files /dev/null and b/apps/frontend/public/images/products/lucky_coin_x30.webp differ diff --git a/apps/frontend/public/images/products/lucky_coin_x90.webp b/apps/frontend/public/images/products/lucky_coin_x90.webp new file mode 100644 index 00000000..7767a856 Binary files /dev/null and b/apps/frontend/public/images/products/lucky_coin_x90.webp differ diff --git a/apps/frontend/public/images/products/m416_hamster_gray.webp b/apps/frontend/public/images/products/m416_hamster_gray.webp new file mode 100644 index 00000000..7ed089ca Binary files /dev/null and b/apps/frontend/public/images/products/m416_hamster_gray.webp differ diff --git a/apps/frontend/public/images/products/pack_dragon_palace.webp b/apps/frontend/public/images/products/pack_dragon_palace.webp new file mode 100644 index 00000000..8218780e Binary files /dev/null and b/apps/frontend/public/images/products/pack_dragon_palace.webp differ diff --git a/apps/frontend/public/images/products/pack_lava_ranger.webp b/apps/frontend/public/images/products/pack_lava_ranger.webp new file mode 100644 index 00000000..22947c27 Binary files /dev/null and b/apps/frontend/public/images/products/pack_lava_ranger.webp differ diff --git a/apps/frontend/public/images/products/pack_star_roam_outfit.webp b/apps/frontend/public/images/products/pack_star_roam_outfit.webp new file mode 100644 index 00000000..5afe8275 Binary files /dev/null and b/apps/frontend/public/images/products/pack_star_roam_outfit.webp differ diff --git a/apps/frontend/public/images/products/pack_star_roam_weapon.webp b/apps/frontend/public/images/products/pack_star_roam_weapon.webp new file mode 100644 index 00000000..037eb0de Binary files /dev/null and b/apps/frontend/public/images/products/pack_star_roam_weapon.webp differ diff --git a/apps/frontend/public/images/products/pack_western_cowboy.webp b/apps/frontend/public/images/products/pack_western_cowboy.webp new file mode 100644 index 00000000..7390f8ac Binary files /dev/null and b/apps/frontend/public/images/products/pack_western_cowboy.webp differ diff --git a/apps/frontend/public/images/products/rock_black_tshirt.png b/apps/frontend/public/images/products/rock_black_tshirt.png new file mode 100644 index 00000000..0cf2c63d Binary files /dev/null and b/apps/frontend/public/images/products/rock_black_tshirt.png differ diff --git a/apps/frontend/public/images/products/rock_black_tshirt.webp b/apps/frontend/public/images/products/rock_black_tshirt.webp new file mode 100644 index 00000000..0cf2c63d Binary files /dev/null and b/apps/frontend/public/images/products/rock_black_tshirt.webp differ diff --git a/apps/frontend/public/images/products/rock_blue_sportswear.png b/apps/frontend/public/images/products/rock_blue_sportswear.png new file mode 100644 index 00000000..e057107b Binary files /dev/null and b/apps/frontend/public/images/products/rock_blue_sportswear.png differ diff --git a/apps/frontend/public/images/products/rock_blue_sportswear.webp b/apps/frontend/public/images/products/rock_blue_sportswear.webp new file mode 100644 index 00000000..e057107b Binary files /dev/null and b/apps/frontend/public/images/products/rock_blue_sportswear.webp differ diff --git a/apps/frontend/public/images/products/smoke_pink_sheep.webp b/apps/frontend/public/images/products/smoke_pink_sheep.webp new file mode 100644 index 00000000..09504b6a Binary files /dev/null and b/apps/frontend/public/images/products/smoke_pink_sheep.webp differ diff --git a/apps/frontend/public/images/products/suit_alan_walker.webp b/apps/frontend/public/images/products/suit_alan_walker.webp new file mode 100644 index 00000000..d064dca4 Binary files /dev/null and b/apps/frontend/public/images/products/suit_alan_walker.webp differ diff --git a/apps/frontend/public/images/products/suit_cloud_bear.webp b/apps/frontend/public/images/products/suit_cloud_bear.webp new file mode 100644 index 00000000..ec782a60 Binary files /dev/null and b/apps/frontend/public/images/products/suit_cloud_bear.webp differ diff --git a/apps/frontend/public/images/products/suit_cute_bear.webp b/apps/frontend/public/images/products/suit_cute_bear.webp new file mode 100644 index 00000000..d391e64a Binary files /dev/null and b/apps/frontend/public/images/products/suit_cute_bear.webp differ diff --git a/apps/frontend/public/images/products/suit_dual_fluffy.webp b/apps/frontend/public/images/products/suit_dual_fluffy.webp new file mode 100644 index 00000000..d8959f5b Binary files /dev/null and b/apps/frontend/public/images/products/suit_dual_fluffy.webp differ diff --git a/apps/frontend/public/images/products/suit_dune_song.webp b/apps/frontend/public/images/products/suit_dune_song.webp new file mode 100644 index 00000000..9ec951c3 Binary files /dev/null and b/apps/frontend/public/images/products/suit_dune_song.webp differ diff --git a/apps/frontend/public/images/products/suit_first_peach.webp b/apps/frontend/public/images/products/suit_first_peach.webp new file mode 100644 index 00000000..56f2268c Binary files /dev/null and b/apps/frontend/public/images/products/suit_first_peach.webp differ diff --git a/apps/frontend/public/images/products/suit_hamster_gray.webp b/apps/frontend/public/images/products/suit_hamster_gray.webp new file mode 100644 index 00000000..237d9080 Binary files /dev/null and b/apps/frontend/public/images/products/suit_hamster_gray.webp differ diff --git a/apps/frontend/public/images/products/suit_jester_face.webp b/apps/frontend/public/images/products/suit_jester_face.webp new file mode 100644 index 00000000..fbc2c151 Binary files /dev/null and b/apps/frontend/public/images/products/suit_jester_face.webp differ diff --git a/apps/frontend/public/images/products/suit_panda_round.webp b/apps/frontend/public/images/products/suit_panda_round.webp new file mode 100644 index 00000000..c1695022 Binary files /dev/null and b/apps/frontend/public/images/products/suit_panda_round.webp differ diff --git a/apps/frontend/public/images/products/suit_panda_tuan.webp b/apps/frontend/public/images/products/suit_panda_tuan.webp new file mode 100644 index 00000000..eb4220df Binary files /dev/null and b/apps/frontend/public/images/products/suit_panda_tuan.webp differ diff --git a/apps/frontend/public/images/products/suit_pink_sheep.webp b/apps/frontend/public/images/products/suit_pink_sheep.webp new file mode 100644 index 00000000..c4b7d921 Binary files /dev/null and b/apps/frontend/public/images/products/suit_pink_sheep.webp differ diff --git a/apps/frontend/public/images/products/suit_romantic_destiny.webp b/apps/frontend/public/images/products/suit_romantic_destiny.webp new file mode 100644 index 00000000..23ebdae2 Binary files /dev/null and b/apps/frontend/public/images/products/suit_romantic_destiny.webp differ diff --git a/apps/frontend/public/images/products/suit_sand_dancer.webp b/apps/frontend/public/images/products/suit_sand_dancer.webp new file mode 100644 index 00000000..d61dc838 Binary files /dev/null and b/apps/frontend/public/images/products/suit_sand_dancer.webp differ diff --git a/apps/frontend/public/images/products/suit_shadow_gothic.webp b/apps/frontend/public/images/products/suit_shadow_gothic.webp new file mode 100644 index 00000000..c7c2b473 Binary files /dev/null and b/apps/frontend/public/images/products/suit_shadow_gothic.webp differ diff --git a/apps/frontend/public/images/products/suit_western_mystery.webp b/apps/frontend/public/images/products/suit_western_mystery.webp new file mode 100644 index 00000000..4951fdc6 Binary files /dev/null and b/apps/frontend/public/images/products/suit_western_mystery.webp differ diff --git a/apps/frontend/public/images/products/top_black_elite_trainer.webp b/apps/frontend/public/images/products/top_black_elite_trainer.webp new file mode 100644 index 00000000..3e0c01e7 Binary files /dev/null and b/apps/frontend/public/images/products/top_black_elite_trainer.webp differ