/** * 閮ㄩ棬鍒嗘瀽API * 鎻愪緵鍚勯儴闂ㄧ淮搴︾殑鏁版嵁鎺ュ彛 */ const express = require('express'); const router = express.Router(); const dayjs = require('dayjs'); const dataSource = require('../config/database'); const redis = require('../config/redis'); const cloudDb = require('../config/cloudDatabase'); const planningDatabase = require('../services/planningDatabase'); const authMiddleware = require('../middleware/auth'); const dataPageCache = require('../services/dataPageCache'); const { BRAND_OPTIONS, getBrandAnalysisDetails, getBrandAnalysisList, } = require('../services/brandAnalysis'); const { getPddNewProductSelectionCategories, getPddNewProductSelectionLibraryFilterOptions, getPddNewProductSelectionLibraryList, getPddNewProductSelectionLogDetail, getPddNewProductSelectionLogs, searchPddNewProductSelection, } = require('../services/aiImageSearch'); const { queryLogisticsPickupBoard, normalizeSnapshotData, readLogisticsPickupBoardSnapshot, } = require('../services/logisticsPickupBoard'); const { queryLogisticsDeliveryDurationBoard, queryLogisticsDeliveryDurationDetails, } = require('../services/logisticsDeliveryDurationBoard'); const registerPddSkuDimensionBoard = require('./pddSkuDimensionBoard'); const { ensurePlanningStockSelectionSchema } = require('../services/planningSupportSchema'); const localDb = dataSource.local; const stockSelectionDb = planningDatabase; const IDENTIFIER_REGEXP = /^[A-Za-z_][A-Za-z0-9_]*$/; const PDD_DATA_GOODS_SCHEMA = 'dbo'; const PDD_DATA_GOODS_TABLE = 'pdd_DataGoods'; const PDD_SPREAD_GOODS_TABLE = 'pdd_ReportGoodsSpread'; const PDD_EVALUATION_TABLE = 'pdd_Evaluation'; const PDD_JST_SKU_TABLE_FULL_NAME = 'dbo.jst_sku'; const PDD_STOCK_SELECTION_BASE_TABLE = 'op_BarcodeChannelDecisionBase'; const PDD_STOCK_SELECTION_RESULT_TABLE = 'op_BarcodeChannelDecisionResult'; const PDD_TARGET_TABLE_FULL_NAME = 'JSDataMiddlePlatform.dbo.op_TargetManagement'; const PDD_EMPLOYEE_REGISTER_TABLE_FULL_NAME = 'JSDataMiddlePlatform.dbo.hr_employee_register'; const PDD_DATA_GOODS_FIELDS = [ { key: 'shopName', label: '店铺名称' }, { key: 'createTime', label: '发生时间' }, { key: 'goodsId', label: '商品id' }, { key: 'goodsName', label: '商品名称' }, { key: 'hdThumbUrl', label: '商品图' }, { key: 'goodsUv', label: '商品访客数' }, { key: 'goodsPv', label: '商品浏览量' }, { key: 'payOrdrAmt', label: '成交金额' }, { key: 'payOrdrGoodsQty', label: '成交件数' }, { key: 'payOrdrCnt', label: '成交订单数' }, { key: 'payOrdrUsrCnt', label: '成交买家数' }, { key: 'goodsVcr', label: '成交转化率' }, { key: 'goodsFavCnt', label: '商品收藏用户数' }, { key: 'ordrVstrRto', label: '下单率' }, { key: 'payOrdrRto', label: '成交率' }, { key: 'imprUsrCnt', label: '流量损失指数' }, { key: 'goodsPtHelpRate', label: '商品平台求助率' }, ]; const PDD_SPREAD_GOODS_FIELDS = [ { key: 'goodsName', label: '商品名称' }, { key: 'goodsId', label: '商品id' }, { key: 'minGroupPrice', label: '拼单价(厘)' }, { key: 'targetRoi', label: '目标投产比' }, { key: 'spend', label: '总花费' }, { key: 'gmv', label: '交易额' }, { key: 'roi', label: '实际投产比' }, { key: 'orderNum', label: '成交笔数' }, { key: 'transactionCost', label: '每笔成交花费' }, { key: 'avgPayAmount', label: '每笔成交金额' }, { key: 'directGmv', label: '直接交易额' }, { key: 'indirectGmv', label: '间接交易额' }, { key: 'directOrderNum', label: '直接成交笔数' }, { key: 'indirectOrderNum', label: '间接成交笔数' }, { key: 'avgDirectPayAmount', label: '每笔直接成交金额' }, { key: 'avgIndirectPayAmount', label: '每笔间接成交金额' }, { key: 'globalTakeRate', label: '全站推广费用占比' }, { key: 'impression', label: '曝光量' }, { key: 'click', label: '点击量' }, { key: 'ctr', label: '点击率' }, { key: 'cvr', label: '点击转化率' }, { key: 'shopName', label: '店铺名称' }, { key: 'createTime', label: '抓取时间' }, { key: 'orderSpend', label: '成交花费' }, { key: 'netGmv', label: '净交易额' }, { key: 'netRoi', label: '净实际投产比' }, { key: 'netOrderNum', label: '净成交笔数' }, { key: 'netAvgPayAmount', label: '每笔净成交花费' }, { key: 'netRate', label: '净交易额占比' }, { key: 'settlementGmv', label: '结算金额' }, { key: 'settlementRoi', label: '结算投产比' }, { key: 'settlementOrder', label: '结算笔数' }, { key: 'refundGmvRate30d', label: '退款率' }, { key: 'refundOrderRate30d', label: '退单率' }, { key: 'exemptRefundGmvRate30d', label: '退款豁免率' }, { key: 'exemptRefundOrderRate30d', label: '退单豁免率' }, { key: 'settlementGmvRate', label: '交易额结算率' }, { key: 'settlementOrderRate', label: '订单结算率' }, { key: 'costPerSettlementOrderForDeal', label: '结算订单成本' }, { key: 'inquirySpend', label: '询单花费' }, { key: 'multiGoalInquiryNum', label: '询单量' }, { key: 'multiGoalCostPerInquiry', label: '平均询单成本' }, { key: 'goodsFavSpend', label: '收藏花费' }, { key: 'multiGoalGoodsFavNum', label: '收藏量' }, { key: 'multiGoalCostPerGoodsFav', label: '平均收藏成本' }, { key: 'mallFavSpend', label: '关注花费' }, { key: 'multiGoalMallFavNum', label: '关注量' }, { key: 'multiGoalCostPerMallFav', label: '平均关注成本' }, { key: 'billingImpression', label: '推广曝光量' }, { key: 'settlementOrderSpend', label: '实际成交花费' }, { key: 'targetBid', label: '净成交出价' }, ]; const PDD_EVALUATION_FIELDS = [ { key: 'descScore', label: '评价分' }, { key: 'comment', label: '评价内容' }, { key: 'created', label: '评价时间' }, { key: 'orderSn', label: '订单号' }, { key: 'name', label: '买家昵称' }, { key: 'goodsName', label: '商品名称' }, { key: 'goodsId', label: '商品id' }, { key: 'thumbUrl', label: '商品图' }, { key: 'specs', label: '购买明细' }, { key: 'keywords', label: '标签' }, { key: 'shopName', label: '店铺名称' }, { key: 'createTime', label: '发生时间' }, ]; const PDD_JOIN_EXTRA_FIELDS = [ { key: 'idOwner', label: 'id负责人', expr: 'tm.[ider]' }, { key: 'ownerDept', label: '所属部门', expr: 'tm.[dept]' }, ]; const PDD_SPREAD_EXTRA_FIELDS = [ { key: 'hdThumbUrl', label: '商品图', expr: 'dg.[hdThumbUrl]' }, ...PDD_JOIN_EXTRA_FIELDS, ]; const PDD_DATA_GOODS_DERIVED_FIELDS = [ { key: 'orderUserCount', label: '下单人数', expr: 'CAST(TRY_CONVERT(DECIMAL(38, 6), g.[goodsUv]) * TRY_CONVERT(DECIMAL(38, 6), g.[ordrVstrRto]) AS DECIMAL(38, 6))', }, { key: 'unpaidUserCount', label: '未支付人数', expr: 'CAST((TRY_CONVERT(DECIMAL(38, 6), g.[goodsUv]) * TRY_CONVERT(DECIMAL(38, 6), g.[ordrVstrRto])) - TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrUsrCnt]) AS DECIMAL(38, 6))', }, { key: 'customerUnitPrice', label: '客单价', expr: 'CAST(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrAmt]) / NULLIF(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrUsrCnt]), 0) AS DECIMAL(38, 6))', }, { key: 'pieceUnitPrice', label: '件单价', expr: 'CAST(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrAmt]) / NULLIF(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrGoodsQty]), 0) AS DECIMAL(38, 6))', }, ]; const PDD_DEPARTMENT_FIELD_CANDIDATES = [ 'departmentName', 'department', 'dept', 'belongDept', 'belongDepartment', 'cater', 'currCater', ]; const PDD_SHOP_FIELD_CANDIDATES = [ 'shopName', 'shop_name', 'storeName', 'store_name', 'mallName', 'mall_name', ]; const SQL_TEXT_TYPES = new Set([ 'char', 'nchar', 'varchar', 'nvarchar', 'text', 'ntext', ]); const SQL_DATE_TYPES = new Set([ 'date', 'datetime', 'datetime2', 'smalldatetime', 'datetimeoffset', 'time', ]); const SQL_NUMBER_TYPES = new Set([ 'bigint', 'int', 'smallint', 'tinyint', 'decimal', 'numeric', 'float', 'real', 'money', 'smallmoney', ]); const PDD_SPREAD_DIVIDE_BY_1000_FIELDS = new Set([ 'minGroupPrice', 'spend', 'gmv', 'transactionCost', 'avgPayAmount', 'directGmv', 'indirectGmv', 'avgDirectPayAmount', 'avgIndirectPayAmount', 'orderSpend', 'netGmv', 'netAvgPayAmount', 'settlementGmv', 'costPerSettlementOrderForDeal', 'inquirySpend', 'multiGoalCostPerInquiry', 'goodsFavSpend', 'multiGoalCostPerGoodsFav', 'mallFavSpend', 'multiGoalCostPerMallFav', 'settlementOrderSpend', 'targetBid', ]); const PDD_SPREAD_DIVIDE_BY_10000_FIELDS = new Set([ 'targetRoi', ]); const PDD_META_CACHE_TTL_MS = 10 * 60 * 1000; const PDD_COUNT_CACHE_TTL_MS = 60 * 1000; const PDD_DASHBOARD_BIZ_DATE_CACHE_TTL_SECONDS = 180; const PDD_PRODUCT_BOARD_CACHE_VERSION = 'v1'; const ANALYSIS_HOT_CACHE_VERSION = 'v1'; const ANALYSIS_HOT_MEMORY_TTL_SECONDS = 10 * 60; const ANALYSIS_HOT_REDIS_TTL_SECONDS = 30 * 60; const PDD_LATEST_DATE_CACHE_TTL_MS = 60 * 1000; const PDD_DASHBOARD_RESULT_CACHE_TTL_MS = 60 * 1000; const PDD_PRICE_LIMIT_INDEX_CACHE_TTL_MS = 5 * 60 * 1000; const PDD_PRICE_LIMIT_STATE_CACHE_TTL_MS = 60 * 1000; const PDD_PRICE_LIMIT_TABLE_SPECS = [ { tableName: 'Bulk_Price_Limit', keyFields: [ 'product_code', 'style_code', 'spu' ], ordinaryFields: [ 'price_limit', 'min_price', 'alimama_limit_price', 'jingxi_limit_price_1', 'jingxi_limit_price_2', 'jingxi_limit_price' ], spuFields: [ 'price_limit', 'min_price', 'alimama_limit_price', 'jingxi_limit_price_1', 'jingxi_limit_price_2', 'jingxi_limit_price' ], orderField: 'process_end_date', }, { tableName: 'Brand_Price_Limit', keyFields: [ 'spu', 'product_no', 'sku_id', 'style_code' ], ordinaryFields: [ 'non_spu_manager_limit_price_new', 'non_spu_manager_limit_price', 'main_sales_pdd_subsidy_limit_price', 'pdd_limit_price_new' ], spuFields: [ 'expected_daily_limit_price', 'main_sales_pdd_subsidy_limit_price', 'pdd_limit_price_new' ], fallbackFields: [ 'expected_daily_limit_price', 'main_sales_pdd_subsidy_limit_price', 'pdd_limit_price_new' ], managerField: 'spu_manager', orderField: 'update_date', }, { tableName: 'Combo_Product_Price_Limit', keyFields: [ 'combo_sku_id', 'child_sku_id', 'style_code' ], ordinaryFields: [ 'final_combo_product_limit_price', 'combo_product_limit_price_aux', 'price_limit', 'pdd_al_limit_price', 'jingxi_limit_price', 'combo_product_jingxi_limit_price' ], spuFields: [ 'final_combo_product_limit_price', 'combo_product_limit_price_aux', 'price_limit', 'pdd_al_limit_price', 'jingxi_limit_price', 'combo_product_jingxi_limit_price' ], orderField: 'modified_time', }, { tableName: 'Brand_Combo_Code_Price_Limit', keyFields: [ 'combo_sku_id', 'child_sku_id', 'spu', 'product_no', 'style_code' ], ordinaryFields: [ 'non_spu_manager_limit_price_new', 'non_spu_manager_limit_price', 'main_sales_pdd_subsidy_limit_price', 'pdd_limit_price_new' ], spuFields: [ 'spu_manager_limit_price_combo_2', 'spu_manager_limit_price_combo', 'expected_daily_limit_price', 'main_sales_pdd_subsidy_limit_price', 'pdd_limit_price_new' ], fallbackFields: [ 'expected_daily_limit_price', 'main_sales_pdd_subsidy_limit_price', 'pdd_limit_price_new' ], managerField: 'spu_manager', orderField: 'update_date', }, ]; function getDefaultAnalysisDateRange() { return { startDate: dayjs().startOf('month').format('YYYY-MM-DD'), endDate: dayjs().format('YYYY-MM-DD'), }; } async function getCachedAnalysisPayload({ scope, payload, label, memoryTtlSeconds = ANALYSIS_HOT_MEMORY_TTL_SECONDS, redisTtlSeconds = ANALYSIS_HOT_REDIS_TTL_SECONDS, loader, }) { const cacheKey = dataPageCache.buildHashedCacheKey( 'analysis:hot', { scope, ...(payload || {}), }, ANALYSIS_HOT_CACHE_VERSION ); return dataPageCache.getOrSetCachedPayload({ key: cacheKey, memoryTtlSeconds, redisTtlSeconds, label, loader, }); } async function queryWarehouseKpi(start, end) { const result = await localDb.query(` SELECT COALESCE(SUM(ship_count), 0) as shipCount, COALESCE(SUM(refund_count), 0) as refundCount, ROUND(COALESCE(SUM(refund_count), 0) * 100.0 / NULLIF(SUM(ship_count), 0), 2) as refundRate, COUNT(DISTINCT warehouse_name) as warehouseCount FROM summary_daily_warehouse WHERE stat_date BETWEEN ? AND ? `, [start, end]); return result[0] || {}; } async function queryWarehouseRanking(start, end, limit) { return localDb.query(` SELECT warehouse_name as name, SUM(ship_count) as shipCount, SUM(refund_count) as refundCount, ROUND(SUM(refund_count) * 100.0 / NULLIF(SUM(ship_count), 0), 2) as refundRate, ROUND(AVG(NULLIF(timely_rate, 0)), 1) as timelyRate FROM summary_daily_warehouse WHERE stat_date BETWEEN ? AND ? GROUP BY warehouse_name ORDER BY refundRate ASC LIMIT ? `, [start, end, parseInt(limit, 10)]); } async function queryLogisticsKpi(start, end) { const result = await localDb.query(` SELECT COALESCE(SUM(ship_count), 0) as shipCount, COALESCE(SUM(refund_count), 0) as refundCount, ROUND(COALESCE(SUM(refund_count), 0) * 100.0 / NULLIF(SUM(ship_count), 0), 2) as refundRate, ROUND(AVG(avg_delivery_hours), 1) as avgDeliveryHours, ROUND(AVG(pickup_timely_rate), 2) as pickupTimelyRate, COALESCE(SUM(damage_count), 0) as damageCount, COALESCE(SUM(damage_base_count), 0) as damageBaseCount, ROUND(COALESCE(SUM(damage_count), 0) * 100.0 / NULLIF(SUM(damage_base_count), 0), 2) as damageRate, ROUND(AVG(bad_review_rate), 2) as badReviewRate, COUNT(DISTINCT courier_name) as courierCount FROM summary_daily_courier WHERE stat_date BETWEEN ? AND ? `, [start, end]); return result[0] || {}; } async function queryLogisticsRanking(start, end, limit) { return localDb.query(` SELECT c.courier_name as name, SUM(c.ship_count) as shipCount, SUM(c.refund_count) as refundCount, ROUND(SUM(c.refund_count) * 100.0 / NULLIF(SUM(c.ship_count), 0), 2) as refundRate, ROUND(AVG(NULLIF(c.avg_delivery_hours, 0)), 1) as avgDeliveryHours, ROUND( (SUM(COALESCE(p.total_order_count, 0)) - SUM(COALESCE(p.timeout_count, 0))) * 100.0 / NULLIF(SUM(COALESCE(p.total_order_count, 0)), 0), 2 ) as pickupRate, SUM(COALESCE(p.total_order_count, 0)) as pickupBaseCount, ROUND(AVG(c.bad_review_rate), 2) as badReviewRate, SUM(COALESCE(c.damage_count, 0)) as damageCount, SUM(COALESCE(c.damage_base_count, 0)) as damageBaseCount, ROUND(SUM(COALESCE(c.damage_count, 0)) * 100.0 / NULLIF(SUM(COALESCE(c.damage_base_count, 0)), 0), 2) as damageRate FROM summary_daily_courier c LEFT JOIN summary_daily_pickup_courier p ON c.stat_date = p.stat_date AND c.courier_name = p.courier_name WHERE c.stat_date BETWEEN ? AND ? GROUP BY c.courier_name ORDER BY refundRate ASC LIMIT ? `, [start, end, parseInt(limit, 10)]); } async function queryLogisticsAbnormalTypes(start, end) { const sql = ` WITH abnormal_base AS ( SELECT CASE WHEN ISNULL(e.first_question_type, N'') LIKE N'%涓欢%' OR ISNULL(e.first_question_type, N'') LIKE N'%涓㈠け%' OR ISNULL(e.logistic_status_name, N'') LIKE N'%涓欢%' OR ISNULL(e.logistic_status_name, N'') LIKE N'%涓㈠け%' OR ISNULL(e.last_logistic_info, N'') LIKE N'%涓欢%' OR ISNULL(e.last_logistic_info, N'') LIKE N'%涓㈠け%' THEN N'涓欢' WHEN ISNULL(e.first_question_type, N'') LIKE N'%鐮存崯%' OR ISNULL(e.first_question_type, N'') LIKE N'%鎹熷潖%' OR ISNULL(e.logistic_status_name, N'') LIKE N'%鐮存崯%' OR ISNULL(e.logistic_status_name, N'') LIKE N'%鎹熷潖%' OR ISNULL(e.last_logistic_info, N'') LIKE N'%鐮存崯%' OR ISNULL(e.last_logistic_info, N'') LIKE N'%鎹熷潖%' THEN N'鐮存崯' WHEN ISNULL(e.first_question_type, N'') LIKE N'%寤惰%' OR ISNULL(e.first_question_type, N'') LIKE N'%瓒呮椂%' OR ISNULL(e.first_question_type, N'') LIKE N'%婊炵暀%' OR ISNULL(e.logistic_status_name, N'') LIKE N'%寤惰%' OR ISNULL(e.logistic_status_name, N'') LIKE N'%瓒呮椂%' OR ISNULL(e.logistic_status_name, N'') LIKE N'%婊炵暀%' OR ISNULL(e.last_logistic_info, N'') LIKE N'%寤惰%' OR ISNULL(e.last_logistic_info, N'') LIKE N'%瓒呮椂%' OR ISNULL(e.last_logistic_info, N'') LIKE N'%婊炵暀%' THEN N'寤惰' WHEN ISNULL(e.first_question_type, N'') LIKE N'%鎷掓敹%' OR ISNULL(e.first_question_type, N'') LIKE N'%绛炬敹寮傚父%' OR ISNULL(e.first_question_type, N'') LIKE N'%閫€鍥?' OR ISNULL(e.logistic_status_name, N'') LIKE N'%鎷掓敹%' OR ISNULL(e.logistic_status_name, N'') LIKE N'%绛炬敹寮傚父%' OR ISNULL(e.logistic_status_name, N'') LIKE N'%閫€鍥?' OR ISNULL(e.last_logistic_info, N'') LIKE N'%鎷掓敹%' OR ISNULL(e.last_logistic_info, N'') LIKE N'%绛炬敹寮傚父%' OR ISNULL(e.last_logistic_info, N'') LIKE N'%閫€鍥?' THEN N'绛炬敹寮傚父' ELSE N'鍏朵粬寮傚父' END AS abnormalType, COUNT(DISTINCT e.so_id) AS orderCount FROM jst_order_express_details e WHERE CONVERT(date, e.send_time) BETWEEN @startDate AND @endDate AND ( ISNULL(e.first_question_type, N'') <> N'' OR ISNULL(e.logistic_status_name, N'') <> N'' OR ISNULL(e.last_logistic_info, N'') <> N'' ) GROUP BY CASE WHEN ISNULL(e.first_question_type, N'') LIKE N'%涓欢%' OR ISNULL(e.first_question_type, N'') LIKE N'%涓㈠け%' OR ISNULL(e.logistic_status_name, N'') LIKE N'%涓欢%' OR ISNULL(e.logistic_status_name, N'') LIKE N'%涓㈠け%' OR ISNULL(e.last_logistic_info, N'') LIKE N'%涓欢%' OR ISNULL(e.last_logistic_info, N'') LIKE N'%涓㈠け%' THEN N'涓欢' WHEN ISNULL(e.first_question_type, N'') LIKE N'%鐮存崯%' OR ISNULL(e.first_question_type, N'') LIKE N'%鎹熷潖%' OR ISNULL(e.logistic_status_name, N'') LIKE N'%鐮存崯%' OR ISNULL(e.logistic_status_name, N'') LIKE N'%鎹熷潖%' OR ISNULL(e.last_logistic_info, N'') LIKE N'%鐮存崯%' OR ISNULL(e.last_logistic_info, N'') LIKE N'%鎹熷潖%' THEN N'鐮存崯' WHEN ISNULL(e.first_question_type, N'') LIKE N'%寤惰%' OR ISNULL(e.first_question_type, N'') LIKE N'%瓒呮椂%' OR ISNULL(e.first_question_type, N'') LIKE N'%婊炵暀%' OR ISNULL(e.logistic_status_name, N'') LIKE N'%寤惰%' OR ISNULL(e.logistic_status_name, N'') LIKE N'%瓒呮椂%' OR ISNULL(e.logistic_status_name, N'') LIKE N'%婊炵暀%' OR ISNULL(e.last_logistic_info, N'') LIKE N'%寤惰%' OR ISNULL(e.last_logistic_info, N'') LIKE N'%瓒呮椂%' OR ISNULL(e.last_logistic_info, N'') LIKE N'%婊炵暀%' THEN N'寤惰' WHEN ISNULL(e.first_question_type, N'') LIKE N'%鎷掓敹%' OR ISNULL(e.first_question_type, N'') LIKE N'%绛炬敹寮傚父%' OR ISNULL(e.first_question_type, N'') LIKE N'%閫€鍥?' OR ISNULL(e.logistic_status_name, N'') LIKE N'%鎷掓敹%' OR ISNULL(e.logistic_status_name, N'') LIKE N'%绛炬敹寮傚父%' OR ISNULL(e.logistic_status_name, N'') LIKE N'%閫€鍥?' OR ISNULL(e.last_logistic_info, N'') LIKE N'%鎷掓敹%' OR ISNULL(e.last_logistic_info, N'') LIKE N'%绛炬敹寮傚父%' OR ISNULL(e.last_logistic_info, N'') LIKE N'%閫€鍥?' THEN N'绛炬敹寮傚父' ELSE N'鍏朵粬寮傚父' END ) SELECT abnormalType AS name, orderCount AS value, CAST(orderCount * 100.0 / NULLIF(SUM(orderCount) OVER (), 0) AS DECIMAL(10,2)) AS pct FROM abnormal_base ORDER BY orderCount DESC `; return cloudDb.query(sql, { startDate: start, endDate: end }); } async function queryLogisticsTrend(start, end, dimension) { let groupExpr = "DATE_FORMAT(stat_date, '%m-%d')"; if (dimension === 'weekly') { groupExpr = "CONCAT(DATE_FORMAT(MIN(stat_date), '%m-%d'), '~', DATE_FORMAT(MAX(stat_date), '%m-%d'))"; } else if (dimension === 'monthly') { groupExpr = "DATE_FORMAT(stat_date, '%Y-%m')"; } return localDb.query(` SELECT ${groupExpr} as date, ROUND(SUM(total_delivery_hours) / NULLIF(SUM(total_order_count), 0), 2) as avgTime, SUM(total_order_count) as orderCount FROM summary_daily_delivery WHERE stat_date BETWEEN ? AND ? GROUP BY ${dimension === 'daily' ? 'stat_date' : dimension === 'weekly' ? 'YEARWEEK(stat_date, 1)' : "DATE_FORMAT(stat_date, '%Y-%m')"} ORDER BY MIN(stat_date) ASC `, [start, end]); } async function queryRegionKpi(start, end) { const result = await localDb.query(` SELECT COALESCE(SUM(ship_count), 0) as shipCount, COALESCE(SUM(refund_count), 0) as refundCount, ROUND(COALESCE(SUM(refund_count), 0) * 100.0 / NULLIF(SUM(ship_count), 0), 2) as refundRate, ROUND(AVG(avg_delivery_hours), 1) as avgDeliveryHours, COUNT(DISTINCT province) as provinceCount FROM summary_daily_province WHERE stat_date BETWEEN ? AND ? `, [start, end]); return result[0] || {}; } async function queryRegionRanking(start, end, limit) { return localDb.query(` SELECT province as name, SUM(ship_count) as shipCount, SUM(refund_count) as refundCount, ROUND(SUM(refund_count) * 100.0 / NULLIF(SUM(ship_count), 0), 2) as refundRate, ROUND(AVG(avg_delivery_hours), 1) as avgDeliveryHours FROM summary_daily_province WHERE stat_date BETWEEN ? AND ? GROUP BY province ORDER BY shipCount DESC LIMIT ? `, [start, end, parseInt(limit, 10)]); } async function querySupplierRanking(start, end, limit) { return localDb.query(` SELECT supplier_name as name, SUM(ship_count) as shipCount, SUM(refund_count) as refundCount, ROUND(SUM(refund_count) * 100.0 / NULLIF(SUM(ship_count), 0), 2) as refundRate FROM summary_daily_supplier WHERE stat_date BETWEEN ? AND ? GROUP BY supplier_name ORDER BY refundRate ASC LIMIT ? `, [start, end, parseInt(limit, 10)]); } async function queryCsRanking(start, end, limit) { return localDb.query(` SELECT cs_name as name, SUM(ship_count) as shipCount, SUM(refund_count) as refundCount, ROUND(SUM(refund_count) * 100.0 / NULLIF(SUM(ship_count), 0), 2) as refundRate, SUM(compensation_count) as compensationCount, SUM(chat_issue_count) as chatIssueCount FROM summary_daily_cs WHERE stat_date BETWEEN ? AND ? GROUP BY cs_name ORDER BY refundRate ASC LIMIT ? `, [start, end, parseInt(limit, 10)]); } async function queryProcurementRanking(start, end, limit) { return localDb.query(` SELECT purchaser_name as name, SUM(ship_count) as shipCount, SUM(refund_count) as refundCount, ROUND(SUM(refund_count) * 100.0 / NULLIF(SUM(ship_count), 0), 2) as refundRate, SUM(under10_count) as under10Count FROM summary_daily_purchaser WHERE stat_date BETWEEN ? AND ? GROUP BY purchaser_name ORDER BY refundRate ASC LIMIT ? `, [start, end, parseInt(limit, 10)]); } async function queryDesignRanking(start, end, limit) { return localDb.query(` SELECT designer_name as name, SUM(ship_count) as shipCount, SUM(refund_count) as refundCount, ROUND(SUM(refund_count) * 100.0 / NULLIF(SUM(ship_count), 0), 2) as refundRate, SUM(problem_spu_count) as problemSpuCount FROM summary_daily_designer WHERE stat_date BETWEEN ? AND ? GROUP BY designer_name ORDER BY refundRate ASC LIMIT ? `, [start, end, parseInt(limit, 10)]); } async function queryChatIssues(start, end, limit) { return localDb.query(` SELECT main_reason as mainReason, second_reason as secondReason, SUM(issue_count) as count, ROUND(SUM(issue_count) * 100.0 / (SELECT SUM(issue_count) FROM summary_daily_chat_issue WHERE stat_date BETWEEN ? AND ?), 2) as percent FROM summary_daily_chat_issue WHERE stat_date BETWEEN ? AND ? GROUP BY main_reason, second_reason ORDER BY count DESC LIMIT ? `, [start, end, start, end, parseInt(limit, 10)]); } async function prewarmHotAnalysisPages() { const { startDate, endDate } = getDefaultAnalysisDateRange(); const warmTasks = [ () => getCachedAnalysisPayload({ scope: 'warehouse:kpi', payload: { startDate, endDate }, label: 'analysis:warehouse:kpi', loader: () => queryWarehouseKpi(startDate, endDate), }), () => getCachedAnalysisPayload({ scope: 'warehouse:ranking', payload: { startDate, endDate, limit: 20 }, label: 'analysis:warehouse:ranking', loader: () => queryWarehouseRanking(startDate, endDate, 20), }), () => getCachedAnalysisPayload({ scope: 'logistics:kpi', payload: { startDate, endDate }, label: 'analysis:logistics:kpi', loader: () => queryLogisticsKpi(startDate, endDate), }), () => getCachedAnalysisPayload({ scope: 'logistics:ranking', payload: { startDate, endDate, limit: 20 }, label: 'analysis:logistics:ranking', loader: () => queryLogisticsRanking(startDate, endDate, 20), }), () => getCachedAnalysisPayload({ scope: 'logistics:abnormal-types', payload: { startDate, endDate }, label: 'analysis:logistics:abnormal-types', loader: () => queryLogisticsAbnormalTypes(startDate, endDate), }), () => getCachedAnalysisPayload({ scope: 'logistics:trend', payload: { startDate, endDate, dimension: 'daily' }, label: 'analysis:logistics:trend', loader: () => queryLogisticsTrend(startDate, endDate, 'daily'), }), () => getCachedAnalysisPayload({ scope: 'region:kpi', payload: { startDate, endDate }, label: 'analysis:region:kpi', loader: () => queryRegionKpi(startDate, endDate), }), () => getCachedAnalysisPayload({ scope: 'region:ranking', payload: { startDate, endDate, limit: 30 }, label: 'analysis:region:ranking', loader: () => queryRegionRanking(startDate, endDate, 30), }), () => getCachedAnalysisPayload({ scope: 'supplier:ranking', payload: { startDate, endDate, limit: 20 }, label: 'analysis:supplier:ranking', loader: () => querySupplierRanking(startDate, endDate, 20), }), () => getCachedAnalysisPayload({ scope: 'cs:ranking', payload: { startDate, endDate, limit: 20 }, label: 'analysis:cs:ranking', loader: () => queryCsRanking(startDate, endDate, 20), }), () => getCachedAnalysisPayload({ scope: 'procurement:ranking', payload: { startDate, endDate, limit: 20 }, label: 'analysis:procurement:ranking', loader: () => queryProcurementRanking(startDate, endDate, 20), }), () => getCachedAnalysisPayload({ scope: 'design:ranking', payload: { startDate, endDate, limit: 20 }, label: 'analysis:design:ranking', loader: () => queryDesignRanking(startDate, endDate, 20), }), () => getCachedAnalysisPayload({ scope: 'chat-issues', payload: { startDate, endDate, limit: 20 }, label: 'analysis:chat-issues', loader: () => queryChatIssues(startDate, endDate, 20), }), ]; for (const run of warmTasks) { await run(); } console.log(`[AnalysisCache] 宸查鐑垎鏋愰〉榛樿鏌ヨ锛屾椂闂磋寖鍥?${startDate} ~ ${endDate}锛屼换鍔℃暟 ${warmTasks.length}`); return { startDate, endDate, taskCount: warmTasks.length }; } const PDD_PRODUCT_BOARD_LIST_MEMORY_TTL_SECONDS = 1800; const PDD_PRODUCT_BOARD_LIST_REDIS_TTL_SECONDS = 3600; const pddMetaCache = new Map(); const pddCountCache = new Map(); const pddLatestDateCache = new Map(); const pddDashboardResultCache = new Map(); const PDD_ACCESS_DEPARTMENT_MAP = { '杩愯惀涓績A': '杩愯惀涓績A', '杩愯惀涓績B': '杩愯惀涓績B', '杩愯惀涓績C': '杩愯惀涓績C', '杩愯惀涓績G': '杩愯惀涓績G', '鍝佺墝涓績': '榫欐櫒鍝佺墝涓績', '榫欐櫒鍝佺墝涓績': '榫欐櫒鍝佺墝涓績', }; function getMemoryCache(map, key) { const cached = map.get(key); if (!cached) return null; if (cached.expiresAt <= Date.now()) { map.delete(key); return null; } return cached.value; } function setMemoryCache(map, key, value, ttlMs) { map.set(key, { value, expiresAt: Date.now() + ttlMs, }); } 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 normalizeMultiValueQueryParam(value) { const rawValues = Array.isArray(value) ? value : [ value ]; return rawValues .flatMap((item) => String(item || '').split(/[,\uff0c]/)) .map((item) => item.trim()) .filter(Boolean); } async function getLatestStockSelectionStatDate() { await ensurePlanningStockSelectionSchema(); const rows = await stockSelectionDb.query( ` SELECT MAX(statDate) AS latestStatDate FROM \`${PDD_STOCK_SELECTION_RESULT_TABLE}\` ` ); return rows?.[0]?.latestStatDate || null; } async function getLatestPddDataGoodsDate() { const rows = await cloudDb.query( ` SELECT MAX(TRY_CONVERT(date, [createTime])) AS latestBizDate FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] WHERE TRY_CONVERT(date, [createTime]) IS NOT NULL ` ); return rows?.[0]?.latestBizDate || null; } async function getLatestPddTableDate(tableName) { const cacheKey = `latest-date:${tableName}`; const cachedValue = getMemoryCache(pddLatestDateCache, cacheKey); if (cachedValue !== null) { return cachedValue; } const rows = await cloudDb.query( ` SELECT MAX(TRY_CONVERT(date, [createTime])) AS latestBizDate FROM [${PDD_DATA_GOODS_SCHEMA}].[${tableName}] WHERE TRY_CONVERT(date, [createTime]) IS NOT NULL ` ); const latestBizDate = rows?.[0]?.latestBizDate || null; setMemoryCache(pddLatestDateCache, cacheKey, latestBizDate, PDD_LATEST_DATE_CACHE_TTL_MS); return latestBizDate; } async function getLatestPddDashboardBizDate(tableNames = []) { const uniqueTableNames = Array.from(new Set((tableNames || []).filter(Boolean))); if (uniqueTableNames.length === 0) { return null; } const cacheKey = dataPageCache.buildHashedCacheKey( 'pdd:dashboard:latest-biz-date', { tableNames: uniqueTableNames.sort() }, PDD_PRODUCT_BOARD_CACHE_VERSION ); const cached = await dataPageCache.getOrSetCachedPayload({ key: cacheKey, memoryTtlSeconds: PDD_DASHBOARD_BIZ_DATE_CACHE_TTL_SECONDS, redisTtlSeconds: PDD_DASHBOARD_BIZ_DATE_CACHE_TTL_SECONDS, loader: async () => { const dateValues = []; for (const tableName of uniqueTableNames) { const latestDate = await getLatestPddTableDate(tableName); if (latestDate) { dateValues.push(dayjs(latestDate).format('YYYY-MM-DD')); } } if (dateValues.length === 0) { return null; } return dateValues.reduce((minDate, currentDate) => ( dayjs(currentDate).isBefore(dayjs(minDate)) ? currentDate : minDate )); }, }); return cached.value; } async function getPddProductDashboardRequestContext(req, tableNames = []) { const page = safePositiveInt(req.query.page, 1, 1, 1000000); const pageSize = safePositiveInt(req.query.pageSize, 20, 1, 200); const accessScope = await getPddDataAccessScope(req.user.sub); if (!accessScope) { const error = new Error('user not found'); error.statusCode = 401; throw error; } const latestBizDate = await getLatestPddDashboardBizDate(tableNames); const endDate = latestBizDate ? dayjs(latestBizDate).format('YYYY-MM-DD') : null; const startDate = endDate ? dayjs(endDate).subtract(6, 'day').format('YYYY-MM-DD') : null; const prevStartDate = endDate ? dayjs(endDate).subtract(13, 'day').format('YYYY-MM-DD') : null; const prevEndDate = endDate ? dayjs(endDate).subtract(7, 'day').format('YYYY-MM-DD') : null; return { page, pageSize, accessScope, startDate, endDate, prevStartDate, prevEndDate, }; } function getPddProductDashboardCommonFilters(req) { return { shopName: String(req.query.shopName || '').trim(), goodsId: String(req.query.goodsId || '').trim(), skuCode: String(req.query.skuCode || '').trim(), department: String(req.query.department || '').trim(), idOwners: normalizeMultiValueQueryParam(req.query.idOwners || req.query.idOwner), }; } function shouldUsePddTargetJoinForDashboardFilters(req, accessScope) { const { department, idOwners } = getPddProductDashboardCommonFilters(req); return !accessScope.isAllData || Boolean(department) || idOwners.length > 0; } function appendPddCreateTimeRangeFilters({ whereParts, params, fieldExpr = 'g.[createTime]', startDate, endDate, startParamName = 'startDateTime', endParamName = 'endDateExclusive', } = {}) { if (startDate) { whereParts.push(`${fieldExpr} >= @${startParamName}`); params[startParamName] = `${dayjs(startDate).format('YYYY-MM-DD')} 00:00:00`; } if (endDate) { whereParts.push(`${fieldExpr} < @${endParamName}`); params[endParamName] = dayjs(endDate).add(1, 'day').format('YYYY-MM-DD 00:00:00'); } } function appendPddProductDashboardCommonFilters({ req, whereParts, params, accessScope, shopNameExpr = "TRY_CONVERT(NVARCHAR(255), g.[shopName])", goodsIdExpr = "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId])))", skuCodeExpr = null, departmentExpr = 'tm.[dept]', idOwnerExpr = 'tm.[ider]', scopeParamPrefix = 'scopeDept', shopNameParamName = 'shopName', goodsIdParamName = 'goodsId', skuCodeParamName = 'skuCode', departmentParamName = 'department', idOwnerParamPrefix = 'idOwner', } = {}) { const { shopName, goodsId, skuCode, department, idOwners } = getPddProductDashboardCommonFilters(req); if (!accessScope.isAllData) { if (accessScope.allowedDepartments.length === 0) { return false; } appendNamedInCondition(whereParts, params, accessScope.allowedDepartments, departmentExpr, scopeParamPrefix); } if (shopName) { whereParts.push(`${shopNameExpr} = @${shopNameParamName}`); params[shopNameParamName] = shopName; } if (goodsId) { whereParts.push(`${goodsIdExpr} = @${goodsIdParamName}`); params[goodsIdParamName] = goodsId; } if (skuCode && skuCodeExpr) { whereParts.push(`${skuCodeExpr} = @${skuCodeParamName}`); params[skuCodeParamName] = skuCode; } if (department && departmentExpr) { whereParts.push(`${departmentExpr} = @${departmentParamName}`); params[departmentParamName] = department; } if (idOwners.length > 0 && idOwnerExpr) { appendNamedInCondition(whereParts, params, idOwners, idOwnerExpr, idOwnerParamPrefix); } return true; } function getPddProductDashboardBaseColumns(extraColumns = []) { return [ { key: 'goodsId', title: '商品id' }, { key: 'goodsName', title: '商品名称' }, { key: 'hdThumbUrl', title: '商品主图' }, { key: 'idOwner', title: 'id负责人' }, { key: 'ownerDept', title: '所属部门' }, { key: 'shopName', title: '店铺名称' }, ...extraColumns, ]; } function buildPddDashboardAccessScopeCacheFragment(accessScope) { if (!accessScope || accessScope.isAllData) { return { scopeType: 'all', departments: [], }; } return { scopeType: 'department', departments: normalizePddDepartments(...(accessScope.allowedDepartments || [])) .map((item) => String(item).trim().toLowerCase()) .sort(), }; } function buildPddProductDashboardResponse({ page, pageSize, total, startDate, endDate, columns, rows, extra = {} }) { return { success: true, data: { page, pageSize, total, startDate, endDate, columns, rows: rows || [], ...extra, }, }; } function buildPddProductDashboardEmptyResponse({ page, pageSize, startDate, endDate, columns, extra = {} }) { return buildPddProductDashboardResponse({ page, pageSize, total: 0, startDate, endDate, columns, rows: [], extra, }); } async function getCachedPddProductDashboardPayload({ boardKey, reqQuery, page, pageSize, accessScope, startDate, endDate, columns, extraCachePayload = {}, extraResponseData = {}, loader, }) { const cacheKey = dataPageCache.buildHashedCacheKey( 'pdd:product-dashboard:list', { boardKey, query: reqQuery || {}, page, pageSize, startDate, endDate, accessScope: buildPddDashboardAccessScopeCacheFragment(accessScope), ...extraCachePayload, }, PDD_PRODUCT_BOARD_CACHE_VERSION ); const cached = await dataPageCache.getOrSetCachedPayload({ key: cacheKey, memoryTtlSeconds: PDD_PRODUCT_BOARD_LIST_MEMORY_TTL_SECONDS, redisTtlSeconds: PDD_PRODUCT_BOARD_LIST_REDIS_TTL_SECONDS, label: `pdd:${boardKey}:list`, loader: async () => { const result = await loader(); return buildPddProductDashboardResponse({ page, pageSize, total: result.total, startDate, endDate, columns, rows: result.rows, extra: extraResponseData, }); }, }); return cached.value; } function buildPddDashboardScopeJoinSql({ accessScope, params, whereParts, goodsIdExpr, alias = 'tm', }) { if (accessScope.isAllData) { return ''; } const joinSql = aliasPddTargetJoinSql(buildPddTargetJoinSqlByExpr({ includeIdOwner: false, goodsIdExpr, }), alias); appendNamedInCondition(whereParts, params, accessScope.allowedDepartments, `${alias}.[dept]`, `${alias}ScopeDept`); return joinSql; } function aliasPddTargetJoinSql(joinSql, alias) { return String(joinSql || '') .replace(/\btm\./g, `${alias}.`) .replace(/\)\s*tm\b/g, `) ${alias}`); } function buildStockSelectionWhereClause({ statDate, poolType, ruleCode, barcode, }) { const whereParts = [ 'r.`statDate` = ?' ]; const params = [ statDate ]; if (poolType) { whereParts.push('r.`poolType` = ?'); params.push(poolType); } if (ruleCode) { whereParts.push('r.`ruleCode` = ?'); params.push(ruleCode); } if (barcode) { whereParts.push('CAST(r.`barcode` AS CHAR) LIKE ?'); params.push(`%${barcode}%`); } return { whereSql: `WHERE ${whereParts.join(' AND ')}`, params, }; } function appendNamedInCondition(whereParts, params, values, fieldExpr, paramPrefix) { if (!Array.isArray(values) || values.length === 0) { return; } const placeholders = values.map((value, index) => { const key = `${paramPrefix}${index}`; params[key] = value; return `@${key}`; }); whereParts.push(`${fieldExpr} IN (${placeholders.join(', ')})`); } async function getPddDataAccessScope(userId) { const rows = await localDb.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: [], user, }; } const allowedDepartments = normalizePddDepartments(user.department_names, user.department_name); return { isAllData: false, allowedDepartments, user, }; } function safeIdentifier(name, fieldName) { if (!name || !IDENTIFIER_REGEXP.test(name)) { const error = new Error(`Invalid ${fieldName}`); error.statusCode = 400; throw error; } return name; } function safePositiveInt(value, defaultValue, minValue, maxValue) { const parsed = Number.parseInt(value, 10); if (!Number.isFinite(parsed)) return defaultValue; return Math.min(maxValue, Math.max(minValue, parsed)); } // ==================== 浠撳簱鍒嗘瀽 ==================== /** * 浠撳簱KPI */ function getPddSpreadFieldTitle(fieldDef) { if (fieldDef.key === 'minGroupPrice') { return '拼单价'; } return fieldDef.label; } function getPddSpreadFieldExpr(fieldKey) { if (PDD_SPREAD_DIVIDE_BY_1000_FIELDS.has(fieldKey)) { return `CAST(TRY_CONVERT(DECIMAL(38, 6), g.[${fieldKey}]) / 1000.0 AS DECIMAL(38, 6))`; } if (PDD_SPREAD_DIVIDE_BY_10000_FIELDS.has(fieldKey)) { return `CAST(TRY_CONVERT(DECIMAL(38, 6), g.[${fieldKey}]) / 10000.0 AS DECIMAL(38, 6))`; } return `g.[${fieldKey}]`; } async function getPddTableColumns(schemaName, tableName) { const metaCacheKey = `${schemaName}.${tableName}.columns`; let allColumns = getMemoryCache(pddMetaCache, metaCacheKey); if (!allColumns) { allColumns = await cloudDb.query( ` SELECT COLUMN_NAME AS columnName, DATA_TYPE AS dataType FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @schemaName AND TABLE_NAME = @tableName ORDER BY ORDINAL_POSITION `, { schemaName, tableName } ); setMemoryCache(pddMetaCache, metaCacheKey, allColumns || [], PDD_META_CACHE_TTL_MS); } return allColumns || []; } function buildPddTargetJoinSql({ includeIdOwner = true } = {}) { return buildPddTargetJoinSqlByExpr({ includeIdOwner, goodsIdExpr: getPddGoodsIdJoinExpr('g'), }); } function getPddGoodsIdJoinExpr(tableAlias = 'g') { return `LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), ${tableAlias}.[goodsId])))`; } function buildPddTargetJoinSqlByExpr({ includeIdOwner = true, goodsIdExpr, } = {}) { const selectFields = includeIdOwner ? ` MAX(src.[ider]) AS [ider], MAX(src.[dept]) AS [dept]` : ` MAX(src.[dept]) AS [dept]`; return ` OUTER APPLY ( SELECT ${selectFields} FROM ${PDD_TARGET_TABLE_FULL_NAME} src WHERE src.[shopproid] = ${goodsIdExpr} ) tm `; } function buildPddTargetApplySql({ includeIdOwner = true, goodsIdExpr, } = {}) { const selectFields = includeIdOwner ? ` MAX(src.[ider]) AS [ider], MAX(src.[dept]) AS [dept] ` : ` MAX(src.[dept]) AS [dept] `; return ` OUTER APPLY ( SELECT ${selectFields} FROM ${PDD_TARGET_TABLE_FULL_NAME} src WHERE src.[shopproid] = ${goodsIdExpr} ) tm `; } function buildPddTargetExtendedJoinSqlByExpr({ goodsIdExpr, fields = [ 'ider', 'dept', 'oper', 'spu' ], } = {}) { const validFields = Array.from(new Set((fields || []).filter(Boolean))); const selectFields = validFields .map((field) => `MAX(src.[${field}]) AS [${field}]`) .join(',\n '); return ` OUTER APPLY ( SELECT ${selectFields} FROM ${PDD_TARGET_TABLE_FULL_NAME} src WHERE src.[shopproid] = ${goodsIdExpr} ) tm `; } function buildPddDataGoodsImageApplySql({ goodsIdExpr, } = {}) { return ` OUTER APPLY ( SELECT TOP (1) TRY_CONVERT(NVARCHAR(1000), src.[hdThumbUrl]) AS [hdThumbUrl] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] src WHERE LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), src.[goodsId]))) = ${goodsIdExpr} AND TRY_CONVERT(NVARCHAR(1000), src.[hdThumbUrl]) IS NOT NULL AND LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(1000), src.[hdThumbUrl]))) <> '' ORDER BY TRY_CONVERT(DATETIME2, src.[createTime]) DESC, TRY_CONVERT(NVARCHAR(1000), src.[hdThumbUrl]) DESC ) dg `; } function buildPddPagedBaseSubquery({ schemaName, tableName, whereSql, orderExpr, orderDirection = 'DESC', fallbackOrderExpr = null, }) { const orderBySql = !fallbackOrderExpr || fallbackOrderExpr === orderExpr ? `${orderExpr} ${orderDirection}` : `${orderExpr} ${orderDirection}, ${fallbackOrderExpr} DESC`; return ` FROM ( SELECT * FROM [${schemaName}].[${tableName}] g ${whereSql} ORDER BY ${orderBySql} OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY ) g `; } function buildPagedRowsQuery({ selectFieldsSql, fromSql, whereSql, orderExpr, }) { return ` SELECT *, COUNT(1) OVER() AS __total ${fromSql} ${whereSql} ORDER BY ${orderExpr} DESC OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY `.replace('SELECT *', `SELECT ${selectFieldsSql}`); } function buildPddDisplayFieldDefs(selectedFieldDefs, options = {}) { const { extraFields = [], extraFieldsAfterKey = null, insertFieldGroups = [], getTitle = (fieldDef) => fieldDef.label, getExpr = (fieldDef) => `g.[${fieldDef.key}]`, getSortExpr = (fieldDef) => fieldDef.sortExpr || getExpr(fieldDef), } = options; const displayFieldDefs = []; const insertFieldGroupMap = new Map(); insertFieldGroups.forEach((group) => { if (!group?.afterKey || !Array.isArray(group.fields) || group.fields.length === 0) { return; } if (!insertFieldGroupMap.has(group.afterKey)) { insertFieldGroupMap.set(group.afterKey, []); } insertFieldGroupMap.get(group.afterKey).push(...group.fields); }); selectedFieldDefs.forEach((fieldDef) => { displayFieldDefs.push({ key: fieldDef.key, title: getTitle(fieldDef), expr: getExpr(fieldDef), sortExpr: getSortExpr(fieldDef), }); if (extraFieldsAfterKey && fieldDef.key === extraFieldsAfterKey) { extraFields.forEach((extraField) => { displayFieldDefs.push({ key: extraField.key, title: extraField.label, expr: extraField.expr, sortExpr: extraField.sortExpr || extraField.expr, }); }); } const groupedFields = insertFieldGroupMap.get(fieldDef.key) || []; groupedFields.forEach((extraField) => { displayFieldDefs.push({ key: extraField.key, title: extraField.label, expr: extraField.expr, sortExpr: extraField.sortExpr || extraField.expr, }); }); }); return displayFieldDefs; } async function queryPddPagedRows({ selectFieldsSql, fromSql, whereSql, orderExpr, params, page, pageSize, fallbackCountSql = null, }) { const pagingParams = { ...params, offset: (page - 1) * pageSize, pageSize, }; const rows = await cloudDb.query( buildPagedRowsQuery({ selectFieldsSql, fromSql, whereSql, orderExpr, }), pagingParams ); let total = Number(rows?.[0]?.__total || 0); const normalizedRows = (rows || []).map((row) => { const nextRow = { ...row }; delete nextRow.__total; return nextRow; }); if (normalizedRows.length === 0 && page > 1 && fallbackCountSql) { const totalRows = await cloudDb.query(fallbackCountSql, params); total = Number(totalRows?.[0]?.total || 0); } return { total, rows: normalizedRows, }; } async function queryPddDashboardBasePagedRows({ baseCteSql, selectFieldsSql, orderBySql, extraFromSql = '', params, page, pageSize, }) { const pagingParams = { ...params, offset: (page - 1) * pageSize, pageSize, }; const rows = await cloudDb.query( ` ${baseCteSql} SELECT ${selectFieldsSql}, COUNT(1) OVER() AS [__total] FROM base ${extraFromSql} ORDER BY ${orderBySql} OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY `, pagingParams ); const total = Number(rows?.[0]?.__total || 0); const normalizedRows = (rows || []).map((row) => { const nextRow = { ...row }; delete nextRow.__total; return nextRow; }); return { total, rows: normalizedRows, }; } async function hydratePddDashboardRowsWithGoodsDetails(rows, { detailTableName = PDD_DATA_GOODS_TABLE, startDate = null, endDate = null, includeIdOwner = true, includeImage = true, } = {}) { if (!Array.isArray(rows) || rows.length === 0) { return rows || []; } const goodsIds = Array.from(new Set(rows.map((row) => String(row.goodsId || '').trim()).filter(Boolean))); if (goodsIds.length === 0) { return rows; } const numericGoodsIds = goodsIds.every((value) => /^\d+$/.test(value)) ? goodsIds.map((value) => Number(value)) : null; const detailParams = {}; const detailWhereParts = []; const hasNativeImageColumn = detailTableName === PDD_DATA_GOODS_TABLE; appendNamedInCondition( detailWhereParts, detailParams, numericGoodsIds || goodsIds, numericGoodsIds ? 'src.[goodsId]' : 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), src.[goodsId])))', 'detailGoodsId' ); appendPddCreateTimeRangeFilters({ whereParts: detailWhereParts, params: detailParams, fieldExpr: 'src.[createTime]', startDate, endDate, startParamName: 'detailStartDateTime', endParamName: 'detailEndDateExclusive', }); const detailRows = await cloudDb.query( ` SELECT CONVERT(NVARCHAR(50), src.[goodsId]) AS [goodsId], MAX(TRY_CONVERT(NVARCHAR(500), src.[goodsName])) AS [goodsName], MAX(TRY_CONVERT(NVARCHAR(255), src.[shopName])) AS [shopName], ${includeImage && hasNativeImageColumn ? 'MAX(TRY_CONVERT(NVARCHAR(1000), src.[hdThumbUrl])) AS [hdThumbUrl],' : 'CAST(NULL AS NVARCHAR(1000)) AS [hdThumbUrl],'} CAST(NULL AS NVARCHAR(255)) AS [idOwner], CAST(NULL AS NVARCHAR(255)) AS [ownerDept] FROM [${PDD_DATA_GOODS_SCHEMA}].[${detailTableName}] src WHERE ${detailWhereParts.join(' AND ')} GROUP BY src.[goodsId] `, detailParams ); const detailMap = new Map((detailRows || []).map((row) => [ String(row.goodsId || ''), row ])); if (includeIdOwner) { const ownerParams = {}; const ownerWhereParts = [ 'src.[shopproid] IS NOT NULL', "LTRIM(RTRIM(src.[shopproid])) <> ''", ]; appendNamedInCondition( ownerWhereParts, ownerParams, goodsIds, 'src.[shopproid]', 'ownerGoodsId' ); const ownerRows = await cloudDb.query( ` SELECT src.[shopproid] AS [goodsId], MAX(src.[ider]) AS [idOwner], MAX(src.[dept]) AS [ownerDept] FROM ${PDD_TARGET_TABLE_FULL_NAME} src WHERE ${ownerWhereParts.join(' AND ')} GROUP BY src.[shopproid] `, ownerParams ); for (const ownerRow of ownerRows || []) { const key = String(ownerRow.goodsId || '').trim(); if (!key) { continue; } const detail = detailMap.get(key) || { goodsId: key }; detail.idOwner = ownerRow.idOwner || detail.idOwner || null; detail.ownerDept = ownerRow.ownerDept || detail.ownerDept || null; detailMap.set(key, detail); } } if (includeImage && !hasNativeImageColumn) { const imageParams = {}; const imageWhereParts = []; appendNamedInCondition( imageWhereParts, imageParams, numericGoodsIds || goodsIds, numericGoodsIds ? 'src.[goodsId]' : 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), src.[goodsId])))', 'imageGoodsId' ); const imageRows = await cloudDb.query( ` SELECT CONVERT(NVARCHAR(50), src.[goodsId]) AS [goodsId], MAX(TRY_CONVERT(NVARCHAR(1000), src.[hdThumbUrl])) AS [hdThumbUrl] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] src WHERE ${imageWhereParts.join(' AND ')} GROUP BY src.[goodsId] `, imageParams ); for (const imageRow of imageRows || []) { const key = String(imageRow.goodsId || '').trim(); if (!key) { continue; } const detail = detailMap.get(key) || { goodsId: key }; detail.hdThumbUrl = imageRow.hdThumbUrl || detail.hdThumbUrl || null; detailMap.set(key, detail); } } return rows.map((row) => { const key = String(row.goodsId || ''); const detail = detailMap.get(key) || {}; return { ...row, goodsName: detail.goodsName || row.goodsName || null, hdThumbUrl: includeImage ? (detail.hdThumbUrl || row.hdThumbUrl || null) : (row.hdThumbUrl || null), idOwner: includeIdOwner ? (detail.idOwner || row.idOwner || null) : (row.idOwner || null), ownerDept: includeIdOwner ? (detail.ownerDept || row.ownerDept || null) : (row.ownerDept || null), shopName: detail.shopName || row.shopName || null, }; }); } async function hydratePddDashboardRowsWithTargetFields(rows, fields = []) { if (!Array.isArray(rows) || rows.length === 0) { return rows || []; } const validFields = Array.from(new Set((fields || []).filter(Boolean))); if (validFields.length === 0) { return rows; } const goodsIds = Array.from(new Set(rows.map((row) => String(row.goodsId || '').trim()).filter(Boolean))); if (goodsIds.length === 0) { return rows; } const params = {}; const whereParts = [ 'src.[shopproid] IS NOT NULL', "LTRIM(RTRIM(src.[shopproid])) <> ''", ]; appendNamedInCondition(whereParts, params, goodsIds, 'src.[shopproid]', 'targetGoodsId'); const selectFieldsSql = validFields .map((field) => `MAX(src.[${field}]) AS [${field}]`) .join(',\n '); const targetRows = await cloudDb.query( ` SELECT src.[shopproid] AS [goodsId], ${selectFieldsSql} FROM ${PDD_TARGET_TABLE_FULL_NAME} src WHERE ${whereParts.join(' AND ')} GROUP BY src.[shopproid] `, params ); const targetMap = new Map((targetRows || []).map((row) => [ String(row.goodsId || '').trim(), row ])); return rows.map((row) => { const key = String(row.goodsId || '').trim(); const targetRow = targetMap.get(key) || {}; const nextRow = { ...row }; for (const field of validFields) { nextRow[field] = targetRow[field] || nextRow[field] || null; } return nextRow; }); } function getPddDashboardCacheKey(scope, payload) { return JSON.stringify({ scope, ...payload }); } async function getOrSetPddDashboardResultCache(cacheKey, loader, ttlMs = PDD_DASHBOARD_RESULT_CACHE_TTL_MS) { const cachedValue = getMemoryCache(pddDashboardResultCache, cacheKey); if (cachedValue !== null) { return cachedValue; } const value = await loader(); setMemoryCache(pddDashboardResultCache, cacheKey, value, ttlMs); return value; } function normalizePddPriceLimitIdentifier(value) { if (value === null || value === undefined) { return ''; } return String(value).trim().toLowerCase(); } function toPddPositiveNumber(value, fallback = null) { const numeric = Number(value); return Number.isFinite(numeric) && numeric > 0 ? numeric : fallback; } function pickLowestPddPriceLimitCandidate(row, fields = []) { return fields .map((field) => ({ field, value: toPddPositiveNumber(row?.[field], null), })) .filter((item) => item.value !== null) .sort((left, right) => left.value - right.value)[0] || null; } function getPddPriceLimitSelectFields(spec) { return Array.from(new Set([ ...(spec.keyFields || []), ...(spec.ordinaryFields || []), ...(spec.spuFields || []), ...(spec.fallbackFields || []), spec.managerField, spec.orderField, ].filter(Boolean))); } function getPddPriceLimitSortValue(value) { if (!value) { return 0; } const parsed = dayjs(value); return parsed.isValid() ? parsed.valueOf() : 0; } async function getPddPriceLimitSourceState() { const cacheKey = 'pdd:price-limit:source-state'; return getOrSetPddDashboardResultCache(cacheKey, async () => { const params = PDD_PRICE_LIMIT_TABLE_SPECS.reduce((acc, spec, index) => { acc[`tableName${index}`] = spec.tableName; return acc; }, {}); const rows = await cloudDb.query( ` SELECT t.name AS [tableName], SUM(p.rows) AS [rowCount] FROM sys.tables t INNER JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1) WHERE t.name IN (${PDD_PRICE_LIMIT_TABLE_SPECS.map((_, index) => `@tableName${index}`).join(', ')}) GROUP BY t.name `, params ); const rowMap = new Map((rows || []).map((row) => [ String(row.tableName || ''), Number(row.rowCount || 0) ])); const tables = PDD_PRICE_LIMIT_TABLE_SPECS.map((spec) => ({ tableName: spec.tableName, rowCount: rowMap.get(spec.tableName) || 0, })); return { ok: true, tables, totalRows: tables.reduce((sum, item) => sum + Number(item.rowCount || 0), 0), }; }, PDD_PRICE_LIMIT_STATE_CACHE_TTL_MS); } async function loadPddPriceLimitTableIndex(spec) { const cacheKey = `pdd:price-limit:index:${spec.tableName}`; return getOrSetPddDashboardResultCache(cacheKey, async () => { const selectSql = getPddPriceLimitSelectFields(spec) .map((field) => `[${field}] AS [${field}]`) .join(',\n '); const rows = await cloudDb.query( ` SELECT ${selectSql} FROM dbo.[${spec.tableName}] `, {}, { requestTimeout: 120000 } ); const keyIndex = new Map(); for (const row of rows || []) { const sortValue = getPddPriceLimitSortValue(row?.[spec.orderField]); for (const fieldName of spec.keyFields || []) { const identifier = normalizePddPriceLimitIdentifier(row?.[fieldName]); if (!identifier) { continue; } const existed = keyIndex.get(identifier); if (!existed || sortValue >= existed.__sortValue) { keyIndex.set(identifier, { ...row, __sortValue: sortValue, }); } } } return { ok: true, tableName: spec.tableName, keyIndex, }; }, PDD_PRICE_LIMIT_INDEX_CACHE_TTL_MS); } async function loadPddPriceLimitIndexes(activeTableNames = []) { const activeSet = new Set((activeTableNames || []).map((item) => String(item))); const results = await Promise.all( PDD_PRICE_LIMIT_TABLE_SPECS .filter((spec) => activeSet.size === 0 || activeSet.has(spec.tableName)) .map(async (spec) => [ spec.tableName, await loadPddPriceLimitTableIndex(spec) ]) ); return new Map(results); } function queryPddPriceLimitCandidates({ identifiers = [], ownerOper = '' }, priceLimitIndexes) { const normalizedIdentifiers = Array.from(new Set( (identifiers || []) .map((item) => normalizePddPriceLimitIdentifier(item)) .filter(Boolean) )); if (normalizedIdentifiers.length === 0) { return []; } const normalizedOwnerOper = normalizePddPriceLimitIdentifier(ownerOper); const results = []; for (const spec of PDD_PRICE_LIMIT_TABLE_SPECS) { const indexPayload = priceLimitIndexes?.get(spec.tableName); if (!indexPayload?.ok || !indexPayload.keyIndex) { continue; } const matchedRows = []; const dedupeKeys = new Set(); for (const identifier of normalizedIdentifiers) { const matchedRow = indexPayload.keyIndex.get(identifier); if (!matchedRow) { continue; } const dedupeKey = JSON.stringify(getPddPriceLimitSelectFields(spec).map((field) => matchedRow?.[field] ?? null)); if (dedupeKeys.has(dedupeKey)) { continue; } dedupeKeys.add(dedupeKey); matchedRows.push(matchedRow); } let bestCandidate = null; for (const row of matchedRows) { const normalizedManager = normalizePddPriceLimitIdentifier(spec.managerField ? row?.[spec.managerField] : row?.spu_manager); const useSpuBranch = Boolean(normalizedOwnerOper && normalizedManager && normalizedOwnerOper === normalizedManager); const directCandidate = pickLowestPddPriceLimitCandidate(row, useSpuBranch ? spec.spuFields : spec.ordinaryFields); const fallbackCandidate = directCandidate || pickLowestPddPriceLimitCandidate(row, spec.fallbackFields || []); if (!fallbackCandidate) { continue; } const candidatePayload = { tableName: spec.tableName, limitFieldUsed: fallbackCandidate.field, effectiveLimitPrice: fallbackCandidate.value, }; if (!bestCandidate || candidatePayload.effectiveLimitPrice < bestCandidate.effectiveLimitPrice) { bestCandidate = candidatePayload; } } if (bestCandidate) { results.push(bestCandidate); } } return results.sort((left, right) => left.effectiveLimitPrice - right.effectiveLimitPrice); } async function loadPddPriceBreakFlagByGoodsIds({ goodsIds = [], startDate, endDate }) { const normalizedGoodsIds = Array.from(new Set( (goodsIds || []) .map((item) => String(item || '').trim()) .filter(Boolean) )); if (normalizedGoodsIds.length === 0 || !startDate || !endDate) { return new Map(); } const priceLimitState = await getPddPriceLimitSourceState(); if (!priceLimitState?.ok || Number(priceLimitState.totalRows || 0) <= 0) { return new Map(normalizedGoodsIds.map((goodsId) => [ goodsId, false ])); } const activeTables = (priceLimitState.tables || []) .filter((item) => Number(item.rowCount || 0) > 0) .map((item) => item.tableName); const priceLimitIndexes = await loadPddPriceLimitIndexes(activeTables); if (!(priceLimitIndexes instanceof Map) || priceLimitIndexes.size === 0) { return new Map(normalizedGoodsIds.map((goodsId) => [ goodsId, false ])); } const params = { startDate, endDate }; const whereParts = [ 'g.[goodsId] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) <> ''", ]; appendNamedInCondition( whereParts, params, normalizedGoodsIds, 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId])))', 'priceBreakGoodsId' ); appendPddCreateTimeRangeFilters({ whereParts, params, fieldExpr: 'g.[createTime]', startDate, endDate, }); const baseRows = await cloudDb.query( ` SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) AS [goodsId], MAX(TRY_CONVERT(NVARCHAR(255), tm.[oper])) AS [oper], MAX(TRY_CONVERT(NVARCHAR(255), tm.[spu])) AS [spu], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrAmt]), 0)) AS [payAmt], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrGoodsQty]), 0)) AS [goodsQty] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${buildPddTargetExtendedJoinSqlByExpr({ goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId])))', })} WHERE ${whereParts.join(' AND ')} GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) HAVING SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrGoodsQty]), 0)) > 0 `, params, { requestTimeout: 120000 } ); if (!Array.isArray(baseRows) || baseRows.length === 0) { return new Map(normalizedGoodsIds.map((goodsId) => [ goodsId, false ])); } const uniqueSpus = Array.from(new Set( baseRows .map((row) => String(row.spu || '').trim()) .filter(Boolean) )); const spuIdentifierMap = new Map(); if (uniqueSpus.length > 0) { const skuParams = {}; const skuWhereParts = [ 'src.[other_1] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), src.[other_1]))) <> ''", ]; appendNamedInCondition( skuWhereParts, skuParams, uniqueSpus, 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), src.[other_1])))', 'priceBreakSpu' ); const skuRows = await cloudDb.query( ` SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), src.[other_1]))) AS [spu], LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), src.[sku_id]))) AS [skuId], LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), src.[other_7]))) AS [productNo], LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), src.[i_id]))) AS [styleCode] FROM ${PDD_JST_SKU_TABLE_FULL_NAME} src WHERE ${skuWhereParts.join(' AND ')} `, skuParams, { requestTimeout: 120000 } ); for (const row of skuRows || []) { const spu = String(row.spu || '').trim(); if (!spu) { continue; } if (!spuIdentifierMap.has(spu)) { spuIdentifierMap.set(spu, new Set([ spu ])); } [ row.skuId, row.productNo, row.styleCode ].forEach((value) => { const normalizedValue = String(value || '').trim(); if (normalizedValue) { spuIdentifierMap.get(spu).add(normalizedValue); } }); } } const resultMap = new Map(normalizedGoodsIds.map((goodsId) => [ goodsId, false ])); for (const row of baseRows) { const pieceUnitPrice = Number(row.goodsQty) > 0 ? Number(row.payAmt) / Number(row.goodsQty) : null; if (!Number.isFinite(pieceUnitPrice) || pieceUnitPrice <= 0) { continue; } const spu = String(row.spu || '').trim(); const identifierSet = new Set([ spu ].filter(Boolean)); const extraIdentifiers = spuIdentifierMap.get(spu); if (extraIdentifiers) { extraIdentifiers.forEach((value) => identifierSet.add(value)); } const candidates = queryPddPriceLimitCandidates({ identifiers: Array.from(identifierSet), ownerOper: row.oper, }, priceLimitIndexes); const bestCandidate = candidates[0] || null; if (!bestCandidate || !Number.isFinite(bestCandidate.effectiveLimitPrice)) { continue; } if (pieceUnitPrice < Number(bestCandidate.effectiveLimitPrice)) { resultMap.set(String(row.goodsId || '').trim(), true); } } return resultMap; } function getPddColumnSortExpr({ fieldKey, fieldExpr, availableColumns, columnTypeMap, tableAlias = 'g', }) { if (!availableColumns.has(fieldKey)) { return fieldExpr; } const dataType = columnTypeMap.get(fieldKey); if (SQL_NUMBER_TYPES.has(dataType)) { return `TRY_CONVERT(DECIMAL(38, 6), ${tableAlias}.[${fieldKey}])`; } if (SQL_DATE_TYPES.has(dataType)) { return `TRY_CONVERT(DATETIME2, ${tableAlias}.[${fieldKey}])`; } if (SQL_TEXT_TYPES.has(dataType)) { return `TRY_CONVERT(NVARCHAR(255), ${tableAlias}.[${fieldKey}])`; } return fieldExpr; } function createPddShopOptionsHandler({ schemaName, tableName }) { return async (req, res) => { try { const keyword = (req.query.keyword || '').trim(); const accessScope = await getPddDataAccessScope(req.user.sub); if (!accessScope) { return res.status(401).json({ success: false, error: 'user not found' }); } if (!accessScope.isAllData && accessScope.allowedDepartments.length === 0) { return res.json({ success: true, data: [] }); } const allColumns = await getPddTableColumns(schemaName, tableName); const availableColumns = new Set((allColumns || []).map((c) => c.columnName)); const columnTypeMap = new Map((allColumns || []).map((c) => [c.columnName, String(c.dataType || '').toLowerCase()])); const shopField = PDD_SHOP_FIELD_CANDIDATES.find((field) => availableColumns.has(field)) || null; const hasGoodsId = availableColumns.has('goodsId'); const goodsIdDataType = columnTypeMap.get('goodsId'); const hasNumericGoodsId = SQL_NUMBER_TYPES.has(goodsIdDataType); if (!shopField) { return res.json({ success: true, data: [] }); } if (!accessScope.isAllData && !hasGoodsId) { return res.json({ success: true, data: [] }); } const whereParts = [ `g.[${shopField}] IS NOT NULL`, `LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), g.[${shopField}]))) <> ''`, ]; const params = {}; let joinSql = ''; if (!accessScope.isAllData && hasGoodsId) { joinSql = buildPddTargetJoinSqlByExpr({ includeIdOwner: false, goodsIdExpr: getPddGoodsIdJoinExpr('g'), }).replace('LEFT JOIN', 'INNER JOIN'); appendNamedInCondition(whereParts, params, accessScope.allowedDepartments, 'tm.[dept]', 'scopeDept'); } if (keyword) { whereParts.push(`TRY_CONVERT(NVARCHAR(255), g.[${shopField}]) LIKE @keyword`); params.keyword = `%${keyword}%`; } const whereSql = whereParts.length ? `WHERE ${whereParts.join(' AND ')}` : ''; const rows = await cloudDb.query( ` SELECT TOP (1000) TRY_CONVERT(NVARCHAR(255), g.[${shopField}]) AS shopName, COUNT(1) AS shopCount FROM [${schemaName}].[${tableName}] g ${joinSql} ${whereSql} GROUP BY TRY_CONVERT(NVARCHAR(255), g.[${shopField}]) ORDER BY TRY_CONVERT(NVARCHAR(255), g.[${shopField}]) ASC `, params ); res.json({ success: true, data: (rows || []).map((r) => ({ shopName: r.shopName, rowCount: Number(r.shopCount || 0), })), }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }; } function createPddIdOwnerOptionsHandler() { return async (req, res) => { try { const keyword = (req.query.keyword || '').trim(); const whereParts = [ "emp.[sys00-name] IS NOT NULL", "LTRIM(RTRIM(emp.[sys00-name])) <> ''", "LTRIM(RTRIM(emp.[sys00-dept])) IN (N'杩愯惀涓績A', N'杩愯惀涓績B', N'杩愯惀涓績C', N'杩愯惀涓績G', N'榫欐櫒鍝佺墝涓績', N'鍝佺墝涓績')", ]; const params = {}; if (keyword) { whereParts.push('LTRIM(RTRIM(emp.[sys00-name])) LIKE @keyword'); params.keyword = `%${keyword}%`; } const rows = await cloudDb.query( ` SELECT DISTINCT TOP (500) LTRIM(RTRIM(emp.[sys00-name])) AS ownerName FROM ${PDD_EMPLOYEE_REGISTER_TABLE_FULL_NAME} emp WHERE ${whereParts.join(' AND ')} ORDER BY ownerName ASC `, params ); res.json({ success: true, data: rows || [], }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }; } function createPddListHandler({ schemaName, tableName, fieldDefs, emptyTableMessage, emptyFieldsMessage, extraFields = [], extraFieldsAfterKey = null, insertFieldGroups = [], countCacheScope, getFieldTitle = (fieldDef) => fieldDef.label, getFieldExpr = (fieldDef) => `g.[${fieldDef.key}]`, additionalJoinSql = '', }) { return async (req, res) => { try { const page = safePositiveInt(req.query.page, 1, 1, 1000000); const pageSize = safePositiveInt(req.query.pageSize, 20, 1, 200); const sortField = (req.query.sortField || '').trim(); const sortOrder = (req.query.sortOrder || '').trim().toLowerCase(); const startDate = (req.query.startDate || '').trim(); const endDate = (req.query.endDate || '').trim(); const shopName = (req.query.shopName || '').trim(); const goodsId = (req.query.goodsId || '').trim(); const department = (req.query.department || '').trim(); const idOwners = normalizeMultiValueQueryParam(req.query.idOwners || req.query.idOwner); const accessScope = await getPddDataAccessScope(req.user.sub); if (!accessScope) { return res.status(401).json({ success: false, error: 'user not found' }); } const allColumns = await getPddTableColumns(schemaName, tableName); if (!allColumns || allColumns.length === 0) { return res.status(404).json({ success: false, error: emptyTableMessage, }); } const availableColumns = new Set((allColumns || []).map((c) => c.columnName)); const columnTypeMap = new Map((allColumns || []).map((c) => [c.columnName, String(c.dataType || '').toLowerCase()])); const selectedFieldDefs = fieldDefs.filter((fieldDef) => availableColumns.has(fieldDef.key)); if (selectedFieldDefs.length === 0) { return res.status(400).json({ success: false, error: emptyFieldsMessage, }); } const shopField = PDD_SHOP_FIELD_CANDIDATES.find((field) => availableColumns.has(field)) || null; const hasGoodsId = availableColumns.has('goodsId'); const createTimeDataType = columnTypeMap.get('createTime'); const goodsIdDataType = columnTypeMap.get('goodsId'); const shopDataType = shopField ? columnTypeMap.get(shopField) : null; const hasDateCreateTime = SQL_DATE_TYPES.has(createTimeDataType); const hasTextShopField = SQL_TEXT_TYPES.has(shopDataType); const hasNumericGoodsId = SQL_NUMBER_TYPES.has(goodsIdDataType); const joinSql = hasGoodsId ? buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: getPddGoodsIdJoinExpr('g'), }) : ''; const displayFieldDefs = buildPddDisplayFieldDefs(selectedFieldDefs, { extraFields, extraFieldsAfterKey, insertFieldGroups, getTitle: getFieldTitle, getExpr: getFieldExpr, getSortExpr: (fieldDef) => getPddColumnSortExpr({ fieldKey: fieldDef.key, fieldExpr: getFieldExpr(fieldDef), availableColumns, columnTypeMap, tableAlias: 'g', }), }); const whereParts = []; const params = {}; if (startDate && availableColumns.has('createTime')) { if (hasDateCreateTime) { whereParts.push('g.[createTime] >= @startDate'); } else { whereParts.push('TRY_CONVERT(date, g.[createTime]) >= @startDate'); } params.startDate = startDate; } if (endDate && availableColumns.has('createTime')) { if (hasDateCreateTime) { whereParts.push('g.[createTime] < DATEADD(DAY, 1, @endDate)'); } else { whereParts.push('TRY_CONVERT(date, g.[createTime]) <= @endDate'); } params.endDate = endDate; } if (shopName && shopField) { if (hasTextShopField) { whereParts.push(`g.[${shopField}] = @shopName`); } else { whereParts.push(`TRY_CONVERT(NVARCHAR(255), g.[${shopField}]) = @shopName`); } params.shopName = shopName; } if (goodsId && availableColumns.has('goodsId')) { if (hasNumericGoodsId && !/^\d+$/.test(goodsId)) { return res.status(400).json({ success: false, error: 'goodsId 涓烘暟鍊煎瓧娈碉紝璇疯緭鍏ョ函鏁板瓧', }); } whereParts.push('g.[goodsId] = @goodsId'); params.goodsId = goodsId; } if (department && hasGoodsId) { whereParts.push('tm.[dept] = @department'); params.department = department; } if (idOwners.length > 0 && hasGoodsId) { appendNamedInCondition(whereParts, params, idOwners, 'tm.[ider]', 'idOwner'); } if (!accessScope.isAllData) { if (accessScope.allowedDepartments.length === 0 || !hasGoodsId) { return res.json({ success: true, data: { page, pageSize, total: 0, columns: displayFieldDefs.map((fieldDef) => ({ key: fieldDef.key, title: fieldDef.title })), rows: [], departmentField: 'op_TargetManagement.dept', departmentOptions: [], }, }); } appendNamedInCondition(whereParts, params, accessScope.allowedDepartments, 'tm.[dept]', 'scopeDept'); } const whereSql = whereParts.length ? ` WHERE ${whereParts.join(' AND ')}` : ''; const sortFieldDef = displayFieldDefs.find((fieldDef) => fieldDef.key === sortField) || null; const defaultOrderExpr = availableColumns.has('createTime') ? 'g.[createTime]' : displayFieldDefs[0].sortExpr; const orderExpr = sortFieldDef?.sortExpr || defaultOrderExpr; const orderDirection = sortOrder === 'ascend' ? 'ASC' : 'DESC'; const selectFieldsSql = displayFieldDefs.map((fieldDef) => `${fieldDef.expr} AS [${fieldDef.key}]`).join(', '); const needJoinForCount = hasGoodsId && (Boolean(department) || idOwners.length > 0 || !accessScope.isAllData); const countFromSql = needJoinForCount ? ` FROM [${schemaName}].[${tableName}] g ${joinSql} ${additionalJoinSql} ${whereSql} ` : ` FROM [${schemaName}].[${tableName}] g ${additionalJoinSql} ${whereSql} `; const countCacheKey = JSON.stringify({ scope: countCacheScope, userId: req.user.sub, startDate, endDate, shopName, goodsId, department, idOwners, scopeDepartments: accessScope.allowedDepartments, isAllData: accessScope.isAllData, }); let total = getMemoryCache(pddCountCache, countCacheKey); if (total === null) { const totalRows = await cloudDb.query( ` SELECT COUNT(1) AS total ${countFromSql} `, params ); total = Number(totalRows?.[0]?.total || 0); setMemoryCache(pddCountCache, countCacheKey, total, PDD_COUNT_CACHE_TTL_MS); } const rowsFromSql = needJoinForCount ? ` FROM [${schemaName}].[${tableName}] g ${joinSql} ${additionalJoinSql} ` : ` ${buildPddPagedBaseSubquery({ schemaName, tableName, whereSql, orderExpr, orderDirection, fallbackOrderExpr: defaultOrderExpr, })} ${hasGoodsId ? buildPddTargetApplySql({ includeIdOwner: true, goodsIdExpr: getPddGoodsIdJoinExpr('g'), }) : ''} ${additionalJoinSql} `; const pagingParams = { ...params, offset: (page - 1) * pageSize, pageSize, }; const rows = total > 0 ? await cloudDb.query( ` SELECT ${selectFieldsSql} ${rowsFromSql} ${needJoinForCount ? whereSql : ''} ORDER BY ${orderExpr} ${orderDirection}${defaultOrderExpr && defaultOrderExpr !== orderExpr ? `, ${defaultOrderExpr} DESC` : ''} `, pagingParams ) : []; res.json({ success: true, data: { page, pageSize, total, columns: displayFieldDefs.map((fieldDef) => ({ key: fieldDef.key, title: fieldDef.title })), rows: rows || [], sortField: sortFieldDef?.key || '', sortOrder: sortFieldDef ? sortOrder || 'descend' : '', departmentField: 'op_TargetManagement.dept', departmentOptions: [], }, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }; } const pddDataCenterListHandler = createPddListHandler({ schemaName: PDD_DATA_GOODS_SCHEMA, tableName: PDD_DATA_GOODS_TABLE, fieldDefs: PDD_DATA_GOODS_FIELDS, emptyTableMessage: '未读取到字段,请确认业务数据库已配置且存在 dbo.pdd_DataGoods 表', emptyFieldsMessage: 'dbo.pdd_DataGoods 表存在,但未匹配到预期字段', extraFields: PDD_JOIN_EXTRA_FIELDS, extraFieldsAfterKey: 'hdThumbUrl', insertFieldGroups: [ { afterKey: 'goodsVcr', fields: PDD_DATA_GOODS_DERIVED_FIELDS, }, ], countCacheScope: 'pdd-data-center', }); const pddSpreadShopOptionsHandler = createPddShopOptionsHandler({ schemaName: PDD_DATA_GOODS_SCHEMA, tableName: PDD_SPREAD_GOODS_TABLE, }); const pddIdOwnerOptionsHandler = createPddIdOwnerOptionsHandler(); const pddSpreadListHandler = createPddListHandler({ schemaName: PDD_DATA_GOODS_SCHEMA, tableName: PDD_SPREAD_GOODS_TABLE, fieldDefs: PDD_SPREAD_GOODS_FIELDS, emptyTableMessage: '未读取到字段,请确认业务数据库已配置且存在 dbo.pdd_ReportGoodsSpread 表', emptyFieldsMessage: 'dbo.pdd_ReportGoodsSpread 表存在,但未匹配到预期字段', extraFields: PDD_SPREAD_EXTRA_FIELDS, extraFieldsAfterKey: 'goodsId', countCacheScope: 'pdd-spread-center', getFieldTitle: getPddSpreadFieldTitle, getFieldExpr: (fieldDef) => getPddSpreadFieldExpr(fieldDef.key), additionalJoinSql: buildPddDataGoodsImageApplySql({ goodsIdExpr: getPddGoodsIdJoinExpr('g'), }), }); router.get('/warehouse/kpi', async (req, res) => { try { const { startDate, endDate } = req.query; const start = startDate || dayjs().startOf('month').format('YYYY-MM-DD'); const end = endDate || dayjs().format('YYYY-MM-DD'); const cached = await getCachedAnalysisPayload({ scope: 'warehouse:kpi', payload: { startDate: start, endDate: end }, label: 'analysis:warehouse:kpi', loader: () => queryWarehouseKpi(start, end), }); res.json({ success: true, data: cached.value, cached: cached.hit !== 'miss' }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); /** * 浠撳簱鎺掑悕 */ router.get('/warehouse/ranking', async (req, res) => { try { const { startDate, endDate, limit = 20 } = req.query; const start = startDate || dayjs().startOf('month').format('YYYY-MM-DD'); const end = endDate || dayjs().format('YYYY-MM-DD'); const cached = await getCachedAnalysisPayload({ scope: 'warehouse:ranking', payload: { startDate: start, endDate: end, limit: parseInt(limit, 10) }, label: 'analysis:warehouse:ranking', loader: () => queryWarehouseRanking(start, end, limit), }); res.json({ success: true, data: cached.value, cached: cached.hit !== 'miss' }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // ==================== 蹇€掑垎鏋?==================== /** * 蹇€扠PI */ router.get('/logistics/kpi', async (req, res) => { try { const { startDate, endDate } = req.query; const start = startDate || dayjs().startOf('month').format('YYYY-MM-DD'); const end = endDate || dayjs().format('YYYY-MM-DD'); const cached = await getCachedAnalysisPayload({ scope: 'logistics:kpi', payload: { startDate: start, endDate: end }, label: 'analysis:logistics:kpi', loader: () => queryLogisticsKpi(start, end), }); res.json({ success: true, data: cached.value, cached: cached.hit !== 'miss' }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); /** * 蹇€掑叕鍙告帓鍚? */ router.get('/logistics/ranking', async (req, res) => { try { const { startDate, endDate, limit = 20 } = req.query; const start = startDate || dayjs().startOf('month').format('YYYY-MM-DD'); const end = endDate || dayjs().format('YYYY-MM-DD'); const cached = await getCachedAnalysisPayload({ scope: 'logistics:ranking', payload: { startDate: start, endDate: end, limit: parseInt(limit, 10) }, label: 'analysis:logistics:ranking', loader: () => queryLogisticsRanking(start, end, limit), }); res.json({ success: true, data: cached.value, cached: cached.hit !== 'miss' }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); /** * 鐗╂祦寮傚父绫诲瀷鍒嗗竷 */ router.get('/logistics/abnormal-types', async (req, res) => { try { const { startDate, endDate } = req.query; const start = startDate || dayjs().startOf('month').format('YYYY-MM-DD'); const end = endDate || dayjs().format('YYYY-MM-DD'); const cached = await getCachedAnalysisPayload({ scope: 'logistics:abnormal-types', payload: { startDate: start, endDate: end }, label: 'analysis:logistics:abnormal-types', loader: () => queryLogisticsAbnormalTypes(start, end), }); res.json({ success: true, data: cached.value, cached: cached.hit !== 'miss' }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); /** * 蹇€掓椂鏁堣秼鍔? */ router.get('/logistics/trend', async (req, res) => { try { const { startDate, endDate, dimension = 'daily' } = req.query; const start = startDate || dayjs().startOf('month').format('YYYY-MM-DD'); const end = endDate || dayjs().format('YYYY-MM-DD'); const cached = await getCachedAnalysisPayload({ scope: 'logistics:trend', payload: { startDate: start, endDate: end, dimension }, label: `analysis:logistics:trend:${dimension}`, loader: () => queryLogisticsTrend(start, end, dimension), }); res.json({ success: true, data: cached.value, cached: cached.hit !== 'miss' }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // ==================== 鍦板尯鍒嗘瀽 ==================== /** * 鍦板尯KPI */ router.get('/region/kpi', async (req, res) => { try { const { startDate, endDate } = req.query; const start = startDate || dayjs().startOf('month').format('YYYY-MM-DD'); const end = endDate || dayjs().format('YYYY-MM-DD'); const cached = await getCachedAnalysisPayload({ scope: 'region:kpi', payload: { startDate: start, endDate: end }, label: 'analysis:region:kpi', loader: () => queryRegionKpi(start, end), }); res.json({ success: true, data: cached.value, cached: cached.hit !== 'miss' }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); /** * 鐪佷唤鎺掑悕 */ router.get('/region/ranking', async (req, res) => { try { const { startDate, endDate, limit = 30 } = req.query; const start = startDate || dayjs().startOf('month').format('YYYY-MM-DD'); const end = endDate || dayjs().format('YYYY-MM-DD'); const cached = await getCachedAnalysisPayload({ scope: 'region:ranking', payload: { startDate: start, endDate: end, limit: parseInt(limit, 10) }, label: 'analysis:region:ranking', loader: () => queryRegionRanking(start, end, limit), }); res.json({ success: true, data: cached.value, cached: cached.hit !== 'miss' }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // ==================== 渚涘簲鍟嗗垎鏋?==================== /** * 渚涘簲鍟嗘帓鍚? */ router.get('/supplier/ranking', async (req, res) => { try { const { startDate, endDate, limit = 20 } = req.query; const start = startDate || dayjs().startOf('month').format('YYYY-MM-DD'); const end = endDate || dayjs().format('YYYY-MM-DD'); const cached = await getCachedAnalysisPayload({ scope: 'supplier:ranking', payload: { startDate: start, endDate: end, limit: parseInt(limit, 10) }, label: 'analysis:supplier:ranking', loader: () => querySupplierRanking(start, end, limit), }); res.json({ success: true, data: cached.value, cached: cached.hit !== 'miss' }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // ==================== 瀹㈡湇鍒嗘瀽 ==================== /** * 瀹㈡湇鎺掑悕 */ router.get('/cs/ranking', async (req, res) => { try { const { startDate, endDate, limit = 20 } = req.query; const start = startDate || dayjs().startOf('month').format('YYYY-MM-DD'); const end = endDate || dayjs().format('YYYY-MM-DD'); const cached = await getCachedAnalysisPayload({ scope: 'cs:ranking', payload: { startDate: start, endDate: end, limit: parseInt(limit, 10) }, label: 'analysis:cs:ranking', loader: () => queryCsRanking(start, end, limit), }); res.json({ success: true, data: cached.value, cached: cached.hit !== 'miss' }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // ==================== 閲囪喘鍒嗘瀽 ==================== /** * 閲囪喘鍛樻帓鍚? */ router.get('/procurement/ranking', async (req, res) => { try { const { startDate, endDate, limit = 20 } = req.query; const start = startDate || dayjs().startOf('month').format('YYYY-MM-DD'); const end = endDate || dayjs().format('YYYY-MM-DD'); const cached = await getCachedAnalysisPayload({ scope: 'procurement:ranking', payload: { startDate: start, endDate: end, limit: parseInt(limit, 10) }, label: 'analysis:procurement:ranking', loader: () => queryProcurementRanking(start, end, limit), }); res.json({ success: true, data: cached.value, cached: cached.hit !== 'miss' }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // ==================== 寮€鍙?璁捐鍒嗘瀽 ==================== /** * 璁捐甯堟帓鍚? */ router.get('/design/ranking', async (req, res) => { try { const { startDate, endDate, limit = 20 } = req.query; const start = startDate || dayjs().startOf('month').format('YYYY-MM-DD'); const end = endDate || dayjs().format('YYYY-MM-DD'); const cached = await getCachedAnalysisPayload({ scope: 'design:ranking', payload: { startDate: start, endDate: end, limit: parseInt(limit, 10) }, label: 'analysis:design:ranking', loader: () => queryDesignRanking(start, end, limit), }); res.json({ success: true, data: cached.value, cached: cached.hit !== 'miss' }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // ==================== 閫氱敤鑱婂ぉ闂鍒嗘瀽 ==================== /** * 鑱婂ぉ闂鍒嗗竷锛堝悇閮ㄩ棬閫氱敤锛? */ router.get('/chat-issues', async (req, res) => { try { const { startDate, endDate, limit = 20 } = req.query; const start = startDate || dayjs().startOf('month').format('YYYY-MM-DD'); const end = endDate || dayjs().format('YYYY-MM-DD'); const cached = await getCachedAnalysisPayload({ scope: 'chat-issues', payload: { startDate: start, endDate: end, limit: parseInt(limit, 10) }, label: 'analysis:chat-issues', loader: () => queryChatIssues(start, end, limit), }); res.json({ success: true, data: cached.value, cached: cached.hit !== 'miss' }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // ==================== 涓氬姟鏁版嵁搴撻€氱敤鏌ヨ ==================== router.get('/business-db/tables', async (req, res) => { try { const schemaName = req.query.schemaName ? safeIdentifier(req.query.schemaName, 'schemaName') : null; const sql = ` SELECT TABLE_SCHEMA AS schemaName, TABLE_NAME AS tableName FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE IN ('BASE TABLE', 'VIEW') AND (@schemaName IS NULL OR TABLE_SCHEMA = @schemaName) ORDER BY TABLE_SCHEMA ASC, TABLE_NAME ASC `; const rows = await cloudDb.query(sql, { schemaName }); res.json({ success: true, data: rows || [] }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/business-db/preview', async (req, res) => { try { const schemaName = safeIdentifier(req.query.schemaName || 'dbo', 'schemaName'); const tableName = safeIdentifier(req.query.tableName, 'tableName'); const limit = safePositiveInt(req.query.limit, 50, 1, 500); const sql = `SELECT TOP (${limit}) * FROM [${schemaName}].[${tableName}]`; const rows = await cloudDb.query(sql); const columns = rows.length > 0 ? Object.keys(rows[0]) : []; res.json({ success: true, data: { schemaName, tableName, limit, columns, rows }, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); // ==================== 鎷煎澶?鏁版嵁涓績鍟嗗搧鏄庣粏 ==================== router.get('/pdd/data-center/tables', async (_req, res) => { res.json({ success: true, data: [{ schemaName: PDD_DATA_GOODS_SCHEMA, tableName: PDD_DATA_GOODS_TABLE, }], }); }); router.get('/pdd/data-center/columns', async (req, res) => { try { const allColumns = await cloudDb.query( ` SELECT COLUMN_NAME AS columnName, DATA_TYPE AS dataType FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @schemaName AND TABLE_NAME = @tableName ORDER BY ORDINAL_POSITION `, { schemaName: PDD_DATA_GOODS_SCHEMA, tableName: PDD_DATA_GOODS_TABLE } ); const availableColumns = new Set((allColumns || []).map((c) => c.columnName)); const selectedFieldDefs = PDD_DATA_GOODS_FIELDS.filter((f) => availableColumns.has(f.key)); const displayFieldDefs = buildPddDisplayFieldDefs(selectedFieldDefs, { extraFields: PDD_JOIN_EXTRA_FIELDS, extraFieldsAfterKey: 'hdThumbUrl', insertFieldGroups: [ { afterKey: 'goodsVcr', fields: PDD_DATA_GOODS_DERIVED_FIELDS, }, ], }); res.json({ success: true, data: { columns: displayFieldDefs.map((fieldDef) => ({ key: fieldDef.key, title: fieldDef.title })), departmentField: 'op_TargetManagement.dept', }, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get( '/pdd/data-center/shop-options', authMiddleware, createPddShopOptionsHandler({ schemaName: PDD_DATA_GOODS_SCHEMA, tableName: PDD_DATA_GOODS_TABLE, }) ); router.get('/pdd/id-owner-options', authMiddleware, async (req, res) => { return pddIdOwnerOptionsHandler(req, res); }); router.get('/pdd/data-center/list', authMiddleware, async (req, res) => { return pddDataCenterListHandler(req, res); try { const page = safePositiveInt(req.query.page, 1, 1, 1000000); const pageSize = safePositiveInt(req.query.pageSize, 20, 1, 200); const startDate = (req.query.startDate || '').trim(); const endDate = (req.query.endDate || '').trim(); const shopName = (req.query.shopName || '').trim(); const goodsId = (req.query.goodsId || '').trim(); const department = (req.query.department || '').trim(); const idOwners = normalizeMultiValueQueryParam(req.query.idOwners || req.query.idOwner); const accessScope = await getPddDataAccessScope(req.user.sub); if (!accessScope) { return res.status(401).json({ success: false, error: 'user not found' }); } const metaCacheKey = `${PDD_DATA_GOODS_SCHEMA}.${PDD_DATA_GOODS_TABLE}.columns`; let allColumns = getMemoryCache(pddMetaCache, metaCacheKey); if (!allColumns) { allColumns = await cloudDb.query( ` SELECT COLUMN_NAME AS columnName, DATA_TYPE AS dataType FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @schemaName AND TABLE_NAME = @tableName ORDER BY ORDINAL_POSITION `, { schemaName: PDD_DATA_GOODS_SCHEMA, tableName: PDD_DATA_GOODS_TABLE } ); setMemoryCache(pddMetaCache, metaCacheKey, allColumns || [], PDD_META_CACHE_TTL_MS); } const tableExists = Boolean(allColumns && allColumns.length > 0); if (!tableExists) { return res.status(404).json({ success: false, error: `业务数据库[${currentDbName}]中不存在表 dbo.pdd_DataGoods`, }); } const availableColumns = new Set((allColumns || []).map((c) => c.columnName)); const columnTypeMap = new Map((allColumns || []).map((c) => [c.columnName, String(c.dataType || '').toLowerCase()])); const selectedFieldDefs = PDD_DATA_GOODS_FIELDS.filter((f) => availableColumns.has(f.key)); if (selectedFieldDefs.length === 0) { return res.status(400).json({ success: false, error: `表 dbo.pdd_DataGoods 存在,但未匹配到预期字段,请核对字段名。当前库: ${currentDbName}`, }); } const shopField = PDD_SHOP_FIELD_CANDIDATES.find((field) => availableColumns.has(field)) || null; const hasGoodsId = availableColumns.has('goodsId'); const createTimeDataType = columnTypeMap.get('createTime'); const goodsIdDataType = columnTypeMap.get('goodsId'); const shopDataType = shopField ? columnTypeMap.get(shopField) : null; const hasDateCreateTime = SQL_DATE_TYPES.has(createTimeDataType); const hasTextShopField = SQL_TEXT_TYPES.has(shopDataType); const hasNumericGoodsId = SQL_NUMBER_TYPES.has(goodsIdDataType); const joinSql = hasGoodsId ? ` LEFT JOIN ( SELECT TRY_CONVERT(BIGINT, shopproid) AS shopproidKey, MAX(ider) AS ider, MAX(dept) AS dept FROM ${PDD_TARGET_TABLE_FULL_NAME} WHERE TRY_CONVERT(BIGINT, shopproid) IS NOT NULL GROUP BY TRY_CONVERT(BIGINT, shopproid) ) tm ON g.[goodsId] = tm.shopproidKey ` : ''; const displayFieldDefs = []; selectedFieldDefs.forEach((f) => { displayFieldDefs.push({ key: f.key, title: f.label, expr: `g.[${f.key}]` }); if (f.key === 'hdThumbUrl') { PDD_JOIN_EXTRA_FIELDS.forEach((extra) => displayFieldDefs.push({ key: extra.key, title: extra.label, expr: extra.expr })); } }); const whereParts = []; const params = {}; if (startDate && availableColumns.has('createTime')) { if (hasDateCreateTime) { whereParts.push('g.[createTime] >= @startDate'); } else { whereParts.push('TRY_CONVERT(date, g.[createTime]) >= @startDate'); } params.startDate = startDate; } if (endDate && availableColumns.has('createTime')) { if (hasDateCreateTime) { whereParts.push('g.[createTime] < DATEADD(DAY, 1, @endDate)'); } else { whereParts.push('TRY_CONVERT(date, g.[createTime]) <= @endDate'); } params.endDate = endDate; } if (shopName && shopField) { if (hasTextShopField) { whereParts.push(`g.[${shopField}] = @shopName`); params.shopName = shopName; } else { whereParts.push(`TRY_CONVERT(NVARCHAR(255), g.[${shopField}]) = @shopName`); params.shopName = shopName; } } if (goodsId && availableColumns.has('goodsId')) { if (hasNumericGoodsId && !/^\d+$/.test(goodsId)) { return res.status(400).json({ success: false, error: 'goodsId 涓烘暟鍊煎瓧娈碉紝璇疯緭鍏ョ函鏁板瓧', }); } whereParts.push('g.[goodsId] = @goodsId'); params.goodsId = goodsId; } if (department && hasGoodsId) { whereParts.push('tm.[dept] = @department'); params.department = department; } if (!accessScope.isAllData) { if (accessScope.allowedDepartments.length === 0) { return res.json({ success: true, data: { page, pageSize, total: 0, columns: displayFieldDefs.map((f) => ({ key: f.key, title: f.title })), rows: [], departmentField: 'op_TargetManagement.dept', departmentOptions: [], }, }); } appendNamedInCondition(whereParts, params, accessScope.allowedDepartments, 'tm.[dept]', 'scopeDept'); } const whereSql = whereParts.length ? ` WHERE ${whereParts.join(' AND ')}` : ''; const orderExpr = availableColumns.has('createTime') ? 'g.[createTime]' : displayFieldDefs[0].expr; const selectFieldsSql = displayFieldDefs.map((f) => `${f.expr} AS [${f.key}]`).join(', '); const needJoinForCount = hasGoodsId && (Boolean(department) || !accessScope.isAllData); const countFromSql = needJoinForCount ? ` FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${joinSql} ${whereSql} ` : ` FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${whereSql} `; const countCacheKey = JSON.stringify({ userId: req.user.sub, startDate, endDate, shopName, goodsId, department, scopeDepartments: accessScope.allowedDepartments, isAllData: accessScope.isAllData, }); let total = getMemoryCache(pddCountCache, countCacheKey); if (total === null) { const totalRows = await cloudDb.query( ` SELECT COUNT(1) AS total ${countFromSql} `, params ); total = Number(totalRows?.[0]?.total || 0); setMemoryCache(pddCountCache, countCacheKey, total, PDD_COUNT_CACHE_TTL_MS); } const pagingParams = { ...params, offset: (page - 1) * pageSize, pageSize, }; const rows = total > 0 ? await cloudDb.query( ` SELECT ${selectFieldsSql} FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${joinSql} ${whereSql} ORDER BY ${orderExpr} DESC OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY `, pagingParams ) : []; res.json({ success: true, data: { page, pageSize, total, columns: displayFieldDefs.map((f) => ({ key: f.key, title: f.title })), rows: rows || [], departmentField: 'op_TargetManagement.dept', departmentOptions: [], }, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); // ==================== 鎷煎澶?鎺ㄥ箍涓績鍟嗗搧鏄庣粏 ==================== router.get('/pdd/spread-center/shop-options', authMiddleware, async (req, res) => { return pddSpreadShopOptionsHandler(req, res); try { const keyword = (req.query.keyword || '').trim(); const accessScope = await getPddDataAccessScope(req.user.sub); if (!accessScope) { return res.status(401).json({ success: false, error: 'user not found' }); } if (!accessScope.isAllData && accessScope.allowedDepartments.length === 0) { return res.json({ success: true, data: [] }); } const metaCacheKey = `${PDD_DATA_GOODS_SCHEMA}.${PDD_SPREAD_GOODS_TABLE}.columns`; let allColumns = getMemoryCache(pddMetaCache, metaCacheKey); if (!allColumns) { allColumns = await cloudDb.query( ` SELECT COLUMN_NAME AS columnName, DATA_TYPE AS dataType FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @schemaName AND TABLE_NAME = @tableName ORDER BY ORDINAL_POSITION `, { schemaName: PDD_DATA_GOODS_SCHEMA, tableName: PDD_SPREAD_GOODS_TABLE } ); setMemoryCache(pddMetaCache, metaCacheKey, allColumns || [], PDD_META_CACHE_TTL_MS); } const availableColumns = new Set((allColumns || []).map((c) => c.columnName)); const shopField = PDD_SHOP_FIELD_CANDIDATES.find((field) => availableColumns.has(field)) || null; if (!shopField) { return res.json({ success: true, data: [] }); } const whereParts = [ `g.[${shopField}] IS NOT NULL`, `LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), g.[${shopField}]))) <> ''`, ]; const params = {}; let joinSql = ''; if (!accessScope.isAllData) { joinSql = ` INNER JOIN ( SELECT TRY_CONVERT(BIGINT, shopproid) AS shopproidKey, MAX(dept) AS dept FROM ${PDD_TARGET_TABLE_FULL_NAME} WHERE TRY_CONVERT(BIGINT, shopproid) IS NOT NULL GROUP BY TRY_CONVERT(BIGINT, shopproid) ) tm ON g.[goodsId] = tm.shopproidKey `; appendNamedInCondition(whereParts, params, accessScope.allowedDepartments, 'tm.[dept]', 'scopeDept'); } if (keyword) { whereParts.push(`TRY_CONVERT(NVARCHAR(255), g.[${shopField}]) LIKE @keyword`); params.keyword = `%${keyword}%`; } const whereSql = whereParts.length ? `WHERE ${whereParts.join(' AND ')}` : ''; const rows = await cloudDb.query( ` SELECT TOP (1000) TRY_CONVERT(NVARCHAR(255), g.[${shopField}]) AS shopName, COUNT(1) AS shopCount FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_SPREAD_GOODS_TABLE}] g ${joinSql} ${whereSql} GROUP BY TRY_CONVERT(NVARCHAR(255), g.[${shopField}]) ORDER BY TRY_CONVERT(NVARCHAR(255), g.[${shopField}]) ASC `, params ); res.json({ success: true, data: (rows || []).map((r) => ({ shopName: r.shopName, rowCount: Number(r.shopCount || 0), })), }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/spread-center/list', authMiddleware, async (req, res) => { return pddSpreadListHandler(req, res); try { const page = safePositiveInt(req.query.page, 1, 1, 1000000); const pageSize = safePositiveInt(req.query.pageSize, 20, 1, 200); const startDate = (req.query.startDate || '').trim(); const endDate = (req.query.endDate || '').trim(); const shopName = (req.query.shopName || '').trim(); const goodsId = (req.query.goodsId || '').trim(); const department = (req.query.department || '').trim(); const idOwners = normalizeMultiValueQueryParam(req.query.idOwners || req.query.idOwner); const accessScope = await getPddDataAccessScope(req.user.sub); if (!accessScope) { return res.status(401).json({ success: false, error: 'user not found' }); } const metaCacheKey = `${PDD_DATA_GOODS_SCHEMA}.${PDD_SPREAD_GOODS_TABLE}.columns`; let allColumns = getMemoryCache(pddMetaCache, metaCacheKey); if (!allColumns) { allColumns = await cloudDb.query( ` SELECT COLUMN_NAME AS columnName, DATA_TYPE AS dataType FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @schemaName AND TABLE_NAME = @tableName ORDER BY ORDINAL_POSITION `, { schemaName: PDD_DATA_GOODS_SCHEMA, tableName: PDD_SPREAD_GOODS_TABLE } ); setMemoryCache(pddMetaCache, metaCacheKey, allColumns || [], PDD_META_CACHE_TTL_MS); } if (!allColumns || allColumns.length === 0) { return res.status(404).json({ success: false, error: '未读取到字段,请确认业务数据库已配置且存在 dbo.pdd_ReportGoodsSpread 表', }); } const availableColumns = new Set((allColumns || []).map((c) => c.columnName)); const columnTypeMap = new Map((allColumns || []).map((c) => [c.columnName, String(c.dataType || '').toLowerCase()])); const selectedFieldDefs = PDD_SPREAD_GOODS_FIELDS.filter((f) => availableColumns.has(f.key)); if (selectedFieldDefs.length === 0) { return res.status(400).json({ success: false, error: 'dbo.pdd_ReportGoodsSpread 表存在,但未匹配到预期字段', }); } const shopField = PDD_SHOP_FIELD_CANDIDATES.find((field) => availableColumns.has(field)) || null; const hasGoodsId = availableColumns.has('goodsId'); const createTimeDataType = columnTypeMap.get('createTime'); const goodsIdDataType = columnTypeMap.get('goodsId'); const shopDataType = shopField ? columnTypeMap.get(shopField) : null; const hasDateCreateTime = SQL_DATE_TYPES.has(createTimeDataType); const hasTextShopField = SQL_TEXT_TYPES.has(shopDataType); const hasNumericGoodsId = SQL_NUMBER_TYPES.has(goodsIdDataType); const joinSql = hasGoodsId ? ` LEFT JOIN ( SELECT TRY_CONVERT(BIGINT, shopproid) AS shopproidKey, MAX(ider) AS ider, MAX(dept) AS dept FROM ${PDD_TARGET_TABLE_FULL_NAME} WHERE TRY_CONVERT(BIGINT, shopproid) IS NOT NULL GROUP BY TRY_CONVERT(BIGINT, shopproid) ) tm ON g.[goodsId] = tm.shopproidKey ` : ''; const displayFieldDefs = []; selectedFieldDefs.forEach((fieldDef) => { displayFieldDefs.push({ key: fieldDef.key, title: getPddSpreadFieldTitle(fieldDef), expr: getPddSpreadFieldExpr(fieldDef.key), }); if (fieldDef.key === 'goodsId') { PDD_JOIN_EXTRA_FIELDS.forEach((extra) => { displayFieldDefs.push({ key: extra.key, title: extra.label, expr: extra.expr }); }); } }); const whereParts = []; const params = {}; if (startDate && availableColumns.has('createTime')) { if (hasDateCreateTime) { whereParts.push('g.[createTime] >= @startDate'); } else { whereParts.push('TRY_CONVERT(date, g.[createTime]) >= @startDate'); } params.startDate = startDate; } if (endDate && availableColumns.has('createTime')) { if (hasDateCreateTime) { whereParts.push('g.[createTime] < DATEADD(DAY, 1, @endDate)'); } else { whereParts.push('TRY_CONVERT(date, g.[createTime]) <= @endDate'); } params.endDate = endDate; } if (shopName && shopField) { if (hasTextShopField) { whereParts.push(`g.[${shopField}] = @shopName`); } else { whereParts.push(`TRY_CONVERT(NVARCHAR(255), g.[${shopField}]) = @shopName`); } params.shopName = shopName; } if (goodsId && availableColumns.has('goodsId')) { if (hasNumericGoodsId && !/^\d+$/.test(goodsId)) { return res.status(400).json({ success: false, error: 'goodsId 涓烘暟鍊煎瓧娈碉紝璇疯緭鍏ョ函鏁板瓧', }); } whereParts.push('g.[goodsId] = @goodsId'); params.goodsId = goodsId; } if (department && hasGoodsId) { whereParts.push('tm.[dept] = @department'); params.department = department; } if (!accessScope.isAllData) { if (accessScope.allowedDepartments.length === 0) { return res.json({ success: true, data: { page, pageSize, total: 0, columns: displayFieldDefs.map((f) => ({ key: f.key, title: f.title })), rows: [], departmentField: 'op_TargetManagement.dept', departmentOptions: [], }, }); } appendNamedInCondition(whereParts, params, accessScope.allowedDepartments, 'tm.[dept]', 'scopeDept'); } const whereSql = whereParts.length ? ` WHERE ${whereParts.join(' AND ')}` : ''; const orderExpr = availableColumns.has('createTime') ? 'g.[createTime]' : displayFieldDefs[0].expr; const selectFieldsSql = displayFieldDefs.map((f) => `${f.expr} AS [${f.key}]`).join(', '); const needJoinForCount = hasGoodsId && (Boolean(department) || !accessScope.isAllData); const countFromSql = needJoinForCount ? ` FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_SPREAD_GOODS_TABLE}] g ${joinSql} ${whereSql} ` : ` FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_SPREAD_GOODS_TABLE}] g ${whereSql} `; const countCacheKey = JSON.stringify({ scope: 'pdd-spread-center', userId: req.user.sub, startDate, endDate, shopName, goodsId, department, scopeDepartments: accessScope.allowedDepartments, isAllData: accessScope.isAllData, }); let total = getMemoryCache(pddCountCache, countCacheKey); if (total === null) { const totalRows = await cloudDb.query( ` SELECT COUNT(1) AS total ${countFromSql} `, params ); total = Number(totalRows?.[0]?.total || 0); setMemoryCache(pddCountCache, countCacheKey, total, PDD_COUNT_CACHE_TTL_MS); } const pagingParams = { ...params, offset: (page - 1) * pageSize, pageSize, }; const rows = total > 0 ? await cloudDb.query( ` SELECT ${selectFieldsSql} FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_SPREAD_GOODS_TABLE}] g ${joinSql} ${whereSql} ORDER BY ${orderExpr} DESC OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY `, pagingParams ) : []; res.json({ success: true, data: { page, pageSize, total, columns: displayFieldDefs.map((f) => ({ key: f.key, title: f.title })), rows: rows || [], departmentField: 'op_TargetManagement.dept', departmentOptions: [], }, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/goods-detail-summary/list', authMiddleware, async (req, res) => { try { const page = safePositiveInt(req.query.page, 1, 1, 1000000); const pageSize = safePositiveInt(req.query.pageSize, 20, 1, 200); const sortField = (req.query.sortField || '').trim(); const sortOrder = (req.query.sortOrder || '').trim().toLowerCase(); const startDate = (req.query.startDate || '').trim(); const endDate = (req.query.endDate || '').trim(); const shopName = (req.query.shopName || '').trim(); const goodsId = (req.query.goodsId || '').trim(); const department = (req.query.department || '').trim(); const idOwners = normalizeMultiValueQueryParam(req.query.idOwners || req.query.idOwner); const accessScope = await getPddDataAccessScope(req.user.sub); if (!accessScope) { return res.status(401).json({ success: false, error: 'user not found' }); } const baseColumns = await getPddTableColumns(PDD_DATA_GOODS_SCHEMA, PDD_DATA_GOODS_TABLE); const spreadColumns = await getPddTableColumns(PDD_DATA_GOODS_SCHEMA, PDD_SPREAD_GOODS_TABLE); if (!baseColumns || baseColumns.length === 0) { return res.status(404).json({ success: false, error: '未读取到字段,请确认业务数据库已配置且存在 dbo.pdd_DataGoods 表', }); } if (!spreadColumns || spreadColumns.length === 0) { return res.status(404).json({ success: false, error: '未读取到字段,请确认业务数据库已配置且存在 dbo.pdd_ReportGoodsSpread 表', }); } const baseAvailableColumns = new Set(baseColumns.map((c) => c.columnName)); const baseColumnTypeMap = new Map(baseColumns.map((c) => [c.columnName, String(c.dataType || '').toLowerCase()])); const spreadAvailableColumns = new Set(spreadColumns.map((c) => c.columnName)); const selectedBaseFieldDefs = PDD_DATA_GOODS_FIELDS.filter((fieldDef) => baseAvailableColumns.has(fieldDef.key)); if (selectedBaseFieldDefs.length === 0) { return res.status(400).json({ success: false, error: 'dbo.pdd_DataGoods 表存在,但未匹配到预期字段', }); } const baseDisplayFieldDefs = buildPddDisplayFieldDefs(selectedBaseFieldDefs, { extraFields: PDD_JOIN_EXTRA_FIELDS, extraFieldsAfterKey: 'hdThumbUrl', insertFieldGroups: [ { afterKey: 'goodsVcr', fields: PDD_DATA_GOODS_DERIVED_FIELDS, }, ], }); const existingFieldKeys = new Set(baseDisplayFieldDefs.map((fieldDef) => fieldDef.key)); const spreadStartIndex = PDD_SPREAD_GOODS_FIELDS.findIndex((fieldDef) => fieldDef.key === 'minGroupPrice'); const appendSpreadFieldDefs = PDD_SPREAD_GOODS_FIELDS .slice(Math.max(spreadStartIndex, 0)) .filter((fieldDef) => spreadAvailableColumns.has(fieldDef.key)) .filter((fieldDef) => !existingFieldKeys.has(fieldDef.key)); const summaryDisplayFieldDefs = [ ...baseDisplayFieldDefs, ...appendSpreadFieldDefs.map((fieldDef) => ({ key: fieldDef.key, title: getPddSpreadFieldTitle(fieldDef), expr: `spread.[${fieldDef.key}]`, sortExpr: `TRY_CONVERT(DECIMAL(38, 6), spread.[${fieldDef.key}])`, })), ]; const shopField = PDD_SHOP_FIELD_CANDIDATES.find((field) => baseAvailableColumns.has(field)) || null; const createTimeDataType = baseColumnTypeMap.get('createTime'); const goodsIdDataType = baseColumnTypeMap.get('goodsId'); const shopDataType = shopField ? baseColumnTypeMap.get(shopField) : null; const hasDateCreateTime = SQL_DATE_TYPES.has(createTimeDataType); const hasTextShopField = SQL_TEXT_TYPES.has(shopDataType); const hasNumericGoodsId = SQL_NUMBER_TYPES.has(goodsIdDataType); const baseJoinSql = buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: getPddGoodsIdJoinExpr('g'), }); const spreadSelectSql = appendSpreadFieldDefs.map((fieldDef) => { const rawExpr = getPddSpreadFieldExpr(fieldDef.key).replace(/g\.\[/g, 's.['); return `${rawExpr} AS [${fieldDef.key}]`; }).join(',\n '); const spreadApplySql = ` OUTER APPLY ( SELECT TOP (1) ${spreadSelectSql} FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_SPREAD_GOODS_TABLE}] s WHERE ${getPddGoodsIdJoinExpr('s')} = ${getPddGoodsIdJoinExpr('g')} AND TRY_CONVERT(date, s.[createTime]) = TRY_CONVERT(date, g.[createTime]) ORDER BY s.[createTime] DESC ) spread `; const spreadDateWhereParts = [ `${getPddGoodsIdJoinExpr('s')} <> ''`, 'TRY_CONVERT(date, s.[createTime]) IS NOT NULL', ]; if (startDate) { spreadDateWhereParts.push('TRY_CONVERT(date, s.[createTime]) >= @startDate'); } if (endDate) { spreadDateWhereParts.push('TRY_CONVERT(date, s.[createTime]) <= @endDate'); } const spreadSubquerySql = ` LEFT JOIN ( SELECT * FROM ( SELECT ${getPddGoodsIdJoinExpr('s')} AS goodsIdKey, TRY_CONVERT(date, s.[createTime]) AS dataDate, ${spreadSelectSql}, ROW_NUMBER() OVER ( PARTITION BY ${getPddGoodsIdJoinExpr('s')}, TRY_CONVERT(date, s.[createTime]) ORDER BY s.[createTime] DESC ) AS rn FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_SPREAD_GOODS_TABLE}] s WHERE ${spreadDateWhereParts.join('\n AND ')} ) spreadLatest WHERE spreadLatest.rn = 1 ) spread ON ${getPddGoodsIdJoinExpr('g')} = spread.[goodsIdKey] AND TRY_CONVERT(date, g.[createTime]) = spread.[dataDate] `; const whereParts = []; const params = {}; if (startDate && baseAvailableColumns.has('createTime')) { if (hasDateCreateTime) { whereParts.push('g.[createTime] >= @startDate'); } else { whereParts.push('TRY_CONVERT(date, g.[createTime]) >= @startDate'); } params.startDate = startDate; } if (endDate && baseAvailableColumns.has('createTime')) { if (hasDateCreateTime) { whereParts.push('g.[createTime] < DATEADD(DAY, 1, @endDate)'); } else { whereParts.push('TRY_CONVERT(date, g.[createTime]) <= @endDate'); } params.endDate = endDate; } if (shopName && shopField) { if (hasTextShopField) { whereParts.push(`g.[${shopField}] = @shopName`); } else { whereParts.push(`TRY_CONVERT(NVARCHAR(255), g.[${shopField}]) = @shopName`); } params.shopName = shopName; } if (goodsId && baseAvailableColumns.has('goodsId')) { if (hasNumericGoodsId && !/^\d+$/.test(goodsId)) { return res.status(400).json({ success: false, error: 'goodsId 涓烘暟鍊煎瓧娈碉紝璇疯緭鍏ョ函鏁板瓧', }); } whereParts.push('g.[goodsId] = @goodsId'); params.goodsId = goodsId; } if (department) { whereParts.push('tm.[dept] = @department'); params.department = department; } if (idOwners.length > 0) { appendNamedInCondition(whereParts, params, idOwners, 'tm.[ider]', 'idOwner'); } if (!accessScope.isAllData) { if (accessScope.allowedDepartments.length === 0) { return res.json({ success: true, data: { page, pageSize, total: 0, columns: summaryDisplayFieldDefs.map((fieldDef) => ({ key: fieldDef.key, title: fieldDef.title })), rows: [], departmentField: 'op_TargetManagement.dept', departmentOptions: [], }, }); } appendNamedInCondition(whereParts, params, accessScope.allowedDepartments, 'tm.[dept]', 'scopeDept'); } const whereSql = whereParts.length ? ` WHERE ${whereParts.join(' AND ')}` : ''; const sortFieldDef = summaryDisplayFieldDefs.find((fieldDef) => fieldDef.key === sortField) || null; const defaultOrderExpr = baseAvailableColumns.has('createTime') ? 'g.[createTime]' : (summaryDisplayFieldDefs[0].sortExpr || summaryDisplayFieldDefs[0].expr); const orderExpr = sortFieldDef?.sortExpr || sortFieldDef?.expr || defaultOrderExpr; const orderDirection = sortOrder === 'ascend' ? 'ASC' : 'DESC'; const selectFieldsSql = summaryDisplayFieldDefs.map((fieldDef) => `${fieldDef.expr} AS [${fieldDef.key}]`).join(', '); const needJoinForCount = Boolean(department) || idOwners.length > 0 || !accessScope.isAllData; const countFromSql = needJoinForCount ? ` FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${baseJoinSql} ${whereSql} ` : ` FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${whereSql} `; const fallbackCountSql = ` SELECT COUNT(1) AS total ${countFromSql} `; const countCacheKey = JSON.stringify({ scope: 'pdd-goods-detail-summary', userId: req.user.sub, startDate, endDate, shopName, goodsId, department, idOwners, scopeDepartments: accessScope.allowedDepartments, isAllData: accessScope.isAllData, }); let total = getMemoryCache(pddCountCache, countCacheKey); if (total === null) { const totalRows = await cloudDb.query(fallbackCountSql, params); total = Number(totalRows?.[0]?.total || 0); setMemoryCache(pddCountCache, countCacheKey, total, PDD_COUNT_CACHE_TTL_MS); } const rowsFromSql = needJoinForCount ? ` FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${baseJoinSql} ${spreadSubquerySql} ` : ` ${buildPddPagedBaseSubquery({ schemaName: PDD_DATA_GOODS_SCHEMA, tableName: PDD_DATA_GOODS_TABLE, whereSql, orderExpr, orderDirection, fallbackOrderExpr: defaultOrderExpr, })} ${buildPddTargetApplySql({ includeIdOwner: true, goodsIdExpr: getPddGoodsIdJoinExpr('g'), })} ${spreadApplySql} `; const pagingParams = { ...params, offset: (page - 1) * pageSize, pageSize, }; const rows = total > 0 ? await cloudDb.query( ` SELECT ${selectFieldsSql} ${rowsFromSql} ${needJoinForCount ? whereSql : ''} ORDER BY ${orderExpr} ${orderDirection}${defaultOrderExpr && defaultOrderExpr !== orderExpr ? `, ${defaultOrderExpr} DESC` : ''} `, pagingParams ) : []; res.json({ success: true, data: { page, pageSize, total, columns: summaryDisplayFieldDefs.map((fieldDef) => ({ key: fieldDef.key, title: fieldDef.title })), rows: rows || [], sortField: sortFieldDef?.key || '', sortOrder: sortFieldDef ? sortOrder || 'descend' : '', departmentField: 'op_TargetManagement.dept', departmentOptions: [], }, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/evaluation-summary/list', authMiddleware, async (req, res) => { try { const page = safePositiveInt(req.query.page, 1, 1, 1000000); const pageSize = safePositiveInt(req.query.pageSize, 20, 1, 200); const sortField = (req.query.sortField || '').trim(); const sortOrder = (req.query.sortOrder || '').trim().toLowerCase(); const startDate = (req.query.startDate || '').trim(); const endDate = (req.query.endDate || '').trim(); const shopName = (req.query.shopName || '').trim(); const goodsId = (req.query.goodsId || '').trim(); const department = (req.query.department || '').trim(); const idOwners = normalizeMultiValueQueryParam(req.query.idOwners || req.query.idOwner); const accessScope = await getPddDataAccessScope(req.user.sub); if (!accessScope) { return res.status(401).json({ success: false, error: 'user not found' }); } const allColumns = await getPddTableColumns(PDD_DATA_GOODS_SCHEMA, PDD_DATA_GOODS_TABLE); if (!allColumns || allColumns.length === 0) { return res.status(404).json({ success: false, error: '未读取到字段,请确认业务数据库已配置且存在 dbo.pdd_DataGoods 表', }); } const availableColumns = new Set((allColumns || []).map((c) => c.columnName)); const columnTypeMap = new Map((allColumns || []).map((c) => [c.columnName, String(c.dataType || '').toLowerCase()])); const selectedFieldDefs = PDD_DATA_GOODS_FIELDS.filter((fieldDef) => availableColumns.has(fieldDef.key)); if (selectedFieldDefs.length === 0) { return res.status(400).json({ success: false, error: 'dbo.pdd_DataGoods 表存在,但未匹配到预期字段', }); } const shopField = PDD_SHOP_FIELD_CANDIDATES.find((field) => availableColumns.has(field)) || null; const createTimeDataType = columnTypeMap.get('createTime'); const goodsIdDataType = columnTypeMap.get('goodsId'); const shopDataType = shopField ? columnTypeMap.get(shopField) : null; const hasDateCreateTime = SQL_DATE_TYPES.has(createTimeDataType); const hasTextShopField = SQL_TEXT_TYPES.has(shopDataType); const hasNumericGoodsId = SQL_NUMBER_TYPES.has(goodsIdDataType); const joinSql = buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: getPddGoodsIdJoinExpr('g'), }); const displayFieldDefs = buildPddDisplayFieldDefs(selectedFieldDefs, { extraFields: PDD_JOIN_EXTRA_FIELDS, extraFieldsAfterKey: 'hdThumbUrl', insertFieldGroups: [ { afterKey: 'goodsVcr', fields: PDD_DATA_GOODS_DERIVED_FIELDS, }, ], }); displayFieldDefs.push({ key: 'badReviewCount', title: '差评条数', expr: 'bad.badReviewCount', sortExpr: 'TRY_CONVERT(INT, bad.badReviewCount)', }); const badReviewWhereParts = [ 'TRY_CONVERT(INT, e.[descScore]) IN (1, 2, 3)', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), e.[goodsId]))) <> ''", 'TRY_CONVERT(date, e.[createTime]) IS NOT NULL', ]; if (startDate) { badReviewWhereParts.push('TRY_CONVERT(date, e.[createTime]) >= @startDate'); } if (endDate) { badReviewWhereParts.push('TRY_CONVERT(date, e.[createTime]) <= @endDate'); } const badReviewApplySql = ` CROSS APPLY ( SELECT COUNT(1) AS [badReviewCount] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_EVALUATION_TABLE}] e WHERE TRY_CONVERT(INT, e.[descScore]) IN (1, 2, 3) AND ${getPddGoodsIdJoinExpr('e')} = ${getPddGoodsIdJoinExpr('g')} AND TRY_CONVERT(date, e.[createTime]) = TRY_CONVERT(date, g.[createTime]) HAVING COUNT(1) > 0 ) bad `; const badReviewJoinSql = ` INNER JOIN ( SELECT ${getPddGoodsIdJoinExpr('e')} AS goodsIdKey, TRY_CONVERT(date, e.[createTime]) AS dataDate, COUNT(1) AS badReviewCount FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_EVALUATION_TABLE}] e WHERE ${badReviewWhereParts.join('\n AND ')} GROUP BY ${getPddGoodsIdJoinExpr('e')}, TRY_CONVERT(date, e.[createTime]) ) bad ON ${getPddGoodsIdJoinExpr('g')} = bad.goodsIdKey AND TRY_CONVERT(date, g.[createTime]) = bad.dataDate `; const whereParts = []; const params = {}; if (startDate && availableColumns.has('createTime')) { if (hasDateCreateTime) { whereParts.push('g.[createTime] >= @startDate'); } else { whereParts.push('TRY_CONVERT(date, g.[createTime]) >= @startDate'); } params.startDate = startDate; } if (endDate && availableColumns.has('createTime')) { if (hasDateCreateTime) { whereParts.push('g.[createTime] < DATEADD(DAY, 1, @endDate)'); } else { whereParts.push('TRY_CONVERT(date, g.[createTime]) <= @endDate'); } params.endDate = endDate; } if (shopName && shopField) { if (hasTextShopField) { whereParts.push(`g.[${shopField}] = @shopName`); } else { whereParts.push(`TRY_CONVERT(NVARCHAR(255), g.[${shopField}]) = @shopName`); } params.shopName = shopName; } if (goodsId && availableColumns.has('goodsId')) { if (hasNumericGoodsId && !/^\d+$/.test(goodsId)) { return res.status(400).json({ success: false, error: 'goodsId 涓烘暟鍊煎瓧娈碉紝璇疯緭鍏ョ函鏁板瓧', }); } whereParts.push('g.[goodsId] = @goodsId'); params.goodsId = goodsId; } if (department) { whereParts.push('tm.[dept] = @department'); params.department = department; } if (idOwners.length > 0) { appendNamedInCondition(whereParts, params, idOwners, 'tm.[ider]', 'idOwner'); } if (!accessScope.isAllData) { if (accessScope.allowedDepartments.length === 0) { return res.json({ success: true, data: { page, pageSize, total: 0, columns: displayFieldDefs.map((fieldDef) => ({ key: fieldDef.key, title: fieldDef.title })), rows: [], departmentField: 'op_TargetManagement.dept', departmentOptions: [], }, }); } appendNamedInCondition(whereParts, params, accessScope.allowedDepartments, 'tm.[dept]', 'scopeDept'); } const whereSql = whereParts.length ? ` WHERE ${whereParts.join(' AND ')}` : ''; const sortFieldDef = displayFieldDefs.find((fieldDef) => fieldDef.key === sortField) || null; const defaultOrderExpr = availableColumns.has('createTime') ? 'g.[createTime]' : (displayFieldDefs[0].sortExpr || displayFieldDefs[0].expr); const orderExpr = sortFieldDef?.sortExpr || sortFieldDef?.expr || defaultOrderExpr; const orderDirection = sortOrder === 'ascend' ? 'ASC' : 'DESC'; const needJoinForCount = Boolean(department) || idOwners.length > 0 || !accessScope.isAllData; const selectFieldsSql = displayFieldDefs.map((fieldDef) => { if (!needJoinForCount && fieldDef.key === 'badReviewCount') { return `g.[badReviewCount] AS [${fieldDef.key}]`; } return `${fieldDef.expr} AS [${fieldDef.key}]`; }).join(', '); const countFromSql = needJoinForCount ? ` FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${joinSql} ${badReviewJoinSql} ${whereSql} ` : ` FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${badReviewJoinSql} ${whereSql} `; const fallbackCountSql = ` SELECT COUNT(1) AS total ${countFromSql} `; const countCacheKey = JSON.stringify({ scope: 'pdd-evaluation-summary', userId: req.user.sub, startDate, endDate, shopName, goodsId, department, idOwners, scopeDepartments: accessScope.allowedDepartments, isAllData: accessScope.isAllData, }); let total = getMemoryCache(pddCountCache, countCacheKey); if (total === null) { const totalRows = await cloudDb.query(fallbackCountSql, params); total = Number(totalRows?.[0]?.total || 0); setMemoryCache(pddCountCache, countCacheKey, total, PDD_COUNT_CACHE_TTL_MS); } const rowsFromSql = needJoinForCount ? ` FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${joinSql} ${badReviewJoinSql} ` : ` FROM ( SELECT g.*, bad.[badReviewCount] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${badReviewJoinSql} ${whereSql} ORDER BY ${orderExpr} ${orderDirection}${defaultOrderExpr && defaultOrderExpr !== orderExpr ? `, ${defaultOrderExpr} DESC` : ''} OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY ) g ${buildPddTargetApplySql({ includeIdOwner: true, goodsIdExpr: getPddGoodsIdJoinExpr('g'), })} `; const pagingParams = { ...params, offset: (page - 1) * pageSize, pageSize, }; const rows = total > 0 ? await cloudDb.query( ` SELECT ${selectFieldsSql} ${rowsFromSql} ${needJoinForCount ? whereSql : ''} ORDER BY ${orderExpr} ${orderDirection}${defaultOrderExpr && defaultOrderExpr !== orderExpr ? `, ${defaultOrderExpr} DESC` : ''} `, pagingParams ) : []; res.json({ success: true, data: { page, pageSize, total, columns: displayFieldDefs.map((fieldDef) => ({ key: fieldDef.key, title: fieldDef.title })), rows: rows || [], sortField: sortFieldDef?.key || '', sortOrder: sortFieldDef ? sortOrder || 'descend' : '', departmentField: 'op_TargetManagement.dept', departmentOptions: [], }, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/evaluation/details', authMiddleware, async (req, res) => { try { const goodsId = (req.query.goodsId || '').trim(); const dataDate = (req.query.dataDate || '').trim(); const accessScope = await getPddDataAccessScope(req.user.sub); if (!accessScope) { return res.status(401).json({ success: false, error: 'user not found' }); } if (!goodsId || !dataDate) { return res.status(400).json({ success: false, error: 'goodsId 鍜?dataDate 蹇呭~' }); } const allColumns = await getPddTableColumns(PDD_DATA_GOODS_SCHEMA, PDD_EVALUATION_TABLE); const availableColumns = new Set((allColumns || []).map((c) => c.columnName)); const columnTypeMap = new Map((allColumns || []).map((c) => [c.columnName, String(c.dataType || '').toLowerCase()])); const createTimeDataType = columnTypeMap.get('createTime'); const descScoreDataType = columnTypeMap.get('descScore'); const needDeptJoin = !accessScope.isAllData; const joinSql = needDeptJoin ? buildPddTargetJoinSqlByExpr({ includeIdOwner: false, goodsIdExpr: getPddGoodsIdJoinExpr('g'), }) : ''; const whereParts = [ SQL_NUMBER_TYPES.has(descScoreDataType) ? 'g.[descScore] IN (1, 2, 3)' : 'TRY_CONVERT(INT, g.[descScore]) IN (1, 2, 3)', `${getPddGoodsIdJoinExpr('g')} = @goodsId`, ]; const params = { goodsId, dataDate, dataDateStart: dataDate, }; if (SQL_DATE_TYPES.has(createTimeDataType)) { whereParts.push('g.[createTime] >= @dataDateStart'); whereParts.push('g.[createTime] < DATEADD(DAY, 1, @dataDateStart)'); } else { whereParts.push('TRY_CONVERT(date, g.[createTime]) = @dataDate'); } if (needDeptJoin) { if (accessScope.allowedDepartments.length === 0) { return res.json({ success: true, data: [] }); } appendNamedInCondition(whereParts, params, accessScope.allowedDepartments, 'tm.[dept]', 'scopeDept'); } const rows = await cloudDb.query( ` WITH filteredEvaluation AS ( SELECT g.[descScore] AS [descScore], g.[comment] AS [comment], CASE WHEN TRY_CONVERT(INT, TRY_CONVERT(NVARCHAR(50), g.[created])) IS NULL THEN TRY_CONVERT(NVARCHAR(50), g.[created]) ELSE CONVERT(VARCHAR(19), DATEADD(SECOND, TRY_CONVERT(INT, TRY_CONVERT(NVARCHAR(50), g.[created])), '1970-01-01'), 120) END AS [created], g.[orderSn] AS [orderSn], g.[name] AS [name], g.[goodsName] AS [goodsName], g.[goodsId] AS [goodsId], ${getPddGoodsIdJoinExpr('g')} AS [goodsIdKey], TRY_CONVERT(NVARCHAR(255), g.[orderSn]) AS [orderSnKey], g.[thumbUrl] AS [thumbUrl], g.[specs] AS [specs], g.[keywords] AS [keywords], g.[shopName] AS [shopName], g.[createTime] AS [createTime] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_EVALUATION_TABLE}] g ${joinSql} WHERE ${whereParts.join(' AND ')} ), dwsSkuMap AS ( SELECT keys.[goodsIdKey], keys.[orderSnKey], STUFF(( SELECT DISTINCT ',' + TRY_CONVERT(NVARCHAR(255), d2.[skuoid]) FROM [JSDataWarehouse].[dws].[dws_sn_sales_full_info_di] d2 WHERE TRY_CONVERT(NVARCHAR(255), d2.[item_id]) = keys.[goodsIdKey] AND TRY_CONVERT(NVARCHAR(255), d2.[sourceno]) = keys.[orderSnKey] AND d2.[skuoid] IS NOT NULL AND LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), d2.[skuoid]))) <> '' FOR XML PATH(''), TYPE ).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS [skuoid] FROM ( SELECT DISTINCT [goodsIdKey], [orderSnKey] FROM filteredEvaluation WHERE [goodsIdKey] IS NOT NULL AND [goodsIdKey] <> '' AND [orderSnKey] IS NOT NULL AND [orderSnKey] <> '' ) keys ) SELECT f.[descScore], f.[comment], f.[created], f.[orderSn], f.[name], f.[goodsName], f.[goodsId], ISNULL(m.[skuoid], '') AS [skuoid], f.[thumbUrl], f.[specs], f.[keywords], f.[shopName], f.[createTime] FROM filteredEvaluation f LEFT JOIN dwsSkuMap m ON f.[goodsIdKey] = m.[goodsIdKey] AND f.[orderSnKey] = m.[orderSnKey] ORDER BY f.[createTime] DESC, f.[created] DESC `, params ); res.json({ success: true, data: rows || [] }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/evaluation/shop-options', authMiddleware, async (req, res) => { try { const keyword = (req.query.keyword || '').trim(); const accessScope = await getPddDataAccessScope(req.user.sub); if (!accessScope) { return res.status(401).json({ success: false, error: 'user not found' }); } if (!accessScope.isAllData && accessScope.allowedDepartments.length === 0) { return res.json({ success: true, data: [] }); } const allColumns = await getPddTableColumns(PDD_DATA_GOODS_SCHEMA, PDD_EVALUATION_TABLE); const availableColumns = new Set((allColumns || []).map((c) => c.columnName)); const shopField = PDD_SHOP_FIELD_CANDIDATES.find((field) => availableColumns.has(field)) || null; if (!shopField || !availableColumns.has('goodsId')) { return res.json({ success: true, data: [] }); } const whereParts = [ 'TRY_CONVERT(INT, g.[descScore]) IN (1, 2, 3)', `g.[${shopField}] IS NOT NULL`, `LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), g.[${shopField}]))) <> ''`, ]; const params = {}; let joinSql = ''; if (!accessScope.isAllData) { joinSql = buildPddTargetJoinSql({ includeIdOwner: false }).replace('LEFT JOIN', 'INNER JOIN'); appendNamedInCondition(whereParts, params, accessScope.allowedDepartments, 'tm.[dept]', 'scopeDept'); } if (keyword) { whereParts.push(`TRY_CONVERT(NVARCHAR(255), g.[${shopField}]) LIKE @keyword`); params.keyword = `%${keyword}%`; } const whereSql = `WHERE ${whereParts.join(' AND ')}`; const rows = await cloudDb.query( ` SELECT TOP (1000) TRY_CONVERT(NVARCHAR(255), g.[${shopField}]) AS shopName, COUNT(1) AS shopCount FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_EVALUATION_TABLE}] g ${joinSql} ${whereSql} GROUP BY TRY_CONVERT(NVARCHAR(255), g.[${shopField}]) ORDER BY TRY_CONVERT(NVARCHAR(255), g.[${shopField}]) ASC `, params ); res.json({ success: true, data: (rows || []).map((row) => ({ shopName: row.shopName, rowCount: Number(row.shopCount || 0), })), }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/evaluation/list', authMiddleware, async (req, res) => { try { const page = safePositiveInt(req.query.page, 1, 1, 1000000); const pageSize = safePositiveInt(req.query.pageSize, 20, 1, 200); const startDate = (req.query.startDate || '').trim(); const endDate = (req.query.endDate || '').trim(); const shopName = (req.query.shopName || '').trim(); const goodsId = (req.query.goodsId || '').trim(); const department = (req.query.department || '').trim(); const accessScope = await getPddDataAccessScope(req.user.sub); if (!accessScope) { return res.status(401).json({ success: false, error: 'user not found' }); } const allColumns = await getPddTableColumns(PDD_DATA_GOODS_SCHEMA, PDD_EVALUATION_TABLE); if (!allColumns || allColumns.length === 0) { return res.status(404).json({ success: false, error: '未读取到字段,请确认业务数据库已配置且存在 dbo.pdd_Evaluation 表', }); } const availableColumns = new Set((allColumns || []).map((c) => c.columnName)); const columnTypeMap = new Map((allColumns || []).map((c) => [c.columnName, String(c.dataType || '').toLowerCase()])); const selectedFieldDefs = PDD_EVALUATION_FIELDS.filter((fieldDef) => availableColumns.has(fieldDef.key)); if (selectedFieldDefs.length === 0) { return res.status(400).json({ success: false, error: 'dbo.pdd_Evaluation 表存在,但未匹配到预期字段', }); } const shopField = PDD_SHOP_FIELD_CANDIDATES.find((field) => availableColumns.has(field)) || null; const createTimeDataType = columnTypeMap.get('createTime'); const goodsIdDataType = columnTypeMap.get('goodsId'); const shopDataType = shopField ? columnTypeMap.get(shopField) : null; const hasDateCreateTime = SQL_DATE_TYPES.has(createTimeDataType); const hasTextShopField = SQL_TEXT_TYPES.has(shopDataType); const hasNumericGoodsId = SQL_NUMBER_TYPES.has(goodsIdDataType); const joinSql = availableColumns.has('goodsId') ? buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: getPddGoodsIdJoinExpr('g'), }) : ''; const displayFieldDefs = buildPddDisplayFieldDefs(selectedFieldDefs, { extraFields: PDD_JOIN_EXTRA_FIELDS, extraFieldsAfterKey: 'thumbUrl', getExpr: (fieldDef) => { if (fieldDef.key === 'created') { return "CASE WHEN TRY_CONVERT(INT, TRY_CONVERT(NVARCHAR(50), g.[created])) IS NULL THEN TRY_CONVERT(NVARCHAR(50), g.[created]) ELSE CONVERT(VARCHAR(19), DATEADD(SECOND, TRY_CONVERT(INT, TRY_CONVERT(NVARCHAR(50), g.[created])), '1970-01-01'), 120) END"; } return `g.[${fieldDef.key}]`; }, }); const whereParts = ['TRY_CONVERT(INT, g.[descScore]) IN (1, 2, 3)']; const params = {}; if (startDate && availableColumns.has('createTime')) { if (hasDateCreateTime) { whereParts.push('g.[createTime] >= @startDate'); } else { whereParts.push('TRY_CONVERT(date, g.[createTime]) >= @startDate'); } params.startDate = startDate; } if (endDate && availableColumns.has('createTime')) { if (hasDateCreateTime) { whereParts.push('g.[createTime] < DATEADD(DAY, 1, @endDate)'); } else { whereParts.push('TRY_CONVERT(date, g.[createTime]) <= @endDate'); } params.endDate = endDate; } if (shopName && shopField) { if (hasTextShopField) { whereParts.push(`g.[${shopField}] = @shopName`); } else { whereParts.push(`TRY_CONVERT(NVARCHAR(255), g.[${shopField}]) = @shopName`); } params.shopName = shopName; } if (goodsId && availableColumns.has('goodsId')) { if (hasNumericGoodsId && !/^\d+$/.test(goodsId)) { return res.status(400).json({ success: false, error: 'goodsId 涓烘暟鍊煎瓧娈碉紝璇疯緭鍏ョ函鏁板瓧', }); } whereParts.push('g.[goodsId] = @goodsId'); params.goodsId = goodsId; } if (department && availableColumns.has('goodsId')) { whereParts.push('tm.[dept] = @department'); params.department = department; } if (!accessScope.isAllData) { if (accessScope.allowedDepartments.length === 0 || !availableColumns.has('goodsId')) { return res.json({ success: true, data: { page, pageSize, total: 0, columns: displayFieldDefs.map((fieldDef) => ({ key: fieldDef.key, title: fieldDef.title })), rows: [], departmentField: 'op_TargetManagement.dept', departmentOptions: [], }, }); } appendNamedInCondition(whereParts, params, accessScope.allowedDepartments, 'tm.[dept]', 'scopeDept'); } const whereSql = ` WHERE ${whereParts.join(' AND ')}`; const orderExpr = availableColumns.has('createTime') ? 'g.[createTime]' : 'g.[descScore]'; const selectFieldsSql = displayFieldDefs.map((fieldDef) => `${fieldDef.expr} AS [${fieldDef.key}]`).join(', '); const countCacheKey = JSON.stringify({ scope: 'pdd-evaluation', userId: req.user.sub, startDate, endDate, shopName, goodsId, department, scopeDepartments: accessScope.allowedDepartments, isAllData: accessScope.isAllData, }); const needJoinForCount = Boolean(department) || !accessScope.isAllData; let total = getMemoryCache(pddCountCache, countCacheKey); if (total === null) { const totalRows = await cloudDb.query( ` SELECT COUNT(1) AS total FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_EVALUATION_TABLE}] g ${needJoinForCount ? joinSql : ''} ${whereSql} `, params ); total = Number(totalRows?.[0]?.total || 0); setMemoryCache(pddCountCache, countCacheKey, total, PDD_COUNT_CACHE_TTL_MS); } const pagingParams = { ...params, offset: (page - 1) * pageSize, pageSize, }; const rowsFromSql = needJoinForCount ? ` FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_EVALUATION_TABLE}] g ${joinSql} ` : ` ${buildPddPagedBaseSubquery({ schemaName: PDD_DATA_GOODS_SCHEMA, tableName: PDD_EVALUATION_TABLE, whereSql, orderExpr, })} ${buildPddTargetApplySql({ includeIdOwner: true, goodsIdExpr: getPddGoodsIdJoinExpr('g'), })} `; const rows = total > 0 ? await cloudDb.query( ` SELECT ${selectFieldsSql} ${rowsFromSql} ${needJoinForCount ? whereSql : ''} ORDER BY ${orderExpr} DESC `, pagingParams ) : []; res.json({ success: true, data: { page, pageSize, total, columns: displayFieldDefs.map((fieldDef) => ({ key: fieldDef.key, title: fieldDef.title })), rows: rows || [], departmentField: 'op_TargetManagement.dept', departmentOptions: [], }, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/brand-analysis/list', authMiddleware, async (req, res) => { try { const accessScope = await getPddDataAccessScope(req.user.sub); if (!accessScope) { return res.status(401).json({ success: false, error: 'user not found' }); } const result = await getBrandAnalysisList({ accessScope, brandName: req.query.brandName, startDate: req.query.startDate, endDate: req.query.endDate, page: req.query.page, pageSize: req.query.pageSize, onlyWithIds: req.query.onlyWithIds, }); if (result.pending) { res.set('Retry-After', '5'); } res.json({ success: true, data: { ...result, brandOptions: BRAND_OPTIONS, }, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/stock-selection/data-dates', authMiddleware, async (_req, res) => { try { await ensurePlanningStockSelectionSchema(); const rows = await stockSelectionDb.query( ` SELECT statDate FROM \`${PDD_STOCK_SELECTION_RESULT_TABLE}\` GROUP BY statDate ORDER BY statDate DESC LIMIT 30 ` ); res.json({ success: true, data: (rows || []).map((row) => ({ statDate: row.statDate, })), }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/stock-selection/overview', authMiddleware, async (req, res) => { try { const requestedStatDate = (req.query.statDate || '').trim(); const poolType = (req.query.poolType || '').trim(); const ruleCode = (req.query.ruleCode || '').trim(); const barcode = (req.query.barcode || '').trim(); const statDate = requestedStatDate || await getLatestStockSelectionStatDate(); if (!statDate) { return res.json({ success: true, data: { statDate: null, totals: { totalCount: 0, pickPoolCount: 0, goodPickPoolCount: 0, clearPoolCount: 0, banPoolCount: 0, reviewPoolCount: 0, naCount: 0, }, poolDistribution: [], ruleDistribution: [], summaryRows: [], ruleOptions: [], poolOptions: [], }, }); } const { whereSql, params } = buildStockSelectionWhereClause({ statDate, poolType, ruleCode, barcode, }); await ensurePlanningStockSelectionSchema(); const totalsRows = await stockSelectionDb.query( ` SELECT ? AS statDate, COUNT(1) AS totalCount, SUM(CASE WHEN r.\`poolType\` = '选品待选池' THEN 1 ELSE 0 END) AS pickPoolCount, SUM(CASE WHEN r.\`poolType\` = '优质选品待选池' THEN 1 ELSE 0 END) AS goodPickPoolCount, SUM(CASE WHEN r.\`poolType\` = '清货待选池' THEN 1 ELSE 0 END) AS clearPoolCount, SUM(CASE WHEN r.\`poolType\` = '禁止选品' THEN 1 ELSE 0 END) AS banPoolCount, SUM(CASE WHEN r.\`poolType\` = '企划复盘' THEN 1 ELSE 0 END) AS reviewPoolCount, SUM(CASE WHEN r.\`poolType\` = '未命中' THEN 1 ELSE 0 END) AS naCount FROM \`${PDD_STOCK_SELECTION_RESULT_TABLE}\` r ${whereSql} `, [ statDate, ...params ] ); const poolDistribution = await stockSelectionDb.query( ` SELECT r.\`poolType\` AS name, COUNT(1) AS value FROM \`${PDD_STOCK_SELECTION_RESULT_TABLE}\` r ${whereSql} GROUP BY r.\`poolType\` ORDER BY COUNT(1) DESC, r.\`poolType\` `, params ); const ruleDistribution = await stockSelectionDb.query( ` SELECT r.\`ruleCode\` AS name, COUNT(1) AS value FROM \`${PDD_STOCK_SELECTION_RESULT_TABLE}\` r ${whereSql} GROUP BY r.\`ruleCode\` ORDER BY COUNT(1) DESC, r.\`ruleCode\` `, params ); const summaryRowsRaw = await stockSelectionDb.query( ` SELECT r.\`ruleCode\` AS ruleCode, r.\`poolType\` AS poolType, MAX(r.\`ruleTag\`) AS ruleTag, COUNT(1) AS hitCount FROM \`${PDD_STOCK_SELECTION_RESULT_TABLE}\` r ${whereSql} GROUP BY r.\`ruleCode\`, r.\`poolType\` ORDER BY COUNT(1) DESC, r.\`ruleCode\`, r.\`poolType\` `, params ); const ruleOptionsRows = await stockSelectionDb.query( ` SELECT DISTINCT r.\`ruleCode\` AS ruleCode FROM \`${PDD_STOCK_SELECTION_RESULT_TABLE}\` r WHERE r.\`statDate\` = ? ORDER BY r.\`ruleCode\` `, [ statDate ] ); const poolOptionsRows = await stockSelectionDb.query( ` SELECT DISTINCT r.\`poolType\` AS poolType FROM \`${PDD_STOCK_SELECTION_RESULT_TABLE}\` r WHERE r.\`statDate\` = ? ORDER BY r.\`poolType\` `, [ statDate ] ); const totals = totalsRows?.[0] || {}; const ruleOptions = (ruleOptionsRows || []).map((row) => row.ruleCode).filter(Boolean); const poolOptions = (poolOptionsRows || []).map((row) => row.poolType).filter(Boolean); const totalCount = Number(totals.totalCount || 0); const summaryRows = summaryRowsRaw.map((row, index) => ({ key: `${row.ruleCode || 'NA'}-${row.poolType || 'NA'}-${index}`, ruleCode: row.ruleCode, poolType: row.poolType, ruleTag: row.ruleTag, hitCount: Number(row.hitCount || 0), hitRate: totalCount > 0 ? Number(row.hitCount || 0) / totalCount : 0, })); res.json({ success: true, data: { statDate, totals: { totalCount: Number(totals.totalCount || 0), pickPoolCount: Number(totals.pickPoolCount || 0), goodPickPoolCount: Number(totals.goodPickPoolCount || 0), clearPoolCount: Number(totals.clearPoolCount || 0), banPoolCount: Number(totals.banPoolCount || 0), reviewPoolCount: Number(totals.reviewPoolCount || 0), naCount: Number(totals.naCount || 0), }, poolDistribution: poolDistribution.map((row) => ({ name: row.name || '未分类', value: Number(row.value || 0) })), ruleDistribution: ruleDistribution.map((row) => ({ name: row.name || 'NA', value: Number(row.value || 0) })), summaryRows, ruleOptions, poolOptions, }, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/brand-analysis/details', authMiddleware, async (req, res) => { try { const accessScope = await getPddDataAccessScope(req.user.sub); if (!accessScope) { return res.status(401).json({ success: false, error: 'user not found' }); } const result = await getBrandAnalysisDetails({ accessScope, brandSpu: req.query.brandSpu, startDate: req.query.startDate, endDate: req.query.endDate, }); res.json({ success: true, data: result }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/stock-selection/list', authMiddleware, async (req, res) => { try { const page = safePositiveInt(req.query.page, 1, 1, 1000000); const pageSize = safePositiveInt(req.query.pageSize, 20, 1, 200); const requestedStatDate = (req.query.statDate || '').trim(); const poolType = (req.query.poolType || '').trim(); const ruleCode = (req.query.ruleCode || '').trim(); const barcode = (req.query.barcode || '').trim(); const statDate = requestedStatDate || await getLatestStockSelectionStatDate(); if (!statDate) { return res.json({ success: true, data: { statDate: null, page, pageSize, total: 0, rows: [], }, }); } const { whereSql, params } = buildStockSelectionWhereClause({ statDate, poolType, ruleCode, barcode, }); await ensurePlanningStockSelectionSchema(); const totalRows = await stockSelectionDb.query( ` SELECT COUNT(1) AS total FROM \`${PDD_STOCK_SELECTION_RESULT_TABLE}\` r ${whereSql} `, params ); const total = Number(totalRows?.[0]?.total || 0); const rows = total > 0 ? await stockSelectionDb.query( ` SELECT r.\`statDate\` AS statDate, r.\`channelName\` AS channelName, r.\`barcode\` AS barcode, r.\`ruleCode\` AS ruleCode, r.\`poolType\` AS poolType, r.\`ruleTag\` AS ruleTag, r.\`ruleExplain\` AS ruleExplain FROM \`${PDD_STOCK_SELECTION_RESULT_TABLE}\` r ${whereSql} ORDER BY CASE WHEN r.\`ruleCode\` = 'NA' THEN 1 ELSE 0 END, r.\`poolType\` ASC, r.\`ruleCode\` ASC, r.\`barcode\` ASC LIMIT ? OFFSET ? `, [ ...params, pageSize, (page - 1) * pageSize ] ) : []; res.json({ success: true, data: { statDate, page, pageSize, total, rows: rows || [], }, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/new-product-selection/categories', authMiddleware, async (_req, res) => { try { const categories = await getPddNewProductSelectionCategories(); res.json({ success: true, data: categories, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/new-product-selection/library/filters', authMiddleware, async (_req, res) => { try { const data = await getPddNewProductSelectionLibraryFilterOptions(); res.json({ success: true, data, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/new-product-selection/library', authMiddleware, async (req, res) => { try { const data = await getPddNewProductSelectionLibraryList({ page: req.query.page, pageSize: req.query.pageSize, categoryName: req.query.categoryName, keyword: req.query.keyword, styleTag: req.query.styleTag, colorTag: req.query.colorTag, materialTag: req.query.materialTag, }); res.json({ success: true, data, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/new-product-selection/logs', authMiddleware, async (req, res) => { try { const data = await getPddNewProductSelectionLogs({ limit: req.query.limit, page: req.query.page, pageSize: req.query.pageSize, operatorKeyword: req.query.operatorKeyword, sourceType: req.query.sourceType, selectedCategoryTag: req.query.selectedCategoryTag, status: req.query.status, keyword: req.query.keyword, }); res.json({ success: true, data, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/new-product-selection/logs/:id', authMiddleware, async (req, res) => { try { const detail = await getPddNewProductSelectionLogDetail(req.params.id); res.json({ success: true, data: detail, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.post('/pdd/new-product-selection/search', authMiddleware, async (req, res) => { try { const result = await searchPddNewProductSelection({ userId: req.user.sub, imageUrl: req.body?.imageUrl, imageBase64: req.body?.imageBase64, selectedCategoryTag: req.body?.selectedCategoryTag, similarityThreshold: req.body?.similarityThreshold, limit: req.body?.limit, sourceInput: req.body?.sourceInput, }); res.json({ success: true, data: result, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/invalid-paid/list', authMiddleware, async (req, res) => { const columns = getPddProductDashboardBaseColumns([ { key: 'spend', title: '近7天总花费' }, { key: 'gmv', title: '近7天交易额' }, { key: 'orderNum', title: '近7天成交笔数' }, ]); try { const { page, pageSize, accessScope, startDate, endDate } = await getPddProductDashboardRequestContext(req, [PDD_SPREAD_GOODS_TABLE]); if (!endDate) { return res.json(buildPddProductDashboardEmptyResponse({ page, pageSize, startDate, endDate, columns })); } const payload = await getCachedPddProductDashboardPayload({ boardKey: 'invalid-paid', reqQuery: req.query, page, pageSize, accessScope, startDate, endDate, columns, loader: async () => { const params = { startDate, endDate, offset: (page - 1) * pageSize, pageSize }; const whereParts = [ 'g.[goodsId] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) <> ''", ]; appendPddCreateTimeRangeFilters({ whereParts, params, fieldExpr: 'g.[createTime]', startDate, endDate }); const needsTargetJoin = shouldUsePddTargetJoinForDashboardFilters(req, accessScope); if (!appendPddProductDashboardCommonFilters({ req, whereParts, params, accessScope, departmentExpr: needsTargetJoin ? 'tm.[dept]' : null, idOwnerExpr: needsTargetJoin ? 'tm.[ider]' : null, })) { return { total: 0, rows: [] }; } const whereSql = `WHERE ${whereParts.join(' AND ')}`; const baseCteSql = ` WITH base AS ( SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) AS [goodsId], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[spend]), 0)) / 1000.0 AS [spend], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[gmv]), 0)) / 1000.0 AS [gmv], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[orderNum]), 0)) AS [orderNum] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_SPREAD_GOODS_TABLE}] g ${needsTargetJoin ? buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId])))', }) : ''} ${whereSql} GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) HAVING SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[spend]), 0)) > 0 AND SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[spend]), 0)) < 10000 ) `; const result = await queryPddDashboardBasePagedRows({ baseCteSql, selectFieldsSql: ` [goodsId], CAST([spend] AS DECIMAL(38, 2)) AS [spend], CAST([gmv] AS DECIMAL(38, 2)) AS [gmv], CAST([orderNum] AS DECIMAL(38, 2)) AS [orderNum] `, orderBySql: '[spend] ASC, [goodsId] ASC', params, page, pageSize, }); result.rows = await hydratePddDashboardRowsWithGoodsDetails(result.rows, { detailTableName: PDD_SPREAD_GOODS_TABLE, startDate, endDate, includeIdOwner: true, includeImage: true, }); return result; }, }); res.json(payload); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/bad-review-feedback/list', authMiddleware, async (req, res) => { const columns = getPddProductDashboardBaseColumns([ { key: 'badReviewCount', title: '近7天差评条数' }, { key: 'score1Count', title: '1星条数' }, { key: 'score2Count', title: '2星条数' }, { key: 'score3Count', title: '3星条数' }, ]); try { const { page, pageSize, accessScope, startDate, endDate } = await getPddProductDashboardRequestContext(req, [PDD_EVALUATION_TABLE]); if (!endDate) { return res.json(buildPddProductDashboardEmptyResponse({ page, pageSize, startDate, endDate, columns })); } const payload = await getCachedPddProductDashboardPayload({ boardKey: 'bad-review-feedback', reqQuery: req.query, page, pageSize, accessScope, startDate, endDate, columns, loader: async () => { const params = { startDate, endDate, offset: (page - 1) * pageSize, pageSize }; const whereParts = [ 'TRY_CONVERT(INT, g.[descScore]) IN (1, 2, 3)', 'g.[goodsId] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) <> ''", ]; appendPddCreateTimeRangeFilters({ whereParts, params, fieldExpr: 'g.[createTime]', startDate, endDate }); const needsTargetJoin = shouldUsePddTargetJoinForDashboardFilters(req, accessScope); if (!appendPddProductDashboardCommonFilters({ req, whereParts, params, accessScope, departmentExpr: needsTargetJoin ? 'tm.[dept]' : null, idOwnerExpr: needsTargetJoin ? 'tm.[ider]' : null, })) { return { total: 0, rows: [] }; } const whereSql = `WHERE ${whereParts.join(' AND ')}`; const baseCteSql = ` WITH base AS ( SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) AS [goodsId], COUNT(1) AS [badReviewCount], SUM(CASE WHEN TRY_CONVERT(INT, g.[descScore]) = 1 THEN 1 ELSE 0 END) AS [score1Count], SUM(CASE WHEN TRY_CONVERT(INT, g.[descScore]) = 2 THEN 1 ELSE 0 END) AS [score2Count], SUM(CASE WHEN TRY_CONVERT(INT, g.[descScore]) = 3 THEN 1 ELSE 0 END) AS [score3Count] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_EVALUATION_TABLE}] g ${needsTargetJoin ? buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId])))', }) : ''} ${whereSql} GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) ) `; const result = await queryPddDashboardBasePagedRows({ baseCteSql, selectFieldsSql: ` [goodsId], [badReviewCount], [score1Count], [score2Count], [score3Count] `, orderBySql: '[badReviewCount] DESC, [score1Count] DESC, [goodsId] ASC', params, page, pageSize, }); result.rows = await hydratePddDashboardRowsWithGoodsDetails(result.rows, { detailTableName: PDD_EVALUATION_TABLE, startDate, endDate, includeIdOwner: true, includeImage: true, }); return result; }, }); res.json(payload); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/inquiry-board/list', authMiddleware, async (req, res) => { const columns = getPddProductDashboardBaseColumns([ { key: 'inquiryNum', title: '近7天询单量' }, { key: 'inquirySpend', title: '近7天询单花费' }, { key: 'avgInquiryCost', title: '近7天平均询单成本' }, ]); try { const { page, pageSize, accessScope, startDate, endDate } = await getPddProductDashboardRequestContext(req, [PDD_SPREAD_GOODS_TABLE]); if (!endDate) { return res.json(buildPddProductDashboardEmptyResponse({ page, pageSize, startDate, endDate, columns })); } const payload = await getCachedPddProductDashboardPayload({ boardKey: 'inquiry-board', reqQuery: req.query, page, pageSize, accessScope, startDate, endDate, columns, loader: async () => { const params = { startDate, endDate, offset: (page - 1) * pageSize, pageSize }; const whereParts = [ 'g.[goodsId] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) <> ''", ]; appendPddCreateTimeRangeFilters({ whereParts, params, fieldExpr: 'g.[createTime]', startDate, endDate }); const needsTargetJoin = shouldUsePddTargetJoinForDashboardFilters(req, accessScope); if (!appendPddProductDashboardCommonFilters({ req, whereParts, params, accessScope, departmentExpr: needsTargetJoin ? 'tm.[dept]' : null, idOwnerExpr: needsTargetJoin ? 'tm.[ider]' : null, })) { return { total: 0, rows: [] }; } const whereSql = `WHERE ${whereParts.join(' AND ')}`; const baseCteSql = ` WITH base AS ( SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) AS [goodsId], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[multiGoalInquiryNum]), 0)) AS [inquiryNum], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[inquirySpend]), 0)) / 1000.0 AS [inquirySpend] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_SPREAD_GOODS_TABLE}] g ${needsTargetJoin ? buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId])))', }) : ''} ${whereSql} GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) HAVING SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[multiGoalInquiryNum]), 0)) > 0 ) `; const result = await queryPddDashboardBasePagedRows({ baseCteSql, selectFieldsSql: ` [goodsId], CAST([inquiryNum] AS DECIMAL(38, 2)) AS [inquiryNum], CAST([inquirySpend] AS DECIMAL(38, 2)) AS [inquirySpend], CAST([inquirySpend] / NULLIF([inquiryNum], 0) AS DECIMAL(38, 2)) AS [avgInquiryCost] `, orderBySql: '[inquiryNum] DESC, [goodsId] ASC', params, page, pageSize, }); result.rows = await hydratePddDashboardRowsWithGoodsDetails(result.rows, { detailTableName: PDD_SPREAD_GOODS_TABLE, startDate, endDate, includeIdOwner: true, includeImage: true, }); return result; }, }); res.json(payload); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/after-sale-consulting/list', authMiddleware, async (req, res) => { const columns = getPddProductDashboardBaseColumns([ { key: 'warningTag', title: '标签' }, { key: 'currentOrderNum', title: '近7天订单量' }, { key: 'prevOrderNum', title: '前7天订单量' }, { key: 'orderDelta', title: '订单量变化' }, { key: 'currentBadReviewCount', title: '近7天差评量' }, { key: 'prevBadReviewCount', title: '前7天差评量' }, { key: 'badReviewDelta', title: '差评量变化' }, { key: 'currentAfterSaleCount', title: '近7天售后单量' }, { key: 'prevAfterSaleCount', title: '前7天售后单量' }, { key: 'afterSaleDelta', title: '售后单量变化' }, ]); try { const { page, pageSize, accessScope, startDate, endDate, prevStartDate, prevEndDate } = await getPddProductDashboardRequestContext(req, [PDD_DATA_GOODS_TABLE, PDD_EVALUATION_TABLE]); if (!endDate || !prevStartDate || !prevEndDate) { return res.json(buildPddProductDashboardEmptyResponse({ page, pageSize, startDate, endDate, columns })); } const payload = await getCachedPddProductDashboardPayload({ boardKey: 'after-sale-consulting', reqQuery: req.query, page, pageSize, accessScope, startDate, endDate, columns, extraCachePayload: { prevStartDate, prevEndDate, }, extraResponseData: { prevStartDate, prevEndDate, }, loader: async () => { const params = { startDate, endDate, prevStartDate, prevEndDate, currentStartDateTime: `${dayjs(startDate).format('YYYY-MM-DD')} 00:00:00`, currentEndDateExclusive: dayjs(endDate).add(1, 'day').format('YYYY-MM-DD 00:00:00'), prevStartDateTime: `${dayjs(prevStartDate).format('YYYY-MM-DD')} 00:00:00`, prevEndDateExclusive: dayjs(prevEndDate).add(1, 'day').format('YYYY-MM-DD 00:00:00'), offset: (page - 1) * pageSize, pageSize, }; const scopeWhereParts = [ 'g.[goodsId] IS NOT NULL', ]; appendPddCreateTimeRangeFilters({ whereParts: scopeWhereParts, params, fieldExpr: 'g.[createTime]', startDate: prevStartDate, endDate, startParamName: 'scopeStartDateTime', endParamName: 'scopeEndDateExclusive', }); const needsTargetJoin = shouldUsePddTargetJoinForDashboardFilters(req, accessScope); if (!appendPddProductDashboardCommonFilters({ req, whereParts: scopeWhereParts, params, accessScope, departmentExpr: needsTargetJoin ? 'tm.[dept]' : null, idOwnerExpr: needsTargetJoin ? 'tm.[ider]' : null, goodsIdExpr: 'CONVERT(NVARCHAR(50), g.[goodsId])', })) { return { total: 0, rows: [] }; } const scopeWhereSql = `WHERE ${scopeWhereParts.join(' AND ')}`; const batchSql = ` IF OBJECT_ID('tempdb..#goods_scope') IS NOT NULL DROP TABLE #goods_scope; IF OBJECT_ID('tempdb..#order_agg') IS NOT NULL DROP TABLE #order_agg; IF OBJECT_ID('tempdb..#review_agg') IS NOT NULL DROP TABLE #review_agg; IF OBJECT_ID('tempdb..#after_sale_agg') IS NOT NULL DROP TABLE #after_sale_agg; IF OBJECT_ID('tempdb..#base') IS NOT NULL DROP TABLE #base; SELECT DISTINCT g.[goodsId] AS [goodsId] INTO #goods_scope FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${needsTargetJoin ? buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: 'CONVERT(NVARCHAR(50), g.[goodsId])', }) : ''} ${scopeWhereSql}; CREATE UNIQUE CLUSTERED INDEX IX_goods_scope_goodsId ON #goods_scope([goodsId]); SELECT g.[goodsId] AS [goodsId], SUM(CASE WHEN g.[createTime] >= @currentStartDateTime AND g.[createTime] < @currentEndDateExclusive THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrCnt]), 0) ELSE 0 END) AS [currentOrderNum], SUM(CASE WHEN g.[createTime] >= @prevStartDateTime AND g.[createTime] < @prevEndDateExclusive THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrCnt]), 0) ELSE 0 END) AS [prevOrderNum] INTO #order_agg FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g INNER JOIN #goods_scope gs ON gs.[goodsId] = g.[goodsId] WHERE g.[createTime] >= @scopeStartDateTime AND g.[createTime] < @scopeEndDateExclusive GROUP BY g.[goodsId]; CREATE UNIQUE CLUSTERED INDEX IX_order_agg_goodsId ON #order_agg([goodsId]); SELECT e.[goodsId] AS [goodsId], SUM(CASE WHEN e.[createTime] >= @currentStartDateTime AND e.[createTime] < @currentEndDateExclusive THEN 1 ELSE 0 END) AS [currentBadReviewCount], SUM(CASE WHEN e.[createTime] >= @prevStartDateTime AND e.[createTime] < @prevEndDateExclusive THEN 1 ELSE 0 END) AS [prevBadReviewCount] INTO #review_agg FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_EVALUATION_TABLE}] e INNER JOIN #goods_scope gs ON gs.[goodsId] = e.[goodsId] WHERE e.[descScore] IN (1, 2, 3) AND e.[createTime] >= @scopeStartDateTime AND e.[createTime] < @scopeEndDateExclusive GROUP BY e.[goodsId]; CREATE UNIQUE CLUSTERED INDEX IX_review_agg_goodsId ON #review_agg([goodsId]); SELECT gs.[goodsId] AS [goodsId], SUM(CASE WHEN d.[stat_date] BETWEEN TRY_CONVERT(DATE, @startDate) AND TRY_CONVERT(DATE, @endDate) THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[refund_count]), 0) ELSE 0 END) AS [currentAfterSaleCount], SUM(CASE WHEN d.[stat_date] BETWEEN TRY_CONVERT(DATE, @prevStartDate) AND TRY_CONVERT(DATE, @prevEndDate) THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[refund_count]), 0) ELSE 0 END) AS [prevAfterSaleCount] INTO #after_sale_agg FROM [JSDataWarehouse].[dws].[dws_sn_sales_full_info_di] d INNER JOIN #goods_scope gs ON d.[item_id] = CONVERT(VARCHAR(50), gs.[goodsId]) WHERE d.[stat_date] BETWEEN TRY_CONVERT(DATE, @prevStartDate) AND TRY_CONVERT(DATE, @endDate) AND ISNULL(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), d.[platform]))), N'') IN (N'Pinduoduo', N'拼多多') GROUP BY gs.[goodsId]; CREATE UNIQUE CLUSTERED INDEX IX_after_sale_agg_goodsId ON #after_sale_agg([goodsId]); SELECT gs.[goodsId], CAST(ISNULL(o.[currentOrderNum], 0) AS DECIMAL(38, 2)) AS [currentOrderNum], CAST(ISNULL(o.[prevOrderNum], 0) AS DECIMAL(38, 2)) AS [prevOrderNum], CAST(ISNULL(o.[currentOrderNum], 0) - ISNULL(o.[prevOrderNum], 0) AS DECIMAL(38, 2)) AS [orderDelta], CAST(ISNULL(r.[currentBadReviewCount], 0) AS DECIMAL(38, 2)) AS [currentBadReviewCount], CAST(ISNULL(r.[prevBadReviewCount], 0) AS DECIMAL(38, 2)) AS [prevBadReviewCount], CAST(ISNULL(r.[currentBadReviewCount], 0) - ISNULL(r.[prevBadReviewCount], 0) AS DECIMAL(38, 2)) AS [badReviewDelta], CAST(ISNULL(a.[currentAfterSaleCount], 0) AS DECIMAL(38, 2)) AS [currentAfterSaleCount], CAST(ISNULL(a.[prevAfterSaleCount], 0) AS DECIMAL(38, 2)) AS [prevAfterSaleCount], CAST(ISNULL(a.[currentAfterSaleCount], 0) - ISNULL(a.[prevAfterSaleCount], 0) AS DECIMAL(38, 2)) AS [afterSaleDelta], CASE WHEN ISNULL(o.[currentOrderNum], 0) <= ISNULL(o.[prevOrderNum], 0) AND ISNULL(r.[currentBadReviewCount], 0) > ISNULL(r.[prevBadReviewCount], 0) AND ISNULL(a.[currentAfterSaleCount], 0) > ISNULL(a.[prevAfterSaleCount], 0) THEN N'售后单量上升,请立即排查记录' WHEN ISNULL(o.[currentOrderNum], 0) > ISNULL(o.[prevOrderNum], 0) AND ( ISNULL(r.[currentBadReviewCount], 0) > ISNULL(r.[prevBadReviewCount], 0) OR ISNULL(a.[currentAfterSaleCount], 0) > ISNULL(a.[prevAfterSaleCount], 0) ) THEN N'售后单量持续上升注意排查' ELSE NULL END AS [warningTag], CASE WHEN ISNULL(o.[currentOrderNum], 0) <= ISNULL(o.[prevOrderNum], 0) AND ISNULL(r.[currentBadReviewCount], 0) > ISNULL(r.[prevBadReviewCount], 0) AND ISNULL(a.[currentAfterSaleCount], 0) > ISNULL(a.[prevAfterSaleCount], 0) THEN 2 WHEN ISNULL(o.[currentOrderNum], 0) > ISNULL(o.[prevOrderNum], 0) AND ( ISNULL(r.[currentBadReviewCount], 0) > ISNULL(r.[prevBadReviewCount], 0) OR ISNULL(a.[currentAfterSaleCount], 0) > ISNULL(a.[prevAfterSaleCount], 0) ) THEN 1 ELSE 0 END AS [warningPriority] INTO #base FROM #goods_scope gs LEFT JOIN #order_agg o ON o.[goodsId] = gs.[goodsId] LEFT JOIN #review_agg r ON r.[goodsId] = gs.[goodsId] LEFT JOIN #after_sale_agg a ON a.[goodsId] = gs.[goodsId] WHERE ( ISNULL(o.[currentOrderNum], 0) <= ISNULL(o.[prevOrderNum], 0) AND ISNULL(r.[currentBadReviewCount], 0) > ISNULL(r.[prevBadReviewCount], 0) AND ISNULL(a.[currentAfterSaleCount], 0) > ISNULL(a.[prevAfterSaleCount], 0) ) OR ( ISNULL(o.[currentOrderNum], 0) > ISNULL(o.[prevOrderNum], 0) AND ( ISNULL(r.[currentBadReviewCount], 0) > ISNULL(r.[prevBadReviewCount], 0) OR ISNULL(a.[currentAfterSaleCount], 0) > ISNULL(a.[prevAfterSaleCount], 0) ) ); CREATE CLUSTERED INDEX IX_base_priority ON #base([warningPriority] DESC, [afterSaleDelta] DESC, [badReviewDelta] DESC, [goodsId] ASC); SELECT CONVERT(NVARCHAR(50), [goodsId]) AS [goodsId], [warningTag], [currentOrderNum], [prevOrderNum], [orderDelta], [currentBadReviewCount], [prevBadReviewCount], [badReviewDelta], [currentAfterSaleCount], [prevAfterSaleCount], [afterSaleDelta], COUNT(1) OVER() AS [__total] FROM #base ORDER BY [warningPriority] DESC, [afterSaleDelta] DESC, [badReviewDelta] DESC, [goodsId] ASC OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY; `; const rows = await cloudDb.query(batchSql, params, { requestTimeout: 120000 }); const total = Number(rows?.[0]?.__total || 0); const result = { total, rows: (rows || []).map((row) => { const nextRow = { ...row }; delete nextRow.__total; return nextRow; }), }; result.rows = await hydratePddDashboardRowsWithGoodsDetails(result.rows, { detailTableName: PDD_DATA_GOODS_TABLE, startDate, endDate, includeIdOwner: true, includeImage: true, }); return result; }, }); res.json(payload); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/indirect-deal/list', authMiddleware, async (req, res) => { const columns = getPddProductDashboardBaseColumns([ { key: 'orderNum', title: '近7天总成交笔数' }, { key: 'indirectOrderNum', title: '近7天间接成交笔数' }, { key: 'indirectRate', title: '间接成交占比' }, ]); try { const { page, pageSize, accessScope, startDate, endDate } = await getPddProductDashboardRequestContext(req, [PDD_SPREAD_GOODS_TABLE]); if (!endDate) { return res.json(buildPddProductDashboardEmptyResponse({ page, pageSize, startDate, endDate, columns })); } const payload = await getCachedPddProductDashboardPayload({ boardKey: 'indirect-deal', reqQuery: req.query, page, pageSize, accessScope, startDate, endDate, columns, loader: async () => { const params = { startDate, endDate, offset: (page - 1) * pageSize, pageSize }; const whereParts = [ 'g.[goodsId] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) <> ''", ]; appendPddCreateTimeRangeFilters({ whereParts, params, fieldExpr: 'g.[createTime]', startDate, endDate }); const needsTargetJoin = shouldUsePddTargetJoinForDashboardFilters(req, accessScope); if (!appendPddProductDashboardCommonFilters({ req, whereParts, params, accessScope, departmentExpr: needsTargetJoin ? 'tm.[dept]' : null, idOwnerExpr: needsTargetJoin ? 'tm.[ider]' : null, })) { return { total: 0, rows: [] }; } const whereSql = `WHERE ${whereParts.join(' AND ')}`; const baseCteSql = ` WITH base AS ( SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) AS [goodsId], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[orderNum]), 0)) AS [orderNum], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[indirectOrderNum]), 0)) AS [indirectOrderNum] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_SPREAD_GOODS_TABLE}] g ${needsTargetJoin ? buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId])))', }) : ''} ${whereSql} GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) HAVING SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[orderNum]), 0)) > 0 AND SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[indirectOrderNum]), 0)) / NULLIF(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[orderNum]), 0)), 0) > 0.2 ) `; const result = await queryPddDashboardBasePagedRows({ baseCteSql, selectFieldsSql: ` [goodsId], CAST([orderNum] AS DECIMAL(38, 2)) AS [orderNum], CAST([indirectOrderNum] AS DECIMAL(38, 2)) AS [indirectOrderNum], CAST([indirectOrderNum] / NULLIF([orderNum], 0) * 100.0 AS DECIMAL(38, 2)) AS [indirectRate] `, orderBySql: '([indirectOrderNum] / NULLIF([orderNum], 0)) DESC, [goodsId] ASC', params, page, pageSize, }); result.rows = await hydratePddDashboardRowsWithGoodsDetails(result.rows, { detailTableName: PDD_SPREAD_GOODS_TABLE, startDate, endDate, includeIdOwner: true, includeImage: true, }); return result; }, }); res.json(payload); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/urgent-review-link/list', authMiddleware, async (req, res) => { const columns = getPddProductDashboardBaseColumns([ { key: 'currentGmv', title: '近7天GMV' }, { key: 'prevGmv', title: '前7天GMV' }, { key: 'gmvChangeRate', title: 'GMV环比' }, { key: 'currentOrderNum', title: '近7天订单数' }, { key: 'prevOrderNum', title: '前7天订单数' }, { key: 'orderChangeRate', title: '订单环比' }, ]); try { const { page, pageSize, accessScope, startDate, endDate, prevStartDate, prevEndDate } = await getPddProductDashboardRequestContext(req, [PDD_DATA_GOODS_TABLE]); if (!endDate) { return res.json(buildPddProductDashboardEmptyResponse({ page, pageSize, startDate, endDate, columns })); } const payload = await getCachedPddProductDashboardPayload({ boardKey: 'urgent-review-link', reqQuery: req.query, page, pageSize, accessScope, startDate, endDate, columns, extraCachePayload: { prevStartDate, prevEndDate }, loader: async () => { const params = { startDate, endDate, prevStartDate, prevEndDate, offset: (page - 1) * pageSize, pageSize }; const whereParts = [ 'g.[goodsId] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) <> ''", ]; appendPddCreateTimeRangeFilters({ whereParts, params, fieldExpr: 'g.[createTime]', startDate: prevStartDate, endDate, startParamName: 'prevStartDateTime', endParamName: 'endDateExclusive', }); const needsTargetJoin = shouldUsePddTargetJoinForDashboardFilters(req, accessScope); if (!appendPddProductDashboardCommonFilters({ req, whereParts, params, accessScope, departmentExpr: needsTargetJoin ? 'tm.[dept]' : null, idOwnerExpr: needsTargetJoin ? 'tm.[ider]' : null, })) { return { total: 0, rows: [] }; } const whereSql = `WHERE ${whereParts.join(' AND ')}`; const baseCteSql = ` WITH base AS ( SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) AS [goodsId], SUM(CASE WHEN g.[createTime] >= @startDateTime AND g.[createTime] < @endDateExclusive THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrAmt]), 0) ELSE 0 END) AS [currentGmv], SUM(CASE WHEN g.[createTime] >= @prevStartDateTime AND g.[createTime] < @startDateTime THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrAmt]), 0) ELSE 0 END) AS [prevGmv], SUM(CASE WHEN g.[createTime] >= @startDateTime AND g.[createTime] < @endDateExclusive THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrCnt]), 0) ELSE 0 END) AS [currentOrderNum], SUM(CASE WHEN g.[createTime] >= @prevStartDateTime AND g.[createTime] < @startDateTime THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrCnt]), 0) ELSE 0 END) AS [prevOrderNum] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${needsTargetJoin ? buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId])))', }) : ''} ${whereSql} GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) HAVING SUM(CASE WHEN g.[createTime] >= @prevStartDateTime AND g.[createTime] < @startDateTime THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrAmt]), 0) ELSE 0 END) > 0 AND SUM(CASE WHEN g.[createTime] >= @prevStartDateTime AND g.[createTime] < @startDateTime THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrCnt]), 0) ELSE 0 END) > 0 AND SUM(CASE WHEN g.[createTime] >= @startDateTime AND g.[createTime] < @endDateExclusive THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrAmt]), 0) ELSE 0 END) < SUM(CASE WHEN g.[createTime] >= @prevStartDateTime AND g.[createTime] < @startDateTime THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrAmt]), 0) ELSE 0 END) AND SUM(CASE WHEN g.[createTime] >= @startDateTime AND g.[createTime] < @endDateExclusive THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrCnt]), 0) ELSE 0 END) < SUM(CASE WHEN g.[createTime] >= @prevStartDateTime AND g.[createTime] < @startDateTime THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrCnt]), 0) ELSE 0 END) ) `; const result = await queryPddDashboardBasePagedRows({ baseCteSql, selectFieldsSql: ` [goodsId], CAST([currentGmv] AS DECIMAL(38, 2)) AS [currentGmv], CAST([prevGmv] AS DECIMAL(38, 2)) AS [prevGmv], CAST(([currentGmv] - [prevGmv]) / NULLIF([prevGmv], 0) * 100.0 AS DECIMAL(38, 2)) AS [gmvChangeRate], CAST([currentOrderNum] AS DECIMAL(38, 2)) AS [currentOrderNum], CAST([prevOrderNum] AS DECIMAL(38, 2)) AS [prevOrderNum], CAST(([currentOrderNum] - [prevOrderNum]) / NULLIF([prevOrderNum], 0) * 100.0 AS DECIMAL(38, 2)) AS [orderChangeRate] `, orderBySql: '(([currentGmv] - [prevGmv]) / NULLIF([prevGmv], 0)) ASC, [goodsId] ASC', params, page, pageSize, }); result.rows = await hydratePddDashboardRowsWithGoodsDetails(result.rows, { detailTableName: PDD_DATA_GOODS_TABLE, startDate, endDate, includeIdOwner: true, includeImage: true, }); return result; }, }); res.json(payload); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/amplify-board/list', authMiddleware, async (req, res) => { const columns = getPddProductDashboardBaseColumns([ { key: 'currentGmv', title: '近7天GMV' }, { key: 'prevGmv', title: '前7天GMV' }, { key: 'gmvChangeRate', title: 'GMV环比' }, { key: 'currentOrderNum', title: '近7天订单数' }, { key: 'prevOrderNum', title: '前7天订单数' }, { key: 'orderChangeRate', title: '订单环比' }, ]); try { const { page, pageSize, accessScope, startDate, endDate, prevStartDate, prevEndDate } = await getPddProductDashboardRequestContext(req, [PDD_DATA_GOODS_TABLE]); if (!endDate) { return res.json(buildPddProductDashboardEmptyResponse({ page, pageSize, startDate, endDate, columns })); } const payload = await getCachedPddProductDashboardPayload({ boardKey: 'amplify-board', reqQuery: req.query, page, pageSize, accessScope, startDate, endDate, columns, extraCachePayload: { prevStartDate, prevEndDate }, loader: async () => { const params = { startDate, endDate, prevStartDate, prevEndDate, offset: (page - 1) * pageSize, pageSize }; const whereParts = [ 'g.[goodsId] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) <> ''", ]; appendPddCreateTimeRangeFilters({ whereParts, params, fieldExpr: 'g.[createTime]', startDate: prevStartDate, endDate, startParamName: 'prevStartDateTime', endParamName: 'endDateExclusive', }); const needsTargetJoin = shouldUsePddTargetJoinForDashboardFilters(req, accessScope); if (!appendPddProductDashboardCommonFilters({ req, whereParts, params, accessScope, departmentExpr: needsTargetJoin ? 'tm.[dept]' : null, idOwnerExpr: needsTargetJoin ? 'tm.[ider]' : null, })) { return { total: 0, rows: [] }; } const whereSql = `WHERE ${whereParts.join(' AND ')}`; const baseCteSql = ` WITH base AS ( SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) AS [goodsId], SUM(CASE WHEN g.[createTime] >= @startDateTime AND g.[createTime] < @endDateExclusive THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrAmt]), 0) ELSE 0 END) AS [currentGmv], SUM(CASE WHEN g.[createTime] >= @prevStartDateTime AND g.[createTime] < @startDateTime THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrAmt]), 0) ELSE 0 END) AS [prevGmv], SUM(CASE WHEN g.[createTime] >= @startDateTime AND g.[createTime] < @endDateExclusive THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrCnt]), 0) ELSE 0 END) AS [currentOrderNum], SUM(CASE WHEN g.[createTime] >= @prevStartDateTime AND g.[createTime] < @startDateTime THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrCnt]), 0) ELSE 0 END) AS [prevOrderNum] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${needsTargetJoin ? buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId])))', }) : ''} ${whereSql} GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) HAVING SUM(CASE WHEN g.[createTime] >= @prevStartDateTime AND g.[createTime] < @startDateTime THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrAmt]), 0) ELSE 0 END) > 0 AND SUM(CASE WHEN g.[createTime] >= @prevStartDateTime AND g.[createTime] < @startDateTime THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrCnt]), 0) ELSE 0 END) > 0 AND SUM(CASE WHEN g.[createTime] >= @startDateTime AND g.[createTime] < @endDateExclusive THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrAmt]), 0) ELSE 0 END) > SUM(CASE WHEN g.[createTime] >= @prevStartDateTime AND g.[createTime] < @startDateTime THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrAmt]), 0) ELSE 0 END) AND SUM(CASE WHEN g.[createTime] >= @startDateTime AND g.[createTime] < @endDateExclusive THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrCnt]), 0) ELSE 0 END) > SUM(CASE WHEN g.[createTime] >= @prevStartDateTime AND g.[createTime] < @startDateTime THEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrCnt]), 0) ELSE 0 END) ) `; const result = await queryPddDashboardBasePagedRows({ baseCteSql, selectFieldsSql: ` [goodsId], CAST([currentGmv] AS DECIMAL(38, 2)) AS [currentGmv], CAST([prevGmv] AS DECIMAL(38, 2)) AS [prevGmv], CAST(([currentGmv] - [prevGmv]) / NULLIF([prevGmv], 0) * 100.0 AS DECIMAL(38, 2)) AS [gmvChangeRate], CAST([currentOrderNum] AS DECIMAL(38, 2)) AS [currentOrderNum], CAST([prevOrderNum] AS DECIMAL(38, 2)) AS [prevOrderNum], CAST(([currentOrderNum] - [prevOrderNum]) / NULLIF([prevOrderNum], 0) * 100.0 AS DECIMAL(38, 2)) AS [orderChangeRate] `, orderBySql: '(([currentGmv] - [prevGmv]) / NULLIF([prevGmv], 0)) DESC, [goodsId] ASC', params, page, pageSize, }); result.rows = await hydratePddDashboardRowsWithGoodsDetails(result.rows, { detailTableName: PDD_DATA_GOODS_TABLE, startDate, endDate, includeIdOwner: true, includeImage: true, }); return result; }, }); res.json(payload); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/deal-cost/list', authMiddleware, async (req, res) => { const columns = getPddProductDashboardBaseColumns([ { key: 'pieceUnitPrice', title: '近7天件单价' }, { key: 'avgDealCost', title: '近7天成交花费' }, { key: 'costRate', title: '成交花费占件单价比' }, ]); try { const { page, pageSize, accessScope, startDate, endDate } = await getPddProductDashboardRequestContext(req, [PDD_DATA_GOODS_TABLE, PDD_SPREAD_GOODS_TABLE]); if (!endDate) { return res.json(buildPddProductDashboardEmptyResponse({ page, pageSize, startDate, endDate, columns })); } const payload = await getCachedPddProductDashboardPayload({ boardKey: 'deal-cost', reqQuery: req.query, page, pageSize, accessScope, startDate, endDate, columns, loader: async () => { const params = { startDate, endDate, offset: (page - 1) * pageSize, pageSize }; const dgWhereParts = [ 'dg.[goodsId] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), dg.[goodsId]))) <> ''", ]; const spWhereParts = [ 'sp.[goodsId] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), sp.[goodsId]))) <> ''", ]; appendPddCreateTimeRangeFilters({ whereParts: dgWhereParts, params, fieldExpr: 'dg.[createTime]', startDate, endDate }); appendPddCreateTimeRangeFilters({ whereParts: spWhereParts, params, fieldExpr: 'sp.[createTime]', startDate, endDate, startParamName: 'spreadStartDateTime', endParamName: 'spreadEndDateExclusive', }); if (!appendPddProductDashboardCommonFilters({ req, whereParts: dgWhereParts, params, accessScope, shopNameExpr: "TRY_CONVERT(NVARCHAR(255), dg.[shopName])", goodsIdExpr: "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), dg.[goodsId])))", departmentExpr: 'tm1.[dept]', idOwnerExpr: 'tm1.[ider]', scopeParamPrefix: 'scopeDeptDg', shopNameParamName: 'shopNameDg', goodsIdParamName: 'goodsIdDg', departmentParamName: 'departmentDg', idOwnerParamPrefix: 'idOwnerDg', })) { return { total: 0, rows: [] }; } const needsSpreadTargetJoin = shouldUsePddTargetJoinForDashboardFilters(req, { isAllData: true, allowedDepartments: [] }) || Boolean(String(req.query.department || '').trim()); if (!appendPddProductDashboardCommonFilters({ req, whereParts: spWhereParts, params, accessScope: { isAllData: true, allowedDepartments: [] }, shopNameExpr: "TRY_CONVERT(NVARCHAR(255), sp.[shopName])", goodsIdExpr: "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), sp.[goodsId])))", departmentExpr: 'tm2.[dept]', idOwnerExpr: null, shopNameParamName: 'shopNameSp', goodsIdParamName: 'goodsIdSp', departmentParamName: 'departmentSp', })) { return { total: 0, rows: [] }; } const baseCteSql = ` WITH dg_base AS ( SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), dg.[goodsId]))) AS [goodsId], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), dg.[payOrdrAmt]), 0)) AS [payAmt], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), dg.[payOrdrGoodsQty]), 0)) AS [goodsQty] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] dg ${aliasPddTargetJoinSql(buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), dg.[goodsId])))', }), 'tm1')} WHERE ${dgWhereParts.join(' AND ')} GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), dg.[goodsId]))) ), sp_base AS ( SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), sp.[goodsId]))) AS [goodsId], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), sp.[spend]), 0)) / 1000.0 AS [spendYuan], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), sp.[orderNum]), 0)) AS [orderNum] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_SPREAD_GOODS_TABLE}] sp ${needsSpreadTargetJoin ? aliasPddTargetJoinSql(buildPddTargetJoinSqlByExpr({ includeIdOwner: false, goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), sp.[goodsId])))', }), 'tm2') : ''} WHERE ${spWhereParts.join(' AND ')} GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), sp.[goodsId]))) ), base AS ( SELECT dg.[goodsId], dg.[payAmt] / NULLIF(dg.[goodsQty], 0) AS [pieceUnitPrice], sp.[spendYuan] / NULLIF(sp.[orderNum], 0) AS [avgDealCost] FROM dg_base dg INNER JOIN sp_base sp ON dg.[goodsId] = sp.[goodsId] WHERE dg.[goodsQty] > 0 AND sp.[orderNum] > 0 AND sp.[spendYuan] / NULLIF(sp.[orderNum], 0) > (dg.[payAmt] / NULLIF(dg.[goodsQty], 0)) * 0.2 ) `; const result = await queryPddDashboardBasePagedRows({ baseCteSql, selectFieldsSql: ` [goodsId], CAST([pieceUnitPrice] AS DECIMAL(38, 2)) AS [pieceUnitPrice], CAST([avgDealCost] AS DECIMAL(38, 2)) AS [avgDealCost], CAST([avgDealCost] / NULLIF([pieceUnitPrice], 0) * 100.0 AS DECIMAL(38, 2)) AS [costRate] `, orderBySql: '([avgDealCost] / NULLIF([pieceUnitPrice], 0)) DESC, [goodsId] ASC', params, page, pageSize, }); result.rows = await hydratePddDashboardRowsWithGoodsDetails(result.rows, { detailTableName: PDD_DATA_GOODS_TABLE, startDate, endDate, includeIdOwner: true, includeImage: true, }); return result; }, }); res.json(payload); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/inquiry-cost/list', authMiddleware, async (req, res) => { const columns = getPddProductDashboardBaseColumns([ { key: 'pieceUnitPrice', title: '近7天件单价' }, { key: 'avgInquiryCost', title: '近7天平均询单成本' }, { key: 'costRate', title: '询单花费占件单价比' }, ]); try { const { page, pageSize, accessScope, startDate, endDate } = await getPddProductDashboardRequestContext(req, [PDD_DATA_GOODS_TABLE, PDD_SPREAD_GOODS_TABLE]); if (!endDate) { return res.json(buildPddProductDashboardEmptyResponse({ page, pageSize, startDate, endDate, columns })); } const payload = await getCachedPddProductDashboardPayload({ boardKey: 'inquiry-cost', reqQuery: req.query, page, pageSize, accessScope, startDate, endDate, columns, loader: async () => { const params = { startDate, endDate, offset: (page - 1) * pageSize, pageSize }; const dgWhereParts = [ 'dg.[goodsId] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), dg.[goodsId]))) <> ''", ]; const spWhereParts = [ 'sp.[goodsId] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), sp.[goodsId]))) <> ''", ]; appendPddCreateTimeRangeFilters({ whereParts: dgWhereParts, params, fieldExpr: 'dg.[createTime]', startDate, endDate }); appendPddCreateTimeRangeFilters({ whereParts: spWhereParts, params, fieldExpr: 'sp.[createTime]', startDate, endDate, startParamName: 'spreadStartDateTime', endParamName: 'spreadEndDateExclusive', }); if (!appendPddProductDashboardCommonFilters({ req, whereParts: dgWhereParts, params, accessScope, shopNameExpr: "TRY_CONVERT(NVARCHAR(255), dg.[shopName])", goodsIdExpr: "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), dg.[goodsId])))", departmentExpr: 'tm1.[dept]', idOwnerExpr: 'tm1.[ider]', scopeParamPrefix: 'scopeDeptDg', shopNameParamName: 'shopNameDg', goodsIdParamName: 'goodsIdDg', departmentParamName: 'departmentDg', idOwnerParamPrefix: 'idOwnerDg', })) { return { total: 0, rows: [] }; } const needsSpreadTargetJoin = shouldUsePddTargetJoinForDashboardFilters(req, { isAllData: true, allowedDepartments: [] }) || Boolean(String(req.query.department || '').trim()); if (!appendPddProductDashboardCommonFilters({ req, whereParts: spWhereParts, params, accessScope: { isAllData: true, allowedDepartments: [] }, shopNameExpr: "TRY_CONVERT(NVARCHAR(255), sp.[shopName])", goodsIdExpr: "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), sp.[goodsId])))", departmentExpr: 'tm2.[dept]', idOwnerExpr: null, shopNameParamName: 'shopNameSp', goodsIdParamName: 'goodsIdSp', departmentParamName: 'departmentSp', })) { return { total: 0, rows: [] }; } const baseCteSql = ` WITH dg_base AS ( SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), dg.[goodsId]))) AS [goodsId], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), dg.[payOrdrAmt]), 0)) AS [payAmt], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), dg.[payOrdrGoodsQty]), 0)) AS [goodsQty] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] dg ${aliasPddTargetJoinSql(buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), dg.[goodsId])))', }), 'tm1')} WHERE ${dgWhereParts.join(' AND ')} GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), dg.[goodsId]))) ), sp_base AS ( SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), sp.[goodsId]))) AS [goodsId], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), sp.[inquirySpend]), 0)) / 1000.0 AS [inquirySpendYuan], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), sp.[multiGoalInquiryNum]), 0)) AS [inquiryNum] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_SPREAD_GOODS_TABLE}] sp ${needsSpreadTargetJoin ? aliasPddTargetJoinSql(buildPddTargetJoinSqlByExpr({ includeIdOwner: false, goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), sp.[goodsId])))', }), 'tm2') : ''} WHERE ${spWhereParts.join(' AND ')} GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), sp.[goodsId]))) ), base AS ( SELECT dg.[goodsId], dg.[payAmt] / NULLIF(dg.[goodsQty], 0) AS [pieceUnitPrice], sp.[inquirySpendYuan] / NULLIF(sp.[inquiryNum], 0) AS [avgInquiryCost] FROM dg_base dg INNER JOIN sp_base sp ON dg.[goodsId] = sp.[goodsId] WHERE dg.[goodsQty] > 0 AND sp.[inquiryNum] > 0 AND sp.[inquirySpendYuan] / NULLIF(sp.[inquiryNum], 0) > (dg.[payAmt] / NULLIF(dg.[goodsQty], 0)) * 0.4 ) `; const result = await queryPddDashboardBasePagedRows({ baseCteSql, selectFieldsSql: ` [goodsId], CAST([pieceUnitPrice] AS DECIMAL(38, 2)) AS [pieceUnitPrice], CAST([avgInquiryCost] AS DECIMAL(38, 2)) AS [avgInquiryCost], CAST([avgInquiryCost] / NULLIF([pieceUnitPrice], 0) * 100.0 AS DECIMAL(38, 2)) AS [costRate] `, orderBySql: '([avgInquiryCost] / NULLIF([pieceUnitPrice], 0)) DESC, [goodsId] ASC', params, page, pageSize, }); result.rows = await hydratePddDashboardRowsWithGoodsDetails(result.rows, { detailTableName: PDD_DATA_GOODS_TABLE, startDate, endDate, includeIdOwner: true, includeImage: true, }); return result; }, }); res.json(payload); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/price-break-monitor/list', authMiddleware, async (req, res) => { const columns = getPddProductDashboardBaseColumns([ { key: 'spu', title: 'SPU' }, { key: 'oper', title: '运营负责人' }, { key: 'pieceUnitPrice', title: '近7天件单价' }, { key: 'effectiveLimitPrice', title: '有效限价' }, { key: 'breakDiffAmount', title: '破价差额' }, { key: 'breakDiffRate', title: '破价幅度' }, { key: 'limitFieldUsed', title: '命中限价字段' }, { key: 'priceLimitSourceTable', title: '限价来源表' }, ]); try { const { page, pageSize, accessScope, startDate, endDate } = await getPddProductDashboardRequestContext(req, [PDD_DATA_GOODS_TABLE]); if (!endDate) { return res.json(buildPddProductDashboardEmptyResponse({ page, pageSize, startDate, endDate, columns })); } const payload = await getCachedPddProductDashboardPayload({ boardKey: 'price-break-monitor', reqQuery: req.query, page, pageSize, accessScope, startDate, endDate, columns, loader: async () => { const priceLimitState = await getPddPriceLimitSourceState(); if (!priceLimitState?.ok || Number(priceLimitState.totalRows || 0) <= 0) { return { total: 0, rows: [] }; } const params = { startDate, endDate }; const whereParts = [ 'g.[goodsId] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) <> ''", ]; appendPddCreateTimeRangeFilters({ whereParts, params, fieldExpr: 'g.[createTime]', startDate, endDate }); if (!appendPddProductDashboardCommonFilters({ req, whereParts, params, accessScope })) { return { total: 0, rows: [] }; } const baseRows = await cloudDb.query( ` SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) AS [goodsId], MAX(TRY_CONVERT(NVARCHAR(500), g.[goodsName])) AS [goodsName], MAX(TRY_CONVERT(NVARCHAR(255), g.[shopName])) AS [shopName], MAX(TRY_CONVERT(NVARCHAR(255), tm.[ider])) AS [idOwner], MAX(TRY_CONVERT(NVARCHAR(255), tm.[dept])) AS [ownerDept], MAX(TRY_CONVERT(NVARCHAR(255), tm.[oper])) AS [oper], MAX(TRY_CONVERT(NVARCHAR(255), tm.[spu])) AS [spu], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrAmt]), 0)) AS [payAmt], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrGoodsQty]), 0)) AS [goodsQty] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${buildPddTargetExtendedJoinSqlByExpr({ goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId])))', })} WHERE ${whereParts.join(' AND ')} GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) HAVING SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrGoodsQty]), 0)) > 0 `, params, { requestTimeout: 120000 } ); if (!Array.isArray(baseRows) || baseRows.length === 0) { return { total: 0, rows: [] }; } const uniqueSpus = Array.from(new Set( baseRows .map((row) => String(row.spu || '').trim()) .filter(Boolean) )); const spuIdentifierMap = new Map(); if (uniqueSpus.length > 0) { const skuParams = {}; const skuWhereParts = [ 'src.[other_1] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), src.[other_1]))) <> ''", ]; appendNamedInCondition(skuWhereParts, skuParams, uniqueSpus, 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), src.[other_1])))', 'spu'); const skuRows = await cloudDb.query( ` SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), src.[other_1]))) AS [spu], LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), src.[sku_id]))) AS [skuId], LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), src.[other_7]))) AS [productNo], LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), src.[i_id]))) AS [styleCode] FROM ${PDD_JST_SKU_TABLE_FULL_NAME} src WHERE ${skuWhereParts.join(' AND ')} `, skuParams, { requestTimeout: 120000 } ); for (const row of skuRows || []) { const spu = String(row.spu || '').trim(); if (!spu) { continue; } if (!spuIdentifierMap.has(spu)) { spuIdentifierMap.set(spu, new Set([ spu ])); } [ row.skuId, row.productNo, row.styleCode ].forEach((value) => { const normalizedValue = String(value || '').trim(); if (normalizedValue) { spuIdentifierMap.get(spu).add(normalizedValue); } }); } } const activeTables = (priceLimitState.tables || []) .filter((item) => Number(item.rowCount || 0) > 0) .map((item) => item.tableName); const priceLimitIndexes = await loadPddPriceLimitIndexes(activeTables); const matchedRows = []; for (const row of baseRows) { const pieceUnitPrice = Number(row.goodsQty) > 0 ? Number(row.payAmt) / Number(row.goodsQty) : null; if (!Number.isFinite(pieceUnitPrice) || pieceUnitPrice <= 0) { continue; } const spu = String(row.spu || '').trim(); const identifierSet = new Set([ spu ].filter(Boolean)); const extraIdentifiers = spuIdentifierMap.get(spu); if (extraIdentifiers) { extraIdentifiers.forEach((value) => identifierSet.add(value)); } const candidates = queryPddPriceLimitCandidates({ identifiers: Array.from(identifierSet), ownerOper: row.oper, }, priceLimitIndexes); const bestCandidate = candidates[0] || null; if (!bestCandidate || !Number.isFinite(bestCandidate.effectiveLimitPrice)) { continue; } if (pieceUnitPrice >= Number(bestCandidate.effectiveLimitPrice)) { continue; } const breakDiffAmount = Number(bestCandidate.effectiveLimitPrice) - pieceUnitPrice; const breakDiffRate = Number(bestCandidate.effectiveLimitPrice) > 0 ? (breakDiffAmount / Number(bestCandidate.effectiveLimitPrice)) * 100 : null; matchedRows.push({ goodsId: row.goodsId, goodsName: row.goodsName, idOwner: row.idOwner, ownerDept: row.ownerDept, shopName: row.shopName, spu: row.spu, oper: row.oper, pieceUnitPrice: Number(pieceUnitPrice.toFixed(2)), effectiveLimitPrice: Number(Number(bestCandidate.effectiveLimitPrice).toFixed(2)), breakDiffAmount: Number(breakDiffAmount.toFixed(2)), breakDiffRate: breakDiffRate === null ? null : Number(breakDiffRate.toFixed(2)), limitFieldUsed: bestCandidate.limitFieldUsed, priceLimitSourceTable: bestCandidate.tableName, }); } matchedRows.sort((left, right) => ( Number(right.breakDiffAmount || 0) - Number(left.breakDiffAmount || 0) || Number(right.breakDiffRate || 0) - Number(left.breakDiffRate || 0) || String(left.goodsId || '').localeCompare(String(right.goodsId || '')) )); const total = matchedRows.length; const pagedRows = matchedRows.slice((page - 1) * pageSize, page * pageSize); if (pagedRows.length > 0) { const imageParams = {}; appendNamedInCondition( [], imageParams, pagedRows.map((item) => item.goodsId), 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), src.[goodsId])))', 'goodsId' ); const imageRows = await cloudDb.query( ` SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), src.[goodsId]))) AS [goodsId], MAX(TRY_CONVERT(NVARCHAR(1000), src.[hdThumbUrl])) AS [hdThumbUrl] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] src WHERE LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), src.[goodsId]))) IN (${pagedRows.map((_, index) => `@goodsId${index}`).join(', ')}) GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), src.[goodsId]))) `, imageParams ); const imageMap = new Map((imageRows || []).map((item) => [ String(item.goodsId || ''), item.hdThumbUrl || null ])); pagedRows.forEach((item) => { item.hdThumbUrl = imageMap.get(String(item.goodsId || '')) || null; }); } return { total, rows: pagedRows, }; }, }); res.json(payload); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/loss-id/list', authMiddleware, async (req, res) => { const columns = getPddProductDashboardBaseColumns([ { key: 'negativeDays', title: '近7天亏损天数' }, { key: 'realPerformance', title: '近7天真实业绩' }, { key: 'realConcludeFee', title: '近7天实际站点费用' }, { key: 'profit', title: '近7天亏损金额' }, { key: 'feeRatio', title: '近7天费比' }, ]); try { const { page, pageSize, accessScope, startDate, endDate } = await getPddProductDashboardRequestContext(req, [PDD_DATA_GOODS_TABLE]); if (!startDate || !endDate) { return res.json(buildPddProductDashboardEmptyResponse({ page, pageSize, startDate, endDate, columns })); } const payload = await getCachedPddProductDashboardPayload({ boardKey: 'loss-id', reqQuery: req.query, page, pageSize, accessScope, startDate, endDate, columns, loader: async () => { const params = { startDate, endDate, offset: (page - 1) * pageSize, pageSize, }; const whereParts = [ 'a.[item_id] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), a.[item_id]))) <> ''", 'TRY_CONVERT(DATE, a.[stat_date]) BETWEEN @startDate AND @endDate', "ISNULL(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), a.[platform]))), N'') IN (N'Pinduoduo', N'拼多多')", ]; const needsTargetJoin = shouldUsePddTargetJoinForDashboardFilters(req, accessScope); if (!appendPddProductDashboardCommonFilters({ req, whereParts, params, accessScope, shopNameExpr: 'TRY_CONVERT(NVARCHAR(255), a.[shop_name])', goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), a.[item_id])))', departmentExpr: needsTargetJoin ? 'tm.[dept]' : null, idOwnerExpr: needsTargetJoin ? 'tm.[ider]' : null, })) { return { total: 0, rows: [] }; } const whereSql = `WHERE ${whereParts.join(' AND ')}`; const baseCteSql = ` WITH daily AS ( SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), a.[item_id]))) AS [goodsId], TRY_CONVERT(DATE, a.[stat_date]) AS [statDate], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), a.[real_performance]), 0)) AS [realPerformance], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), a.[real_conclude_fee]), 0)) AS [realConcludeFee] FROM [JSDataWarehouse].[ads].[ads_itemid_sales_cost_ratio_v2_di] a ${needsTargetJoin ? buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), a.[item_id])))', }) : ''} ${whereSql} GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), a.[item_id]))), TRY_CONVERT(DATE, a.[stat_date]) ), base AS ( SELECT [goodsId], SUM(CASE WHEN [realPerformance] - [realConcludeFee] < 0 THEN 1 ELSE 0 END) AS [negativeDays], SUM([realPerformance]) AS [realPerformance], SUM([realConcludeFee]) AS [realConcludeFee], SUM([realPerformance] - [realConcludeFee]) AS [profit] FROM daily GROUP BY [goodsId] HAVING SUM([realPerformance] - [realConcludeFee]) <= -1 ) `; const result = await queryPddDashboardBasePagedRows({ baseCteSql, selectFieldsSql: ` [goodsId], CAST([negativeDays] AS DECIMAL(38, 0)) AS [negativeDays], CAST([realPerformance] AS DECIMAL(38, 2)) AS [realPerformance], CAST([realConcludeFee] AS DECIMAL(38, 2)) AS [realConcludeFee], CAST([profit] AS DECIMAL(38, 2)) AS [profit], CAST(CASE WHEN [realPerformance] = 0 THEN 0 ELSE [realConcludeFee] * 100.0 / NULLIF([realPerformance], 0) END AS DECIMAL(38, 2)) AS [feeRatio] `, orderBySql: '[profit] ASC, [goodsId] ASC', params, page, pageSize, }); result.rows = await hydratePddDashboardRowsWithGoodsDetails(result.rows, { detailTableName: PDD_DATA_GOODS_TABLE, startDate, endDate, includeIdOwner: true, includeImage: true, }); return result; }, }); res.json(payload); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/loss-amount/list', authMiddleware, async (req, res) => { const columns = getPddProductDashboardBaseColumns([ { key: 'goodsUv', title: '近7天访客数' }, { key: 'orderUserCount', title: '近7天下单人数' }, { key: 'payOrdrUsrCnt', title: '近7天成交买家数' }, { key: 'unpaidUserCount', title: '近7天未支付人数' }, { key: 'customerUnitPrice', title: '近7天客单价' }, { key: 'lossAmount', title: '近7天损失金额' }, ]); try { const { page, pageSize, accessScope, startDate, endDate } = await getPddProductDashboardRequestContext(req, [PDD_DATA_GOODS_TABLE]); if (!endDate) { return res.json({ success: true, data: { page, pageSize, total: 0, startDate, endDate, columns, rows: [] } }); } const params = { startDate, endDate, offset: (page - 1) * pageSize, pageSize }; const whereParts = [ 'g.[goodsId] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) <> ''", ]; appendPddCreateTimeRangeFilters({ whereParts, params, fieldExpr: 'g.[createTime]', startDate, endDate }); const needsTargetJoin = shouldUsePddTargetJoinForDashboardFilters(req, accessScope); if (!appendPddProductDashboardCommonFilters({ req, whereParts, params, accessScope, departmentExpr: needsTargetJoin ? 'tm.[dept]' : null, idOwnerExpr: needsTargetJoin ? 'tm.[ider]' : null, })) { return res.json({ success: true, data: { page, pageSize, total: 0, startDate, endDate, columns, rows: [] } }); } const whereSql = `WHERE ${whereParts.join(' AND ')}`; const baseCteSql = ` WITH base AS ( SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) AS [goodsId], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[goodsUv]), 0)) AS [goodsUv], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrUsrCnt]), 0)) AS [payOrdrUsrCnt], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrAmt]), 0)) AS [payOrdrAmt], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[goodsUv]), 0) * ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[ordrVstrRto]), 0)) AS [orderUserCount] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${needsTargetJoin ? buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId])))', }) : ''} ${whereSql} GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) ) `; const result = await queryPddDashboardBasePagedRows({ baseCteSql, selectFieldsSql: ` [goodsId], CAST([goodsUv] AS DECIMAL(38, 2)) AS [goodsUv], CAST([orderUserCount] AS DECIMAL(38, 2)) AS [orderUserCount], CAST([payOrdrUsrCnt] AS DECIMAL(38, 2)) AS [payOrdrUsrCnt], CAST([orderUserCount] - [payOrdrUsrCnt] AS DECIMAL(38, 2)) AS [unpaidUserCount], CAST([payOrdrAmt] / NULLIF([payOrdrUsrCnt], 0) AS DECIMAL(38, 2)) AS [customerUnitPrice], CAST(([orderUserCount] - [payOrdrUsrCnt]) * ([payOrdrAmt] / NULLIF([payOrdrUsrCnt], 0)) AS DECIMAL(38, 2)) AS [lossAmount] `, orderBySql: '(([orderUserCount] - [payOrdrUsrCnt]) * ([payOrdrAmt] / NULLIF([payOrdrUsrCnt], 0))) DESC, [goodsId] ASC', params, page, pageSize, }); result.rows = await hydratePddDashboardRowsWithGoodsDetails(result.rows, { detailTableName: PDD_DATA_GOODS_TABLE, startDate, endDate, includeIdOwner: true, includeImage: true, }); res.json({ success: true, data: { page, pageSize, total: result.total, startDate, endDate, columns, rows: result.rows || [], }, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/invalid-link/list', authMiddleware, async (req, res) => { const columns = [ { key: 'goodsId', title: '商品id' }, { key: 'goodsName', title: '商品名称' }, { key: 'hdThumbUrl', title: '商品主图' }, { key: 'idOwner', title: 'id负责人' }, { key: 'ownerDept', title: '所属部门' }, { key: 'shopName', title: '店铺名称' }, { key: 'goodsUv', title: '近7天商品访客数' }, { key: 'goodsPv', title: '近7天商品浏览量' }, { key: 'payOrdrCnt', title: '近7天成交订单数' }, { key: 'payOrdrUsrCnt', title: '近7天成交买家数' }, { key: 'payOrdrAmt', title: '近7天成交金额' }, ]; try { const page = safePositiveInt(req.query.page, 1, 1, 1000000); const pageSize = safePositiveInt(req.query.pageSize, 20, 1, 200); const minGoodsUv = Number.isFinite(Number(req.query.minGoodsUv)) ? Number(req.query.minGoodsUv) : 100; const accessScope = await getPddDataAccessScope(req.user.sub); if (!accessScope) { return res.status(401).json({ success: false, error: 'user not found' }); } const latestBizDate = await getLatestPddDashboardBizDate([PDD_DATA_GOODS_TABLE]); if (!latestBizDate) { return res.json(buildPddProductDashboardEmptyResponse({ page, pageSize, startDate: null, endDate: null, columns, extra: { minGoodsUv }, })); } const endDate = dayjs(latestBizDate).format('YYYY-MM-DD'); const startDate = dayjs(latestBizDate).subtract(6, 'day').format('YYYY-MM-DD'); const payload = await getCachedPddProductDashboardPayload({ boardKey: 'invalid-link', reqQuery: req.query, page, pageSize, accessScope, startDate, endDate, columns, extraResponseData: { minGoodsUv }, extraCachePayload: { minGoodsUv }, loader: async () => { const params = { startDate, endDate, minGoodsUv, offset: (page - 1) * pageSize, pageSize, }; const whereParts = [ 'g.[goodsId] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) <> ''", ]; appendPddCreateTimeRangeFilters({ whereParts, params, fieldExpr: 'g.[createTime]', startDate, endDate }); const needsTargetJoin = shouldUsePddTargetJoinForDashboardFilters(req, accessScope); if (!appendPddProductDashboardCommonFilters({ req, whereParts, params, accessScope, departmentExpr: needsTargetJoin ? 'tm.[dept]' : null, idOwnerExpr: needsTargetJoin ? 'tm.[ider]' : null, })) { return { total: 0, rows: [] }; } const whereSql = `WHERE ${whereParts.join(' AND ')}`; const baseCteSql = ` WITH base AS ( SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) AS [goodsId], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[goodsUv]), 0)) AS [goodsUv], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[goodsPv]), 0)) AS [goodsPv], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrCnt]), 0)) AS [payOrdrCnt], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrUsrCnt]), 0)) AS [payOrdrUsrCnt], SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrAmt]), 0)) AS [payOrdrAmt] FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${needsTargetJoin ? buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId])))', }) : ''} ${whereSql} GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) HAVING SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[goodsUv]), 0)) >= @minGoodsUv AND SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrCnt]), 0)) = 0 ), paged AS ( SELECT [goodsId], CAST([goodsUv] AS DECIMAL(38, 2)) AS [goodsUv], CAST([goodsPv] AS DECIMAL(38, 2)) AS [goodsPv], CAST([payOrdrCnt] AS DECIMAL(38, 2)) AS [payOrdrCnt], CAST([payOrdrUsrCnt] AS DECIMAL(38, 2)) AS [payOrdrUsrCnt], CAST([payOrdrAmt] AS DECIMAL(38, 2)) AS [payOrdrAmt], COUNT(1) OVER() AS [__total] FROM base ORDER BY [goodsUv] DESC, [goodsId] ASC OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY ) `; const result = await cloudDb.query( ` ${baseCteSql} SELECT p.[goodsId], p.[goodsUv], p.[goodsPv], p.[payOrdrCnt], p.[payOrdrUsrCnt], p.[payOrdrAmt], p.[__total] FROM paged p ORDER BY p.[goodsUv] DESC, p.[goodsId] ASC `, params ); const total = Number(result?.[0]?.__total || 0); const baseRows = (result || []).map((row) => { const nextRow = { ...row }; delete nextRow.__total; return nextRow; }); const rows = await hydratePddDashboardRowsWithGoodsDetails(baseRows, { detailTableName: PDD_DATA_GOODS_TABLE, startDate, endDate, includeIdOwner: true, includeImage: true, }); return { total, rows }; }, }); res.json(payload); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/id-dimension-board/list', authMiddleware, async (req, res) => { const columns = [ ...getPddProductDashboardBaseColumns([ { key: 'spu', title: 'SPU' }, { 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: 'payOrdrGoodsQty', title: '近7天成交件数' }, { key: 'orderNum', title: '近7天成交订单数' }, { key: 'payOrdrAmt', title: '近7天真实业绩' }, { key: 'payOrdrUsrCnt', title: '近7天买家数' }, { key: 'unpaidUserCount', title: '近7天未支付人数' }, { key: 'customerUnitPrice', title: '近7天客单价' }, { key: 'pieceUnitPrice', title: '近7天件单价' }, { key: 'spend', title: '近7天推广花费' }, { key: 'feeRatio', title: '近7天费比' }, { key: 'badReviewCount', title: '近7天差评量' }, { key: 'afterSaleCount', title: '近7天售后单量' }, { key: 'afterSaleRate', title: '近7天售后率' }, ]), ]; try { const sortField = (req.query.sortField || '').trim(); const sortOrder = (req.query.sortOrder || '').trim().toLowerCase(); const sortFieldDefs = [ { key: 'goodsId', sortExpr: 'gl.[goodsId]' }, { key: 'goodsName', sortExpr: 'gl.[goodsName]' }, { key: 'idOwner', sortExpr: 'gl.[idOwner]' }, { key: 'ownerDept', sortExpr: 'gl.[ownerDept]' }, { key: 'shopName', sortExpr: 'gl.[shopName]' }, { key: 'spu', sortExpr: 'gl.[spu]' }, { key: 'goodsUv', sortExpr: 'ISNULL(gl.[goodsUv], 0)' }, { key: 'inquiryNum', sortExpr: 'ISNULL(gl.[inquiryNum], 0)' }, { key: 'goodsFavCnt', sortExpr: 'ISNULL(gl.[goodsFavCnt], 0)' }, { key: 'goodsFavNum', sortExpr: 'ISNULL(gl.[goodsFavNum], 0)' }, { key: 'orderUserCount', sortExpr: 'ISNULL(gl.[orderUserCount], 0)' }, { key: 'ctr', sortExpr: 'ISNULL(gl.[ctr], 0)' }, { key: 'clickConversionRate', sortExpr: 'ISNULL(gl.[clickConversionRate], 0)' }, { key: 'goodsVcr', sortExpr: 'ISNULL(gl.[goodsVcr], 0)' }, { key: 'ordrVstrRto', sortExpr: 'ISNULL(gl.[ordrVstrRto], 0)' }, { key: 'payOrdrRto', sortExpr: 'ISNULL(gl.[payOrdrRto], 0)' }, { key: 'imprUsrCnt', sortExpr: 'ISNULL(gl.[imprUsrCnt], 0)' }, { key: 'payOrdrGoodsQty', sortExpr: 'ISNULL(gl.[payOrdrGoodsQty], 0)' }, { key: 'orderNum', sortExpr: 'ISNULL(gl.[orderNum], 0)' }, { key: 'payOrdrAmt', sortExpr: 'ISNULL(gl.[payOrdrAmt], 0)' }, { key: 'payOrdrUsrCnt', sortExpr: 'ISNULL(gl.[payOrdrUsrCnt], 0)' }, { key: 'unpaidUserCount', sortExpr: 'ISNULL(gl.[unpaidUserCount], 0)' }, { key: 'customerUnitPrice', sortExpr: 'ISNULL(gl.[customerUnitPrice], 0)' }, { key: 'pieceUnitPrice', sortExpr: 'ISNULL(gl.[pieceUnitPrice], 0)' }, { key: 'spend', sortExpr: 'ISNULL(gl.[spend], 0)' }, { key: 'feeRatio', sortExpr: 'ISNULL(gl.[feeRatio], 0)' }, { key: 'badReviewCount', sortExpr: 'ISNULL(gl.[badReviewCount], 0)' }, { key: 'afterSaleCount', sortExpr: 'ISNULL(gl.[afterSaleCount], 0)' }, { key: 'afterSaleRate', sortExpr: 'ISNULL(gl.[afterSaleRate], 0)' }, ]; const sortFieldDef = sortFieldDefs.find((item) => item.key === sortField) || null; const orderExpr = sortFieldDef?.sortExpr || 'ISNULL(gl.[payOrdrAmt], 0)'; const orderDirection = sortOrder === 'ascend' ? 'ASC' : 'DESC'; const orderTieBreaker = sortFieldDef?.key === 'goodsId' ? '' : ', gl.[goodsId] ASC'; const { page, pageSize, accessScope, startDate, endDate } = await getPddProductDashboardRequestContext(req, [ PDD_DATA_GOODS_TABLE, PDD_SPREAD_GOODS_TABLE, PDD_EVALUATION_TABLE, ]); if (!startDate || !endDate) { return res.json(buildPddProductDashboardEmptyResponse({ page, pageSize, startDate, endDate, columns, })); } const payload = await getCachedPddProductDashboardPayload({ boardKey: 'id-dimension-board', reqQuery: req.query, page, pageSize, accessScope, startDate, endDate, columns, extraResponseData: { sortField: sortFieldDef?.key || '', sortOrder: sortFieldDef ? (sortOrder || 'descend') : '', }, loader: async () => { const params = { startDate, endDate, }; const scopeWhereParts = [ 'g.[goodsId] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) <> ''", ]; appendPddCreateTimeRangeFilters({ whereParts: scopeWhereParts, params, fieldExpr: 'g.[createTime]', startDate, endDate, }); const needsTargetJoin = shouldUsePddTargetJoinForDashboardFilters(req, accessScope); if (!appendPddProductDashboardCommonFilters({ req, whereParts: scopeWhereParts, params, accessScope, departmentExpr: needsTargetJoin ? 'tm.[dept]' : null, idOwnerExpr: needsTargetJoin ? 'tm.[ider]' : null, })) { return { total: 0, rows: [] }; } const scopeWhereSql = `WHERE ${scopeWhereParts.join(' AND ')}`; const batchSql = ` IF OBJECT_ID('tempdb..#goods_scope') IS NOT NULL DROP TABLE #goods_scope; IF OBJECT_ID('tempdb..#goods_agg') IS NOT NULL DROP TABLE #goods_agg; IF OBJECT_ID('tempdb..#paged_goods') IS NOT NULL DROP TABLE #paged_goods; IF OBJECT_ID('tempdb..#goods_detail') IS NOT NULL DROP TABLE #goods_detail; IF OBJECT_ID('tempdb..#target_agg') IS NOT NULL DROP TABLE #target_agg; IF OBJECT_ID('tempdb..#spread_agg') IS NOT NULL DROP TABLE #spread_agg; IF OBJECT_ID('tempdb..#review_agg') IS NOT NULL DROP TABLE #review_agg; IF OBJECT_ID('tempdb..#dws_order_agg') IS NOT NULL DROP TABLE #dws_order_agg; SELECT DISTINCT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) AS [goodsId] INTO #goods_scope FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${needsTargetJoin ? buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId])))', }) : ''} ${scopeWhereSql}; CREATE UNIQUE CLUSTERED INDEX IX_goods_scope_goodsId ON #goods_scope([goodsId]); SELECT gs.[goodsId], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[goodsUv]), 0)) AS DECIMAL(38, 2)) AS [goodsUv], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[goodsFavCnt]), 0)) AS DECIMAL(38, 2)) AS [goodsFavCnt], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[imprUsrCnt]), 0)) AS DECIMAL(38, 2)) AS [imprUsrCnt], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrGoodsQty]), 0)) AS DECIMAL(38, 2)) AS [payOrdrGoodsQty], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrCnt]), 0)) AS DECIMAL(38, 2)) AS [orderNum], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrAmt]), 0)) AS DECIMAL(38, 2)) AS [payOrdrAmt], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrUsrCnt]), 0)) AS DECIMAL(38, 2)) AS [payOrdrUsrCnt], CAST(ROUND(SUM( ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[goodsUv]), 0) * ( ISNULL(TRY_CONVERT(DECIMAL(38, 6), REPLACE(TRY_CONVERT(NVARCHAR(50), g.[ordrVstrRto]), '%', '')), 0) / CASE WHEN CHARINDEX('%', TRY_CONVERT(NVARCHAR(50), g.[ordrVstrRto])) > 0 THEN 100.0 WHEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[ordrVstrRto]), 0) > 1 THEN 100.0 ELSE 1.0 END ) ), 0) AS BIGINT) AS [orderUserCount] INTO #goods_agg FROM #goods_scope gs INNER JOIN [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ON LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) = gs.[goodsId] WHERE g.[createTime] >= @startDateTime AND g.[createTime] < @endDateExclusive GROUP BY gs.[goodsId]; CREATE UNIQUE CLUSTERED INDEX IX_goods_agg_goodsId ON #goods_agg([goodsId]); DECLARE @total BIGINT; SET @total = (SELECT COUNT(1) FROM #goods_agg); SELECT gs.[goodsId], MAX(TRY_CONVERT(NVARCHAR(500), g.[goodsName])) AS [goodsName], MAX(TRY_CONVERT(NVARCHAR(255), g.[shopName])) AS [shopName], MAX(TRY_CONVERT(NVARCHAR(1000), g.[hdThumbUrl])) AS [hdThumbUrl] INTO #goods_detail FROM #goods_scope gs INNER JOIN [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ON LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) = gs.[goodsId] WHERE g.[createTime] >= @startDateTime AND g.[createTime] < @endDateExclusive GROUP BY gs.[goodsId]; CREATE UNIQUE CLUSTERED INDEX IX_goods_detail_goodsId ON #goods_detail([goodsId]); SELECT gs.[goodsId], MAX(src.[ider]) AS [idOwner], MAX(src.[dept]) AS [ownerDept], MAX(src.[spu]) AS [spu] INTO #target_agg FROM #goods_scope gs LEFT JOIN ${PDD_TARGET_TABLE_FULL_NAME} src ON src.[shopproid] = gs.[goodsId] GROUP BY gs.[goodsId]; CREATE UNIQUE CLUSTERED INDEX IX_target_agg_goodsId ON #target_agg([goodsId]); SELECT gs.[goodsId], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[spend]), 0)) / 1000.0 AS DECIMAL(38, 2)) AS [spend], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[click]), 0)) AS DECIMAL(38, 2)) AS [click], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[impression]), 0)) AS DECIMAL(38, 2)) AS [impression], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[orderNum]), 0)) AS DECIMAL(38, 2)) AS [spreadOrderNum], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[multiGoalInquiryNum]), 0)) AS DECIMAL(38, 2)) AS [inquiryNum], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[multiGoalGoodsFavNum]), 0)) AS DECIMAL(38, 2)) AS [goodsFavNum] INTO #spread_agg FROM #goods_scope gs INNER JOIN [${PDD_DATA_GOODS_SCHEMA}].[${PDD_SPREAD_GOODS_TABLE}] s ON LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), s.[goodsId]))) = gs.[goodsId] WHERE s.[createTime] >= @startDateTime AND s.[createTime] < @endDateExclusive GROUP BY gs.[goodsId]; CREATE UNIQUE CLUSTERED INDEX IX_spread_agg_goodsId ON #spread_agg([goodsId]); SELECT gs.[goodsId], COUNT(1) AS [badReviewCount] INTO #review_agg FROM #goods_scope gs INNER JOIN [${PDD_DATA_GOODS_SCHEMA}].[${PDD_EVALUATION_TABLE}] e ON LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), e.[goodsId]))) = gs.[goodsId] WHERE e.[createTime] >= @startDateTime AND e.[createTime] < @endDateExclusive AND TRY_CONVERT(INT, e.[descScore]) IN (1, 2, 3) GROUP BY gs.[goodsId]; CREATE UNIQUE CLUSTERED INDEX IX_review_agg_goodsId ON #review_agg([goodsId]); SELECT gs.[goodsId], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[sales_amount]), 0)) AS DECIMAL(38, 2)) AS [dwsSalesAmount], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[goods_count]), 0)) AS DECIMAL(38, 2)) AS [dwsGoodsCount], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[real_conclude_fee]), 0)) AS DECIMAL(38, 2)) AS [dwsRealConcludeFee], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[real_performance]), 0)) AS DECIMAL(38, 2)) AS [dwsRealPerformance], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[after_sale_amount]), 0)) AS DECIMAL(38, 2)) AS [afterSaleAmount], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[refund_count]), 0)) AS DECIMAL(38, 2)) AS [afterSaleCount], CAST(COUNT(DISTINCT CASE WHEN NULLIF(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), d.[sourceno]))), N'') IS NOT NULL THEN LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), d.[sourceno]))) ELSE NULL END) AS DECIMAL(38, 2)) AS [dwsOrderNum] INTO #dws_order_agg FROM [JSDataWarehouse].[dws].[dws_sn_sales_full_info_di] d INNER JOIN #goods_scope gs ON d.[item_id] = CONVERT(VARCHAR(50), gs.[goodsId]) WHERE d.[stat_date] BETWEEN TRY_CONVERT(DATE, @startDate) AND TRY_CONVERT(DATE, @endDate) AND ISNULL(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), d.[platform]))), N'') IN (N'Pinduoduo', N'拼多多') GROUP BY gs.[goodsId]; CREATE UNIQUE CLUSTERED INDEX IX_dws_order_agg_goodsId ON #dws_order_agg([goodsId]); SELECT ga.[goodsId], gd.[goodsName], gd.[hdThumbUrl], ta.[idOwner], ta.[ownerDept], gd.[shopName], ta.[spu], ga.[goodsUv], ISNULL(sp.[inquiryNum], 0) AS [inquiryNum], ga.[goodsFavCnt], ISNULL(sp.[goodsFavNum], 0) AS [goodsFavNum], ISNULL(ga.[orderUserCount], 0) AS [orderUserCount], CAST(CASE WHEN ISNULL(sp.[impression], 0) = 0 THEN 0 ELSE ISNULL(sp.[click], 0) * 100.0 / NULLIF(sp.[impression], 0) END AS DECIMAL(38, 2)) AS [ctr], CAST(CASE WHEN ISNULL(sp.[click], 0) = 0 THEN 0 ELSE ISNULL(sp.[spreadOrderNum], 0) * 100.0 / NULLIF(sp.[click], 0) END AS DECIMAL(38, 2)) AS [clickConversionRate], CAST(CASE WHEN ISNULL(ga.[goodsUv], 0) = 0 THEN 0 ELSE ISNULL(ga.[payOrdrUsrCnt], 0) * 100.0 / NULLIF(ga.[goodsUv], 0) END AS DECIMAL(38, 2)) AS [goodsVcr], CAST(CASE WHEN ISNULL(ga.[goodsUv], 0) = 0 THEN 0 ELSE ISNULL(ga.[orderUserCount], 0) * 100.0 / NULLIF(ga.[goodsUv], 0) END AS DECIMAL(38, 2)) AS [ordrVstrRto], CAST(CASE WHEN ISNULL(ga.[orderUserCount], 0) = 0 THEN 0 ELSE ISNULL(ga.[payOrdrUsrCnt], 0) * 100.0 / NULLIF(ga.[orderUserCount], 0) END AS DECIMAL(38, 2)) AS [payOrdrRto], ga.[imprUsrCnt], ISNULL(da.[dwsGoodsCount], 0) AS [payOrdrGoodsQty], ISNULL(da.[dwsOrderNum], 0) AS [orderNum], ISNULL(da.[dwsRealPerformance], 0) AS [payOrdrAmt], ga.[payOrdrUsrCnt], CAST(ROUND(ISNULL(ga.[orderUserCount], 0) - ISNULL(ga.[payOrdrUsrCnt], 0), 0) AS BIGINT) AS [unpaidUserCount], CAST(CASE WHEN ISNULL(ga.[payOrdrUsrCnt], 0) = 0 THEN 0 ELSE ISNULL(da.[dwsRealPerformance], 0) / NULLIF(ga.[payOrdrUsrCnt], 0) END AS DECIMAL(38, 2)) AS [customerUnitPrice], CAST(CASE WHEN ISNULL(da.[dwsGoodsCount], 0) = 0 THEN 0 ELSE ISNULL(da.[dwsRealPerformance], 0) / NULLIF(da.[dwsGoodsCount], 0) END AS DECIMAL(38, 2)) AS [pieceUnitPrice], ISNULL(sp.[spend], 0) AS [spend], CAST(CASE WHEN ISNULL(da.[dwsRealPerformance], 0) = 0 THEN 0 ELSE ISNULL(da.[dwsRealConcludeFee], 0) * 100.0 / NULLIF(da.[dwsRealPerformance], 0) END AS DECIMAL(38, 2)) AS [feeRatio], CAST(ISNULL(rv.[badReviewCount], 0) AS DECIMAL(38, 2)) AS [badReviewCount], ISNULL(da.[afterSaleCount], 0) AS [afterSaleCount], CAST(CASE WHEN ISNULL(da.[dwsRealPerformance], 0) = 0 THEN 0 ELSE ISNULL(da.[afterSaleAmount], 0) * 100.0 / NULLIF(da.[dwsRealPerformance], 0) END AS DECIMAL(38, 2)) AS [afterSaleRate] INTO #goods_list FROM #goods_agg ga LEFT JOIN #goods_detail gd ON gd.[goodsId] = ga.[goodsId] LEFT JOIN #target_agg ta ON ta.[goodsId] = ga.[goodsId] LEFT JOIN #spread_agg sp ON sp.[goodsId] = ga.[goodsId] LEFT JOIN #dws_order_agg da ON da.[goodsId] = ga.[goodsId] LEFT JOIN #review_agg rv ON rv.[goodsId] = ga.[goodsId]; CREATE UNIQUE CLUSTERED INDEX IX_goods_list_goodsId ON #goods_list([goodsId]); SET @total = (SELECT COUNT(1) FROM #goods_list); SELECT * INTO #paged_goods FROM ( SELECT gl.*, ROW_NUMBER() OVER (ORDER BY ${orderExpr} ${orderDirection}${orderTieBreaker}) AS [rn] FROM #goods_list gl ) src WHERE src.[rn] > @offset AND src.[rn] <= (@offset + @pageSize); CREATE UNIQUE CLUSTERED INDEX IX_paged_goods_goodsId ON #paged_goods([goodsId]); SELECT pg.*, CAST(@total AS BIGINT) AS [__total] FROM #paged_goods pg ORDER BY pg.[rn]; `; const result = await cloudDb.query(batchSql, { ...params, offset: (page - 1) * pageSize, pageSize, }, { requestTimeout: 120000, }); const total = Number(result?.[0]?.__total || 0); const normalizedRows = (result || []).map((row) => { const nextRow = { ...row }; delete nextRow.__total; delete nextRow.rn; return nextRow; }); return { total, rows: normalizedRows, sortField: sortFieldDef?.key || '', sortOrder: sortFieldDef ? (sortOrder || 'descend') : '', }; }, }); res.json(payload); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/shop-dimension-board/list', authMiddleware, async (req, res) => { const columns = [ { key: 'shopName', title: '店铺名称' }, { 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: 'payOrdrGoodsQty', title: '近7天成交件数' }, { key: 'orderNum', title: '近7天订单量' }, { key: 'payOrdrUsrCnt', title: '近7天买家数' }, { key: 'customerUnitPrice', title: '近7天客单价' }, { key: 'pieceUnitPrice', title: '近7天件单价' }, { key: 'spreadGmv', title: '近7天总交易额' }, { key: 'indirectGmv', title: '近7天间接交易额' }, { key: 'indirectOrderNum', title: '近7天间接成交笔数' }, { key: 'spend', title: '近7天总花费' }, { key: 'realConcludeFee', title: '近7天实际站点费用' }, { key: 'feeRatio', title: '近7天费比' }, { key: 'overallRoi', title: '近7天实际投产比' }, { key: 'netRoi', title: '近7天净实际投产比' }, { key: 'avgDealCost', title: '近7天每笔成交花费' }, { key: 'avgInquiryCost', title: '近7天平均询单成本' }, ]; try { const sortField = (req.query.sortField || '').trim(); const sortOrder = (req.query.sortOrder || '').trim().toLowerCase(); const sortFieldDefs = [ { key: 'shopName', sortExpr: 'sl.[shopName]' }, { key: 'ownerDept', sortExpr: 'sl.[ownerDept]' }, { key: 'goodsUv', sortExpr: 'ISNULL(sl.[goodsUv], 0)' }, { key: 'inquiryNum', sortExpr: 'ISNULL(sl.[inquiryNum], 0)' }, { key: 'goodsFavCnt', sortExpr: 'ISNULL(sl.[goodsFavCnt], 0)' }, { key: 'goodsFavNum', sortExpr: 'ISNULL(sl.[goodsFavNum], 0)' }, { key: 'orderUserCount', sortExpr: 'ISNULL(sl.[orderUserCount], 0)' }, { key: 'ctr', sortExpr: 'ISNULL(sl.[ctr], 0)' }, { key: 'clickConversionRate', sortExpr: 'ISNULL(sl.[clickConversionRate], 0)' }, { key: 'goodsVcr', sortExpr: 'ISNULL(sl.[goodsVcr], 0)' }, { key: 'ordrVstrRto', sortExpr: 'ISNULL(sl.[ordrVstrRto], 0)' }, { key: 'payOrdrRto', sortExpr: 'ISNULL(sl.[payOrdrRto], 0)' }, { key: 'imprUsrCnt', sortExpr: 'ISNULL(sl.[imprUsrCnt], 0)' }, { key: 'unpaidUserCount', sortExpr: 'ISNULL(sl.[unpaidUserCount], 0)' }, { key: 'payOrdrAmt', sortExpr: 'ISNULL(sl.[payOrdrAmt], 0)' }, { key: 'payOrdrGoodsQty', sortExpr: 'ISNULL(sl.[payOrdrGoodsQty], 0)' }, { key: 'orderNum', sortExpr: 'ISNULL(sl.[orderNum], 0)' }, { key: 'payOrdrUsrCnt', sortExpr: 'ISNULL(sl.[payOrdrUsrCnt], 0)' }, { key: 'customerUnitPrice', sortExpr: 'ISNULL(sl.[customerUnitPrice], 0)' }, { key: 'pieceUnitPrice', sortExpr: 'ISNULL(sl.[pieceUnitPrice], 0)' }, { key: 'spreadGmv', sortExpr: 'ISNULL(sl.[spreadGmv], 0)' }, { key: 'indirectGmv', sortExpr: 'ISNULL(sl.[indirectGmv], 0)' }, { key: 'indirectOrderNum', sortExpr: 'ISNULL(sl.[indirectOrderNum], 0)' }, { key: 'spend', sortExpr: 'ISNULL(sl.[spend], 0)' }, { key: 'realConcludeFee', sortExpr: 'ISNULL(sl.[realConcludeFee], 0)' }, { key: 'feeRatio', sortExpr: 'ISNULL(sl.[feeRatio], 0)' }, { key: 'overallRoi', sortExpr: 'ISNULL(sl.[overallRoi], 0)' }, { key: 'netRoi', sortExpr: 'ISNULL(sl.[netRoi], 0)' }, { key: 'avgDealCost', sortExpr: 'ISNULL(sl.[avgDealCost], 0)' }, { key: 'avgInquiryCost', sortExpr: 'ISNULL(sl.[avgInquiryCost], 0)' }, ]; const sortFieldDef = sortFieldDefs.find((item) => item.key === sortField) || null; const orderExpr = sortFieldDef?.sortExpr || 'ISNULL(sl.[payOrdrAmt], 0)'; const orderDirection = sortOrder === 'ascend' ? 'ASC' : 'DESC'; const orderTieBreaker = sortFieldDef?.key === 'shopName' ? '' : ', sl.[shopName] ASC'; const { page, pageSize, accessScope, startDate, endDate } = await getPddProductDashboardRequestContext(req, [ PDD_DATA_GOODS_TABLE, PDD_SPREAD_GOODS_TABLE, PDD_EVALUATION_TABLE, ]); if (!startDate || !endDate) { return res.json(buildPddProductDashboardEmptyResponse({ page, pageSize, startDate, endDate, columns, })); } const payload = await getCachedPddProductDashboardPayload({ boardKey: 'shop-dimension-board', reqQuery: req.query, page, pageSize, accessScope, startDate, endDate, columns, extraResponseData: { sortField: sortFieldDef?.key || '', sortOrder: sortFieldDef ? (sortOrder || 'descend') : '', }, loader: async () => { const params = { startDate, endDate, }; const scopeWhereParts = [ 'g.[shopName] IS NOT NULL', "LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), g.[shopName]))) <> ''", ]; appendPddCreateTimeRangeFilters({ whereParts: scopeWhereParts, params, fieldExpr: 'g.[createTime]', startDate, endDate, }); const needsTargetJoin = shouldUsePddTargetJoinForDashboardFilters(req, accessScope); if (!appendPddProductDashboardCommonFilters({ req, whereParts: scopeWhereParts, params, shopNameExpr: 'TRY_CONVERT(NVARCHAR(255), g.[shopName])', departmentExpr: needsTargetJoin ? 'tm.[dept]' : null, idOwnerExpr: needsTargetJoin ? 'tm.[ider]' : null, accessScope, })) { return { total: 0, rows: [] }; } const scopeWhereSql = `WHERE ${scopeWhereParts.join(' AND ')}`; const batchSql = ` IF OBJECT_ID('tempdb..#shop_scope') IS NOT NULL DROP TABLE #shop_scope; IF OBJECT_ID('tempdb..#target_agg') IS NOT NULL DROP TABLE #target_agg; IF OBJECT_ID('tempdb..#goods_agg') IS NOT NULL DROP TABLE #goods_agg; IF OBJECT_ID('tempdb..#spread_agg') IS NOT NULL DROP TABLE #spread_agg; IF OBJECT_ID('tempdb..#ads_agg') IS NOT NULL DROP TABLE #ads_agg; IF OBJECT_ID('tempdb..#shop_list') IS NOT NULL DROP TABLE #shop_list; IF OBJECT_ID('tempdb..#paged_shop') IS NOT NULL DROP TABLE #paged_shop; SELECT DISTINCT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), g.[shopName]))) AS [shopName] INTO #shop_scope FROM [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ${needsTargetJoin ? buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId])))', }) : ''} ${scopeWhereSql}; CREATE UNIQUE CLUSTERED INDEX IX_shop_scope_shopName ON #shop_scope([shopName]); SELECT ss.[shopName], MAX(src.[ider]) AS [idOwner], MAX(src.[dept]) AS [ownerDept] INTO #target_agg FROM #shop_scope ss LEFT JOIN [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ON LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), g.[shopName]))) = ss.[shopName] AND g.[createTime] >= @startDateTime AND g.[createTime] < @endDateExclusive LEFT JOIN ${PDD_TARGET_TABLE_FULL_NAME} src ON src.[shopproid] = LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), g.[goodsId]))) GROUP BY ss.[shopName]; CREATE UNIQUE CLUSTERED INDEX IX_target_agg_shopName ON #target_agg([shopName]); SELECT ss.[shopName], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[goodsUv]), 0)) AS DECIMAL(38, 2)) AS [goodsUv], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[goodsFavCnt]), 0)) AS DECIMAL(38, 2)) AS [goodsFavCnt], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[imprUsrCnt]), 0)) AS DECIMAL(38, 2)) AS [imprUsrCnt], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrGoodsQty]), 0)) AS DECIMAL(38, 2)) AS [payOrdrGoodsQty], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrCnt]), 0)) AS DECIMAL(38, 2)) AS [orderNum], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[payOrdrUsrCnt]), 0)) AS DECIMAL(38, 2)) AS [payOrdrUsrCnt], CAST(ROUND(SUM( ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[goodsUv]), 0) * ( ISNULL(TRY_CONVERT(DECIMAL(38, 6), REPLACE(TRY_CONVERT(NVARCHAR(50), g.[ordrVstrRto]), '%', '')), 0) / CASE WHEN CHARINDEX('%', TRY_CONVERT(NVARCHAR(50), g.[ordrVstrRto])) > 0 THEN 100.0 WHEN ISNULL(TRY_CONVERT(DECIMAL(38, 6), g.[ordrVstrRto]), 0) > 1 THEN 100.0 ELSE 1.0 END ) ), 0) AS BIGINT) AS [orderUserCount] INTO #goods_agg FROM #shop_scope ss INNER JOIN [${PDD_DATA_GOODS_SCHEMA}].[${PDD_DATA_GOODS_TABLE}] g ON LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), g.[shopName]))) = ss.[shopName] WHERE g.[createTime] >= @startDateTime AND g.[createTime] < @endDateExclusive GROUP BY ss.[shopName]; CREATE UNIQUE CLUSTERED INDEX IX_goods_agg_shopName ON #goods_agg([shopName]); SELECT ss.[shopName], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[spend]), 0)) / 1000.0 AS DECIMAL(38, 2)) AS [spend], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[gmv]), 0)) / 1000.0 AS DECIMAL(38, 2)) AS [spreadGmv], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[indirectGmv]), 0)) / 1000.0 AS DECIMAL(38, 2)) AS [indirectGmv], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[indirectOrderNum]), 0)) AS DECIMAL(38, 2)) AS [indirectOrderNum], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[click]), 0)) AS DECIMAL(38, 2)) AS [click], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[impression]), 0)) AS DECIMAL(38, 2)) AS [impression], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[orderNum]), 0)) AS DECIMAL(38, 2)) AS [spreadOrderNum], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[multiGoalInquiryNum]), 0)) AS DECIMAL(38, 2)) AS [inquiryNum], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[multiGoalGoodsFavNum]), 0)) AS DECIMAL(38, 2)) AS [goodsFavNum], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[orderSpend]), 0)) / 1000.0 AS DECIMAL(38, 2)) AS [orderSpend], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[inquirySpend]), 0)) / 1000.0 AS DECIMAL(38, 2)) AS [inquirySpend], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), s.[netGmv]), 0)) / 1000.0 AS DECIMAL(38, 2)) AS [netGmv] INTO #spread_agg FROM #shop_scope ss INNER JOIN [${PDD_DATA_GOODS_SCHEMA}].[${PDD_SPREAD_GOODS_TABLE}] s ON LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), s.[shopName]))) = ss.[shopName] WHERE s.[createTime] >= @startDateTime AND s.[createTime] < @endDateExclusive GROUP BY ss.[shopName]; CREATE UNIQUE CLUSTERED INDEX IX_spread_agg_shopName ON #spread_agg([shopName]); SELECT LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), a.[shop_name]))) AS [shopName], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), a.[real_performance]), 0)) AS DECIMAL(38, 2)) AS [realPerformance], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), a.[real_conclude_fee]), 0)) AS DECIMAL(38, 2)) AS [realConcludeFee] INTO #ads_agg FROM [JSDataWarehouse].[ads].[ads_itemid_sales_cost_ratio_v2_di] a ${needsTargetJoin ? buildPddTargetJoinSqlByExpr({ includeIdOwner: true, goodsIdExpr: 'LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), a.[item_id])))', }) : ''} WHERE LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), a.[shop_name]))) IN (SELECT [shopName] FROM #shop_scope) AND TRY_CONVERT(DATE, a.[stat_date]) BETWEEN @startDate AND @endDate AND ISNULL(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), a.[platform]))), N'') IN (N'Pinduoduo', N'拼多多') GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), a.[shop_name]))); CREATE UNIQUE CLUSTERED INDEX IX_ads_agg_shopName ON #ads_agg([shopName]); SELECT ss.[shopName], ta.[ownerDept], ISNULL(ga.[goodsUv], 0) AS [goodsUv], ISNULL(sa.[inquiryNum], 0) AS [inquiryNum], ISNULL(ga.[goodsFavCnt], 0) AS [goodsFavCnt], ISNULL(sa.[goodsFavNum], 0) AS [goodsFavNum], ISNULL(ga.[orderUserCount], 0) AS [orderUserCount], CAST(CASE WHEN ISNULL(sa.[impression], 0) = 0 THEN 0 ELSE ISNULL(sa.[click], 0) * 100.0 / NULLIF(sa.[impression], 0) END AS DECIMAL(38, 2)) AS [ctr], CAST(CASE WHEN ISNULL(sa.[click], 0) = 0 THEN 0 ELSE ISNULL(sa.[spreadOrderNum], 0) * 100.0 / NULLIF(sa.[click], 0) END AS DECIMAL(38, 2)) AS [clickConversionRate], CAST(CASE WHEN ISNULL(ga.[goodsUv], 0) = 0 THEN 0 ELSE ISNULL(ga.[payOrdrUsrCnt], 0) * 100.0 / NULLIF(ga.[goodsUv], 0) END AS DECIMAL(38, 2)) AS [goodsVcr], CAST(CASE WHEN ISNULL(ga.[goodsUv], 0) = 0 THEN 0 ELSE ISNULL(ga.[orderUserCount], 0) * 100.0 / NULLIF(ga.[goodsUv], 0) END AS DECIMAL(38, 2)) AS [ordrVstrRto], CAST(CASE WHEN ISNULL(ga.[orderUserCount], 0) = 0 THEN 0 ELSE ISNULL(ga.[payOrdrUsrCnt], 0) * 100.0 / NULLIF(ga.[orderUserCount], 0) END AS DECIMAL(38, 2)) AS [payOrdrRto], ISNULL(ga.[imprUsrCnt], 0) AS [imprUsrCnt], CAST(ROUND(ISNULL(ga.[orderUserCount], 0) - ISNULL(ga.[payOrdrUsrCnt], 0), 0) AS BIGINT) AS [unpaidUserCount], ISNULL(aa.[realPerformance], 0) AS [payOrdrAmt], ISNULL(ga.[payOrdrGoodsQty], 0) AS [payOrdrGoodsQty], ISNULL(ga.[orderNum], 0) AS [orderNum], ISNULL(ga.[payOrdrUsrCnt], 0) AS [payOrdrUsrCnt], CAST(CASE WHEN ISNULL(ga.[payOrdrUsrCnt], 0) = 0 THEN 0 ELSE ISNULL(aa.[realPerformance], 0) / NULLIF(ga.[payOrdrUsrCnt], 0) END AS DECIMAL(38, 2)) AS [customerUnitPrice], CAST(CASE WHEN ISNULL(ga.[payOrdrGoodsQty], 0) = 0 THEN 0 ELSE ISNULL(aa.[realPerformance], 0) / NULLIF(ga.[payOrdrGoodsQty], 0) END AS DECIMAL(38, 2)) AS [pieceUnitPrice], ISNULL(sa.[spreadGmv], 0) AS [spreadGmv], ISNULL(sa.[indirectGmv], 0) AS [indirectGmv], ISNULL(sa.[indirectOrderNum], 0) AS [indirectOrderNum], ISNULL(sa.[spend], 0) AS [spend], ISNULL(aa.[realConcludeFee], 0) AS [realConcludeFee], CAST(CASE WHEN ISNULL(aa.[realPerformance], 0) = 0 THEN 0 ELSE ISNULL(aa.[realConcludeFee], 0) * 100.0 / NULLIF(aa.[realPerformance], 0) END AS DECIMAL(38, 2)) AS [feeRatio], CAST(CASE WHEN ISNULL(sa.[spend], 0) = 0 THEN 0 ELSE ISNULL(sa.[spreadGmv], 0) / NULLIF(sa.[spend], 0) END AS DECIMAL(38, 2)) AS [overallRoi], CAST(CASE WHEN ISNULL(sa.[orderSpend], 0) = 0 THEN 0 ELSE ISNULL(sa.[netGmv], 0) / NULLIF(sa.[orderSpend], 0) END AS DECIMAL(38, 2)) AS [netRoi], CAST(CASE WHEN ISNULL(sa.[spreadOrderNum], 0) = 0 THEN 0 ELSE ISNULL(sa.[orderSpend], 0) / NULLIF(sa.[spreadOrderNum], 0) END AS DECIMAL(38, 2)) AS [avgDealCost], CAST(CASE WHEN ISNULL(sa.[inquiryNum], 0) = 0 THEN 0 ELSE ISNULL(sa.[inquirySpend], 0) / NULLIF(sa.[inquiryNum], 0) END AS DECIMAL(38, 2)) AS [avgInquiryCost] INTO #shop_list FROM #shop_scope ss LEFT JOIN #target_agg ta ON ta.[shopName] = ss.[shopName] LEFT JOIN #goods_agg ga ON ga.[shopName] = ss.[shopName] LEFT JOIN #spread_agg sa ON sa.[shopName] = ss.[shopName] LEFT JOIN #ads_agg aa ON aa.[shopName] = ss.[shopName]; CREATE UNIQUE CLUSTERED INDEX IX_shop_list_shopName ON #shop_list([shopName]); DECLARE @total BIGINT; SET @total = (SELECT COUNT(1) FROM #shop_list); SELECT * INTO #paged_shop FROM ( SELECT sl.*, ROW_NUMBER() OVER (ORDER BY ${orderExpr} ${orderDirection}${orderTieBreaker}) AS [rn] FROM #shop_list sl ) src WHERE src.[rn] > @offset AND src.[rn] <= (@offset + @pageSize); SELECT ps.*, CAST(@total AS BIGINT) AS [__total] FROM #paged_shop ps ORDER BY ps.[rn]; `; const result = await cloudDb.query(batchSql, { ...params, offset: (page - 1) * pageSize, pageSize, }, { requestTimeout: 120000, }); const total = Number(result?.[0]?.__total || 0); const normalizedRows = (result || []).map((row) => { const nextRow = { ...row }; delete nextRow.__total; delete nextRow.rn; return nextRow; }); return { total, rows: normalizedRows, }; }, }); res.json(payload); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); router.get('/pdd/product-dashboard/id-dimension-board/barcode-detail', authMiddleware, async (req, res) => { try { const goodsId = String(req.query.goodsId || '').trim(); if (!goodsId) { return res.status(400).json({ success: false, error: 'goodsId is required' }); } const { accessScope, startDate, endDate } = await getPddProductDashboardRequestContext(req, [ PDD_DATA_GOODS_TABLE, PDD_SPREAD_GOODS_TABLE, PDD_EVALUATION_TABLE, ]); if (!startDate || !endDate) { return res.json({ success: true, data: { goodsId, startDate, endDate, rows: [], }, }); } const params = { goodsId, startDate, endDate, }; if (!accessScope.isAllData) { if (accessScope.allowedDepartments.length === 0) { return res.json({ success: true, data: { goodsId, startDate, endDate, rows: [], }, }); } const permissionParams = { goodsId, }; const permissionWhereParts = [ 'src.[shopproid] = @goodsId', ]; appendNamedInCondition( permissionWhereParts, permissionParams, accessScope.allowedDepartments, 'src.[dept]', 'detailScopeDept' ); const permissionRows = await cloudDb.query( ` SELECT TOP (1) 1 AS [allowed] FROM ${PDD_TARGET_TABLE_FULL_NAME} src WHERE ${permissionWhereParts.join(' AND ')} `, permissionParams, { requestTimeout: 30000, } ); if (!Array.isArray(permissionRows) || permissionRows.length === 0) { return res.json({ success: true, data: { goodsId, startDate, endDate, rows: [], }, }); } } const cacheKey = getPddDashboardCacheKey('pdd:id-dimension:barcode-detail', { goodsId, startDate, endDate, accessScope: buildPddDashboardAccessScopeCacheFragment(accessScope), }); const rows = await getOrSetPddDashboardResultCache(cacheKey, async () => { return cloudDb.query( ` SELECT @goodsId AS [goodsId], LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), d.[skuoid]))) AS [barcode], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[sales_amount]), 0)) AS DECIMAL(38, 2)) AS [salesAmount], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[goods_count]), 0)) AS DECIMAL(38, 2)) AS [goodsCount], CAST(COUNT(DISTINCT CASE WHEN NULLIF(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), d.[sourceno]))), N'') IS NOT NULL THEN LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(100), d.[sourceno]))) ELSE NULL END) AS DECIMAL(38, 2)) AS [orderCount], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[real_conclude_fee]), 0)) AS DECIMAL(38, 2)) AS [realConcludeFee], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[real_performance]), 0)) AS DECIMAL(38, 2)) AS [realPerformance], CAST(CASE WHEN SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[real_performance]), 0)) = 0 THEN 0 ELSE SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[real_conclude_fee]), 0)) * 100.0 / NULLIF(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[real_performance]), 0)), 0) END AS DECIMAL(38, 2)) AS [feeRatio], CAST(SUM(ISNULL(TRY_CONVERT(DECIMAL(38, 6), d.[refund_count]), 0)) AS DECIMAL(38, 2)) AS [afterSaleCount] FROM [JSDataWarehouse].[dws].[dws_sn_sales_full_info_di] d WHERE d.[item_id] = CONVERT(VARCHAR(50), @goodsId) AND d.[stat_date] BETWEEN TRY_CONVERT(DATE, @startDate) AND TRY_CONVERT(DATE, @endDate) AND ISNULL(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(50), d.[platform]))), N'') IN (N'Pinduoduo', N'拼多多') AND NULLIF(LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), d.[skuoid]))), N'') IS NOT NULL GROUP BY LTRIM(RTRIM(TRY_CONVERT(NVARCHAR(255), d.[skuoid]))) ORDER BY [realPerformance] DESC, [goodsCount] DESC, [barcode] ASC; `, params, { requestTimeout: 120000, } ); }, 300000); res.json({ success: true, data: { goodsId, startDate, endDate, rows, }, }); } catch (error) { res.status(error.statusCode || 500).json({ success: false, error: error.message }); } }); registerPddSkuDimensionBoard({ router, authMiddleware, cloudDb, PDD_DATA_GOODS_SCHEMA, PDD_DATA_GOODS_TABLE, PDD_SPREAD_GOODS_TABLE, PDD_EVALUATION_TABLE, PDD_TARGET_TABLE_FULL_NAME, PDD_JST_SKU_TABLE_FULL_NAME, getPddProductDashboardRequestContext, buildPddProductDashboardEmptyResponse, getCachedPddProductDashboardPayload, getPddDashboardCacheKey, getOrSetPddDashboardResultCache, appendPddCreateTimeRangeFilters, appendPddProductDashboardCommonFilters, loadPddPriceBreakFlagByGoodsIds, }); router.prewarmHotAnalysisPages = prewarmHotAnalysisPages; router.get('/logistics/pickup-board', async (req, res) => { try { const { startDate, endDate, shops, warehouses, purchasers, suppliers, skuIds, } = req.query; const start = startDate || dayjs().startOf('month').format('YYYY-MM-DD'); const end = endDate || dayjs().format('YYYY-MM-DD'); const hasFilters = [shops, warehouses, purchasers, suppliers, skuIds].some((value) => value && String(value).trim() !== ''); const effectiveEnd = dayjs(end).isAfter(dayjs().subtract(1, 'day'), 'day') ? dayjs().subtract(1, 'day').format('YYYY-MM-DD') : end; if (!hasFilters) { const snapshot = readLogisticsPickupBoardSnapshot(start, end); if (snapshot) { return res.json({ success: true, data: normalizeSnapshotData(snapshot, effectiveEnd), cached: true, source: 'snapshot' }); } } const cached = await getCachedAnalysisPayload({ scope: 'logistics:pickup-board', payload: { startDate: start, endDate: end, shops, warehouses, purchasers, suppliers, skuIds }, label: 'analysis:logistics:pickup-board', loader: () => queryLogisticsPickupBoard({ startDate: start, endDate: end, shops, warehouses, purchasers, suppliers, skuIds, }), memoryTtlSeconds: 30, redisTtlSeconds: 120, }); res.json({ success: true, data: cached.value, cached: cached.hit !== 'miss' }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); router.get('/logistics/delivery-duration-board', async (req, res) => { try { const { startDate, endDate, warehouses, couriers, provinces, } = req.query; const start = startDate || dayjs().startOf('month').format('YYYY-MM-DD'); const end = endDate || dayjs().format('YYYY-MM-DD'); const cached = await getCachedAnalysisPayload({ scope: 'logistics:delivery-duration-board', payload: { startDate: start, endDate: end, warehouses, couriers, provinces }, label: 'analysis:logistics:delivery-duration-board', loader: () => queryLogisticsDeliveryDurationBoard({ startDate: start, endDate: end, warehouses, couriers, provinces, }), memoryTtlSeconds: 30, redisTtlSeconds: 120, }); res.json({ success: true, data: cached.value, cached: cached.hit !== 'miss' }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); router.get('/logistics/delivery-duration-board/details', async (req, res) => { try { const { startDate, endDate, warehouses, couriers, provinces, page, pageSize, } = req.query; const start = startDate || dayjs().startOf('month').format('YYYY-MM-DD'); const end = endDate || dayjs().format('YYYY-MM-DD'); const data = await queryLogisticsDeliveryDurationDetails({ startDate: start, endDate: end, warehouses, couriers, provinces, page, pageSize, }); res.json({ success: true, data }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); module.exports = router;