feat: 新增门店选址分析功能
- 创建选址分析7个SQL视图+5张物化表(38s→33ms) - 后端新增5个选址API端点 - 前端新增SiteSelectionPage页面(散点图+4Tab) - 侧边栏新增门店选址导航入口 - 修复SQL列引用错误(d.→a.,去掉重复列) - 创建v_store_action_priority_deep_april和v_store_area_efficiency_april基础视图
This commit is contained in:
+77
-29
@@ -134,12 +134,14 @@ router.get('/stores/quadrant', async (req: AuthRequest, res) => {
|
||||
router.get('/stores/:code', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const code = req.params.code
|
||||
const scorecard = await query(`SELECT * FROM analytics.v_store_scorecard WHERE store_code = $1`, [code])
|
||||
const risk = await query(`SELECT * FROM analytics.v_store_risk_rating WHERE store_code = $1`, [code])
|
||||
const platform = await query(`SELECT * FROM analytics.v_store_platform_economics WHERE store_code = $1`, [code])
|
||||
const benchmark = await query(`SELECT * FROM analytics.v_store_benchmark WHERE store_code = $1`, [code])
|
||||
const action = await query(`SELECT * FROM analytics.v_store_action_list WHERE store_code = $1`, [code])
|
||||
const execution = await query(`SELECT * FROM analytics.v_store_execution_priority WHERE store_code = $1`, [code])
|
||||
const [scorecard, risk, platform, benchmark, action, execution] = await Promise.all([
|
||||
query(`SELECT * FROM analytics.v_store_scorecard WHERE store_code = $1`, [code]),
|
||||
query(`SELECT * FROM analytics.v_store_risk_rating WHERE store_code = $1`, [code]),
|
||||
query(`SELECT * FROM analytics.v_store_platform_economics WHERE store_code = $1`, [code]),
|
||||
query(`SELECT * FROM analytics.v_store_benchmark WHERE store_code = $1`, [code]),
|
||||
query(`SELECT * FROM analytics.v_store_action_list WHERE store_code = $1`, [code]),
|
||||
query(`SELECT * FROM analytics.v_store_execution_priority WHERE store_code = $1`, [code]),
|
||||
])
|
||||
|
||||
if (scorecard.rows.length === 0) {
|
||||
return sendError(res, 'Store not found', 404)
|
||||
@@ -386,41 +388,47 @@ router.get('/stores/:code/meal-period', async (req: AuthRequest, res) => {
|
||||
|
||||
router.get('/stores/:code/category-mix', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const result = await query(`SELECT * FROM analytics.v_store_category_mix WHERE store_code = $1`, [req.params.code])
|
||||
const companyResult = await query(`
|
||||
SELECT
|
||||
SUM(consumption) as total_consumption,
|
||||
SUM(lanzhou_noodle) as total_noodle,
|
||||
SUM(western_staple) as total_western,
|
||||
SUM(delivery_package) as total_delivery,
|
||||
SUM(cold_dishes) as total_cold,
|
||||
SUM(silk_road_food) as total_silk
|
||||
FROM analytics.v_store_category_mix
|
||||
`)
|
||||
const [result, companyResult] = await Promise.all([
|
||||
query(`SELECT * FROM analytics.v_store_category_mix WHERE store_code = $1`, [req.params.code]),
|
||||
query(`
|
||||
SELECT
|
||||
SUM(consumption) as total_consumption,
|
||||
SUM(lanzhou_noodle) as total_noodle,
|
||||
SUM(western_staple) as total_western,
|
||||
SUM(delivery_package) as total_delivery,
|
||||
SUM(cold_dishes) as total_cold,
|
||||
SUM(silk_road_food) as total_silk
|
||||
FROM analytics.v_store_category_mix
|
||||
`),
|
||||
])
|
||||
sendSuccess(res, { store: result.rows[0], company: companyResult.rows[0] })
|
||||
} catch (err: any) { sendError(res, err.message) }
|
||||
})
|
||||
|
||||
router.get('/stores/:code/cost', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const costResult = await query(`SELECT * FROM analytics.v_store_theoretical_actual_cost_april WHERE store_code = $1`, [req.params.code])
|
||||
const catResult = await query(`SELECT * FROM analytics.v_store_category_cost_benchmark_april WHERE store_code = $1`, [req.params.code])
|
||||
const [costResult, catResult] = await Promise.all([
|
||||
query(`SELECT * FROM analytics.v_store_theoretical_actual_cost_april WHERE store_code = $1`, [req.params.code]),
|
||||
query(`SELECT * FROM analytics.v_store_category_cost_benchmark_april WHERE store_code = $1`, [req.params.code]),
|
||||
])
|
||||
sendSuccess(res, { cost: costResult.rows[0], categories: catResult.rows })
|
||||
} catch (err: any) { sendError(res, err.message) }
|
||||
})
|
||||
|
||||
router.get('/stores/:code/member', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const oppResult = await query(`SELECT * FROM analytics.v_store_member_opportunity WHERE store_code = $1`, [req.params.code])
|
||||
const repeatResult = await query(`SELECT * FROM analytics.v_store_repeat_summary_monthly WHERE store_code = $1`, [req.params.code])
|
||||
const monthlyResult = await query(`
|
||||
SELECT store_code, store_name, count(*) as member_count,
|
||||
sum(orders) as total_orders, sum(received) as total_received,
|
||||
avg(orders) as avg_orders, avg(received) as avg_received
|
||||
FROM analytics.v_store_member_monthly_activity
|
||||
WHERE store_code = $1
|
||||
GROUP BY store_code, store_name
|
||||
`, [req.params.code])
|
||||
const [oppResult, repeatResult, monthlyResult] = await Promise.all([
|
||||
query(`SELECT * FROM analytics.v_store_member_opportunity WHERE store_code = $1`, [req.params.code]),
|
||||
query(`SELECT * FROM analytics.v_store_repeat_summary_monthly WHERE store_code = $1`, [req.params.code]),
|
||||
query(`
|
||||
SELECT store_code, store_name, count(*) as member_count,
|
||||
sum(orders) as total_orders, sum(received) as total_received,
|
||||
avg(orders) as avg_orders, avg(received) as avg_received
|
||||
FROM analytics.v_store_member_monthly_activity
|
||||
WHERE store_code = $1
|
||||
GROUP BY store_code, store_name
|
||||
`, [req.params.code]),
|
||||
])
|
||||
sendSuccess(res, { opportunity: oppResult.rows[0], repeat: repeatResult.rows[0], monthly: monthlyResult.rows[0] })
|
||||
} catch (err: any) { sendError(res, err.message) }
|
||||
})
|
||||
@@ -442,4 +450,44 @@ router.get('/region/summary', async (req, res) => {
|
||||
} catch (err: any) { sendError(res, err.message) }
|
||||
})
|
||||
|
||||
// 门店选址分析 — 门店选址画像
|
||||
router.get('/site-selection/profile', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const result = await query(`SELECT * FROM analytics.mv_store_site_profile_april WHERE business_type='标准门店' AND received>0 ORDER BY received DESC`)
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) { sendError(res, err.message) }
|
||||
})
|
||||
|
||||
// 门店选址分析 — 分段基准(场景×面积)
|
||||
router.get('/site-selection/segment-benchmark', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const result = await query(`SELECT * FROM analytics.mv_site_segment_benchmark_april ORDER BY avg_received_per_sqm DESC`)
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) { sendError(res, err.message) }
|
||||
})
|
||||
|
||||
// 门店选址分析 — 复制评分
|
||||
router.get('/site-selection/replication', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const result = await query(`SELECT * FROM analytics.mv_store_site_replication_score_april ORDER BY site_replication_score DESC`)
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) { sendError(res, err.message) }
|
||||
})
|
||||
|
||||
// 门店选址分析 — 重叠风险
|
||||
router.get('/site-selection/overlap-risk', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const result = await query(`SELECT * FROM analytics.mv_store_location_overlap_risk_april ORDER BY distance_km`)
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) { sendError(res, err.message) }
|
||||
})
|
||||
|
||||
// 门店选址分析 — 区域基准
|
||||
router.get('/site-selection/district-benchmark', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const result = await query(`SELECT * FROM analytics.mv_district_site_benchmark_april ORDER BY total_received DESC`)
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) { sendError(res, err.message) }
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
Reference in New Issue
Block a user