const dayjs = require('dayjs'); const database = require('../config/database'); const jianSunDb = require('../config/jianSunDatabase'); const cloudDatabase = require('../config/cloudDatabase'); const DEFAULT_SCOPE_START = '2026-07-15'; const DEFAULT_SCOPE_END = '2026-07-21'; const DEFAULT_END_DATE = '2026-09-02'; const AWARD_PER_ID = 400; const JULY_START = '2026-07-01'; const JULY_END = '2026-07-31'; function text(value, fallback = '') { const valueText = String(value ?? '').trim(); return valueText || fallback; } function validDate(value, fallback) { const valueText = text(value, fallback); return /^\d{4}-\d{2}-\d{2}$/u.test(valueText) && dayjs(valueText).isValid() ? valueText : fallback; } function addDays(value, offset) { return dayjs(value).add(offset, 'day').format('YYYY-MM-DD'); } function resolveQuery(query = {}) { const scopeStart = validDate(query.scopeStart || query.startDate, DEFAULT_SCOPE_START); const scopeEnd = validDate(query.scopeEnd || query.endDate, DEFAULT_SCOPE_END); const endDate = validDate(query.metricEnd || query.asOfDate, DEFAULT_END_DATE); const metricStart = addDays(endDate, -29); const feeCap = scopeStart >= '2026-07-22' ? 0.88 : 0.9; return { scopeStart, scopeEnd: scopeEnd < scopeStart ? scopeStart : scopeEnd, metricStart, metricEnd: endDate, feeCap, ider: text(query.ider), shopName: text(query.shopName), spu: text(query.spu), goodsId: text(query.goodsId), }; } function normalizeNumber(value) { if (value === null || value === undefined || value === '') return null; const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } function normalizeRow(row = {}, query) { const shippedDays30d = Number(row.shippedDays30d || 0); const performance30d = normalizeNumber(row.performance30d); const fee30d = normalizeNumber(row.fee30d); const feeRatio30d = performance30d > 0 && fee30d !== null ? fee30d / performance30d : null; const firstCreatedAt = row.firstCreatedAt ? new Date(row.firstCreatedAt) : null; const listingStart = firstCreatedAt && !Number.isNaN(firstCreatedAt.getTime()) ? dayjs(firstCreatedAt).startOf('day') : null; const metricEndDay = dayjs(query.metricEnd).startOf('day'); const listedCalendarDays = listingStart && metricEndDay.isValid() ? Math.max(1, metricEndDay.diff(listingStart, 'day') + 1) : 30; const metricDenominatorDays = Math.min(30, listedCalendarDays); const sellThroughRate30d = shippedDays30d / metricDenominatorDays; const feeQualified = performance30d > 0 && feeRatio30d !== null && feeRatio30d <= query.feeCap; const bonusQualified = feeQualified && sellThroughRate30d > 0.3; const listingDays = firstCreatedAt && !Number.isNaN(firstCreatedAt.getTime()) ? Math.max(0, dayjs(query.metricEnd).diff(dayjs(firstCreatedAt), 'day')) : null; return { ...row, goodsId: text(row.goodsId), ider: text(row.ider), shopName: text(row.shopName), spu: text(row.spu), goodsName: text(row.goodsName), linkType: text(row.linkType), status: Number(row.onsale || 0) === 1 ? '在售' : '已下架', skuCount: Number(row.skuCount || 0), listingDays, julyPerformance: normalizeNumber(row.julyPerformance), julyFee: normalizeNumber(row.julyFee), julyFeeRatio: normalizeNumber(row.julyPerformance) > 0 ? normalizeNumber(row.julyFee) / normalizeNumber(row.julyPerformance) : null, julyGoodsCount: normalizeNumber(row.julyGoodsCount) || 0, julyShippedDays: Number(row.julyShippedDays || 0), julySellThroughRate: Number(row.julyShippedDays || 0) / 31, performance30d, fee30d, feeRatio30d, goodsCount30d: normalizeNumber(row.goodsCount30d) || 0, shippedDays30d, metricDenominatorDays, sellThroughRate30d, feeQualified, sellThroughQualified: sellThroughRate30d > 0.3, bonusQualified, bonus: bonusQualified ? AWARD_PER_ID : 0, }; } function buildParams(query) { const epoch = (value) => Math.floor(new Date(`${value}T00:00:00Z`).getTime() / 1000); return { scopeStart: query.scopeStart, scopeEnd: query.scopeEnd, metricStart: query.metricStart, metricEnd: query.metricEnd, julyStart: JULY_START, julyEnd: JULY_END, ider: query.ider, shopName: query.shopName, spu: query.spu, goodsId: query.goodsId, scopeStartEpochSec: epoch(query.scopeStart), scopeEndEpochSec: epoch(addDays(query.scopeEnd, 1)), scopeStartEpochMs: epoch(query.scopeStart) * 1000, scopeEndEpochMs: epoch(addDays(query.scopeEnd, 1)) * 1000, }; } function emptyMetrics() { return { julyPerformance: 0, julyFee: 0, julyGoodsCount: 0, julyShippedDays: 0, performance30d: 0, fee30d: 0, goodsCount30d: 0, shippedDays30d: 0 }; } async function loadSalesMetrics(goodsIds, query) { const metrics = new Map(); const ids = [...new Set((goodsIds || []).map((value) => String(value || '').trim()).filter((value) => /^\d+$/u.test(value)))]; for (let offset = 0; offset < ids.length; offset += 1000) { const chunk = ids.slice(offset, offset + 1000); const inList = chunk.map((id) => `'${id}'`).join(','); const rows = await cloudDatabase.query(` SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), d.item_id))) AS goodsId, SUM(CASE WHEN TRY_CONVERT(DATE, d.stat_date) BETWEEN @julyStart AND @julyEnd THEN ISNULL(TRY_CONVERT(DECIMAL(38,6), d.real_performance),0) ELSE 0 END) AS julyPerformance, SUM(CASE WHEN TRY_CONVERT(DATE, d.stat_date) BETWEEN @julyStart AND @julyEnd THEN ISNULL(TRY_CONVERT(DECIMAL(38,6), d.real_conclude_fee),0) ELSE 0 END) AS julyFee, SUM(CASE WHEN TRY_CONVERT(DATE, d.stat_date) BETWEEN @julyStart AND @julyEnd THEN ISNULL(TRY_CONVERT(DECIMAL(38,6), d.goods_count),0) ELSE 0 END) AS julyGoodsCount, COUNT(DISTINCT CASE WHEN TRY_CONVERT(DATE, d.stat_date) BETWEEN @julyStart AND @julyEnd AND TRY_CONVERT(DECIMAL(38,6), d.goods_count) > 0 THEN TRY_CONVERT(DATE, d.stat_date) END) AS julyShippedDays, SUM(CASE WHEN TRY_CONVERT(DATE, d.stat_date) BETWEEN @metricStart AND @metricEnd THEN ISNULL(TRY_CONVERT(DECIMAL(38,6), d.real_performance),0) ELSE 0 END) AS performance30d, SUM(CASE WHEN TRY_CONVERT(DATE, d.stat_date) BETWEEN @metricStart AND @metricEnd THEN ISNULL(TRY_CONVERT(DECIMAL(38,6), d.real_conclude_fee),0) ELSE 0 END) AS fee30d, SUM(CASE WHEN TRY_CONVERT(DATE, d.stat_date) BETWEEN @metricStart AND @metricEnd THEN ISNULL(TRY_CONVERT(DECIMAL(38,6), d.goods_count),0) ELSE 0 END) AS goodsCount30d, COUNT(DISTINCT CASE WHEN TRY_CONVERT(DATE, d.stat_date) BETWEEN @metricStart AND @metricEnd AND TRY_CONVERT(DECIMAL(38,6), d.goods_count) > 0 THEN TRY_CONVERT(DATE, d.stat_date) END) AS shippedDays30d FROM [JSDataWarehouse].[dws].[dws_sn_sales_full_info_di] d WITH (NOLOCK) WHERE d.item_id IN (${inList}) AND d.stat_date BETWEEN @julyStart AND @metricEnd AND ISNULL(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), d.platform))), N'') IN (N'Pinduoduo', N'拼多多') 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 GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), d.item_id))) `, { julyStart: JULY_START, julyEnd: JULY_END, metricStart: query.metricStart, metricEnd: query.metricEnd }, { requestTimeout: 180000 }); rows.forEach((row) => metrics.set(String(row.goodsId), row)); } return metrics; } // pdd_Skus 由外部采集系统写入,存在残缺批次(如 2026-09-02 仅 113 个商品)。 // 从 upperBound 往回 7 天内挑最新一个商品数不低于窗口最大值 70% 的批次,避免用到不完整采集。 async function pickSkuCaptureTime(upperBound) { const end = dayjs(upperBound).isAfter(dayjs(), 'day') ? dayjs().format('YYYY-MM-DD') : dayjs(upperBound).format('YYYY-MM-DD'); const dates = Array.from({ length: 7 }, (_, index) => dayjs(end).subtract(index, 'day').format('YYYY-MM-DD')); const counts = await Promise.all(dates.map((captureTime) => jianSunDb.query( 'SELECT COUNT(DISTINCT goodsId) AS goodsCount FROM dbo.pdd_Skus WITH (NOLOCK, INDEX(pdd_Skus_createTime_index)) WHERE createTime = @captureTime', { captureTime }, { requestTimeout: 60000 } ))); const mapped = dates.map((dateValue, index) => ({ date: dateValue, goodsCount: Number(counts[index]?.[0]?.goodsCount || 0) })); const maxCount = Math.max(...mapped.map((item) => item.goodsCount)); if (maxCount <= 0) return null; return (mapped.find((item) => item.goodsCount >= maxCount * 0.7) || mapped.find((item) => item.goodsCount === maxCount)).date; } async function getOpsBIncentiveBoardLive(input = {}) { const query = resolveQuery(input); const livePeriod = findPeriod(await loadPeriods(), query); if (livePeriod) query.feeCap = livePeriod.feeCap; const targetRows = await jianSunDb.query(`SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), tm.shopproid))) AS goodsId, MAX(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), tm.ider)))) AS ider, MAX(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), tm.shopname)))) AS shopName, MAX(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), tm.spu)))) AS spu, MAX(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), tm.linkType)))) AS linkType FROM dbo.op_TargetManagement tm WITH (NOLOCK) WHERE tm.dept = N'运营中心B' GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), tm.shopproid)))`, {}, { requestTimeout: 60000 }); const latestCreateTime = await pickSkuCaptureTime(query.metricEnd); if (!latestCreateTime) throw new Error('pdd_Skus 近7天无可用完整采集批次'); const epoch = (value) => Math.floor(new Date(`${value}T00:00:00Z`).getTime() / 1000); const startSec = epoch(query.scopeStart); const endSec = epoch(addDays(query.scopeEnd, 1)); const skuRows = await jianSunDb.query(`SELECT CONVERT(NVARCHAR(100), s.goodsId) AS goodsId, MAX(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), s.goodsName)))) AS goodsName, MIN(DATEADD(SECOND, CONVERT(INT, CASE WHEN s.createdAt >= 1000000000000 THEN s.createdAt / 1000 ELSE s.createdAt END), CONVERT(DATETIME2, '1970-01-01'))) AS firstCreatedAt, MAX(CASE WHEN TRY_CONVERT(INT, s.isOnsale) = 1 THEN 1 ELSE 0 END) AS onsale, COUNT_BIG(1) AS skuCount FROM dbo.pdd_Skus s WITH (NOLOCK, INDEX(pdd_Skus_createTime_index)) WHERE s.createTime = @latestCreateTime AND ((s.createdAt >= @startSec AND s.createdAt < @endSec) OR (s.createdAt >= @startMs AND s.createdAt < @endMs)) GROUP BY s.goodsId`, { latestCreateTime, startSec, endSec, startMs: startSec * 1000, endMs: endSec * 1000 }, { requestTimeout: 180000 }); const targetMap = new Map(targetRows.map((row) => [String(row.goodsId), row])); const scopeRows = skuRows.flatMap((row) => { const target = targetMap.get(String(row.goodsId)); if (!target) return []; if ((query.ider && target.ider !== query.ider) || (query.shopName && target.shopName !== query.shopName) || (query.spu && !String(target.spu || '').includes(query.spu)) || (query.goodsId && !String(row.goodsId).includes(query.goodsId))) return []; return [{ ...row, ...target }]; }); const salesMetrics = await loadSalesMetrics(scopeRows.map((row) => row.goodsId), query); const rows = scopeRows.map((row) => ({ ...emptyMetrics(), ...row, ...(salesMetrics.get(String(row.goodsId)) || {}) })); let dwsLatestRows = []; try { dwsLatestRows = await cloudDatabase.query(`SELECT MAX(TRY_CONVERT(DATE, stat_date)) AS latestDate FROM [JSDataWarehouse].[dws].[dws_sn_sales_full_info_di] WITH (NOLOCK) WHERE ISNULL(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), platform))), N'') IN (N'Pinduoduo', N'拼多多')`, {}, { requestTimeout: 60000 }); } catch (error) { console.warn('[OpsBIncentiveBoard] latest DWS date unavailable:', error.message); } const normalizedRows = (rows || []).map((row) => normalizeRow(row, query)); const qualifiedRows = normalizedRows.filter((row) => row.bonusQualified); const ownerMap = new Map(); normalizedRows.forEach((row) => { const current = ownerMap.get(row.ider) || { ider: row.ider, goodsIdCount: 0, goodsCount30d: 0, shippedDays30d: 0, feeQualifiedCount: 0, bonusQualifiedCount: 0, bonus: 0 }; current.goodsIdCount += 1; current.goodsCount30d += row.goodsCount30d; current.shippedDays30d += row.shippedDays30d; current.feeQualifiedCount += row.feeQualified ? 1 : 0; current.bonusQualifiedCount += row.bonusQualified ? 1 : 0; current.bonus += row.bonus; ownerMap.set(row.ider, current); }); return { rows: normalizedRows, bonusRows: qualifiedRows, ownerSummary: [...ownerMap.values()].sort((a, b) => b.bonus - a.bonus || b.goodsIdCount - a.goodsIdCount), summary: { goodsIdCount: normalizedRows.length, feeQualifiedCount: normalizedRows.filter((row) => row.feeQualified).length, sellThroughQualifiedCount: normalizedRows.filter((row) => row.sellThroughQualified).length, bonusQualifiedCount: qualifiedRows.length, bonus: qualifiedRows.length * AWARD_PER_ID, awardPerId: AWARD_PER_ID, }, query: { ...query, julyStart: JULY_START, julyEnd: JULY_END, latestDwsDate: dwsLatestRows?.[0]?.latestDate || null }, }; } // 口径来源:运营中心B激励方案批次表。pullDate = 拉取(冻结)日期,统计窗口固定为 pullDate 往前 30 天。 // 批次配置存 MySQL ops_b_incentive_periods 表(可动态新增批次,无需发版),此列表仅作首次种子与兜底。 const DEFAULT_SNAPSHOT_PERIODS = [ { scopeStart: '2026-07-15', scopeEnd: '2026-07-21', pullDate: '2026-08-29', feeCap: 0.9 }, { scopeStart: '2026-07-22', scopeEnd: '2026-07-28', pullDate: '2026-09-05', feeCap: 0.88 }, { scopeStart: '2026-07-29', scopeEnd: '2026-08-04', pullDate: '2026-09-12', feeCap: 0.875 }, { scopeStart: '2026-08-05', scopeEnd: '2026-08-11', pullDate: '2026-09-19', feeCap: 0.87 }, { scopeStart: '2026-08-12', scopeEnd: '2026-08-18', pullDate: '2026-09-26', feeCap: 0.865 }, { scopeStart: '2026-08-19', scopeEnd: '2026-08-25', pullDate: '2026-10-03', feeCap: 0.86 }, { scopeStart: '2026-08-26', scopeEnd: '2026-09-01', pullDate: '2026-10-10', feeCap: 0.855 }, { scopeStart: '2026-09-02', scopeEnd: '2026-09-08', pullDate: '2026-10-17', feeCap: 0.85 }, { scopeStart: '2026-09-09', scopeEnd: '2026-09-15', pullDate: '2026-10-24', feeCap: 0.845 }, { scopeStart: '2026-09-16', scopeEnd: '2026-09-22', pullDate: '2026-10-31', feeCap: 0.84 }, ]; function makePeriodKey(scopeStart, scopeEnd) { return `ops-b-${scopeStart.replace(/-/g, '')}-${scopeEnd.replace(/-/g, '')}`; } let periodsCache = null; let periodsCacheExpiresAt = 0; async function loadPeriods() { if (periodsCache && Date.now() < periodsCacheExpiresAt) return periodsCache; let rows = await database.local.query("SELECT scope_start, scope_end, pull_date, fee_cap, status FROM ops_b_incentive_periods ORDER BY scope_start, scope_end"); if (!rows.length) { for (const period of DEFAULT_SNAPSHOT_PERIODS) { await database.local.query( 'INSERT IGNORE INTO ops_b_incentive_periods (scope_start, scope_end, pull_date, fee_cap, status) VALUES (?, ?, ?, ?, ?)', [period.scopeStart, period.scopeEnd, period.pullDate, period.feeCap, 'active'] ); } rows = await database.local.query("SELECT scope_start, scope_end, pull_date, fee_cap, status FROM ops_b_incentive_periods ORDER BY scope_start, scope_end"); } periodsCache = rows .filter((row) => String(row.status || 'active') === 'active') .map((row) => ({ key: makePeriodKey(dayjs(row.scope_start).format('YYYY-MM-DD'), dayjs(row.scope_end).format('YYYY-MM-DD')), scopeStart: dayjs(row.scope_start).format('YYYY-MM-DD'), scopeEnd: dayjs(row.scope_end).format('YYYY-MM-DD'), pullDate: dayjs(row.pull_date).format('YYYY-MM-DD'), feeCap: Number(row.fee_cap), })); periodsCacheExpiresAt = Date.now() + 60000; return periodsCache; } function findPeriod(periods, query) { return periods.find((period) => period.scopeStart === query.scopeStart && period.scopeEnd === query.scopeEnd) || null; } async function readSnapshot(period) { const rows = await database.local.query( 'SELECT rows_json, summary_json, metric_end, finalized, generated_at FROM ops_b_incentive_snapshots WHERE snapshot_key = ? AND status = ? LIMIT 1', [period.key, 'completed'] ); const row = rows[0]; if (!row) return null; try { const meta = JSON.parse(row.summary_json || '{}'); return { rows: JSON.parse(row.rows_json || '[]'), summary: meta.summary || {}, ownerSummary: meta.ownerSummary || [], metricEnd: row.metric_end, finalized: Boolean(row.finalized), generatedAt: row.generated_at }; } catch (error) { console.warn('[OpsBIncentiveSnapshot] invalid snapshot:', error.message); return null; } } async function writeSnapshot(period, query, result, finalized) { await database.local.query(` INSERT INTO ops_b_incentive_snapshots (snapshot_key, scope_start, scope_end, metric_end, status, finalized, rows_json, summary_json, generated_at, completed_at, error_message) VALUES (?, ?, ?, ?, 'completed', ?, ?, ?, NOW(), NOW(), NULL) ON DUPLICATE KEY UPDATE metric_end = VALUES(metric_end), finalized = VALUES(finalized), rows_json = VALUES(rows_json), summary_json = VALUES(summary_json), generated_at = VALUES(generated_at), completed_at = VALUES(completed_at), status = 'completed', error_message = NULL `, [period.key, query.scopeStart, query.scopeEnd, query.metricEnd, finalized ? 1 : 0, JSON.stringify(result.rows || []), JSON.stringify({ summary: result.summary || {}, ownerSummary: result.ownerSummary || [] })]); } async function getOpsBIncentiveBoard(input = {}) { const query = resolveQuery(input); const period = findPeriod(await loadPeriods(), query); if (period) query.feeCap = period.feeCap; if (!period) return getOpsBIncentiveBoardLive(input); const existing = await readSnapshot(period); if (!existing) { return { pending: true, rows: [], bonusRows: [], ownerSummary: [], summary: {}, query: { ...query, latestDwsDate: null, snapshot: { cached: false, finalized: false, pending: true } }, }; } const filteredRows = existing.rows.filter((row) => ( (!query.ider || row.ider === query.ider) && (!query.shopName || row.shopName === query.shopName) && (!query.spu || String(row.spu || '').includes(query.spu)) && (!query.goodsId || String(row.goodsId || '').includes(query.goodsId)) )); const ownerMap = new Map(); filteredRows.forEach((row) => { const current = ownerMap.get(row.ider) || { ider: row.ider, goodsIdCount: 0, goodsCount30d: 0, shippedDays30d: 0, feeQualifiedCount: 0, bonusQualifiedCount: 0, bonus: 0 }; current.goodsIdCount += 1; current.goodsCount30d += Number(row.goodsCount30d || 0); current.shippedDays30d += Number(row.shippedDays30d || 0); current.feeQualifiedCount += row.feeQualified ? 1 : 0; current.bonusQualifiedCount += row.bonusQualified ? 1 : 0; current.bonus += Number(row.bonus || 0); ownerMap.set(row.ider, current); }); const bonusRows = filteredRows.filter((row) => row.bonusQualified); return { ...existing, rows: filteredRows, bonusRows, ownerSummary: [...ownerMap.values()].sort((a, b) => b.bonus - a.bonus || b.goodsIdCount - a.goodsIdCount), summary: { goodsIdCount: filteredRows.length, feeQualifiedCount: filteredRows.filter((row) => row.feeQualified).length, sellThroughQualifiedCount: filteredRows.filter((row) => row.sellThroughQualified).length, bonusQualifiedCount: bonusRows.length, bonus: bonusRows.reduce((sum, row) => sum + Number(row.bonus || 0), 0), awardPerId: AWARD_PER_ID, }, query: { ...query, metricEnd: dayjs(existing.metricEnd).format('YYYY-MM-DD'), latestDwsDate: null, snapshot: { cached: true, finalized: existing.finalized, generatedAt: existing.generatedAt }, }, }; } async function refreshOpsBIncentiveSnapshots(source = 'scheduled') { const today = dayjs().format('YYYY-MM-DD'); const results = []; for (const period of await loadPeriods()) { const { scopeStart, scopeEnd } = period; // 拉取日次日首刷才定档,确保统计窗口包含拉取日全天数据(数仓滞后约一天)。 const finalized = today > period.pullDate; const existing = await readSnapshot(period); if (existing?.finalized) { results.push({ key: period.key, rows: existing.rows.length, finalized: true, skipped: true, source }); continue; } const result = await getOpsBIncentiveBoardLive({ scopeStart, scopeEnd, metricEnd: period.pullDate }); await writeSnapshot(period, { scopeStart, scopeEnd, metricEnd: period.pullDate }, result, finalized); results.push({ key: period.key, rows: result.rows.length, finalized, source }); } return results; } async function getOpsBIncentiveBatches() { const periods = await loadPeriods(); const snapshots = await database.local.query( "SELECT snapshot_key, finalized, generated_at, JSON_EXTRACT(summary_json, '$.summary.goodsIdCount') goodsIdCount, JSON_EXTRACT(summary_json, '$.summary.bonusQualifiedCount') bonusQualifiedCount, JSON_EXTRACT(summary_json, '$.summary.bonus') bonus FROM ops_b_incentive_snapshots WHERE status = 'completed'" ); const snapshotMap = new Map(snapshots.map((row) => [row.snapshot_key, row])); return periods.map((period, index) => { const snapshot = snapshotMap.get(period.key) || {}; return { ...period, batchNo: index + 1, finalized: Boolean(snapshot.finalized), generatedAt: snapshot.generated_at || null, goodsIdCount: Number(snapshot.goodsIdCount || 0), bonusQualifiedCount: Number(snapshot.bonusQualifiedCount || 0), bonus: Number(snapshot.bonus || 0), }; }); } async function getOpsBIncentiveDaily(input = {}) { const query = resolveQuery(input); const params = buildParams(query); if (!query.goodsId) return { rows: [], query }; const rows = await cloudDatabase.query(` WITH daily AS ( SELECT TRY_CONVERT(DATE, d.stat_date) AS statDate, SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.goods_count), 0)) AS goodsCount, SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.real_performance), 0)) AS realPerformance, SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.real_conclude_fee), 0)) AS realFee FROM [JSDataWarehouse].[dws].[dws_sn_sales_full_info_di] d WITH (NOLOCK) WHERE LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), d.item_id))) = @goodsId AND TRY_CONVERT(DATE, d.stat_date) BETWEEN @metricStart AND @metricEnd AND ISNULL(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), d.platform))), N'') IN (N'Pinduoduo', N'拼多多') 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 GROUP BY TRY_CONVERT(DATE, d.stat_date) ) SELECT statDate, goodsCount, CASE WHEN goodsCount > 0 THEN N'是' ELSE N'否' END AS isActive, realPerformance, realFee FROM daily ORDER BY statDate ASC `, params, { requestTimeout: 120000 }); return { rows: rows || [], query }; } module.exports = { getOpsBIncentiveBoard, getOpsBIncentiveBoardLive, getOpsBIncentiveDaily, getOpsBIncentiveBatches, refreshOpsBIncentiveSnapshots, resolveQuery, pickSkuCaptureTime, AWARD_PER_ID, DEFAULT_SNAPSHOT_PERIODS };