659 lines
19 KiB
JavaScript
659 lines
19 KiB
JavaScript
'use strict';
|
|
|
|
const dayjs = require('dayjs');
|
|
const sql = require('mssql');
|
|
const database = require('../config/database');
|
|
const cloudDb = require('../config/cloudDatabase');
|
|
|
|
const SOURCE_TABLE = 'dws_sn_sales_full_info_di';
|
|
const SOURCE_FQN = `[JSDataWarehouse].[dws].[${SOURCE_TABLE}]`;
|
|
const LOCAL_ALERT_TABLE = 'jd_fee_monitor_alerts';
|
|
const LOCAL_CONFIG_TABLE = 'jd_fee_monitor_config';
|
|
|
|
const DEFAULT_CONFIG = {
|
|
target_fee_ratio: 0.76,
|
|
platform: '京东供销',
|
|
listed_days_threshold: 30,
|
|
};
|
|
|
|
function normalizeText(value) {
|
|
if (value === null || value === undefined) {
|
|
return '';
|
|
}
|
|
|
|
return String(value).trim();
|
|
}
|
|
|
|
function toNumber(value, defaultValue = null) {
|
|
const num = Number(value);
|
|
return Number.isFinite(num) ? num : defaultValue;
|
|
}
|
|
|
|
function parsePercent(value, fallback) {
|
|
const num = Number(value);
|
|
if (!Number.isFinite(num)) {
|
|
return fallback;
|
|
}
|
|
|
|
return num > 1 ? num / 100 : num;
|
|
}
|
|
|
|
function normalizeRatio(value, defaultValue = null) {
|
|
const num = Number(value);
|
|
if (!Number.isFinite(num)) {
|
|
return defaultValue;
|
|
}
|
|
|
|
if (num > 1 && num <= 100) {
|
|
return num / 100;
|
|
}
|
|
|
|
return num;
|
|
}
|
|
|
|
function hasDirectSourceConfig() {
|
|
return Boolean(process.env.CLOUD_DB_HOST && process.env.CLOUD_DB_NAME && process.env.CLOUD_DB_USER);
|
|
}
|
|
|
|
function buildDirectSourceConfig() {
|
|
if (!hasDirectSourceConfig()) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
server: process.env.CLOUD_DB_HOST,
|
|
port: Number.parseInt(process.env.CLOUD_DB_PORT || '1433', 10) || 1433,
|
|
database: process.env.CLOUD_DB_NAME,
|
|
user: process.env.CLOUD_DB_USER,
|
|
password: process.env.CLOUD_DB_PASSWORD || '',
|
|
options: {
|
|
encrypt: String(process.env.CLOUD_DB_SSL || '').toLowerCase() === 'true',
|
|
trustServerCertificate: true,
|
|
},
|
|
connectionTimeout: Number.parseInt(process.env.CLOUD_DB_CONNECT_TIMEOUT_MS || '30000', 10) || 30000,
|
|
requestTimeout: Number.parseInt(process.env.CLOUD_DB_REQUEST_TIMEOUT_MS || '600000', 10) || 600000,
|
|
};
|
|
}
|
|
|
|
async function runSourceQuery(sqlText, params = {}, missingMessage = 'source unavailable') {
|
|
const directConfig = buildDirectSourceConfig();
|
|
|
|
if (directConfig) {
|
|
let pool = null;
|
|
try {
|
|
pool = await sql.connect(directConfig);
|
|
const request = pool.request();
|
|
for (const [key, value] of Object.entries(params || {})) {
|
|
request.input(key, value);
|
|
}
|
|
const result = await request.query(cloudDb.rewriteReportReplicaSql(sqlText, {
|
|
database: directConfig.database,
|
|
}));
|
|
return { ok: true, rows: result.recordset || [], message: '' };
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
rows: [],
|
|
message: missingMessage || error.message || 'source unavailable',
|
|
};
|
|
} finally {
|
|
if (pool) {
|
|
try {
|
|
await pool.close();
|
|
} catch {
|
|
// ignore close errors
|
|
}
|
|
try {
|
|
sql.close();
|
|
} catch {
|
|
// ignore close errors
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
const rows = await cloudDb.query(sqlText, params);
|
|
return { ok: true, rows: rows || [], message: '' };
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
rows: [],
|
|
message: missingMessage || error.message || 'source unavailable',
|
|
};
|
|
}
|
|
}
|
|
|
|
function classifyRisk(feeRatioGap) {
|
|
if (feeRatioGap === null || feeRatioGap === undefined || Number.isNaN(feeRatioGap)) {
|
|
return {
|
|
risk_level: 'missing_source',
|
|
risk_label: '待补数',
|
|
};
|
|
}
|
|
|
|
if (feeRatioGap <= -0.5) {
|
|
return {
|
|
risk_level: 'severe_over_fee',
|
|
risk_label: '严重超费比,请立即排查,减少高费比情况',
|
|
};
|
|
}
|
|
|
|
if (feeRatioGap >= -0.5 && feeRatioGap < -0.2) {
|
|
return {
|
|
risk_level: 'over_fee',
|
|
risk_label: '费比超过目标费比,需排查核对数据',
|
|
};
|
|
}
|
|
|
|
if (feeRatioGap > 0.05) {
|
|
return {
|
|
risk_level: 'optimize_conversion',
|
|
risk_label: '建议优化转化率',
|
|
};
|
|
}
|
|
|
|
if (feeRatioGap > 0) {
|
|
return {
|
|
risk_level: 'safe_expand',
|
|
risk_label: '酌情放大转化率',
|
|
};
|
|
}
|
|
|
|
return {
|
|
risk_level: 'neutral',
|
|
risk_label: '刚好打平',
|
|
};
|
|
}
|
|
|
|
function buildFollowUpHints(realPerformance) {
|
|
const hints = [];
|
|
|
|
if (realPerformance !== null && realPerformance !== undefined) {
|
|
hints.push(realPerformance < 1000 ? '真实业绩偏小,先判断是否值得继续加投或扩量' : '真实业绩有基础,可优先考虑放量或精细化优化');
|
|
} else {
|
|
hints.push('真实业绩缺失,建议补数后再判断放量价值');
|
|
}
|
|
|
|
return hints;
|
|
}
|
|
|
|
function normalizeConfigRows(rows) {
|
|
const config = { ...DEFAULT_CONFIG };
|
|
|
|
for (const row of rows || []) {
|
|
const key = normalizeText(row.config_key).toLowerCase();
|
|
if (!key) continue;
|
|
|
|
if (key === 'target_fee_ratio') {
|
|
config.target_fee_ratio = parsePercent(row.config_value, DEFAULT_CONFIG.target_fee_ratio);
|
|
} else if (key === 'platform') {
|
|
config.platform = normalizeText(row.config_value) || DEFAULT_CONFIG.platform;
|
|
} else if (key === 'listed_days_threshold') {
|
|
config.listed_days_threshold = Math.max(
|
|
1,
|
|
Number.parseInt(row.config_value, 10) || DEFAULT_CONFIG.listed_days_threshold
|
|
);
|
|
}
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
async function getFeeMonitorConfig() {
|
|
const rows = await database.local.query(
|
|
`SELECT config_key, config_value, remark, updated_at
|
|
FROM ${LOCAL_CONFIG_TABLE}
|
|
ORDER BY config_key ASC`
|
|
);
|
|
|
|
return normalizeConfigRows(rows || []);
|
|
}
|
|
|
|
async function upsertFeeMonitorConfig(input = {}, operatorId = null) {
|
|
const entries = [
|
|
['target_fee_ratio', parsePercent(input.targetFeeRatio ?? input.target_fee_ratio, DEFAULT_CONFIG.target_fee_ratio)],
|
|
['platform', normalizeText(input.platform) || DEFAULT_CONFIG.platform],
|
|
[
|
|
'listed_days_threshold',
|
|
Math.max(
|
|
1,
|
|
Number.parseInt(input.listedDaysThreshold ?? input.listed_days_threshold, 10) ||
|
|
DEFAULT_CONFIG.listed_days_threshold
|
|
),
|
|
],
|
|
];
|
|
|
|
for (const [configKey, configValue] of entries) {
|
|
await database.local.query(
|
|
`
|
|
INSERT INTO ${LOCAL_CONFIG_TABLE} (config_key, config_value, remark, updated_by)
|
|
VALUES (?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
config_value = VALUES(config_value),
|
|
remark = VALUES(remark),
|
|
updated_by = VALUES(updated_by),
|
|
updated_at = CURRENT_TIMESTAMP
|
|
`,
|
|
[configKey, String(configValue), normalizeText(input.remark) || null, operatorId || null]
|
|
);
|
|
}
|
|
|
|
return getFeeMonitorConfig();
|
|
}
|
|
|
|
async function buildSourceSnapshotQuery({ platform, listedDaysThreshold }) {
|
|
const sourceSql = `
|
|
WITH source_base AS (
|
|
SELECT
|
|
TRY_CONVERT(date, d.stat_date) AS stat_date,
|
|
NULLIF(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), d.skuoid))), '') AS skuoid,
|
|
NULLIF(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), d.item_id))), '') AS item_id,
|
|
NULLIF(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), d.ider))), '') AS ider,
|
|
NULLIF(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), d.cater))), '') AS cater,
|
|
NULLIF(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), d.dept_two))), '') AS dept_two,
|
|
NULLIF(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(200), d.shop_name))), '') AS shop_name,
|
|
NULLIF(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), d.platform))), '') AS platform,
|
|
TRY_CONVERT(DECIMAL(18, 4), d.real_conclude_fee) AS real_conclude_fee,
|
|
TRY_CONVERT(DECIMAL(18, 4), d.real_performance) AS real_performance
|
|
FROM ${SOURCE_FQN} d WITH (NOLOCK)
|
|
WHERE NULLIF(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), d.platform))), '') = @platform
|
|
AND NULLIF(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), d.skuoid))), '') IS NOT NULL
|
|
AND TRY_CONVERT(date, d.stat_date) IS NOT NULL
|
|
),
|
|
latest_stat AS (
|
|
SELECT MAX(stat_date) AS stat_date
|
|
FROM source_base
|
|
),
|
|
first_listed AS (
|
|
SELECT
|
|
skuoid,
|
|
MAX(item_id) AS item_id,
|
|
MAX(ider) AS ider,
|
|
MAX(cater) AS cater,
|
|
MAX(dept_two) AS dept_two,
|
|
shop_name,
|
|
platform,
|
|
MIN(stat_date) AS first_stat_date
|
|
FROM source_base
|
|
GROUP BY skuoid, shop_name, platform
|
|
),
|
|
agg AS (
|
|
SELECT
|
|
b.stat_date,
|
|
b.skuoid,
|
|
COALESCE(MAX(b.item_id), b.skuoid) AS item_id,
|
|
MAX(b.ider) AS ider,
|
|
MAX(b.cater) AS cater,
|
|
MAX(b.dept_two) AS dept_two,
|
|
b.shop_name,
|
|
b.platform,
|
|
fl.first_stat_date,
|
|
DATEDIFF(DAY, fl.first_stat_date, b.stat_date) AS listed_days,
|
|
SUM(ISNULL(b.real_conclude_fee, 0)) AS real_conclude_fee,
|
|
SUM(ISNULL(b.real_performance, 0)) AS real_performance
|
|
FROM source_base b
|
|
INNER JOIN latest_stat ls ON b.stat_date = ls.stat_date
|
|
INNER JOIN first_listed fl
|
|
ON fl.skuoid = b.skuoid
|
|
AND fl.shop_name = b.shop_name
|
|
AND fl.platform = b.platform
|
|
GROUP BY b.stat_date, b.skuoid, b.shop_name, b.platform, fl.first_stat_date
|
|
)
|
|
SELECT
|
|
stat_date,
|
|
skuoid,
|
|
item_id,
|
|
ider,
|
|
cater,
|
|
dept_two,
|
|
shop_name,
|
|
platform,
|
|
listed_days,
|
|
real_conclude_fee,
|
|
real_performance,
|
|
CASE
|
|
WHEN real_performance IS NULL OR real_performance = 0 THEN NULL
|
|
ELSE real_conclude_fee / NULLIF(real_performance, 0)
|
|
END AS real_fee_ratio
|
|
FROM agg
|
|
WHERE listed_days >= @listedDaysThreshold
|
|
ORDER BY stat_date DESC, real_fee_ratio ASC, real_conclude_fee DESC, skuoid ASC, shop_name ASC
|
|
`;
|
|
|
|
return {
|
|
ok: true,
|
|
sql: sourceSql,
|
|
params: {
|
|
platform,
|
|
listedDaysThreshold,
|
|
},
|
|
message: '',
|
|
};
|
|
}
|
|
|
|
function formatMonitorRow(row, targetFeeRatio) {
|
|
const realConcludeFee = toNumber(row.real_conclude_fee, null);
|
|
const realPerformance = toNumber(row.real_performance, null);
|
|
const realFeeRatio =
|
|
normalizeRatio(row.real_fee_ratio, null) ??
|
|
(realConcludeFee !== null && realPerformance ? realConcludeFee / realPerformance : null);
|
|
const feeRatioGap = realFeeRatio === null ? null : Number((targetFeeRatio - realFeeRatio).toFixed(4));
|
|
const risk = classifyRisk(feeRatioGap);
|
|
const statDate = row.stat_date ? dayjs(row.stat_date).format('YYYY-MM-DD') : null;
|
|
const skuoid = normalizeText(row.skuoid);
|
|
const itemId = normalizeText(row.item_id) || skuoid || null;
|
|
const ider = normalizeText(row.ider) || null;
|
|
const cater = normalizeText(row.cater) || null;
|
|
const deptTwo = normalizeText(row.dept_two) || null;
|
|
const shopName = normalizeText(row.shop_name) || null;
|
|
|
|
return {
|
|
stat_date: statDate,
|
|
skuoid,
|
|
item_id: itemId,
|
|
procode: itemId || skuoid,
|
|
ider,
|
|
cater,
|
|
dept_two: deptTwo,
|
|
shop_name: shopName,
|
|
listed_days: toNumber(row.listed_days, null),
|
|
target_fee_ratio: Number(targetFeeRatio.toFixed(4)),
|
|
real_conclude_fee: realConcludeFee,
|
|
real_performance: realPerformance,
|
|
real_fee_ratio: realFeeRatio,
|
|
fee_ratio_gap: feeRatioGap,
|
|
gmv: realPerformance,
|
|
owner: null,
|
|
dept: null,
|
|
risk_level: risk.risk_level,
|
|
risk_label: risk.risk_label,
|
|
follow_up_json: JSON.stringify(buildFollowUpHints(realPerformance)),
|
|
};
|
|
}
|
|
|
|
async function refreshFeeMonitorAlerts(input = {}, operatorId = null) {
|
|
const config = await getFeeMonitorConfig();
|
|
const platform = normalizeText(input.platform) || config.platform;
|
|
const targetFeeRatio = parsePercent(input.targetFeeRatio ?? input.target_fee_ratio, config.target_fee_ratio);
|
|
const listedDaysThreshold = Math.max(
|
|
1,
|
|
Number.parseInt(input.listedDaysThreshold ?? input.listed_days_threshold, 10) || config.listed_days_threshold
|
|
);
|
|
|
|
const sourceQuery = await buildSourceSnapshotQuery({ platform, listedDaysThreshold });
|
|
if (!sourceQuery.ok) {
|
|
return sourceQuery;
|
|
}
|
|
|
|
const sourceRowsResult = await runSourceQuery(sourceQuery.sql, sourceQuery.params, sourceQuery.message);
|
|
if (!sourceRowsResult.ok) {
|
|
return sourceRowsResult;
|
|
}
|
|
|
|
const formattedRows = sourceRowsResult.rows.map((row) => formatMonitorRow(row, targetFeeRatio));
|
|
|
|
await database.local.query(`DELETE FROM ${LOCAL_ALERT_TABLE} WHERE platform = ?`, [platform]);
|
|
|
|
for (const row of formattedRows) {
|
|
await database.local.query(
|
|
`
|
|
INSERT INTO ${LOCAL_ALERT_TABLE} (
|
|
stat_date, platform, skuoid, item_id, procode, ider, cater, dept_two, shop_name, listed_days,
|
|
target_fee_ratio, real_conclude_fee, real_performance, real_fee_ratio, fee_ratio_gap,
|
|
click_rate, conversion_rate, gmv,
|
|
risk_level, risk_label, follow_up_json, owner, dept, created_by, updated_by
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
skuoid = VALUES(skuoid),
|
|
item_id = VALUES(item_id),
|
|
procode = VALUES(procode),
|
|
ider = VALUES(ider),
|
|
cater = VALUES(cater),
|
|
dept_two = VALUES(dept_two),
|
|
shop_name = VALUES(shop_name),
|
|
listed_days = VALUES(listed_days),
|
|
target_fee_ratio = VALUES(target_fee_ratio),
|
|
real_conclude_fee = VALUES(real_conclude_fee),
|
|
real_performance = VALUES(real_performance),
|
|
real_fee_ratio = VALUES(real_fee_ratio),
|
|
fee_ratio_gap = VALUES(fee_ratio_gap),
|
|
click_rate = VALUES(click_rate),
|
|
conversion_rate = VALUES(conversion_rate),
|
|
gmv = VALUES(gmv),
|
|
risk_level = VALUES(risk_level),
|
|
risk_label = VALUES(risk_label),
|
|
follow_up_json = VALUES(follow_up_json),
|
|
owner = VALUES(owner),
|
|
dept = VALUES(dept),
|
|
updated_by = VALUES(updated_by),
|
|
updated_at = CURRENT_TIMESTAMP
|
|
`,
|
|
[
|
|
row.stat_date,
|
|
row.platform || platform,
|
|
row.skuoid,
|
|
row.item_id,
|
|
row.procode || row.item_id || row.skuoid || '',
|
|
row.ider,
|
|
row.cater,
|
|
row.dept_two,
|
|
row.shop_name || '',
|
|
row.listed_days,
|
|
row.target_fee_ratio,
|
|
row.real_conclude_fee,
|
|
row.real_performance,
|
|
row.real_fee_ratio,
|
|
row.fee_ratio_gap,
|
|
row.click_rate,
|
|
row.conversion_rate,
|
|
row.gmv,
|
|
row.risk_level,
|
|
row.risk_label,
|
|
row.follow_up_json,
|
|
row.owner,
|
|
row.dept,
|
|
operatorId || null,
|
|
operatorId || null,
|
|
]
|
|
);
|
|
}
|
|
|
|
return {
|
|
ok: true,
|
|
message: '费比监控刷新成功',
|
|
inserted: formattedRows.length,
|
|
config: {
|
|
target_fee_ratio: Number(targetFeeRatio.toFixed(4)),
|
|
platform,
|
|
listed_days_threshold: listedDaysThreshold,
|
|
},
|
|
};
|
|
}
|
|
|
|
async function listFeeMonitorAlerts(query = {}) {
|
|
const page = Math.max(1, Number.parseInt(query.page, 10) || 1);
|
|
const pageSize = Math.max(1, Math.min(200, Number.parseInt(query.pageSize, 10) || 20));
|
|
const offset = (page - 1) * pageSize;
|
|
const where = [];
|
|
const params = [];
|
|
|
|
const statDateStart = normalizeText(query.startDate || query.statDateStart);
|
|
const statDateEnd = normalizeText(query.endDate || query.statDateEnd);
|
|
const platform = normalizeText(query.platform);
|
|
const skuoid = normalizeText(query.skuoid || query.itemId || query.goodsId);
|
|
const shopName = normalizeText(query.shopName);
|
|
const riskLevel = normalizeText(query.riskLevel);
|
|
const owner = normalizeText(query.owner);
|
|
const dept = normalizeText(query.dept);
|
|
|
|
if (statDateStart) {
|
|
where.push('stat_date >= ?');
|
|
params.push(statDateStart);
|
|
}
|
|
if (statDateEnd) {
|
|
where.push('stat_date <= ?');
|
|
params.push(statDateEnd);
|
|
}
|
|
if (platform) {
|
|
where.push('platform = ?');
|
|
params.push(platform);
|
|
}
|
|
if (skuoid) {
|
|
where.push('(skuoid = ? OR item_id = ?)');
|
|
params.push(skuoid, skuoid);
|
|
}
|
|
if (shopName) {
|
|
where.push('shop_name = ?');
|
|
params.push(shopName);
|
|
}
|
|
if (riskLevel) {
|
|
where.push('risk_level = ?');
|
|
params.push(riskLevel);
|
|
}
|
|
if (owner) {
|
|
where.push('owner = ?');
|
|
params.push(owner);
|
|
}
|
|
if (dept) {
|
|
where.push('dept = ?');
|
|
params.push(dept);
|
|
}
|
|
|
|
const countRows = await database.local.query(
|
|
`
|
|
SELECT COUNT(1) AS total
|
|
FROM ${LOCAL_ALERT_TABLE}
|
|
${where.length ? `WHERE ${where.join(' AND ')}` : ''}
|
|
`,
|
|
params
|
|
);
|
|
const total = Number(countRows?.[0]?.total || 0);
|
|
|
|
const rows = await database.local.query(
|
|
`
|
|
SELECT
|
|
id,
|
|
stat_date AS statDate,
|
|
platform,
|
|
skuoid,
|
|
item_id AS itemId,
|
|
procode,
|
|
ider,
|
|
cater,
|
|
dept_two AS deptTwo,
|
|
shop_name AS shopName,
|
|
listed_days AS listedDays,
|
|
target_fee_ratio AS targetFeeRatio,
|
|
real_conclude_fee AS realConcludeFee,
|
|
real_performance AS realPerformance,
|
|
real_fee_ratio AS realFeeRatio,
|
|
fee_ratio_gap AS feeRatioGap,
|
|
click_rate AS clickRate,
|
|
conversion_rate AS conversionRate,
|
|
gmv,
|
|
risk_level AS riskLevel,
|
|
risk_label AS riskLabel,
|
|
follow_up_json AS followUpJson,
|
|
owner,
|
|
dept,
|
|
created_at AS createdAt,
|
|
updated_at AS updatedAt
|
|
FROM ${LOCAL_ALERT_TABLE}
|
|
${where.length ? `WHERE ${where.join(' AND ')}` : ''}
|
|
ORDER BY stat_date DESC, fee_ratio_gap ASC, real_conclude_fee DESC, id DESC
|
|
LIMIT ? OFFSET ?
|
|
`,
|
|
[...params, pageSize, offset]
|
|
);
|
|
|
|
return {
|
|
rows: rows || [],
|
|
total,
|
|
page,
|
|
pageSize,
|
|
};
|
|
}
|
|
|
|
async function getFeeMonitorDetail(query = {}) {
|
|
const skuoid = normalizeText(query.skuoid || query.itemId || query.goodsId);
|
|
const shopName = normalizeText(query.shopName);
|
|
const procode = normalizeText(query.procode);
|
|
|
|
const where = [];
|
|
const params = [];
|
|
|
|
if (skuoid) {
|
|
where.push('(skuoid = ? OR item_id = ?)');
|
|
params.push(skuoid, skuoid);
|
|
}
|
|
if (shopName) {
|
|
where.push('shop_name = ?');
|
|
params.push(shopName);
|
|
}
|
|
if (procode) {
|
|
where.push('procode = ?');
|
|
params.push(procode);
|
|
}
|
|
|
|
const rows = await database.local.query(
|
|
`
|
|
SELECT
|
|
id,
|
|
stat_date AS statDate,
|
|
platform,
|
|
skuoid,
|
|
item_id AS itemId,
|
|
procode,
|
|
ider,
|
|
cater,
|
|
dept_two AS deptTwo,
|
|
shop_name AS shopName,
|
|
listed_days AS listedDays,
|
|
real_conclude_fee AS realConcludeFee,
|
|
real_performance AS realPerformance,
|
|
real_fee_ratio AS realFeeRatio,
|
|
fee_ratio_gap AS feeRatioGap,
|
|
click_rate AS clickRate,
|
|
conversion_rate AS conversionRate,
|
|
gmv,
|
|
risk_level AS riskLevel,
|
|
risk_label AS riskLabel,
|
|
follow_up_json AS followUpJson,
|
|
owner,
|
|
dept,
|
|
created_at AS createdAt,
|
|
updated_at AS updatedAt
|
|
FROM ${LOCAL_ALERT_TABLE}
|
|
${where.length ? `WHERE ${where.join(' AND ')}` : ''}
|
|
ORDER BY stat_date DESC, id DESC
|
|
LIMIT 100
|
|
`,
|
|
params
|
|
);
|
|
|
|
return rows || [];
|
|
}
|
|
|
|
function getFeeMonitorMeta() {
|
|
return {
|
|
boardKey: 'fee-monitor',
|
|
title: '费比监控',
|
|
description: '监控京东供销铺货超过30天的 skuoid 费比差值,按费用合计 / 真实业绩 判定风险等级',
|
|
defaultDateRangeDays: 7,
|
|
supportedFilters: ['platform', 'shopName', 'skuoid', 'itemId', 'riskLevel', 'owner', 'dept'],
|
|
calcStatusDefault: 'ok',
|
|
defaultConfig: DEFAULT_CONFIG,
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
DEFAULT_CONFIG,
|
|
getFeeMonitorConfig,
|
|
getFeeMonitorDetail,
|
|
getFeeMonitorMeta,
|
|
listFeeMonitorAlerts,
|
|
refreshFeeMonitorAlerts,
|
|
upsertFeeMonitorConfig,
|
|
};
|