cangku/backend/src/services/delistTaskService.js

215 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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,
};