+ {/* 标题 */}
+
+
+
银行授信数据报告
+
企业经营数据 · 信用评估 · 2026年4月
+
+
+
+
+ {/* 评分规则配置面板 */}
+ {showConfig && (
+
+
+
+ {(Object.keys(config) as (keyof ScoringConfig)[]).map(key => {
+ const rule = config[key]
+ const labels: Record
= { netMargin: '净利率(%)', stability: '营收稳定性(CV)', storeHealth: '门店健康度(红色占比%)', scale: '规模体量(元)', discountRate: '优惠率(%)', memberShare: '会员占比(%)' }
+ return (
+
+
{labels[key]}
+
+
+ updateRule(key, 'weight', parseFloat(e.target.value) || 0)}
+ className="w-full rounded border px-2 py-1 text-xs" />
+
+
+
+ updateRule(key, 'excellent', parseFloat(e.target.value) || 0)}
+ className="w-full rounded border px-2 py-1 text-xs" />
+
+
+
+ updateRule(key, 'pass', parseFloat(e.target.value) || 0)}
+ className="w-full rounded border px-2 py-1 text-xs" />
+
+
+ )
+ })}
+
+
+ 权重总和: {scoring?.totalWeight || 0}%
+
+
+
+
+ )}
+
+ {/* ① 企业经营概况 */}
+
+
+
+
+
+
+
+
+
+
+
+
+ v >= 10000 ? `${(v / 10000).toFixed(0)}万` : v} tick={{ fontSize: 10 }} />
+ [formatCurrency(v), '日实收']} />
+
+
+
+
+
+
+ {/* ② 盈利能力分析 */}
+
+ {wf && (
+ <>
+
+ 0 ? 'good' : 'bad'} description={`净利率 ${wf.received > 0 ? formatPercent(wf.net_profit / wf.received * 100) : '-'}`} />
+ 0 ? wf.food_cost / wf.received * 100 : 0} format="percent" status={wf.received > 0 && wf.food_cost / wf.received * 100 > 35 ? 'warn' : 'good'} description="食材成本 ÷ 实收" />
+ 0 ? wf.total_expense / wf.received * 100 : 0} format="percent" status={wf.received > 0 && wf.total_expense / wf.received * 100 > 45 ? 'warn' : 'good'} description="经营费用 ÷ 实收" />
+ 0 ? wf.net_profit / wf.received * 100 : 0} format="percent" description="净利润占实收比例" />
+
+
+
+
+
+
+ v >= 10000 ? `${(v / 10000).toFixed(0)}万` : v} tick={{ fontSize: 10 }} />
+ [formatCurrency(v), '金额']} />
+ {
+ const { x, y, width, height, payload, fill } = props
+ if (!payload.value || payload.value === 0) return
+ const scale = height / payload.value
+ const newY = y - payload.base * scale
+ return
+ }}>
+ {waterfallChart.map((entry, i) => | )}
+
+
+
+
+ >
+ )}
+
+
+ {/* ③ 门店资产质量 */}
+
+
+
+
+
+ `${e.name}: ${e.value}家`}>
+ {riskPie.map((entry: any, i: number) => | )}
+
+ [`${v}家`, name]} />
+
+
+
+
+
+
+
+
+ v >= 10000 ? `${(v / 10000).toFixed(0)}万` : v} tick={{ fontSize: 10 }} />
+
+ [formatCurrency(v), '实收']} />
+
+
+
+
+
+
+
+ {/* ④ 经营稳定性指标 */}
+
+
+
+ 25 ? 'bad' : Number(ov?.discount_rate_pct) > 18 ? 'warn' : 'good'} description="优惠额 ÷ (实收+优惠额)" />
+ 15 ? 'good' : 'warn'} description="会员账单 ÷ 总账单" />
+
+
+ {channelPie.length > 0 && (
+
+
+
+ `${CHANNEL_LABELS[e.name] || e.name} ${totalChannel > 0 ? (e.value / totalChannel * 100).toFixed(1) : 0}%`}>
+ {channelPie.map((entry: any, i: number) => | )}
+
+ [formatCurrency(v), CHANNEL_LABELS[name] || name]} />
+
+
+
+ )}
+
+
+ {/* ⑤ 银行授信评估摘要 */}
+
+ {scoring && (
+ <>
+ {/* 信用等级总览 */}
+
+
+ {scoring.grade.grade}
+
+
+
{scoring.grade.label}
+
{scoring.totalScore} / 100 分
+
综合信用评分 · 权重总和 {scoring.totalWeight}%
+
+
+
+ {/* 各指标得分明细 */}
+
+
+
+
+ | 指标 |
+ 实际值 |
+ 优秀阈值 |
+ 及格阈值 |
+ 权重 |
+ 得分 |
+ 加权得分 |
+
+
+
+ {scoring.items.map(item => (
+
+ | {item.label} |
+ {item.unit === '元' ? formatCurrency(item.value) : `${item.value.toFixed(2)}${item.unit}`} |
+ {item.unit === '元' ? formatCurrency(item.rule.excellent) : `${item.rule.excellent}${item.unit}`} |
+ {item.unit === '元' ? formatCurrency(item.rule.pass) : `${item.rule.pass}${item.unit}`} |
+ {item.rule.weight}% |
+
+ = 80 ? '#22c55e' : item.score >= 50 ? '#eab308' : '#ef4444' }}>{item.score}
+ |
+ {(item.score * item.rule.weight / 100).toFixed(1)} |
+
+ ))}
+
+
+
+
+ {/* 风险提示 */}
+
+
风险提示
+ {scoring.items.filter(i => i.score < 60).map(i => (
+
+ ⚠
+ {i.label}:得分 {i.score},实际值 {i.unit === '元' ? formatCurrency(i.value) : `${i.value.toFixed(2)}${i.unit}`},{i.rule.higherIsBetter ? `建议提升至 ${i.unit === '元' ? formatCurrency(i.rule.excellent) : i.rule.excellent + i.unit} 以上` : `建议控制在 ${i.rule.excellent}${i.unit} 以内`}
+
+ ))}
+ {scoring.items.filter(i => i.score < 60).length === 0 && (
+
+ ✓
+ 各项指标均达标,经营状况良好
+
+ )}
+
+ >
+ )}
+
+
+ )
+}
diff --git a/server/src/routes/data.ts b/server/src/routes/data.ts
index e1f365e..8be775c 100644
--- a/server/src/routes/data.ts
+++ b/server/src/routes/data.ts
@@ -660,4 +660,83 @@ router.get('/revenue/daily-summary', async (req: AuthRequest, res) => {
}
})
+// 银行授信报告
+router.get('/bank/report', async (req: AuthRequest, res) => {
+ try {
+ const [overview, daily, waterfall, risk, channel, storeRanking] = await Promise.all([
+ query(`SELECT bill_count, received, avg_bill_value, discount_rate_pct, theoretical_margin_pct, member_bills, member_share_pct FROM analytics.mv_overview_monthly WHERE month = DATE '2026-04-01'`),
+ query(`SELECT business_date, bill_count, received, avg_bill_value, discount_rate_pct FROM analytics.mv_overview_daily WHERE month = DATE '2026-04-01' ORDER BY business_date`),
+ query(`
+ WITH full_scope AS (
+ SELECT r.received, COALESCE(e.actual_food_cost,0) AS food_cost, COALESCE(e.wage_expense,0) AS wage, COALESCE(e.rent_expense,0) AS rent,
+ COALESCE(e.utility_expense,0) AS utility, COALESCE(e.dorm_expense,0) AS dorm, COALESCE(e.delivery_commission_expense,0) AS commission,
+ COALESCE(e.card_fee_expense,0) AS card_fee, COALESCE(e.repair_clean_expense,0) AS repair, COALESCE(e.operating_expense,0) AS operating_expense
+ FROM analytics.mv_store_risk_rating r
+ LEFT JOIN analytics.mv_store_operating_expense_monthly e ON r.store_code = e.sales_store_code AND e.report_month = DATE '2026-04-01'
+ WHERE r.received IS NOT NULL
+ )
+ SELECT round(sum(received)::numeric,2) AS received, round(sum(food_cost)::numeric,2) AS food_cost, round(sum(wage)::numeric,2) AS wage,
+ round(sum(rent)::numeric,2) AS rent, round(sum(utility)::numeric,2) AS utility, round(sum(dorm)::numeric,2) AS dorm,
+ round(sum(commission)::numeric,2) AS commission, round(sum(card_fee+repair)::numeric,2) AS other_expense,
+ round(sum(operating_expense)::numeric,2) AS total_expense,
+ round(sum(received)-sum(food_cost)-sum(operating_expense),2) AS net_profit
+ FROM full_scope
+ `),
+ query(`SELECT risk_level, count(*) AS store_count, round(sum(received)::numeric,2) AS total_received, round(avg(theoretical_margin_pct)::numeric,2) AS avg_margin_pct, round(avg(discount_rate_pct)::numeric,2) AS avg_discount_pct FROM analytics.mv_store_risk_rating WHERE received IS NOT NULL GROUP BY risk_level ORDER BY risk_level`),
+ query(`SELECT round(sum(cash)::numeric,2) AS cash, round(sum(alipay)::numeric,2) AS alipay, round(sum(wechat)::numeric,2) AS wechat, round(sum(meituan)::numeric,2) AS meituan, round(sum(unionpay)::numeric,2) AS unionpay, round(sum(douyin)::numeric,2) AS douyin, round(sum(credit)::numeric,2) AS credit, round(sum(jd_delivery)::numeric,2) AS jd_delivery, round(sum(meituan_delivery)::numeric,2) AS meituan_delivery, round(sum(taobao_delivery)::numeric,2) AS taobao_delivery FROM analytics.v_channel_daily WHERE business_date >= DATE '2026-04-01'`),
+ query(`SELECT store_code, store_name, round(received::numeric,2) AS received, bill_count, round(avg_bill_value::numeric,2) AS avg_bill_value, round(discount_rate_pct::numeric,2) AS discount_rate_pct, round(theoretical_margin_pct::numeric,2) AS theoretical_margin_pct, risk_level FROM analytics.mv_store_risk_rating WHERE received IS NOT NULL ORDER BY received DESC`),
+ ])
+
+ const ov = overview.rows[0] as any
+ const dailyRows = daily.rows.map((r: any) => ({
+ ...r,
+ received: parseFloat(r.received),
+ bill_count: parseInt(r.bill_count),
+ avg_bill_value: parseFloat(r.avg_bill_value),
+ discount_rate_pct: parseFloat(r.discount_rate_pct),
+ }))
+ const wf = waterfall.rows[0] as any
+ const riskRows = risk.rows.map((r: any) => ({
+ ...r,
+ store_count: parseInt(r.store_count),
+ total_received: parseFloat(r.total_received),
+ }))
+ const ch = channel.rows[0] as any
+ const channelTotals: Record