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
+2
View File
@@ -18,6 +18,7 @@ import { RiskPage } from '@/pages/RiskPage'
import { TimePage } from '@/pages/TimePage'
import { DataQualityPage } from '@/pages/DataQualityPage'
import { OntologyPage } from '@/pages/OntologyPage'
import { SiteSelectionPage } from '@/pages/SiteSelectionPage'
import { LoginPage } from '@/pages/LoginPage'
const queryClient = new QueryClient({
@@ -77,6 +78,7 @@ export default function App() {
<Route path="/data-quality" element={<DataQualityPage />} />
<Route path="/indicators" element={<IndicatorsPage />} />
<Route path="/ontology" element={<OntologyPage />} />
<Route path="/site-selection" element={<SiteSelectionPage />} />
<Route path="/login" element={<Navigate to="/" />} />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
+2 -1
View File
@@ -1,6 +1,6 @@
import { ReactNode, useState } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { LayoutDashboard, Store, ClipboardList, TrendingUp, Settings, LogOut, Menu, X, Package, DollarSign, ShoppingBag, Users, AlertTriangle, Clock, Database } from 'lucide-react'
import { LayoutDashboard, Store, ClipboardList, TrendingUp, Settings, LogOut, Menu, X, Package, DollarSign, ShoppingBag, Users, AlertTriangle, Clock, Database, MapPin } from 'lucide-react'
import { cn } from '@/lib/utils'
interface LayoutProps {
@@ -41,6 +41,7 @@ const menuGroups: MenuGroup[] = [
{ path: '/member', label: '会员复购', icon: Users, roles: ['hq', 'dept'] },
{ path: '/risk', label: '风险内控', icon: AlertTriangle, roles: ['hq', 'dept'] },
{ path: '/time', label: '时间分析', icon: Clock, roles: ['hq', 'dept'] },
{ path: '/site-selection', label: '门店选址', icon: MapPin, roles: ['hq', 'dept'] },
],
},
{
+316
View File
@@ -0,0 +1,316 @@
import { useQuery } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
import api from '@/lib/api'
import { DataTable } from '@/components/DataTable'
import { Pagination } from '@/components/Pagination'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import { CollapsibleSection } from '@/components/CollapsibleSection'
import { MetricCard } from '@/components/MetricCard'
import { formatCurrency, formatNumber, formatPercent } from '@/lib/utils'
import { useState, useMemo } from 'react'
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ScatterChart, Scatter, ZAxis, ReferenceLine, Cell } from 'recharts'
const PAGE_SIZE = 15
const SCENE_COLORS: Record<string, string> = {
'办公园区': '#3b82f6', '商场商业体': '#a855f7', '社区居民': '#22c55e', '街边综合': '#f59e0b', '交通枢纽': '#ef4444', '校园档口': '#06b6d4', '特殊业态': '#6b7280',
}
const RISK_COLORS: Record<string, string> = { '高风险:距离近且至少一家经营承压': '#ef4444', '中风险:需核查客群和配送圈重叠': '#eab308', '观察': '#22c55e' }
type SiteTab = 'benchmark' | 'replication' | 'overlap' | 'district'
const TABS: { key: SiteTab; label: string }[] = [
{ key: 'benchmark', label: '分段基准' },
{ key: 'replication', label: '复制评分' },
{ key: 'overlap', label: '重叠风险' },
{ key: 'district', label: '区域基准' },
]
export function SiteSelectionPage() {
const navigate = useNavigate()
const [tab, setTab] = useState<SiteTab>('benchmark')
const [sceneFilter, setSceneFilter] = useState('')
const [replPage, setReplPage] = useState(1)
const [overlapPage, setOverlapPage] = useState(1)
const [profilePage, setProfilePage] = useState(1)
const { data: profileData, isLoading: profileLoading } = useQuery({
queryKey: ['site-selection/profile'],
queryFn: () => api.get('/site-selection/profile'),
})
const { data: segData, isLoading: segLoading } = useQuery({
queryKey: ['site-selection/segment-benchmark'],
queryFn: () => api.get('/site-selection/segment-benchmark'),
})
const { data: replData, isLoading: replLoading } = useQuery({
queryKey: ['site-selection/replication'],
queryFn: () => api.get('/site-selection/replication'),
})
const { data: overlapData, isLoading: overlapLoading } = useQuery({
queryKey: ['site-selection/overlap-risk'],
queryFn: () => api.get('/site-selection/overlap-risk'),
})
const { data: districtData, isLoading: districtLoading } = useQuery({
queryKey: ['site-selection/district-benchmark'],
queryFn: () => api.get('/site-selection/district-benchmark'),
})
const profileRows = ((profileData as any)?.data || []).filter((r: any) => !sceneFilter || r.site_scene === sceneFilter)
const segRows = (segData as any)?.data || []
const replRows = (replData as any)?.data || []
const overlapRows = (overlapData as any)?.data || []
const districtRows = (districtData as any)?.data || []
const pagedRepl = useMemo(() => replRows.slice((replPage - 1) * PAGE_SIZE, replPage * PAGE_SIZE), [replRows, replPage])
const pagedOverlap = useMemo(() => overlapRows.slice((overlapPage - 1) * PAGE_SIZE, overlapPage * PAGE_SIZE), [overlapRows, overlapPage])
const pagedProfile = useMemo(() => profileRows.slice((profilePage - 1) * PAGE_SIZE, profilePage * PAGE_SIZE), [profileRows, profilePage])
const pageLoading = profileLoading || segLoading || replLoading || overlapLoading || districtLoading
if (pageLoading) {
return <LoadingSpinner text="加载选址分析数据..." />
}
const totalStores = profileRows.length
const totalScenes = Object.keys(profileRows.reduce((acc: any, r: any) => { acc[r.site_scene] = true; return acc }, {})).length
const highRiskCount = overlapRows.filter((r: any) => r.overlap_risk?.startsWith('高风险')).length
const topReplication = replRows.filter((r: any) => r.replication_recommendation === '优先提炼选址原型').length
const scatterData = profileRows.map((r: any) => ({
store_name: r.store_name,
area_sqm: Number(r.area_sqm || 0),
received_per_sqm: Number(r.monthly_received_per_sqm || 0),
received: Number(r.received || 0),
site_scene: r.site_scene,
}))
return (
<div className="space-y-4">
<div>
<h1 className="text-xl font-bold"></h1>
<p className="mt-0.5 text-xs text-muted-foreground"> · 20264 · 91</p>
</div>
{/* 概览指标 */}
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
<MetricCard title="标准门店数" value={totalStores} format="number" description="有面积和经营数据的标准门店" />
<MetricCard title="场景类型" value={totalScenes} format="number" description="办公/社区/商场/街边/交通枢纽等" />
<MetricCard title="高风险重叠对" value={highRiskCount} format="number" description="距离<1km且至少一家经营承压" />
<MetricCard title="优先提炼原型" value={topReplication} format="number" description="复制评分≥75且问题数≤1" />
</div>
{/* 面积×坪效散点图 */}
<CollapsibleSection title="面积 × 坪效分布" subtitle="每个点代表一家门店,颜色区分场景">
<ResponsiveContainer width="100%" height={300}>
<ScatterChart margin={{ left: 20, right: 20, top: 10, bottom: 10 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" dataKey="area_sqm" name="面积(㎡)" tick={{ fontSize: 10 }} label={{ value: '面积(㎡)', position: 'bottom', offset: 0, fontSize: 11 }} />
<YAxis type="number" dataKey="received_per_sqm" name="月坪效(元/㎡)" tick={{ fontSize: 10 }} tickFormatter={(v) => v >= 1000 ? `${(v / 1000).toFixed(1)}k` : v} />
<ZAxis type="number" dataKey="received" range={[40, 400]} name="实收" />
<Tooltip
cursor={{ strokeDasharray: '3 3' }}
content={({ payload }: any) => {
if (!payload || !payload.length) return null
const d = payload[0].payload
return (
<div className="rounded border bg-white p-2 text-xs shadow">
<p className="font-bold">{d.store_name}</p>
<p>: {d.site_scene}</p>
<p>: {d.area_sqm}</p>
<p>: {formatCurrency(d.received_per_sqm)}</p>
<p>: {formatCurrency(d.received)}</p>
</div>
)
}}
/>
<Scatter data={scatterData}>
{scatterData.map((entry: any, i: number) => (
<Cell key={i} fill={SCENE_COLORS[entry.site_scene] || '#999'} />
))}
</Scatter>
</ScatterChart>
</ResponsiveContainer>
<div className="mt-2 flex flex-wrap gap-3 text-xs">
{Object.entries(SCENE_COLORS).map(([scene, color]) => (
<span key={scene} className="flex items-center gap-1">
<span className="inline-block h-3 w-3 rounded-full" style={{ background: color }} />
{scene}
</span>
))}
</div>
</CollapsibleSection>
{/* Tab 切换 */}
<div className="flex gap-2 border-b">
{TABS.map((t) => (
<button
key={t.key}
onClick={() => setTab(t.key)}
className={`px-4 py-2 text-sm font-medium transition-colors ${
tab === t.key ? 'border-b-2 border-primary text-primary' : 'text-muted-foreground hover:text-foreground'
}`}
>
{t.label}
</button>
))}
</div>
{/* 分段基准 */}
{tab === 'benchmark' && (
<div className="space-y-4">
<CollapsibleSection title={`场景 × 面积分段基准 (${segRows.length})`} subtitle="各场景各面积段的经营基准,按坪效降序">
<DataTable
columns={[
{ key: 'site_scene', label: '场景' },
{ key: 'area_band', label: '面积段' },
{ key: 'store_count', label: '门店数', align: 'center' },
{ key: 'avg_area_sqm', label: '平均面积', align: 'right', render: (r) => `${r.avg_area_sqm}` },
{ key: 'avg_received', label: '平均实收', align: 'right', render: (r) => formatCurrency(r.avg_received) },
{ key: 'median_received', label: '中位实收', align: 'right', render: (r) => formatCurrency(r.median_received) },
{ key: 'avg_received_per_sqm', label: '平均坪效', align: 'right', render: (r) => formatCurrency(r.avg_received_per_sqm) },
{ key: 'median_received_per_sqm', label: '中位坪效', align: 'right', render: (r) => formatCurrency(r.median_received_per_sqm) },
{ key: 'avg_bill_value', label: '客单价', align: 'right', render: (r) => formatCurrency(r.avg_bill_value) },
{ key: 'avg_repeat_rate_pct', label: '复购率', align: 'right', render: (r) => formatPercent(r.avg_repeat_rate_pct) },
]}
data={segRows}
/>
</CollapsibleSection>
<CollapsibleSection title="门店画像明细" subtitle="全部标准门店选址画像,可按场景筛选">
<div className="mb-3 flex gap-2">
{['', '办公园区', '商场商业体', '社区居民', '街边综合'].map((f) => (
<button
key={f || 'all'}
onClick={() => { setSceneFilter(f); setProfilePage(1) }}
className={`rounded-md border px-3 py-1.5 text-sm ${sceneFilter === f ? 'border-primary bg-primary text-primary-foreground' : ''}`}
>
{f || '全部'}
</button>
))}
</div>
<Pagination page={profilePage} pageSize={PAGE_SIZE} total={profileRows.length} onPageChange={setProfilePage} />
<div className="mt-3">
<DataTable
columns={[
{ key: 'store_name', label: '门店' },
{ key: 'site_scene', label: '场景', render: (r) => <span className="rounded px-2 py-0.5 text-xs" style={{ background: (SCENE_COLORS[r.site_scene] || '#999') + '20', color: SCENE_COLORS[r.site_scene] || '#999' }}>{r.site_scene}</span> },
{ key: 'area_band', label: '面积段' },
{ key: 'area_sqm', label: '面积(㎡)', align: 'right', render: (r) => r.area_sqm ? `${r.area_sqm}` : '-' },
{ key: 'received', label: '实收', align: 'right', render: (r) => formatCurrency(r.received) },
{ key: 'monthly_received_per_sqm', label: '坪效', align: 'right', render: (r) => formatCurrency(r.monthly_received_per_sqm) },
{ key: 'avg_bill_value', label: '客单价', align: 'right', render: (r) => formatCurrency(r.avg_bill_value) },
{ key: 'repeat_rate_pct', label: '复购率', align: 'right', render: (r) => formatPercent(r.repeat_rate_pct) },
{ key: 'actual_food_cost_rate_pct', label: '成本率', align: 'right', render: (r) => formatPercent(r.actual_food_cost_rate_pct) },
]}
data={pagedProfile}
onRowClick={(r) => navigate(`/stores/${r.store_code}`)}
/>
</div>
</CollapsibleSection>
</div>
)}
{/* 复制评分 */}
{tab === 'replication' && (
<CollapsibleSection title={`门店复制评分 (${replRows.length})`} subtitle="综合坪效、日均、复购、优惠纪律、成本、平台、执行七维评分">
<Pagination page={replPage} pageSize={PAGE_SIZE} total={replRows.length} onPageChange={setReplPage} />
<div className="mt-3">
<DataTable
columns={[
{ key: 'store_name', label: '门店' },
{ key: 'site_scene', label: '场景' },
{ key: 'area_sqm', label: '面积', align: 'right', render: (r) => r.area_sqm ? `${r.area_sqm}` : '-' },
{ key: 'received', label: '实收', align: 'right', render: (r) => formatCurrency(r.received) },
{ key: 'monthly_received_per_sqm', label: '坪效', align: 'right', render: (r) => formatCurrency(r.monthly_received_per_sqm) },
{ key: 'nearest_store_name', label: '最近门店' },
{ key: 'nearest_distance_km', label: '距离(km)', align: 'right', render: (r) => `${r.nearest_distance_km}km` },
{ key: 'site_replication_score', label: '复制评分', align: 'right', render: (r) => {
const v = Number(r.site_replication_score || 0)
return <span className={v >= 75 ? 'font-bold text-green-600' : v >= 60 ? 'text-blue-600' : v < 40 ? 'text-red-600' : ''}>{v.toFixed(2)}</span>
} },
{ key: 'replication_recommendation', label: '复制建议', render: (r) => {
const rec = r.replication_recommendation
const cls = rec === '优先提炼选址原型' ? 'bg-green-100 text-green-700' : rec === '不宜作为选址标杆' ? 'bg-red-100 text-red-700' : 'bg-blue-100 text-blue-700'
return <span className={`rounded px-2 py-0.5 text-xs ${cls}`}>{rec}</span>
} },
{ key: 'spatial_recommendation', label: '空间建议', render: (r) => <span className="text-xs text-muted-foreground">{r.spatial_recommendation}</span> },
]}
data={pagedRepl}
onRowClick={(r) => navigate(`/stores/${r.store_code}`)}
/>
</div>
</CollapsibleSection>
)}
{/* 重叠风险 */}
{tab === 'overlap' && (
<div className="space-y-4">
<div className="grid grid-cols-3 gap-3">
<MetricCard title="高度重叠(<1km)" value={overlapRows.filter((r: any) => r.distance_km < 1).length} format="number" description="需做顾客来源和配送圈验证" />
<MetricCard title="较高重叠(1-2km)" value={overlapRows.filter((r: any) => r.distance_km >= 1 && r.distance_km < 2).length} format="number" description="检查道路阻隔和商圈边界" />
<MetricCard title="观察(2-3km)" value={overlapRows.filter((r: any) => r.distance_km >= 2).length} format="number" description="不能仅用直线距离判断" />
</div>
<CollapsibleSection title={`空间重叠风险 (${overlapRows.length}对)`} subtitle="3公里内门店两两距离与重叠风险等级">
<Pagination page={overlapPage} pageSize={PAGE_SIZE} total={overlapRows.length} onPageChange={setOverlapPage} />
<div className="mt-3">
<DataTable
columns={[
{ key: 'store_name_a', label: '门店A' },
{ key: 'store_name_b', label: '门店B' },
{ key: 'distance_km', label: '距离(km)', align: 'right', render: (r) => `${Number(r.distance_km).toFixed(2)}km` },
{ key: 'received_a', label: 'A实收', align: 'right', render: (r) => formatCurrency(r.received_a) },
{ key: 'received_b', label: 'B实收', align: 'right', render: (r) => formatCurrency(r.received_b) },
{ key: 'overlap_risk', label: '风险等级', render: (r) => {
const risk = r.overlap_risk
const color = RISK_COLORS[risk] || '#999'
return <span className="rounded px-2 py-0.5 text-xs font-medium" style={{ background: color + '20', color }}>{risk}</span>
} },
]}
data={pagedOverlap}
/>
</div>
</CollapsibleSection>
</div>
)}
{/* 区域基准 */}
{tab === 'district' && (
<div className="space-y-4">
<CollapsibleSection title="区域基准柱状图" subtitle="各区域平均实收对比">
<ResponsiveContainer width="100%" height={280}>
<BarChart data={districtRows.slice(0, 15)} margin={{ left: 20 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="district" tick={{ fontSize: 10 }} angle={-30} textAnchor="end" height={60} />
<YAxis tick={{ fontSize: 10 }} tickFormatter={(v) => v >= 10000 ? `${(v / 10000).toFixed(0)}` : v} />
<Tooltip formatter={(v: any) => formatCurrency(v)} />
<Bar dataKey="avg_received" fill="#3b82f6" name="平均实收" />
<Bar dataKey="median_received" fill="#22c55e" name="中位实收" />
</BarChart>
</ResponsiveContainer>
</CollapsibleSection>
<CollapsibleSection title={`区域基准明细 (${districtRows.length})`} subtitle="按区域汇总的经营基准">
<DataTable
columns={[
{ key: 'district', label: '区域' },
{ key: 'store_count', label: '门店数', align: 'center' },
{ key: 'avg_area_sqm', label: '平均面积', align: 'right', render: (r) => `${r.avg_area_sqm}` },
{ key: 'total_received', label: '总实收', align: 'right', render: (r) => formatCurrency(r.total_received) },
{ key: 'avg_received', label: '平均实收', align: 'right', render: (r) => formatCurrency(r.avg_received) },
{ key: 'median_received', label: '中位实收', align: 'right', render: (r) => formatCurrency(r.median_received) },
{ key: 'avg_received_per_sqm', label: '平均坪效', align: 'right', render: (r) => formatCurrency(r.avg_received_per_sqm) },
{ key: 'avg_bill_value', label: '客单价', align: 'right', render: (r) => formatCurrency(r.avg_bill_value) },
{ key: 'avg_repeat_rate_pct', label: '复购率', align: 'right', render: (r) => formatPercent(r.avg_repeat_rate_pct) },
{ key: 'p0_count', label: 'P0', align: 'center', render: (r) => r.p0_count > 0 ? <span className="font-bold text-red-600">{r.p0_count}</span> : '0' },
{ key: 'p1_count', label: 'P1', align: 'center', render: (r) => r.p1_count > 0 ? <span className="font-bold text-yellow-600">{r.p1_count}</span> : '0' },
]}
data={districtRows}
/>
</CollapsibleSection>
</div>
)}
</div>
)
}
+1
View File
@@ -41,6 +41,7 @@ export function StoreDetailPage() {
const { data: tasksData } = useQuery({
queryKey: ['store', code, 'tasks'],
queryFn: () => api.get('/tasks', { params: { store_code: code, month: '2026-05', page_size: 50 } }),
enabled: tab === 'tasks',
})
const { data: mealData } = useQuery({
+112
View File
@@ -0,0 +1,112 @@
# 马兰拉面数字化运营管理平台 - 产品演示视频
## 视频文件
- **product-demo.mp4** — 32秒,16张截图,每张2秒,1920×1080,硬切无黑屏
## 截图编排(按《餐易通×收钱吧战略融合价值分析》逻辑组织)
### 第一章:行业本质问题——餐饮数字化的"半截子"困境
| 序号 | 文件 | 页面 | 对应文档逻辑 |
|---|---|---|---|
| s01 | s01-dashboard-top.png | 总部驾驶舱-顶部 | **底层基础设施已就位**:支付数据已沉淀,KPI卡片总览全局经营 |
| s02 | s02-dashboard-charts.png | 总部驾驶舱-图表 | **但数据未转化为经营决策**:象限散点图、瀑布图、平台成本率图展示数据深度分析能力 |
### 第二章:战略定位——从"帮商户收钱"到"帮商户赚钱"
| 序号 | 文件 | 页面 | 对应文档逻辑 |
|---|---|---|---|
| s03 | s03-regional-overview.png | 区域经理工作台 | **区域经营汇总**:12个区按实收排名,红黄绿风险分布,从GMV到经营利润的价值锚点转换 |
### 第三章:数据飞轮——融合后的增长引擎
| 序号 | 文件 | 页面 | 对应文档逻辑 |
|---|---|---|---|
| s04 | s04-store-manager-full.png | 店长工作台-全页 | **数据→分析→指令→行动闭环**:经营概览→指标进度→SKU备货→任务执行 |
| s05 | s05-store-manager-top.png | 店长工作台-顶部 | **可执行的经营指令**:最新经营概览(异常标注)+ 本周指标进度条(达标判定),店长几分钟看完就知道今天干什么 |
### 第四章:餐易通为什么能做到——不是软件公司,是餐饮产业公司
| 序号 | 文件 | 页面 | 对应文档逻辑 |
|---|---|---|---|
| s06 | s06-store-detail-overview.png | 门店详情-概览 | **从支付→菜品→成本全链路**:菜百店KPI、日度趋势、平台经济性 |
| s07 | s07-store-detail-meal-period.png | 门店详情-餐段分析 | **AI客流预测基础**:午市/晚市/下午茶/早市客流热力分布 |
| s08 | s08-store-detail-category.png | 门店详情-品类结构 | **标准化SKU结构**:品类销售占比、搭售分析 |
| s09 | s09-store-detail-cost.png | 门店详情-成本分析 | **成本诊断模型**:理论成本vs实际成本差异定位 |
| s10 | s10-store-detail-member.png | 门店详情-会员分析 | **会员复购分析**RFM分层、沉睡会员召回机会 |
| s11 | s11-store-detail-anomaly.png | 门店详情-异常账单 | **风险内控**:异常账单识别与闭环处理 |
### 第五章:产业级影响——从服务商户到定义标准
| 序号 | 文件 | 页面 | 对应文档逻辑 |
|---|---|---|---|
| s12 | s12-monthly-review-summary.png | 月度验收-验收汇总 | **从"数字化"到"智能化"**:月度验收KPI汇总,每天只推少量异常和待办 |
| s13 | s13-monthly-review-grade.png | 月度验收-升降级面板 | **从"开店"到"复制盈利模型"**:P0/P1/P2等级变化,标准化经营模型验证 |
| s14 | s14-monthly-review-completion.png | 月度验收-任务完成率 | **闭环验证**:按优先级汇总任务完成率,验证改善效果 |
| s15 | s15-monthly-review-activity.png | 月度验收-活动清单 | **平台经济性分析**:逐活动贡献毛利,停/改/留清单由数据驱动 |
| s16 | s16-monthly-review-sku.png | 月度验收-SKU治理 | **标准化SKU输出**:ABC分类饼图,长尾SKU精简治理 |
## 视频合成命令
```bash
# 每张图2秒,硬切无转场,1920×1080
cd demo && ffmpeg -y -framerate 1/2 -pattern_type glob -i 's*.png' \
-c:v libx264 -pix_fmt yuv420p \
-vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:0x0F0F0F" \
-r 30 product-demo.mp4
```
## 推荐旁白文案(按文档九章结构)
> **s01-s02 总部驾驶舱**
> "中国餐饮数字化面临'半截子'困境:支付基础设施已铺到位,但数据没有向上转化为经营决策。我们的平台从支付数据出发,构建象限散点图、瀑布图、平台成本率图,让总部一屏看全局。"
>
> **s03 区域经理工作台**
> "区域经理工作台,按12个区汇总经营数据,红黄绿风险一目了然。价值锚点从交易规模转向经营利润,从'帮商户收钱'升级为'帮商户赚钱'。"
>
> **s04-s05 店长工作台**
> "数据飞轮的核心:经营概览标注异常项,本周指标进度条直观显示达标情况,核心SKU备货提醒确保不断货,待办任务指引每日行动。店长几分钟看完就知道今天干什么——不是更多报表,而是更少的、更准的、能直接行动的指令。"
>
> **s06 门店详情-概览**
> "以菜百店为例,从支付到菜品到成本的全链路分析。日度实收趋势、平台经济性,一切用数据说话。"
>
> **s07 餐段分析**
> "餐段分析精准定位午市、晚市、下午茶、早市的客流与客单价差异,为AI客流预测和智能排班提供基础。"
>
> **s08 品类结构**
> "品类结构分析揭示销售集中度和搭售机会,为标准化SKU模型提供数据支撑。"
>
> **s09 成本分析**
> "成本诊断模型对比理论成本与实际成本,精准定位差异来源——这不是给商户一套漂亮的报表,而是给出一个店长明天就能执行的动作。"
>
> **s10 会员分析**
> "会员复购分析,基于支付身份识别自动分层,发现沉睡会员召回机会,构建精准营销闭环。"
>
> **s11 异常账单**
> "风险内控模块自动识别异常账单,从发现到处理全程闭环,让每一家门店的经营都合规可控。"
>
> **s12 月度验收汇总**
> "月度验收与复盘,从'数字化'升级为'智能化'。不是给商户更多报表,而是更少的、更准的、能直接行动的指令。"
>
> **s13 升降级面板**
> "P0/P1/P2等级变化记录,验证标准化经营模型的可复制性。从'开店'到'复制盈利模型''开一家赚一家'取代'开了再说'。"
>
> **s14 任务完成率**
> "任务完成率按优先级汇总,闭环验证改善效果。每一个改善都可追踪、可量化。"
>
> **s15 活动清单**
> "逐活动贡献毛利分析,识别'越卖越亏'的活动。停、改、留清单由数据驱动而非经验判断。"
>
> **s16 SKU治理**
> "ABC分类识别长尾SKU,推动SKU精简治理。标准SKU结构、标准备货量、标准成本管控——这就是可复制的盈利模型。"
>
> **结尾**
> "马兰拉面数字化运营管理平台——支付底座+经营大脑,数据驱动,闭环管理,让每一家门店都更好。"
## 截图参数
- 分辨率:1440×900 viewport
- 格式:PNG
- 设备像素比:deviceRetina高清)
- 门店:菜百店(store_code=1111
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Add subtitle text to each screenshot, then build video with ffmpeg."""
import os, subprocess
from PIL import Image, ImageDraw, ImageFont
DEMO_DIR = os.path.dirname(os.path.abspath(__file__))
FONT_PATH = "/System/Library/Fonts/STHeiti Medium.ttc"
FONT_SIZE = 36
# (filename, subtitle_text)
screenshots = [
("s01-dashboard-top.png", "总部驾驶舱 — 全局经营总览,KPI一屏掌握"),
("s02-dashboard-charts.png", "象限散点图·瀑布图·平台成本率 — 数据深度分析"),
("s03-regional-overview.png", "区域经理工作台 — 12区经营汇总,红黄绿风险分布"),
("s04-store-manager-full.png", "店长工作台 — 经营概览·指标进度·SKU备货·任务闭环"),
("s05-store-manager-top.png", "店长工作台 — 异常标注+指标进度条,几分钟看完就知道今天干什么"),
("s06-store-detail-overview.png", "门店详情·概览 — 菜百店全链路:KPI·日度趋势·平台经济性"),
("s07-store-detail-meal-period.png", "门店详情·餐段分析 — 午市/晚市/下午茶/早市客流与客单价"),
("s08-store-detail-category.png", "门店详情·品类结构 — 销售集中度与搭售机会分析"),
("s09-store-detail-cost.png", "门店详情·成本分析 — 理论成本vs实际成本,精准定位差异"),
("s10-store-detail-member.png", "门店详情·会员分析 — RFM分层,沉睡会员召回机会"),
("s11-store-detail-anomaly.png", "门店详情·异常账单 — 风险内控,自动识别与闭环处理"),
("s12-monthly-review-summary.png", "月度验收·验收汇总 — 从数字化到智能化,只推可执行指令"),
("s13-monthly-review-grade.png", "月度验收·升降级面板 — P0/P1/P2等级变化,复制盈利模型"),
("s14-monthly-review-completion.png","月度验收·任务完成率 — 按优先级汇总,闭环验证改善效果"),
("s15-monthly-review-activity.png", "月度验收·活动清单 — 逐活动贡献毛利,停改留由数据驱动"),
("s16-monthly-review-sku.png", "月度验收·SKU治理 — ABC分类,长尾精简,标准化输出"),
]
sub_dir = os.path.join(DEMO_DIR, "sub")
os.makedirs(sub_dir, exist_ok=True)
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
for fname, text in screenshots:
src = os.path.join(DEMO_DIR, fname)
img = Image.open(src).convert("RGBA")
w, h = img.size
# Create overlay for text background bar
overlay = Image.new("RGBA", (w, h), (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
# Measure text
bbox = draw.textbbox((0, 0), text, font=font)
tw = bbox[2] - bbox[0]
th = bbox[3] - bbox[1]
# Draw semi-transparent black bar at bottom
bar_h = th + 30
bar_y = h - bar_h
draw.rectangle([0, bar_y, w, h], fill=(0, 0, 0, 180))
# Draw text centered
tx = (w - tw) // 2
ty = bar_y + 15
draw.text((tx, ty), text, font=font, fill=(255, 255, 255, 255))
# Composite
result = Image.alpha_composite(img, overlay).convert("RGB")
dst = os.path.join(sub_dir, fname)
result.save(dst, "PNG")
print(f" {fname} -> sub/{fname}")
print(f"\nGenerated {len(screenshots)} annotated screenshots in sub/")
# Build video
print("\nBuilding video with ffmpeg...")
cmd = [
"ffmpeg", "-y",
"-framerate", "1/2",
"-pattern_type", "glob",
"-i", os.path.join(sub_dir, "s*.png"),
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
"-vf", "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:0x0F0F0F",
"-r", "30",
os.path.join(DEMO_DIR, "product-demo.mp4")
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print("Video built: product-demo.mp4")
else:
print(f"Error: {result.stderr[-500:]}")
BIN
View File
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
cd /Users/freedak/Documents/AIDashboard/SBrainCO/demo
ffmpeg -y -framerate 1/2 -pattern_type glob -i 's*.png' \
-c:v libx264 -pix_fmt yuv420p \
-vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:0x0F0F0F,subtitles=subtitles.srt:force_style='FontName=Heiti SC,FontSize=22,PrimaryColour=&H00FFFFFF,OutlineColour=&H000F0F0F,BorderStyle=3,Outline=2,Shadow=0,MarginV=40,Alignment=2'" \
-r 30 product-demo.mp4
+32
View File
@@ -0,0 +1,32 @@
file 's01-dashboard-top.png'
file 's01-dashboard-top.png'
file 's02-dashboard-charts.png'
file 's02-dashboard-charts.png'
file 's03-regional-overview.png'
file 's03-regional-overview.png'
file 's04-store-manager-full.png'
file 's04-store-manager-full.png'
file 's05-store-manager-top.png'
file 's05-store-manager-top.png'
file 's06-store-detail-overview.png'
file 's06-store-detail-overview.png'
file 's07-store-detail-meal-period.png'
file 's07-store-detail-meal-period.png'
file 's08-store-detail-category.png'
file 's08-store-detail-category.png'
file 's09-store-detail-cost.png'
file 's09-store-detail-cost.png'
file 's10-store-detail-member.png'
file 's10-store-detail-member.png'
file 's11-store-detail-anomaly.png'
file 's11-store-detail-anomaly.png'
file 's12-monthly-review-summary.png'
file 's12-monthly-review-summary.png'
file 's13-monthly-review-grade.png'
file 's13-monthly-review-grade.png'
file 's14-monthly-review-completion.png'
file 's14-monthly-review-completion.png'
file 's15-monthly-review-activity.png'
file 's15-monthly-review-activity.png'
file 's16-monthly-review-sku.png'
file 's16-monthly-review-sku.png'
+3
View File
@@ -0,0 +1,3 @@
scale=1920:1080:force_original_aspect_ratio=decrease
pad=1920:1080:(ow-iw)/2:(oh-ih)/2:0x0F0F0F
subtitles=subtitles.srt:force_style='FontName=Heiti SC,FontSize=22,PrimaryColour=&H00FFFFFF,OutlineColour=&H000F0F0F,BorderStyle=3,Outline=2,Shadow=0,MarginV=40,Alignment=2'
Binary file not shown.
+71
View File
@@ -0,0 +1,71 @@
1
00:00:00,000 --> 00:00:02,000
总部驾驶舱 — 全局经营总览,KPI一屏掌握
2
00:00:02,000 --> 00:00:04,000
象限散点图·瀑布图·平台成本率 — 数据深度分析
3
00:00:00,000 --> 00:00:02,000
总部驾驶舱 — 全局经营总览,KPI一屏掌握
2
00:00:02,000 --> 00:00:04,000
象限散点图·瀑布图·平台成本率 — 数据深度分析
3
00:00:04,000 --> 00:00:06,000
区域经理工作台 — 12区经营汇总,红黄绿风险分布
4
00:00:06,000 --> 00:00:08,000
店长工作台 — 经营概览·指标进度·SKU备货·任务闭环
5
00:00:08,000 --> 00:00:10,000
店长工作台 — 异常标注+指标进度条,几分钟看完就知道今天干什么
6
00:00:10,000 --> 00:00:12,000
门店详情·概览 — 菜百店全链路:KPI·日度趋势·平台经济性
7
00:00:12,000 --> 00:00:14,000
门店详情·餐段分析 — 午市/晚市/下午茶/早市客流与客单价
8
00:00:14,000 --> 00:00:16,000
门店详情·品类结构 — 销售集中度与搭售机会分析
9
00:00:16,000 --> 00:00:18,000
门店详情·成本分析 — 理论成本vs实际成本,精准定位差异
10
00:00:18,000 --> 00:00:20,000
门店详情·会员分析 — RFM分层,沉睡会员召回机会
11
00:00:20,000 --> 00:00:22,000
门店详情·异常账单 — 风险内控,自动识别与闭环处理
12
00:00:22,000 --> 00:00:24,000
月度验收·验收汇总 — 从数字化到智能化,只推可执行指令
13
00:00:24,000 --> 00:00:26,000
月度验收·升降级面板 — P0/P1/P2等级变化,复制盈利模型
14
00:00:26,000 --> 00:00:28,000
月度验收·任务完成率 — 按优先级汇总,闭环验证改善效果
15
00:00:28,000 --> 00:00:30,000
月度验收·活动清单 — 逐活动贡献毛利,停改留由数据驱动
16
00:00:30,000 --> 00:00:32,000
月度验收·SKU治理 — ABC分类,长尾精简,标准化输出
+10
View File
@@ -0,0 +1,10 @@
-- ============================================================
-- 09_add_indexes.sql
-- 门店详情页性能优化索引
-- ============================================================
-- bill_fact 表:门店+日期复合索引(日度趋势、概览等查询使用)
CREATE INDEX IF NOT EXISTS idx_bill_fact_store_closed ON analytics.bill_fact (store_code, closed_at);
-- bill_fact 物化视图:单独的 store_code 索引(其他视图按 store_code 过滤时使用)
CREATE INDEX IF NOT EXISTS idx_bill_fact_store ON analytics.bill_fact (store_code);
+492
View File
@@ -0,0 +1,492 @@
-- ============================================================
-- 10_materialize_slow_views.sql
-- 将门店详情页慢视图转为物化视图,加 store_code 索引
-- 预期:/stores/:code 从 ~15s 降至 <100ms
-- ============================================================
-- ============================================================
-- Step 1: 依赖视图按逆序 DROP
-- ============================================================
DROP VIEW IF EXISTS analytics.v_store_execution_priority;
DROP VIEW IF EXISTS analytics.v_store_deep_diagnosis_april;
DROP VIEW IF EXISTS analytics.v_store_benchmark_composite;
DROP VIEW IF EXISTS analytics.v_store_action_list;
-- ============================================================
-- Step 2: 基础视图 → 物化视图
-- ============================================================
-- 2a. v_store_platform_economics
DROP VIEW IF EXISTS analytics.v_store_platform_economics;
CREATE MATERIALIZED VIEW analytics.v_store_platform_economics AS
SELECT NULLIF(bill_records.c002, ''::text) AS store_code,
NULLIF(bill_records.c003, ''::text) AS store_name,
sum(COALESCE(NULLIF(bill_records.c151, ''::text)::numeric, 0::numeric)) AS meituan_received,
sum(COALESCE(NULLIF(bill_records.c101, ''::text)::numeric, 0::numeric)) AS meituan_discount,
sum(COALESCE(NULLIF(bill_records.c097, ''::text)::numeric, 0::numeric)) AS meituan_commission,
sum(COALESCE(NULLIF(bill_records.c152, ''::text)::numeric, 0::numeric)) AS taobao_received,
sum(COALESCE(NULLIF(bill_records.c102, ''::text)::numeric, 0::numeric)) AS taobao_discount,
sum(COALESCE(NULLIF(bill_records.c098, ''::text)::numeric, 0::numeric)) AS taobao_commission,
sum(COALESCE(NULLIF(bill_records.c150, ''::text)::numeric, 0::numeric)) AS jd_received,
sum(COALESCE(NULLIF(bill_records.c099, ''::text)::numeric, 0::numeric)) AS jd_discount,
sum(COALESCE(NULLIF(bill_records.c100, ''::text)::numeric, 0::numeric)) AS jd_commission,
round((sum(COALESCE(NULLIF(bill_records.c101, ''::text)::numeric, 0::numeric)) + sum(COALESCE(NULLIF(bill_records.c097, ''::text)::numeric, 0::numeric))) / NULLIF(sum(COALESCE(NULLIF(bill_records.c151, ''::text)::numeric, 0::numeric)) + sum(COALESCE(NULLIF(bill_records.c101, ''::text)::numeric, 0::numeric)) + sum(COALESCE(NULLIF(bill_records.c097, ''::text)::numeric, 0::numeric)), 0::numeric) * 100::numeric, 2) AS meituan_cost_rate_pct,
round((sum(COALESCE(NULLIF(bill_records.c102, ''::text)::numeric, 0::numeric)) + sum(COALESCE(NULLIF(bill_records.c098, ''::text)::numeric, 0::numeric))) / NULLIF(sum(COALESCE(NULLIF(bill_records.c152, ''::text)::numeric, 0::numeric)) + sum(COALESCE(NULLIF(bill_records.c102, ''::text)::numeric, 0::numeric)) + sum(COALESCE(NULLIF(bill_records.c098, ''::text)::numeric, 0::numeric)), 0::numeric) * 100::numeric, 2) AS taobao_cost_rate_pct,
round((sum(COALESCE(NULLIF(bill_records.c099, ''::text)::numeric, 0::numeric)) + sum(COALESCE(NULLIF(bill_records.c100, ''::text)::numeric, 0::numeric))) / NULLIF(sum(COALESCE(NULLIF(bill_records.c150, ''::text)::numeric, 0::numeric)) + sum(COALESCE(NULLIF(bill_records.c099, ''::text)::numeric, 0::numeric)) + sum(COALESCE(NULLIF(bill_records.c100, ''::text)::numeric, 0::numeric)), 0::numeric) * 100::numeric, 2) AS jd_cost_rate_pct
FROM bill_records
WHERE NULLIF(bill_records.c005, ''::text) IS NOT NULL
GROUP BY (NULLIF(bill_records.c002, ''::text)), (NULLIF(bill_records.c003, ''::text))
WITH DATA;
CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_platform_economics_store ON analytics.v_store_platform_economics (store_code);
-- 2b. v_store_category_mix
DROP VIEW IF EXISTS analytics.v_store_category_mix;
CREATE MATERIALIZED VIEW analytics.v_store_category_mix AS
SELECT NULLIF(bill_records.c002, ''::text) AS store_code,
NULLIF(bill_records.c003, ''::text) AS store_name,
sum(COALESCE(NULLIF(bill_records.c009, ''::text)::numeric, 0::numeric)) AS consumption,
sum(COALESCE(NULLIF(bill_records.c010, ''::text)::numeric, 0::numeric)) AS lanzhou_noodle,
sum(COALESCE(NULLIF(bill_records.c016, ''::text)::numeric, 0::numeric)) AS western_staple,
sum(COALESCE(NULLIF(bill_records.c027, ''::text)::numeric, 0::numeric)) AS delivery_package,
sum(COALESCE(NULLIF(bill_records.c013, ''::text)::numeric, 0::numeric)) AS night_bbq,
sum(COALESCE(NULLIF(bill_records.c015, ''::text)::numeric, 0::numeric)) AS cold_dishes,
sum(COALESCE(NULLIF(bill_records.c019, ''::text)::numeric, 0::numeric)) AS silk_road_food,
round(sum(COALESCE(NULLIF(bill_records.c010, ''::text)::numeric, 0::numeric)) / NULLIF(sum(COALESCE(NULLIF(bill_records.c009, ''::text)::numeric, 0::numeric)), 0::numeric) * 100::numeric, 2) AS noodle_share_pct,
round(sum(COALESCE(NULLIF(bill_records.c027, ''::text)::numeric, 0::numeric)) / NULLIF(sum(COALESCE(NULLIF(bill_records.c009, ''::text)::numeric, 0::numeric)), 0::numeric) * 100::numeric, 2) AS delivery_package_share_pct,
round(GREATEST(sum(COALESCE(NULLIF(bill_records.c010, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c016, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c027, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c013, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c015, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c019, ''::text)::numeric, 0::numeric))) / NULLIF(sum(COALESCE(NULLIF(bill_records.c009, ''::text)::numeric, 0::numeric)), 0::numeric) * 100::numeric, 2) AS top_category_share_pct,
CASE GREATEST(sum(COALESCE(NULLIF(bill_records.c010, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c016, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c027, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c013, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c015, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c019, ''::text)::numeric, 0::numeric)))
WHEN sum(COALESCE(NULLIF(bill_records.c010, ''::text)::numeric, 0::numeric)) THEN '兰州牛肉面'::text
WHEN sum(COALESCE(NULLIF(bill_records.c016, ''::text)::numeric, 0::numeric)) THEN '西部主食'::text
WHEN sum(COALESCE(NULLIF(bill_records.c027, ''::text)::numeric, 0::numeric)) THEN '外卖套餐'::text
WHEN sum(COALESCE(NULLIF(bill_records.c013, ''::text)::numeric, 0::numeric)) THEN '夜市烧烤'::text
WHEN sum(COALESCE(NULLIF(bill_records.c015, ''::text)::numeric, 0::numeric)) THEN '爽口凉菜'::text
ELSE '丝路美食'::text
END AS top_category
FROM bill_records
WHERE NULLIF(bill_records.c005, ''::text) IS NOT NULL
GROUP BY (NULLIF(bill_records.c002, ''::text)), (NULLIF(bill_records.c003, ''::text))
WITH DATA;
CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_category_mix_store ON analytics.v_store_category_mix (store_code);
-- 2c. v_store_benchmark
DROP VIEW IF EXISTS analytics.v_store_benchmark;
CREATE MATERIALIZED VIEW analytics.v_store_benchmark AS
WITH eligible AS (
SELECT v_store_risk_rating.store_code,
v_store_risk_rating.store_name,
v_store_risk_rating.bill_count,
v_store_risk_rating.active_days,
v_store_risk_rating.received,
v_store_risk_rating.avg_daily_received,
v_store_risk_rating.avg_bill_value,
v_store_risk_rating.avg_guest_value,
v_store_risk_rating.discount_rate_pct,
v_store_risk_rating.theoretical_margin_pct,
v_store_risk_rating.member_bill_share_pct,
v_store_risk_rating.anomaly_rate_pct,
v_store_risk_rating.risk_level,
v_store_risk_rating.primary_issue
FROM analytics.v_store_risk_rating
WHERE v_store_risk_rating.active_days >= 25 AND v_store_risk_rating.bill_count >= 5000
), stats AS (
SELECT percentile_cont(0.25::double precision) WITHIN GROUP (ORDER BY (eligible.avg_daily_received::double precision)) AS revenue_p25,
percentile_cont(0.50::double precision) WITHIN GROUP (ORDER BY (eligible.avg_daily_received::double precision)) AS revenue_p50,
percentile_cont(0.75::double precision) WITHIN GROUP (ORDER BY (eligible.avg_daily_received::double precision)) AS revenue_p75,
percentile_cont(0.50::double precision) WITHIN GROUP (ORDER BY (eligible.theoretical_margin_pct::double precision)) AS margin_p50,
percentile_cont(0.75::double precision) WITHIN GROUP (ORDER BY (eligible.theoretical_margin_pct::double precision)) AS margin_p75,
percentile_cont(0.75::double precision) WITHIN GROUP (ORDER BY (eligible.discount_rate_pct::double precision)) AS discount_p75
FROM eligible
), base AS (
SELECT e.store_code,
e.store_name,
e.bill_count,
e.active_days,
e.received,
e.avg_daily_received,
e.avg_bill_value,
e.avg_guest_value,
e.discount_rate_pct,
e.theoretical_margin_pct,
e.member_bill_share_pct,
e.anomaly_rate_pct,
e.risk_level,
e.primary_issue,
s.revenue_p25,
s.revenue_p50,
s.revenue_p75,
s.margin_p50,
s.margin_p75,
s.discount_p75
FROM eligible e
CROSS JOIN stats s
)
SELECT base.store_code,
base.store_name,
base.bill_count,
base.active_days,
base.received,
base.avg_daily_received,
base.avg_bill_value,
base.avg_guest_value,
base.discount_rate_pct,
base.theoretical_margin_pct,
base.member_bill_share_pct,
base.anomaly_rate_pct,
base.risk_level,
base.primary_issue,
base.revenue_p25,
base.revenue_p50,
base.revenue_p75,
base.margin_p50,
base.margin_p75,
base.discount_p75,
CASE
WHEN base.avg_daily_received::double precision >= base.revenue_p75 AND base.theoretical_margin_pct::double precision >= base.margin_p50 THEN '明星门店'::text
WHEN base.avg_daily_received::double precision >= base.revenue_p75 AND base.theoretical_margin_pct::double precision < base.margin_p50 THEN '规模承压'::text
WHEN base.avg_daily_received::double precision < base.revenue_p75 AND base.theoretical_margin_pct::double precision >= base.margin_p75 THEN '高效潜力'::text
WHEN base.avg_daily_received::double precision <= base.revenue_p25 OR base.theoretical_margin_pct < 68::numeric OR base.discount_rate_pct::double precision > base.discount_p75 THEN '重点改善'::text
ELSE '稳健经营'::text
END AS management_quadrant,
round(GREATEST(base.received / NULLIF(1::numeric - base.discount_rate_pct / 100::numeric, 0::numeric) * (base.discount_rate_pct - 20.23) / 100::numeric, 0::numeric), 2) AS discount_saving_to_company_avg,
round(GREATEST(base.received * 0.7092 - base.received * base.theoretical_margin_pct / 100::numeric, 0::numeric), 2) AS profit_uplift_to_company_margin
FROM base
WITH DATA;
CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_benchmark_store ON analytics.v_store_benchmark (store_code);
-- ============================================================
-- Step 3: 组合视图 → 物化视图
-- ============================================================
-- 3a. v_store_action_list (joins benchmark + category_mix + platform_economics)
CREATE MATERIALIZED VIEW analytics.v_store_action_list AS
SELECT b.store_code,
b.store_name,
b.management_quadrant,
b.risk_level,
b.primary_issue,
b.received,
b.avg_daily_received,
b.avg_bill_value,
b.discount_rate_pct,
b.theoretical_margin_pct,
b.anomaly_rate_pct,
b.member_bill_share_pct,
b.discount_saving_to_company_avg,
b.profit_uplift_to_company_margin,
c.top_category,
c.top_category_share_pct,
c.delivery_package_share_pct,
p.meituan_cost_rate_pct,
p.taobao_cost_rate_pct,
p.jd_cost_rate_pct
FROM analytics.v_store_benchmark b
LEFT JOIN analytics.v_store_category_mix c USING (store_code, store_name)
LEFT JOIN analytics.v_store_platform_economics p USING (store_code, store_name)
WITH DATA;
CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_action_list_store ON analytics.v_store_action_list (store_code);
-- 3b. v_store_execution_priority (joins action_list + meal + member + zero_bill)
CREATE MATERIALIZED VIEW analytics.v_store_execution_priority AS
WITH meal AS (
SELECT v_store_meal_opportunity.store_code,
sum(v_store_meal_opportunity.avg_bill_uplift_scenario) AS meal_uplift_scenario
FROM analytics.v_store_meal_opportunity
WHERE v_store_meal_opportunity.bill_count >= 500
GROUP BY v_store_meal_opportunity.store_code
), zero_bill AS (
SELECT v_zero_received_detail.store_code,
count(*) AS zero_received_bills,
count(*) FILTER (WHERE v_zero_received_detail.zero_received_type = '优惠不足但无实收'::text) AS unexplained_zero_bills
FROM analytics.v_zero_received_detail
GROUP BY v_zero_received_detail.store_code
)
SELECT a.store_code,
a.store_name,
a.management_quadrant,
a.risk_level,
a.primary_issue,
a.received,
a.avg_daily_received,
a.avg_bill_value,
a.discount_rate_pct,
a.theoretical_margin_pct,
a.anomaly_rate_pct,
a.member_bill_share_pct,
a.discount_saving_to_company_avg,
a.profit_uplift_to_company_margin,
a.top_category,
a.top_category_share_pct,
a.delivery_package_share_pct,
a.meituan_cost_rate_pct,
a.taobao_cost_rate_pct,
a.jd_cost_rate_pct,
round(COALESCE(m.meal_uplift_scenario, 0::numeric), 2) AS meal_uplift_scenario,
mo.member_share_pct,
mo.conversion_bill_scenario,
mo.revenue_uplift_scenario AS member_revenue_uplift_scenario,
COALESCE(z.zero_received_bills, 0::bigint) AS zero_received_bills,
COALESCE(z.unexplained_zero_bills, 0::bigint) AS unexplained_zero_bills
FROM analytics.v_store_action_list a
LEFT JOIN meal m USING (store_code)
LEFT JOIN analytics.v_store_member_opportunity mo USING (store_code, store_name)
LEFT JOIN zero_bill z USING (store_code)
WITH DATA;
CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_execution_priority_store ON analytics.v_store_execution_priority (store_code);
-- ============================================================
-- Step 4: 重建依赖普通视图(基于物化视图,查询会走索引)
-- ============================================================
-- 4a. v_store_benchmark_composite
CREATE OR REPLACE VIEW analytics.v_store_benchmark_composite AS
WITH eligible AS (
SELECT b.store_code,
b.store_name,
b.received,
b.avg_daily_received,
b.avg_bill_value,
b.discount_rate_pct,
b.theoretical_margin_pct,
b.anomaly_rate_pct,
b.member_bill_share_pct,
r.identified_members,
r.repeat_rate_pct,
r.avg_orders,
r.repeat_revenue_share_pct,
p.meituan_cost_rate_pct,
p.taobao_cost_rate_pct,
p.jd_cost_rate_pct
FROM analytics.v_store_benchmark b
JOIN analytics.v_store_repeat_summary_monthly r ON r.store_code = b.store_code AND r.month_start = '2026-04-01'::date
LEFT JOIN analytics.v_store_platform_economics p ON p.store_code = b.store_code
WHERE b.theoretical_margin_pct >= 60::numeric AND b.theoretical_margin_pct <= 80::numeric AND r.identified_members >= 500
), scored AS (
SELECT eligible.store_code,
eligible.store_name,
eligible.received,
eligible.avg_daily_received,
eligible.avg_bill_value,
eligible.discount_rate_pct,
eligible.theoretical_margin_pct,
eligible.anomaly_rate_pct,
eligible.member_bill_share_pct,
eligible.identified_members,
eligible.repeat_rate_pct,
eligible.avg_orders,
eligible.repeat_revenue_share_pct,
eligible.meituan_cost_rate_pct,
eligible.taobao_cost_rate_pct,
eligible.jd_cost_rate_pct,
percent_rank() OVER (ORDER BY eligible.avg_daily_received) AS revenue_score,
percent_rank() OVER (ORDER BY eligible.theoretical_margin_pct) AS margin_score,
1::double precision - percent_rank() OVER (ORDER BY eligible.discount_rate_pct) AS discount_score,
1::double precision - percent_rank() OVER (ORDER BY eligible.anomaly_rate_pct) AS anomaly_score,
percent_rank() OVER (ORDER BY eligible.repeat_rate_pct) AS repeat_score
FROM eligible
)
SELECT scored.store_code,
scored.store_name,
scored.received,
scored.avg_daily_received,
scored.avg_bill_value,
scored.discount_rate_pct,
scored.theoretical_margin_pct,
scored.anomaly_rate_pct,
scored.member_bill_share_pct,
scored.identified_members,
scored.repeat_rate_pct,
scored.avg_orders,
scored.repeat_revenue_share_pct,
scored.meituan_cost_rate_pct,
scored.taobao_cost_rate_pct,
scored.jd_cost_rate_pct,
scored.revenue_score,
scored.margin_score,
scored.discount_score,
scored.anomaly_score,
scored.repeat_score,
round(((scored.revenue_score * 0.25::double precision + scored.margin_score * 0.25::double precision + scored.discount_score * 0.20::double precision + scored.anomaly_score * 0.15::double precision + scored.repeat_score * 0.15::double precision) * 100::double precision)::numeric, 2) AS benchmark_score
FROM scored;
-- 4b. v_store_deep_diagnosis_april
CREATE OR REPLACE VIEW analytics.v_store_deep_diagnosis_april AS
WITH store_base AS (
SELECT s.store_code,
s.store_name,
s.bill_count,
s.active_days,
s.received,
s.avg_daily_received,
s.avg_bill_value,
s.discount_rate_pct,
s.theoretical_margin_pct,
s.member_bill_share_pct,
d.items_per_bill,
d.skus_per_bill,
d.delivery_bill_share_pct,
d.noodle_snack_attach_pct,
d.noodle_drink_attach_pct,
d.noodle_cold_attach_pct,
d.combo_bill_share_pct,
c.theoretical_cost,
c.actual_food_cost,
c.food_cost_variance,
c.theoretical_cost_rate_pct,
c.actual_food_cost_rate_pct,
c.variance_to_theoretical_pct,
c.comparison_status,
c.variance_level,
b.identified_members,
b.repeat_rate_pct,
b.repeat_revenue_share_pct,
b.benchmark_score,
p.meituan_received,
p.taobao_received,
p.jd_received,
round((COALESCE(p.meituan_discount, 0::numeric) + COALESCE(p.taobao_discount, 0::numeric) + COALESCE(p.jd_discount, 0::numeric) + COALESCE(p.meituan_commission, 0::numeric) + COALESCE(p.taobao_commission, 0::numeric) + COALESCE(p.jd_commission, 0::numeric)) / NULLIF(COALESCE(p.meituan_received, 0::numeric) + COALESCE(p.taobao_received, 0::numeric) + COALESCE(p.jd_received, 0::numeric) + COALESCE(p.meituan_discount, 0::numeric) + COALESCE(p.taobao_discount, 0::numeric) + COALESCE(p.jd_discount, 0::numeric) + COALESCE(p.meituan_commission, 0::numeric) + COALESCE(p.taobao_commission, 0::numeric) + COALESCE(p.jd_commission, 0::numeric), 0::numeric) * 100::numeric, 2) AS combined_platform_cost_rate_pct,
CASE
WHEN s.store_name ~ '机场|火锅|商城|快手|哈马尔罕'::text THEN '特殊业态'::text
ELSE '标准门店'::text
END AS business_type
FROM analytics.v_store_scorecard s
LEFT JOIN analytics.dish_store_summary_april d USING (store_code)
LEFT JOIN analytics.v_store_theoretical_actual_cost_april c USING (store_code)
LEFT JOIN analytics.v_store_benchmark_composite b USING (store_code)
LEFT JOIN analytics.v_store_platform_economics p USING (store_code)
), tiered AS (
SELECT store_base.store_code,
store_base.store_name,
store_base.bill_count,
store_base.active_days,
store_base.received,
store_base.avg_daily_received,
store_base.avg_bill_value,
store_base.discount_rate_pct,
store_base.theoretical_margin_pct,
store_base.member_bill_share_pct,
store_base.items_per_bill,
store_base.skus_per_bill,
store_base.delivery_bill_share_pct,
store_base.noodle_snack_attach_pct,
store_base.noodle_drink_attach_pct,
store_base.noodle_cold_attach_pct,
store_base.combo_bill_share_pct,
store_base.theoretical_cost,
store_base.actual_food_cost,
store_base.food_cost_variance,
store_base.theoretical_cost_rate_pct,
store_base.actual_food_cost_rate_pct,
store_base.variance_to_theoretical_pct,
store_base.comparison_status,
store_base.variance_level,
store_base.identified_members,
store_base.repeat_rate_pct,
store_base.repeat_revenue_share_pct,
store_base.benchmark_score,
store_base.meituan_received,
store_base.taobao_received,
store_base.jd_received,
store_base.combined_platform_cost_rate_pct,
store_base.business_type,
CASE
WHEN store_base.business_type = '特殊业态'::text THEN '特殊业态'::text
WHEN percent_rank() OVER (PARTITION BY store_base.business_type ORDER BY store_base.received) >= 0.67::double precision THEN '高规模'::text
WHEN percent_rank() OVER (PARTITION BY store_base.business_type ORDER BY store_base.received) >= 0.33::double precision THEN '中规模'::text
ELSE '低规模'::text
END AS scale_tier
FROM store_base
)
SELECT tiered.store_code,
tiered.store_name,
tiered.bill_count,
tiered.active_days,
tiered.received,
tiered.avg_daily_received,
tiered.avg_bill_value,
tiered.discount_rate_pct,
tiered.theoretical_margin_pct,
tiered.member_bill_share_pct,
tiered.items_per_bill,
tiered.skus_per_bill,
tiered.delivery_bill_share_pct,
tiered.noodle_snack_attach_pct,
tiered.noodle_drink_attach_pct,
tiered.noodle_cold_attach_pct,
tiered.combo_bill_share_pct,
tiered.theoretical_cost,
tiered.actual_food_cost,
tiered.food_cost_variance,
tiered.theoretical_cost_rate_pct,
tiered.actual_food_cost_rate_pct,
tiered.variance_to_theoretical_pct,
tiered.comparison_status,
tiered.variance_level,
tiered.identified_members,
tiered.repeat_rate_pct,
tiered.repeat_revenue_share_pct,
tiered.benchmark_score,
tiered.meituan_received,
tiered.taobao_received,
tiered.jd_received,
tiered.combined_platform_cost_rate_pct,
tiered.business_type,
tiered.scale_tier,
CASE
WHEN tiered.comparison_status = '可比'::text AND tiered.variance_to_theoretical_pct >= 20::numeric THEN 1
ELSE 0
END +
CASE
WHEN tiered.discount_rate_pct >= 23.02 THEN 1
ELSE 0
END +
CASE
WHEN tiered.theoretical_margin_pct < 70::numeric THEN 1
ELSE 0
END +
CASE
WHEN tiered.identified_members >= 500 AND tiered.repeat_rate_pct < 30::numeric THEN 1
ELSE 0
END +
CASE
WHEN tiered.combined_platform_cost_rate_pct >= 40::numeric THEN 1
ELSE 0
END +
CASE
WHEN tiered.noodle_drink_attach_pct < 12::numeric THEN 1
ELSE 0
END AS problem_count,
concat_ws(''::text,
CASE
WHEN tiered.comparison_status = '理论成本口径异常'::text THEN '成本口径异常'::text
ELSE NULL::text
END,
CASE
WHEN tiered.comparison_status = '可比'::text AND tiered.variance_to_theoretical_pct >= 20::numeric THEN '实际成本严重超耗'::text
ELSE NULL::text
END,
CASE
WHEN tiered.discount_rate_pct >= 23.02 THEN '优惠偏高'::text
ELSE NULL::text
END,
CASE
WHEN tiered.theoretical_margin_pct < 70::numeric THEN '理论毛利偏低'::text
ELSE NULL::text
END,
CASE
WHEN tiered.identified_members >= 500 AND tiered.repeat_rate_pct < 30::numeric THEN '会员复购偏低'::text
ELSE NULL::text
END,
CASE
WHEN tiered.combined_platform_cost_rate_pct >= 40::numeric THEN '平台成本偏高'::text
ELSE NULL::text
END,
CASE
WHEN tiered.noodle_drink_attach_pct < 12::numeric THEN '饮品搭售偏低'::text
ELSE NULL::text
END) AS problem_combination
FROM tiered;
-- ============================================================
-- Step 5: 验证
-- ============================================================
SELECT 'Materialized views created successfully' AS status;
+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
@@ -0,0 +1,215 @@
# 基于全部现有数据的门店选址分析报告
> 分析期:2026年4月
> 数据范围:91家经营门店,其中90家实体店完成精确地理编码;结合账单、菜品、会员复购、平台、成本、库存、面积、开业日期、租约和地址信息。
## 一、结论先行
现有数据能够解决新店选址中的四个核心问题:
1. **开什么店型**:办公、社区、商场、街边或小型高效店。
2. **多大面积更合适**:面积越大,绝对收入通常越高,但坪效明显下降;不能为了形象盲目拿大铺。
3. **应该复制哪些门店经验**:按场景、面积、经营质量寻找3—5家相似标杆,而不是简单照搬销售额最高的门店。
4. **是否会分流现有门店**:利用坐标计算与现店距离,初步识别1—3公里范围内的重叠风险。
但内部经营数据不能单独回答“某个具体铺位一定能不能开”。最终决策必须补充候选点客流、租金、竞品、人口、办公人数、配送覆盖、座位和投资数据。
## 二、现有成熟店经营基准
85家正常销售标准门店的4月基准:
| 指标 | P25 | 中位数 | P75 |
|---|---:|---:|---:|
| 月实收 | 47.55万元 | 60.94万元 | 79.91万元 |
其他中位指标:日均实收1.97万元、客单价33.38元、优惠率20.51%、复购率33.60%、实际成本率28.12%、平台合并加权成本率37.67%。
因此,新店成熟后的月实收可暂设三个情景:
- 保守情景:约47.5万元/月;
- 基准情景:约60.9万元/月;
- 较好情景:约79.9万元/月。
这只是成熟店参照。当前只有2026年4月一个完整经营月,尚不能可靠估计淡旺季和新店爬坡速度。
## 三、面积如何选择
| 面积段 | 门店数 | 平均面积 | 平均月实收 | 月实收中位数 | 平均月坪效 |
|---|---:|---:|---:|---:|---:|
| ≤180㎡ | 8 | 141㎡ | 46.66万元 | 47.43万元 | 3,281元/㎡ |
| 181—250㎡ | 23 | 219㎡ | 56.54万元 | 54.42万元 | 2,603元/㎡ |
| 251—350㎡ | 26 | 298㎡ | 60.04万元 | 55.68万元 | 2,025元/㎡ |
| 351—500㎡ | 12 | 418㎡ | 81.32万元 | 83.03万元 | 1,945元/㎡ |
| >500㎡ | 12 | 615㎡ | 88.78万元 | 73.92万元 | 1,464元/㎡ |
经营含义:
- 180㎡以下适合做“小面积、高周转、高坪效”模型,但收入天花板相对较低。
- 181—250㎡是较稳健的轻量标准店区间,兼顾收入和坪效。
- 251—350㎡适合堂食更完整的标准门店,但必须有足够客流支撑。
- 350㎡以上虽然平均销售更高,但坪效下降明显,必须验证座位利用率、包间需求和租金条件。
- 500㎡以上不应作为默认开店规格,只适用于已经验证的强商圈、旗舰或特殊经营需求。
面积决策不能只看坪效,还要核算后厨面积、座位数、翻台率、人工和装修投入。
## 四、场景表现及可复制方向
| 场景 | 样本数 | 平均月实收 | 月实收中位数 | 平均月坪效 | 平均复购率 |
|---|---:|---:|---:|---:|---:|
| 办公园区 | 8 | 82.32万元 | 72.61万元 | 2,213元/㎡ | 50.2% |
| 商场商业体 | 4 | 71.49万元 | 73.09万元 | 2,242元/㎡ | 37.6% |
| 社区居民 | 13 | 71.61万元 | 64.16万元 | 2,371元/㎡ | 28.1% |
| 街边综合 | 56 | 60.73万元 | 59.26万元 | 2,182元/㎡ | 37.2% |
方向判断:
- **办公园区**:收入和复购表现突出,可重点研究稳定就业人口、工作日午餐刚需和企业团餐机会。但需核查周末及晚餐空档。
- **社区居民**:坪效较好,但当前复购率偏低于办公和街边样本,选址时要重点验证常住人口、家庭结构、晚餐和周末需求。
- **商场商业体**:现有样本只有4家,不能仅凭均值扩大结论;必须核算扣点、推广费、营业时间和同层竞品。
- **街边综合**:样本最多、差异也最大,必须进一步按道路等级、周边功能和面积匹配标杆。
场景分类目前主要由门店地址文本推断,只适合作为第一轮分组,后续应通过现场和地图数据校正。
## 五、可提炼的门店原型
综合坪效、日均销售、复购、优惠纪律、实际成本、平台成本和执行问题后,当前靠前样本包括:
| 门店 | 场景 | 面积 | 4月实收 | 月坪效 | 最近现店距离 | 复制评分 |
|---|---|---:|---:|---:|---:|---:|
| 三里河店 | 街边综合 | 155㎡ | 90.91万元 | 5,865元/㎡ | 0.99km | 85.98 |
| 为公桥店 | 街边综合 | 400㎡ | 141.59万元 | 3,540元/㎡ | 0.57km | 81.73 |
| 航天桥店 | 街边综合 | 198㎡ | 94.22万元 | 4,759元/㎡ | 0.57km | 80.35 |
| 联想桥店 | 街边综合 | 200㎡ | 81.80万元 | 4,090元/㎡ | 0.75km | 80.13 |
| 七里庄店 | 街边综合 | 204㎡ | 90.91万元 | 4,463元/㎡ | 1.06km | 76.46 |
| 天秀路店 | 办公园区 | 295㎡ | 95.46万元 | 3,236元/㎡ | 3.07km | 76.10 |
建议提炼三类原型:
1. **150—220㎡高效街边店**:三里河、航天桥、联想桥、七里庄。适合租金较高、客流明确、强调高周转的铺位。
2. **250—350㎡办公园区标准店**:天秀路等。重点复制工作日午餐、会员复购和团餐能力。
3. **350—500㎡强商圈大店**:为公桥等。只在收入确定性高、租金可承受时采用。
“原型可复制”不等于可以在原店旁边继续开店。为公桥—航天桥等距离很近,其高业绩可能来自商圈总容量、门店定位差异或已有分流,必须另行研究顾客来源。
## 六、空间重叠风险
90家实体经营门店的两两距离结果:
| 距离范围 | 门店对数 | 初步管理含义 |
|---|---:|---|
| <1公里 | 22 | 高度重叠,新增门店必须做顾客来源和配送圈验证 |
| 1—2公里 | 57 | 较高重叠,检查道路阻隔、商圈边界和客群差异 |
| 2—3公里 | 64 | 观察区,不能只用直线距离判断 |
| ≥3公里 | 3,862 | 相对独立,但不代表区域具备新店需求 |
建议把距离作为否决前的预警,而不是单独的开店规则:
- 北京高密度城区,1—3公里可能跨越多个独立商圈;
- 道路、河流、地铁、园区门禁和商场动线会改变实际可达性;
- 外卖配送圈重叠可能比堂食商圈重叠更严重;
- 距离近且现店经营承压时,应优先改善现店或迁址,不宜继续加密。
## 七、如果开新门店,具体怎么做
### 第一步:先定店型,不先找铺
由运营明确目标模型:办公午餐店、社区全天店、街边标准店、商场店、小型高效店或外卖补充店。同步确定目标客单、堂食/外卖结构、面积、座位和营业餐段。
### 第二步:为候选点匹配3—5家相似老店
匹配顺序:场景相同 → 面积相近 → 城市和商圈等级接近 → 楼层相近 → 外卖结构相近。不得直接拿全公司平均数作为唯一预算依据。
输出相似店的:月实收、日均实收、午晚餐结构、客单、单均件数、复购率、优惠率、外送占比、成本率、平台成本、坪效和问题数。
### 第三步:建立三情景收入预算
以相似店P25、中位数、P75或公司成熟店47.5万、60.9万、79.9万元为起点,再按候选点客流和现场条件修正。
收入预算应拆成:
`月销售 = 午市日均单量 × 午市客单 + 晚市日均单量 × 晚市客单 + 其他餐段 + 外卖净增量`,再乘营业天数。
外卖不能全部当作新增收入,需要扣除与堂食及现有店配送圈的重叠。
### 第四步:用面积和坪效做交叉校验
例如计划开220㎡门店,若按基准情景60.9万元计算,月坪效约2,770元/㎡,高于181—250㎡现店平均2,603元/㎡,属于略有挑战但可解释的目标。若面积500㎡、预算仍只有60万元,则坪效约1,200元/㎡,应优先缩面积或放弃。
### 第五步:计算盈亏平衡和租金上限
完整模型:
`门店贡献 = 实收 - 食材成本 - 平台及支付成本 - 人工 - 租金物业 - 水电能耗 - 其他可变费用`
`盈亏平衡销售额 = 固定成本 ÷ 综合贡献毛利率`
租金上限不能用统一比例粗暴决定,应在保守收入情景下反推:
`可承受租金物业 = 保守实收 - 食材 - 平台 - 人工 - 水电 - 其他费用 - 目标利润`
只有基准或较好情景盈利、保守情景严重亏损的项目,应谨慎立项。
### 第六步:检查现店分流
列出候选点1公里、2公里、3公里内所有现店,比较:堂食商圈、外卖配送圈、地铁出口、道路阻隔、顾客来源和餐段。若分流不可避免,预算必须以“区域总增量”而非“新店销售额”评价。
### 第七步:连续7天现场验证
至少覆盖2个工作日午市、2个工作日晚市、周五晚市、周六和周日;每30分钟记录:
- 经过人数和进入餐饮门店人数;
- 同类竞品进店人数、排队和翻台;
- 外卖骑手取餐量;
- 停车、地铁、公交、步行可达性;
- 门头可见性、上下楼障碍和反向动线;
- 办公园区周末、社区工作日午间等弱势时段。
开发提供铺位条件,运营负责客流和竞品判断,财务审核预算和投资回收期,三方共同签字。
## 八、候选点100分打分卡
| 模块 | 权重 | 主要依据 |
|---|---:|---|
| 需求与有效客流 | 25 | 分时客流、办公/常住人口、餐饮转化 |
| 与成功原型匹配度 | 15 | 场景、面积、楼层、餐段结构 |
| 销售预测可信度 | 15 | 相似店、现场数客、三情景预测 |
| 盈利与租金承受力 | 20 | 保守情景利润、盈亏平衡、投资回收 |
| 竞争与品牌空白 | 10 | 同类数量、价格带、竞品经营状态 |
| 与现店重叠风险 | 10 | 1—3公里门店、配送圈、客群分流 |
| 物业与经营条件 | 5 | 排烟、燃气、电力、消防、门头、证照 |
建议设置红线:总分低于70不立项;盈利模块或有效客流模块低于及格线直接否决;数据缺失不得用主观高分填补。
## 九、现阶段还缺什么数据
优先补齐:
1. 每家店租金、物业费、人工、水电、装修及设备投资;
2. 座位数、前厅/后厨面积、营业时间和翻台率;
3. 候选点分时客流、周边办公人数和常住人口;
4. 三公里内竞品名称、品类、客单、销量或繁忙度;
5. 外卖订单的收货网格或脱敏坐标,用于真实配送热力和分流判断;
6. 会员居住/工作网格或消费商圈,不保留直接身份信息;
7. 至少12—24个月连续经营数据,用于季节性、趋势和新店爬坡曲线;
8. 新店筹建、开业日期、开业营销费用及月度损益,用于回测选址模型。
## 十、管理建议
短期不要直接做“自动推荐地址”,而应先建立候选点审批表:每个候选点录入坐标、面积、楼层、租金、客流、竞品和物业条件,系统自动匹配相似店、计算距离、生成三情景预算和100分评分。
每开一家店,应保存立项预测和开业后1、3、6、12个月结果,持续对比“预测与实际”。经过一批新店回测后,才能把当前经验评分升级为真正可验证的选址模型。
## 十一、配套SQL
本报告对应SQL`连锁门店选址分析.sql`
已建立视图:
- `analytics.v_store_site_profile_april`
- `analytics.v_store_spatial_pairs_april`
- `analytics.v_store_nearest_neighbor_april`
- `analytics.v_site_segment_benchmark_april`
- `analytics.v_district_site_benchmark_april`
- `analytics.v_store_site_replication_score_april`
- `analytics.v_store_location_overlap_risk_april`
+190
View File
@@ -0,0 +1,190 @@
-- 连锁餐饮门店选址分析
-- 基准期:2026年4月
-- 数据:经营、菜品、会员、平台、成本、库存、面积、店龄、租约及精确坐标
CREATE OR REPLACE VIEW analytics.v_store_site_profile_april AS
SELECT a.*,
CASE
WHEN a.business_type='特殊业态' THEN '特殊业态'
WHEN a.business_address ~ '机场|航站楼' THEN '交通枢纽'
WHEN a.business_address ~ '大学|食堂|档口' THEN '校园档口'
WHEN a.business_address ~ '总部|科技园|产业园|创业园|商务楼|写字楼|信息产业基地|生命科学园|自贸试验区|经海|荣华' THEN '办公园区'
WHEN a.business_address ~ '商场|商城|购物|超市|万科|龙湖|大悦|搜秀|美食城|商业大厦|商铺' THEN '商场商业体'
WHEN a.business_address ~ '社区|小区|家园|里|园一区|园东街' THEN '社区居民'
ELSE '街边综合'
END AS site_scene,
CASE
WHEN a.business_address ~ '地下一层|负一层|-1层|-1至|B1|b1' THEN '地下层'
WHEN a.business_address ~ '二层|2层|四层|4层|4F|五层|5层|23层' THEN '非首层'
WHEN a.business_address ~ '一层|1层|底商' THEN '首层'
ELSE '楼层不明'
END AS floor_type,
CASE
WHEN a.area_sqm IS NULL THEN '面积缺失'
WHEN a.area_sqm<=180 THEN '≤180㎡'
WHEN a.area_sqm<=250 THEN '181-250㎡'
WHEN a.area_sqm<=350 THEN '251-350㎡'
WHEN a.area_sqm<=500 THEN '351-500㎡'
ELSE '>500㎡'
END AS area_band,
CASE
WHEN a.store_age_years IS NULL THEN '店龄缺失'
WHEN a.store_age_years<1 THEN '新店<1年'
WHEN a.store_age_years<3 THEN '成长期1-3年'
WHEN a.store_age_years<8 THEN '成熟期3-8年'
ELSE '老店≥8年'
END AS age_band
FROM analytics.v_store_area_efficiency_april a;
CREATE OR REPLACE VIEW analytics.v_store_spatial_pairs_april AS
WITH physical AS (
SELECT store_code,store_name,district,site_scene,action_priority,received,
monthly_received_per_sqm,repeat_rate_pct,actual_food_cost_rate_pct,
latitude_gcj02 AS lat,longitude_gcj02 AS lon
FROM analytics.v_store_site_profile_april
WHERE latitude_gcj02 IS NOT NULL AND longitude_gcj02 IS NOT NULL
), pairs AS (
SELECT a.store_code AS store_code_a,a.store_name AS store_name_a,
b.store_code AS store_code_b,b.store_name AS store_name_b,
a.district AS district_a,b.district AS district_b,
a.site_scene AS scene_a,b.site_scene AS scene_b,
a.action_priority AS priority_a,b.action_priority AS priority_b,
a.received AS received_a,b.received AS received_b,
a.monthly_received_per_sqm AS sqm_efficiency_a,
b.monthly_received_per_sqm AS sqm_efficiency_b,
6371 * acos(least(1.0,greatest(-1.0,
cos(radians(a.lat))*cos(radians(b.lat))*cos(radians(b.lon-a.lon))+
sin(radians(a.lat))*sin(radians(b.lat))
))) AS distance_km
FROM physical a
JOIN physical b ON a.store_code<b.store_code
)
SELECT *,
CASE WHEN distance_km<1 THEN '高度重叠<1km'
WHEN distance_km<2 THEN '较高重叠1-2km'
WHEN distance_km<3 THEN '观察2-3km'
ELSE '相对独立≥3km' END AS proximity_level
FROM pairs;
CREATE OR REPLACE VIEW analytics.v_store_nearest_neighbor_april AS
WITH directed AS (
SELECT store_code_a AS store_code,store_name_a AS store_name,
store_code_b AS nearest_store_code,store_name_b AS nearest_store_name,
distance_km
FROM analytics.v_store_spatial_pairs_april
UNION ALL
SELECT store_code_b,store_name_b,store_code_a,store_name_a,distance_km
FROM analytics.v_store_spatial_pairs_april
), ranked AS (
SELECT *,row_number() OVER(PARTITION BY store_code ORDER BY distance_km) AS rn
FROM directed
)
SELECT store_code,store_name,nearest_store_code,nearest_store_name,
round(distance_km::numeric,2) AS nearest_distance_km,
CASE WHEN distance_km<1 THEN '高度重叠<1km'
WHEN distance_km<2 THEN '较高重叠1-2km'
WHEN distance_km<3 THEN '观察2-3km'
ELSE '相对独立≥3km' END AS nearest_proximity_level
FROM ranked WHERE rn=1;
CREATE OR REPLACE VIEW analytics.v_site_segment_benchmark_april AS
SELECT site_scene,area_band,count(*) AS store_count,
round(avg(area_sqm)::numeric,1) AS avg_area_sqm,
round(avg(received)::numeric,2) AS avg_received,
round(percentile_cont(0.5) WITHIN GROUP(ORDER BY received)::numeric,2) AS median_received,
round(avg(monthly_received_per_sqm)::numeric,2) AS avg_received_per_sqm,
round(percentile_cont(0.5) WITHIN GROUP(ORDER BY monthly_received_per_sqm)::numeric,2) AS median_received_per_sqm,
round(avg(avg_bill_value)::numeric,2) AS avg_bill_value,
round(avg(discount_rate_pct)::numeric,2) AS avg_discount_rate_pct,
round(avg(repeat_rate_pct)::numeric,2) AS avg_repeat_rate_pct,
round(avg(actual_food_cost_rate_pct) FILTER(WHERE comparison_status='可比')::numeric,2) AS avg_actual_cost_rate_pct,
round(avg(delivery_bill_share_pct)::numeric,2) AS avg_delivery_share_pct,
round(avg(noodle_drink_attach_pct)::numeric,2) AS avg_drink_attach_pct
FROM analytics.v_store_site_profile_april
WHERE business_type='标准门店' AND received>0 AND area_sqm IS NOT NULL
GROUP BY site_scene,area_band;
CREATE OR REPLACE VIEW analytics.v_district_site_benchmark_april AS
SELECT city,district,count(*) AS store_count,
round(avg(area_sqm)::numeric,1) AS avg_area_sqm,
round(sum(received)::numeric,2) AS total_received,
round(avg(received)::numeric,2) AS avg_received,
round(percentile_cont(0.5) WITHIN GROUP(ORDER BY received)::numeric,2) AS median_received,
round(avg(monthly_received_per_sqm)::numeric,2) AS avg_received_per_sqm,
round(avg(avg_bill_value)::numeric,2) AS avg_bill_value,
round(avg(discount_rate_pct)::numeric,2) AS avg_discount_rate_pct,
round(avg(repeat_rate_pct)::numeric,2) AS avg_repeat_rate_pct,
round(avg(actual_food_cost_rate_pct) FILTER(WHERE comparison_status='可比')::numeric,2) AS avg_actual_cost_rate_pct,
round(avg(combined_platform_cost_rate_pct)::numeric,2) AS avg_platform_cost_rate_pct,
count(*) FILTER(WHERE action_priority LIKE 'P0%') AS p0_count,
count(*) FILTER(WHERE action_priority='P1-重点整改') AS p1_count
FROM analytics.v_store_site_profile_april
WHERE business_type='标准门店' AND received>0
GROUP BY city,district;
CREATE OR REPLACE VIEW analytics.v_store_site_replication_score_april AS
WITH eligible AS (
SELECT p.*,n.nearest_store_code,n.nearest_store_name,n.nearest_distance_km,
percent_rank() OVER(ORDER BY p.monthly_received_per_sqm) AS sqm_score,
percent_rank() OVER(ORDER BY p.avg_daily_received) AS daily_score,
percent_rank() OVER(ORDER BY p.repeat_rate_pct NULLS FIRST) AS repeat_score,
1-percent_rank() OVER(ORDER BY p.discount_rate_pct) AS discount_score,
1-percent_rank() OVER(ORDER BY p.actual_food_cost_rate_pct NULLS LAST) AS cost_score,
1-percent_rank() OVER(ORDER BY p.combined_platform_cost_rate_pct NULLS LAST) AS platform_score,
greatest(0,1-p.problem_count/6.0) AS execution_score
FROM analytics.v_store_site_profile_april p
LEFT JOIN analytics.v_store_nearest_neighbor_april n USING(store_code,store_name)
WHERE p.business_type='标准门店' AND p.received>0 AND p.area_sqm IS NOT NULL
), scored AS (
SELECT *,round((
0.30*sqm_score+0.20*daily_score+0.15*repeat_score+
0.10*discount_score+0.10*cost_score+0.10*platform_score+
0.05*execution_score
)::numeric*100,2) AS site_replication_score
FROM eligible
)
SELECT *,
CASE
WHEN site_replication_score>=75 AND problem_count<=1 THEN '优先提炼选址原型'
WHEN site_replication_score>=60 THEN '可作为同类参考'
WHEN site_replication_score<40 THEN '不宜作为选址标杆'
ELSE '观察验证'
END AS replication_recommendation,
CASE
WHEN nearest_distance_km>=3 AND site_replication_score>=70 THEN '高表现且周边相对独立,可研究相似商圈扩张'
WHEN nearest_distance_km<1.5 THEN '邻店较近,新址需重点防止同店分流'
ELSE '常规评估'
END AS spatial_recommendation
FROM scored;
CREATE OR REPLACE VIEW analytics.v_store_location_overlap_risk_april AS
SELECT p.*,
a.problem_count AS problem_count_a,b.problem_count AS problem_count_b,
CASE
WHEN p.distance_km<1 AND (a.action_priority LIKE 'P0%' OR b.action_priority LIKE 'P0%' OR a.action_priority='P1-重点整改' OR b.action_priority='P1-重点整改')
THEN '高风险:距离近且至少一家经营承压'
WHEN p.distance_km<1.5 THEN '中风险:需核查客群和配送圈重叠'
ELSE '观察'
END AS overlap_risk
FROM analytics.v_store_spatial_pairs_april p
JOIN analytics.v_store_site_profile_april a ON a.store_code=p.store_code_a
JOIN analytics.v_store_site_profile_april b ON b.store_code=p.store_code_b
WHERE p.distance_km<3;
-- 典型结果查询
SELECT site_scene,count(*) store_count,round(avg(received)::numeric,0) avg_received,
round(avg(monthly_received_per_sqm)::numeric,2) avg_received_per_sqm
FROM analytics.v_store_site_profile_april
WHERE business_type='标准门店' AND received>0 AND area_sqm IS NOT NULL
GROUP BY site_scene ORDER BY avg_received_per_sqm DESC;
SELECT store_name,site_scene,area_sqm,received,monthly_received_per_sqm,
nearest_store_name,nearest_distance_km,site_replication_score,
replication_recommendation,spatial_recommendation
FROM analytics.v_store_site_replication_score_april
ORDER BY site_replication_score DESC LIMIT 20;
SELECT store_name_a,store_name_b,round(distance_km::numeric,2) distance_km,
received_a,received_b,overlap_risk
FROM analytics.v_store_location_overlap_risk_april
ORDER BY distance_km LIMIT 30;
@@ -0,0 +1,278 @@
# 餐易通 × 收钱吧:从战略全局看融合价值
> 日期:2026年7月
---
## 合作概要
**合作定位**:收钱吧提供支付底座、商户渠道和线下服务网络;餐易通提供智能经营系统与餐饮产业资源,共同打造"支付 + 智能经营"的一体化平台。
**收钱吧价值**:作为线下聚合支付领军者,坐拥海量商户渠道资源,拥有稳定可靠的支付交易链路与成熟的智能硬件终端,依托强大的线下地推能力实现市场的广泛覆盖与深度渗透。核心营收来自支付手续费、SaaS增值订阅、智能硬件进销、金融服务导流及营销生态收益。
**餐易通能力**:以五大智能体系(利润诊断、AI客流预测、智能排班、数字化经营闭环、增值服务生态)为软件内核,以股东30余年餐饮经营、20余年供应链/食品加工/人力资源/系统开发的产业积淀为资源底座,提供从成本诊断到供应链集采、从客流预测到灵活用工、从标准化SOP到金融赋能的全链路经营支持,目标是由收银工具升级为门店"盈利引擎"。
**标杆项目**:以"麦鲜面"为首个合作案例,规划从2027年北京30+家门店,扩张至2029年全球2000+家门店,并支持国内及东南亚支付体系。
**资金建议**:一期系统集成与定制开发预算300万元;二期迭代研发和联合推广预算500万元,建议由华夏银行、收钱吧和餐易通共同承担。
**预期收益**:提升商户留存与付费意愿,拓宽SaaS、金融、供应链、硬件及支付分润等收入来源,并形成双方联合运营和收益共享机制。
---
## 一、行业本质问题:餐饮数字化的"半截子"困境
中国餐饮数字化目前分两层,且两层之间严重断裂:
- **底层基础设施**(收钱吧):支付、硬件、地推——已经铺到位,渗透率极高,但**天花板已现**。支付费率内卷、商户流失率高、增值服务停留在"点餐+会员"浅水区。功能应用浅层化,仅满足收款、接单等基础功能,缺乏深度的经营分析与智能运营模型;数据停留在交易流水层面,未能转化为驱动决策的商业价值与洞察;增值服务品类单一,难以形成深度绑定,商户长期增收与升级空间受限。
- **上层经营智能**(餐易通):成本诊断、客流预测、排班优化、利润管理——真正决定商户"能不能活下去、能不能扩张"的能力,但**缺乏渠道和入口**,触达效率低。
两层之间**断裂**:支付数据没有向上转化为经营决策,经营分析没有向下触达交易终端。结果是——商户用了支付工具还在亏钱,用了分析工具却落不到每天的操作里。
**餐易通 × 收钱吧的本质,是打通这个断裂层。**
---
## 二、战略定位:不是"合作",是"重新定义赛道"
这不是两个产品的功能叠加,而是**重新定义餐饮SaaS的价值坐标**:
| 维度 | 传统模式 | 融合后 |
|---|---|---|
| **价值锚点** | 交易规模(GMV) | 经营利润(可控净利润) |
| **数据深度** | 支付流水 | 从支付→菜品→成本→人员→利润全链路 |
| **商户关系** | 工具提供方(可替换) | 经营合伙人(深度绑定) |
| **竞争维度** | 费率、硬件、地推人数 | 数据智能+产业理解+生态资源 |
| **收入模型** | 单一支付手续费 | 支付+SaaS+金融+供应链多元分成 |
**核心命题**:从"帮商户收钱"升级为"帮商户赚钱"。
收钱吧解决的是**钱怎么进来**——提供稳固的支付与流量底座;餐易通解决的是**钱怎么留住、怎么多赚**——注入智能经营大脑。两者合一,覆盖了餐饮经营的完整价值链,实现从基础收款到主动盈利的质变。
---
## 三、数据飞轮:融合后的增长引擎
真正的战略壁垒不是某个功能,而是**数据飞轮的自我强化**:
```
收钱吧海量支付入口
→ 沉淀全量交易数据(规模优势)
→ 餐易通经营分析模型加工(智能优势)
→ 输出可执行的经营指令(行动优势)
→ 商户利润提升 → 粘性增强 → 交易量增长
→ 更多数据回流 → 模型更准 → 指令更精准
→ 飞轮加速
```
这个飞轮有三个关键特性:
1. **规模递增**:商户越多,数据越丰富,模型越准,新商户转化越容易
2. **切换成本递增**:商户用的越深(成本管理、排班、会员、供应链),越不可能换支付商
3. **竞争者难以复制**:收钱吧有渠道但建不了分析体系,餐易通有体系但没有渠道——各自都缺另一半,融合后形成"1+1>2"的不可复制壁垒
---
## 四、餐易通为什么能做到:不是软件公司,是餐饮产业公司
餐易通之所以能提供上述经营智能能力,根本原因在于它**不是一家从技术出发的SaaS公司,而是一家从餐饮产业土壤中长出来的数字化平台**。这与纯技术团队做餐饮软件有本质区别。
### 4.1 股东背景:跨越餐饮全产业链的复合积淀
餐易通的股东及核心团队拥有跨越餐饮经营全链路的深厚产业积淀:
- **30余年餐饮经营经验**:深刻理解门店运营痛点,懂需求更懂经营本质。不是从报表里"猜"商户需要什么,而是从后厨、前厅、采购、排班的实战中知道商户每天在为什么发愁。
- **20余年供应链深耕**:打通上游源头渠道,严控食材成本与品质稳定性。理解食材从采购到出品的全过程,知道成本在哪里流失、在哪里可以优化。
- **20余年食品加工经验**:掌握标准化出品核心技术,保障口味统一与出餐效率。理解BOM、出成率、损耗率这些成本管理的关键参数不是理论概念,而是每天在车间里面对的现实。
- **20余年人力资源管理**:深谙餐饮用工规律与管理技巧。理解排班不是排表,而是要在客流峰谷之间平衡人效与员工稳定性。
- **20余年系统开发经验**:将上述产业理解转化为数字化工具,技术与业务场景深度融合,拒绝脱节。
**这种跨领域的复合基因,是餐易通区别于所有纯技术公司的核心壁垒。** 它确保每一个数字化解决方案都能真正"懂餐饮、可落地、见实效"——不是给商户一套漂亮的报表,而是给出一个店长明天就能执行的动作。
### 4.2 新品牌从第一天就按数字化经营模式运营
餐易通不是先做软件再找商户试用,而是**先用自己的品牌验证体系,再向行业输出**。
以旗下核心品牌「麦鲜面」为例:
- **从第一家店起,就按照餐易通数字化体系运营**——菜品结构、成本核算、客流预测、排班优化、会员复购,全部从开业第一天就嵌入日常经营流程。
- **不是"先开店再上系统"**,而是"系统与门店同步生长"。这意味着餐易通的每一个功能模块,都是在真实门店的日常经营中打磨出来的,不是在实验室里设计的。
- **麦鲜面的扩张计划本身就是餐易通体系的规模化验证**——从30家到2000家的跨越,不是靠人盯人,而是靠可复制的标准化经营模型。如果自己的品牌都跑不通,就没有资格给别人的品牌做数字化。
**这解决了餐饮SaaS行业最大的信任问题**:商户不需要听"理论上能帮你赚钱",而是看到"他们自己的品牌就是这么赚钱的"。
### 4.3 股东产业资源 × 数字化平台 = 餐饮企业最核心的运营支持
餐易通的独特之处在于:它不仅提供数字化分析工具,更**将股东背后的产业资源通过平台转化为餐饮企业可直接调用的运营服务**。
这些服务直击餐饮经营最核心的"活下去、活得好"的痛点:
**供应链赋能**
- 股东20余年供应链资源转化为平台采购能力,为商户提供低于市场价的食材集采渠道
- 基于餐易通销售预测数据,实现精准采购和配送,减少商户库存积压和缺货损失
- 从"商户自己找供应商"升级为"平台按需配送",降低采购成本和管理复杂度
**灵活用工支持**
- 股东20余年人力资源经验转化为灵活用工服务,为商户提供峰谷匹配的用工解决方案
- 基于餐易通客流预测和排班模型,精准计算各时段人力需求,按需调配用工
- 从"商户自己招人、养人"升级为"平台按需供人",解决餐饮行业最大的弹性人力难题
**标准化SOP输出**
- 股东30余年餐饮经营和20余年食品加工经验,沉淀为可复制的标准化作业流程
- 从菜品出品标准、盘点流程、报损管理到服务话术,形成完整的门店运营手册
- 新店从开业第一天就有标准可依,而不是靠店长个人经验摸索
**金融赋能**
- 基于餐易通经营数据,为商户提供经营贷、供应链金融等金融服务对接
- 金融机构看到的不是静态报表,而是实时经营数据——收入、成本、客流、复购全透明
- "看得见利润"的商户更容易获得低息贷款,解决加盟商资金压力
**新店筹建与老店托管**
- 从选址分析、装修标准、设备配置到人员培训、开业营销,提供一站式新店筹建服务
- 对经营不善的门店提供托管服务,基于餐易通诊断模型快速定位问题并输出整改方案
**这些服务不是独立的增值项目,而是与餐易通数字化体系深度耦合的生态能力。** 数据分析告诉你"哪里有问题",产业资源帮你"解决问题"——这才是真正的闭环。纯技术公司只能做到前者,纯供应链公司只能做到后者,餐易通是唯一同时拥有两者的平台。
---
## 五、产业级影响:从服务商户到定义标准
上升到产业层面,这次融合的影响远超两家公司:
### 5.1 对商户:从"数字化"到"智能化"
当前餐饮行业的问题不是"没有系统",而是"系统太多、数据不通、看了不知道做什么"。餐易通已经证明:可以做到**每天只推少量异常和待办**,让店长几分钟看完就知道今天干什么。
融合后,这个能力通过收钱吧的渠道覆盖到**数十万商户**——不是给商户更多报表,而是给商户**更少的、更准的、能直接行动的指令**。
### 5.2 对连锁品牌:从"开店"到"复制盈利模型"
以麦鲜面为例,从30家到2000家的跨越,核心不是资金和人力,而是**能否复制一套可标准化的经营模型**:
- 标准SKU结构和搭售模型
- 标准备货量和排班模型
- 标准成本管控和盘点流程
- 标准会员复购和召回机制
餐易通已经把这些拆成可验证的模块(标杆店拆项试点→持续观察→标准化推广),收钱吧提供全国渠道和支付基础设施。**融合后,"开一家赚一家"取代"开了再说"成为连锁扩张的底层逻辑。**
### 5.3 对行业:从"工具竞争"到"生态竞争"
当"支付+经营"一体化模式在麦鲜面验证成功,就形成了**行业标杆效应**:
- 其他连锁品牌会主动寻求同样的数字化能力
- 金融机构愿意为"看得见利润"的商户提供经营贷
- 供应链愿意接入"能预测需求"的平台做精准配送
- 行业标准从"谁费率低"转向"谁能帮商户赚更多"
**这才是"行业格局重塑"的真正含义——不是打败某个竞品,而是改变了竞争的维度。**
---
## 六、具体融合路径
餐易通与收钱吧的融合不是简单的接口对接,而是从数据、产品、硬件到生态的全面打通。以下六条路径构成融合的核心抓手:
### 6.1 支付数据 → 经营数据闭环
以支付为入口,向上游延伸至菜品明细、成本核算、会员复购、排班执行,形成完整经营链路:
```
收钱吧支付数据(交易入口)
→ 餐易通菜品销售分析(SKU结构、搭售率、套餐渗透)
→ 餐易通理论成本 vs 实际倒挤成本(门店成本差异定位)
→ 餐易通会员复购分析(复购率、沉睡召回)
→ 餐易通排班与人员执行(餐段客流匹配、人效优化)
→ 门店整改任务闭环(问题分级 → 责任人 → 验收)
```
**价值**:收钱吧从"收款工具"升级为"经营诊断平台",商户每天看到的不再是流水总额,而是可执行的经营指令。这是从"功能应用浅层化"到"深度经营赋能"的直接跃迁。
### 6.2 智能利润诊断 × 收钱吧商户基数
将餐易通已验证的成本诊断模型推广到收钱吧覆盖的数十万餐饮商户:
- 收钱吧地推团队在拜访商户时,直接展示"你的食材成本率高于同类门店",以数据驱动的方式建立专业信任
- 以"利润诊断报告"作为增值服务入口,提升商户付费意愿,从免费工具用户转化为付费经营客户
- 收入模式从单一支付手续费升级为 SaaS增值订阅 + 成本管理服务费,突破手续费内卷困局
### 6.3 AI客流预测 × 支付时段数据
- 收钱吧支付数据提供精准的每笔交易时间戳,构建小时级客流热力图 → 餐易通AI模型预测下一餐段需求
- 自动生成备货建议(核心SKU需求量)和排班建议(峰谷人力匹配)
- 直接解决收钱吧商户"管控粗放、食材浪费"的经营痛点,让数据从流水变为决策依据
### 6.4 会员与复购 × 支付身份识别
- 收钱吧每笔支付关联用户身份(微信/支付宝ID),天然具备全量会员识别能力 → 餐易通统一会员主数据
- 基于支付行为自动分层(RFM),自动触发沉睡会员召回,实现全量顾客复购分析
- 收钱吧的营销流量 + 餐易通的复购模型 = 精准营销闭环,解决"商户长期粘性弱"的短板
### 6.5 平台经济性 × 聚合支付
- 收钱吧作为聚合支付方,掌握各平台费率谈判筹码
- 餐易通提供逐活动贡献毛利分析,识别"越卖越亏"的活动和SKU
- 双方联合优化平台活动配置:停、改、留清单由数据驱动而非经验判断,帮助商户真正算清平台账
### 6.6 硬件 + 软件一体化
- 收钱吧POS终端直接嵌入餐易通经营卡,收银员在收银同时看到搭售建议和备货提醒
- 扫码王语音播报扩展为"经营播报"(如:今日已完成目标80%)
- 硬件从单一收款设备升级为数据采集终端 + 经营指令触达终端,软硬一体化形成完整闭环
---
## 七、收益共赢模型
双方通过优势互补,构建可持续的商业闭环,不仅实现短期的收益增长,更着眼于长期的生态价值共建。
### 7.1 双方各自获益
| 维度 | 收钱吧获益 | 餐易通获益 |
|---|---|---|
| **商户粘性** | 从收款工具→经营伙伴,降低流失率 | 借力海量商户网络快速扩张 |
| **收入结构** | 突破单一手续费,增加SaaS订阅+成本管理+金融导流 | 共享支付流水分润+硬件销售现金流 |
| **竞争壁垒** | "支付+经营"双引擎,难以被竞品复制 | 依托收钱吧渠道形成规模护城河 |
| **数据价值** | 交易流水→经营洞察,数据变现能力跃升 | 支付数据补全会员识别和客流预测 |
### 7.2 联合运营机制
- **收入分成模式**:基于业务增量进行收益共享,风险共担,价值共创
- **联合运营机制**:打通渠道与产品体系,协同推进市场落地与客户服务
- **生态资源共享**:技术、数据与渠道资源互补,共建餐饮数字化新生态
---
## 八、合作目标与愿景
### 8.1 近期目标(1年内)
- 以麦鲜面为标杆,完成"支付+智能经营"一体化模式的验证与标准化
- 打通收钱吧支付数据与餐易通经营分析体系,实现门店每日经营卡上线
- 建立联合运营机制,形成可复制的商户拓展与服务流程
- 将成本诊断、客流预测、会员复购能力打包为SaaS增值服务,在地推网络中试跑
### 8.2 中期目标(3年内)
- 支撑麦鲜面从30家到2000家的规模化扩张,验证标准化经营模型的可复制性
- 将一体化平台推广至收钱吧覆盖的连锁餐饮商户网络,实现市场份额的指数级增长
- 接入金融赋能与供应链服务,构建多元化营收结构
- 形成行业标杆示范效应,吸引更多连锁品牌加入
### 8.3 长期愿景
- **引领行业升级**:推动餐饮行业从基础数字化建设向智能化运营深度转型,树立行业数字化变革新标杆
- **定义行业标准**:"支付+经营"一体化模式成为餐饮数字化的标配,双方共同定义行业标准
- **构建生态护城河**:打造"支付底座+智能经营大脑"的双引擎模式,整合数据与服务的双重优势,形成行业内难以复制的核心竞争护城河
- **实现全球化支撑**:无论是国内扩张还是东南亚出海战略,合作方案都能提供标准化、可扩展的数字化解决方案
---
## 九、总结
> **收钱吧是餐饮数字化的"修路者"——路已修到每家店门口;餐易通是"造车者"——车能跑、能导航、能算账。两者结合,不是在路上多放一个工具,而是让每条路上跑的车都知道去哪、怎么走最省、怎么跑最快。**
从战略全局看,这次融合的价值不是1+1=2的功能叠加,而是**重新定义了餐饮SaaS的竞争维度**——从"谁的费率低、谁的硬件便宜"升级为"谁能让商户真正赚到钱"。在这个新维度上,"支付+经营"一体化平台没有竞品,因为目前没有人同时拥有这两层能力。
收钱吧拥有规模化商户覆盖、成熟支付链路和强大地推网络,餐易通拥有五大智能经营体系和深厚的餐饮产业资源积淀。双方结合,将实现从"开店工具"到"盈利引擎"的价值跃迁,助力商户可持续增长,引领餐饮数字化新纪元。
**携手共进,以创新模式重塑餐饮数字化经营新生态。**