forked from lunanting/cangku
509 lines
23 KiB
JavaScript
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,
|
|
};
|