forked from lunanting/cangku
304 lines
11 KiB
JavaScript
304 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const {
|
|
normalizeAlibaba1688Product,
|
|
normalizeAlibaba1688ImageUrl,
|
|
mapProductRow,
|
|
listAlibaba1688Products,
|
|
replaceAlibaba1688Product,
|
|
} = require('../src/services/alibaba1688ProductService');
|
|
const planningDatabase = require('../src/services/planningDatabase');
|
|
|
|
function product(overrides = {}) {
|
|
return {
|
|
productId: '1073252781818',
|
|
title: '圆桌测试商品',
|
|
sourceUrl: 'https://detail.1688.com/offer/1073252781818.html',
|
|
mainImages: ['https://img.example.com/main.jpg'],
|
|
detailImages: ['https://img.example.com/detail.jpg'],
|
|
videos: [{
|
|
url: 'https://cloud.video.taobao.com/play/example.mp4',
|
|
coverUrl: 'https://img.example.com/video-cover.jpg_sum.jpg',
|
|
duration: 20.4,
|
|
width: 720,
|
|
height: 1280,
|
|
source: 'main-gallery',
|
|
}],
|
|
skuDimensions: [
|
|
{ name: '颜色', values: ['原木色', '胡桃色'] },
|
|
{ name: '尺寸', values: ['60*60', '70*70'] },
|
|
],
|
|
skus: [{
|
|
name: '原木色 / 60*60',
|
|
specs: [{ name: '颜色', value: '原木色' }, { name: '尺寸', value: '60*60' }],
|
|
image: 'https://img.example.com/sku-original-60.jpg',
|
|
price: 180,
|
|
freight: 40,
|
|
}],
|
|
attributes: [{ name: '材质', value: '实木' }],
|
|
storeCategory: '鞋架/鞋柜',
|
|
platformCategory: '家装建材 > 简易家具',
|
|
dropshipSupported: true,
|
|
importTag: '测款',
|
|
collectedAt: '2026-08-14T02:13:08.923Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test('normalizes a complete 1688 snapshot for full server replacement', () => {
|
|
const normalized = normalizeAlibaba1688Product({
|
|
dingtalkUserName: '香茗',
|
|
sourceType: 'product',
|
|
sourceImportId: 'import-1',
|
|
product: product(),
|
|
}, '1073252781818');
|
|
|
|
assert.equal(normalized.productId, '1073252781818');
|
|
assert.equal(normalized.detailImageCount, 1);
|
|
assert.equal(normalized.skuCount, 1);
|
|
assert.equal(normalized.snapshot.skuDimensions.length, 2);
|
|
assert.deepEqual(normalized.snapshot.skuImages, ['https://img.example.com/sku-original-60.jpg']);
|
|
assert.equal(normalized.snapshot.skus[0].image, 'https://img.example.com/sku-original-60.jpg');
|
|
assert.deepEqual(normalized.snapshot.videos, [{
|
|
url: 'https://cloud.video.taobao.com/play/example.mp4',
|
|
coverUrl: 'https://img.example.com/video-cover.jpg',
|
|
duration: 20.4,
|
|
width: 720,
|
|
height: 1280,
|
|
mimeType: '',
|
|
source: 'main-gallery',
|
|
}]);
|
|
assert.equal(normalized.snapshot.schemaVersion, 2);
|
|
assert.match(normalized.skuFingerprint, /^[a-f0-9]{64}$/);
|
|
assert.equal(normalized.snapshot.storeCategory, '鞋架/鞋柜');
|
|
assert.equal(normalized.snapshot.platformCategory, '家装建材 > 简易家具');
|
|
assert.equal(normalized.snapshot.dropshipSupported, true);
|
|
assert.deepEqual(normalized.snapshot.skus[0].specs, [
|
|
{ name: '颜色', value: '原木色' },
|
|
{ name: '尺寸', value: '60*60' },
|
|
]);
|
|
assert.equal(normalized.syncOperatorName, '香茗');
|
|
});
|
|
|
|
test('stores original Alibaba SKU image URLs instead of _sum thumbnails', () => {
|
|
const compressed = 'https://cbu01.alicdn.com/img/ibank/example.jpg_sum.jpg';
|
|
const original = 'https://cbu01.alicdn.com/img/ibank/example.jpg';
|
|
const normalized = normalizeAlibaba1688Product({
|
|
dingtalkUserName: '香茗',
|
|
sourceType: 'product',
|
|
product: product({
|
|
images: [compressed],
|
|
mainImages: [compressed],
|
|
detailImages: [compressed],
|
|
skuImages: [compressed],
|
|
skus: [{ name: '原木色', image: compressed }],
|
|
baseSkuRows: [{ name: '原木色', image: compressed }],
|
|
}),
|
|
});
|
|
|
|
assert.equal(normalizeAlibaba1688ImageUrl(`${compressed}?x=1`), `${original}?x=1`);
|
|
assert.deepEqual(normalized.snapshot.images, [original]);
|
|
assert.deepEqual(normalized.snapshot.mainImages, [original]);
|
|
assert.deepEqual(normalized.snapshot.detailImages, [original]);
|
|
assert.deepEqual(normalized.snapshot.skuImages, [original]);
|
|
assert.equal(normalized.snapshot.skus[0].image, original);
|
|
assert.equal(normalized.snapshot.baseSkuRows[0].image, original);
|
|
});
|
|
|
|
test('promotes videos preserved by older servers in skuScan metadata', () => {
|
|
const normalized = normalizeAlibaba1688Product({
|
|
product: product({
|
|
videos: [],
|
|
skuScan: {
|
|
complete: true,
|
|
videos: [{
|
|
src: 'https://cloud.video.taobao.com/play/legacy.mp4',
|
|
poster: 'https://img.example.com/legacy-cover.jpg',
|
|
duration: 18,
|
|
}],
|
|
},
|
|
}),
|
|
});
|
|
|
|
assert.equal(normalized.snapshot.videos.length, 1);
|
|
assert.equal(normalized.snapshot.videos[0].url, 'https://cloud.video.taobao.com/play/legacy.mp4');
|
|
});
|
|
|
|
test('rejects incomplete snapshots and mismatched route IDs', () => {
|
|
assert.throws(
|
|
() => normalizeAlibaba1688Product({ product: product({ detailImages: [] }) }),
|
|
error => error.statusCode === 422 && error.code === 'DETAIL_IMAGES_REQUIRED'
|
|
);
|
|
assert.throws(
|
|
() => normalizeAlibaba1688Product({ product: product() }, 'different-id'),
|
|
error => error.statusCode === 409 && error.code === 'PRODUCT_ID_MISMATCH'
|
|
);
|
|
});
|
|
|
|
test('maps server rows without exposing the large snapshot in list mode', () => {
|
|
const row = {
|
|
product_id: '1073252781818',
|
|
title: '圆桌测试商品',
|
|
product_snapshot_json: JSON.stringify(product()),
|
|
main_image_count: 16,
|
|
detail_image_count: 24,
|
|
sku_count: 66,
|
|
attribute_count: 49,
|
|
sync_version: 3,
|
|
};
|
|
|
|
const summary = mapProductRow(row, false);
|
|
const detail = mapProductRow(row, true);
|
|
|
|
assert.equal(summary.product, undefined);
|
|
assert.equal(summary.skuCount, 66);
|
|
assert.equal(detail.product.productId, '1073252781818');
|
|
assert.equal(detail.syncVersion, 3);
|
|
});
|
|
|
|
test('lists 1688 products with multi-status filtering and unfiltered status summary', async () => {
|
|
const originalQuery = planningDatabase.query;
|
|
const queries = [];
|
|
planningDatabase.query = async (sql, params = []) => {
|
|
queries.push({ sql, params });
|
|
if (sql.includes('information_schema.tables')) return [{ exists: 1 }];
|
|
if (sql.includes('COUNT(*) AS total') && !sql.includes('GROUP BY')) return [{ total: 2 }];
|
|
if (sql.includes('GROUP BY publish_status')) {
|
|
return [
|
|
{ publish_status: 'pending', total: 4, latest_updated_at: '2026-08-14 10:00:00' },
|
|
{ publish_status: 'ready', total: 2, latest_updated_at: '2026-08-14 11:00:00' },
|
|
{ publish_status: 'published', total: 3, latest_updated_at: '2026-08-13 12:00:00' },
|
|
];
|
|
}
|
|
return [{
|
|
product_id: '1073252781818',
|
|
title: '圆桌测试商品',
|
|
publish_status: 'ready',
|
|
sync_version: 2,
|
|
}];
|
|
};
|
|
|
|
try {
|
|
const result = await listAlibaba1688Products({
|
|
keyword: '圆桌',
|
|
importTag: '测款',
|
|
sourceType: 'product',
|
|
syncOperatorName: '香茗',
|
|
publishStatus: 'pending,ready',
|
|
page: 2,
|
|
pageSize: 10,
|
|
});
|
|
|
|
assert.equal(result.total, 2);
|
|
assert.equal(result.items.length, 1);
|
|
assert.equal(result.summary.pending, 4);
|
|
assert.equal(result.summary.ready, 2);
|
|
assert.equal(result.summary.published, 3);
|
|
assert.equal(result.summary.failed, 0);
|
|
assert.equal(result.latestUpdatedAt, '2026-08-14 11:00:00');
|
|
|
|
const countQuery = queries.find(query => query.sql.includes('COUNT(*) AS total') && !query.sql.includes('GROUP BY'));
|
|
assert.match(countQuery.sql, /publish_status IN \(\?, \?\)/);
|
|
assert.deepEqual(countQuery.params.slice(-2), ['pending', 'ready']);
|
|
|
|
const summaryQuery = queries.find(query => query.sql.includes('GROUP BY publish_status'));
|
|
assert.doesNotMatch(summaryQuery.sql, /publish_status IN/);
|
|
assert.deepEqual(summaryQuery.params.slice(-3), ['测款', 'product', '%香茗%']);
|
|
|
|
const listQuery = queries.find(query => query.sql.includes('ORDER BY updated_at DESC'));
|
|
assert.deepEqual(listQuery.params.slice(-4), ['pending', 'ready', 10, 10]);
|
|
} finally {
|
|
planningDatabase.query = originalQuery;
|
|
}
|
|
});
|
|
|
|
test('paginates 1688 products and applies common server-side filters', async () => {
|
|
const originalQuery = planningDatabase.query;
|
|
const calls = [];
|
|
planningDatabase.query = async (sql, params = []) => {
|
|
calls.push({ sql, params });
|
|
if (sql.includes('information_schema.tables')) return [{ exists: 1 }];
|
|
if (sql.includes('COUNT(*) AS total') && !sql.includes('GROUP BY')) return [{ total: 51 }];
|
|
if (sql.includes('GROUP BY publish_status')) return [];
|
|
return [{
|
|
product_id: '1073252781818',
|
|
title: '圆桌测试商品',
|
|
store_name: '测试家具厂',
|
|
category: '餐厅家具',
|
|
import_tag: '测款',
|
|
publish_status: 'pending',
|
|
sync_operator_name: '香茗',
|
|
source_type: 'store',
|
|
product_snapshot_json: JSON.stringify(product()),
|
|
}];
|
|
};
|
|
|
|
try {
|
|
const result = await listAlibaba1688Products({
|
|
page: 2,
|
|
pageSize: 50,
|
|
keyword: '桌',
|
|
importTag: '测款',
|
|
publishStatus: 'pending',
|
|
syncOperatorName: '香茗',
|
|
sourceType: 'store',
|
|
});
|
|
const countCall = calls.find(call => call.sql.includes('COUNT(*) AS total'));
|
|
const listCall = calls.find(call => call.sql.includes('ORDER BY updated_at DESC'));
|
|
|
|
assert.equal(result.page, 2);
|
|
assert.equal(result.pageSize, 50);
|
|
assert.equal(result.total, 51);
|
|
assert.equal(result.items[0].syncOperatorName, '香茗');
|
|
assert.match(countCall.sql, /category LIKE \?/);
|
|
assert.match(countCall.sql, /sync_operator_name LIKE \?/);
|
|
assert.match(countCall.sql, /source_type = \?/);
|
|
assert.deepEqual(countCall.params, [
|
|
'%桌%', '%桌%', '%桌%', '%桌%', '%桌%', '测款', 'store', '%香茗%', 'pending',
|
|
]);
|
|
assert.deepEqual(listCall.params.slice(-2), [50, 50]);
|
|
} finally {
|
|
planningDatabase.query = originalQuery;
|
|
}
|
|
});
|
|
|
|
test('replaces the complete server snapshot without resetting publish workflow state', async () => {
|
|
const originalQuery = planningDatabase.query;
|
|
const originalExecute = planningDatabase.execute;
|
|
const executed = [];
|
|
planningDatabase.query = async (sql) => {
|
|
if (sql.includes('information_schema.tables')) return [{ exists: 1 }];
|
|
return [{
|
|
product_id: '1073252781818',
|
|
title: '新标题',
|
|
product_snapshot_json: JSON.stringify(product({ title: '新标题' })),
|
|
sync_version: 3,
|
|
}];
|
|
};
|
|
planningDatabase.execute = async (sql, params) => {
|
|
executed.push({ sql, params });
|
|
return { affectedRows: 2 };
|
|
};
|
|
|
|
try {
|
|
const result = await replaceAlibaba1688Product(
|
|
'1073252781818',
|
|
{ product: product({ title: '新标题' }) },
|
|
{ apiKeyId: 'key-id', apiKeyName: 'desktop' }
|
|
);
|
|
assert.equal(result.operation, 'replaced');
|
|
assert.equal(result.item.title, '新标题');
|
|
assert.equal(executed.length, 1);
|
|
assert.equal(executed[0].params.length, 23);
|
|
assert.doesNotMatch(executed[0].sql, /publish_status = 'pending'/);
|
|
assert.doesNotMatch(executed[0].sql, /published_goods_json = NULL/);
|
|
assert.doesNotMatch(executed[0].sql, /last_published_at = NULL/);
|
|
assert.match(executed[0].sql, /sync_version = sync_version \+ 1/);
|
|
} finally {
|
|
planningDatabase.query = originalQuery;
|
|
planningDatabase.execute = originalExecute;
|
|
}
|
|
});
|