forked from lunanting/cangku
516 lines
18 KiB
JavaScript
516 lines
18 KiB
JavaScript
const planningDatabase = require('./planningDatabase');
|
|
|
|
const TAOBAO_PRODUCT_RECORDS_TABLE = 'taobao_product_records';
|
|
const APPEARANCE_RECOGNITION_CODE_COLUMN = 'appearance_recognition_code';
|
|
const APPEARANCE_RECOGNITION_CODE_LABEL = '外观识别编码';
|
|
const APPEARANCE_RECOGNITION_CODE_INDEX = 'idx_taobao_product_records_appearance_code';
|
|
const MAY_APPEARANCE_RECOGNITION_CODE_COLUMN = 'may_appearance_recognition_code';
|
|
const MAY_SKU_CODE_COLUMN = 'may_sku_code';
|
|
|
|
const TAOBAO_PRODUCT_RECORDS_STANDARD_COLUMNS = Object.freeze([
|
|
{ name: 'planning_date', label: '日期', type: 'VARCHAR(64)' },
|
|
{ name: 'category_level1', label: '一级类目', type: 'VARCHAR(255)' },
|
|
{ name: 'category_level2', label: '二级类目', type: 'VARCHAR(255)' },
|
|
{ name: 'category_level3', label: '三级类目', type: 'VARCHAR(255)' },
|
|
{ name: 'market_common_category_name', label: '市场共性分类名', type: 'VARCHAR(255)' },
|
|
{ name: APPEARANCE_RECOGNITION_CODE_COLUMN, label: APPEARANCE_RECOGNITION_CODE_LABEL, type: 'VARCHAR(160)' },
|
|
{ name: 'item_id', label: '商品ID', type: 'VARCHAR(100)' },
|
|
{ name: 'main_image_url', label: '主图', type: 'VARCHAR(1000)' },
|
|
{ name: 'title', label: '商品标题', type: 'VARCHAR(1000)' },
|
|
{ name: 'analysis_sales', label: '销量', type: 'VARCHAR(120)' },
|
|
{ name: 'lowest_list_price', label: '最低标价', type: 'VARCHAR(120)' },
|
|
{ name: 'product_material', label: '产品材质', type: 'VARCHAR(500)' },
|
|
{ name: 'core_material_name', label: '核心主材质', type: 'VARCHAR(255)' },
|
|
{ name: 'standard_product_type', label: '标准产品类型', type: 'VARCHAR(255)' },
|
|
{ name: 'core_product_type_name', label: '核心产品类型', type: 'VARCHAR(255)' },
|
|
{ name: 'market_category_name', label: '市场分类名', type: 'VARCHAR(500)' },
|
|
{ name: 'sku_price_range', label: 'SKU价格区间', type: 'VARCHAR(120)' },
|
|
{ name: 'actual_hand_price', label: '实际到手价', type: 'VARCHAR(120)' },
|
|
{ name: 'price_band', label: '价格带', type: 'VARCHAR(120)' },
|
|
{ name: 'price_band_rank', label: '价格带排名', type: 'VARCHAR(120)' },
|
|
{ name: 'shop_type', label: '店铺类型', type: 'VARCHAR(120)' },
|
|
{ name: 'shop_name', label: '店铺名称', type: 'VARCHAR(255)' },
|
|
{ name: 'search_keyword', label: '搜索关键词', type: 'VARCHAR(500)' },
|
|
{ name: 'product_keywords', label: '商品关键词', type: 'TEXT' },
|
|
{ name: 'industry_rank', label: '行业排名', type: 'VARCHAR(120)' },
|
|
{ name: 'paying_buyers', label: '支付买家数', type: 'VARCHAR(120)' },
|
|
{ name: 'visitors', label: '访客数', type: 'VARCHAR(120)' },
|
|
{ name: 'trend', label: '趋势', type: 'VARCHAR(120)' },
|
|
{ name: 'time_dimension', label: '时间维度', type: 'VARCHAR(120)' },
|
|
{ name: 'sales_amount', label: '销售额', type: 'VARCHAR(120)' },
|
|
{ name: 'browse_users', label: '浏览人数', type: 'VARCHAR(120)' },
|
|
{ name: 'search_users', label: '搜索人数', type: 'VARCHAR(120)' },
|
|
{ name: 'favorite_users', label: '收藏人数', type: 'VARCHAR(120)' },
|
|
{ name: 'cart_users', label: '加购人数', type: 'VARCHAR(120)' },
|
|
{ name: 'purchase_users', label: '购买人数', type: 'VARCHAR(120)' },
|
|
{ name: 'conversion_rate', label: '转化率', type: 'VARCHAR(120)' },
|
|
{ name: 'search_user_ratio', label: '搜索人数占比', type: 'VARCHAR(120)' },
|
|
{ name: 'cart_rate', label: '加购率', type: 'VARCHAR(120)' },
|
|
{ name: MAY_APPEARANCE_RECOGNITION_CODE_COLUMN, label: 'MAY-外观识别编码', type: 'VARCHAR(160)' },
|
|
{ name: 'may_sku_image_url', label: 'MAY-SKU图片', type: 'VARCHAR(1000)' },
|
|
{ name: MAY_SKU_CODE_COLUMN, label: 'MAY-SKU编码', type: 'VARCHAR(100)' },
|
|
{ name: 'may_sales_status', label: 'MAY-销售状态', type: 'VARCHAR(120)' },
|
|
{ name: 'may_core_material', label: 'MAY-核心材质', type: 'VARCHAR(255)' },
|
|
{ name: 'may_min_limit_price', label: 'MAY-最低限价', type: 'VARCHAR(120)' },
|
|
{ name: 'may_size', label: 'MAY-尺寸', type: 'VARCHAR(255)' },
|
|
{ name: 'may_color_name', label: 'MAY-颜色名称', type: 'VARCHAR(255)' },
|
|
{ name: 'may_performance_amount', label: 'MAY-30天销售额', type: 'VARCHAR(120)' },
|
|
{ name: 'may_goods_id_count', label: 'MAY-ID数量', type: 'VARCHAR(120)' },
|
|
{ name: 'may_covered_channels', label: 'MAY-ID覆盖的渠道', type: 'VARCHAR(500)' },
|
|
{ name: 'may_1688_sales_ratio', label: 'MAY-1688销售占比', type: 'VARCHAR(120)' },
|
|
{ name: 'may_taobao_sales_ratio', label: 'MAY-淘宝销售占比', type: 'VARCHAR(120)' },
|
|
{ name: 'may_tmall_sales_ratio', label: 'MAY-天猫销售占比', type: 'VARCHAR(120)' },
|
|
{ name: 'may_pdd_sales_ratio', label: 'MAY-拼多多销售占比', type: 'VARCHAR(120)' },
|
|
{ name: 'may_jd_sales_ratio', label: 'MAY-京东销售占比', type: 'VARCHAR(120)' },
|
|
{ name: 'may_douyin_sales_ratio', label: 'MAY-抖音销售占比', type: 'VARCHAR(120)' },
|
|
{ name: 'may_vip_sales_ratio', label: 'MAY-唯品会销售占比', type: 'VARCHAR(120)' },
|
|
{ name: 'may_other_channel_sales_ratio', label: 'MAY-其他渠道销售占比', type: 'VARCHAR(120)' },
|
|
{ name: 'may_promotion_fee_30d', label: 'MAY-近30天推广费用', type: 'VARCHAR(120)' },
|
|
{ name: 'may_d_style_efficiency', label: 'MAY-D款效', type: 'VARCHAR(120)' },
|
|
{ name: 'may_inventory_quantity', label: 'MAY-库存', type: 'VARCHAR(120)' },
|
|
{ name: 'may_real_company_inventory_quantity', label: 'MAY-真实公司库存', type: 'VARCHAR(120)' },
|
|
{ name: 'may_estimated_sales_days', label: 'MAY-预计销售时长', type: 'VARCHAR(120)' },
|
|
{ name: 'may_shipping_warehouse', label: 'MAY-发货仓库', type: 'VARCHAR(500)' },
|
|
{ name: 'may_inventory_scope_remark', label: 'MAY-库存备注', type: 'VARCHAR(255)' },
|
|
{ name: 'may_avg_daily_sales_30d', label: 'MAY-30天平均日销', type: 'VARCHAR(120)' },
|
|
{ name: 'may_avg_daily_sales_7d', label: 'MAY-7天日均销量', type: 'VARCHAR(120)' },
|
|
{ name: 'may_sales_growth_rate_15d', label: 'MAY-15天环比增长率', type: 'VARCHAR(120)' },
|
|
{ name: 'may_listing_duration_days', label: 'MAY-铺货时长(店铺铺货时间)', type: 'VARCHAR(120)' },
|
|
{ name: 'may_gross_margin_rate', label: 'MAY-销售毛利率', type: 'VARCHAR(120)' },
|
|
{ name: 'may_after_sales_ratio', label: 'MAY-售后占比', type: 'VARCHAR(120)' },
|
|
{ name: 'may_active_sales_day_ratio_30d', label: 'MAY-动销率(近 30 天有销量的天数占比)', type: 'VARCHAR(120)' },
|
|
]);
|
|
|
|
const TAOBAO_PRODUCT_RECORDS_STANDARD_INDEXES = Object.freeze([
|
|
{
|
|
name: APPEARANCE_RECOGNITION_CODE_INDEX,
|
|
columns: [APPEARANCE_RECOGNITION_CODE_COLUMN],
|
|
},
|
|
{
|
|
name: 'idx_taobao_product_records_may_appearance_code',
|
|
columns: [MAY_APPEARANCE_RECOGNITION_CODE_COLUMN],
|
|
},
|
|
{
|
|
name: 'idx_taobao_product_records_may_sku_code',
|
|
columns: [MAY_SKU_CODE_COLUMN],
|
|
},
|
|
]);
|
|
|
|
const LEGACY_APPEARANCE_CODE_COLUMNS = Object.freeze([
|
|
'similar_group_code',
|
|
'appearance_group_key',
|
|
'appearance_code',
|
|
]);
|
|
|
|
const JSON_APPEARANCE_CODE_COLUMNS = Object.freeze([
|
|
'analysis_json',
|
|
'normalized_item_json',
|
|
'raw_data_json',
|
|
'raw_response_json',
|
|
]);
|
|
|
|
const APPEARANCE_CODE_KEYS = Object.freeze([
|
|
'appearance_recognition_code',
|
|
'appearanceRecognitionCode',
|
|
'appearance_group_key',
|
|
'appearanceGroupKey',
|
|
'appearance_code',
|
|
'appearanceCode',
|
|
'外观识别编码',
|
|
'外观相似款组键',
|
|
'外观编码',
|
|
]);
|
|
|
|
const APPEARANCE_CODE_TEXT_MARKERS = Object.freeze([
|
|
'appearance_recognition_code',
|
|
'appearanceRecognitionCode',
|
|
'appearance_group_key',
|
|
'appearanceGroupKey',
|
|
'appearance_code',
|
|
'appearanceCode',
|
|
'外观识别编码',
|
|
'外观相似款组键',
|
|
'外观编码',
|
|
]);
|
|
|
|
let ensureState = {
|
|
ready: false,
|
|
pending: null,
|
|
};
|
|
|
|
function escapeIdentifier(identifier) {
|
|
return `\`${String(identifier || '').replace(/`/g, '``')}\``;
|
|
}
|
|
|
|
function escapeSqlComment(value) {
|
|
return String(value || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
}
|
|
|
|
function buildColumnDefinition(column) {
|
|
return `${escapeIdentifier(column.name)} ${column.type} NULL COMMENT '${escapeSqlComment(column.label)}'`;
|
|
}
|
|
|
|
function normalizeText(value) {
|
|
if (value === null || value === undefined) {
|
|
return '';
|
|
}
|
|
return String(value).replace(/\u00a0/g, ' ').trim();
|
|
}
|
|
|
|
function firstMeaningfulText(...values) {
|
|
for (const value of values) {
|
|
const normalized = normalizeText(value);
|
|
if (
|
|
normalized
|
|
&& !['—', '-', '--', '未配置', '暂无', '无', 'null', 'NULL', 'undefined'].includes(normalized)
|
|
) {
|
|
return normalized;
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function safeJsonParse(value) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
if (typeof value === 'object') {
|
|
return value;
|
|
}
|
|
try {
|
|
return JSON.parse(String(value));
|
|
} catch (_error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function findAppearanceCodeInJson(value, depth = 0, visited = new Set()) {
|
|
if (!value || depth > 6) {
|
|
return '';
|
|
}
|
|
|
|
if (typeof value !== 'object') {
|
|
return '';
|
|
}
|
|
|
|
if (visited.has(value)) {
|
|
return '';
|
|
}
|
|
visited.add(value);
|
|
|
|
if (Array.isArray(value)) {
|
|
for (const item of value.slice(0, 200)) {
|
|
const matched = findAppearanceCodeInJson(item, depth + 1, visited);
|
|
if (matched) {
|
|
return matched;
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
for (const key of APPEARANCE_CODE_KEYS) {
|
|
const matched = firstMeaningfulText(value[key]);
|
|
if (matched) {
|
|
return matched;
|
|
}
|
|
}
|
|
|
|
for (const item of Object.values(value)) {
|
|
const matched = findAppearanceCodeInJson(item, depth + 1, visited);
|
|
if (matched) {
|
|
return matched;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function resolveAppearanceCodeFromRow(row = {}, columnNames = new Set()) {
|
|
const directCandidates = LEGACY_APPEARANCE_CODE_COLUMNS
|
|
.filter((columnName) => columnNames.has(columnName))
|
|
.map((columnName) => row[columnName]);
|
|
const directCode = firstMeaningfulText(...directCandidates);
|
|
if (directCode) {
|
|
return directCode;
|
|
}
|
|
|
|
for (const columnName of JSON_APPEARANCE_CODE_COLUMNS) {
|
|
if (!columnNames.has(columnName)) {
|
|
continue;
|
|
}
|
|
const parsed = safeJsonParse(row[columnName]);
|
|
const matched = findAppearanceCodeInJson(parsed);
|
|
if (matched) {
|
|
return matched;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
async function getColumnNames(tableName = TAOBAO_PRODUCT_RECORDS_TABLE) {
|
|
const rows = await planningDatabase.query(
|
|
`SELECT column_name AS columnName
|
|
FROM information_schema.columns
|
|
WHERE table_schema = DATABASE()
|
|
AND table_name = ?
|
|
ORDER BY ordinal_position ASC`,
|
|
[tableName]
|
|
);
|
|
|
|
return rows.map((row) => row.columnName);
|
|
}
|
|
|
|
async function tableExists(tableName = TAOBAO_PRODUCT_RECORDS_TABLE) {
|
|
const rows = await planningDatabase.query(
|
|
`SELECT 1 AS ok
|
|
FROM information_schema.tables
|
|
WHERE table_schema = DATABASE()
|
|
AND table_name = ?
|
|
LIMIT 1`,
|
|
[tableName]
|
|
);
|
|
return rows.length > 0;
|
|
}
|
|
|
|
async function indexExists(tableName, indexName) {
|
|
const rows = await planningDatabase.query(
|
|
`SELECT 1 AS ok
|
|
FROM information_schema.statistics
|
|
WHERE table_schema = DATABASE()
|
|
AND table_name = ?
|
|
AND index_name = ?
|
|
LIMIT 1`,
|
|
[tableName, indexName]
|
|
);
|
|
return rows.length > 0;
|
|
}
|
|
|
|
async function ensureAppearanceColumn(options = {}) {
|
|
const includeIndexes = options.includeIndexes !== false;
|
|
if (!await tableExists(TAOBAO_PRODUCT_RECORDS_TABLE)) {
|
|
return {
|
|
tableExists: false,
|
|
columnAdded: false,
|
|
indexAdded: false,
|
|
addedColumns: [],
|
|
addedIndexes: [],
|
|
};
|
|
}
|
|
|
|
const columnNames = new Set(await getColumnNames(TAOBAO_PRODUCT_RECORDS_TABLE));
|
|
const addedColumns = [];
|
|
const addedIndexes = [];
|
|
|
|
for (const column of TAOBAO_PRODUCT_RECORDS_STANDARD_COLUMNS) {
|
|
if (columnNames.has(column.name)) {
|
|
continue;
|
|
}
|
|
await planningDatabase.execute(
|
|
`ALTER TABLE ${escapeIdentifier(TAOBAO_PRODUCT_RECORDS_TABLE)}
|
|
ADD COLUMN ${buildColumnDefinition(column)}`
|
|
);
|
|
columnNames.add(column.name);
|
|
addedColumns.push(column.name);
|
|
}
|
|
|
|
if (includeIndexes) {
|
|
for (const indexDefinition of TAOBAO_PRODUCT_RECORDS_STANDARD_INDEXES) {
|
|
if (indexDefinition.columns.some((columnName) => !columnNames.has(columnName))) {
|
|
continue;
|
|
}
|
|
if (await indexExists(TAOBAO_PRODUCT_RECORDS_TABLE, indexDefinition.name)) {
|
|
continue;
|
|
}
|
|
await planningDatabase.execute(
|
|
`ALTER TABLE ${escapeIdentifier(TAOBAO_PRODUCT_RECORDS_TABLE)}
|
|
ADD KEY ${escapeIdentifier(indexDefinition.name)} (${indexDefinition.columns.map((columnName) => escapeIdentifier(columnName)).join(', ')})`
|
|
);
|
|
addedIndexes.push(indexDefinition.name);
|
|
}
|
|
}
|
|
|
|
return {
|
|
tableExists: true,
|
|
columnAdded: addedColumns.length > 0,
|
|
indexAdded: addedIndexes.length > 0,
|
|
addedColumns,
|
|
addedIndexes,
|
|
};
|
|
}
|
|
|
|
async function backfillAppearanceCodes() {
|
|
const columnNames = new Set(await getColumnNames(TAOBAO_PRODUCT_RECORDS_TABLE));
|
|
if (!columnNames.has(APPEARANCE_RECOGNITION_CODE_COLUMN)) {
|
|
return 0;
|
|
}
|
|
|
|
const directSourceColumns = LEGACY_APPEARANCE_CODE_COLUMNS
|
|
.filter((columnName) => columnNames.has(columnName));
|
|
let updatedCount = 0;
|
|
|
|
if (directSourceColumns.length > 0) {
|
|
const directExpression = directSourceColumns
|
|
.map((columnName) => `NULLIF(TRIM(${escapeIdentifier(columnName)}), '')`)
|
|
.join(', ');
|
|
const directResult = await planningDatabase.execute(
|
|
`UPDATE ${escapeIdentifier(TAOBAO_PRODUCT_RECORDS_TABLE)}
|
|
SET ${escapeIdentifier(APPEARANCE_RECOGNITION_CODE_COLUMN)} = COALESCE(${directExpression})
|
|
WHERE NULLIF(TRIM(${escapeIdentifier(APPEARANCE_RECOGNITION_CODE_COLUMN)}), '') IS NULL
|
|
AND COALESCE(${directExpression}) IS NOT NULL`
|
|
);
|
|
updatedCount += Number(directResult?.affectedRows || 0);
|
|
}
|
|
|
|
if (directSourceColumns.includes('similar_group_code')) {
|
|
return updatedCount;
|
|
}
|
|
|
|
const selectColumns = [
|
|
'id',
|
|
...LEGACY_APPEARANCE_CODE_COLUMNS,
|
|
...JSON_APPEARANCE_CODE_COLUMNS,
|
|
].filter((columnName, index, list) => columnNames.has(columnName) && list.indexOf(columnName) === index);
|
|
|
|
if (selectColumns.length <= 1) {
|
|
return 0;
|
|
}
|
|
|
|
const candidateConditions = [];
|
|
const candidateParams = [];
|
|
|
|
LEGACY_APPEARANCE_CODE_COLUMNS.forEach((columnName) => {
|
|
if (columnNames.has(columnName)) {
|
|
candidateConditions.push(`NULLIF(TRIM(${escapeIdentifier(columnName)}), '') IS NOT NULL`);
|
|
}
|
|
});
|
|
|
|
JSON_APPEARANCE_CODE_COLUMNS.forEach((columnName) => {
|
|
if (!columnNames.has(columnName)) {
|
|
return;
|
|
}
|
|
APPEARANCE_CODE_TEXT_MARKERS.forEach((marker) => {
|
|
candidateConditions.push(`${escapeIdentifier(columnName)} LIKE ?`);
|
|
candidateParams.push(`%${marker}%`);
|
|
});
|
|
});
|
|
|
|
if (candidateConditions.length === 0) {
|
|
return 0;
|
|
}
|
|
|
|
let lastId = 0;
|
|
const batchSize = 500;
|
|
|
|
while (true) {
|
|
const rows = await planningDatabase.query(
|
|
`SELECT ${selectColumns.map((columnName) => escapeIdentifier(columnName)).join(', ')}
|
|
FROM ${escapeIdentifier(TAOBAO_PRODUCT_RECORDS_TABLE)}
|
|
WHERE id > ?
|
|
AND NULLIF(TRIM(${escapeIdentifier(APPEARANCE_RECOGNITION_CODE_COLUMN)}), '') IS NULL
|
|
AND (${candidateConditions.join(' OR ')})
|
|
ORDER BY id ASC
|
|
LIMIT ?`,
|
|
[lastId, ...candidateParams, batchSize]
|
|
);
|
|
|
|
if (rows.length === 0) {
|
|
break;
|
|
}
|
|
|
|
const updates = rows
|
|
.map((row) => ({
|
|
id: Number(row.id),
|
|
code: resolveAppearanceCodeFromRow(row, columnNames),
|
|
}))
|
|
.filter((item) => Number.isFinite(item.id) && item.code);
|
|
|
|
for (const updateChunk of chunkArray(updates, 100)) {
|
|
const caseFragments = [];
|
|
const params = [];
|
|
const ids = [];
|
|
|
|
updateChunk.forEach((item) => {
|
|
caseFragments.push('WHEN ? THEN ?');
|
|
params.push(item.id, item.code);
|
|
ids.push(item.id);
|
|
});
|
|
|
|
if (ids.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
await planningDatabase.execute(
|
|
`UPDATE ${escapeIdentifier(TAOBAO_PRODUCT_RECORDS_TABLE)}
|
|
SET ${escapeIdentifier(APPEARANCE_RECOGNITION_CODE_COLUMN)} = CASE id ${caseFragments.join(' ')} ELSE ${escapeIdentifier(APPEARANCE_RECOGNITION_CODE_COLUMN)} END
|
|
WHERE id IN (${ids.map(() => '?').join(', ')})`,
|
|
[...params, ...ids]
|
|
);
|
|
updatedCount += ids.length;
|
|
}
|
|
|
|
lastId = Number(rows[rows.length - 1]?.id || lastId);
|
|
if (rows.length < batchSize) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return updatedCount;
|
|
}
|
|
|
|
function chunkArray(list, size) {
|
|
const safeList = Array.isArray(list) ? list : [];
|
|
const chunkSize = Math.max(Number(size) || 1, 1);
|
|
const chunks = [];
|
|
for (let index = 0; index < safeList.length; index += chunkSize) {
|
|
chunks.push(safeList.slice(index, index + chunkSize));
|
|
}
|
|
return chunks;
|
|
}
|
|
|
|
async function ensureTaobaoProductRecordsAppearanceCodeColumn(options = {}) {
|
|
const forceRefresh = options.forceRefresh === true;
|
|
if (!forceRefresh && ensureState.ready) {
|
|
return {
|
|
tableExists: true,
|
|
columnAdded: false,
|
|
indexAdded: false,
|
|
backfilledCount: 0,
|
|
cached: true,
|
|
};
|
|
}
|
|
if (!forceRefresh && ensureState.pending) {
|
|
return ensureState.pending;
|
|
}
|
|
|
|
ensureState.pending = (async () => {
|
|
const columnResult = await ensureAppearanceColumn(options);
|
|
const shouldBackfillExistingCodes = options.backfillExistingCodes === true;
|
|
const backfilledCount = columnResult.tableExists && shouldBackfillExistingCodes
|
|
? await backfillAppearanceCodes()
|
|
: 0;
|
|
ensureState.ready = columnResult.tableExists;
|
|
return {
|
|
...columnResult,
|
|
backfilledCount,
|
|
cached: false,
|
|
};
|
|
})();
|
|
|
|
try {
|
|
return await ensureState.pending;
|
|
} finally {
|
|
ensureState.pending = null;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
TAOBAO_PRODUCT_RECORDS_TABLE,
|
|
TAOBAO_PRODUCT_RECORDS_STANDARD_COLUMNS,
|
|
APPEARANCE_RECOGNITION_CODE_COLUMN,
|
|
APPEARANCE_RECOGNITION_CODE_LABEL,
|
|
MAY_APPEARANCE_RECOGNITION_CODE_COLUMN,
|
|
MAY_SKU_CODE_COLUMN,
|
|
ensureTaobaoProductRecordsAppearanceCodeColumn,
|
|
};
|