'use strict'; const dayjs = require('dayjs'); const dataSource = require('../config/database'); const { FOLLOW_UP_TARGET_TABLE, FOLLOW_UP_DAILY_TABLE, DEFAULT_GMV_THRESHOLD, DEFAULT_FEE_RATIO_THRESHOLD, DEFAULT_AFTER_SALE_RATE_THRESHOLD, } = require('../services/followUpMonitorSync'); const localDb = dataSource.local; const PLATFORM = 'pdd'; const PDD_ACCESS_DEPARTMENT_MAP = { '运营中心A': '运营中心A', '运营中心B': '运营中心B', '运营中心C': '运营中心C', '运营中心G': '运营中心G', '品牌中心': '龙晨品牌中心', '龙晨品牌中心': '龙晨品牌中心', }; const FOLLOW_UP_MONITOR_COLUMNS = [ { key: 'goodsId', title: '商品id' }, { key: 'goodsName', title: '商品名称' }, { key: 'shopName', title: '店铺名称' }, { key: 'idOwner', title: 'id负责人' }, { key: 'ownerDept', title: '所属部门' }, { key: 'status', title: '状态' }, { key: 'gmvThreshold', title: 'GMV阈值' }, { key: 'feeRatioThreshold', title: '费比阈值' }, { key: 'afterSaleRateThreshold', title: '售后阈值' }, { key: 'latestStatDate', title: '最新业务日期' }, { key: 'latestGmv', title: '当日GMV' }, { key: 'latestFeeRatio', title: '当日费比' }, { key: 'latestAfterSaleRate', title: '当日售后率' }, { key: 'gmvAlertDays', title: 'GMV连续异常天数' }, { key: 'feeRatioAlertDays', title: '费比连续异常天数' }, { key: 'afterSaleAlertDays', title: '售后连续异常天数' }, { key: 'warningLevel', title: '预警级别' }, { key: 'issueSummary', title: '异常摘要' }, { key: 'actionAdvice', title: '处理建议' }, { key: 'remark', title: '备注' }, { key: 'updatedAt', title: '更新时间' }, ]; function normalizePddDepartments(...departmentValues) { const result = new Set(); departmentValues .filter(Boolean) .flatMap((value) => String(value).split(/[\/,,、]/)) .map((value) => value.trim()) .filter(Boolean) .forEach((value) => { const mapped = PDD_ACCESS_DEPARTMENT_MAP[value]; if (mapped) { result.add(mapped); } }); return Array.from(result); } async function getPddDataAccessScope(userId) { const rows = await localDb.query( `SELECT u.id, u.role, u.department_names, d.name AS department_name FROM users u LEFT JOIN departments d ON u.department_id = d.id WHERE u.id = ? LIMIT 1`, [ userId ] ); const user = rows[0]; if (!user) { return null; } if (user.role === 'admin') { return { isAllData: true, allowedDepartments: [], }; } return { isAllData: false, allowedDepartments: normalizePddDepartments(user.department_names, user.department_name), }; } function toFiniteNumber(value, fallback = 0) { const numericValue = Number(value); return Number.isFinite(numericValue) ? numericValue : fallback; } function normalizeMonitorTargetStatus(value) { return String(value || '').trim().toLowerCase() === 'inactive' ? 'inactive' : 'active'; } function parseThreshold(value, fallback) { if (value === '' || value === undefined || value === null) { return fallback; } const parsed = Number(value); return Number.isFinite(parsed) ? parsed : fallback; } function validateThresholds({ gmvThreshold, feeRatioThreshold, afterSaleRateThreshold }) { if (gmvThreshold < 0) { const error = new Error('GMV阈值不能小于 0'); error.statusCode = 400; throw error; } if (afterSaleRateThreshold < 0 || afterSaleRateThreshold > 10) { const error = new Error('售后阈值只能填写 0-10'); error.statusCode = 400; throw error; } if (feeRatioThreshold < 20 || feeRatioThreshold > 100) { const error = new Error('费比阈值只能填写 20-100'); error.statusCode = 400; throw error; } } function countConsecutiveAlerts(rows, predicate) { let count = 0; for (const row of rows) { if (!predicate(row)) break; count += 1; } return count; } function buildFollowUpWarningMeta({ gmvAlertDays, feeRatioAlertDays, afterSaleAlertDays }) { const maxAlertDays = Math.max(gmvAlertDays, feeRatioAlertDays, afterSaleAlertDays); if (afterSaleAlertDays >= 20) { return { warningLevel: '立即下架', actionAdvice: '售后指标连续 20 天超阈值,建议立即下架并通知负责人处理。', }; } if (maxAlertDays >= 20) { return { warningLevel: '重点处理', actionAdvice: '指标异常已连续超过 20 天,建议升级到管理层并明确整改时限。', }; } if (maxAlertDays >= 10) { return { warningLevel: '预警', actionAdvice: '指标异常已连续超过 10 天,建议尽快复盘并执行优化动作。', }; } return { warningLevel: '正常', actionAdvice: '当前未达到连续预警阈值,继续观察即可。', }; } function buildIssueSummary({ gmvAlertDays, feeRatioAlertDays, afterSaleAlertDays }) { const segments = []; if (gmvAlertDays > 0) segments.push(`GMV连续异常${gmvAlertDays}天`); if (feeRatioAlertDays > 0) segments.push(`费比连续异常${feeRatioAlertDays}天`); if (afterSaleAlertDays > 0) segments.push(`售后连续异常${afterSaleAlertDays}天`); return segments.join(',') || '暂无连续异常'; } async function listFollowUpMonitorTargets() { return localDb.query( `SELECT id, goods_id AS goodsId, fee_ratio_threshold AS feeRatioThreshold, gmv_threshold AS gmvThreshold, after_sale_rate_threshold AS afterSaleRateThreshold, status, remark, created_by AS createdBy, updated_by AS updatedBy, DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s') AS createdAt, DATE_FORMAT(updated_at, '%Y-%m-%d %H:%i:%s') AS updatedAt FROM \`${FOLLOW_UP_TARGET_TABLE}\` WHERE platform = ? ORDER BY status = 'active' DESC, updated_at DESC, id DESC`, [ PLATFORM ] ); } async function getLatestDailyWindow() { const rows = await localDb.query( `SELECT DATE_FORMAT(MAX(stat_date), '%Y-%m-%d') AS latestStatDate FROM \`${FOLLOW_UP_DAILY_TABLE}\` WHERE platform = ?`, [ PLATFORM ] ); const latestStatDate = rows?.[0]?.latestStatDate || null; if (!latestStatDate) return null; return { latestStatDate, startDate: dayjs(latestStatDate).subtract(29, 'day').format('YYYY-MM-DD'), }; } async function getFollowUpMonitorSeries(targetIds, startDate, endDate) { if (!Array.isArray(targetIds) || targetIds.length === 0) { return new Map(); } const rows = await localDb.query( `SELECT target_id AS targetId, goods_id AS goodsId, goods_name AS goodsName, shop_name AS shopName, id_owner AS idOwner, owner_dept AS ownerDept, DATE_FORMAT(stat_date, '%Y-%m-%d') AS statDate, gmv, fee_ratio AS feeRatio, after_sale_rate AS afterSaleRate, gmv_alert_flag AS gmvAlertFlag, fee_ratio_alert_flag AS feeRatioAlertFlag, after_sale_alert_flag AS afterSaleAlertFlag FROM \`${FOLLOW_UP_DAILY_TABLE}\` WHERE platform = ? AND target_id IN (${targetIds.map(() => '?').join(', ')}) AND stat_date BETWEEN ? AND ? ORDER BY target_id ASC, stat_date DESC`, [ PLATFORM, ...targetIds, startDate, endDate ] ); const seriesMap = new Map(); rows.forEach((row) => { const targetId = Number(row.targetId || 0); if (!targetId) return; if (!seriesMap.has(targetId)) seriesMap.set(targetId, []); seriesMap.get(targetId).push({ ...row, gmv: toFiniteNumber(row.gmv, 0), feeRatio: toFiniteNumber(row.feeRatio, 0), afterSaleRate: toFiniteNumber(row.afterSaleRate, 0), gmvAlertFlag: Number(row.gmvAlertFlag || 0), feeRatioAlertFlag: Number(row.feeRatioAlertFlag || 0), afterSaleAlertFlag: Number(row.afterSaleAlertFlag || 0), }); }); return seriesMap; } async function buildFollowUpMonitorList(accessScope) { const targets = await listFollowUpMonitorTargets(); const activeTargets = targets.filter((item) => normalizeMonitorTargetStatus(item.status) === 'active'); const latestWindow = await getLatestDailyWindow(); if (!latestWindow) { return { success: true, data: { columns: FOLLOW_UP_MONITOR_COLUMNS, rows: [], total: 0, startDate: null, endDate: null, latestStatDate: null, }, }; } const seriesMap = await getFollowUpMonitorSeries( activeTargets.map((item) => item.id), latestWindow.startDate, latestWindow.latestStatDate ); const allowedDepartments = accessScope?.isAllData ? null : new Set(Array.isArray(accessScope?.allowedDepartments) ? accessScope.allowedDepartments : []); const rows = activeTargets.map((target) => { const targetRows = seriesMap.get(target.id) || []; const latestRow = targetRows[0] || null; if (latestRow && allowedDepartments && allowedDepartments.size > 0 && !allowedDepartments.has(String(latestRow.ownerDept || '').trim())) { return null; } if (!latestRow) { return { id: target.id, goodsId: target.goodsId, goodsName: '', shopName: '', idOwner: '', ownerDept: '', status: normalizeMonitorTargetStatus(target.status), gmvThreshold: target.gmvThreshold, feeRatioThreshold: target.feeRatioThreshold, afterSaleRateThreshold: target.afterSaleRateThreshold, latestStatDate: latestWindow.latestStatDate, latestGmv: 0, latestFeeRatio: 0, latestAfterSaleRate: 0, gmvAlertDays: 0, feeRatioAlertDays: 0, afterSaleAlertDays: 0, warningLevel: '无数据', issueSummary: '最近30天没有监控结果', actionAdvice: '请确认该商品ID是否存在拼多多源数据,或等待定时任务执行。', remark: target.remark || '', updatedAt: target.updatedAt, }; } const gmvAlertDays = countConsecutiveAlerts(targetRows, (row) => Number(row.gmvAlertFlag || 0) === 1); const feeRatioAlertDays = countConsecutiveAlerts(targetRows, (row) => Number(row.feeRatioAlertFlag || 0) === 1); const afterSaleAlertDays = countConsecutiveAlerts(targetRows, (row) => Number(row.afterSaleAlertFlag || 0) === 1); const warningMeta = buildFollowUpWarningMeta({ gmvAlertDays, feeRatioAlertDays, afterSaleAlertDays }); return { id: target.id, goodsId: target.goodsId, goodsName: latestRow.goodsName || '', shopName: latestRow.shopName || '', idOwner: latestRow.idOwner || '', ownerDept: latestRow.ownerDept || '', status: normalizeMonitorTargetStatus(target.status), gmvThreshold: target.gmvThreshold, feeRatioThreshold: target.feeRatioThreshold, afterSaleRateThreshold: target.afterSaleRateThreshold, latestStatDate: latestRow.statDate, latestGmv: latestRow.gmv, latestFeeRatio: latestRow.feeRatio, latestAfterSaleRate: latestRow.afterSaleRate, gmvAlertDays, feeRatioAlertDays, afterSaleAlertDays, warningLevel: warningMeta.warningLevel, issueSummary: buildIssueSummary({ gmvAlertDays, feeRatioAlertDays, afterSaleAlertDays }), actionAdvice: warningMeta.actionAdvice, remark: target.remark || '', updatedAt: target.updatedAt, }; }).filter(Boolean); return { success: true, data: { columns: FOLLOW_UP_MONITOR_COLUMNS, rows, total: rows.length, startDate: latestWindow.startDate, endDate: latestWindow.latestStatDate, latestStatDate: latestWindow.latestStatDate, }, }; } module.exports = function registerPddFollowUpBoard({ router, authMiddleware }) { router.get('/pdd/product-dashboard/follow-up-board/list', authMiddleware, async (req, res) => { try { const userId = req.user?.sub || req.user?.id || req.user?.userId; if (!userId) { return res.status(401).json({ success: false, error: '未获取到登录用户信息' }); } const accessScope = await getPddDataAccessScope(userId); if (!accessScope) { return res.status(403).json({ success: false, error: '未获取到数据权限' }); } res.json(await buildFollowUpMonitorList(accessScope)); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/follow-up-board/targets', authMiddleware, async (_req, res) => { try { const rows = await listFollowUpMonitorTargets(); res.json({ success: true, data: rows }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.post('/pdd/product-dashboard/follow-up-board/targets', authMiddleware, async (req, res) => { try { const userId = req.user?.sub || req.user?.id || req.user?.userId || null; const goodsId = String(req.body?.goodsId || '').trim(); if (!goodsId) { return res.status(400).json({ success: false, error: 'goodsId is required' }); } const gmvThreshold = parseThreshold(req.body?.gmvThreshold, DEFAULT_GMV_THRESHOLD); const feeRatioThreshold = parseThreshold(req.body?.feeRatioThreshold, DEFAULT_FEE_RATIO_THRESHOLD); const afterSaleRateThreshold = parseThreshold(req.body?.afterSaleRateThreshold, DEFAULT_AFTER_SALE_RATE_THRESHOLD); validateThresholds({ gmvThreshold, feeRatioThreshold, afterSaleRateThreshold }); const status = normalizeMonitorTargetStatus(req.body?.status); const remark = String(req.body?.remark || '').trim() || null; await localDb.query( `INSERT INTO \`${FOLLOW_UP_TARGET_TABLE}\` (platform, goods_id, gmv_threshold, fee_ratio_threshold, after_sale_rate_threshold, status, remark, created_by, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE gmv_threshold = VALUES(gmv_threshold), fee_ratio_threshold = VALUES(fee_ratio_threshold), after_sale_rate_threshold = VALUES(after_sale_rate_threshold), status = VALUES(status), remark = VALUES(remark), updated_by = VALUES(updated_by)`, [ PLATFORM, goodsId, gmvThreshold, feeRatioThreshold, afterSaleRateThreshold, status, remark, userId, userId ] ); res.json({ success: true }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.post('/pdd/product-dashboard/follow-up-board/targets/:id/toggle', authMiddleware, async (req, res) => { try { const userId = req.user?.sub || req.user?.id || req.user?.userId || null; const id = Math.max(parseInt(req.params.id || '0', 10), 0); if (!id) { return res.status(400).json({ success: false, error: 'id is required' }); } await localDb.query( `UPDATE \`${FOLLOW_UP_TARGET_TABLE}\` SET status = CASE WHEN status = 'active' THEN 'inactive' ELSE 'active' END, updated_by = ? WHERE id = ? AND platform = ?`, [ userId, id, PLATFORM ] ); const rows = await localDb.query( `SELECT goods_id AS goodsId, status FROM \`${FOLLOW_UP_TARGET_TABLE}\` WHERE id = ? AND platform = ? LIMIT 1`, [ id, PLATFORM ] ); res.json({ success: true }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.delete('/pdd/product-dashboard/follow-up-board/targets/:id', authMiddleware, async (req, res) => { try { const id = Math.max(parseInt(req.params.id || '0', 10), 0); if (!id) { return res.status(400).json({ success: false, error: 'id is required' }); } await localDb.query(`DELETE FROM \`${FOLLOW_UP_DAILY_TABLE}\` WHERE target_id = ? AND platform = ?`, [ id, PLATFORM ]); await localDb.query(`DELETE FROM \`${FOLLOW_UP_TARGET_TABLE}\` WHERE id = ? AND platform = ?`, [ id, PLATFORM ]); res.json({ success: true }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); };