forked from lunanting/cangku
266 lines
8.6 KiB
JavaScript
266 lines
8.6 KiB
JavaScript
'use strict';
|
|
|
|
const jianSunDb = require('../config/jianSunDatabase');
|
|
|
|
const DEFAULT_PAGE_SIZE = 100;
|
|
const MAX_PAGE_SIZE = 500;
|
|
const ACTION_REMARKS = {
|
|
create: '新增预售',
|
|
extend: '延长预售',
|
|
cancel: '取消预售',
|
|
};
|
|
|
|
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 normalizeRunDate(value) {
|
|
const runDate = normalizeText(value, 10);
|
|
if (!runDate) return '';
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(runDate)) {
|
|
const error = new Error('runDate 必须使用 YYYY-MM-DD 格式');
|
|
error.statusCode = 400;
|
|
throw error;
|
|
}
|
|
const parsed = new Date(`${runDate}T00:00:00Z`);
|
|
if (Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== runDate) {
|
|
const error = new Error('runDate 不是有效日期');
|
|
error.statusCode = 400;
|
|
throw error;
|
|
}
|
|
return runDate;
|
|
}
|
|
|
|
function normalizeInput(input = {}) {
|
|
const dingtalkUserId = normalizeText(
|
|
input.dingtalkUserId
|
|
|| input.dingtalk_user_id
|
|
|| input.iderUserId
|
|
|| input.ider_userid
|
|
|| input.userId
|
|
|| input.user_id,
|
|
120
|
|
);
|
|
const dingtalkUserName = normalizeText(
|
|
input.dingtalkUserName
|
|
|| input.dingtalk_user_name
|
|
|| input.dingtalkUsername
|
|
|| input.userName
|
|
|| input.user_name,
|
|
100
|
|
);
|
|
if (!dingtalkUserId && !dingtalkUserName) {
|
|
const error = new Error('缺少人员信息,请提供 dingtalkUserId 或 dingtalkUserName');
|
|
error.statusCode = 400;
|
|
throw error;
|
|
}
|
|
|
|
const page = normalizePositiveInteger(input.page, 1);
|
|
const pageSize = normalizePositiveInteger(
|
|
input.pageSize || input.page_size,
|
|
DEFAULT_PAGE_SIZE,
|
|
MAX_PAGE_SIZE
|
|
);
|
|
return {
|
|
dingtalkUserId,
|
|
dingtalkUserName,
|
|
runDate: normalizeRunDate(input.runDate || input.run_date || input.date),
|
|
actionableOnly: normalizeBoolean(input.actionableOnly || input.actionable_only, false),
|
|
page,
|
|
pageSize,
|
|
offset: (page - 1) * pageSize,
|
|
};
|
|
}
|
|
|
|
function buildRecipientFilter(input) {
|
|
if (input.dingtalkUserId) {
|
|
return {
|
|
sql: 'ider_userid = @dingtalkUserId',
|
|
params: { dingtalkUserId: input.dingtalkUserId },
|
|
};
|
|
}
|
|
return {
|
|
sql: 'ider = @dingtalkUserName',
|
|
params: { dingtalkUserName: input.dingtalkUserName },
|
|
};
|
|
}
|
|
|
|
function getActionType(row = {}) {
|
|
const noticeRemark = normalizeText(row.notice_remark ?? row.noticeRemark, 100);
|
|
const presalePolicy = normalizeText(row.presale_policy ?? row.presalePolicy, 100);
|
|
if (noticeRemark === ACTION_REMARKS.cancel || presalePolicy === ACTION_REMARKS.cancel) {
|
|
return 'CANCEL_PRESALE';
|
|
}
|
|
if (noticeRemark === ACTION_REMARKS.create) return 'CREATE_PRESALE';
|
|
if (noticeRemark === ACTION_REMARKS.extend) return 'EXTEND_PRESALE';
|
|
return 'NO_ACTION';
|
|
}
|
|
|
|
function mapTaskRow(row = {}) {
|
|
const actionType = getActionType(row);
|
|
return {
|
|
runDate: row.run_date || null,
|
|
noticeTime: row.notice_time || null,
|
|
noticeRemark: row.notice_remark || '',
|
|
actionValue: row.action_value || '',
|
|
actionType,
|
|
requiresAction: actionType !== 'NO_ACTION',
|
|
operatorName: row.ider || '',
|
|
operatorUserId: row.ider_userid || '',
|
|
shopHead: row.shop_head || '',
|
|
shopName: row.shopname || '',
|
|
department: row.dept || '',
|
|
currentCategory: row.currCater || '',
|
|
shopId: row.shop_id || '',
|
|
channel: row.channel || '',
|
|
companyId: row.co_id || '',
|
|
productId: row.shop_i_id || '',
|
|
shopSkuId: row.shop_sku_id || '',
|
|
erpSkuId: row.sku_id || '',
|
|
skuName: row.sku_name || '',
|
|
shopQuantity: Number(row.shop_qty || 0),
|
|
outOfStockQuantity: Number(row.out_of_stock_qty || 0),
|
|
jsUseDays: row.js_use_days === null || row.js_use_days === undefined
|
|
? null
|
|
: Number(row.js_use_days),
|
|
stockoutWarning: row.stockout_warning || '',
|
|
presalePolicy: row.presale_policy || '',
|
|
presaleDeadline: row.presale_deadline || null,
|
|
};
|
|
}
|
|
|
|
function mapSummary(row = {}) {
|
|
return {
|
|
totalRows: Number(row.total_rows || 0),
|
|
productCount: Number(row.product_count || 0),
|
|
actionableRows: Number(row.actionable_rows || 0),
|
|
createPresaleRows: Number(row.create_presale_rows || 0),
|
|
extendPresaleRows: Number(row.extend_presale_rows || 0),
|
|
cancelPresaleRows: Number(row.cancel_presale_rows || 0),
|
|
noActionRows: Number(row.no_action_rows || 0),
|
|
};
|
|
}
|
|
|
|
async function listPresaleTasks(rawInput = {}, options = {}) {
|
|
const input = normalizeInput(rawInput);
|
|
const query = options.query || jianSunDb.query;
|
|
const recipient = buildRecipientFilter(input);
|
|
const actionParams = {
|
|
createRemark: ACTION_REMARKS.create,
|
|
extendRemark: ACTION_REMARKS.extend,
|
|
cancelRemark: ACTION_REMARKS.cancel,
|
|
};
|
|
const baseParams = { ...recipient.params, ...actionParams };
|
|
|
|
let runDate = input.runDate;
|
|
if (!runDate) {
|
|
const latestRows = await query(
|
|
`SELECT CONVERT(varchar(10), MAX(run_date), 120) AS run_date
|
|
FROM dbo.dm_stockout_id_daily WITH (NOLOCK)
|
|
WHERE ${recipient.sql}`,
|
|
recipient.params
|
|
);
|
|
runDate = latestRows[0]?.run_date || '';
|
|
}
|
|
|
|
if (!runDate) {
|
|
return {
|
|
runDate: null,
|
|
recipient: {
|
|
dingtalkUserId: input.dingtalkUserId || '',
|
|
dingtalkUserName: input.dingtalkUserName || '',
|
|
},
|
|
summary: mapSummary(),
|
|
pagination: { page: input.page, pageSize: input.pageSize, total: 0, totalPages: 0 },
|
|
items: [],
|
|
};
|
|
}
|
|
|
|
const filters = ['run_date = @runDate', recipient.sql];
|
|
if (input.actionableOnly) {
|
|
filters.push(`(
|
|
notice_remark IN (@createRemark, @extendRemark, @cancelRemark)
|
|
OR presale_policy = @cancelRemark
|
|
)`);
|
|
}
|
|
const whereSql = filters.join('\n AND ');
|
|
const params = { ...baseParams, runDate };
|
|
|
|
const [summaryRows, taskRows] = await Promise.all([
|
|
query(
|
|
`SELECT
|
|
COUNT(*) AS total_rows,
|
|
COUNT(DISTINCT shop_i_id) AS product_count,
|
|
COALESCE(SUM(CASE WHEN notice_remark IN (@createRemark, @extendRemark, @cancelRemark)
|
|
OR presale_policy = @cancelRemark THEN 1 ELSE 0 END), 0) AS actionable_rows,
|
|
COALESCE(SUM(CASE WHEN notice_remark = @createRemark THEN 1 ELSE 0 END), 0) AS create_presale_rows,
|
|
COALESCE(SUM(CASE WHEN notice_remark = @extendRemark THEN 1 ELSE 0 END), 0) AS extend_presale_rows,
|
|
COALESCE(SUM(CASE WHEN notice_remark = @cancelRemark
|
|
OR presale_policy = @cancelRemark THEN 1 ELSE 0 END), 0) AS cancel_presale_rows,
|
|
COALESCE(SUM(CASE WHEN notice_remark NOT IN (@createRemark, @extendRemark, @cancelRemark)
|
|
AND presale_policy <> @cancelRemark THEN 1 ELSE 0 END), 0) AS no_action_rows
|
|
FROM dbo.dm_stockout_id_daily WITH (NOLOCK)
|
|
WHERE ${whereSql}`,
|
|
params
|
|
),
|
|
query(
|
|
`SELECT
|
|
CONVERT(varchar(10), run_date, 120) AS run_date,
|
|
CONVERT(varchar(19), notice_time, 120) AS notice_time,
|
|
notice_remark, action_value, ider, ider_userid, shop_head, shopname, dept,
|
|
currCater, shop_id, channel, co_id, shop_i_id, shop_sku_id, sku_id, sku_name,
|
|
shop_qty, out_of_stock_qty, js_use_days, stockout_warning, presale_policy,
|
|
CONVERT(varchar(19), presale_deadline, 120) AS presale_deadline
|
|
FROM dbo.dm_stockout_id_daily WITH (NOLOCK)
|
|
WHERE ${whereSql}
|
|
ORDER BY notice_time DESC, shop_i_id, shop_sku_id, sku_id
|
|
OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY`,
|
|
{ ...params, offset: input.offset, pageSize: input.pageSize }
|
|
),
|
|
]);
|
|
|
|
const items = taskRows.map(mapTaskRow);
|
|
const summary = mapSummary(summaryRows[0]);
|
|
const firstItem = items[0];
|
|
return {
|
|
runDate,
|
|
recipient: {
|
|
dingtalkUserId: firstItem?.operatorUserId || input.dingtalkUserId || '',
|
|
dingtalkUserName: firstItem?.operatorName || input.dingtalkUserName || '',
|
|
},
|
|
actionableOnly: input.actionableOnly,
|
|
summary,
|
|
pagination: {
|
|
page: input.page,
|
|
pageSize: input.pageSize,
|
|
total: summary.totalRows,
|
|
totalPages: Math.ceil(summary.totalRows / input.pageSize),
|
|
},
|
|
items,
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
listPresaleTasks,
|
|
_test: {
|
|
getActionType,
|
|
mapTaskRow,
|
|
normalizeInput,
|
|
normalizeRunDate,
|
|
},
|
|
};
|