cangku/backend/src/api/analysis.js

7501 lines
272 KiB
JavaScript

/**
* 閮ㄩ棬鍒嗘瀽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;