|
|
|
@@ -129,7 +129,7 @@ router.get('/stores', async (req: AuthRequest, res) => {
|
|
|
|
|
router.get('/stores/risk', async (req: AuthRequest, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const month = parseMonth(req)
|
|
|
|
|
const result = await query(`SELECT * FROM analytics.mv_store_risk_rating_monthly WHERE month_start = $1 ORDER BY risk_level, received DESC`, [month])
|
|
|
|
|
const result = await query(`SELECT store_code, store_name, risk_level, risk_score, received, bill_count, avg_daily_received, avg_bill_value, discount_rate_pct, theoretical_margin_pct, member_bill_share_pct, anomaly_rate_pct, repeat_rate_pct, delivery_bill_share_pct, combo_rate_pct, avg_items_per_bill, primary_issue, month_start FROM analytics.mv_store_risk_rating_monthly WHERE month_start = $1 ORDER BY risk_level, received DESC`, [month])
|
|
|
|
|
sendSuccess(res, result.rows)
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
sendError(res, err.message)
|
|
|
|
@@ -173,7 +173,7 @@ router.get('/stores/:code', async (req: AuthRequest, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const code = req.params.code
|
|
|
|
|
const month = parseMonth(req)
|
|
|
|
|
const [scorecard, risk, platform, benchmark, action] = await Promise.all([
|
|
|
|
|
const [scorecard, risk, platform, benchmark, action, companyAvg] = await Promise.all([
|
|
|
|
|
query(`
|
|
|
|
|
SELECT c002 AS store_code, c003 AS store_name,
|
|
|
|
|
count(*) AS bill_count,
|
|
|
|
@@ -198,6 +198,20 @@ router.get('/stores/:code', async (req: AuthRequest, res) => {
|
|
|
|
|
query(`SELECT * FROM analytics.mv_store_platform_economics_monthly WHERE month_start = $2 AND store_code = $1`, [code, month]),
|
|
|
|
|
query(`SELECT * FROM analytics.mv_store_benchmark_composite_monthly WHERE month_start = $2 AND store_code = $1`, [code, month]),
|
|
|
|
|
query(`SELECT * FROM analytics.mv_store_action_priority_deep_monthly WHERE month_start = $1 AND store_code = $2`, [month, code]),
|
|
|
|
|
query(`
|
|
|
|
|
SELECT
|
|
|
|
|
round(avg(avg_daily_received)::numeric, 0) AS daily_rev,
|
|
|
|
|
round(avg(avg_bill_value)::numeric, 1) AS bill,
|
|
|
|
|
round(avg(discount_rate_pct)::numeric, 1) AS discount,
|
|
|
|
|
round(avg(theoretical_margin_pct)::numeric, 1) AS margin,
|
|
|
|
|
round(avg(member_bill_share_pct)::numeric, 1) AS member,
|
|
|
|
|
round(avg(COALESCE(repeat_rate_pct, 0))::numeric, 1) AS repeat,
|
|
|
|
|
round(avg(COALESCE(delivery_bill_share_pct, 0))::numeric, 1) AS delivery,
|
|
|
|
|
round(avg(COALESCE(combo_rate_pct, 0))::numeric, 1) AS combo,
|
|
|
|
|
round(avg(COALESCE(avg_items_per_bill, 0))::numeric, 2) AS items
|
|
|
|
|
FROM analytics.mv_store_risk_rating_monthly
|
|
|
|
|
WHERE month_start = $1
|
|
|
|
|
`, [month]),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
if (scorecard.rows.length === 0) {
|
|
|
|
@@ -212,6 +226,7 @@ router.get('/stores/:code', async (req: AuthRequest, res) => {
|
|
|
|
|
platform: null,
|
|
|
|
|
benchmark: null,
|
|
|
|
|
action: { management_quadrant: '问题门店', risk_level: null },
|
|
|
|
|
companyAvg: companyAvg.rows[0] || null,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -235,6 +250,7 @@ router.get('/stores/:code', async (req: AuthRequest, res) => {
|
|
|
|
|
platform: platform.rows[0],
|
|
|
|
|
benchmark: benchmark.rows[0],
|
|
|
|
|
action: { ...act, management_quadrant: quadrant, risk_level: rk?.risk_level },
|
|
|
|
|
companyAvg: companyAvg.rows[0] || null,
|
|
|
|
|
})
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
sendError(res, err.message)
|
|
|
|
@@ -337,7 +353,7 @@ router.get('/cost/inventory', async (req: AuthRequest, res) => {
|
|
|
|
|
router.get('/platform/economics', async (req: AuthRequest, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const month = parseMonth(req)
|
|
|
|
|
const result = await query(`SELECT * FROM analytics.mv_store_platform_economics_monthly WHERE month_start = $1 ORDER BY meituan_received DESC NULLS LAST`, [month])
|
|
|
|
|
const result = await query(`SELECT store_code, store_name, month_start, platform_share, meituan_received, meituan_cost_rate_pct, eleme_received, eleme_cost_rate_pct, douyin_received, douyin_cost_rate_pct, self_delivery_received, dine_in_received, dine_in_share_pct, delivery_bill_share_pct FROM analytics.mv_store_platform_economics_monthly WHERE month_start = $1 ORDER BY meituan_received DESC NULLS LAST LIMIT 200`, [month])
|
|
|
|
|
sendSuccess(res, result.rows)
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
sendError(res, err.message)
|
|
|
|
@@ -484,12 +500,16 @@ router.get('/risk/zero-received', async (req: AuthRequest, res) => {
|
|
|
|
|
const baseWhere = baseConditions.join(' AND ')
|
|
|
|
|
const countResult = await query(`SELECT count(*) FILTER (WHERE zero_received_type = '全额优惠') AS full_discount, count(*) FILTER (WHERE zero_received_type = '无消费无优惠') AS empty_bill FROM mv_risk_zero WHERE ${baseWhere}`, baseParams)
|
|
|
|
|
const totalResult = await query(`SELECT count(*) AS total FROM mv_risk_zero WHERE ${whereClause}`, params)
|
|
|
|
|
const sort = (req.query.sort as string) || 'consumption'
|
|
|
|
|
const order = (req.query.order as string) === 'asc' ? 'ASC' : 'DESC'
|
|
|
|
|
const validSorts = ['consumption', 'discount_total', 'store_name']
|
|
|
|
|
const sortCol = validSorts.includes(sort) ? sort : 'consumption'
|
|
|
|
|
const result = await query(`
|
|
|
|
|
SELECT store_name, bill_no, meal_period, consumption, discount_total,
|
|
|
|
|
0 AS received_total, cashier, closed_at, zero_received_type
|
|
|
|
|
FROM mv_risk_zero
|
|
|
|
|
WHERE ${whereClause}
|
|
|
|
|
ORDER BY consumption DESC
|
|
|
|
|
ORDER BY ${sortCol} ${order}
|
|
|
|
|
LIMIT $${paramIdx} OFFSET $${paramIdx + 1}
|
|
|
|
|
`, [...params, pageSize, offset])
|
|
|
|
|
sendSuccess(res, result.rows, { total: parseInt(totalResult.rows[0].total), full_discount: parseInt(countResult.rows[0].full_discount), empty_bill: parseInt(countResult.rows[0].empty_bill), page, page_size: pageSize })
|
|
|
|
@@ -501,15 +521,20 @@ router.get('/risk/zero-received', async (req: AuthRequest, res) => {
|
|
|
|
|
router.get('/risk/cashier', async (req: AuthRequest, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const month = parseMonth(req)
|
|
|
|
|
const sort = (req.query.sort as string) || 'anomaly_bills'
|
|
|
|
|
const order = (req.query.order as string) === 'asc' ? 'ASC' : 'DESC'
|
|
|
|
|
const validSorts = ['anomaly_rate_pct', 'bill_count', 'received', 'anomaly_bills', 'consumption_total']
|
|
|
|
|
const sortCol = validSorts.includes(sort) ? sort : 'anomaly_bills'
|
|
|
|
|
const result = await query(`
|
|
|
|
|
SELECT store_name, cashier, bill_count,
|
|
|
|
|
received,
|
|
|
|
|
(anomaly_no_received + anomaly_discount_over + anomaly_unbalanced) AS anomaly_bills,
|
|
|
|
|
round((anomaly_no_received + anomaly_discount_over + anomaly_unbalanced)::numeric / bill_count * 100, 2) AS anomaly_rate_pct,
|
|
|
|
|
consumption_total
|
|
|
|
|
consumption_total,
|
|
|
|
|
consumption_total AS anomaly_consumption
|
|
|
|
|
FROM mv_risk_cashier
|
|
|
|
|
WHERE month = to_char($1::date, 'YYYY-MM') AND cashier IS NOT NULL AND cashier != ''
|
|
|
|
|
ORDER BY (anomaly_no_received + anomaly_discount_over + anomaly_unbalanced) DESC NULLS LAST
|
|
|
|
|
ORDER BY ${sortCol} ${order} NULLS LAST
|
|
|
|
|
`, [month])
|
|
|
|
|
sendSuccess(res, result.rows)
|
|
|
|
|
} catch (err: any) {
|
|
|
|
@@ -586,7 +611,7 @@ router.get('/channel', async (req: AuthRequest, res) => {
|
|
|
|
|
const month = parseMonth(req)
|
|
|
|
|
const result = await query(`
|
|
|
|
|
SELECT business_date, cash, alipay, wechat, meituan, unionpay, douyin, credit, jd_delivery, meituan_delivery, taobao_delivery
|
|
|
|
|
FROM mv_channel_daily
|
|
|
|
|
FROM analytics.mv_channel_daily
|
|
|
|
|
WHERE month = to_char($1::date, 'YYYY-MM')
|
|
|
|
|
ORDER BY business_date
|
|
|
|
|
`, [month])
|
|
|
|
@@ -598,6 +623,7 @@ router.get('/channel', async (req: AuthRequest, res) => {
|
|
|
|
|
|
|
|
|
|
router.get('/data-quality', async (req: AuthRequest, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const month = parseMonth(req)
|
|
|
|
|
const billStats = await query(`
|
|
|
|
|
SELECT
|
|
|
|
|
count(*) AS total_bills,
|
|
|
|
@@ -611,15 +637,16 @@ router.get('/data-quality', async (req: AuthRequest, res) => {
|
|
|
|
|
min(closed_at)::text AS min_date,
|
|
|
|
|
max(closed_at)::text AS max_date
|
|
|
|
|
FROM analytics.bill_fact
|
|
|
|
|
`)
|
|
|
|
|
WHERE closed_at >= $1::date AND closed_at < ($1::date + INTERVAL '1 month')
|
|
|
|
|
`, [month])
|
|
|
|
|
const dishStats = await query(`
|
|
|
|
|
SELECT
|
|
|
|
|
count(*) AS total_dish_records,
|
|
|
|
|
count(*) FILTER (WHERE store_code IS NULL OR store_code = '') AS dish_missing_store,
|
|
|
|
|
count(*) FILTER (WHERE dish_name IS NULL OR dish_name = '') AS dish_missing_dish
|
|
|
|
|
FROM public.dish_sales_details
|
|
|
|
|
`)
|
|
|
|
|
const month = parseMonth(req)
|
|
|
|
|
WHERE business_date >= $1::date AND business_date < ($1::date + INTERVAL '1 month')
|
|
|
|
|
`, [month])
|
|
|
|
|
const inventoryStats = await query(`
|
|
|
|
|
SELECT
|
|
|
|
|
count(*) FILTER (WHERE consumption_amount < 0) AS negative_consumption_count,
|
|
|
|
@@ -972,7 +999,7 @@ router.get('/region/summary', async (req: AuthRequest, res) => {
|
|
|
|
|
round(avg(b.received_total), 2) AS avg_bill_value,
|
|
|
|
|
round(sum(b.discount_total) / NULLIF(sum(b.consumption), 0) * 100, 2) AS avg_discount_rate,
|
|
|
|
|
round(sum(b.theoretical_profit) / NULLIF(sum(b.received_total), 0) * 100, 2) AS avg_margin_rate,
|
|
|
|
|
0 AS avg_anomaly_rate,
|
|
|
|
|
round(avg(COALESCE(r.anomaly_rate_pct, 0)), 2) AS avg_anomaly_rate,
|
|
|
|
|
count(DISTINCT CASE WHEN r.risk_level = '红色' THEN b.store_code END) AS red_count,
|
|
|
|
|
count(DISTINCT CASE WHEN r.risk_level = '黄色' THEN b.store_code END) AS yellow_count,
|
|
|
|
|
count(DISTINCT CASE WHEN r.risk_level = '绿色' THEN b.store_code END) AS green_count
|
|
|
|
@@ -992,7 +1019,7 @@ router.get('/region/summary', async (req: AuthRequest, res) => {
|
|
|
|
|
router.get('/site-selection/profile', async (req: AuthRequest, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const month = parseMonth(req)
|
|
|
|
|
const result = await query(`SELECT * FROM analytics.mv_store_site_profile_monthly WHERE month_start = $1 AND business_type='标准门店' AND received>0 ORDER BY received DESC`, [month])
|
|
|
|
|
const result = await query(`SELECT store_code, store_name, received, avg_daily_received, avg_bill_value, discount_rate_pct, theoretical_margin_pct, member_bill_share_pct, delivery_bill_share_pct, combo_bill_share_pct, business_type, scale_tier, area_sqm, business_address, city, district, latitude_gcj02, longitude_gcj02, open_date, lease_expiry_date, store_age_years, monthly_received_per_sqm, daily_received_per_sqm, estimated_inventory_days, site_scene, floor_type, area_band, age_band FROM analytics.mv_store_site_profile_monthly WHERE month_start = $1 AND business_type='标准门店' AND received>0 ORDER BY received DESC`, [month])
|
|
|
|
|
sendSuccess(res, result.rows)
|
|
|
|
|
} catch (err: any) { sendError(res, err.message) }
|
|
|
|
|
})
|
|
|
|
@@ -1001,7 +1028,7 @@ router.get('/site-selection/profile', async (req: AuthRequest, res) => {
|
|
|
|
|
router.get('/site-selection/segment-benchmark', async (req: AuthRequest, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const month = parseMonth(req)
|
|
|
|
|
const result = await query(`SELECT * FROM analytics.mv_site_segment_benchmark_monthly WHERE month_start = $1 ORDER BY avg_received_per_sqm DESC`, [month])
|
|
|
|
|
const result = await query(`SELECT site_scene, area_band, store_count, avg_area_sqm, avg_received, median_received, avg_received_per_sqm, median_received_per_sqm, avg_bill_value, avg_discount_rate_pct, avg_repeat_rate_pct, avg_actual_cost_rate_pct, avg_delivery_share_pct, avg_drink_attach_pct FROM analytics.mv_site_segment_benchmark_monthly WHERE month_start = $1 ORDER BY avg_received_per_sqm DESC`, [month])
|
|
|
|
|
sendSuccess(res, result.rows)
|
|
|
|
|
} catch (err: any) { sendError(res, err.message) }
|
|
|
|
|
})
|
|
|
|
@@ -1010,7 +1037,7 @@ router.get('/site-selection/segment-benchmark', async (req: AuthRequest, res) =>
|
|
|
|
|
router.get('/site-selection/replication', async (req: AuthRequest, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const month = parseMonth(req)
|
|
|
|
|
const result = await query(`SELECT * FROM analytics.mv_store_site_replication_monthly WHERE month_start = $1 ORDER BY site_replication_score DESC`, [month])
|
|
|
|
|
const result = await query(`SELECT store_code, store_name, received, avg_bill_value, discount_rate_pct, theoretical_margin_pct, member_bill_share_pct, repeat_rate_pct, benchmark_score, area_sqm, business_address, city, district, latitude_gcj02, longitude_gcj02, site_scene, area_band, nearest_store_code, nearest_store_name, nearest_distance_km, site_replication_score, replication_recommendation, spatial_recommendation FROM analytics.mv_store_site_replication_monthly WHERE month_start = $1 ORDER BY site_replication_score DESC`, [month])
|
|
|
|
|
sendSuccess(res, result.rows)
|
|
|
|
|
} catch (err: any) { sendError(res, err.message) }
|
|
|
|
|
})
|
|
|
|
@@ -1019,7 +1046,7 @@ router.get('/site-selection/replication', async (req: AuthRequest, res) => {
|
|
|
|
|
router.get('/site-selection/overlap-risk', async (req: AuthRequest, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const month = parseMonth(req)
|
|
|
|
|
const result = await query(`SELECT * FROM analytics.mv_store_overlap_risk_monthly WHERE month_start = $1 ORDER BY distance_km`, [month])
|
|
|
|
|
const result = await query(`SELECT store_code_a, store_name_a, store_code_b, store_name_b, district_a, district_b, scene_a, scene_b, priority_a, priority_b, received_a, received_b, sqm_efficiency_a, sqm_efficiency_b, distance_km, proximity_level, problem_count_a, problem_count_b, overlap_risk FROM analytics.mv_store_overlap_risk_monthly WHERE month_start = $1 ORDER BY distance_km LIMIT 200`, [month])
|
|
|
|
|
sendSuccess(res, result.rows)
|
|
|
|
|
} catch (err: any) { sendError(res, err.message) }
|
|
|
|
|
})
|
|
|
|
@@ -1028,7 +1055,7 @@ router.get('/site-selection/overlap-risk', async (req: AuthRequest, res) => {
|
|
|
|
|
router.get('/site-selection/district-benchmark', async (req: AuthRequest, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const month = parseMonth(req)
|
|
|
|
|
const result = await query(`SELECT * FROM analytics.mv_district_site_benchmark_monthly WHERE month_start = $1 ORDER BY total_received DESC`, [month])
|
|
|
|
|
const result = await query(`SELECT city, district, store_count, avg_area_sqm, total_received, avg_received, median_received, avg_received_per_sqm, avg_bill_value, avg_discount_rate_pct, avg_repeat_rate_pct, avg_actual_cost_rate_pct, avg_platform_cost_rate_pct, p0_count, p1_count FROM analytics.mv_district_site_benchmark_monthly WHERE month_start = $1 ORDER BY total_received DESC`, [month])
|
|
|
|
|
sendSuccess(res, result.rows)
|
|
|
|
|
} catch (err: any) { sendError(res, err.message) }
|
|
|
|
|
})
|
|
|
|
@@ -1543,6 +1570,7 @@ router.get('/revenue/store-ranking', async (req: AuthRequest, res) => {
|
|
|
|
|
WHERE business_date >= $1::date AND business_date < ($1::date + INTERVAL '1 month')
|
|
|
|
|
GROUP BY store_code, store_name
|
|
|
|
|
ORDER BY sum(received) DESC
|
|
|
|
|
LIMIT 200
|
|
|
|
|
`, [month])
|
|
|
|
|
sendSuccess(res, result.rows)
|
|
|
|
|
} catch (err: any) {
|
|
|
|
@@ -1710,7 +1738,7 @@ router.get('/central-kitchen/dashboard', async (req: AuthRequest, res) => {
|
|
|
|
|
round(sum(c.inbound_quantity * c.inbound_avg_unit_price)::numeric,2) AS calculated_inbound_value,
|
|
|
|
|
round((sum(c.inbound_quantity * c.inbound_avg_unit_price) - sum(p.full_manufacturing_cost))::numeric,2) AS manufacturing_margin
|
|
|
|
|
FROM analytics.v_central_kitchen_product_full_cost p
|
|
|
|
|
JOIN central_kitchen_processing_cost c ON p.report_month = c.report_month AND p.product_code = c.product_code
|
|
|
|
|
LEFT JOIN central_kitchen_processing_cost c ON p.report_month = c.report_month AND p.product_code = c.product_code
|
|
|
|
|
WHERE p.report_month = $1::date
|
|
|
|
|
`, [month]),
|
|
|
|
|
// 产品明细列表
|
|
|
|
@@ -1734,7 +1762,7 @@ router.get('/central-kitchen/dashboard', async (req: AuthRequest, res) => {
|
|
|
|
|
round((c.inbound_quantity * c.inbound_avg_unit_price)::numeric,2) AS inbound_value,
|
|
|
|
|
round((c.inbound_quantity * c.inbound_avg_unit_price - p.full_manufacturing_cost)::numeric,2) AS product_margin
|
|
|
|
|
FROM analytics.v_central_kitchen_product_full_cost p
|
|
|
|
|
JOIN central_kitchen_processing_cost c ON p.report_month = c.report_month AND p.product_code = c.product_code
|
|
|
|
|
LEFT JOIN central_kitchen_processing_cost c ON p.report_month = c.report_month AND p.product_code = c.product_code
|
|
|
|
|
WHERE p.report_month = $1::date
|
|
|
|
|
ORDER BY p.full_manufacturing_cost DESC
|
|
|
|
|
`, [month]),
|
|
|
|
@@ -1925,7 +1953,7 @@ router.get('/distribution/reconciliation', async (req: AuthRequest, res) => {
|
|
|
|
|
// 检查配送物化视图是否包含当前月份数据
|
|
|
|
|
const distMvCheck = await query(`SELECT 1 FROM mv_distribution_monthly WHERE report_month = $1::date LIMIT 1`, [month])
|
|
|
|
|
if (distMvCheck.rows.length === 0) {
|
|
|
|
|
await query(`REFRESH MATERIALIZED VIEW CONCURRENTLY mv_distribution_monthly`)
|
|
|
|
|
console.warn(`[distribution/reconciliation] mv_distribution_monthly 缺少 ${month} 数据,请通过ETL流程刷新物化视图`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [summary, storeReconciliation, topVariances, unmatchedItems, categoryReconciliation] = await Promise.all([
|
|
|
|
@@ -2089,9 +2117,10 @@ router.get('/distribution/reconciliation', async (req: AuthRequest, res) => {
|
|
|
|
|
GROUP BY COALESCE(NULLIF(d.minor_category,''),'未分类')
|
|
|
|
|
),
|
|
|
|
|
item_cat AS (
|
|
|
|
|
SELECT DISTINCT item_code, COALESCE(NULLIF(minor_category,''),'未分类') AS minor_category
|
|
|
|
|
FROM mv_distribution_monthly
|
|
|
|
|
WHERE report_month = $1::date
|
|
|
|
|
SELECT DISTINCT ic.item_code, COALESCE(NULLIF(dm.minor_category,''), NULLIF(ic.minor_category,''),'未分类') AS minor_category
|
|
|
|
|
FROM mv_distribution_monthly ic
|
|
|
|
|
LEFT JOIN analytics.dim_material dm ON ic.item_code = dm.material_code
|
|
|
|
|
WHERE ic.report_month = $1::date
|
|
|
|
|
),
|
|
|
|
|
inv AS (
|
|
|
|
|
SELECT ic.minor_category,
|
|
|
|
@@ -2396,13 +2425,12 @@ router.get('/sales-driven/production-plan', async (req: AuthRequest, res) => {
|
|
|
|
|
const client = await pool.connect()
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 检查物化视图是否包含当前月份数据,如缺失则刷新
|
|
|
|
|
// 检查物化视图是否包含当前月份数据
|
|
|
|
|
const mvCheck = await client.query(`
|
|
|
|
|
SELECT 1 FROM mv_dish_sales_monthly WHERE month = $1::date LIMIT 1
|
|
|
|
|
`, [month])
|
|
|
|
|
if (mvCheck.rows.length === 0) {
|
|
|
|
|
// 物化视图中没有该月数据,刷新物化视图
|
|
|
|
|
await client.query(`REFRESH MATERIALIZED VIEW CONCURRENTLY mv_dish_sales_monthly`)
|
|
|
|
|
console.warn(`[production-plan] mv_dish_sales_monthly 缺少 ${month} 数据,请通过ETL流程刷新物化视图`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查配送物化视图是否包含当前月份数据
|
|
|
|
@@ -2410,7 +2438,7 @@ router.get('/sales-driven/production-plan', async (req: AuthRequest, res) => {
|
|
|
|
|
SELECT 1 FROM mv_distribution_monthly WHERE report_month = $1::date LIMIT 1
|
|
|
|
|
`, [month])
|
|
|
|
|
if (distMvCheck.rows.length === 0) {
|
|
|
|
|
await client.query(`REFRESH MATERIALIZED VIEW CONCURRENTLY mv_distribution_monthly`)
|
|
|
|
|
console.warn(`[production-plan] mv_distribution_monthly 缺少 ${month} 数据,请通过ETL流程刷新物化视图`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 从物化视图创建临时表(索引扫描,毫秒级)
|
|
|
|
@@ -2519,7 +2547,8 @@ router.get('/sales-driven/production-plan', async (req: AuthRequest, res) => {
|
|
|
|
|
count(DISTINCT s.sku_code) AS sku_count,
|
|
|
|
|
round(sum(s.qty)::numeric,2) AS total_qty,
|
|
|
|
|
round(sum(s.amt)::numeric,2) AS total_amt,
|
|
|
|
|
round(sum(s.qty * b.standard_gross_quantity)::numeric,4) AS total_material_demand_qty
|
|
|
|
|
round(sum(s.qty * b.standard_gross_quantity)::numeric,4) AS total_material_demand_qty,
|
|
|
|
|
count(DISTINCT b.unit) AS material_unit_count
|
|
|
|
|
FROM tmp_sales_agg s
|
|
|
|
|
LEFT JOIN analytics.fact_recipe_bom b ON s.sku_code = b.sku_code
|
|
|
|
|
GROUP BY s.store_code, s.store_name
|
|
|
|
@@ -2531,7 +2560,8 @@ router.get('/sales-driven/production-plan', async (req: AuthRequest, res) => {
|
|
|
|
|
round(sd.total_material_demand_qty::numeric,4) AS total_material_demand_qty,
|
|
|
|
|
COALESCE(inv.ending_qty, 0) AS ending_inventory_qty,
|
|
|
|
|
COALESCE(inv.ending_amt, 0) AS ending_inventory_amt,
|
|
|
|
|
round((sd.total_material_demand_qty - COALESCE(inv.ending_qty, 0))::numeric,4) AS suggested_order_qty
|
|
|
|
|
round((sd.total_material_demand_qty - COALESCE(inv.ending_qty, 0))::numeric,4) AS suggested_order_qty,
|
|
|
|
|
sd.material_unit_count
|
|
|
|
|
FROM store_demand sd
|
|
|
|
|
LEFT JOIN (
|
|
|
|
|
SELECT store_code,
|
|
|
|
@@ -2555,8 +2585,8 @@ router.get('/sales-driven/production-plan', async (req: AuthRequest, res) => {
|
|
|
|
|
FROM tmp_sales_agg s
|
|
|
|
|
JOIN analytics.fact_recipe_bom b ON s.sku_code = b.sku_code
|
|
|
|
|
JOIN analytics.dim_material m ON b.material_code = m.material_code
|
|
|
|
|
WHERE m.material_name IN (
|
|
|
|
|
SELECT product_name FROM central_kitchen_processing_cost WHERE report_month = $1::date
|
|
|
|
|
WHERE b.material_code IN (
|
|
|
|
|
SELECT product_code FROM central_kitchen_processing_cost WHERE report_month = $1::date
|
|
|
|
|
)
|
|
|
|
|
GROUP BY b.material_code, m.material_name, b.unit
|
|
|
|
|
),
|
|
|
|
@@ -2577,14 +2607,14 @@ router.get('/sales-driven/production-plan', async (req: AuthRequest, res) => {
|
|
|
|
|
COALESCE(a.actual_inbound_qty, 0) AS actual_inbound_qty,
|
|
|
|
|
COALESCE(a.actual_cost, 0) AS actual_cost
|
|
|
|
|
FROM ck_demand d
|
|
|
|
|
LEFT JOIN ck_actual a ON d.ck_product_name = a.product_name
|
|
|
|
|
LEFT JOIN ck_actual a ON d.ck_product_code = a.product_code
|
|
|
|
|
|
|
|
|
|
UNION ALL
|
|
|
|
|
|
|
|
|
|
SELECT a.product_code, a.product_name, '',
|
|
|
|
|
0, 0, a.actual_inbound_qty, a.actual_cost
|
|
|
|
|
FROM ck_actual a
|
|
|
|
|
WHERE a.product_name NOT IN (SELECT ck_product_name FROM ck_demand)
|
|
|
|
|
WHERE a.product_code NOT IN (SELECT ck_product_code FROM ck_demand)
|
|
|
|
|
)
|
|
|
|
|
SELECT product_code, product_name, unit,
|
|
|
|
|
demand_qty, store_count, actual_inbound_qty, actual_cost,
|
|
|
|
|