forked from lunanting/cangku
215 lines
7.8 KiB
JavaScript
215 lines
7.8 KiB
JavaScript
'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,
|
||
};
|