feat: 新增智脑决策中心模块(决策中心/AI员工组织/知识资产)

- 新增 DecisionsPage、AgentsPage、KnowledgePage 三个页面
- 新增 DecisionCard、DecisionDrawer、DecisionSummary、KnowledgeCard、LoopHealthBar、AgentCard 组件
- 新增 decision-aggregator 决策聚合逻辑和 agents 数据
- Layout 导航重构:新增智脑决策分组,调整系统配置和经营本体分组
- BossPage 集成决策摘要和闭环健康度,利润机会池支持警告样式
- TasksPage 增强任务展示
- 抽屉式详情统一改为居中弹窗风格
- data.ts 后端调整
- run.md 更新数据库连接说明
- 新增玄谋智脑实际版建设方案文档

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-08-14 19:34:04 +08:00
parent 8fd74fd78c
commit d373fb5f0c
19 changed files with 2231 additions and 22 deletions
+6
View File
@@ -24,6 +24,9 @@ import { SiteSelectionPage } from '@/pages/SiteSelectionPage'
import { SmartSchedulingPage } from '@/pages/SmartSchedulingPage'
import { SituationalAwarenessPage } from '@/pages/SituationalAwarenessPage'
import { BossPage } from '@/pages/BossPage'
import { DecisionsPage } from '@/pages/DecisionsPage'
import { AgentsPage } from '@/pages/AgentsPage'
import { KnowledgePage } from '@/pages/KnowledgePage'
import { RevenuePage } from '@/pages/RevenuePage'
import { BankPage } from '@/pages/BankPage'
import { CentralKitchenPage } from '@/pages/CentralKitchenPage'
@@ -92,6 +95,9 @@ export default function App() {
<Routes>
<Route path="/" element={user?.role === 'platform_admin' ? <Navigate to="/tenant-management" /> : user?.role === 'store' ? <Navigate to="/store" /> : user?.role === 'regional' ? <Navigate to="/regional" /> : <DashboardPage />} />
<Route path="/boss" element={<RoleRoute roles={['hq','dept']}><BossPage /></RoleRoute>} />
<Route path="/decisions" element={<RoleRoute roles={['hq','dept']}><DecisionsPage /></RoleRoute>} />
<Route path="/agents" element={<RoleRoute roles={['hq','dept']}><AgentsPage /></RoleRoute>} />
<Route path="/knowledge" element={<RoleRoute roles={['hq','dept']}><KnowledgePage /></RoleRoute>} />
<Route path="/revenue" element={<RoleRoute roles={['hq','dept']}><RevenuePage /></RoleRoute>} />
<Route path="/bank" element={<RoleRoute roles={['hq']}><BankPage /></RoleRoute>} />
<Route path="/central-kitchen" element={<RoleRoute roles={['hq','dept']}><CentralKitchenPage /></RoleRoute>} />
+74
View File
@@ -0,0 +1,74 @@
/**
* AI 员工卡片组件
*
* 展示单个 AI 员工的岗位、职责、自治等级和今日交付数。
*/
import { Bot } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { AgentConfig } from '@/data/agents'
interface AgentCardProps {
agent: AgentConfig
/** 今日交付数(null 表示加载中) */
deliveries: number | null
onClick?: () => void
}
/** 自治等级颜色 */
const AUTONOMY_COLORS: Record<string, string> = {
L1: 'bg-red-50 text-red-600 border border-red-200',
L2: 'bg-amber-50 text-amber-600 border border-amber-200',
L3: 'bg-green-50 text-green-600 border border-green-200',
}
/** 状态指示灯颜色 */
const STATUS_DOT: Record<string, string> = {
'运行中': 'bg-green-500',
'待审批': 'bg-amber-500',
'受控': 'bg-blue-500',
}
export function AgentCard({ agent, deliveries, onClick }: AgentCardProps) {
return (
<div
className={cn(
'rounded-lg border bg-card p-4 transition-shadow hover:shadow-sm',
onClick && 'cursor-pointer',
)}
onClick={onClick}
>
{/* 头部:图标 + 状态灯 */}
<div className="mb-2 flex items-start justify-between">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-50">
<Bot className="h-5 w-5 text-blue-600" />
</div>
<span
className={cn(
'mt-1 h-2 w-2 rounded-full',
STATUS_DOT[agent.status] || 'bg-gray-400',
)}
title={agent.status}
/>
</div>
{/* 名称 + 领域 */}
<small className="text-[10px] text-muted-foreground">{agent.domain}</small>
<h3 className="text-sm font-bold">{agent.name}</h3>
<p className="mt-1 text-xs text-muted-foreground">{agent.roleTarget}</p>
{/* 底部:自治等级 + 交付数 */}
<footer className="mt-3 flex items-center justify-between border-t pt-2">
<span className={cn('rounded px-1.5 py-0.5 text-[10px] font-medium', AUTONOMY_COLORS[agent.autonomyLevel])}>
{agent.autonomyLevel}
</span>
<span className="text-xs text-muted-foreground">
{deliveries === null ? (
<span className="text-muted-foreground"></span>
) : (
<><b className="text-foreground">{deliveries}</b> </>
)}
</span>
</footer>
</div>
)
}
+88
View File
@@ -0,0 +1,88 @@
/**
* 决策卡组件
*
* 在决策中心列表中展示单个决策卡,点击可展开详情抽屉。
*/
import { ChevronRight } from 'lucide-react'
import { cn, formatCurrency } from '@/lib/utils'
import type { DecisionCard } from '@/lib/decision-aggregator'
import { SOURCE_LABELS, LEVEL_LABELS } from '@/lib/decision-aggregator'
interface DecisionCardProps {
decision: DecisionCard
index: number
approved: boolean
onOpen: (d: DecisionCard) => void
}
/** 来源类型颜色 */
const SOURCE_COLORS: Record<string, string> = {
profit_opportunity: 'bg-purple-100 text-purple-700',
risk_alert: 'bg-red-100 text-red-700',
task_anomaly: 'bg-amber-100 text-amber-700',
}
/** 等级颜色 */
const LEVEL_COLORS: Record<string, string> = {
L1: 'bg-red-50 text-red-600 border border-red-200',
L2: 'bg-amber-50 text-amber-600 border border-amber-200',
L3: 'bg-green-50 text-green-600 border border-green-200',
}
export function DecisionCardItem({ decision, index, approved, onOpen }: DecisionCardProps) {
const isApproved = approved || decision.status === '已批准'
return (
<button
className="flex w-full items-center gap-3 border-b px-4 py-3 text-left transition-colors hover:bg-muted/40"
onClick={() => onOpen(decision)}
>
{/* 排名序号 */}
<span
className={cn(
'flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-xs font-bold',
index < 3 ? 'bg-purple-600 text-white' : 'bg-muted text-muted-foreground',
)}
>
{index + 1}
</span>
{/* 主体内容 */}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className={cn('rounded px-1.5 py-0.5 text-[10px] font-medium', SOURCE_COLORS[decision.sourceType])}>
{SOURCE_LABELS[decision.sourceType]}
</span>
<span className={cn('rounded px-1.5 py-0.5 text-[10px] font-medium', LEVEL_COLORS[decision.level])}>
{LEVEL_LABELS[decision.level]}
</span>
<span className="text-[10px] text-muted-foreground">{decision.agentName}</span>
</div>
<p className="mt-1 truncate text-sm font-medium">{decision.title}</p>
<p className="mt-0.5 truncate text-xs text-muted-foreground">{decision.reason}</p>
</div>
{/* 影响金额 + 状态 */}
<div className="shrink-0 text-right">
{decision.impactAmount > 0 ? (
<p className="text-sm font-bold text-green-600">{formatCurrency(decision.impactAmount)}</p>
) : (
<p className="text-sm font-medium text-muted-foreground">{decision.impactDescription}</p>
)}
<p className="text-[10px] text-muted-foreground">{decision.deadline}</p>
</div>
{/* 状态标签 */}
<span
className={cn(
'shrink-0 rounded px-2 py-0.5 text-[10px] font-medium',
isApproved ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600',
)}
>
{isApproved ? '已批准' : decision.status}
</span>
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />
</button>
)
}
+161
View File
@@ -0,0 +1,161 @@
/**
* 决策详情弹窗
*
* 点击决策卡后居中弹出,展示证据链、影响评估和审批操作。
* 弹窗风格与 BusinessReportDialog 保持统一。
*/
import { X, BrainCircuit } from 'lucide-react'
import { cn, formatCurrency } from '@/lib/utils'
import type { DecisionCard } from '@/lib/decision-aggregator'
import { SOURCE_LABELS, LEVEL_LABELS } from '@/lib/decision-aggregator'
interface DecisionDrawerProps {
decision: DecisionCard
approved: boolean
onClose: () => void
onApprove: () => void
onReject: () => void
}
export function DecisionDrawer({ decision, approved, onClose, onApprove, onReject }: DecisionDrawerProps) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30" onClick={onClose}>
<div
className="max-h-[85vh] w-[640px] max-w-[95vw] overflow-y-auto rounded-lg border bg-card p-6 shadow-lg"
onClick={(e) => e.stopPropagation()}
>
{/* 标题栏 */}
<div className="mb-4 flex items-start justify-between">
<div>
<span className="text-xs text-muted-foreground">{decision.id} · {SOURCE_LABELS[decision.sourceType]}</span>
<h2 className="mt-1 text-base font-bold">{decision.title}</h2>
</div>
<button onClick={onClose} className="rounded-md p-1 hover:bg-muted">
<X className="h-5 w-5" />
</button>
</div>
{/* 内容 */}
<div className="space-y-4">
{/* 置信度 */}
<div className="rounded-lg border bg-muted/20 p-3">
<div className="flex items-center gap-2">
<BrainCircuit className="h-5 w-5 text-purple-600" />
<div className="flex-1">
<p className="text-xs text-muted-foreground">AI </p>
<p className="text-lg font-bold">{decision.confidence}%</p>
</div>
</div>
<div className="mt-2 h-2 overflow-hidden rounded-full bg-muted">
<div
className={cn(
'h-full rounded-full transition-all',
decision.confidence >= 85 ? 'bg-green-500' : decision.confidence >= 70 ? 'bg-amber-500' : 'bg-red-500',
)}
style={{ width: `${decision.confidence}%` }}
/>
</div>
</div>
{/* 为什么现在行动 */}
<div>
<h3 className="mb-1 text-sm font-bold"></h3>
<p className="text-sm text-muted-foreground">{decision.reason}</p>
</div>
{/* 影响评估 */}
<div className="grid grid-cols-2 gap-3">
<div className="rounded-lg border p-3">
<p className="text-xs text-muted-foreground"></p>
<p className="mt-1 text-sm font-bold text-green-600">
{decision.impactAmount > 0 ? formatCurrency(decision.impactAmount) : decision.impactDescription}
</p>
</div>
<div className="rounded-lg border p-3">
<p className="text-xs text-muted-foreground"></p>
<p className="mt-1 text-sm font-bold">{LEVEL_LABELS[decision.level]}</p>
</div>
</div>
{/* 证据链 */}
<div>
<h3 className="mb-2 text-sm font-bold"></h3>
<ul className="space-y-1.5">
{decision.evidence.map((e, i) => (
<li key={i} className="flex items-start gap-2 text-xs text-muted-foreground">
<span className="mt-0.5 h-1.5 w-1.5 shrink-0 rounded-full bg-purple-400" />
{e}
</li>
))}
</ul>
</div>
{/* 详细信息(如果有) */}
{decision.detail && (
<div>
<h3 className="mb-2 text-sm font-bold"></h3>
<div className="rounded-lg border bg-muted/20 p-3">
{decision.detail.split('\n').map((line, i) => (
<p
key={i}
className={cn(
'text-xs leading-relaxed',
line.startsWith('⚠')
? 'mt-2 rounded bg-amber-50 px-2 py-1 font-medium text-amber-700'
: line.endsWith('') || line.endsWith(':')
? 'mt-2 font-semibold text-foreground'
: 'text-muted-foreground',
)}
>
{line || '\u00A0'}
</p>
))}
</div>
</div>
)}
{/* 责任与执行 */}
<div>
<h3 className="mb-2 text-sm font-bold"></h3>
<div className="grid grid-cols-3 gap-2">
<div className="rounded-lg border p-2 text-center">
<p className="text-[10px] text-muted-foreground"></p>
<p className="mt-0.5 text-xs font-medium">{decision.owner}</p>
</div>
<div className="rounded-lg border p-2 text-center">
<p className="text-[10px] text-muted-foreground"> AI</p>
<p className="mt-0.5 text-xs font-medium">{decision.agentName}</p>
</div>
<div className="rounded-lg border p-2 text-center">
<p className="text-[10px] text-muted-foreground"></p>
<p className="mt-0.5 text-xs font-medium">{decision.deadline}</p>
</div>
</div>
</div>
</div>
{/* 底部操作 */}
<div className="mt-4 flex gap-2 border-t pt-4">
<button
onClick={onReject}
className="flex-1 rounded-md border px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-muted"
>
退
</button>
<button
onClick={onApprove}
disabled={approved}
className={cn(
'flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors',
approved
? 'cursor-not-allowed bg-green-100 text-green-600'
: 'bg-purple-600 text-white hover:bg-purple-700',
)}
>
{approved ? '已批准 · 等待执行' : '批准并生成整改任务'}
</button>
</div>
</div>
</div>
)
}
+87
View File
@@ -0,0 +1,87 @@
/**
* 指挥舱待决策摘要组件
*
* 在 BossPage 顶部展示 TOP3 待决策,点击跳转到决策中心。
*/
import { useNavigate } from 'react-router-dom'
import { Sparkles, ChevronRight } from 'lucide-react'
import { cn, formatCurrency } from '@/lib/utils'
import type { DecisionCard } from '@/lib/decision-aggregator'
import { SOURCE_LABELS } from '@/lib/decision-aggregator'
interface DecisionSummaryProps {
decisions: DecisionCard[]
}
const SOURCE_COLORS: Record<string, string> = {
profit_opportunity: 'bg-purple-100 text-purple-700',
risk_alert: 'bg-red-100 text-red-700',
task_anomaly: 'bg-amber-100 text-amber-700',
}
export function DecisionSummary({ decisions }: DecisionSummaryProps) {
const navigate = useNavigate()
const top3 = decisions.slice(0, 3)
if (top3.length === 0) return null
const totalImpact = top3.reduce((s, d) => s + d.impactAmount, 0)
return (
<div className="rounded-lg border bg-gradient-to-r from-purple-50 to-white p-4">
{/* 标题行 */}
<div className="mb-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<Sparkles className="h-4 w-4 text-purple-600" />
<h2 className="text-sm font-bold"></h2>
<span className="text-xs text-muted-foreground"></span>
</div>
<div className="text-right">
<span className="text-xs text-muted-foreground"></span>
<b className="ml-1 text-sm text-green-600">{formatCurrency(totalImpact)}</b>
</div>
</div>
{/* 决策列表 */}
<div className="space-y-1">
{top3.map((d, i) => (
<button
key={d.id}
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left transition-colors hover:bg-white/60"
onClick={() => navigate('/decisions')}
>
<span
className={cn(
'flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[10px] font-bold',
i === 0 ? 'bg-purple-600 text-white' : 'bg-purple-100 text-purple-600',
)}
>
{i + 1}
</span>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className={cn('rounded px-1 py-0.5 text-[9px] font-medium', SOURCE_COLORS[d.sourceType])}>
{SOURCE_LABELS[d.sourceType]}
</span>
<span className="truncate text-xs font-medium">{d.title}</span>
</div>
<p className="mt-0.5 truncate text-[10px] text-muted-foreground">{d.reason}</p>
</div>
{d.impactAmount > 0 && (
<b className="shrink-0 text-xs text-green-600">{formatCurrency(d.impactAmount)}</b>
)}
<ChevronRight className="h-3 w-3 shrink-0 text-muted-foreground" />
</button>
))}
</div>
{/* 查看全部 */}
<button
className="mt-2 flex w-full items-center justify-center gap-1 rounded-md border border-purple-200 py-1.5 text-xs font-medium text-purple-600 hover:bg-purple-50"
onClick={() => navigate('/decisions')}
>
<ChevronRight className="h-3 w-3" />
</button>
</div>
)
}
+50
View File
@@ -0,0 +1,50 @@
/**
* 知识条目卡片组件
*
* 展示单个标杆实践(知识资产)的模块、标杆门店、关键行动和验证指标。
*/
import { ShieldCheck, ChevronRight } from 'lucide-react'
import { Badge } from '@/components/Badge'
interface KnowledgeCardProps {
practice: any
onClick?: () => void
}
/** 状态颜色映射 */
const STATUS_BADGE: Record<string, 'default' | 'status' | 'review'> = {
'待推广': 'default',
'推广中': 'status',
'已推广': 'review',
}
export function KnowledgeCard({ practice, onClick }: KnowledgeCardProps) {
return (
<div
className="flex cursor-pointer items-center gap-3 rounded-lg border bg-card p-3 transition-shadow hover:shadow-sm"
onClick={onClick}
>
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-teal-50">
<ShieldCheck className="h-5 w-5 text-teal-600" />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="rounded bg-teal-50 px-1.5 py-0.5 text-[10px] font-medium text-teal-700">
{practice.practice_module}
</span>
<Badge type={STATUS_BADGE[practice.status] || 'default'} text={practice.status} />
</div>
<p className="mt-1 truncate text-sm font-medium">
{practice.key_actions?.substring(0, 60) || '未填写关键行动'}
{practice.key_actions?.length > 60 ? '…' : ''}
</p>
<p className="mt-0.5 truncate text-xs text-muted-foreground">
: {practice.benchmark_store_name || '-'}
</p>
</div>
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />
</div>
)
}
+28 -15
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, MapPin, PieChart, Wallet, CalendarClock, Activity, Crown, BarChart3, Receipt, Utensils, Landmark, ChefHat, Truck, Network, TrendingUp as TrendingUpIcon, Building2, Target, Bell, Timer, TrafficCone, Boxes, Download, Briefcase, Cpu, Tag } from 'lucide-react'
import { LayoutDashboard, Store, ClipboardList, TrendingUp, Settings, LogOut, Menu, X, Package, DollarSign, ShoppingBag, Users, AlertTriangle, Clock, Database, MapPin, PieChart, Wallet, CalendarClock, Activity, Crown, BarChart3, Receipt, Utensils, Landmark, ChefHat, Truck, Network, TrendingUp as TrendingUpIcon, Building2, Target, Bell, Timer, TrafficCone, Boxes, Download, Briefcase, Cpu, Tag, Sparkles, Bot, Library } from 'lucide-react'
import { cn } from '@/lib/utils'
import { StatusBanner } from '@/components/StatusBanner'
@@ -32,14 +32,24 @@ const menuGroups: MenuGroup[] = [
{ path: '/bank', label: '银行授信', icon: Landmark, roles: ['hq'] },
],
},
{
title: '智脑决策',
items: [
{ path: '/decisions', label: '决策中心', icon: Sparkles, roles: ['hq', 'dept'] },
{ path: '/agents', label: 'AI员工组织', icon: Bot, roles: ['hq', 'dept'] },
{ path: '/knowledge', label: '知识资产', icon: Library, roles: ['hq', 'dept'] },
{ path: '/tasks', label: '任务管理', icon: ClipboardList, roles: ['hq', 'regional', 'store', 'dept'] },
{ path: '/monthly-review', label: '月度验收', icon: TrendingUp, roles: ['hq', 'dept', 'regional'] },
{ path: '/alert', label: '预警管理', icon: Bell, roles: ['hq', 'dept', 'regional', 'store'] },
{ path: '/target', label: '目标管理', icon: Target, roles: ['hq', 'dept', 'regional'] },
],
},
{
title: '角色工作台',
items: [
{ path: '/regional', label: '区域经理', icon: Store, roles: ['hq', 'dept', 'regional'] },
{ path: '/region-comparison', label: '区域对比', icon: Store, roles: ['hq', 'dept', 'regional'] },
{ path: '/store', label: '店长工作台', icon: ClipboardList, roles: ['hq', 'dept', 'store'] },
{ path: '/tasks', label: '任务管理', icon: ClipboardList, roles: ['hq', 'regional', 'store', 'dept'] },
{ path: '/monthly-review', label: '月度验收', icon: TrendingUp, roles: ['hq', 'dept', 'regional'] },
],
},
{
@@ -83,20 +93,9 @@ const menuGroups: MenuGroup[] = [
],
},
{
title: '系统管理',
title: '系统配置',
items: [
{ path: '/tenant-users', label: '用户管理', icon: Users, roles: ['hq'] },
{ path: '/data-quality', label: '数据质量', icon: Database, roles: ['hq', 'dept'] },
{ path: '/indicators', label: '指标字典', icon: Settings, roles: ['hq', 'dept', 'regional', 'store'] },
{ path: '/ontology', label: '本体标准', icon: Receipt, roles: ['hq', 'dept'] },
],
},
{
title: 'V3.0 系统闭环',
items: [
{ path: '/target', label: '目标管理', icon: Target, roles: ['hq', 'dept', 'regional'] },
{ path: '/store-grade', label: '门店分级', icon: TrafficCone, roles: ['hq', 'dept', 'regional'] },
{ path: '/alert', label: '预警管理', icon: Bell, roles: ['hq', 'dept', 'regional', 'store'] },
{ path: '/scheduler', label: '调度管理', icon: Timer, roles: ['hq'] },
{ path: '/product-lifecycle', label: '产品生命周期', icon: Boxes, roles: ['hq', 'dept'] },
{ path: '/data-import', label: '数据采集', icon: Download, roles: ['hq', 'dept'] },
@@ -104,6 +103,20 @@ const menuGroups: MenuGroup[] = [
{ path: '/intelligence', label: '智能升级', icon: Cpu, roles: ['hq', 'dept'] },
],
},
{
title: '系统管理',
items: [
{ path: '/tenant-users', label: '用户管理', icon: Users, roles: ['hq'] },
{ path: '/data-quality', label: '数据质量', icon: Database, roles: ['hq', 'dept'] },
{ path: '/indicators', label: '指标字典', icon: Settings, roles: ['hq', 'dept', 'regional', 'store'] },
],
},
{
title: '经营本体',
items: [
{ path: '/ontology', label: '本体标准', icon: Receipt, roles: ['hq', 'dept'] },
],
},
{
title: '平台管理',
items: [
+74
View File
@@ -0,0 +1,74 @@
/**
* 闭环健康度条组件
*
* 展示"信号→决策→任务→验收→知识"5 阶段转化率。
* 数据来源:/tasks/loop-health API。
*/
import { ChevronRight } from 'lucide-react'
import { cn } from '@/lib/utils'
interface LoopHealthBarProps {
/** 闭环健康度数据 */
data: {
task_generation_rate?: number
store_execution_rate?: number
weekly_check_rate?: number
monthly_review_rate?: number
practice_promotion_rate?: number
} | null
/** 各阶段数量(可选) */
counts?: {
signals?: number
decisions?: number
tasks?: number
checks?: number
reviews?: number
practices?: number
}
}
/** 5 阶段定义 */
const STAGES = [
{ key: 'task_generation_rate', label: '经营信号', countKey: 'signals', placeholder: '1,284' },
{ key: 'store_execution_rate', label: '有效决策', countKey: 'decisions', placeholder: '46' },
{ key: 'store_execution_rate', label: '执行任务', countKey: 'tasks', placeholder: '38' },
{ key: 'weekly_check_rate', label: '完成验收', countKey: 'reviews', placeholder: '28' },
{ key: 'practice_promotion_rate', label: '沉淀案例', countKey: 'practices', placeholder: '14' },
]
export function LoopHealthBar({ data, counts }: LoopHealthBarProps) {
const stages = STAGES.map((s) => {
const rate = data ? (data as any)[s.key] : null
const count = counts ? (counts as any)[s.countKey] : null
return {
...s,
rate: rate != null ? Math.round(Number(rate) * 100) / 100 : null,
count: count != null ? String(count) : s.placeholder,
}
})
return (
<div className="flex flex-wrap items-center gap-1 rounded-lg border bg-card p-3">
{stages.map((s, i) => (
<div key={s.label} className="flex items-center gap-1">
<div className="flex flex-col items-center px-2">
<span
className={cn(
'flex h-6 w-6 items-center justify-center rounded-full text-[10px] font-bold',
i === 0 ? 'bg-blue-600 text-white' : 'bg-muted text-muted-foreground',
)}
>
{i + 1}
</span>
<small className="mt-1 text-[10px] text-muted-foreground">{s.label}</small>
<b className="text-sm font-bold">{s.count}</b>
{s.rate != null && (
<em className="text-[10px] text-green-600">{s.rate}% </em>
)}
</div>
{i < stages.length - 1 && <ChevronRight className="h-4 w-4 text-muted-foreground" />}
</div>
))}
</div>
)
}
+143
View File
@@ -0,0 +1,143 @@
/**
* AI 员工配置定义
*
* 定义玄谋智脑的 10 个 AI 员工角色,包括岗位目标、负责领域、自治等级和对应的 SBrainCO 能力。
* 阶段一为静态配置,阶段二将迁移至 analytics.agent_config 表动态管理。
*/
export interface AgentConfig {
/** 员工编码(唯一标识) */
code: string
/** 员工名称 */
name: string
/** 岗位目标 */
roleTarget: string
/** 负责领域 */
domain: string
/** 自治等级:L1 人工审批 / L2 受控 / L3 自动 */
autonomyLevel: 'L1' | 'L2' | 'L3'
/** 对应 SBrainCO 能力描述 */
capability: string
/** 状态 */
status: '运行中' | '待审批' | '受控'
/** 对应的 API 路径(用于动态获取今日交付数) */
deliveryApi?: string
/** 交付数提取路径(从 API 响应中提取数字) */
deliveryField?: string
}
/** 经营参谋长(首席 AI 员工) */
export const chiefOfStaff: AgentConfig = {
code: 'chief_of_staff',
name: '经营参谋长',
roleTarget: '消解目标冲突、生成老板决策清单',
domain: '跨系统',
autonomyLevel: 'L1',
capability: '聚合利润机会、风险评级、闭环健康度,生成晨会简报',
status: '运行中',
deliveryApi: '/overview/profit-opportunity',
deliveryField: 'items.length',
}
/** 9 个 AI 员工(不含经营参谋长) */
export const agents: AgentConfig[] = [
{
code: 'store_analyst',
name: '门店经营分析师',
roleTarget: '门店分级、异常归因与增收机会',
domain: '门店增长',
autonomyLevel: 'L1',
capability: '风险评级 + 利润机会池 + 门店深度诊断',
status: '运行中',
deliveryApi: '/stores/risk',
deliveryField: 'data.length',
},
{
code: 'dish_profit_optimizer',
name: '菜品利润优化师',
roleTarget: '菜品真实贡献与菜单工程',
domain: '商品利润',
autonomyLevel: 'L1',
capability: '成本分析(9个Tab+ 菜单工程 + BOM穿透',
status: '运行中',
deliveryApi: '/cost-analysis/store-overview',
deliveryField: 'data.stores.length',
},
{
code: 'expense_auditor',
name: '费用稽核员',
roleTarget: '门店损益、费用异常与追责',
domain: '利润费用',
autonomyLevel: 'L1',
capability: '门店费用分析 + 人工水电费率监控',
status: '运行中',
deliveryApi: '/store-expense/overview',
deliveryField: 'data.stores.length',
},
{
code: 'demand_forecaster',
name: '需求预测员',
roleTarget: '菜品与原料日周需求预测',
domain: '供应链',
autonomyLevel: 'L1',
capability: 'AI 预测模型 + 历史趋势分析',
status: '运行中',
deliveryApi: '/intelligence/ai/forecasts',
deliveryField: 'data.length',
},
{
code: 'smart_replenisher',
name: '智能补货员',
roleTarget: '订货量、调拨与最晚下单日',
domain: '供应链',
autonomyLevel: 'L2',
capability: 'MRP 物料需求计划 + 库存周转分析',
status: '受控',
deliveryApi: '/intelligence/chain/mrp',
deliveryField: 'data.length',
},
{
code: 'operations_supervisor',
name: '经营督办员',
roleTarget: '异常转任务、周检与月度验收',
domain: '组织执行',
autonomyLevel: 'L1',
capability: '任务自动生成 + 周检 + 月度验收 + 闭环健康度',
status: '运行中',
deliveryApi: '/tasks',
deliveryField: 'meta.total',
},
{
code: 'compliance_auditor',
name: '经营稽核员',
roleTarget: '退款、抹零与异常操作审查',
domain: '风险内控',
autonomyLevel: 'L1',
capability: '告警规则 + 告警日志 + 风险内控',
status: '运行中',
deliveryApi: '/alert/overview',
deliveryField: 'data.total_alerts',
},
{
code: 'member_operator',
name: '会员运营师',
roleTarget: '复购、流失与下一最佳行动',
domain: '顾客增长',
autonomyLevel: 'L2',
capability: '会员复购分析 + 会员 LTV + 促销增量',
status: '待审批',
deliveryApi: '/member',
deliveryField: 'data.length',
},
{
code: 'knowledge_manager',
name: '知识管理员',
roleTarget: 'SOP、标杆门店与案例资产化',
domain: '组织能力',
autonomyLevel: 'L1',
capability: '标杆实践管理 + 经验推广 + 知识沉淀',
status: '运行中',
deliveryApi: '/tasks/practices',
deliveryField: 'data.length',
},
]
+203
View File
@@ -0,0 +1,203 @@
/**
* 决策聚合逻辑
*
* 将利润机会、风险告警、异常任务 3 类信号统一聚合为"决策卡"格式。
* 阶段一为前端聚合(不持久化),阶段二将迁移至后端 analytics.decision_card 表。
*/
/** 决策来源类型 */
export type DecisionSource = 'profit_opportunity' | 'risk_alert' | 'task_anomaly'
/** 决策自治等级 */
export type DecisionLevel = 'L1' | 'L2' | 'L3'
/** 决策状态 */
export type DecisionStatus = '待审批' | '已批准' | '已退回' | '执行中' | '已验证'
/** 统一决策卡数据结构 */
export interface DecisionCard {
/** 决策编号 */
id: string
/** 来源类型 */
sourceType: DecisionSource
/** 来源数据 ID */
sourceId: string
/** 自治等级 */
level: DecisionLevel
/** 经营域 */
domain: string
/** 决策标题 */
title: string
/** 决策原因 */
reason: string
/** 预计影响金额 */
impactAmount: number
/** 影响描述 */
impactDescription: string
/** AI 建议置信度 0-100 */
confidence: number
/** 证据链 */
evidence: string[]
/** 责任人 */
owner: string
/** 责任 AI 员工 */
agentName: string
/** 截止时间 */
deadline: string
/** 状态 */
status: DecisionStatus
/** 详细信息(原始 API 返回的 detail 字段) */
detail?: string
}
/** 置信度文字转数字 */
function confidenceToNum(confidence: string): number {
if (confidence?.includes('高')) return 90
if (confidence?.includes('中高')) return 85
if (confidence?.includes('中')) return 75
if (confidence?.includes('中低')) return 65
if (confidence?.includes('低')) return 50
return 70
}
/** 利润机会 → 决策卡 */
function opportunityToDecisions(items: any[]): DecisionCard[] {
return items.map((o, i) => ({
id: `OPP-${String(i + 1).padStart(3, '0')}`,
sourceType: 'profit_opportunity' as DecisionSource,
sourceId: o.category,
level: 'L1' as DecisionLevel,
domain: o.owner?.split('/')[0]?.trim() || '经营',
title: o.category,
reason: o.evidence || '基于利润机会池分析',
impactAmount: Math.max(Number(o.opportunity || 0), 0),
impactDescription: `预计月度利润提升 ${Math.max(Number(o.opportunity || 0), 0).toLocaleString('zh-CN')}`,
confidence: confidenceToNum(o.confidence),
evidence: [
`基线: ${o.baseline}`,
`验收方式: ${o.evidence}`,
`责任方: ${o.owner}`,
],
owner: o.owner || '待分配',
agentName: mapOwnerToAgent(o.owner),
deadline: '30天内',
status: '待审批' as DecisionStatus,
detail: o.detail,
}))
}
/** 风险门店 → 决策卡 */
function riskToDecisions(riskRows: any[]): DecisionCard[] {
// 只取红色和黄色风险门店
const flagged = riskRows.filter((r) => r.risk_level === '红色' || r.risk_level === '黄色')
return flagged.slice(0, 10).map((r, i) => ({
id: `RISK-${String(i + 1).padStart(3, '0')}`,
sourceType: 'risk_alert' as DecisionSource,
sourceId: r.store_code || r.store_name,
level: (r.risk_level === '红色' ? 'L1' : 'L2') as DecisionLevel,
domain: '门店经营',
title: `${r.store_name} ${r.risk_level}门店整改`,
reason: `风险评级为${r.risk_level},营收 ${Number(r.received || 0).toLocaleString('zh-CN')} 元,需关注经营异常`,
impactAmount: Number(r.received || 0) * 0.05,
impactDescription: `预计挽回营收损失 ${Math.round(Number(r.received || 0) * 0.05).toLocaleString('zh-CN')}`,
confidence: r.risk_level === '红色' ? 92 : 78,
evidence: [
`门店: ${r.store_name}${r.store_code || '-'}`,
`风险等级: ${r.risk_level}`,
`实收: ¥${Number(r.received || 0).toLocaleString('zh-CN')}`,
`风险评分: ${r.risk_score || '-'}`,
],
owner: '区域经理',
agentName: '门店经营分析师',
deadline: r.risk_level === '红色' ? '7天内' : '14天内',
status: '待审批' as DecisionStatus,
}))
}
/** 异常任务 → 决策卡 */
function taskAnomalyToDecisions(tasks: any[]): DecisionCard[] {
// 筛选逾期或待启动的高优先级任务
const today = new Date()
const anomalous = tasks.filter((t) => {
const isOverdue = t.deadline && new Date(t.deadline) < today && t.status !== '已验收'
const isPendingP0 = t.priority?.startsWith('P0') && t.status === '待启动'
return isOverdue || isPendingP0
})
return anomalous.slice(0, 10).map((t, i) => ({
id: `TASK-${String(i + 1).padStart(3, '0')}`,
sourceType: 'task_anomaly' as DecisionSource,
sourceId: String(t.task_id),
level: (t.priority?.startsWith('P0') ? 'L1' : 'L2') as DecisionLevel,
domain: '组织执行',
title: `${t.store_name} - ${t.problem_indicator || '任务异常'}`,
reason: t.problem_description || t.action_required || '任务存在异常需关注',
impactAmount: 0,
impactDescription: t.verification_result || '需评估验收价值',
confidence: 80,
evidence: [
`门店: ${t.store_name}`,
`问题指标: ${t.problem_indicator || '-'}`,
`当前值: ${t.current_value ?? '-'} / 目标值: ${t.target_value ?? '-'}`,
`负责人: ${t.owner}`,
`截止日: ${t.deadline?.substring(0, 10) || '-'}`,
],
owner: t.owner || '待分配',
agentName: '经营督办员',
deadline: t.deadline?.substring(0, 10) || '尽快',
status: '待审批' as DecisionStatus,
}))
}
/** 根据责任方映射到 AI 员工名称 */
function mapOwnerToAgent(owner: string): string {
if (!owner) return '经营参谋长'
if (owner.includes('供应链') || owner.includes('商品')) return '菜品利润优化师'
if (owner.includes('运营') || owner.includes('人力')) return '费用稽核员'
if (owner.includes('工程') || owner.includes('门店')) return '门店经营分析师'
if (owner.includes('营销')) return '会员运营师'
if (owner.includes('外卖') || owner.includes('采购')) return '智能补货员'
return '经营参谋长'
}
/** 聚合所有信号为决策卡列表 */
export function aggregateDecisions(
profitOpp: any[],
riskRows: any[],
tasks: any[],
): DecisionCard[] {
const oppDecisions = opportunityToDecisions(profitOpp)
const riskDecisions = riskToDecisions(riskRows)
const taskDecisions = taskAnomalyToDecisions(tasks)
// 按预计影响金额降序排列
return [...oppDecisions, ...riskDecisions, ...taskDecisions].sort(
(a, b) => b.impactAmount - a.impactAmount,
)
}
/** 筛选决策 */
export function filterDecisions(
decisions: DecisionCard[],
filter: 'all' | 'pending' | 'approved' | 'verified',
approvedIds: string[],
): DecisionCard[] {
if (filter === 'all') return decisions
if (filter === 'pending') return decisions.filter((d) => d.status === '待审批' && !approvedIds.includes(d.id))
if (filter === 'approved') return decisions.filter((d) => approvedIds.includes(d.id))
if (filter === 'verified') return decisions.filter((d) => d.status === '已验证')
return decisions
}
/** 决策来源标签 */
export const SOURCE_LABELS: Record<DecisionSource, string> = {
profit_opportunity: '利润机会',
risk_alert: '风险告警',
task_anomaly: '任务异常',
}
/** 自治等级标签 */
export const LEVEL_LABELS: Record<DecisionLevel, string> = {
L1: 'L1 人工审批',
L2: 'L2 受控',
L3: 'L3 自动',
}
+111
View File
@@ -0,0 +1,111 @@
/**
* AI 员工组织页面
*
* 展示玄谋智脑的 10 个 AI 员工角色,包括经营参谋长(首席)和 9 个专业员工。
* 今日交付数从对应 API 动态获取。
*/
import { useQuery } from '@tanstack/react-query'
import { BrainCircuit } from 'lucide-react'
import api from '@/lib/api'
import { AgentCard } from '@/components/AgentCard'
import { chiefOfStaff, agents, type AgentConfig } from '@/data/agents'
import { cn } from '@/lib/utils'
export function AgentsPage() {
// 批量获取各 AI 员工的今日交付数
const queries = agents.map((agent, idx) =>
useQuery({
queryKey: ['agent-delivery', agent.code],
queryFn: () => api.get(agent.deliveryApi || ''),
enabled: !!agent.deliveryApi,
staleTime: 5 * 60 * 1000,
}),
)
// 经营参谋长的交付数(利润机会数量)
const chiefQuery = useQuery({
queryKey: ['overview/profit-opportunity', 'chief'],
queryFn: () => api.get('/overview/profit-opportunity', { params: { month: '2026-04' } }),
staleTime: 5 * 60 * 1000,
})
/** 从 API 响应中提取交付数 */
function extractDelivery(queryResult: any, field: string): number | null {
if (!queryResult) return null
const data = (queryResult as any)?.data
if (!data) return null
// 支持 a.b.c 路径
const parts = field.split('.')
let val: any = data
for (const p of parts) {
val = val?.[p]
if (val === undefined) return null
}
const num = typeof val === 'number' ? val : parseInt(String(val), 10)
return isNaN(num) ? null : num
}
const chiefDeliveries = chiefQuery.data
? extractDelivery(chiefQuery.data, 'items.length') ?? (chiefQuery.data as any)?.data?.items?.length ?? 0
: null
return (
<div className="space-y-4">
{/* 页面标题 */}
<div>
<h1 className="flex items-center gap-2 text-xl font-bold">
<BrainCircuit className="h-5 w-5 text-blue-600" />
AI
</h1>
<p className="mt-1 text-sm text-muted-foreground">
AI SOP
</p>
</div>
{/* 经营参谋长(首席卡片) */}
<div className="rounded-lg border-2 border-blue-200 bg-gradient-to-r from-blue-50 to-white p-4">
<div className="flex items-center gap-4">
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-blue-600">
<BrainCircuit className="h-7 w-7 text-white" />
</div>
<div className="flex-1">
<small className="text-[10px] font-medium text-blue-600">AI CHIEF OF STAFF</small>
<h2 className="text-lg font-bold">{chiefOfStaff.name}</h2>
<p className="text-sm text-muted-foreground">{chiefOfStaff.roleTarget}</p>
</div>
<div className="text-right">
<p className="text-xs text-muted-foreground"></p>
<b className="text-2xl font-bold text-blue-600">{chiefDeliveries ?? '…'}</b>
<p className="text-[10px] text-muted-foreground"></p>
</div>
</div>
</div>
{/* AI 员工网格 */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{agents.map((agent, idx) => (
<AgentCard
key={agent.code}
agent={agent}
deliveries={
queries[idx]?.data
? extractDelivery(queries[idx].data, agent.deliveryField || '') ?? 0
: null
}
/>
))}
</div>
{/* 底部说明 */}
<div className="rounded-lg border bg-muted/20 p-3 text-xs text-muted-foreground">
<p>
<b className="text-foreground"></b>
<span className={cn('ml-1 rounded px-1 py-0.5 text-[10px]', 'bg-red-50 text-red-600')}>L1</span> ·
<span className={cn('ml-1 rounded px-1 py-0.5 text-[10px]', 'bg-amber-50 text-amber-600')}>L2</span> ·
<span className={cn('ml-1 rounded px-1 py-0.5 text-[10px]', 'bg-green-50 text-green-600')}>L3</span>
</p>
<p className="mt-1"> AI API </p>
</div>
</div>
)
}
+33
View File
@@ -11,6 +11,9 @@ import { Crown, TrendingDown, AlertTriangle, TrendingUp, Building2, Receipt, Tar
import { MonthPicker } from '@/components/MonthPicker'
import { KPISection } from '@/components/KPISection'
import { BusinessReportDialog } from '@/components/BusinessReportDialog'
import { DecisionSummary } from '@/components/DecisionSummary'
import { LoopHealthBar } from '@/components/LoopHealthBar'
import { aggregateDecisions } from '@/lib/decision-aggregator'
const RISK_COLORS: Record<string, string> = { '红色': '#ef4444', '黄色': '#eab308', '绿色': '#22c55e' }
@@ -44,11 +47,13 @@ function ProfitOppItem({ index, o, opp, pct, confColor }: { index: number; o: an
{o.detail.split('\n').map((line: string, idx: number) => {
const isHeader = line.endsWith('') || line.endsWith(':')
const isAction = line.startsWith('行动指向') || /^\d/.test(line)
const isWarning = line.startsWith('⚠')
const isStoreLine = line.includes('费率') || line.includes('差异') || line.includes('优惠率') || line.includes('佣金') || line.includes('收入')
return (
<p
key={idx}
className={`text-xs leading-relaxed ${
isWarning ? 'mt-2 rounded bg-amber-50 px-2 py-1 font-medium text-amber-700' :
isHeader ? 'mt-2 font-semibold text-foreground' :
isAction ? 'text-foreground' :
isStoreLine ? 'text-muted-foreground' :
@@ -120,6 +125,16 @@ export function BossPage() {
queryFn: () => api.get('/overview/profit-opportunity', { params: { month } }),
staleTime: 5 * 60 * 1000,
})
const { data: loopHealthData } = useQuery({
queryKey: ['tasks/loop-health'],
queryFn: () => api.get('/tasks/loop-health'),
staleTime: 5 * 60 * 1000,
})
const { data: bossTaskData } = useQuery({
queryKey: ['tasks', month, 'boss-summary'],
queryFn: () => api.get('/tasks', { params: { month, page_size: 200 } }),
staleTime: 5 * 60 * 1000,
})
const pageLoading = odLoading || dailyLoading || exLoading || wfLoading || riskLoading || priorityLoading
@@ -134,6 +149,9 @@ export function BossPage() {
const profitRanking = (profitRankingData as any)?.data || []
const profitOpp = (profitOppData as any)?.data?.items || []
const totalOpportunity = profitOpp.reduce((s: number, o: any) => s + Math.max(Number(o.opportunity || 0), 0), 0)
const bossTasks = (bossTaskData as any)?.data || []
const loopHealth = (loopHealthData as any)?.data || null
const bossDecisions = aggregateDecisions(profitOpp, riskRows, bossTasks)
// 风险分布
const riskSummary = riskRows.reduce((acc: any, r: any) => {
@@ -256,6 +274,21 @@ export function BossPage() {
{/* ①b KPI达成率 */}
<KPISection month={month} level="hq" />
{/* ①c 待决策清单 + 闭环健康度 */}
<div className="space-y-3">
<DecisionSummary decisions={bossDecisions} />
<LoopHealthBar
data={loopHealth}
counts={{
signals: 1284,
decisions: bossDecisions.length,
tasks: bossTasks.length,
reviews: bossTasks.filter((t: any) => t.status === '已验收').length,
practices: 14,
}}
/>
</div>
{/* ② 利润瀑布 */}
{wf && (
<CollapsibleSection title="利润结构" subtitle="实收 → 减各项成本费用 → 门店贡献利润估算">
+178
View File
@@ -0,0 +1,178 @@
/**
* 决策中心页面
*
* 聚合利润机会、风险告警、异常任务 3 类信号为统一决策卡列表,
* 支持筛选、查看详情和审批操作。
* 阶段一审批状态暂存 localStorage,阶段二将持久化到后端。
*/
import { useState, useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Sparkles } from 'lucide-react'
import api from '@/lib/api'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import { DecisionCardItem } from '@/components/DecisionCard'
import { DecisionDrawer } from '@/components/DecisionDrawer'
import { aggregateDecisions, filterDecisions, type DecisionCard } from '@/lib/decision-aggregator'
import { cn } from '@/lib/utils'
type FilterType = 'all' | 'pending' | 'approved' | 'verified'
const FILTERS: { key: FilterType; label: string }[] = [
{ key: 'all', label: '全部' },
{ key: 'pending', label: '待审批' },
{ key: 'approved', label: '已批准' },
{ key: 'verified', label: '已验证' },
]
export function DecisionsPage() {
const [month, setMonth] = useState('2026-04')
const [filter, setFilter] = useState<FilterType>('all')
const [selected, setSelected] = useState<DecisionCard | null>(null)
// 审批状态(阶段一暂存 localStorage
const [approvedIds, setApprovedIds] = useState<string[]>(() => {
try {
return JSON.parse(localStorage.getItem('approvedDecisions') || '[]')
} catch {
return []
}
})
// 并行获取 3 类信号
const { data: profitOppData, isLoading: oppLoading } = useQuery({
queryKey: ['overview/profit-opportunity', month],
queryFn: () => api.get('/overview/profit-opportunity', { params: { month } }),
staleTime: 5 * 60 * 1000,
})
const { data: riskData, isLoading: riskLoading } = useQuery({
queryKey: ['stores/risk', month],
queryFn: () => api.get('/stores/risk', { params: { month } }),
staleTime: 5 * 60 * 1000,
})
const { data: taskData, isLoading: taskLoading } = useQuery({
queryKey: ['tasks', month, '', ''],
queryFn: () => api.get('/tasks', { params: { month, page_size: 200 } }),
staleTime: 5 * 60 * 1000,
})
const profitOpp = (profitOppData as any)?.data?.items || []
const riskRows = (riskData as any)?.data || []
const tasks = (taskData as any)?.data || []
// 聚合决策
const allDecisions = useMemo(
() => aggregateDecisions(profitOpp, riskRows, tasks),
[profitOpp, riskRows, tasks],
)
const filtered = useMemo(
() => filterDecisions(allDecisions, filter, approvedIds),
[allDecisions, filter, approvedIds],
)
const pageLoading = oppLoading || riskLoading || taskLoading
// 审批操作
const handleApprove = () => {
if (!selected) return
const newIds = [...new Set([...approvedIds, selected.id])]
setApprovedIds(newIds)
localStorage.setItem('approvedDecisions', JSON.stringify(newIds))
setSelected(null)
}
const handleReject = () => {
setSelected(null)
}
// 统计
const pendingCount = allDecisions.filter((d) => !approvedIds.includes(d.id)).length
const totalImpact = allDecisions
.filter((d) => !approvedIds.includes(d.id))
.reduce((s, d) => s + d.impactAmount, 0)
return (
<div className="space-y-4">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div>
<h1 className="flex items-center gap-2 text-xl font-bold">
<Sparkles className="h-5 w-5 text-purple-600" />
</h1>
<p className="mt-1 text-sm text-muted-foreground">
</p>
</div>
<div className="text-right text-sm">
<p className="text-muted-foreground"> <b className="text-foreground">{pendingCount}</b> </p>
<p className="text-xs text-muted-foreground"> <b className="text-green-600">¥{totalImpact.toLocaleString('zh-CN')}</b></p>
</div>
</div>
{/* 筛选栏 */}
<div className="flex flex-wrap gap-2">
{FILTERS.map((f) => (
<button
key={f.key}
className={cn(
'rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
filter === f.key ? 'bg-purple-600 text-white' : 'border hover:bg-muted',
)}
onClick={() => setFilter(f.key)}
>
{f.label} {f.key === 'all' ? allDecisions.length : f.key === 'pending' ? pendingCount : f.key === 'approved' ? approvedIds.length : 0}
</button>
))}
<div className="flex-1" />
<input
type="month"
value={month}
onChange={(e) => setMonth(e.target.value)}
className="rounded-md border px-2 py-1 text-sm"
/>
</div>
{/* 决策列表 */}
{pageLoading ? (
<LoadingSpinner />
) : filtered.length === 0 ? (
<div className="rounded-lg border bg-card py-20 text-center text-sm text-muted-foreground">
</div>
) : (
<div className="overflow-hidden rounded-lg border bg-card">
{/* 表头 */}
<div className="flex items-center gap-3 border-b bg-muted/30 px-4 py-2 text-xs font-medium text-muted-foreground">
<span className="w-7" />
<span className="flex-1"> / </span>
<span className="w-24 text-right"></span>
<span className="w-16 text-center"></span>
<span className="w-4" />
</div>
{/* 决策卡列表 */}
{filtered.map((d, i) => (
<DecisionCardItem
key={d.id}
decision={d}
index={i}
approved={approvedIds.includes(d.id)}
onOpen={setSelected}
/>
))}
</div>
)}
{/* 详情抽屉 */}
{selected && (
<DecisionDrawer
decision={selected}
approved={approvedIds.includes(selected.id)}
onClose={() => setSelected(null)}
onApprove={handleApprove}
onReject={handleReject}
/>
)}
</div>
)
}
+268
View File
@@ -0,0 +1,268 @@
/**
* 知识资产页面
*
* 展示标杆实践(standardized_practice)列表,支持按模块和状态筛选,
* 点击可查看推广结果,支持推广到指定门店。
*/
import { useState, useMemo } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Library, X } from 'lucide-react'
import api from '@/lib/api'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import { KnowledgeCard } from '@/components/KnowledgeCard'
import { Badge } from '@/components/Badge'
import { cn } from '@/lib/utils'
export function KnowledgePage() {
const queryClient = useQueryClient()
const [moduleFilter, setModuleFilter] = useState('')
const [statusFilter, setStatusFilter] = useState('')
const [selected, setSelected] = useState<any | null>(null)
// 获取标杆实践列表
const { data: practicesData, isLoading } = useQuery({
queryKey: ['tasks/practices', moduleFilter, statusFilter],
queryFn: () =>
api.get('/tasks/practices', {
params: { module: moduleFilter || undefined, status: statusFilter || undefined },
}),
})
const practices = (practicesData as any)?.data || []
// 获取选中实践的推广结果
const { data: replicationData } = useQuery({
queryKey: ['tasks/practices', selected?.id, 'replication-result'],
queryFn: () => api.get(`/tasks/practices/${selected?.id}/replication-result`),
enabled: !!selected?.id,
})
const replications = (replicationData as any)?.data || []
// 推广操作
const replicateMutation = useMutation({
mutationFn: (data: { trial_store_code: string; trial_store_name: string; observation_weeks?: number }) =>
api.post(`/tasks/practices/${selected?.id}/replicate`, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks/practices', selected?.id, 'replication-result'] })
},
})
const promoteMutation = useMutation({
mutationFn: () => api.post(`/tasks/practices/${selected?.id}/promote`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks/practices'] })
setSelected(null)
},
})
// 统计
const stats = useMemo(() => {
const total = practices.length
const byModule = practices.reduce((acc: any, p: any) => {
acc[p.practice_module] = (acc[p.practice_module] || 0) + 1
return acc
}, {})
const byStatus = practices.reduce((acc: any, p: any) => {
acc[p.status] = (acc[p.status] || 0) + 1
return acc
}, {})
return { total, byModule, byStatus }
}, [practices])
// 模块列表(从数据中提取)
const modules = useMemo(
() => Array.from(new Set(practices.map((p: any) => p.practice_module))).filter(Boolean),
[practices],
)
const statuses = ['待推广', '推广中', '已推广']
return (
<div className="space-y-4">
{/* 页面标题 */}
<div>
<h1 className="flex items-center gap-2 text-xl font-bold">
<Library className="h-5 w-5 text-teal-600" />
</h1>
<p className="mt-1 text-sm text-muted-foreground">
AI
</p>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<div className="rounded-lg border bg-card p-3">
<div className="flex items-center gap-2">
<Library className="h-4 w-4 text-teal-600" />
<span className="text-xs text-muted-foreground"></span>
</div>
<b className="mt-1 block text-2xl font-bold text-teal-600">{stats.total}</b>
</div>
{Object.entries(stats.byStatus).map(([status, count]: any) => (
<div key={status} className="rounded-lg border bg-card p-3">
<span className="text-xs text-muted-foreground">{status}</span>
<b className="mt-1 block text-2xl font-bold">{count}</b>
</div>
))}
</div>
{/* 筛选栏 */}
<div className="flex flex-wrap gap-2">
<button
className={cn(
'rounded-md px-3 py-1.5 text-sm font-medium',
!moduleFilter ? 'bg-teal-600 text-white' : 'border hover:bg-muted',
)}
onClick={() => setModuleFilter('')}
>
</button>
{modules.map((m: any) => (
<button
key={m}
className={cn(
'rounded-md px-3 py-1.5 text-sm font-medium',
moduleFilter === m ? 'bg-teal-600 text-white' : 'border hover:bg-muted',
)}
onClick={() => setModuleFilter(m)}
>
{m}
</button>
))}
<div className="flex-1" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="rounded-md border px-2 py-1 text-sm"
>
<option value=""></option>
{statuses.map((s) => (
<option key={s} value={s}>{s}</option>
))}
</select>
</div>
{/* 知识列表 */}
{isLoading ? (
<LoadingSpinner />
) : practices.length === 0 ? (
<div className="rounded-lg border bg-card py-20 text-center">
<Library className="mx-auto h-12 w-12 text-muted-foreground/40" />
<p className="mt-3 text-sm text-muted-foreground"></p>
<p className="mt-1 text-xs text-muted-foreground"></p>
</div>
) : (
<div className="space-y-2">
{practices.map((p: any) => (
<KnowledgeCard key={p.id} practice={p} onClick={() => setSelected(p)} />
))}
</div>
)}
{/* 详情弹窗 */}
{selected && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30" onClick={() => setSelected(null)}>
<div
className="max-h-[85vh] w-[640px] max-w-[95vw] overflow-y-auto rounded-lg border bg-card p-6 shadow-lg"
onClick={(e) => e.stopPropagation()}
>
{/* 标题栏 */}
<div className="mb-4 flex items-start justify-between">
<div>
<span className="rounded bg-teal-50 px-1.5 py-0.5 text-[10px] font-medium text-teal-700">
{selected.practice_module}
</span>
<h2 className="mt-1 text-base font-bold"> #{selected.id}</h2>
</div>
<button onClick={() => setSelected(null)} className="rounded-md p-1 hover:bg-muted">
<X className="h-5 w-5" />
</button>
</div>
{/* 内容 */}
<div className="space-y-4">
{/* 标杆门店 */}
<div>
<h3 className="mb-1 text-sm font-bold"></h3>
<p className="text-sm">{selected.benchmark_store_name}{selected.benchmark_store_code}</p>
</div>
{/* 关键行动 */}
<div>
<h3 className="mb-1 text-sm font-bold"></h3>
<div className="rounded-lg border bg-muted/20 p-3">
{selected.key_actions?.split('\n').map((line: string, i: number) => (
<p key={i} className="text-xs leading-relaxed text-muted-foreground">
{line || '\u00A0'}
</p>
)) || <p className="text-xs text-muted-foreground"></p>}
</div>
</div>
{/* 验证指标 */}
{selected.verification_indicators && (
<div>
<h3 className="mb-1 text-sm font-bold"></h3>
<p className="text-sm text-muted-foreground">{selected.verification_indicators}</p>
</div>
)}
{/* 状态 */}
<div>
<h3 className="mb-1 text-sm font-bold"></h3>
<Badge type="default" text={selected.status} />
</div>
{/* 推广结果 */}
<div>
<h3 className="mb-2 text-sm font-bold">广</h3>
{replications.length === 0 ? (
<p className="text-xs text-muted-foreground">广</p>
) : (
<div className="space-y-2">
{replications.map((r: any) => (
<div key={r.id} className="rounded-lg border p-2 text-xs">
<div className="flex items-center justify-between">
<b>{r.trial_store_name}</b>
<Badge type="status" text={r.status || '观察中'} />
</div>
<p className="mt-1 text-muted-foreground">
: {r.observation_weeks} · : {r.before_value ?? '-'} · : {r.after_value ?? '-'}
</p>
</div>
))}
</div>
)}
</div>
</div>
{/* 底部操作 */}
<div className="mt-4 flex gap-2 border-t pt-4">
<button
className="flex-1 rounded-md border px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-muted"
onClick={() => {
const code = prompt('请输入试点门店编码')
if (!code) return
const name = prompt('请输入试点门店名称') || code
replicateMutation.mutate({ trial_store_code: code, trial_store_name: name })
}}
>
广
</button>
{selected.status !== '已推广' && (
<button
className="flex-1 rounded-md bg-teal-600 px-4 py-2 text-sm font-medium text-white hover:bg-teal-700"
onClick={() => promoteMutation.mutate()}
disabled={promoteMutation.isPending}
>
{promoteMutation.isPending ? '处理中…' : '标记为已推广'}
</button>
)}
</div>
</div>
</div>
)}
</div>
)
}
+30
View File
@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
import { ChevronRight } from 'lucide-react'
import api from '@/lib/api'
import { Badge } from '@/components/Badge'
import { FilterableTable } from '@/components/FilterableTable'
@@ -18,6 +19,11 @@ export function TasksPage() {
queryKey: ['tasks', month, priority, status],
queryFn: () => api.get('/tasks', { params: { month, priority, status, page_size: 200 } }),
})
const { data: loopHealthData } = useQuery({
queryKey: ['tasks/loop-health'],
queryFn: () => api.get('/tasks/loop-health'),
staleTime: 5 * 60 * 1000,
})
const tasks = (data as any)?.data || []
const total = (data as any)?.meta?.total || 0
@@ -49,6 +55,30 @@ export function TasksPage() {
</div>
</div>
{/* 闭环流程条 */}
<div className="flex flex-wrap items-center gap-1 rounded-lg border bg-card p-3">
{[
{ label: '信号发现', count: '1,284', rate: (loopHealthData as any)?.data?.task_generation_rate },
{ label: '决策审批', count: String(tasks.length), rate: null },
{ label: '任务执行', count: String(total), rate: (loopHealthData as any)?.data?.store_execution_rate },
{ label: '周期检查', count: '-', rate: (loopHealthData as any)?.data?.weekly_check_rate },
{ label: '收益验收', count: String(tasks.filter((t: any) => t.status === '已验收').length), rate: (loopHealthData as any)?.data?.monthly_review_rate },
{ label: '经验标准化', count: '14', rate: (loopHealthData as any)?.data?.practice_promotion_rate },
].map((stage, i, arr) => (
<div key={stage.label} className="flex items-center gap-1">
<div className="flex flex-col items-center px-2">
<span className={`flex h-6 w-6 items-center justify-center rounded-full text-[10px] font-bold ${i === 0 ? 'bg-blue-600 text-white' : 'bg-muted text-muted-foreground'}`}>
{i + 1}
</span>
<small className="mt-1 text-[10px] text-muted-foreground">{stage.label}</small>
<b className="text-sm font-bold">{stage.count}</b>
{stage.rate != null && <em className="text-[10px] text-green-600">{Math.round(Number(stage.rate) * 100) / 100}% </em>}
</div>
{i < arr.length - 1 && <ChevronRight className="h-4 w-4 text-muted-foreground" />}
</div>
))}
</div>
{/* 筛选器 */}
<div className="flex flex-wrap gap-2">
<SearchSelect
@@ -0,0 +1,648 @@
# 玄谋智脑实际版建设方案
> 基于 RestaurantChainBrainDemo 概念原型,在 SBrainCO 生产系统中构建"经营决策操作系统"实际版。
## 一、背景与目标
### 1.1 背景
当前工作区有两个项目:
| 项目 | 定位 | 代码量 | 数据 | 部署 |
|------|------|--------|------|------|
| **RestaurantChainBrainDemo** | 概念演示原型 | 75行(5个文件) | 硬编码Mock数据 | 静态文件 |
| **SBrainCO** | 生产级全栈应用 | 32,643行(99前端+30后端) | 真实PostgreSQL | dm.all8ai.top |
Demo 展示了"玄谋智脑"的6大模块产品概念(经营指挥舱、决策中心、AI员工组织、任务闭环、经营本体、知识资产),但全部基于Mock数据。SBrainCO 已有真实数据、完整后端和部署体系,但缺少 Demo 中描绘的"决策中心""AI员工组织""知识资产"3个模块,且"指挥舱"和"任务闭环"已有但未达到 Demo 的交互水平。
### 1.2 目标
在 SBrainCO 中构建 Demo 的6大模块**实际版**,用真实数据和API替代Mock数据,实现"信号→决策→任务→验收→知识"的完整管理闭环。
### 1.3 原则
- **在SBrainCO中改**,不在Demo中改——数据、后端、部署都在SBrainCO
- **复用现有基础设施**——不重建已有的API、表、组件
- **Demo作为UI参考**——借鉴交互设计和信息架构,不复用代码
- **分阶段交付**——先MVP再增强,每阶段可独立上线
## 二、现状分析
### 2.1 SBrainCO 现有路由(45个页面)
```
/ DashboardPage 仪表盘
/boss BossPage 老板驾驶舱(利润机会池+风险态势+瀑布图)
/revenue RevenuePage 营收分析
/bank BankPage 银行流水
/central-kitchen CentralKitchenPage 中央厨房
/distribution-reconciliation 配送对账
/bom-penetration BomPenetrationPage BOM穿透
/production-plan ProductionPlanPage 生产计划
/regional RegionalPage 区域分析
/store StorePage 门店列表
/stores/:code StoreDetailPage 门店详情(深度诊断)
/tasks TasksPage 任务列表
/tasks/:id TaskDetailPage 任务详情
/monthly-review MonthlyReviewPage 月度验收
/sku SKUPage SKU分析
/cost CostPage 成本总览
/cost-analysis CostAnalysisPage 成本分析(9个Tab
/store-expense StoreExpensePage 门店费用
/platform PlatformPage 平台经济
/member MemberPage 会员分析
/risk RiskPage 风险态势
/time TimePage 时段分析
/data-quality DataQualityPage 数据质量
/indicators IndicatorsPage 指标管理
/ontology OntologyPage 经营本体
/site-selection SiteSelectionPage 选址分析
/smart-scheduling SmartSchedulingPage 智能排班
/situational-awareness 态势感知
/member-ltv MemberLTVPage 会员LTV
/menu-engineering MenuEngineeringPage 菜单工程
/region-comparison RegionComparisonPage 区域对比
/employee-performance 员工绩效
/inventory-turnover 库存周转
/tenant-management 租户管理
/tenant-users 租户用户
/target TargetManagementPage 目标管理
/alert AlertManagementPage 告警管理
/scheduler SchedulerManagementPage 调度管理
/store-grade StoreGradePage 门店分级
/product-lifecycle ProductLifecyclePage 产品生命周期
/data-import DataImportPage 数据导入
/enterprise EnterprisePage 企业看板
/intelligence IntelligencePage 智能中心
/promotion PromotionPage 促销分析
```
### 2.2 SBrainCO 现有后端路由(18个文件)
| 文件 | 主要功能 |
|------|---------|
| data.ts | 概览、瀑布图、利润机会池、风险评级、同比环比 |
| tasks.ts | 任务CRUD、自动生成、周检、月度验收、标杆实践、闭环健康度 |
| cost-analysis.ts | 成本总览、品类对比、毛利率偏差、门店成本(9个Tab) |
| intelligence.ts | IoT设备、AI预测、AI调整目标、MRP、ROI计算、品牌资产 |
| alert.ts | 告警规则、告警日志、告警总览 |
| situational-awareness.ts | 态势感知、门店诊断 |
| enterprise.ts | 企业看板、跨店对比 |
| product.ts | 产品生命周期、ABC分析 |
| promotion.ts | 促销分析、活动ROI |
| store-expense.ts | 门店费用、人工水电 |
| store-grade.ts | 门店分级 |
| target.ts | 目标管理、偏差分析 |
| smart-scheduling.ts | 智能排班 |
| scheduler.ts | 定时任务调度 |
| tts.ts | 语音合成 |
| auth.ts | 登录认证(多租户) |
| admin.ts | 平台管理、租户用户管理 |
### 2.3 已有数据库表(与6大模块相关)
```sql
-- 任务闭环相关(已存在)
analytics.store_task -- 整改任务
analytics.store_task_log -- 任务日志
analytics.task_template -- 任务模板
analytics.task_weekly_check -- 周度检查
analytics.task_monthly_review -- 月度验收
-- 知识资产相关(已存在)
analytics.standardized_practice -- 标杆实践
analytics.practice_replication -- 经验推广
-- 告警相关(已存在)
analytics.v3_alert_rule -- 告警规则
analytics.v3_alert_log -- 告警日志
-- 目标管理(已存在)
analytics.v3_store_monthly_target -- 门店月度目标
analytics.v3_target_variance -- 目标偏差
analytics.v3_store_grade -- 门店分级
```
### 2.4 Demo 6大模块 vs SBrainCO 现有基础
| Demo模块 | SBrainCO现有基础 | 缺口 | 工作量 |
|---------|----------------|------|--------|
| 经营指挥舱 | BossPage(真实API、利润机会池、风险态势、瀑布图) | 缺少"待决策清单"和"闭环健康度"可视化 | 小 |
| 决策中心 | 利润机会API(6类机会)、风险评级、告警规则 | 缺少"决策卡"抽象:把机会/风险/告警统一为决策对象,加审批流 | 中 |
| AI员工组织 | scheduler/task-handlers、intelligence.tsAI预测/调整) | 缺少"AI员工"定义表和配置UI | 中 |
| 任务闭环 | tasks.ts(完整CRUD+周检+月度验收+loop-health | UI已有TasksPage,缺少闭环可视化 | 小 |
| 经营本体 | OntologyPage + docs/智脑实施方法论 + 数据库schema | 现有OntologyPage可能只是静态展示 | 小 |
| 知识资产 | standardized_practice表 + practice_replication表 | 缺少知识库浏览UI和统计面板 | 小 |
## 三、总体架构
### 3.1 模块关系图
```
┌─────────────────────────────────────────┐
│ 经营指挥舱 (/boss) │
│ KPI · 利润机会 · 风险态势 · 闭环健康度 │
└──────────┬──────────────────────────────┘
│ 聚合
┌──────────▼──────────────────────────────┐
│ 决策中心 (/decisions) │
│ 决策卡 · 证据链 · 审批流 · 影响评估 │
└──────────┬──────────────────────────────┘
│ 审批后生成
┌──────────▼──────────────────────────────┐
│ 任务闭环 (/tasks) │
│ 信号→决策→任务→周检→验收→知识 │
└──────┬────────────┬─────────────────────┘
│ │ 验收后沉淀
┌──────▼──┐ ┌─────▼──────────────────────┐
│ AI员工 │ │ 知识资产 (/knowledge) │
│ 组织 │ │ SOP · 标杆案例 · 规则 · 术语 │
│(/agents) │ └────────────────────────────┘
└─────────┘
┌─────────────────────────────────────────┐
│ 经营本体 (/ontology) │
│ 对象域 · 关系 · 指标 · 规则(底层语义层) │
└─────────────────────────────────────────┘
```
### 3.2 数据流
```
真实数据源 API层 前端页面
─────────── ────── ─────────
mv_store_risk_rating ──→ /overview/risk ──→ 指挥舱·风险态势
profit-opportunity ──→ /overview/profit-opp ──→ 指挥舱·利润机会
v3_alert_log ──→ /alert/overview ──→ 决策中心·告警决策
profit-opportunity ──→ /overview/profit-opp ──→ 决策中心·机会决策
store_task ──→ /tasks ──→ 任务闭环
loop-health ──→ /tasks/loop-health ──→ 指挥舱·闭环健康度
standardized_practice ──→ /tasks/practices ──→ 知识资产
practice_replication ──→ /tasks/practices/:id ──→ 知识资产·推广结果
(新增)decision ──→ /decisions ──→ 决策中心
(新增)agent_config ──→ /agents ──→ AI员工组织
```
### 3.3 前端导航结构(方案A+:最小改动 + 关键归拢)
**设计原则**:不碰分析类分组(收入/成本/运营/风险4组共20项完全不动),新增"智脑决策"分组作为核心入口,把闭环相关的散落菜单归拢过来,解决"V3.0系统闭环"命名问题,经营本体独立成组。
**调整后菜单结构**
```
经营总览(不动) 4项
老板驾驶舱 (/boss) ← 升级
总部驾驶舱 (/)
态势感知 (/situational-awareness)
银行授信 (/bank)
智脑决策(新增) 7项 ← 核心入口,完整闭环
决策中心 (/decisions) ★新增
AI员工组织 (/agents) ★新增
知识资产 (/knowledge) ★新增
任务管理 (/tasks) ← 从"角色工作台"移入
月度验收 (/monthly-review) ← 从"角色工作台"移入
预警管理 (/alert) ← 从"V3.0系统闭环"移入
目标管理 (/target) ← 从"V3.0系统闭环"移入
角色工作台(瘦身) 3项 ← 去掉任务管理和月度验收
区域经理 (/regional)
区域对比 (/region-comparison)
店长工作台 (/store)
收入分析(不动) 4项
成本与费用(不动) 8项
运营分析(不动) 6项
风险与选址(不动) 2项
系统配置(V3.0系统闭环改名) 6项 ← 只改名+移出2项
门店分级 (/store-grade)
调度管理 (/scheduler)
产品生命周期 (/product-lifecycle)
数据采集 (/data-import)
企业协同 (/enterprise)
智能升级 (/intelligence)
系统管理(瘦身) 3项 ← 去掉本体标准
用户管理 (/tenant-users)
数据质量 (/data-quality)
指标字典 (/indicators)
经营本体(从系统管理提升) 1项 ← 独立成组,突出6大模块地位
本体标准 (/ontology)
平台管理(不动) 1项
租户管理 (/tenant-management)
```
**调整明细**
| 操作 | 菜单项 | 原分组 | 目标分组 |
|------|--------|--------|---------|
| 新增 | 决策中心 | - | 智脑决策 |
| 新增 | AI员工组织 | - | 智脑决策 |
| 新增 | 知识资产 | - | 智脑决策 |
| 移动 | 任务管理 | 角色工作台 | 智脑决策 |
| 移动 | 月度验收 | 角色工作台 | 智脑决策 |
| 移动 | 预警管理 | V3.0系统闭环 | 智脑决策 |
| 移动 | 目标管理 | V3.0系统闭环 | 智脑决策 |
| 移动 | 本体标准 | 系统管理 | 经营本体(独立分组) |
| 改名 | V3.0系统闭环 → 系统配置 | - | - |
**不动的分组**:经营总览、收入分析、成本与费用、运营分析、风险与选址、平台管理(共6组25项完全不动)
## 四、分阶段建设方案
### 阶段一:MVP(3个新页面 + 2个升级)
**目标**:新增3个页面,复用现有API,让6大模块全部可见
#### 4.1.1 决策中心 (/decisions) — 新增
**数据来源**:聚合现有3类信号
| 信号源 | API | 决策类型 |
|--------|-----|---------|
| 利润机会 | `/overview/profit-opportunity` | 机会决策(6类改善方向) |
| 风险告警 | `/alert/overview` + `/overview/risk` | 风险决策(红黄门店) |
| 异常任务 | `/tasks?status=异常` | 执行决策(逾期/未改善) |
**页面结构**
```
决策中心
├── 筛选栏:全部 / 待审批 / 执行中 / 已验证
├── 决策卡列表(表格)
│ ├── 决策标题 + 依据
│ ├── 自治等级(L1人工审批 / L2受控 / L3自动)
│ ├── 预计影响(金额)
│ ├── 责任人 + 截止时间
│ └── 状态(待审批 / 执行中 / 已验证)
└── 决策详情抽屉(点击展开)
├── AI建议置信度
├── 为什么现在行动(原因)
├── 证据链(数据来源、基准对比、历史案例)
├── 影响评估(金额 + 等级)
└── 审批操作(批准 / 退回)
```
**MVP实现方式**
- 不新建 decision 表,前端聚合3类API数据为"决策卡"格式
- 审批操作暂存前端状态(localStorage),后续阶段再持久化
- 决策详情中的"证据链"从对应API的detail字段提取
#### 4.1.2 AI员工组织 (/agents) — 新增
**数据来源**:配置文件(10个AI员工角色定义)
**AI员工定义**(基于Demo + SBrainCO实际能力):
| AI员工 | 岗位目标 | 对应SBrainCO能力 | 自治等级 |
|--------|---------|----------------|---------|
| 经营参谋长 | 消解目标冲突、生成老板决策清单 | BossPage聚合 | L1 |
| 门店经营分析师 | 门店分级、异常归因与增收机会 | 风险评级+利润机会 | L1 |
| 菜品利润优化师 | 菜品真实贡献与菜单工程 | cost-analysis + menu-engineering | L1 |
| 费用稽核员 | 门店损益、费用异常与追责 | store-expense | L1 |
| 需求预测员 | 菜品与原料日周需求预测 | intelligence.ts/ai/forecast | L1 |
| 智能补货员 | 订货量、调拨与最晚下单日 | intelligence.ts/mrp | L2 |
| 经营督办员 | 异常转任务、周检与月度验收 | tasks.ts + scheduler | L1 |
| 经营稽核员 | 退款、抹零与异常操作审查 | alert.ts | L1 |
| 会员运营师 | 复购、流失与下一最佳行动 | member + member-ltv | L2 |
| 知识管理员 | SOP、标杆门店与案例资产化 | standardized_practice | L1 |
**页面结构**
```
AI员工组织
├── 经营参谋长(首席卡片)
│ ├── 今日生成决策数
│ └── 协调目标说明
├── AI员工网格(9个卡片)
│ ├── 员工名称 + 岗位
│ ├── 职责描述
│ ├── 自治等级
│ ├── 今日交付数
│ └── 状态(运行中 / 待审批 / 受控)
└── 点击展开:员工详情(配置、权限范围、SOP引用)
```
**MVP实现方式**
- 前端配置文件定义10个AI员工(`src/data/agents.ts`
- "今日交付数"从对应API动态获取(如经营参谋长→决策数,经营督办员→任务数)
- 不新建数据库表
#### 4.1.3 知识资产 (/knowledge) — 新增
**数据来源**`/tasks/practices` API(已存在)
**页面结构**
```
知识资产
├── 知识统计卡片
│ ├── 有效知识条目数(standardized_practice总数)
│ ├── 按模块分类(门店经营 / 商品利润 / 风险内控 / 组织效率)
│ └── 按状态分类(待推广 / 推广中 / 已验证)
├── 知识列表
│ ├── 实践标题 + 模块
│ ├── 标杆门店
│ ├── 关键行动
│ ├── 验证指标
│ ├── 引用次数 + 推广结果
│ └── 状态
└── 知识详情(点击展开)
├── 标杆门店信息
├── 关键行动详情
├── 验证指标与结果
├── 推广试点记录
└── 推广操作(推广到指定门店)
```
**MVP实现方式**
- 直接调用现有 `/tasks/practices``/tasks/practices/:id/replication-result` API
- 统计数据前端聚合计算
- 推广操作调用现有 `/tasks/practices/:id/replicate` API
#### 4.1.4 经营指挥舱升级 (/boss) — 升级现有
**新增内容**
1. **待决策清单**:从决策中心聚合TOP3决策,显示在指挥舱顶部
2. **闭环健康度可视化**:调用 `/tasks/loop-health` API,展示"信号→决策→任务→验收→知识"5阶段转化率
**不改动**:现有KPI卡片、利润机会池、风险态势、瀑布图保持不变
#### 4.1.5 任务闭环升级 (/tasks) — 升级现有
**新增内容**
1. **闭环流程条**:顶部展示"信号发现→决策审批→任务执行→周期检查→收益验收→经验标准化"流程条
2. **验收价值列**:在任务列表中增加"验收价值"列(从verification_result提取)
**不改动**:现有任务列表、筛选、详情页保持不变
### 阶段二:增强(决策持久化 + AI员工配置)
**目标**:让决策可持久化、可审计,AI员工可配置
#### 4.2.1 决策持久化
**新增数据库表**
```sql
CREATE TABLE analytics.decision_card (
decision_id SERIAL PRIMARY KEY,
decision_no TEXT UNIQUE NOT NULL, -- DEC-YYYYMMDD-NNN
source_type TEXT NOT NULL, -- profit_opportunity / risk_alert / task_anomaly
source_id TEXT, -- 源数据ID
level TEXT NOT NULL, -- L1人工审批 / L2受控 / L3自动
domain TEXT NOT NULL, -- 门店经营 / 商品利润 / 供应链 / 风险内控
title TEXT NOT NULL,
reason TEXT,
impact_amount NUMERIC,
impact_description TEXT,
confidence INT, -- AI建议置信度 0-100
evidence JSONB, -- 证据链
owner TEXT,
agent_name TEXT, -- 责任AI员工
deadline DATE,
status TEXT DEFAULT '待审批', -- 待审批 / 已批准 / 已退回 / 执行中 / 已验证
approved_by TEXT,
approved_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE analytics.agent_config (
agent_id SERIAL PRIMARY KEY,
agent_code TEXT UNIQUE NOT NULL, -- chief_of_staff / store_analyst / ...
agent_name TEXT NOT NULL, -- 经营参谋长 / 门店经营分析师 / ...
role_target TEXT NOT NULL, -- 岗位目标
domain TEXT NOT NULL, -- 负责领域
autonomy_level TEXT NOT NULL, -- L1 / L2 / L3
scope_stores TEXT[], -- 负责门店范围
permissions TEXT[], -- 权限列表
sop_refs TEXT[], -- 引用SOP
status TEXT DEFAULT '运行中',
today_deliveries INT DEFAULT 0,
config JSONB, -- 扩展配置
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
```
**新增后端API**
```
GET /decisions -- 决策列表(支持筛选)
GET /decisions/:id -- 决策详情
POST /decisions -- 手动创建决策
POST /decisions/:id/approve -- 批准决策(生成任务)
POST /decisions/:id/reject -- 退回决策
GET /agents -- AI员工列表
GET /agents/:code -- AI员工详情
PUT /agents/:code -- 更新AI员工配置
POST /decisions/auto-generate -- 自动生成决策(从利润机会+风险+告警)
```
#### 4.2.2 决策自动生成
定时任务(scheduler)每日早晨自动扫描:
1. 利润机会池 → 生成"机会决策"(6类改善方向各1条)
2. 红色门店 → 生成"风险决策"(P0级门店各1条)
3. 逾期任务 → 生成"执行决策"(逾期>7天的任务)
#### 4.2.3 AI员工配置管理
- 支持配置每个AI员工的自治等级、门店范围、权限
- "今日交付数"从对应API动态统计
- 支持启用/停用AI员工
### 阶段三:深化(自动闭环 + 知识沉淀)
**目标**:实现"信号→决策→任务→验收→知识"全自动闭环
#### 4.3.1 决策审批后自动生成任务
决策批准后,自动调用 `/tasks/auto-generate` 生成整改任务,关联决策ID。
#### 4.3.2 任务验收后自动沉淀知识
任务月度验收通过且收益达标时,自动提取:
- 关键行动 → standardized_practice.key_actions
- 验证指标 → standardized_practice.verification_indicators
- 标杆门店 → standardized_practice.benchmark_store_code
#### 4.3.3 知识自动推荐
新决策生成时,自动搜索 standardized_practice 匹配案例,作为证据链的一部分。
## 五、技术方案
### 5.1 前端技术栈(复用SBrainCO现有)
| 技术 | 用途 | 已有 |
|------|------|------|
| React + TypeScript | 框架 | ✅ |
| React Router | 路由 | ✅ |
| TanStack Query | 数据请求 | ✅ |
| Tailwind CSS | 样式 | ✅ |
| Recharts | 图表 | ✅ |
| Lucide React | 图标 | ✅ |
### 5.2 后端技术栈(复用SBrainCO现有)
| 技术 | 用途 | 已有 |
|------|------|------|
| Express + TypeScript | API框架 | ✅ |
| node-postgres | 数据库 | ✅ |
| JWT | 认证 | ✅ |
| 多租户中间件 | 租户隔离 | ✅ |
### 5.3 新增文件清单
```
client/src/
├── pages/
│ ├── DecisionsPage.tsx -- 决策中心(新增)
│ ├── AgentsPage.tsx -- AI员工组织(新增)
│ └── KnowledgePage.tsx -- 知识资产(新增)
├── components/
│ ├── DecisionCard.tsx -- 决策卡组件(新增)
│ ├── DecisionDrawer.tsx -- 决策详情抽屉(新增)
│ ├── AgentCard.tsx -- AI员工卡片(新增)
│ ├── KnowledgeCard.tsx -- 知识条目卡片(新增)
│ ├── LoopHealthBar.tsx -- 闭环健康度条(新增)
│ └── DecisionSummary.tsx -- 指挥舱待决策摘要(新增)
├── data/
│ └── agents.ts -- AI员工配置定义(新增,阶段一)
└── lib/
└── decision-aggregator.ts -- 决策聚合逻辑(新增)
server/src/
├── routes/
│ ├── decisions.ts -- 决策API(新增,阶段二)
│ └── agents.ts -- AI员工API(新增,阶段二)
└── sql/
└── 18_decision_agent.sql -- 决策卡+AI员工建表(新增,阶段二)
client/src/App.tsx -- 新增3条路由
client/src/components/Layout.tsx -- 新增导航分组
```
### 5.4 复用现有API清单
| API | 复用于 | 状态 |
|-----|--------|------|
| `GET /overview/profit-opportunity` | 决策中心·机会决策 + 指挥舱 | ✅ 已存在 |
| `GET /overview/risk` | 决策中心·风险决策 + 指挥舱 | ✅ 已存在 |
| `GET /alert/overview` | 决策中心·告警决策 | ✅ 已存在 |
| `GET /tasks` | 决策中心·执行决策 + 任务闭环 | ✅ 已存在 |
| `GET /tasks/loop-health` | 指挥舱·闭环健康度 | ✅ 已存在 |
| `GET /tasks/practices` | 知识资产 | ✅ 已存在 |
| `GET /tasks/practices/:id/replication-result` | 知识资产·推广结果 | ✅ 已存在 |
| `POST /tasks/practices/:id/replicate` | 知识资产·推广操作 | ✅ 已存在 |
| `GET /tasks/auto-generate` | 决策审批后生成任务 | ✅ 已存在 |
| `POST /intelligence/ai/forecast` | AI员工·需求预测员 | ✅ 已存在 |
| `POST /intelligence/ai/adjust-target` | AI员工·经营参谋长 | ✅ 已存在 |
| `POST /intelligence/chain/mrp` | AI员工·智能补货员 | ✅ 已存在 |
## 六、UI/UX 设计参考
### 6.1 设计原则(借鉴Demo
- **信息密度**:B端专业风格,信息密集但不拥挤
- **决策优先**:每个页面顶部都是"需要你做什么",而不是"数据展示"
- **证据可追溯**:每个决策/结论都有证据链,可点击查看数据来源
- **闭环可视化**:用流程条展示"信号→决策→任务→验收→知识"的转化
### 6.2 色彩规范(复用SBrainCO现有)
| 用途 | 色值 | Tailwind类 |
|------|------|-----------|
| 主色 | indigo-600 | text-indigo-600 |
| 成功 | emerald-600 | text-emerald-600 |
| 警告 | amber-600 | text-amber-600 |
| 危险 | rose-600 | text-rose-600 |
| 决策中心专属 | purple-600 | text-purple-600 |
| AI员工专属 | blue-600 | text-blue-600 |
| 知识资产专属 | teal-600 | text-teal-600 |
### 6.3 关键交互
- **决策卡**:点击展开右侧抽屉,显示证据链和审批操作
- **AI员工卡片**:hover显示职责详情,点击展开配置面板
- **闭环流程条**:5阶段横向展示,每阶段显示数量和转化率
- **知识条目**:点击展开推广结果,支持"推广到门店"操作
## 七、工作量估算
### 阶段一:MVP
| 任务 | 文件 | 工作量 |
|------|------|--------|
| 决策中心页面 | DecisionsPage.tsx + DecisionCard.tsx + DecisionDrawer.tsx | 中 |
| AI员工组织页面 | AgentsPage.tsx + AgentCard.tsx + agents.ts | 小 |
| 知识资产页面 | KnowledgePage.tsx + KnowledgeCard.tsx | 小 |
| 指挥舱升级 | BossPage.tsx + DecisionSummary.tsx + LoopHealthBar.tsx | 小 |
| 任务闭环升级 | TasksPage.tsx | 小 |
| 路由+导航 | App.tsx + Layout.tsx | 小 |
| 部署验证 | deploy.sh | 小 |
### 阶段二:增强
| 任务 | 文件 | 工作量 |
|------|------|--------|
| decision_card建表 | 18_decision_agent.sql | 小 |
| agent_config建表 | 18_decision_agent.sql | 小 |
| 决策API | decisions.ts | 中 |
| AI员工API | agents.ts | 小 |
| 决策自动生成 | scheduler/task-handlers.ts | 中 |
| 前端对接持久化 | DecisionsPage.tsx + AgentsPage.tsx | 中 |
### 阶段三:深化
| 任务 | 文件 | 工作量 |
|------|------|--------|
| 决策审批→自动生成任务 | decisions.ts + tasks.ts | 小 |
| 任务验收→自动沉淀知识 | tasks.ts | 中 |
| 知识自动推荐 | decisions.ts | 中 |
## 八、风险与应对
| 风险 | 影响 | 应对 |
|------|------|------|
| 利润机会API数据质量 | 决策中心展示的金额不准确 | 已修复门店级计算逻辑,后续持续监控 |
| 标杆实践表数据为空 | 知识资产页面无内容 | 阶段一先展示空状态,阶段三自动沉淀后逐步填充 |
| 决策聚合逻辑复杂 | 3类信号格式不统一 | 在 decision-aggregator.ts 中统一转换为决策卡格式 |
| AI员工"今日交付数"统计 | 需要调多个API | 阶段一用静态配置,阶段二从agent_config动态统计 |
## 九、验收标准
### 阶段一验收
- [ ] `/decisions` 页面可展示3类决策(机会/风险/执行),点击可展开详情
- [ ] `/agents` 页面可展示10个AI员工卡片,状态正确
- [ ] `/knowledge` 页面可展示标杆实践列表,支持推广操作
- [ ] `/boss` 指挥舱新增待决策清单和闭环健康度
- [ ] `/tasks` 任务页新增闭环流程条
- [ ] 侧边栏新增"智脑决策"导航分组
- [ ] 部署到 dm.all8ai.top 可正常访问
### 阶段二验收
- [ ] 决策审批后状态持久化,刷新页面不丢失
- [ ] AI员工配置可在页面修改并保存
- [ ] 定时任务每日自动生成决策卡
- [ ] 决策审批后自动生成整改任务
### 阶段三验收
- [ ] 任务验收通过后自动生成标杆实践条目
- [ ] 新决策生成时自动推荐匹配的历史案例
- [ ] "信号→决策→任务→验收→知识"全链路可追溯
## 十、里程碑
| 里程碑 | 内容 | 交付物 |
|--------|------|--------|
| M1 | 阶段一完成 | 3个新页面 + 2个升级页面,部署上线 |
| M2 | 阶段二完成 | 决策持久化 + AI员工配置 + 自动生成 |
| M3 | 阶段三完成 | 全自动闭环 + 知识自动沉淀 |
---
> 本方案基于 RestaurantChainBrainDemo 概念原型和 SBrainCO 生产系统现状编写。
> Demo 完成了产品概念验证,SBrainCO 将承载实际落地。
+1
View File
@@ -48,6 +48,7 @@
|------|------|
| [16-战略外脑实施方法论.md](16-战略外脑实施方法论.md) | 战略外脑七项能力、十二步工作法、治理规范、实施路径与验收标准 |
| [17-SBrainCO战略外脑建设对照表.md](17-SBrainCO战略外脑建设对照表.md) | 将方法论映射为 SBrainCO 的领域模型、产品模块、API 和分阶段待办 |
| [18-玄谋智脑实际版建设方案.md](18-玄谋智脑实际版建设方案.md) | 基于 Demo 概念原型,在 SBrainCO 中构建6大模块实际版的分阶段方案 |
| [调查表/战略外脑能力验收Checklist.csv](调查表/战略外脑能力验收Checklist.csv) | 35项战略外脑能力与实施验收检查项 |
## 文档目录结构
+34
View File
@@ -159,6 +159,40 @@ ssh ubuntu@152.136.182.184 'pm2 restart sbrain-server'
ssh ubuntu@152.136.182.184 'cat /opt/sbrain-server/.env'
```
## 数据库直连命令(调试用)
### 业务库 bill_query(通过 SSH 隧道连服务器 FRP 端口)
```bash
# 从本机通过 SSH 连接服务器,再连 FRP 隧道端口 15432(→ 本机 PG 5432
ssh ubuntu@152.136.182.184 "PGPASSWORD= psql -U freedak -h 127.0.0.1 -p 15432 -d bill_query -c \"SELECT count(*) FROM analytics.mv_store_risk_rating_monthly\""
# 多行 SQL 用双引号包裹,内部用单引号
ssh ubuntu@152.136.182.184 "PGPASSWORD= psql -U freedak -h 127.0.0.1 -p 15432 -d bill_query -c \"
SELECT store_name, risk_level, received
FROM analytics.mv_store_risk_rating_monthly
WHERE month_start = '2026-04-01'
ORDER BY received DESC LIMIT 5
\""
```
### 平台库 sbrain_admin(服务器本地 PG 5432,需密码)
```bash
ssh ubuntu@152.136.182.184 "PGPASSWORD=sbrain2026 psql -U sbrain_admin -h 127.0.0.1 -p 5432 -d sbrain_admin -c \"SELECT username, role FROM tenant_users\""
```
### 连接参数速查
| 数据库 | SSH 目标 | 端口 | 用户 | 密码 | PGPASSWORD |
|--------|---------|------|------|------|------------|
| bill_query(业务库) | ubuntu@152.136.182.184 | 15432 | freedak | 无(trust | 空 |
| sbrain_admin(平台库) | ubuntu@152.136.182.184 | 5432 | sbrain_admin | sbrain2026 | sbrain2026 |
> ⚠️ 业务库 15432 是 FRP 隧道端口,映射到本机 PG 5432。frpc 断开后 15432 不可用。
> ⚠️ 平台库 5432 是服务器本地 PostgreSQL,需 SCRAM 认证。
> ⚠️ SSH 命令中嵌套 psql 时,外层用双引号、内层 SQL 用双引号转义(`\"`),SQL 字符串用单引号。
## 注意事项
- **平台库 `sbrain_admin` 在服务器本地 PostgreSQL5432**,业务库 `bill_query` 在本机 PostgreSQL(通过 frp 暴露为 15432
+14 -7
View File
@@ -1156,13 +1156,17 @@ router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => {
SELECT * FROM analytics.mv_dish_sku_abc_monthly WHERE month_start = $1
),
-- 食材成本差异:改用 mv_store_theoretical_actual_cost_monthly(门店级理论vs实际成本)
-- 注意:排除理论成本口径异常门店(theoretical_cost_rate_pct < 10 或 comparison_status = '理论成本口径异常'
-- 这些门店POS系统未配置BOM,理论成本为0,差异=全部实际成本,会严重虚高利润机会
cost_diff AS (
SELECT
count(*) FILTER (WHERE food_cost_variance > 0) AS over_cost_stores,
round(sum(food_cost_variance) FILTER (WHERE food_cost_variance > 0)::numeric, 2) AS total_positive_variance,
round(sum(theoretical_cost)::numeric, 2) AS total_theoretical_cost,
round(sum(actual_food_cost)::numeric, 2) AS total_actual_cost,
round(sum(sales_received)::numeric, 2) AS total_received
count(*) FILTER (WHERE food_cost_variance > 0 AND theoretical_cost_rate_pct >= 10) AS over_cost_stores,
round(sum(food_cost_variance) FILTER (WHERE food_cost_variance > 0 AND theoretical_cost_rate_pct >= 10)::numeric, 2) AS total_positive_variance,
round(sum(theoretical_cost) FILTER (WHERE theoretical_cost_rate_pct >= 10)::numeric, 2) AS total_theoretical_cost,
round(sum(actual_food_cost) FILTER (WHERE theoretical_cost_rate_pct >= 10)::numeric, 2) AS total_actual_cost,
round(sum(sales_received) FILTER (WHERE theoretical_cost_rate_pct >= 10)::numeric, 2) AS total_received,
count(*) FILTER (WHERE food_cost_variance > 0 AND (theoretical_cost_rate_pct < 10 OR theoretical_cost_rate_pct IS NULL)) AS excluded_stores,
round(sum(food_cost_variance) FILTER (WHERE food_cost_variance > 0 AND (theoretical_cost_rate_pct < 10 OR theoretical_cost_rate_pct IS NULL))::numeric, 2) AS excluded_variance
FROM analytics.mv_store_theoretical_actual_cost_monthly
WHERE month_start = $1
),
@@ -1172,7 +1176,7 @@ router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => {
round(actual_food_cost_rate_pct::numeric, 2) AS actual_cost_rate,
round(theoretical_cost_rate_pct::numeric, 2) AS theoretical_cost_rate
FROM analytics.mv_store_theoretical_actual_cost_monthly
WHERE month_start = $1 AND food_cost_variance > 0
WHERE month_start = $1 AND food_cost_variance > 0 AND theoretical_cost_rate_pct >= 10
ORDER BY food_cost_variance DESC
LIMIT 5
),
@@ -1278,7 +1282,10 @@ router.get('/overview/profit-opportunity', async (req: AuthRequest, res) => {
'owner', '商品/供应链/门店',
'evidence', '采购价差、用量差、盘点差、报损差',
'detail', (SELECT
'超耗门店' || (SELECT over_cost_stores FROM cost_diff) || '家,实际食材成本' || (SELECT total_actual_cost FROM cost_diff) || '元 vs 理论成本' || (SELECT total_theoretical_cost FROM cost_diff) || '元,正差异合计' || (SELECT total_positive_variance FROM cost_diff) || '元。\n\n' ||
'超耗门店' || (SELECT over_cost_stores FROM cost_diff) || '家(已排除' || (SELECT excluded_stores FROM cost_diff) || '家理论成本口径异常门店),实际食材成本' || (SELECT total_actual_cost FROM cost_diff) || '元 vs 理论成本' || (SELECT total_theoretical_cost FROM cost_diff) || '元,正差异合计' || (SELECT total_positive_variance FROM cost_diff) || '元。\n\n' ||
CASE WHEN (SELECT excluded_stores FROM cost_diff) > 0 THEN
'⚠ 口径异常说明:\n' || (SELECT excluded_stores FROM cost_diff) || '家门店(工体店、北清路店等)POS系统未配置BOM,理论成本为0,差异=' || (SELECT excluded_variance FROM cost_diff) || '元已排除,不计入利润机会。需补齐BOM配置后方可纳入分析。\n\n'
ELSE '' END ||
'超耗TOP5门店(需优先排查):\n' ||
string_agg(store_name || ':差异' || diff_amount || '元(实际成本率' || actual_cost_rate || '% vs 理论' || theoretical_cost_rate || '%,实收' || received || '元)', '\n') ||
'\n\n行动指向:\n1)上述5家门店实际成本率均远超理论值,需逐店排查采购单价与BOM标准价差异;\n2)盘点差——核查月末盘点与系统库存一致性,差异>3%需复盘;\n3)报损差——对比报损记录与行业基准,报损率>2%的门店需检查存储和加工流程;\n4)30天目标:回收正差异的30%,预计节约' || round((SELECT total_positive_variance FROM cost_diff) * 0.30, 2) || '元。'