'use strict'; // 停产下架每日数据(只读展示)。 // 数据来源:统一清货模块写入的 ope_clear_task_jxpdd(京喜拼多多清货下架任务), // 关联 ope_clear_pool(清货池)补充图片、负责人、库存与动销字段。 // 本服务只做 SELECT,不写任何业务表;实际下架脚本由统一模块负责。 const jianSunDb = require('../config/jianSunDatabase'); const DEFAULT_PAGE_SIZE = 100; const MAX_PAGE_SIZE = 5000; const STATUS_OPTIONS = new Set(['销售', '退市', '停产']); function normalizeText(value, maxLength = 255) { return String(value ?? '').trim().slice(0, maxLength); } function normalizePositiveInteger(value, fallback, max = Number.MAX_SAFE_INTEGER) { const parsed = Number.parseInt(value, 10); if (!Number.isFinite(parsed) || parsed <= 0) return fallback; return Math.min(parsed, max); } function normalizeBoolean(value, fallback = false) { if (value === true || value === false) return value; const text = String(value ?? '').trim().toLowerCase(); if (['1', 'true', 'yes', 'on'].includes(text)) return true; if (['0', 'false', 'no', 'off'].includes(text)) return false; return fallback; } function normalizeInput(input = {}) { return { keyword: normalizeText(input.keyword, 120), status: STATUS_OPTIONS.has(normalizeText(input.status, 40)) ? normalizeText(input.status, 40) : '', manager: normalizeText(input.manager || input.dingtalkUserName, 100), clearPeriod: normalizeText(input.clearPeriod, 60), inStockOnly: normalizeBoolean(input.inStockOnly || input.in_stock_only), page: normalizePositiveInteger(input.page, 1), pageSize: normalizePositiveInteger(input.pageSize, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE), }; } function mapItemRow(row = {}) { return { skuoid: row.skuoid || '', spu: row.spu || '', pic: row.pic || '', supplierName: row.supplier_name || '', manager: row.manager || '', cater: row.cater || '', status: row.status || '', clearPrice: row.clear_price === null || row.clear_price === undefined ? null : Number(row.clear_price), costPrice: row.cost_price === null || row.cost_price === undefined ? null : Number(row.cost_price), stockJiansun: Number(row.stock_jiansun || 0), stockFactory: Number(row.stock_factory || 0), stockTotal: Number(row.stock_total || 0), dailySale: row.daily_sale === null || row.daily_sale === undefined ? null : Number(row.daily_sale), availableDays: row.available_days === null || row.available_days === undefined ? null : Number(row.available_days), sendCount: Number(row.send_count || 0), refundCount: Number(row.refund_count || 0), priority: row.priority === null || row.priority === undefined ? null : Number(row.priority), clearPeriod: row.clear_period || '', profitCenter: row.profit_center || '', }; } async function listDelistTasks(rawInput = {}, options = {}) { const input = normalizeInput(rawInput); const query = options.query || jianSunDb.query; const monthRows = await query( 'SELECT MAX(stat_month) AS latest FROM dbo.ope_clear_task_jxpdd WITH (NOLOCK)' ); const statMonth = monthRows[0]?.latest ?? null; if (statMonth === null || statMonth === '') { return { statMonth: null, manager: input.manager, summary: { total: 0, productCount: 0, stockTotal: 0, byStatus: {} }, filterOptions: { clearPeriods: [] }, pagination: { page: input.page, pageSize: input.pageSize, total: 0, totalPages: 0 }, items: [], }; } const filters = ['t.stat_month = @statMonth']; if (input.manager) filters.push('p.manager = @manager'); if (input.status) filters.push('p.status = @status'); if (input.clearPeriod) filters.push('p.clear_period = @clearPeriod'); if (input.inStockOnly) filters.push('t.stock_total > 0'); if (input.keyword) { filters.push(`( t.skuoid LIKE @keyword OR p.spu LIKE @keyword OR p.supplier_name LIKE @keyword OR p.manager LIKE @keyword OR p.cater LIKE @keyword )`); } const whereSql = filters.join('\n AND '); const params = { statMonth, status: input.status || undefined, manager: input.manager || undefined, clearPeriod: input.clearPeriod || undefined, keyword: input.keyword ? `%${input.keyword}%` : undefined, }; const summaryRows = await query( `SELECT COUNT(*) AS total, COUNT(DISTINCT p.spu) AS product_count, ISNULL(SUM(t.stock_total), 0) AS stock_total, ISNULL(SUM(CASE WHEN p.status = N'停产' THEN 1 ELSE 0 END), 0) AS stop_count, ISNULL(SUM(CASE WHEN p.status = N'退市' THEN 1 ELSE 0 END), 0) AS retired_count, ISNULL(SUM(CASE WHEN p.status = N'销售' THEN 1 ELSE 0 END), 0) AS selling_count FROM dbo.ope_clear_task_jxpdd t WITH (NOLOCK) INNER JOIN ( SELECT skuoid, spu, pic, supplier_name, manager, cater, status, stock_total AS pool_stock, ROW_NUMBER() OVER (PARTITION BY skuoid ORDER BY stat_month DESC) AS rn FROM dbo.ope_clear_pool WITH (NOLOCK) ) p ON p.skuoid = t.skuoid AND p.rn = 1 WHERE ${whereSql}`, params ); const total = Number(summaryRows[0]?.total || 0); if (!total) { return { statMonth, manager: input.manager, summary: { total: 0, productCount: 0, stockTotal: 0, byStatus: { 停产: 0, 退市: 0, 销售: 0 }, }, filterOptions: { clearPeriods: [] }, pagination: { page: input.page, pageSize: input.pageSize, total: 0, totalPages: 0 }, items: [], }; } const poolLatest = `( SELECT skuoid, spu, pic, supplier_name, manager, cater, status, stock_jiansun, stock_factory, stock_total, daily_sale, available_days, send_count, refund_count, priority, clear_period, ROW_NUMBER() OVER (PARTITION BY skuoid ORDER BY stat_month DESC) AS rn FROM dbo.ope_clear_pool WITH (NOLOCK) )`; const periodRows = await query( `SELECT DISTINCT p.clear_period FROM dbo.ope_clear_task_jxpdd t WITH (NOLOCK) INNER JOIN ${poolLatest} p ON p.skuoid = t.skuoid AND p.rn = 1 WHERE t.stat_month = @statMonth ${input.manager ? 'AND p.manager = @manager' : ''}`, { statMonth, manager: input.manager || undefined } ); const clearPeriods = periodRows .map(row => normalizeText(row.clear_period, 60)) .filter(Boolean) .sort((a, b) => a.localeCompare(b, 'zh-Hans-CN')); const itemRows = await query( `SELECT t.skuoid, t.clear_price, t.profit_center, p.spu, p.pic, p.supplier_name, p.manager, p.cater, p.status, p.stock_jiansun, p.stock_factory, p.stock_total, p.daily_sale, p.available_days, p.send_count, p.refund_count, p.priority, p.clear_period FROM dbo.ope_clear_task_jxpdd t WITH (NOLOCK) INNER JOIN ${poolLatest} p ON p.skuoid = t.skuoid AND p.rn = 1 WHERE ${whereSql} ORDER BY CASE p.status WHEN N'停产' THEN 0 WHEN N'退市' THEN 1 ELSE 2 END, p.priority DESC, t.skuoid OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY`, { ...params, offset: (input.page - 1) * input.pageSize, pageSize: Math.min(input.pageSize, total), } ); return { statMonth, manager: input.manager, summary: { total, productCount: Number(summaryRows[0]?.product_count || 0), stockTotal: Number(summaryRows[0]?.stock_total || 0), byStatus: { 停产: Number(summaryRows[0]?.stop_count || 0), 退市: Number(summaryRows[0]?.retired_count || 0), 销售: Number(summaryRows[0]?.selling_count || 0), }, }, filterOptions: { clearPeriods }, pagination: { page: input.page, pageSize: input.pageSize, total, totalPages: Math.ceil(total / input.pageSize), }, items: itemRows.map(mapItemRow), }; } module.exports = { listDelistTasks, };