/** * 决策聚合逻辑 * * 将利润机会、风险告警、异常任务 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 = { profit_opportunity: '利润机会', risk_alert: '风险告警', task_anomaly: '任务异常', } /** 自治等级标签 */ export const LEVEL_LABELS: Record = { L1: 'L1 人工审批', L2: 'L2 受控', L3: 'L3 自动', }