const dataSource = require('../config/database'); const planningDatabase = require('../services/planningDatabase'); const { ensurePlanningPddSkuDimensionSchema } = require('../services/planningSupportSchema'); const localDb = planningDatabase; const systemDb = dataSource.local; const PDD_ACCESS_DEPARTMENT_MAP = { '运营中心A': '运营中心A', '运营中心B': '运营中心B', '运营中心C': '运营中心C', '运营中心G': '运营中心G', '品牌中心': '龙晨品牌中心', '龙晨品牌中心': '龙晨品牌中心', }; const SPU_TABLE = 'op_pdd_sku_dimension_spu_daily'; const SKU_TABLE = 'op_pdd_sku_dimension_sku_daily'; const GOODS_TABLE = 'op_pdd_sku_dimension_goods_daily'; const CACHE_TTL_MS = 60 * 1000; const MAIN_COLUMNS = [ { key: 'spu', title: 'SPU' }, { key: 'goodsIdCount', title: '铺货商品id数' }, { key: 'activeGoodsIdCount', title: '动销商品id数' }, { key: 'skuListingActiveRate', title: 'SPU铺货动销率' }, { key: 'skuIdEfficiency', title: 'SPU链接效能' }, { key: 'isPriceBreak', title: '是否破价' }, { key: 'payOrdrAmt', title: '近7天真实销售额' }, { key: 'feeRatio', title: '近7天真实费比' }, { key: 'afterSaleRate', title: '近7天真实售后率' }, { key: 'goodsUv', title: '近7天访客数' }, { key: 'orderNum', title: '近7天订单量' }, { key: 'payOrdrUsrCnt', title: '近7天买家数' }, { key: 'payOrdrGoodsQty', title: '近7天成交件数' }, { key: 'ctr', title: '近7天点击率' }, { key: 'goodsVcr', title: '近7天成交转化率' }, { key: 'customerUnitPrice', title: '近7天客单价' }, { key: 'pieceUnitPrice', title: '近7天件单价' }, { key: 'spend', title: '近7天总花费' }, ]; const SKU_DETAIL_COLUMNS = [ { key: 'skuCode', title: 'SKU货号' }, ...MAIN_COLUMNS.slice(1), ]; const GOODS_DETAIL_COLUMNS = [ { key: 'goodsId', title: '商品id' }, { key: 'goodsName', title: '商品名称' }, { key: 'hdThumbUrl', title: '商品主图' }, { key: 'shopName', title: '店铺名称' }, { key: 'idOwner', title: 'id负责人' }, { key: 'ownerDept', title: '所属部门' }, { key: 'goodsUv', title: '近7天访客数' }, { key: 'inquiryNum', title: '近7天询单量' }, { key: 'goodsFavCnt', title: '近7天收藏用户' }, { key: 'goodsFavNum', title: '近7天收藏量' }, { key: 'orderUserCount', title: '近7天下单人数' }, { key: 'ctr', title: '近7天点击率' }, { key: 'clickConversionRate', title: '近7天点击转化率' }, { key: 'goodsVcr', title: '近7天成交转化率' }, { key: 'ordrVstrRto', title: '近7天下单率' }, { key: 'payOrdrRto', title: '近7天成交率' }, { key: 'imprUsrCnt', title: '近7天流失指数' }, { key: 'unpaidUserCount', title: '近7天未支付人数' }, { key: 'payOrdrAmt', title: '近7天真实销售额' }, { key: 'orderNum', title: '近7天订单量' }, { key: 'payOrdrUsrCnt', title: '近7天买家数' }, { key: 'payOrdrGoodsQty', title: '近7天成交件数' }, { key: 'customerUnitPrice', title: '近7天客单价' }, { key: 'pieceUnitPrice', title: '近7天件单价' }, { key: 'spend', title: '近7天总花费' }, { key: 'feeRatio', title: '近7天真实费比' }, { key: 'afterSaleRate', title: '近7天真实售后率' }, { key: 'afterSaleCount', title: '近7天售后单量' }, { key: 'badReviewCount', title: '近7天差评量' }, { key: 'isPriceBreak', title: '是否破价' }, ]; const MAIN_SORT_FIELDS = { spu: '`spu`', goodsIdCount: '`goodsIdCount`', activeGoodsIdCount: '`activeGoodsIdCount`', skuListingActiveRate: '`skuListingActiveRate`', skuIdEfficiency: '`skuIdEfficiency`', isPriceBreak: '`isPriceBreak`', payOrdrAmt: '`payOrdrAmt`', feeRatio: '`feeRatio`', afterSaleRate: '`afterSaleRate`', goodsUv: '`goodsUv`', orderNum: '`orderNum`', payOrdrUsrCnt: '`payOrdrUsrCnt`', payOrdrGoodsQty: '`payOrdrGoodsQty`', ctr: '`ctr`', goodsVcr: '`goodsVcr`', customerUnitPrice: '`customerUnitPrice`', pieceUnitPrice: '`pieceUnitPrice`', spend: '`spend`', }; const valueCache = new Map(); function getMemoryCache(key) { const cached = valueCache.get(key); if (!cached) return null; if (cached.expiresAt <= Date.now()) { valueCache.delete(key); return null; } return cached.value; } function setMemoryCache(key, value, ttlMs = CACHE_TTL_MS) { valueCache.set(key, { value, expiresAt: Date.now() + ttlMs, }); } function normalizeMultiValueQueryParam(value) { const rawValues = Array.isArray(value) ? value : [ value ]; return rawValues .flatMap((item) => String(item || '').split(/[,\uff0c]/)) .map((item) => item.trim()) .filter(Boolean); } 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); } function getEffectiveDepartmentFilters(accessScope, departmentValues = []) { const requestedDepartments = normalizePddDepartments(...departmentValues); if (accessScope?.isAllData) { return requestedDepartments; } const allowedDepartments = Array.isArray(accessScope?.allowedDepartments) ? accessScope.allowedDepartments : []; if (allowedDepartments.length === 0) { return null; } if (requestedDepartments.length === 0) { return allowedDepartments; } const allowedSet = new Set(allowedDepartments); return requestedDepartments.filter((item) => allowedSet.has(item)); } async function getPddDataAccessScope(userId) { const rows = await systemDb.query( `SELECT u.id, u.username, 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), }; } async function getLatestSnapshotInfo(tableName) { await ensurePlanningPddSkuDimensionSchema(); const cacheKey = `pdd:sku-snapshot:latest:${tableName}`; const cached = getMemoryCache(cacheKey); if (cached) { return cached; } const rows = await localDb.query( `SELECT DATE_FORMAT(\`statDate\`, '%Y-%m-%d') AS statDate, DATE_FORMAT(\`startDate\`, '%Y-%m-%d') AS startDate, DATE_FORMAT(\`endDate\`, '%Y-%m-%d') AS endDate FROM \`${tableName}\` ORDER BY \`statDate\` DESC LIMIT 1` ); const value = rows[0] || null; setMemoryCache(cacheKey, value, CACHE_TTL_MS); return value; } function buildLikeAnyClause(columnName, values, params) { if (!Array.isArray(values) || values.length === 0) { return ''; } const placeholders = values.map((value) => { params.push(`%${value}%`); return `${columnName} LIKE ?`; }); return `(${placeholders.join(' OR ')})`; } function getSortSql(sortField, sortOrder, defaultExpr, defaultDirection = 'DESC', tieBreaker = '') { const fieldExpr = MAIN_SORT_FIELDS[sortField] || defaultExpr; const direction = sortOrder === 'ascend' ? 'ASC' : (sortOrder === 'descend' ? 'DESC' : defaultDirection); return `${fieldExpr} ${direction}${tieBreaker}`; } function formatBoolText(value) { return Number(value || 0) === 1 ? '是' : '否'; } function normalizeMainRows(rows = []) { return rows.map((row) => ({ ...row, isPriceBreak: formatBoolText(row.isPriceBreak), })); } function normalizeGoodsRows(rows = []) { return rows.map((row) => ({ ...row, isPriceBreak: formatBoolText(row.isPriceBreak), })); } async function fetchSpuList({ statDate, startDate, endDate, page, pageSize, sortField, sortOrder, spu, shopNames, departments, idOwners, accessScope, }) { const whereParts = [ '`statDate` = ?' ]; const params = [ statDate ]; if (spu) { whereParts.push('`spu` LIKE ?'); params.push(`%${spu}%`); } const shopClause = buildLikeAnyClause('`shopNameList`', shopNames, params); if (shopClause) whereParts.push(shopClause); const departmentFilters = getEffectiveDepartmentFilters(accessScope, departments); if (departmentFilters === null) { return { success: true, data: { columns: MAIN_COLUMNS, rows: [], total: 0, page, pageSize, startDate, endDate, sortField: '', sortOrder: '', }, }; } if (Array.isArray(departmentFilters) && departmentFilters.length === 0 && departments.length > 0) { return { success: true, data: { columns: MAIN_COLUMNS, rows: [], total: 0, page, pageSize, startDate, endDate, sortField: '', sortOrder: '', }, }; } const departmentClause = buildLikeAnyClause('`ownerDeptList`', departmentFilters, params); if (departmentClause) whereParts.push(departmentClause); const idOwnerClause = buildLikeAnyClause('`idOwnerList`', idOwners, params); if (idOwnerClause) whereParts.push(idOwnerClause); const whereSql = `WHERE ${whereParts.join(' AND ')}`; const totalRows = await localDb.query( `SELECT COUNT(1) AS total FROM \`${SPU_TABLE}\` ${whereSql}`, params ); const total = Number(totalRows?.[0]?.total || 0); const orderBySql = getSortSql(sortField, sortOrder, '`payOrdrAmt`', 'DESC', ', `spu` ASC'); const listRows = await localDb.query( `SELECT \`spu\`, \`goodsIdCount\`, \`activeGoodsIdCount\`, \`skuListingActiveRate\`, \`skuIdEfficiency\`, \`isPriceBreak\`, \`payOrdrAmt\`, \`feeRatio\`, \`afterSaleRate\`, \`goodsUv\`, \`orderNum\`, \`payOrdrUsrCnt\`, \`payOrdrGoodsQty\`, \`ctr\`, \`goodsVcr\`, \`customerUnitPrice\`, \`pieceUnitPrice\`, \`spend\` FROM \`${SPU_TABLE}\` ${whereSql} ORDER BY ${orderBySql} LIMIT ? OFFSET ?`, [ ...params, pageSize, (page - 1) * pageSize ] ); return { success: true, data: { columns: MAIN_COLUMNS, rows: normalizeMainRows(listRows), total, page, pageSize, startDate, endDate, sortField: MAIN_SORT_FIELDS[sortField] ? sortField : '', sortOrder: MAIN_SORT_FIELDS[sortField] ? (sortOrder || 'descend') : '', }, }; } async function fetchSkuDetail({ spu, accessScope }) { const latest = await getLatestSnapshotInfo(SKU_TABLE); if (!latest?.statDate) { return { success: true, data: { spu, startDate: null, endDate: null, columns: SKU_DETAIL_COLUMNS, rows: [], }, }; } const whereParts = [ '`statDate` = ?', '`spu` = ?' ]; const params = [ latest.statDate, spu ]; const departmentFilters = getEffectiveDepartmentFilters(accessScope, []); if (departmentFilters === null) { return { success: true, data: { spu, startDate: latest.startDate, endDate: latest.endDate, columns: SKU_DETAIL_COLUMNS, rows: [], }, }; } if (Array.isArray(departmentFilters) && departmentFilters.length > 0) { const scopeClause = buildLikeAnyClause('`ownerDeptList`', departmentFilters, params); if (scopeClause) whereParts.push(scopeClause); } const rows = await localDb.query( `SELECT \`skuCode\`, \`goodsIdCount\`, \`activeGoodsIdCount\`, \`skuListingActiveRate\`, \`skuIdEfficiency\`, \`isPriceBreak\`, \`payOrdrAmt\`, \`feeRatio\`, \`afterSaleRate\`, \`goodsUv\`, \`orderNum\`, \`payOrdrUsrCnt\`, \`payOrdrGoodsQty\`, \`ctr\`, \`goodsVcr\`, \`customerUnitPrice\`, \`pieceUnitPrice\`, \`spend\` FROM \`${SKU_TABLE}\` WHERE ${whereParts.join(' AND ')} ORDER BY \`payOrdrAmt\` DESC, \`goodsIdCount\` DESC, \`skuCode\` ASC`, params ); return { success: true, data: { spu, startDate: latest.startDate, endDate: latest.endDate, columns: SKU_DETAIL_COLUMNS, rows: normalizeMainRows(rows), }, }; } async function fetchGoodsDetail({ skuCode, accessScope }) { const latest = await getLatestSnapshotInfo(GOODS_TABLE); if (!latest?.statDate) { return { success: true, data: { skuCode, startDate: null, endDate: null, columns: GOODS_DETAIL_COLUMNS, rows: [], }, }; } const whereParts = [ '`statDate` = ?', '`skuCode` = ?' ]; const params = [ latest.statDate, skuCode ]; const departmentFilters = getEffectiveDepartmentFilters(accessScope, []); if (departmentFilters === null) { return { success: true, data: { skuCode, startDate: latest.startDate, endDate: latest.endDate, columns: GOODS_DETAIL_COLUMNS, rows: [], }, }; } if (Array.isArray(departmentFilters) && departmentFilters.length > 0) { whereParts.push(`\`ownerDept\` IN (${new Array(departmentFilters.length).fill('?').join(', ')})`); params.push(...departmentFilters); } const rows = await localDb.query( `SELECT \`goodsId\`, \`goodsName\`, \`hdThumbUrl\`, \`shopName\`, \`idOwner\`, \`ownerDept\`, \`goodsUv\`, \`inquiryNum\`, \`goodsFavCnt\`, \`goodsFavNum\`, \`orderUserCount\`, \`ctr\`, \`clickConversionRate\`, \`goodsVcr\`, \`ordrVstrRto\`, \`payOrdrRto\`, \`imprUsrCnt\`, \`unpaidUserCount\`, \`payOrdrAmt\`, \`orderNum\`, \`payOrdrUsrCnt\`, \`payOrdrGoodsQty\`, \`customerUnitPrice\`, \`pieceUnitPrice\`, \`spend\`, \`feeRatio\`, \`afterSaleRate\`, \`afterSaleCount\`, \`badReviewCount\`, \`isPriceBreak\` FROM \`${GOODS_TABLE}\` WHERE ${whereParts.join(' AND ')} ORDER BY \`payOrdrAmt\` DESC, \`goodsUv\` DESC, \`goodsId\` ASC`, params ); return { success: true, data: { skuCode, startDate: latest.startDate, endDate: latest.endDate, columns: GOODS_DETAIL_COLUMNS, rows: normalizeGoodsRows(rows), }, }; } module.exports = function registerPddSkuDimensionBoard({ router, authMiddleware, }) { router.get('/pdd/product-dashboard/sku-dimension-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: '未获取到数据权限' }); } const latest = await getLatestSnapshotInfo(SPU_TABLE); const page = Math.max(parseInt(req.query.page || '1', 10), 1); const pageSize = Math.min(Math.max(parseInt(req.query.pageSize || '20', 10), 1), 500); const sortField = String(req.query.sortField || '').trim(); const sortOrder = String(req.query.sortOrder || '').trim().toLowerCase(); const spu = String(req.query.spu || '').trim(); const shopNames = normalizeMultiValueQueryParam(req.query.shopName); const departments = normalizeMultiValueQueryParam(req.query.department); const idOwners = normalizeMultiValueQueryParam(req.query.idOwners); if (!latest?.statDate) { return res.json({ success: true, data: { columns: MAIN_COLUMNS, rows: [], total: 0, page, pageSize, startDate: null, endDate: null, sortField: '', sortOrder: '', }, }); } const result = await fetchSpuList({ statDate: latest.statDate, startDate: latest.startDate, endDate: latest.endDate, page, pageSize, sortField, sortOrder, spu, shopNames, departments, idOwners, accessScope, }); res.json(result); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/sku-dimension-board/sku-detail', authMiddleware, async (req, res) => { try { const spu = String(req.query.spu || '').trim(); if (!spu) { return res.status(400).json({ success: false, error: 'spu is required' }); } const userId = req.user?.sub || req.user?.id || req.user?.userId; const accessScope = await getPddDataAccessScope(userId); if (!accessScope) { return res.status(403).json({ success: false, error: '未获取到数据权限' }); } res.json(await fetchSkuDetail({ spu, accessScope })); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/sku-dimension-board/goods-detail', authMiddleware, async (req, res) => { try { const skuCode = String(req.query.skuCode || '').trim(); if (!skuCode) { return res.status(400).json({ success: false, error: 'skuCode is required' }); } const userId = req.user?.sub || req.user?.id || req.user?.userId; const accessScope = await getPddDataAccessScope(userId); if (!accessScope) { return res.status(403).json({ success: false, error: '未获取到数据权限' }); } res.json(await fetchGoodsDetail({ skuCode, accessScope })); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); };