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:
freedakgmail
2026-07-27 08:33:42 +08:00
parent 521ba88937
commit 6e0c1bd7a4
18 changed files with 1892 additions and 30 deletions
+77 -29
View File
@@ -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