cangku/backend/src/services/pddSkuDimensionSnapshotSync.js

509 lines
23 KiB
JavaScript

'use strict';
const dayjs = require('dayjs');
const cloudDb = require('../config/cloudDatabase');
const planningDatabase = require('./planningDatabase');
const { ensurePlanningPddSkuDimensionSchema } = require('./planningSupportSchema');
const localDb = planningDatabase;
const SPU_TABLE = 'op_pdd_sku_dimension_spu_daily';
const SKU_TABLE = 'op_pdd_sku_dimension_sku_daily';
const GOODS_TABLE = 'op_pdd_sku_dimension_goods_daily';
const PDD_DATA_GOODS_SCHEMA = 'dbo';
const PDD_DATA_GOODS_TABLE = 'pdd_DataGoods';
const PDD_SPREAD_GOODS_TABLE = 'pdd_ReportGoodsSpread';
const PDD_EVALUATION_TABLE = 'pdd_Evaluation';
const PDD_JST_SKU_TABLE_FULL_NAME = 'dbo.jst_sku';
const PDD_TARGET_TABLE_FULL_NAME = 'JSDataMiddlePlatform.dbo.op_TargetManagement';
const DEFAULT_CHUNK_SIZE = Math.max(parseInt(process.env.PDD_SKU_DIMENSION_SNAPSHOT_CHUNK_SIZE || '500', 10), 100);
const DEFAULT_REQUEST_TIMEOUT_MS = Math.max(parseInt(process.env.PDD_SKU_DIMENSION_SNAPSHOT_TIMEOUT_MS || '1200000', 10), 120000);
const DWS_NON_VIRTUAL_ORDER_FILTER_SQL = `
AND ISNULL(TRY_CONVERT(DECIMAL(18, 2), d.[ps_payment_classify_isYes]), 0) = 0
AND ISNULL(TRY_CONVERT(DECIMAL(18, 2), d.[refund_amount_isZc]), 0) = 0
AND ISNULL(TRY_CONVERT(DECIMAL(18, 2), d.[refund_sale_compensate_isZc]), 0) = 0
AND ISNULL(TRY_CONVERT(DECIMAL(18, 2), d.[oo_mix_zc_kd]), 0) = 0
AND ISNULL(TRY_CONVERT(DECIMAL(18, 2), d.[fr_zc_dynamic_express_cost]), 0) = 0
AND ISNULL(TRY_CONVERT(DECIMAL(18, 2), d.[fr_zcd_pj_fdf_express_cost]), 0) = 0`;
function normalizeDate(value) {
if (!value) return null;
return dayjs(value).format('YYYY-MM-DD');
}
function toNumber(value, digits = 2) {
const parsed = Number(value || 0);
if (!Number.isFinite(parsed)) return 0;
return Number(parsed.toFixed(digits));
}
function joinDistinct(values) {
return Array.from(new Set((values || []).map((item) => String(item || '').trim()).filter(Boolean))).sort().join('、');
}
function buildInsertSql(tableName, columns, rowCount, keyColumns) {
const columnSql = columns.map((column) => `\`${column}\``).join(', ');
const rowPlaceholder = `(${columns.map(() => '?').join(', ')})`;
const valuesSql = new Array(rowCount).fill(rowPlaceholder).join(', ');
const updateSql = columns
.filter((column) => !keyColumns.includes(column))
.map((column) => `\`${column}\` = VALUES(\`${column}\`)`)
.join(', ');
return `
INSERT INTO \`${tableName}\` (${columnSql})
VALUES ${valuesSql}
ON DUPLICATE KEY UPDATE ${updateSql}
`;
}
async function upsertRows(tableName, columns, rows, keyColumns, chunkSize = DEFAULT_CHUNK_SIZE) {
if (!Array.isArray(rows) || rows.length === 0) {
return 0;
}
let affected = 0;
for (let index = 0; index < rows.length; index += chunkSize) {
const chunk = rows.slice(index, index + chunkSize);
const sql = buildInsertSql(tableName, columns, chunk.length, keyColumns);
const params = [];
chunk.forEach((row) => {
columns.forEach((column) => params.push(row[column] ?? null));
});
await localDb.query(sql, params);
affected += chunk.length;
}
return affected;
}
async function getLatestPddDashboardBizDate() {
const tableNames = [ PDD_DATA_GOODS_TABLE, PDD_SPREAD_GOODS_TABLE, PDD_EVALUATION_TABLE ];
const dates = [];
for (const tableName of tableNames) {
const rows = await cloudDb.query(
`
SELECT MAX(TRY_CONVERT(date, [createTime])) AS latestBizDate
FROM [${PDD_DATA_GOODS_SCHEMA}].[${tableName}]
WHERE TRY_CONVERT(date, [createTime]) IS NOT NULL
`,
{},
{ requestTimeout: DEFAULT_REQUEST_TIMEOUT_MS }
);
const value = normalizeDate(rows?.[0]?.latestBizDate || null);
if (value) dates.push(value);
}
if (dates.length === 0) {
return null;
}
return dates.reduce((minDate, currentDate) => (dayjs(currentDate).isBefore(dayjs(minDate)) ? currentDate : minDate));
}
async function fetchGoodsRowsFromSource({ startDate, endDate, requestTimeout = DEFAULT_REQUEST_TIMEOUT_MS }) {
const sql = `
SET NOCOUNT ON;
IF OBJECT_ID('tempdb..#goods_scope') IS NOT NULL DROP TABLE #goods_scope;
IF OBJECT_ID('tempdb..#goods_sku_map') IS NOT NULL DROP TABLE #goods_sku_map;
IF OBJECT_ID('tempdb..#goods_base') IS NOT NULL DROP TABLE #goods_base;
IF OBJECT_ID('tempdb..#spread_agg') IS NOT NULL DROP TABLE #spread_agg;
IF OBJECT_ID('tempdb..#review_agg') IS NOT NULL DROP TABLE #review_agg;
IF OBJECT_ID('tempdb..#dws_agg') IS NOT NULL DROP TABLE #dws_agg;
SELECT DISTINCT
LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) AS [goodsId]
INTO #goods_scope
FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g
WHERE g.[goodsId] IS NOT NULL
AND LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) <> ''
AND g.[createTime] >= @startDateTime
AND g.[createTime] < @endDateExclusive;
CREATE UNIQUE CLUSTERED INDEX IX_goods_scope_goodsId ON #goods_scope([goodsId]);
WITH goods_sku_rank AS (
SELECT
gs.[goodsId],
LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), s.[i_id]))) AS [spu],
LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), s.[other_7]))) AS [skuCode],
ROW_NUMBER() OVER (
PARTITION BY gs.[goodsId]
ORDER BY
SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[real_performance]), 0)) DESC,
SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[goods_count]), 0)) DESC,
LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), s.[other_7]))) ASC
) AS [rn]
FROM #goods_scope gs
INNER JOIN [JSDataWarehouse].[dws].[dws_sn_sales_full_info_di] d
ON d.[item_id] = CONVERT(VARCHAR(50), gs.[goodsId])
INNER JOIN ${PDD_JST_SKU_TABLE_FULL_NAME} s
ON LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), s.[sku_id]))) = LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), d.[skuoid])))
WHERE d.[stat_date] BETWEEN TRY_CONVERT(DATE, @startDate) AND TRY_CONVERT(DATE, @endDate)
AND ISNULL(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), d.[platform]))), N'') IN (N'Pinduoduo', N'拼多多')
AND NULLIF(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), d.[skuoid]))), N'') IS NOT NULL
AND ISNULL(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), s.[other_5]))), N'') = N'销售'
AND NULLIF(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), s.[i_id]))), N'') IS NOT NULL
AND NULLIF(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), s.[other_7]))), N'') IS NOT NULL
${DWS_NON_VIRTUAL_ORDER_FILTER_SQL}
GROUP BY
gs.[goodsId],
LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), s.[i_id]))),
LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), s.[other_7])))
)
SELECT
[goodsId],
[spu],
[skuCode]
INTO #goods_sku_map
FROM goods_sku_rank
WHERE [rn] = 1;
CREATE UNIQUE CLUSTERED INDEX IX_goods_sku_map_goodsId ON #goods_sku_map([goodsId]);
SELECT
LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) AS [goodsId],
MAX(gsm.[spu]) AS [spu],
MAX(gsm.[skuCode]) AS [skuCode],
MAX(TRY_CONVERT(NVARCHAR(500), g.[goodsName])) AS [goodsName],
MAX(TRY_CONVERT(NVARCHAR(255), g.[shopName])) AS [shopName],
MAX(TRY_CONVERT(NVARCHAR(1000), g.[hdThumbUrl])) AS [hdThumbUrl],
MAX(tm.[ider]) AS [idOwner],
MAX(tm.[dept]) AS [ownerDept],
CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[goodsUv]), 0)) AS DECIMAL(38, 2)) AS [goodsUv],
CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[goodsFavCnt]), 0)) AS DECIMAL(38, 2)) AS [goodsFavCnt],
CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[imprUsrCnt]), 0)) AS DECIMAL(38, 2)) AS [imprUsrCnt],
CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrUsrCnt]), 0)) AS DECIMAL(38, 2)) AS [payOrdrUsrCnt],
CAST(ROUND(SUM(
ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[goodsUv]), 0) * (
ISNULL(TRY_CONVERT(DECIMAL(38, 6), REPLACE(TRY_CONVERT(NVARCHAR(50), g.[ordrVstrRto]), '%', '')), 0)
/ CASE
WHEN CHARINDEX('%', TRY_CONVERT(NVARCHAR(50), g.[ordrVstrRto])) > 0 THEN 100.0
WHEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[ordrVstrRto]), 0) > 1 THEN 100.0
ELSE 1.0
END
)
), 0) AS BIGINT) AS [orderUserCount]
INTO #goods_base
FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g
LEFT JOIN ${PDD_TARGET_TABLE_FULL_NAME} tm
ON tm.[shopproid] = LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId])))
INNER JOIN #goods_sku_map gsm
ON gsm.[goodsId] = LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId])))
WHERE g.[goodsId] IS NOT NULL
AND LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) <> ''
AND g.[createTime] >= @startDateTime
AND g.[createTime] < @endDateExclusive
GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId])));
CREATE UNIQUE CLUSTERED INDEX IX_goods_base_goodsId ON #goods_base([goodsId]);
SELECT
gb.[goodsId],
CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[spend]), 0)) / 1000.0 AS DECIMAL(38, 2)) AS [spend],
CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[click]), 0)) AS DECIMAL(38, 2)) AS [spreadClick],
CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[impression]), 0)) AS DECIMAL(38, 2)) AS [spreadImpression],
CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[orderNum]), 0)) AS DECIMAL(38, 2)) AS [spreadOrderNum],
CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[multiGoalInquiryNum]), 0)) AS DECIMAL(38, 2)) AS [inquiryNum],
CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[multiGoalGoodsFavNum]), 0)) AS DECIMAL(38, 2)) AS [goodsFavNum]
INTO #spread_agg
FROM #goods_base gb
INNER JOIN [${PDD_DATA_GOODS_SCHEMA}].[${PDD_SPREAD_GOODS_TABLE}] s
ON LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), s.[goodsId]))) = gb.[goodsId]
WHERE s.[createTime] >= @startDateTime
AND s.[createTime] < @endDateExclusive
GROUP BY gb.[goodsId];
CREATE UNIQUE CLUSTERED INDEX IX_spread_agg_goodsId ON #spread_agg([goodsId]);
SELECT
gb.[goodsId],
COUNT(1) AS [badReviewCount]
INTO #review_agg
FROM #goods_base gb
INNER JOIN [${PDD_DATA_GOODS_SCHEMA}].[${PDD_EVALUATION_TABLE}] e
ON LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), e.[goodsId]))) = gb.[goodsId]
WHERE e.[createTime] >= @startDateTime
AND e.[createTime] < @endDateExclusive
AND TRY_CONVERT(INT, e.[descScore]) IN (1, 2, 3)
GROUP BY gb.[goodsId];
CREATE UNIQUE CLUSTERED INDEX IX_review_agg_goodsId ON #review_agg([goodsId]);
SELECT
gb.[goodsId],
CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[goods_count]), 0)) AS DECIMAL(38, 2)) AS [payOrdrGoodsQty],
CAST(COUNT(DISTINCT CASE
WHEN NULLIF(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), d.[sourceno]))), N'') IS NOT NULL
THEN LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), d.[sourceno])))
ELSE NULL
END) AS DECIMAL(38, 2)) AS [orderNum],
CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[real_conclude_fee]), 0)) AS DECIMAL(38, 2)) AS [realConcludeFee],
CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[real_performance]), 0)) AS DECIMAL(38, 2)) AS [payOrdrAmt],
CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[after_sale_amount]), 0)) AS DECIMAL(38, 2)) AS [afterSaleAmount],
CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[refund_count]), 0)) AS DECIMAL(38, 2)) AS [afterSaleCount]
INTO #dws_agg
FROM [JSDataWarehouse].[dws].[dws_sn_sales_full_info_di] d
INNER JOIN #goods_base gb
ON d.[item_id] = CONVERT(VARCHAR(50), gb.[goodsId])
WHERE d.[stat_date] BETWEEN TRY_CONVERT(DATE, @startDate) AND TRY_CONVERT(DATE, @endDate)
AND ISNULL(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), d.[platform]))), N'') IN (N'Pinduoduo', N'拼多多')
${DWS_NON_VIRTUAL_ORDER_FILTER_SQL}
GROUP BY gb.[goodsId];
CREATE UNIQUE CLUSTERED INDEX IX_dws_agg_goodsId ON #dws_agg([goodsId]);
SELECT
@endDate AS [statDate],
@startDate AS [startDate],
@endDate AS [endDate],
gb.[spu],
gb.[skuCode],
gb.[goodsId],
gb.[goodsName],
gb.[hdThumbUrl],
gb.[idOwner],
gb.[ownerDept],
gb.[shopName],
gb.[goodsUv],
ISNULL(sa.[inquiryNum], 0) AS [inquiryNum],
gb.[goodsFavCnt],
ISNULL(sa.[goodsFavNum], 0) AS [goodsFavNum],
ISNULL(gb.[orderUserCount], 0) AS [orderUserCount],
CAST(CASE WHEN ISNULL(sa.[spreadImpression], 0) = 0 THEN 0 ELSE ISNULL(sa.[spreadClick], 0) * 100.0 / NULLIF(sa.[spreadImpression], 0) END AS DECIMAL(38, 2)) AS [ctr],
CAST(CASE WHEN ISNULL(sa.[spreadClick], 0) = 0 THEN 0 ELSE ISNULL(sa.[spreadOrderNum], 0) * 100.0 / NULLIF(sa.[spreadClick], 0) END AS DECIMAL(38, 2)) AS [clickConversionRate],
CAST(CASE WHEN ISNULL(gb.[goodsUv], 0) = 0 THEN 0 ELSE ISNULL(gb.[payOrdrUsrCnt], 0) * 100.0 / NULLIF(gb.[goodsUv], 0) END AS DECIMAL(38, 2)) AS [goodsVcr],
CAST(CASE WHEN ISNULL(gb.[goodsUv], 0) = 0 THEN 0 ELSE ISNULL(gb.[orderUserCount], 0) * 100.0 / NULLIF(gb.[goodsUv], 0) END AS DECIMAL(38, 2)) AS [ordrVstrRto],
CAST(CASE WHEN ISNULL(gb.[orderUserCount], 0) = 0 THEN 0 ELSE ISNULL(gb.[payOrdrUsrCnt], 0) * 100.0 / NULLIF(gb.[orderUserCount], 0) END AS DECIMAL(38, 2)) AS [payOrdrRto],
gb.[imprUsrCnt],
ISNULL(da.[payOrdrGoodsQty], 0) AS [payOrdrGoodsQty],
ISNULL(da.[orderNum], 0) AS [orderNum],
ISNULL(da.[realConcludeFee], 0) AS [realConcludeFee],
ISNULL(da.[payOrdrAmt], 0) AS [payOrdrAmt],
gb.[payOrdrUsrCnt],
CAST(ROUND(ISNULL(gb.[orderUserCount], 0) - ISNULL(gb.[payOrdrUsrCnt], 0), 0) AS BIGINT) AS [unpaidUserCount],
CAST(CASE WHEN ISNULL(gb.[payOrdrUsrCnt], 0) = 0 THEN 0 ELSE ISNULL(da.[payOrdrAmt], 0) / NULLIF(gb.[payOrdrUsrCnt], 0) END AS DECIMAL(38, 2)) AS [customerUnitPrice],
CAST(CASE WHEN ISNULL(da.[payOrdrGoodsQty], 0) = 0 THEN 0 ELSE ISNULL(da.[payOrdrAmt], 0) / NULLIF(da.[payOrdrGoodsQty], 0) END AS DECIMAL(38, 2)) AS [pieceUnitPrice],
ISNULL(sa.[spend], 0) AS [spend],
CAST(CASE WHEN ISNULL(da.[payOrdrAmt], 0) = 0 THEN 0 ELSE ISNULL(da.[realConcludeFee], 0) * 100.0 / NULLIF(da.[payOrdrAmt], 0) END AS DECIMAL(38, 2)) AS [feeRatio],
CAST(CASE WHEN ISNULL(da.[payOrdrAmt], 0) = 0 THEN 0 ELSE ISNULL(da.[afterSaleAmount], 0) * 100.0 / NULLIF(da.[payOrdrAmt], 0) END AS DECIMAL(38, 2)) AS [afterSaleRate],
ISNULL(da.[afterSaleCount], 0) AS [afterSaleCount],
ISNULL(ra.[badReviewCount], 0) AS [badReviewCount],
CAST(0 AS TINYINT) AS [isPriceBreak]
FROM #goods_base gb
LEFT JOIN #spread_agg sa ON sa.[goodsId] = gb.[goodsId]
LEFT JOIN #dws_agg da ON da.[goodsId] = gb.[goodsId]
LEFT JOIN #review_agg ra ON ra.[goodsId] = gb.[goodsId]
ORDER BY gb.[spu] ASC, gb.[skuCode] ASC, gb.[goodsId] ASC;
`;
return cloudDb.query(sql, {
startDate,
endDate,
startDateTime: `${startDate} 00:00:00`,
endDateExclusive: dayjs(endDate).add(1, 'day').format('YYYY-MM-DD 00:00:00'),
}, {
requestTimeout,
});
}
function aggregateRows(goodsRows) {
const spuMap = new Map();
const skuMap = new Map();
function touch(map, key, base) {
if (!map.has(key)) {
map.set(key, {
...base,
goodsIdCount: 0,
activeGoodsIdCount: 0,
payOrdrAmt: 0,
goodsUv: 0,
orderNum: 0,
payOrdrUsrCnt: 0,
payOrdrGoodsQty: 0,
spend: 0,
realConcludeFee: 0,
afterSaleAmount: 0,
spendClick: 0,
spendImpression: 0,
shopNames: [],
idOwners: [],
ownerDepts: [],
isPriceBreak: 0,
});
}
return map.get(key);
}
for (const row of goodsRows) {
const statDate = normalizeDate(row.statDate);
const startDate = normalizeDate(row.startDate);
const endDate = normalizeDate(row.endDate);
const spu = String(row.spu || '').trim();
const skuCode = String(row.skuCode || '').trim();
const activeFlag = toNumber(row.payOrdrAmt) > 0 || toNumber(row.payOrdrGoodsQty) > 0 ? 1 : 0;
const spuAgg = touch(spuMap, `${statDate}__${spu}`, { statDate, startDate, endDate, spu });
const skuAgg = touch(skuMap, `${statDate}__${spu}__${skuCode}`, { statDate, startDate, endDate, spu, skuCode });
[ spuAgg, skuAgg ].forEach((agg) => {
agg.goodsIdCount += 1;
agg.activeGoodsIdCount += activeFlag;
agg.payOrdrAmt += toNumber(row.payOrdrAmt);
agg.goodsUv += toNumber(row.goodsUv);
agg.orderNum += toNumber(row.orderNum);
agg.payOrdrUsrCnt += toNumber(row.payOrdrUsrCnt);
agg.payOrdrGoodsQty += toNumber(row.payOrdrGoodsQty);
agg.spend += toNumber(row.spend);
agg.realConcludeFee += toNumber(row.realConcludeFee);
agg.afterSaleAmount += toNumber(row.afterSaleAmount);
agg.spendClick += toNumber(row.ctr) * toNumber(row.goodsUv);
agg.spendImpression += toNumber(row.goodsUv);
agg.isPriceBreak = agg.isPriceBreak || Number(row.isPriceBreak || 0) === 1 ? 1 : 0;
agg.shopNames.push(row.shopName);
agg.idOwners.push(row.idOwner);
agg.ownerDepts.push(row.ownerDept);
});
}
function finalize(agg, withSkuCode) {
return {
statDate: agg.statDate,
startDate: agg.startDate,
endDate: agg.endDate,
spu: agg.spu,
...(withSkuCode ? { skuCode: agg.skuCode } : {}),
goodsIdCount: agg.goodsIdCount,
activeGoodsIdCount: agg.activeGoodsIdCount,
skuListingActiveRate: agg.goodsIdCount > 0 ? toNumber((agg.activeGoodsIdCount * 100) / agg.goodsIdCount) : 0,
skuIdEfficiency: agg.goodsIdCount > 0 ? toNumber(agg.payOrdrAmt / agg.goodsIdCount) : 0,
isPriceBreak: agg.isPriceBreak ? 1 : 0,
payOrdrAmt: toNumber(agg.payOrdrAmt),
feeRatio: agg.payOrdrAmt > 0 ? toNumber((agg.realConcludeFee * 100) / agg.payOrdrAmt) : 0,
afterSaleRate: agg.payOrdrAmt > 0 ? toNumber((agg.afterSaleAmount * 100) / agg.payOrdrAmt) : 0,
goodsUv: toNumber(agg.goodsUv),
orderNum: toNumber(agg.orderNum),
payOrdrUsrCnt: toNumber(agg.payOrdrUsrCnt),
payOrdrGoodsQty: toNumber(agg.payOrdrGoodsQty),
ctr: agg.goodsUv > 0 ? toNumber(agg.spendClick / agg.goodsUv) : 0,
goodsVcr: agg.goodsUv > 0 ? toNumber((agg.payOrdrUsrCnt * 100) / agg.goodsUv) : 0,
customerUnitPrice: agg.payOrdrUsrCnt > 0 ? toNumber(agg.payOrdrAmt / agg.payOrdrUsrCnt) : 0,
pieceUnitPrice: agg.payOrdrGoodsQty > 0 ? toNumber(agg.payOrdrAmt / agg.payOrdrGoodsQty) : 0,
spend: toNumber(agg.spend),
shopNameList: joinDistinct(agg.shopNames),
idOwnerList: joinDistinct(agg.idOwners),
ownerDeptList: joinDistinct(agg.ownerDepts),
};
}
return {
spuRows: Array.from(spuMap.values()).map((item) => finalize(item, false)),
skuRows: Array.from(skuMap.values()).map((item) => finalize(item, true)),
};
}
async function replaceSnapshotRows({ statDate, goodsRows, spuRows, skuRows }) {
await localDb.query(`DELETE FROM \`${SPU_TABLE}\` WHERE \`statDate\` = ?`, [ statDate ]);
await localDb.query(`DELETE FROM \`${SKU_TABLE}\` WHERE \`statDate\` = ?`, [ statDate ]);
await localDb.query(`DELETE FROM \`${GOODS_TABLE}\` WHERE \`statDate\` = ?`, [ statDate ]);
const goodsColumns = [
'statDate', 'startDate', 'endDate', 'spu', 'skuCode', 'goodsId', 'goodsName', 'hdThumbUrl', 'idOwner', 'ownerDept',
'shopName', 'goodsUv', 'inquiryNum', 'goodsFavCnt', 'goodsFavNum', 'orderUserCount', 'ctr', 'clickConversionRate',
'goodsVcr', 'ordrVstrRto', 'payOrdrRto', 'imprUsrCnt', 'unpaidUserCount', 'payOrdrAmt', 'orderNum', 'payOrdrUsrCnt',
'payOrdrGoodsQty', 'customerUnitPrice', 'pieceUnitPrice', 'spend', 'feeRatio', 'afterSaleRate', 'afterSaleCount',
'badReviewCount', 'isPriceBreak',
];
const skuColumns = [
'statDate', 'startDate', 'endDate', 'spu', 'skuCode', 'goodsIdCount', 'activeGoodsIdCount', 'skuListingActiveRate',
'skuIdEfficiency', 'isPriceBreak', 'payOrdrAmt', 'feeRatio', 'afterSaleRate', 'goodsUv', 'orderNum', 'payOrdrUsrCnt',
'payOrdrGoodsQty', 'ctr', 'goodsVcr', 'customerUnitPrice', 'pieceUnitPrice', 'spend', 'shopNameList', 'idOwnerList',
'ownerDeptList',
];
const spuColumns = [
'statDate', 'startDate', 'endDate', 'spu', 'goodsIdCount', 'activeGoodsIdCount', 'skuListingActiveRate',
'skuIdEfficiency', 'isPriceBreak', 'payOrdrAmt', 'feeRatio', 'afterSaleRate', 'goodsUv', 'orderNum', 'payOrdrUsrCnt',
'payOrdrGoodsQty', 'ctr', 'goodsVcr', 'customerUnitPrice', 'pieceUnitPrice', 'spend', 'shopNameList', 'idOwnerList',
'ownerDeptList',
];
await upsertRows(SPU_TABLE, spuColumns, spuRows, [ 'statDate', 'spu' ]);
await upsertRows(SKU_TABLE, skuColumns, skuRows, [ 'statDate', 'spu', 'skuCode' ]);
await upsertRows(GOODS_TABLE, goodsColumns, goodsRows, [ 'statDate', 'goodsId' ]);
}
async function syncPddSkuDimensionSnapshots({ targetDate = null, requestTimeout = DEFAULT_REQUEST_TIMEOUT_MS } = {}) {
await ensurePlanningPddSkuDimensionSchema();
const statDate = targetDate || await getLatestPddDashboardBizDate();
if (!statDate) {
return { success: false, skipped: true, reason: 'no_biz_date' };
}
const startDate = dayjs(statDate).subtract(6, 'day').format('YYYY-MM-DD');
const sourceRows = await fetchGoodsRowsFromSource({ startDate, endDate: statDate, requestTimeout });
const goodsRows = (sourceRows || []).map((row) => ({
statDate,
startDate,
endDate: statDate,
spu: String(row.spu || '').trim(),
skuCode: String(row.skuCode || '').trim(),
goodsId: String(row.goodsId || '').trim(),
goodsName: row.goodsName || null,
hdThumbUrl: row.hdThumbUrl || null,
idOwner: row.idOwner || null,
ownerDept: row.ownerDept || null,
shopName: row.shopName || null,
goodsUv: toNumber(row.goodsUv),
inquiryNum: toNumber(row.inquiryNum),
goodsFavCnt: toNumber(row.goodsFavCnt),
goodsFavNum: toNumber(row.goodsFavNum),
orderUserCount: toNumber(row.orderUserCount),
ctr: toNumber(row.ctr),
clickConversionRate: toNumber(row.clickConversionRate),
goodsVcr: toNumber(row.goodsVcr),
ordrVstrRto: toNumber(row.ordrVstrRto),
payOrdrRto: toNumber(row.payOrdrRto),
imprUsrCnt: toNumber(row.imprUsrCnt),
unpaidUserCount: toNumber(row.unpaidUserCount),
payOrdrAmt: toNumber(row.payOrdrAmt),
orderNum: toNumber(row.orderNum),
payOrdrUsrCnt: toNumber(row.payOrdrUsrCnt),
payOrdrGoodsQty: toNumber(row.payOrdrGoodsQty),
customerUnitPrice: toNumber(row.customerUnitPrice),
pieceUnitPrice: toNumber(row.pieceUnitPrice),
spend: toNumber(row.spend),
realConcludeFee: toNumber(row.realConcludeFee),
afterSaleAmount: toNumber(row.afterSaleAmount),
feeRatio: toNumber(row.feeRatio),
afterSaleRate: toNumber(row.afterSaleRate),
afterSaleCount: toNumber(row.afterSaleCount),
badReviewCount: Number(row.badReviewCount || 0),
isPriceBreak: 0,
})).filter((row) => row.goodsId && row.spu && row.skuCode);
const { spuRows, skuRows } = aggregateRows(goodsRows);
await replaceSnapshotRows({ statDate, goodsRows, spuRows, skuRows });
return {
success: true,
statDate,
startDate,
endDate: statDate,
spuRowCount: spuRows.length,
skuRowCount: skuRows.length,
goodsRowCount: goodsRows.length,
};
}
module.exports = {
SPU_TABLE,
SKU_TABLE,
GOODS_TABLE,
DEFAULT_REQUEST_TIMEOUT_MS,
syncPddSkuDimensionSnapshots,
};