diff --git a/.kiro/specs/refactor-analysis-report/design.md b/.kiro/specs/refactor-analysis-report/design.md
new file mode 100644
index 0000000..b9abccb
--- /dev/null
+++ b/.kiro/specs/refactor-analysis-report/design.md
@@ -0,0 +1,502 @@
+# 设计文档:重构分析报告功能
+
+## 概述
+
+重构 AI 补货建议系统的分析报告功能,将现有的四模块宏观决策报告(整体态势研判/风险预警/采购策略/效果预期)替换为四大数据驱动板块(库存总体概览/销量分析/库存构成健康度/补货建议生成情况)。每个板块包含精确的统计数据和 LLM 生成的分析文本。
+
+核心设计变更:
+- 从"LLM 主导分析"转变为"数据统计 + LLM 辅助分析"模式
+- 使用 LangGraph 动态节点并发生成四个板块的 LLM 分析
+- 前端新增 Chart.js 图表支持库存健康度可视化
+- 数据库表结构完全重建,四个 JSON 字段分别存储各板块数据
+
+## 架构
+
+### 整体数据流
+
+```mermaid
+graph TD
+ A[allocate_budget 节点完成] --> B[generate_analysis_report 节点]
+ B --> C[统计计算阶段]
+ C --> C1[库存概览统计]
+ C --> C2[销量分析统计]
+ C --> C3[健康度统计]
+ C --> C4[补货建议统计]
+ C1 & C2 & C3 & C4 --> D[LangGraph 并发 LLM 分析]
+ D --> D1[库存概览 LLM 节点]
+ D --> D2[销量分析 LLM 节点]
+ D --> D3[健康度 LLM 节点]
+ D --> D4[补货建议 LLM 节点]
+ D1 & D2 & D3 & D4 --> E[汇总报告]
+ E --> F[Result_Writer 写入数据库]
+ F --> G[API 返回前端]
+ G --> H[Report_UI 渲染]
+```
+
+### LangGraph 并发子图设计
+
+在现有工作流 `allocate_budget → generate_analysis_report → END` 中,`generate_analysis_report` 节点内部使用一个 LangGraph 子图实现并发:
+
+```mermaid
+graph TD
+ START[统计计算完成] --> FORK{并发分发}
+ FORK --> N1[inventory_overview_llm]
+ FORK --> N2[sales_analysis_llm]
+ FORK --> N3[inventory_health_llm]
+ FORK --> N4[replenishment_summary_llm]
+ N1 & N2 & N3 & N4 --> JOIN[汇总合并]
+ JOIN --> END_NODE[返回完整报告]
+```
+
+实现方式:使用 `langgraph.graph.StateGraph` 构建子图,通过 `add_node` 添加四个并发 LLM 节点,使用 fan-out/fan-in 模式(从 START 分发到四个节点,四个节点汇聚到 END)。每个节点独立调用 LLM,失败不影响其他节点。
+
+## 组件与接口
+
+### 1. 统计计算模块 (analysis_report_node.py)
+
+#### `calculate_inventory_overview(part_ratios: list[dict]) -> dict`
+计算库存总体概览统计数据。有效库存使用 `valid_storage_cnt`(三项公式:in_stock_unlocked_cnt + on_the_way_cnt + has_plan_cnt)。
+
+输入:PartRatio 字典列表
+输出:
+```python
+{
+ "total_valid_storage_cnt": Decimal, # 有效库存总数量(五项之和)
+ "total_valid_storage_amount": Decimal, # 有效库存总金额(资金占用)
+ "total_in_stock_unlocked_cnt": Decimal, # 在库未锁总数量
+ "total_in_stock_unlocked_amount": Decimal,
+ "total_on_the_way_cnt": Decimal, # 在途总数量
+ "total_on_the_way_amount": Decimal,
+ "total_has_plan_cnt": Decimal, # 计划数总数量
+ "total_has_plan_amount": Decimal,
+ "total_transfer_cnt": Decimal, # 主动调拨在途总数量
+ "total_transfer_amount": Decimal,
+ "total_gen_transfer_cnt": Decimal, # 自动调拨在途总数量
+ "total_gen_transfer_amount": Decimal,
+ "total_avg_sales_cnt": Decimal, # 月均销量总数量
+ "overall_ratio": Decimal, # 整体库销比
+ "part_count": int, # 配件总种类数
+}
+```
+
+#### `calculate_sales_analysis(part_ratios: list[dict]) -> dict`
+计算销量分析统计数据。
+
+输入:PartRatio 字典列表
+输出:
+```python
+{
+ "total_avg_sales_cnt": Decimal, # 月均销量总数量
+ "total_avg_sales_amount": Decimal, # 月均销量总金额
+ "total_out_stock_cnt": Decimal, # 90天出库数总量
+ "total_storage_locked_cnt": Decimal, # 未关单已锁总量
+ "total_out_stock_ongoing_cnt": Decimal, # 未关单出库总量
+ "total_buy_cnt": int, # 订件总量
+ "has_sales_part_count": int, # 有销量配件数
+ "no_sales_part_count": int, # 无销量配件数
+}
+```
+
+#### `calculate_inventory_health(part_ratios: list[dict]) -> dict`
+计算库存构成健康度统计数据。
+
+输入:PartRatio 字典列表
+输出:
+```python
+{
+ "shortage": {"count": int, "amount": Decimal, "count_pct": float, "amount_pct": float},
+ "stagnant": {"count": int, "amount": Decimal, "count_pct": float, "amount_pct": float},
+ "low_freq": {"count": int, "amount": Decimal, "count_pct": float, "amount_pct": float},
+ "normal": {"count": int, "amount": Decimal, "count_pct": float, "amount_pct": float},
+ "total_count": int,
+ "total_amount": Decimal,
+}
+```
+
+#### `calculate_replenishment_summary(part_results: list) -> dict`
+计算补货建议生成情况统计数据。
+
+输入:part_results 列表(配件汇总结果)
+输出:
+```python
+{
+ "urgent": {"count": int, "amount": Decimal}, # 急需补货 (priority=1)
+ "suggested": {"count": int, "amount": Decimal}, # 建议补货 (priority=2)
+ "optional": {"count": int, "amount": Decimal}, # 可选补货 (priority=3)
+ "total_count": int,
+ "total_amount": Decimal,
+}
+```
+
+### 2. LLM 分析节点
+
+四个独立的 LLM 分析函数,每个接收对应板块的统计数据,调用 LLM 生成分析文本:
+
+- `llm_analyze_inventory_overview(stats: dict) -> str` — 返回 JSON 字符串
+- `llm_analyze_sales(stats: dict) -> str` — 返回 JSON 字符串
+- `llm_analyze_inventory_health(stats: dict) -> str` — 返回 JSON 字符串
+- `llm_analyze_replenishment_summary(stats: dict) -> str` — 返回 JSON 字符串
+
+每个函数加载对应的提示词模板(统一放在 `prompts/analysis_report.md` 中,按板块分段),填充统计数据,调用 LLM,解析 JSON 响应。
+
+### 3. 提示词文件
+
+拆分为四个独立提示词文件,每个板块一个,确保分析专业且有实际决策价值:
+
+#### prompts/report_inventory_overview.md — 库存概览分析
+
+角色:资深汽车配件库存管理专家。输入统计数据后,要求 LLM 分析:
+- **资金占用评估**:总库存金额是否合理,各构成部分(在库未锁/在途/计划数/主动调拨在途/自动调拨在途)的资金分配比例是否健康
+- **库销比诊断**:当前整体库销比处于什么水平(<1 库存不足,1-2 合理,2-3 偏高,>3 严重积压),对比行业经验给出判断
+- **库存结构建议**:基于五项构成的比例,给出具体的库存结构优化方向
+
+输出 JSON 格式:
+```json
+{
+ "capital_assessment": {
+ "total_evaluation": "总资金占用评估",
+ "structure_ratio": "各构成部分的资金比例分析",
+ "risk_level": "high/medium/low"
+ },
+ "ratio_diagnosis": {
+ "level": "不足/合理/偏高/严重积压",
+ "analysis": "库销比具体分析",
+ "benchmark": "行业参考值对比"
+ },
+ "recommendations": ["具体建议1", "具体建议2"]
+}
+```
+
+#### prompts/report_sales_analysis.md — 销量分析
+
+角色:汽车配件销售数据分析师。输入统计数据后,要求 LLM 分析:
+- **销量构成解读**:90天出库数占比最大说明正常销售为主,未关单已锁/出库占比高说明有大量待处理订单,订件占比高说明客户预订需求旺盛
+- **销售活跃度**:有销量 vs 无销量配件的比例反映 SKU 活跃度,无销量占比过高说明 SKU 管理需要优化
+- **需求趋势判断**:基于各组成部分的比例关系,判断当前需求是稳定、上升还是下降趋势
+
+输出 JSON 格式:
+```json
+{
+ "composition_analysis": {
+ "main_driver": "主要销量来源分析",
+ "pending_orders_impact": "未关单对销量的影响",
+ "booking_trend": "订件趋势分析"
+ },
+ "activity_assessment": {
+ "active_ratio": "活跃SKU占比评估",
+ "optimization_suggestion": "SKU优化建议"
+ },
+ "demand_trend": {
+ "direction": "上升/稳定/下降",
+ "evidence": "判断依据",
+ "forecast": "短期需求预测"
+ }
+}
+```
+
+#### prompts/report_inventory_health.md — 库存健康度分析
+
+角色:汽车配件库存健康度诊断专家。输入统计数据后,要求 LLM 分析:
+- **健康度评分**:基于正常件占比给出整体健康度评分(正常件>70%为健康,50-70%为亚健康,<50%为不健康)
+- **问题诊断**:呆滞件占比高说明采购决策需要优化,缺货件占比高说明补货不及时,低频件占比高说明 SKU 精简空间大
+- **资金释放机会**:呆滞件和低频件占用的资金可以通过促销、退货等方式释放,给出具体金额估算
+- **改善优先级**:按影响程度排序,给出最应优先处理的问题类型
+
+输出 JSON 格式:
+```json
+{
+ "health_score": {
+ "score": "健康/亚健康/不健康",
+ "normal_ratio_evaluation": "正常件占比评估"
+ },
+ "problem_diagnosis": {
+ "stagnant_analysis": "呆滞件问题分析及原因",
+ "shortage_analysis": "缺货件问题分析及影响",
+ "low_freq_analysis": "低频件问题分析及建议"
+ },
+ "capital_release": {
+ "stagnant_releasable": "呆滞件可释放资金估算",
+ "low_freq_releasable": "低频件可释放资金估算",
+ "action_plan": "资金释放行动方案"
+ },
+ "priority_actions": ["最优先处理事项1", "最优先处理事项2"]
+}
+```
+
+#### prompts/report_replenishment_summary.md — 补货建议分析
+
+角色:汽车配件采购策略顾问。输入统计数据后,要求 LLM 分析:
+- **紧迫度评估**:急需补货占比反映当前缺货风险程度,急需占比>30%说明库存管理存在较大问题
+- **资金分配建议**:基于各优先级的金额分布,给出资金分配的先后顺序和比例建议
+- **执行节奏建议**:急需补货应立即执行,建议补货可在1-2周内完成,可选补货可根据资金情况灵活安排
+- **风险提示**:如果可选补货金额占比过高,提示可能存在过度补货风险
+
+输出 JSON 格式:
+```json
+{
+ "urgency_assessment": {
+ "urgent_ratio_evaluation": "急需补货占比评估",
+ "risk_level": "high/medium/low",
+ "immediate_action_needed": true/false
+ },
+ "budget_allocation": {
+ "recommended_order": "建议资金分配顺序",
+ "urgent_budget": "急需补货建议预算",
+ "suggested_budget": "建议补货建议预算",
+ "optional_budget": "可选补货建议预算"
+ },
+ "execution_plan": {
+ "urgent_timeline": "急需补货执行时间建议",
+ "suggested_timeline": "建议补货执行时间建议",
+ "optional_timeline": "可选补货执行时间建议"
+ },
+ "risk_warnings": ["风险提示1", "风险提示2"]
+}
+```
+
+### 4. API 接口 (tasks.py)
+
+更新 `GET /api/tasks/{task_no}/analysis-report` 端点:
+
+响应模型 `AnalysisReportResponse`:
+```python
+class AnalysisReportResponse(BaseModel):
+ id: int
+ task_no: str
+ group_id: int
+ dealer_grouping_id: int
+ dealer_grouping_name: Optional[str]
+ report_type: str
+
+ inventory_overview: Optional[Dict[str, Any]] # 库存概览(统计+分析)
+ sales_analysis: Optional[Dict[str, Any]] # 销量分析(统计+分析)
+ inventory_health: Optional[Dict[str, Any]] # 健康度(统计+分析+图表数据)
+ replenishment_summary: Optional[Dict[str, Any]] # 补货建议(统计+分析)
+
+ llm_provider: Optional[str]
+ llm_model: Optional[str]
+ llm_tokens: int
+ execution_time_ms: int
+ statistics_date: Optional[str]
+ create_time: Optional[str]
+```
+
+### 5. 前端渲染 (app.js)
+
+`renderReportTab()` 重写,渲染四个板块:
+
+1. **库存概览板块**: 统计卡片(总数量、总金额、库销比)+ 五项构成明细表(在库未锁/在途/计划数/主动调拨在途/自动调拨在途)+ LLM 分析文本
+2. **销量分析板块**: 统计卡片(月均销量、总金额)+ 构成明细表 + LLM 分析文本
+3. **健康度板块**: 统计卡片 + Chart.js 环形图(数量占比 + 金额占比)+ LLM 分析文本
+4. **补货建议板块**: 优先级统计表 + LLM 分析文本
+
+### 6. Chart.js 集成
+
+在 `ui/index.html` 中通过 CDN 引入 Chart.js:
+```html
+
+```
+
+健康度板块使用两个环形图(Doughnut Chart):
+- 数量占比图:缺货/呆滞/低频/正常 四类的数量百分比
+- 金额占比图:缺货/呆滞/低频/正常 四类的金额百分比
+
+## 数据模型
+
+### 数据库表 (ai_analysis_report)
+
+```sql
+DROP TABLE IF EXISTS ai_analysis_report;
+CREATE TABLE ai_analysis_report (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
+ task_no VARCHAR(32) NOT NULL COMMENT '任务编号',
+ group_id BIGINT NOT NULL COMMENT '集团ID',
+ dealer_grouping_id BIGINT NOT NULL COMMENT '商家组合ID',
+ dealer_grouping_name VARCHAR(128) COMMENT '商家组合名称',
+ brand_grouping_id BIGINT COMMENT '品牌组合ID',
+ report_type VARCHAR(32) DEFAULT 'replenishment' COMMENT '报告类型',
+
+ -- 四大板块 (JSON 结构化存储,每个字段包含 stats + llm_analysis)
+ inventory_overview JSON COMMENT '库存总体概览(统计数据+LLM分析)',
+ sales_analysis JSON COMMENT '销量分析(统计数据+LLM分析)',
+ inventory_health JSON COMMENT '库存构成健康度(统计数据+图表数据+LLM分析)',
+ replenishment_summary JSON COMMENT '补货建议生成情况(统计数据+LLM分析)',
+
+ -- LLM 元数据
+ llm_provider VARCHAR(32) COMMENT 'LLM提供商',
+ llm_model VARCHAR(64) COMMENT 'LLM模型名称',
+ llm_tokens INT DEFAULT 0 COMMENT 'LLM Token消耗',
+ execution_time_ms INT DEFAULT 0 COMMENT '执行耗时(毫秒)',
+
+ statistics_date VARCHAR(16) COMMENT '统计日期',
+ create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+
+ INDEX idx_task_no (task_no),
+ INDEX idx_group_date (group_id, statistics_date),
+ INDEX idx_dealer_grouping (dealer_grouping_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI补货建议分析报告表-重构版';
+```
+
+### Python 数据模型 (AnalysisReport)
+
+```python
+@dataclass
+class AnalysisReport:
+ task_no: str
+ group_id: int
+ dealer_grouping_id: int
+
+ id: Optional[int] = None
+ dealer_grouping_name: Optional[str] = None
+ brand_grouping_id: Optional[int] = None
+ report_type: str = "replenishment"
+
+ # 四大板块
+ inventory_overview: Optional[Dict[str, Any]] = None
+ sales_analysis: Optional[Dict[str, Any]] = None
+ inventory_health: Optional[Dict[str, Any]] = None
+ replenishment_summary: Optional[Dict[str, Any]] = None
+
+ # LLM 元数据
+ llm_provider: str = ""
+ llm_model: str = ""
+ llm_tokens: int = 0
+ execution_time_ms: int = 0
+
+ statistics_date: str = ""
+ create_time: Optional[datetime] = None
+```
+
+### 每个板块的 JSON 数据结构
+
+每个板块的 JSON 包含 `stats`(统计数据)和 `llm_analysis`(LLM 分析文本)两部分:
+
+```python
+# inventory_overview 示例
+{
+ "stats": {
+ "total_valid_storage_cnt": 12500,
+ "total_valid_storage_amount": 3250000.00,
+ "total_in_stock_unlocked_cnt": 8000,
+ "total_in_stock_unlocked_amount": 2080000.00,
+ "total_on_the_way_cnt": 2500,
+ "total_on_the_way_amount": 650000.00,
+ "total_has_plan_cnt": 1000,
+ "total_has_plan_amount": 260000.00,
+ "total_transfer_cnt": 600,
+ "total_transfer_amount": 156000.00,
+ "total_gen_transfer_cnt": 400,
+ "total_gen_transfer_amount": 104000.00,
+ "total_avg_sales_cnt": 5000,
+ "overall_ratio": 2.5,
+ "part_count": 800
+ },
+ "llm_analysis": { ... } # LLM 返回的 JSON 分析对象
+}
+
+# inventory_health 示例(额外包含 chart_data)
+{
+ "stats": {
+ "shortage": {"count": 50, "amount": 125000, "count_pct": 6.25, "amount_pct": 3.85},
+ "stagnant": {"count": 120, "amount": 480000, "count_pct": 15.0, "amount_pct": 14.77},
+ "low_freq": {"count": 200, "amount": 300000, "count_pct": 25.0, "amount_pct": 9.23},
+ "normal": {"count": 430, "amount": 2345000, "count_pct": 53.75, "amount_pct": 72.15},
+ "total_count": 800,
+ "total_amount": 3250000
+ },
+ "chart_data": {
+ "labels": ["缺货件", "呆滞件", "低频件", "正常件"],
+ "count_values": [50, 120, 200, 430],
+ "amount_values": [125000, 480000, 300000, 2345000]
+ },
+ "llm_analysis": { ... }
+}
+```
+
+
+## 正确性属性
+
+*正确性属性是系统在所有合法执行中都应保持为真的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规范与机器可验证正确性保证之间的桥梁。*
+
+### Property 1: 库存概览统计一致性
+
+*对于任意* PartRatio 字典列表,`calculate_inventory_overview` 的输出应满足:
+- `total_valid_storage_cnt` = 所有配件 `(in_stock_unlocked_cnt + on_the_way_cnt + has_plan_cnt)` 之和
+- `total_valid_storage_amount` = 所有配件 `(in_stock_unlocked_cnt + on_the_way_cnt + has_plan_cnt) × cost_price` 之和
+- `total_in_stock_unlocked_cnt + total_on_the_way_cnt + total_has_plan_cnt` = `total_valid_storage_cnt`(构成不变量)
+- 当 `total_avg_sales_cnt > 0` 时,`overall_ratio` = `total_valid_storage_cnt / total_avg_sales_cnt`
+
+**Validates: Requirements 2.1, 2.2, 2.3**
+
+### Property 2: 销量分析统计一致性
+
+*对于任意* PartRatio 字典列表,`calculate_sales_analysis` 的输出应满足:
+- `total_avg_sales_cnt` = 所有配件 `(out_stock_cnt + storage_locked_cnt + out_stock_ongoing_cnt + buy_cnt) / 3` 之和
+- `(total_out_stock_cnt + total_storage_locked_cnt + total_out_stock_ongoing_cnt + total_buy_cnt) / 3` = `total_avg_sales_cnt`(构成不变量)
+- `has_sales_part_count + no_sales_part_count` = 总配件数
+
+**Validates: Requirements 3.1, 3.2, 3.4**
+
+### Property 3: 健康度分类完备性与一致性
+
+*对于任意* PartRatio 字典列表,`calculate_inventory_health` 的输出应满足:
+- `shortage.count + stagnant.count + low_freq.count + normal.count` = `total_count`(分类完备)
+- `shortage.amount + stagnant.amount + low_freq.amount + normal.amount` = `total_amount`(金额守恒)
+- 每种类型的 `count_pct` = `count / total_count × 100`
+- 所有 `count_pct` 之和 ≈ 100.0(浮点精度容差内)
+
+**Validates: Requirements 4.1, 4.2**
+
+### Property 4: 补货建议统计一致性
+
+*对于任意* part_results 列表,`calculate_replenishment_summary` 的输出应满足:
+- `urgent.count + suggested.count + optional.count` = `total_count`
+- `urgent.amount + suggested.amount + optional.amount` = `total_amount`
+
+**Validates: Requirements 5.1, 5.2**
+
+### Property 5: 报告数据模型序列化 round-trip
+
+*对于任意* 合法的 AnalysisReport 对象,调用 `to_dict()` 后再用返回的字典构造新的 AnalysisReport 对象,两个对象的核心字段应相等。
+
+**Validates: Requirements 10.2, 10.3**
+
+## 错误处理
+
+| 错误场景 | 处理方式 |
+|---------|---------|
+| LLM 单板块调用失败 | 该板块 `llm_analysis` 置为 `{"error": "错误信息"}`,其他板块正常生成 |
+| LLM 返回非法 JSON | 记录原始响应到日志,`llm_analysis` 置为 `{"error": "JSON解析失败", "raw": "原始文本前200字符"}` |
+| PartRatio 列表为空 | 所有统计值为 0,LLM 分析说明无数据 |
+| part_results 列表为空 | 补货建议板块统计值为 0,LLM 分析说明无补货建议 |
+| 数据库写入失败 | 记录错误日志,不中断主工作流,返回包含错误信息的报告 |
+| 提示词文件缺失 | 抛出 FileNotFoundError,由上层节点捕获并记录 |
+
+## 测试策略
+
+### 属性测试 (Property-Based Testing)
+
+使用 `hypothesis` 库进行属性测试,每个属性至少运行 100 次迭代。
+
+- **Property 1**: 生成随机 PartRatio 字典列表(随机数量、随机字段值),验证库存概览统计的不变量
+ - Tag: **Feature: refactor-analysis-report, Property 1: 库存概览统计一致性**
+- **Property 2**: 生成随机 PartRatio 字典列表,验证销量分析统计的不变量
+ - Tag: **Feature: refactor-analysis-report, Property 2: 销量分析统计一致性**
+- **Property 3**: 生成随机 PartRatio 字典列表,验证健康度分类的完备性和一致性
+ - Tag: **Feature: refactor-analysis-report, Property 3: 健康度分类完备性与一致性**
+- **Property 4**: 生成随机 part_results 列表(随机优先级和金额),验证补货建议统计的一致性
+ - Tag: **Feature: refactor-analysis-report, Property 4: 补货建议统计一致性**
+- **Property 5**: 生成随机 AnalysisReport 对象,验证 to_dict round-trip
+ - Tag: **Feature: refactor-analysis-report, Property 5: 报告数据模型序列化 round-trip**
+
+### 单元测试
+
+- 边界情况:空列表输入、月均销量为零、所有配件同一类型
+- LLM 响应解析:合法 JSON、非法 JSON、空响应
+- API 端点:有报告数据、无报告数据
+
+### 集成测试
+
+- 完整报告生成流程(mock LLM)
+- 数据库写入和读取一致性
+- 前端渲染(手动验证)
diff --git a/.kiro/specs/refactor-analysis-report/requirements.md b/.kiro/specs/refactor-analysis-report/requirements.md
new file mode 100644
index 0000000..ab16d55
--- /dev/null
+++ b/.kiro/specs/refactor-analysis-report/requirements.md
@@ -0,0 +1,139 @@
+# 需求文档:重构分析报告功能
+
+## 简介
+
+重构 AI 补货建议系统的分析报告功能。先清理现有分析报告相关代码(仅限分析报告模块,不涉及其他模块),再基于新结构重建。新报告涵盖库存总体概览、销量分析、库存构成健康度、补货建议生成情况四大板块。每个板块包含统计数据和 LLM 生成的分析文本。考虑使用 LangGraph 动态节点并发生成各板块统计及分析,最后汇总。涉及全栈改动:数据库表结构、数据模型、Agent 节点、LLM 提示词、API 接口、前端页面。
+
+## 术语表
+
+- **Report_Generator**: 分析报告生成节点(analysis_report_node),负责统计计算和调用 LLM 生成报告
+- **Report_API**: 分析报告 API 接口,负责从数据库读取报告并返回给前端
+- **Report_UI**: 前端报告渲染模块,负责展示报告数据和图表
+- **PartRatio**: 配件库销比数据,包含库存、销量、成本等原始数据
+- **有效库存**: valid_storage_cnt = in_stock_unlocked_cnt + on_the_way_cnt + has_plan_cnt(在库未锁 + 在途 + 计划数)
+- **月均销量**: (90天出库数 + 未关单已锁 + 未关单出库 + 订件) / 3
+- **库销比**: 有效库存(valid_storage_cnt) / 月均销量
+- **呆滞件**: 有效库存(valid_storage_cnt) > 0 且 90天出库数 = 0 的配件
+- **低频件**: 月均销量 < 1 或 出库次数 < 3 或 出库间隔 >= 30天 的配件
+- **缺货件**: 有效库存(valid_storage_cnt) = 0 且 月均销量 >= 1 的配件
+- **正常件**: 不属于呆滞件、低频件、缺货件的配件
+- **急需补货**: 优先级 1,库销比 < 0.5 且月均销量 >= 1
+- **建议补货**: 优先级 2,库销比 0.5-1.0 且月均销量 >= 1
+- **可选补货**: 优先级 3,库销比 1.0-目标值 且月均销量 >= 1
+- **Result_Writer**: 数据库写入服务,负责将报告持久化到 MySQL
+
+## 需求
+
+### 需求 1:清理现有分析报告代码
+
+**用户故事:** 作为开发者,我希望先清理现有分析报告相关代码,以便在干净的基础上重构新报告功能。
+
+#### 验收标准
+
+1. WHEN 开始重构, THE 开发者 SHALL 清理 analysis_report_node.py 中的现有报告生成逻辑
+2. WHEN 开始重构, THE 开发者 SHALL 清理 analysis_report.py 中的现有数据模型
+3. WHEN 开始重构, THE 开发者 SHALL 清理 prompts/analysis_report.md 中的现有提示词
+4. WHEN 开始重构, THE 开发者 SHALL 清理前端 app.js 中的现有报告渲染代码(renderReportTab、renderOverallAssessment、renderRiskAlerts、renderStrategy、renderExpectedImpact)
+5. WHEN 清理代码, THE 开发者 SHALL 仅清理分析报告相关代码,保持其他模块代码不变
+
+### 需求 2:库存总体概览统计
+
+**用户故事:** 作为采购决策者,我希望看到库存总体概览(总数量、总金额/资金占用、库销比及库存构成明细),以便快速了解当前库存状况和资金占用情况。
+
+#### 验收标准
+
+1. WHEN Report_Generator 接收到 PartRatio 列表, THE Report_Generator SHALL 使用 valid_storage_cnt(in_stock_unlocked_cnt + on_the_way_cnt + has_plan_cnt)计算所有配件的有效库存总数量和总金额(有效库存 × 成本价之和)
+2. WHEN Report_Generator 计算库存构成, THE Report_Generator SHALL 分别统计在库未锁(in_stock_unlocked_cnt)、在途(on_the_way_cnt)、计划数(has_plan_cnt)的总数量/总金额
+3. WHEN Report_Generator 计算库销比, THE Report_Generator SHALL 使用有效库存总数量除以月均销量总数量得到整体库销比
+4. WHEN 库存概览统计完成, THE Report_Generator SHALL 将统计数据作为上下文传递给 LLM 生成库存概览分析文本
+5. IF 月均销量总数量为零, THEN THE Report_Generator SHALL 将库销比标记为特殊值并在分析中说明无销量数据
+
+### 需求 3:销量分析统计
+
+**用户故事:** 作为采购决策者,我希望看到销量分析(月均销量及各组成部分明细),以便了解销售趋势和需求分布。
+
+#### 验收标准
+
+1. WHEN Report_Generator 接收到 PartRatio 列表, THE Report_Generator SHALL 计算所有配件的月均销量总数量和总金额(月均销量 × 成本价之和)
+2. WHEN Report_Generator 计算销量构成, THE Report_Generator SHALL 分别统计90天出库数(out_stock_cnt)总量、未关单已锁(storage_locked_cnt)总量、未关单出库(out_stock_ongoing_cnt)总量、订件(buy_cnt)总量
+3. WHEN 销量统计完成, THE Report_Generator SHALL 将统计数据作为上下文传递给 LLM 生成销量分析文本
+4. WHEN Report_Generator 计算销量分布, THE Report_Generator SHALL 统计有销量配件数(月均销量 > 0)和无销量配件数(月均销量 = 0)
+
+### 需求 4:库存构成健康度统计
+
+**用户故事:** 作为采购决策者,我希望看到库存健康度分析(缺货/呆滞/低频/正常各类型的数量和金额占比),以便识别库存结构问题。
+
+#### 验收标准
+
+1. WHEN Report_Generator 分类配件, THE Report_Generator SHALL 将每个配件归类为缺货件、呆滞件、低频件或正常件中的一种
+2. WHEN Report_Generator 统计各类型, THE Report_Generator SHALL 计算每种类型的配件数量、占总数量的百分比、涉及金额(有效库存 × 成本价)、占总金额的百分比
+3. WHEN 健康度统计完成, THE Report_Generator SHALL 将统计数据作为上下文传递给 LLM 生成健康度分析文本
+4. WHEN Report_API 返回健康度数据, THE Report_API SHALL 返回包含各类型数量和金额的结构化数据,供前端生成图表
+5. WHEN Report_UI 展示健康度数据, THE Report_UI SHALL 使用图表展示各类型配件的数量占比和金额占比
+
+### 需求 5:补货建议生成情况统计
+
+**用户故事:** 作为采购决策者,我希望看到本次补货建议的生成情况(急需/建议/可选各优先级的配件数和金额),以便了解补货的紧迫程度和资金需求。
+
+#### 验收标准
+
+1. WHEN Report_Generator 统计补货建议, THE Report_Generator SHALL 按优先级(急需补货/建议补货/可选补货)分别统计配件种类数和建议补货总金额
+2. WHEN Report_Generator 统计补货总量, THE Report_Generator SHALL 计算所有优先级的配件总种类数和总金额
+3. WHEN 补货统计完成, THE Report_Generator SHALL 将统计数据作为上下文传递给 LLM 生成补货建议分析文本
+
+### 需求 6:数据库表结构重构
+
+**用户故事:** 作为开发者,我希望数据库表结构能够存储新的报告模块数据,以便支持四大板块的数据持久化。
+
+#### 验收标准
+
+1. THE ai_analysis_report 表 SHALL 包含库存概览模块的 JSON 字段(inventory_overview)存储统计数据和 LLM 分析
+2. THE ai_analysis_report 表 SHALL 包含销量分析模块的 JSON 字段(sales_analysis)存储统计数据和 LLM 分析
+3. THE ai_analysis_report 表 SHALL 包含健康度模块的 JSON 字段(inventory_health)存储统计数据和 LLM 分析
+4. THE ai_analysis_report 表 SHALL 包含补货建议模块的 JSON 字段(replenishment_summary)存储统计数据和 LLM 分析
+5. THE ai_analysis_report 表 SHALL 保留 task_no、group_id、dealer_grouping_id 等基础字段和 LLM 元数据字段
+
+### 需求 7:LLM 提示词重构与并发生成
+
+**用户故事:** 作为开发者,我希望使用 LangGraph 动态节点并发生成各板块的 LLM 分析,提高报告生成效率。
+
+#### 验收标准
+
+1. WHEN Report_Generator 生成报告, THE Report_Generator SHALL 使用 LangGraph 动态节点并发调用 LLM 生成四个板块的分析文本
+2. WHEN 每个板块节点调用 LLM, THE 节点 SHALL 在提示词中仅包含该板块相关的统计数据
+3. WHEN LLM 生成分析, THE LLM 输出 SHALL 为合法的 JSON 对象
+4. WHEN 所有板块分析完成, THE Report_Generator SHALL 汇总四个板块的统计数据和 LLM 分析结果为一个完整报告
+5. IF 某个板块的 LLM 调用失败, THEN THE Report_Generator SHALL 记录错误日志,该板块分析文本置为错误提示,其他板块继续正常生成
+
+### 需求 8:API 接口重构
+
+**用户故事:** 作为前端开发者,我希望 API 返回新结构的报告数据,以便前端能够渲染四大板块。
+
+#### 验收标准
+
+1. WHEN 前端请求分析报告, THE Report_API SHALL 返回包含四个模块(inventory_overview、sales_analysis、inventory_health、replenishment_summary)的 JSON 响应
+2. WHEN 报告中包含 JSON 字符串字段, THE Report_API SHALL 将其解析为 JSON 对象后返回
+3. IF 指定 task_no 无报告数据, THEN THE Report_API SHALL 返回 null
+
+### 需求 9:前端报告页面重构
+
+**用户故事:** 作为采购决策者,我希望在前端看到结构清晰的四大板块报告,包含统计数据卡片、图表和 LLM 分析文本。
+
+#### 验收标准
+
+1. WHEN Report_UI 渲染报告, THE Report_UI SHALL 按库存概览、销量分析、健康度、补货建议的顺序展示四个板块
+2. WHEN Report_UI 渲染库存概览板块, THE Report_UI SHALL 展示总库存数量、总金额(资金占用)、库销比的统计卡片,以及三项构成明细(在库未锁/在途/计划数)和 LLM 分析文本
+3. WHEN Report_UI 渲染销量分析板块, THE Report_UI SHALL 展示月均销量总量、销量构成明细(90天出库/未关单已锁/未关单出库/订件)和 LLM 分析文本
+4. WHEN Report_UI 渲染健康度板块, THE Report_UI SHALL 展示各类型配件的数量/金额占比图表和 LLM 分析文本
+5. WHEN Report_UI 渲染补货建议板块, THE Report_UI SHALL 展示各优先级的配件数/金额统计和 LLM 分析文本
+6. WHEN Report_UI 渲染健康度图表, THE Report_UI SHALL 使用 Chart.js 生成饼图或环形图展示数量占比和金额占比
+
+### 需求 10:数据模型与写入服务重构
+
+**用户故事:** 作为开发者,我希望 Python 数据模型和数据库写入服务能够适配新的报告结构。
+
+#### 验收标准
+
+1. THE AnalysisReport 数据模型 SHALL 包含 inventory_overview、sales_analysis、inventory_health、replenishment_summary 四个字典字段
+2. WHEN Result_Writer 保存报告, THE Result_Writer SHALL 将四个模块的字典数据序列化为 JSON 字符串写入对应数据库字段
+3. THE AnalysisReport 数据模型 SHALL 提供 to_dict 方法将报告转换为可序列化的字典
diff --git a/.kiro/specs/refactor-analysis-report/tasks.md b/.kiro/specs/refactor-analysis-report/tasks.md
new file mode 100644
index 0000000..6be77e1
--- /dev/null
+++ b/.kiro/specs/refactor-analysis-report/tasks.md
@@ -0,0 +1,126 @@
+# 实现计划:重构分析报告功能
+
+## 概述
+
+全栈重构分析报告功能,从清理现有代码开始,依次重建数据库表、数据模型、统计计算、LLM 提示词、并发节点、API 接口、前端页面。使用 Python + LangGraph + FastAPI + Chart.js 技术栈。
+
+## 任务
+
+- [x] 1. 清理现有分析报告专属文件
+ - [x] 1.1 清空 `src/fw_pms_ai/models/analysis_report.py`,仅保留文件头注释和必要 import,移除现有 AnalysisReport dataclass 的所有字段和方法
+ - [x] 1.2 清空 `src/fw_pms_ai/agent/analysis_report_node.py`,仅保留文件头注释和必要 import,移除 `_load_prompt`、`_calculate_suggestion_stats`、`_calculate_risk_stats`、`_build_suggestion_summary`、`generate_analysis_report_node` 等所有函数
+ - [x] 1.3 清空 `prompts/analysis_report.md` 的内容
+ - 注意:此步骤仅清理分析报告专属文件,不修改 `tasks.py`、`app.js`、`result_writer.py` 等共享文件(这些文件中的报告相关代码将在后续重建步骤中以替换方式更新)
+ - _Requirements: 1.1, 1.2, 1.3, 1.5_
+
+- [x] 2. 重建数据库表和数据模型
+ - [x] 2.1 重写 `sql/migrate_analysis_report.sql`,创建新表结构(inventory_overview、sales_analysis、inventory_health、replenishment_summary 四个 JSON 字段 + LLM 元数据字段)
+ - _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
+ - [x] 2.2 在已清空的 `src/fw_pms_ai/models/analysis_report.py` 中编写新的 AnalysisReport dataclass(四个 Dict 字段 + to_dict 方法)
+ - _Requirements: 10.1, 10.3_
+ - [x] 2.3 替换 `src/fw_pms_ai/services/result_writer.py` 中的 `save_analysis_report` 方法为新版本,适配新表结构(四个 JSON 字段序列化写入),不修改该文件中的其他方法
+ - _Requirements: 10.2_
+ - [ ]* 2.4 编写属性测试:报告数据模型序列化 round-trip
+ - **Property 5: 报告数据模型序列化 round-trip**
+ - **Validates: Requirements 10.2, 10.3**
+
+- [x] 3. 实现四大板块统计计算函数
+ - [x] 3.1 在已清空的 `src/fw_pms_ai/agent/analysis_report_node.py` 中实现 `calculate_inventory_overview(part_ratios)` 函数
+ - 计算有效库存(valid_storage_cnt = in_stock_unlocked_cnt + on_the_way_cnt + has_plan_cnt)总数量/总金额、三项构成明细、库销比
+ - _Requirements: 2.1, 2.2, 2.3, 2.5_
+ - [x] 3.2 实现 `calculate_sales_analysis(part_ratios)` 函数
+ - 计算月均销量总数量/总金额、各组成部分(out_stock_cnt/storage_locked_cnt/out_stock_ongoing_cnt/buy_cnt)总量、有销量/无销量配件数
+ - _Requirements: 3.1, 3.2, 3.4_
+ - [x] 3.3 实现 `calculate_inventory_health(part_ratios)` 函数
+ - 将配件分类为缺货/呆滞/低频/正常,计算各类型数量/金额/百分比,生成 chart_data
+ - _Requirements: 4.1, 4.2_
+ - [x] 3.4 实现 `calculate_replenishment_summary(part_results)` 函数
+ - 按优先级(1=急需/2=建议/3=可选)统计配件种类数和金额
+ - _Requirements: 5.1, 5.2_
+ - [ ]* 3.5 编写属性测试:库存概览统计一致性
+ - **Property 1: 库存概览统计一致性**
+ - **Validates: Requirements 2.1, 2.2, 2.3**
+ - [ ]* 3.6 编写属性测试:销量分析统计一致性
+ - **Property 2: 销量分析统计一致性**
+ - **Validates: Requirements 3.1, 3.2, 3.4**
+ - [ ]* 3.7 编写属性测试:健康度分类完备性与一致性
+ - **Property 3: 健康度分类完备性与一致性**
+ - **Validates: Requirements 4.1, 4.2**
+ - [ ]* 3.8 编写属性测试:补货建议统计一致性
+ - **Property 4: 补货建议统计一致性**
+ - **Validates: Requirements 5.1, 5.2**
+
+- [ ] 4. Checkpoint - 确保统计计算函数和属性测试通过
+ - 确保所有测试通过,如有问题请向用户确认。
+
+- [x] 5. 创建 LLM 提示词文件
+ - [x] 5.1 创建 `prompts/report_inventory_overview.md`,包含库存概览分析提示词(资金占用评估、库销比诊断、库存结构建议),确保分析专业且有实际决策价值
+ - _Requirements: 7.2_
+ - [x] 5.2 创建 `prompts/report_sales_analysis.md`,包含销量分析提示词(销量构成解读、销售活跃度、需求趋势判断),确保分析专业且有实际决策价值
+ - _Requirements: 7.2_
+ - [x] 5.3 创建 `prompts/report_inventory_health.md`,包含健康度分析提示词(健康度评分、问题诊断、资金释放机会、改善优先级),确保分析专业且有实际决策价值
+ - _Requirements: 7.2_
+ - [x] 5.4 创建 `prompts/report_replenishment_summary.md`,包含补货建议分析提示词(紧迫度评估、资金分配建议、执行节奏、风险提示),确保分析专业且有实际决策价值
+ - _Requirements: 7.2_
+ - [x] 5.5 删除旧的 `prompts/analysis_report.md` 文件(已在步骤1.3清空,此处正式删除)
+ - _Requirements: 1.3_
+
+- [x] 6. 实现 LangGraph 并发 LLM 分析节点
+ - [x] 6.1 在 `src/fw_pms_ai/agent/analysis_report_node.py` 中实现四个 LLM 分析函数(`llm_analyze_inventory_overview`、`llm_analyze_sales`、`llm_analyze_inventory_health`、`llm_analyze_replenishment_summary`),每个函数加载对应提示词、填充统计数据、调用 LLM、解析 JSON 响应
+ - _Requirements: 7.2, 7.3_
+ - [x] 6.2 使用 LangGraph StateGraph 构建并发子图,四个 LLM 节点从 START fan-out 并发执行,结果 fan-in 汇总
+ - _Requirements: 7.1, 7.4_
+ - [x] 6.3 实现新的 `generate_analysis_report_node(state)` 主函数,串联统计计算 → 并发 LLM 分析 → 汇总报告 → 写入数据库,单板块 LLM 失败不影响其他板块
+ - _Requirements: 7.4, 7.5_
+ - [x] 6.4 确认 `src/fw_pms_ai/agent/replenishment.py` 中的工作流引用无需修改(`generate_analysis_report_node` 函数签名保持不变)
+ - _Requirements: 7.1_
+
+- [ ] 7. Checkpoint - 确保后端报告生成流程完整
+ - 确保所有测试通过,如有问题请向用户确认。
+
+- [x] 8. 重建 API 接口
+ - [x] 8.1 替换 `src/fw_pms_ai/api/routes/tasks.py` 中的 `AnalysisReportResponse` 模型为新版本(inventory_overview、sales_analysis、inventory_health、replenishment_summary 四个 Dict 字段),不修改该文件中的其他模型和端点
+ - _Requirements: 8.1_
+ - [x] 8.2 替换 `get_analysis_report` 端点实现,从新表读取数据并解析 JSON 字段,不修改该文件中的其他端点
+ - _Requirements: 8.1, 8.2, 8.3_
+
+- [x] 9. 重建前端报告页面
+ - [x] 9.1 在 `ui/index.html` 中引入 Chart.js CDN
+ - _Requirements: 9.6_
+ - [x] 9.2 替换 `ui/js/app.js` 中的 `renderReportTab` 方法为新版本,渲染四大板块框架,同时移除旧的 `renderOverallAssessment`、`renderRiskAlerts`、`renderStrategy`、`renderExpectedImpact` 方法,不修改该文件中的其他方法
+ - _Requirements: 9.1, 1.4_
+ - [x] 9.3 实现 `renderInventoryOverview` 方法,渲染库存概览板块(统计卡片 + 五项构成明细 + LLM 分析文本)
+ - _Requirements: 9.2_
+ - [x] 9.4 实现 `renderSalesAnalysis` 方法,渲染销量分析板块(统计卡片 + 构成明细 + LLM 分析文本)
+ - _Requirements: 9.3_
+ - [x] 9.5 实现 `renderInventoryHealth` 方法,渲染健康度板块(统计卡片 + Chart.js 环形图 + LLM 分析文本)
+ - _Requirements: 9.4, 9.6_
+ - [x] 9.6 实现 `renderReplenishmentSummary` 方法,渲染补货建议板块(优先级统计表 + LLM 分析文本)
+ - _Requirements: 9.5_
+ - [x] 9.7 在 `ui/css/style.css` 中添加新报告板块的样式(统计卡片、图表容器、分析文本区域),不修改现有样式
+ - _Requirements: 9.1_
+
+- [ ] 10. Final Checkpoint - 全栈集成验证
+ - 确保所有测试通过,如有问题请向用户确认。
+
+## 安全约束:不影响补货建议功能
+
+本次重构严格限定在分析报告模块范围内,以下补货建议相关代码禁止修改:
+
+- `src/fw_pms_ai/agent/nodes.py` — 补货建议核心节点(fetch_part_ratio、sql_agent、allocate_budget)
+- `src/fw_pms_ai/agent/replenishment.py` — 补货建议工作流(仅确认无需修改,不做任何改动)
+- `src/fw_pms_ai/agent/sql_agent/` — SQL Agent 目录
+- `src/fw_pms_ai/models/part_ratio.py` — 配件库销比模型
+- `src/fw_pms_ai/models/replenishment_*.py` — 补货建议相关模型
+- `result_writer.py` 中的 `save_task`、`update_task`、`save_details`、`save_part_summaries`、`save_execution_log` 等方法
+- `tasks.py` 中的任务列表、任务详情、配件明细、配件汇总、执行日志等端点
+- `app.js` 中的任务列表、任务详情、配件明细等渲染方法
+- `prompts/` 中的 `part_shop_analysis*.md`、`suggestion*.md`、`sql_agent.md` 等提示词文件
+
+## 备注
+
+- 标记 `*` 的任务为可选任务,可跳过以加快 MVP 进度
+- 每个任务引用了具体的需求编号以保证可追溯性
+- 属性测试使用 `hypothesis` 库,每个属性至少 100 次迭代
+- Checkpoint 用于阶段性验证,确保增量正确
+- 共享文件(tasks.py、app.js、result_writer.py、style.css)中的修改均采用"替换特定函数/类"方式,明确不修改其他部分
diff --git a/README.md b/README.md
index 87b5d5d..4511233 100644
--- a/README.md
+++ b/README.md
@@ -1,122 +1,239 @@
# fw-pms-ai
-AI 配件系统 - 基于 Python + LangChain + LangGraph
+fw-pms 配件管理系统 AI 扩展平台 — 基于 Python + LangChain + LangGraph
## 项目简介
-本项目是 `fw-pms` 的 AI 扩展模块,使用大语言模型 (LLM) 和 Agent 技术,为配件管理系统提供智能化的补货建议能力。
+本项目是 `fw-pms` 配件管理系统的 **AI 能力扩展平台**,使用大语言模型 (LLM) 和 Agent 技术,为配件业务提供多种智能化功能。
-## 核心技术
+### 功能模块
-### LangChain + LangGraph
-
-| 技术 | 作用 |
-|------|------|
-| **LangChain** | LLM 框架,提供模型抽象、Prompt 管理、消息格式化 |
-| **LangGraph** | Agent 工作流编排,管理状态机、定义节点和边、支持条件分支 |
-| **SQL Agent** | 自定义 Text-to-SQL 实现,支持错误重试和 LLM 数据分析 |
+| 状态 | 模块 | 说明 |
+|------|------|------|
+| ✅ 已实现 | **智能补货建议** | 分析库销比数据,LLM 逐配件分析各门店补货需求,生成结构化建议 |
+| ✅ 已实现 | **分析报告** | 四大板块(库存概览/销量分析/健康度/补货建议)并发 LLM 分析 |
+| 🚧 规划中 | **需求预测** | 基于历史销量预测未来配件需求 |
+| 🚧 规划中 | **异常检测** | 识别库存和销量数据异常 |
+| 🚧 规划中 | **智能定价** | AI 辅助配件定价建议 |
+
+## 技术栈
+
+| 组件 | 技术 | 版本要求 |
+|------|------|---------|
+| 编程语言 | Python | ≥ 3.11 |
+| Agent 框架 | LangChain + LangGraph | LangChain ≥ 0.3, LangGraph ≥ 0.2 |
+| LLM 集成 | 智谱 GLM / 豆包 / OpenAI 兼容 / Anthropic 兼容 | — |
+| Web API | FastAPI + Uvicorn | FastAPI ≥ 0.109 |
+| 数据库 | MySQL (mysql-connector-python + SQLAlchemy) | SQLAlchemy ≥ 2.0 |
+| 任务调度 | APScheduler | ≥ 3.10 |
+| 配置管理 | Pydantic Settings + python-dotenv | Pydantic ≥ 2.0 |
+| HTTP 客户端 | httpx | ≥ 0.25 |
+| 重试机制 | tenacity | ≥ 8.0 |
+
+## 系统架构
```mermaid
-graph LR
- A[用户请求] --> B[LangGraph Agent]
- B --> C[FetchPartRatio]
- C --> D[SQLAgent
LLM分析]
- D --> E[AllocateBudget]
- E --> F[AnalysisReport]
- F --> G[SaveResult]
+flowchart TB
+ subgraph Scheduler ["定时调度 (APScheduler)"]
+ S[每日凌晨触发]
+ end
+
+ subgraph API ["FastAPI API 层"]
+ A[/tasks, details, logs, reports/]
+ end
+
+ subgraph Agent ["LangGraph 工作流"]
+ direction TB
+ B[1. fetch_part_ratio] --> C[2. sql_agent]
+ C --> D{需要重试?}
+ D -->|是| C
+ D -->|否| E[3. allocate_budget]
+ E --> F[4. generate_analysis_report]
+ F --> G[END]
+ end
+
+ subgraph ReportSubgraph ["分析报告并发子图"]
+ direction LR
+ R1[库存概览 LLM] & R2[销量分析 LLM] & R3[健康度 LLM] & R4[补货建议 LLM]
+ end
+
+ subgraph Services ["业务服务层"]
+ DS[DataService]
+ RW[ResultWriter]
+ RP[Repository]
+ end
+
+ subgraph LLM ["LLM 适配层"]
+ L1[GLMClient]
+ L2[DoubaoClient]
+ L3[OpenAICompatClient]
+ L4[AnthropicCompatClient]
+ end
+
+ subgraph DB ["数据存储"]
+ MySQL[(MySQL)]
+ end
+
+ S --> Agent
+ A --> Services
+ B --> DS
+ C --> LLM
+ F --> ReportSubgraph
+ ReportSubgraph --> LLM
+ E --> RW
+ DS --> MySQL
+ RW --> MySQL
+ RP --> MySQL
```
详细架构图见 [docs/architecture.md](docs/architecture.md)
-## 功能模块
+> 平台采用模块化设计,新增 AI 功能模块只需添加对应的 Agent 工作流节点和提示词文件。
-### ✅ 已实现
+## 补货建议工作流
-| 模块 | 功能 | 说明 |
-|------|------|------|
-| **SQL Agent** | LLM 分析 | 直接分析 part_ratio 数据生成补货建议 |
-| **补货分配** | Replenishment | 转换 LLM 建议为补货明细 |
+补货建议模块的核心是一个 **4 节点 LangGraph 工作流**,按顺序执行:
-### 🚧 计划中
+| 序号 | 节点 | 功能 | 说明 |
+|------|------|------|------|
+| 1 | `fetch_part_ratio` | 获取库销比数据 | 通过 dealer_grouping_id 从 part_ratio 表查询配件数据 |
+| 2 | `sql_agent` | LLM 分析 + 建议生成 | 按 part_code 分组,逐配件分析各门店补货需求,支持错误重试 |
+| 3 | `allocate_budget` | 转换补货明细 | 将 LLM 建议转换为结构化的补货明细记录 |
+| 4 | `generate_analysis_report` | 生成分析报告 | 统计计算 + 4 路并发 LLM 分析生成结构化报告 |
-| 模块 | 功能 |
-|------|------|
-| 预测引擎 | 基于历史销量预测未来需求 |
-| 异常检测 | 识别数据异常 |
+### 分析报告四大板块
+
+| 板块 | 统计计算 | LLM 分析 |
+|------|---------|---------|
+| **库存概览** | 有效库存、资金占用、配件总数 | 库存状况综合评价 |
+| **销量分析** | 月均销量、出库频次、销售趋势 | 销售趋势洞察 |
+| **库存健康度** | 缺货/呆滞/低频/正常分类统计 | 健康度风险提示 |
+| **补货建议汇总** | 按优先级分类统计补货数量和金额 | 补货策略建议 |
+
+> 四个 LLM 分析节点使用 LangGraph 子图 **并发执行**,单板块失败不影响其他板块。
+
+## 业务术语
+
+| 术语 | 定义 | 处理方式 |
+|------|------|---------|
+| **呆滞件** | 有效库存 > 0,90天出库数 = 0 | 不做计划 |
+| **低频件** | 月均销量 < 1 或 出库次数 < 3 或 出库间隔 ≥ 30天 | 不做计划 |
+| **缺货件** | 有效库存 = 0,月均销量 ≥ 1 | 需要补货 |
+| **正常件** | 不属于以上三类 | 按需补货 |
## 项目结构
```
fw-pms-ai/
├── src/fw_pms_ai/
-│ ├── agent/ # LangGraph Agent
-│ │ ├── state.py # Agent 状态定义
-│ │ ├── nodes.py # 工作流节点
-│ │ ├── sql_agent.py # SQL Agent(Text-to-SQL + 建议生成)
-│ │ └── replenishment.py
-│ ├── api/ # FastAPI 接口
-│ │ ├── app.py # 应用入口
-│ │ └── routes/ # 路由模块
-│ ├── config/ # 配置管理
-│ ├── llm/ # LLM 集成
-│ │ ├── base.py # 抽象基类
-│ │ ├── glm.py # 智谱 GLM
-│ │ ├── doubao.py # 豆包
-│ │ ├── openai_compat.py
-│ │ └── anthropic_compat.py
-│ ├── models/ # 数据模型
-│ │ ├── task.py # 任务和明细模型
-│ │ ├── execution_log.py # 执行日志模型
-│ │ ├── part_ratio.py # 库销比模型
-│ │ ├── part_summary.py # 配件汇总模型
-│ │ ├── sql_result.py # SQL执行结果模型
-│ │ └── suggestion.py # 补货建议模型
-│ ├── services/ # 业务服务
-│ │ ├── db.py # 数据库连接
-│ │ ├── data_service.py # 数据查询服务
-│ │ └── result_writer.py # 结果写入服务
-│ ├── scheduler/ # 定时任务
-│ └── main.py
-├── prompts/ # AI Prompt 文件
-│ ├── sql_agent.md # SQL Agent 系统提示词
-│ ├── suggestion.md # 补货建议提示词
-│ ├── suggestion_system.md
-│ ├── part_shop_analysis.md
-│ └── part_shop_analysis_system.md
-├── ui/ # 前端静态文件
-├── sql/ # 数据库迁移脚本
-├── pyproject.toml
+│ ├── main.py # 应用入口
+│ ├── agent/ # LangGraph Agent 工作流
+│ │ ├── state.py # Agent 状态定义 (TypedDict + Annotated reducer)
+│ │ ├── nodes.py # 工作流节点 (fetch/sql_agent/allocate)
+│ │ ├── analysis_report_node.py # 分析报告节点 (统计计算 + 并发 LLM 子图)
+│ │ ├── replenishment.py # ReplenishmentAgent 主类 (构建图 + 运行)
+│ │ └── sql_agent/ # SQL Agent 子包
+│ │ ├── agent.py # SQLAgent 主类
+│ │ ├── executor.py # SQL 执行器
+│ │ ├── analyzer.py # 配件分析器 (逐配件 LLM 分析)
+│ │ └── prompts.py # 提示词加载
+│ ├── api/ # FastAPI REST API
+│ │ ├── app.py # FastAPI 应用 (CORS + 静态文件 + 路由)
+│ │ └── routes/
+│ │ └── tasks.py # 任务/明细/日志/汇总/报告 API
+│ ├── config/
+│ │ └── settings.py # Pydantic Settings 配置管理
+│ ├── llm/ # LLM 适配层
+│ │ ├── base.py # BaseLLMClient 抽象基类
+│ │ ├── glm.py # 智谱 GLM 客户端
+│ │ ├── doubao.py # 豆包客户端
+│ │ ├── openai_compat.py # OpenAI 兼容客户端 (火山引擎等)
+│ │ └── anthropic_compat.py # Anthropic 兼容客户端
+│ ├── models/ # 数据模型 (Pydantic)
+│ │ ├── task.py # 任务模型 + 状态枚举
+│ │ ├── suggestion.py # 补货建议模型
+│ │ ├── part_ratio.py # 配件库销比模型
+│ │ ├── part_summary.py # 配件汇总模型
+│ │ ├── analysis_report.py # 分析报告模型
+│ │ ├── execution_log.py # 执行日志模型
+│ │ └── sql_result.py # SQL 执行结果模型
+│ ├── services/ # 业务服务层
+│ │ ├── db.py # 数据库连接管理
+│ │ ├── data_service.py # 数据查询服务
+│ │ ├── result_writer.py # 结果写入服务
+│ │ └── repository/ # 仓库模式
+│ │ ├── task_repo.py # 任务仓库
+│ │ ├── detail_repo.py # 明细仓库
+│ │ └── log_repo.py # 日志仓库
+│ └── scheduler/
+│ └── tasks.py # APScheduler 定时任务 + CLI 参数解析
+├── prompts/ # AI 提示词文件
+│ ├── sql_agent.md # SQL Agent 系统提示词
+│ ├── suggestion.md # 补货建议生成提示词
+│ ├── suggestion_system.md # 补货建议系统提示词
+│ ├── part_shop_analysis.md # 配件门店分析提示词
+│ ├── part_shop_analysis_system.md
+│ ├── report_inventory_overview.md # 分析报告-库存概览提示词
+│ ├── report_sales_analysis.md # 分析报告-销量分析提示词
+│ ├── report_inventory_health.md # 分析报告-库存健康度提示词
+│ └── report_replenishment_summary.md # 分析报告-补货建议提示词
+├── ui/ # 前端静态文件
+│ ├── index.html # 主页面
+│ ├── css/ # 样式
+│ ├── js/ # JavaScript
+│ └── merchant-report/ # 商家组合报告页面
+├── sql/ # 数据库脚本
+│ ├── init.sql # 初始化建表 (4张表)
+│ └── migrate_analysis_report.sql # 分析报告表迁移
+├── docs/ # 文档
+│ ├── architecture.md # 系统架构文档
+│ └── 商家组合维度分析需求设计.md
+├── pyproject.toml # 项目配置 (hatchling 构建)
+├── .env # 环境变量
└── README.md
```
+## 数据表说明
-## 工作流程
+| 表名 | 说明 | SQL 文件 |
+|------|------|---------|
+| `part_ratio` | 配件库销比数据(来源表,只读) | — |
+| `ai_replenishment_task` | 任务记录 | init.sql |
+| `ai_replenishment_detail` | 配件级别补货建议明细 | init.sql |
+| `ai_replenishment_part_summary` | 配件汇总表(按配件编码聚合) | init.sql |
+| `ai_task_execution_log` | 任务执行日志(每步骤详情) | init.sql |
+| `ai_analysis_report` | 结构化分析报告(四大板块 JSON) | migrate_analysis_report.sql |
-```
-1. FetchPartRatio - 从 part_ratio 表获取库销比数据
-2. SQLAgent - LLM 分析数据,生成补货建议
-3. AllocateBudget - 转换建议为补货明细
-4. AnalysisReport - 生成分析报告(风险评估、行动方案)
-5. SaveResult - 写入数据库
-```
+## API 接口
-### 业务术语
+基于 FastAPI 提供 REST API,支持前端 UI 对接。
-| 术语 | 定义 | 处理 |
+| 方法 | 路径 | 说明 |
|------|------|------|
-| **呆滞件** | 有库存,90天无销量 | 不做计划 |
-| **低频件** | 无库存,月均销量<1 | 不做计划 |
-| **缺货件** | 无库存,月均销量≥1 | 需要补货 |
+| GET | `/api/tasks` | 任务列表(分页、状态/组合/日期筛选) |
+| GET | `/api/tasks/{task_no}` | 任务详情 |
+| GET | `/api/tasks/{task_no}/details` | 配件建议明细(分页、排序、搜索) |
+| GET | `/api/tasks/{task_no}/logs` | 执行日志 |
+| GET | `/api/tasks/{task_no}/part-summaries` | 配件汇总列表(分页、优先级筛选) |
+| GET | `/api/tasks/{task_no}/parts/{part_code}/shops` | 指定配件的门店明细 |
+| GET | `/api/tasks/{task_no}/analysis-report` | 分析报告 |
+| GET | `/health` | 健康检查 |
+| GET | `/` | 主页面(静态文件) |
-## 数据表说明
+运行后访问 `/docs` 查看 Swagger 文档。
-| 表名 | 说明 |
-|------|------|
-| `part_ratio` | 配件库销比数据(来源表) |
-| `ai_replenishment_task` | 任务记录 |
-| `ai_replenishment_detail` | 配件级别补货建议 |
-| `ai_replenishment_part_summary` | 配件汇总表 |
-| `ai_task_execution_log` | 任务执行日志 |
+## LLM 集成
+
+支持 4 种 LLM 客户端,通过环境变量自动选择:
+
+| 客户端 | 环境变量 | 说明 |
+|--------|---------|------|
+| `OpenAICompatClient` | `OPENAI_COMPAT_API_KEY` | OpenAI 兼容接口(火山引擎、智谱等) |
+| `AnthropicCompatClient` | `ANTHROPIC_API_KEY` | Anthropic 兼容接口 |
+| `GLMClient` | `GLM_API_KEY` | 智谱 GLM 原生 SDK |
+| `DoubaoClient` | `DOUBAO_API_KEY` | 豆包 |
+
+优先级:`OpenAI Compat` > `Anthropic Compat` > `GLM` > `Doubao`
## 快速开始
@@ -131,39 +248,53 @@ pip install -e .
```bash
cp .env.example .env
+# 编辑 .env 填写以下配置
```
必填配置项:
-- `GLM_API_KEY` / `ANTHROPIC_API_KEY` - LLM API Key
-- `MYSQL_HOST`, `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_DATABASE`
+
+```env
+# LLM (至少配置一种)
+OPENAI_COMPAT_API_KEY=your-key
+OPENAI_COMPAT_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
+OPENAI_COMPAT_MODEL=glm-4-7-251222
+
+# 数据库
+MYSQL_HOST=localhost
+MYSQL_PORT=3306
+MYSQL_USER=root
+MYSQL_PASSWORD=your-password
+MYSQL_DATABASE=fw_pms
+```
### 3. 初始化数据库
```bash
+# 基础表结构
mysql -u root -p fw_pms < sql/init.sql
+
+# 分析报告表
+mysql -u root -p fw_pms < sql/migrate_analysis_report.sql
```
### 4. 运行
```bash
-# 启动定时任务调度器
+# 启动定时任务调度器(默认每日 02:00 执行)
fw-pms-ai
-# 立即执行一次
+# 立即执行一次(所有商家组合)
fw-pms-ai --run-once
-# 指定参数
-fw-pms-ai --run-once --group-id 2
+# 指定集团和商家组合
+fw-pms-ai --run-once --group-id 2 --dealer-grouping-id 100
```
-## AI Prompt 文件
+### 5. 启动 API 服务
-Prompt 文件存放在 `prompts/` 目录:
-
-| 文件 | 用途 |
-|------|------|
-| `suggestion.md` | 补货建议生成(含业务术语定义) |
-| `analyze_inventory.md` | 库存分析 |
+```bash
+uvicorn fw_pms_ai.api.app:app --host 0.0.0.0 --port 8000 --reload
+```
## 开发
@@ -171,6 +302,26 @@ Prompt 文件存放在 `prompts/` 目录:
# 安装开发依赖
pip install -e ".[dev]"
+# 代码格式化
+black src/
+ruff check src/
+
# 运行测试
pytest tests/ -v
```
+
+## 提示词文件
+
+提示词文件存放在 `prompts/` 目录,供 LLM 调用时使用:
+
+| 文件 | 用途 |
+|------|------|
+| `sql_agent.md` | SQL Agent 生成 SQL 查询的系统提示词 |
+| `suggestion.md` | 配件补货建议生成 |
+| `suggestion_system.md` | 补货建议系统角色提示词 |
+| `part_shop_analysis.md` | 配件门店级别分析 |
+| `part_shop_analysis_system.md` | 门店分析系统角色提示词 |
+| `report_inventory_overview.md` | 分析报告 — 库存概览板块 |
+| `report_sales_analysis.md` | 分析报告 — 销量分析板块 |
+| `report_inventory_health.md` | 分析报告 — 库存健康度板块 |
+| `report_replenishment_summary.md` | 分析报告 — 补货建议板块 |
diff --git a/docs/architecture.md b/docs/architecture.md
deleted file mode 100644
index 9ca54e3..0000000
--- a/docs/architecture.md
+++ /dev/null
@@ -1,126 +0,0 @@
-# fw-pms-ai 系统架构
-
-## 技术栈
-
-| 组件 | 技术 |
-|------|------|
-| 编程语言 | Python 3.11+ |
-| Agent 框架 | LangChain + LangGraph |
-| LLM | 智谱 GLM / 豆包 / OpenAI 兼容接口 |
-| 数据库 | MySQL |
-| API 框架 | FastAPI |
-| 任务调度 | APScheduler |
-
----
-
-## 系统架构图
-
-```mermaid
-flowchart TB
- subgraph API ["FastAPI API 层"]
- A[/tasks endpoint/]
- end
-
- subgraph Agent ["LangGraph Agent"]
- direction TB
- B[fetch_part_ratio] --> C[sql_agent]
- C --> D{需要重试?}
- D -->|是| C
- D -->|否| E[allocate_budget]
- E --> E2[generate_analysis_report]
- E2 --> F[END]
- end
-
- subgraph Services ["业务服务层"]
- G[DataService]
- H[ResultWriter]
- end
-
- subgraph LLM ["LLM 集成"]
- I[GLM]
- J[Doubao]
- K[OpenAI Compat]
- end
-
- subgraph DB ["数据存储"]
- L[(MySQL)]
- end
-
- A --> Agent
- B --> G
- C --> LLM
- E --> H
- G --> L
- H --> L
-```
-
----
-
-## 工作流节点说明
-
-| 节点 | 职责 | 输入 | 输出 |
-|------|------|------|------|
-| `fetch_part_ratio` | 获取商家组合的配件库销比数据 | dealer_grouping_id | part_ratios[] |
-| `sql_agent` | LLM 分析配件数据,生成补货建议 | part_ratios[] | llm_suggestions[], part_results[] |
-| `allocate_budget` | 转换 LLM 建议为补货明细 | llm_suggestions[] | details[] |
-| `generate_analysis_report` | 生成分析报告 | part_ratios[], details[] | analysis_report |
-
----
-
-## 核心数据流
-
-```mermaid
-sequenceDiagram
- participant API
- participant Agent
- participant SQLAgent
- participant LLM
- participant DB
-
- API->>Agent: 创建任务
- Agent->>DB: 保存任务记录
- Agent->>DB: 查询 part_ratio
- Agent->>SQLAgent: 分析配件数据
-
- loop 每个配件
- SQLAgent->>LLM: 发送分析请求
- LLM-->>SQLAgent: 返回补货建议
- end
-
- SQLAgent-->>Agent: 汇总建议
- Agent->>DB: 保存补货明细
- Agent->>DB: 更新任务状态
- Agent-->>API: 返回结果
-```
-
----
-
-## 目录结构
-
-```
-src/fw_pms_ai/
-├── agent/ # LangGraph 工作流
-│ ├── state.py # 状态定义 (TypedDict)
-│ ├── nodes.py # 工作流节点
-│ ├── sql_agent.py # SQL Agent 实现
-│ └── replenishment.py # 主入口
-├── api/ # REST API
-├── config/ # 配置管理
-├── llm/ # LLM 适配器
-├── models/ # 数据模型
-├── services/ # 业务服务
-└── scheduler/ # 定时任务
-```
-
----
-
-## 数据库表结构
-
-| 表名 | 用途 |
-|------|------|
-| `part_ratio` | 配件库销比数据(只读) |
-| `ai_replenishment_task` | 任务记录 |
-| `ai_replenishment_detail` | 补货明细 |
-| `ai_replenishment_part_summary` | 配件级汇总 |
-| `ai_task_execution_log` | 执行日志 |
-| `ai_analysis_report` | 分析报告(JSON结构化) |
diff --git a/docs/商家组合维度分析需求设计.md b/docs/商家组合维度分析需求设计.md
new file mode 100644
index 0000000..6f75584
--- /dev/null
+++ b/docs/商家组合维度分析需求设计.md
@@ -0,0 +1,856 @@
+# 商家组合维度分析报告 - 需求分析与设计文档
+
+> **版本**: 2.0.0
+> **日期**: 2026-02-12
+> **项目**: fw-pms-ai(AI配件补货建议系统)
+
+---
+
+## 1. 业务背景与目标
+
+### 1.1 业务痛点
+
+汽车配件管理面临以下核心挑战:
+
+| 痛点 | 描述 | 影响 |
+|------|------|------|
+| **库存失衡** | 部分配件长期缺货,部分配件严重呆滞 | 缺货导致客户流失,呆滞占用资金 |
+| **人工决策低效** | 传统补货依赖采购员经验判断 | 效率低、易出错、难以规模化 |
+| **多门店协调困难** | 同一商家组合下的多门店库存无法统一调配 | 资源利用率低,部分门店过剩而另一部分缺货 |
+| **数据利用不足** | 丰富的销售数据未能有效转化为决策依据 | 补货缺乏数据支撑,决策质量参差不齐 |
+
+### 1.2 项目目标
+
+```mermaid
+mindmap
+ root((商家组合维度分析))
+ 智能补货建议
+ LLM驱动分析
+ 配件级决策
+ 门店级分配
+ 风险识别与预警
+ 呆滞件识别
+ 低频件过滤
+ 缺货预警
+ 数据驱动分析报告
+ 库存概览
+ 销量分析
+ 库存健康度
+ 补货建议汇总
+ 可视化展示
+ 分析报告
+ 配件明细
+ 执行日志
+```
+
+**核心价值主张**:
+1. **智能化**:通过 LLM 自动分析库销比数据,生成专业补货建议
+2. **精细化**:从商家组合维度统一分析,再下钻到配件级、门店级
+3. **专业化**:输出的分析理由贴合采购人员专业度,包含具体数据指标
+
+---
+
+## 2. 功能模块设计
+
+### 2.1 功能架构
+
+```mermaid
+flowchart TB
+ subgraph 用户层["🖥️ 用户层"]
+ UI[Web管理界面]
+ end
+
+ subgraph 应用层["⚙️ 应用层"]
+ API[FastAPI 接口层]
+
+ subgraph Agent["🤖 LangGraph Agent"]
+ N1[获取配件库销比
fetch_part_ratio]
+ N2[LLM分析生成建议
sql_agent]
+ N3[转换补货明细
allocate_budget]
+ N4[生成分析报告
generate_analysis_report]
+ end
+
+ subgraph ReportSubgraph["📊 分析报告并发子图"]
+ R1[库存概览 LLM]
+ R2[销量分析 LLM]
+ R3[健康度 LLM]
+ R4[补货建议 LLM]
+ end
+ end
+
+ subgraph 基础设施["🔧 基础设施"]
+ LLM[LLM服务
智谱GLM/豆包/OpenAI/Anthropic]
+ DB[(MySQL数据库)]
+ end
+
+ UI --> API
+ API --> Agent
+ N1 --> N2 --> N3 --> N4
+ N4 --> ReportSubgraph
+ N1 -.-> DB
+ N2 -.-> LLM
+ N3 -.-> DB
+ R1 & R2 & R3 & R4 -.-> LLM
+```
+
+### 2.2 功能模块清单
+
+| 模块 | 功能 | 输入 | 输出 |
+|------|------|------|------|
+| **数据获取** | 获取商家组合内所有配件的库销比数据 | dealer_grouping_id | part_ratios[] |
+| **LLM分析** | 按配件分组分析,生成补货建议和决策理由 | part_ratios, base_ratio | llm_suggestions[] |
+| **建议转换** | 将LLM建议转换为结构化的补货明细 | llm_suggestions[] | details[], part_summaries[] |
+| **分析报告** | 四大板块统计计算 + 并发LLM分析 | part_ratios, part_results | analysis_report |
+
+---
+
+## 3. 系统架构设计
+
+### 3.1 整体架构
+
+```mermaid
+C4Component
+ title 商家组合维度分析系统 - 组件架构
+
+ Container_Boundary(web, "Web层") {
+ Component(ui, "前端UI", "HTML/CSS/JS", "任务管理、结果展示")
+ }
+
+ Container_Boundary(api, "API层") {
+ Component(routes, "路由模块", "FastAPI", "REST API接口")
+ Component(scheduler, "定时调度", "APScheduler", "任务调度")
+ }
+
+ Container_Boundary(agent, "Agent层") {
+ Component(workflow, "工作流引擎", "LangGraph", "状态机编排")
+ Component(nodes, "节点实现", "Python", "业务逻辑")
+ Component(report_subgraph, "报告子图", "LangGraph 并发子图", "4路并发LLM分析")
+ Component(prompts, "提示词", "Markdown", "LLM指令")
+ }
+
+ Container_Boundary(service, "服务层") {
+ Component(data, "数据服务", "Python", "数据查询")
+ Component(writer, "写入服务", "Python", "结果持久化")
+ }
+
+ Container_Boundary(infra, "基础设施") {
+ ComponentDb(mysql, "MySQL", "数据库", "业务数据存储")
+ Component(llm, "LLM", "GLM/Doubao/OpenAI/Anthropic", "大语言模型")
+ }
+
+ Rel(ui, routes, "HTTP请求")
+ Rel(routes, workflow, "触发任务")
+ Rel(workflow, nodes, "执行")
+ Rel(nodes, report_subgraph, "fan-out/fan-in")
+ Rel(nodes, prompts, "加载")
+ Rel(nodes, llm, "调用")
+ Rel(nodes, data, "查询")
+ Rel(nodes, writer, "写入")
+ Rel(data, mysql, "SQL")
+ Rel(writer, mysql, "SQL")
+```
+
+### 3.2 工作流状态机
+
+```mermaid
+stateDiagram-v2
+ [*] --> FetchPartRatio: 启动任务
+
+ FetchPartRatio --> SQLAgent: 获取库销比数据
+ FetchPartRatio --> [*]: 无数据
+
+ SQLAgent --> SQLAgent: 重试(错误 & 次数<3)
+ SQLAgent --> AllocateBudget: 分析完成
+ SQLAgent --> [*]: 重试失败
+
+ AllocateBudget --> GenerateReport: 转换完成
+
+ GenerateReport --> [*]: 生成报告
+
+ state GenerateReport {
+ [*] --> 统计计算
+ 统计计算 --> 并发LLM子图
+
+ state 并发LLM子图 {
+ [*] --> 库存概览LLM
+ [*] --> 销量分析LLM
+ [*] --> 健康度LLM
+ [*] --> 补货建议LLM
+ 库存概览LLM --> [*]
+ 销量分析LLM --> [*]
+ 健康度LLM --> [*]
+ 补货建议LLM --> [*]
+ }
+
+ 并发LLM子图 --> 汇总写入
+ 汇总写入 --> [*]
+ }
+```
+
+---
+
+## 4. 核心算法说明
+
+### 4.1 三层决策逻辑
+
+```mermaid
+flowchart LR
+ subgraph L1["第一层: 配件级判断"]
+ A1[汇总商家组合内
所有门店数据]
+ A2[计算整体库销比]
+ A3{是否需要补货?}
+ A4[生成配件级理由]
+ end
+
+ subgraph L2["第二层: 门店级分配"]
+ B1[按库销比从低到高排序]
+ B2[计算各门店缺口]
+ B3[分配补货数量]
+ end
+
+ subgraph L3["第三层: 决策理由生成"]
+ C1[状态判定标签]
+ C2[关键指标数据]
+ C3[缺口分析]
+ C4[天数说明]
+ end
+
+ A1 --> A2 --> A3
+ A3 -->|是| L2
+ A3 -->|否| A4
+ B1 --> B2 --> B3
+ B3 --> L3
+ C1 --> C2 --> C3 --> C4
+```
+
+### 4.2 补货数量计算公式
+
+```
+suggest_cnt = ceil(目标库销比 × 月均销量 - 当前库存)
+```
+
+其中:
+- **有效库存** = `in_stock_unlocked_cnt` + `on_the_way_cnt` + `has_plan_cnt`
+- **月均销量** = (`out_stock_cnt` + `storage_locked_cnt` + `out_stock_ongoing_cnt` + `buy_cnt`) / 3
+- **资金占用** = (`in_stock_unlocked_cnt` + `on_the_way_cnt`) × `cost_price`
+
+### 4.3 配件分类与处理规则
+
+| 分类 | 判定条件 | 处理策略 |
+|------|----------|----------|
+| **缺货件** | 有效库存 = 0 且 月均销量 ≥ 1 | 优先补货 |
+| **呆滞件** | 有效库存 > 0 且 90天出库数 = 0 | 不补货,建议清理 |
+| **低频件** | 月均销量 < 1 或 出库次数 < 3 或 出库间隔 ≥ 30天 | 不补货 |
+| **正常件** | 不属于以上三类 | 按缺口补货 |
+
+> 分类优先级:缺货件 > 呆滞件 > 低频件 > 正常件(按顺序判断,命中即止)
+
+### 4.4 优先级判定标准
+
+```mermaid
+flowchart TD
+ A{库存状态} -->|库存=0 且 销量活跃| H[高优先级
急需补货]
+ A -->|库销比<0.5| M[中优先级
建议补货]
+ A -->|0.5≤库销比<目标值| L[低优先级
可选补货]
+ A -->|库销比≥目标值| N[无需补货
库存充足]
+
+ style H fill:#ff6b6b
+ style M fill:#feca57
+ style L fill:#48dbfb
+ style N fill:#2ecc71
+```
+
+---
+
+## 5. 数据模型设计
+
+### 5.1 ER图
+
+```mermaid
+erDiagram
+ AI_REPLENISHMENT_TASK ||--o{ AI_REPLENISHMENT_DETAIL : contains
+ AI_REPLENISHMENT_TASK ||--o{ AI_REPLENISHMENT_PART_SUMMARY : contains
+ AI_REPLENISHMENT_TASK ||--o{ AI_TASK_EXECUTION_LOG : logs
+ AI_REPLENISHMENT_TASK ||--o| AI_ANALYSIS_REPORT : generates
+ PART_RATIO }o--|| AI_REPLENISHMENT_DETAIL : references
+
+ AI_REPLENISHMENT_TASK {
+ bigint id PK
+ varchar task_no UK "AI-开头"
+ bigint group_id "集团ID"
+ bigint dealer_grouping_id
+ varchar dealer_grouping_name
+ bigint brand_grouping_id "品牌组合ID"
+ decimal plan_amount "计划采购金额"
+ decimal actual_amount "实际分配金额"
+ int part_count "配件数量"
+ decimal base_ratio "基准库销比"
+ tinyint status "0运行/1成功/2失败"
+ varchar llm_provider
+ varchar llm_model
+ int llm_total_tokens
+ varchar statistics_date
+ datetime start_time
+ datetime end_time
+ }
+
+ AI_REPLENISHMENT_DETAIL {
+ bigint id PK
+ varchar task_no FK
+ bigint group_id
+ bigint dealer_grouping_id
+ bigint brand_grouping_id
+ bigint shop_id "库房ID"
+ varchar shop_name
+ varchar part_code "配件编码"
+ varchar part_name
+ varchar unit
+ decimal cost_price
+ decimal current_ratio "当前库销比"
+ decimal base_ratio "基准库销比"
+ decimal post_plan_ratio "计划后库销比"
+ decimal valid_storage_cnt "有效库存"
+ decimal avg_sales_cnt "月均销量"
+ int suggest_cnt "建议数量"
+ decimal suggest_amount "建议金额"
+ text suggestion_reason "决策理由"
+ int priority "1高/2中/3低"
+ float llm_confidence "LLM置信度"
+ }
+
+ AI_REPLENISHMENT_PART_SUMMARY {
+ bigint id PK
+ varchar task_no FK
+ bigint group_id
+ bigint dealer_grouping_id
+ varchar part_code "配件编码"
+ varchar part_name
+ varchar unit
+ decimal cost_price
+ decimal total_storage_cnt "总库存"
+ decimal total_avg_sales_cnt "总月均销量"
+ decimal group_current_ratio "商家组合库销比"
+ int total_suggest_cnt "总建议数量"
+ decimal total_suggest_amount "总建议金额"
+ int shop_count "涉及门店数"
+ int need_replenishment_shop_count "需补货门店数"
+ text part_decision_reason "配件级理由"
+ int priority "1高/2中/3低"
+ float llm_confidence
+ }
+
+ AI_TASK_EXECUTION_LOG {
+ bigint id PK
+ varchar task_no FK
+ bigint group_id
+ bigint brand_grouping_id
+ varchar brand_grouping_name
+ bigint dealer_grouping_id
+ varchar dealer_grouping_name
+ varchar step_name "步骤名称"
+ int step_order "步骤顺序"
+ tinyint status "0进行/1成功/2失败/3跳过"
+ text input_data "输入JSON"
+ text output_data "输出JSON"
+ text error_message
+ int retry_count
+ text sql_query
+ text llm_prompt
+ text llm_response
+ int llm_tokens "Token消耗"
+ int execution_time_ms "耗时"
+ }
+
+ AI_ANALYSIS_REPORT {
+ bigint id PK
+ varchar task_no FK
+ bigint group_id
+ bigint dealer_grouping_id
+ varchar dealer_grouping_name
+ bigint brand_grouping_id
+ varchar report_type "默认replenishment"
+ json inventory_overview "库存概览"
+ json sales_analysis "销量分析"
+ json inventory_health "健康度"
+ json replenishment_summary "补货建议"
+ varchar llm_provider
+ varchar llm_model
+ int llm_tokens
+ int execution_time_ms
+ }
+
+ PART_RATIO {
+ bigint id PK
+ bigint shop_id
+ varchar part_code
+ decimal in_stock_unlocked_cnt "在库未锁定"
+ decimal on_the_way_cnt "在途"
+ decimal has_plan_cnt "已有计划"
+ decimal out_stock_cnt "出库数"
+ decimal storage_locked_cnt "库存锁定"
+ decimal out_stock_ongoing_cnt "出库在途"
+ decimal buy_cnt "采购数"
+ decimal cost_price "成本价"
+ int out_times "出库次数"
+ int out_duration "平均出库间隔"
+ }
+```
+
+### 5.2 核心表结构
+
+#### ai_replenishment_task(任务主表)
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| task_no | VARCHAR(32) | 任务编号,AI-开头,唯一 |
+| group_id | BIGINT | 集团ID |
+| dealer_grouping_id | BIGINT | 商家组合ID |
+| dealer_grouping_name | VARCHAR(128) | 商家组合名称 |
+| brand_grouping_id | BIGINT | 品牌组合ID |
+| plan_amount | DECIMAL(14,2) | 计划采购金额(预算) |
+| actual_amount | DECIMAL(14,2) | 实际分配金额 |
+| part_count | INT | 配件数量 |
+| base_ratio | DECIMAL(10,4) | 基准库销比 |
+| status | TINYINT | 状态: 0运行中/1成功/2失败 |
+| llm_provider | VARCHAR(32) | LLM提供商 |
+| llm_model | VARCHAR(64) | LLM模型名称 |
+| statistics_date | VARCHAR(16) | 统计日期 |
+| start_time / end_time | DATETIME | 任务执行起止时间 |
+
+#### ai_replenishment_part_summary(配件汇总表)
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| task_no | VARCHAR(32) | 任务编号 |
+| group_id | BIGINT | 集团ID |
+| dealer_grouping_id | BIGINT | 商家组合ID |
+| part_code | VARCHAR(64) | 配件编码 |
+| part_name | VARCHAR(256) | 配件名称 |
+| cost_price | DECIMAL(14,2) | 成本价 |
+| total_storage_cnt | DECIMAL(14,2) | 商家组合内总库存 |
+| total_avg_sales_cnt | DECIMAL(14,2) | 总月均销量 |
+| group_current_ratio | DECIMAL(10,4) | 商家组合级库销比 |
+| total_suggest_cnt | INT | 总建议数量 |
+| total_suggest_amount | DECIMAL(14,2) | 总建议金额 |
+| shop_count | INT | 涉及门店数 |
+| need_replenishment_shop_count | INT | 需补货门店数 |
+| part_decision_reason | TEXT | 配件级补货理由 |
+| priority | INT | 优先级: 1高/2中/3低 |
+
+#### ai_analysis_report(分析报告表)
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| task_no | VARCHAR(32) | 任务编号 |
+| group_id | BIGINT | 集团ID |
+| dealer_grouping_id | BIGINT | 商家组合ID |
+| report_type | VARCHAR(32) | 报告类型(默认 replenishment) |
+| inventory_overview | JSON | 库存总体概览(stats + llm_analysis) |
+| sales_analysis | JSON | 销量分析(stats + llm_analysis) |
+| inventory_health | JSON | 库存健康度(stats + chart_data + llm_analysis) |
+| replenishment_summary | JSON | 补货建议(stats + llm_analysis) |
+| llm_provider | VARCHAR(32) | LLM提供商 |
+| llm_model | VARCHAR(64) | LLM模型名称 |
+| llm_tokens | INT | LLM Token总消耗 |
+| execution_time_ms | INT | 执行耗时(毫秒) |
+
+---
+
+## 6. API 接口设计
+
+### 6.1 接口总览
+
+```mermaid
+flowchart LR
+ subgraph Tasks["任务管理"]
+ T1["GET /api/tasks"]
+ T2["GET /api/tasks/:task_no"]
+ end
+
+ subgraph Details["明细查询"]
+ D1["GET /api/tasks/:task_no/details"]
+ D2["GET /api/tasks/:task_no/part-summaries"]
+ D3["GET /api/tasks/:task_no/parts/:part_code/shops"]
+ end
+
+ subgraph Reports["报告模块"]
+ R1["GET /api/tasks/:task_no/analysis-report"]
+ R2["GET /api/tasks/:task_no/logs"]
+ end
+
+ subgraph Health["健康检查"]
+ H1["GET /health"]
+ end
+```
+
+### 6.2 核心接口定义
+
+#### 1. 获取任务列表
+```
+GET /api/tasks?page=1&page_size=20&status=1&dealer_grouping_id=100&statistics_date=20260212
+```
+
+**响应示例**:
+```json
+{
+ "items": [
+ {
+ "id": 1,
+ "task_no": "AI-ABC12345",
+ "group_id": 2,
+ "dealer_grouping_id": 100,
+ "dealer_grouping_name": "华东区商家组合",
+ "brand_grouping_id": 50,
+ "plan_amount": 100000.00,
+ "actual_amount": 89520.50,
+ "part_count": 156,
+ "base_ratio": 1.5000,
+ "status": 1,
+ "status_text": "成功",
+ "llm_provider": "openai_compat",
+ "llm_model": "glm-4-7-251222",
+ "llm_total_tokens": 8500,
+ "statistics_date": "20260212",
+ "start_time": "2026-02-12 02:00:00",
+ "end_time": "2026-02-12 02:05:30",
+ "duration_seconds": 330,
+ "create_time": "2026-02-12 02:00:00"
+ }
+ ],
+ "total": 100,
+ "page": 1,
+ "page_size": 20
+}
+```
+
+#### 2. 获取配件汇总(商家组合维度)
+```
+GET /api/tasks/{task_no}/part-summaries?sort_by=total_suggest_amount&sort_order=desc&priority=1
+```
+
+**响应示例**:
+```json
+{
+ "items": [
+ {
+ "id": 1,
+ "task_no": "AI-ABC12345",
+ "part_code": "C211F280503",
+ "part_name": "机油滤芯",
+ "unit": "个",
+ "cost_price": 140.00,
+ "total_storage_cnt": 25,
+ "total_avg_sales_cnt": 18.5,
+ "group_current_ratio": 1.35,
+ "group_post_plan_ratio": 2.0,
+ "total_suggest_cnt": 12,
+ "total_suggest_amount": 1680.00,
+ "shop_count": 5,
+ "need_replenishment_shop_count": 3,
+ "part_decision_reason": "【配件决策】该配件在商家组合内总库存25件...",
+ "priority": 1,
+ "llm_confidence": 0.85
+ }
+ ],
+ "total": 50,
+ "page": 1,
+ "page_size": 50
+}
+```
+
+#### 3. 获取门店级明细
+```
+GET /api/tasks/{task_no}/parts/{part_code}/shops
+```
+
+**响应示例**:
+```json
+{
+ "total": 3,
+ "items": [
+ {
+ "id": 101,
+ "task_no": "AI-ABC12345",
+ "shop_id": 1001,
+ "shop_name": "杭州西湖店",
+ "part_code": "C211F280503",
+ "part_name": "机油滤芯",
+ "cost_price": 140.00,
+ "valid_storage_cnt": 5,
+ "avg_sales_cnt": 6.2,
+ "current_ratio": 0.81,
+ "post_plan_ratio": 1.61,
+ "suggest_cnt": 5,
+ "suggest_amount": 700.00,
+ "suggestion_reason": "「建议补货」当前库存5件,月均销量6.2件...",
+ "priority": 1
+ }
+ ]
+}
+```
+
+#### 4. 获取配件建议明细
+```
+GET /api/tasks/{task_no}/details?page=1&page_size=50&sort_by=suggest_amount&sort_order=desc&part_code=C211
+```
+
+#### 5. 获取分析报告
+```
+GET /api/tasks/{task_no}/analysis-report
+```
+
+**响应示例**:
+```json
+{
+ "id": 1,
+ "task_no": "AI-ABC12345",
+ "group_id": 2,
+ "dealer_grouping_id": 100,
+ "report_type": "replenishment",
+ "inventory_overview": {
+ "stats": {
+ "total_valid_storage_cnt": 2500,
+ "total_valid_storage_amount": 350000.0,
+ "total_capital_occupation": 280000.0,
+ "overall_ratio": 1.35,
+ "part_count": 156
+ },
+ "llm_analysis": { "..." : "LLM生成的分析结论" }
+ },
+ "sales_analysis": {
+ "stats": { "total_avg_sales_cnt": 1850, "..." : "..." },
+ "llm_analysis": { "..." : "..." }
+ },
+ "inventory_health": {
+ "stats": { "shortage": { "count": 12, "amount": 5000 }, "..." : "..." },
+ "chart_data": { "labels": ["缺货件","呆滞件","低频件","正常件"], "..." : "..." },
+ "llm_analysis": { "..." : "..." }
+ },
+ "replenishment_summary": {
+ "stats": { "urgent": { "count": 15, "amount": 25000 }, "..." : "..." },
+ "llm_analysis": { "..." : "..." }
+ },
+ "llm_tokens": 3200,
+ "execution_time_ms": 12000
+}
+```
+
+#### 6. 获取执行日志
+```
+GET /api/tasks/{task_no}/logs
+```
+
+---
+
+## 7. 前端交互设计
+
+### 7.1 页面结构
+
+```mermaid
+flowchart TB
+ subgraph Dashboard["仪表盘"]
+ S1[统计卡片]
+ S2[最近任务列表]
+ end
+
+ subgraph TaskList["任务列表页"]
+ L1[筛选条件]
+ L2[任务表格]
+ L3[分页控件]
+ end
+
+ subgraph TaskDetail["任务详情页"]
+ D1[任务头部信息]
+ D2[统计卡片]
+
+ subgraph Tabs["标签页"]
+ T1[配件明细]
+ T2[分析报告]
+ T3[执行日志]
+ T4[任务信息]
+ end
+ end
+
+ Dashboard --> TaskList --> TaskDetail
+```
+
+### 7.2 配件明细交互
+
+```mermaid
+sequenceDiagram
+ participant U as 用户
+ participant UI as 前端UI
+ participant API as 后端API
+
+ U->>UI: 点击任务详情
+ UI->>API: GET /api/tasks/{task_no}/part-summaries
+ API-->>UI: 返回配件汇总列表
+ UI->>UI: 渲染配件表格(可排序/筛选/优先级)
+
+ U->>UI: 点击展开某配件
+ UI->>API: GET /api/tasks/{task_no}/parts/{part_code}/shops
+ API-->>UI: 返回门店级明细
+ UI->>UI: 展开子表格显示门店数据
+
+ Note over UI: 门店数据包含:
库存、销量、库销比
建议数量、建议理由
计划后库销比
+```
+
+### 7.3 关键UI组件
+
+| 组件 | 功能 | 交互方式 |
+|------|------|----------|
+| **配件汇总表格** | 展示商家组合维度的配件建议 | 支持排序、筛选、分页、优先级筛选 |
+| **可展开行** | 展示配件下的门店明细 | 点击行展开/收起 |
+| **配件决策卡片** | 显示LLM生成的配件级理由 | 展开配件时显示 |
+| **库销比指示器** | 直观显示库销比健康度 | 颜色渐变(红/黄/绿) |
+| **分析报告面板** | 四大板块数据驱动展示 | 统计数据 + LLM 分析 + 图表 |
+
+---
+
+## 8. 分析报告设计
+
+### 8.1 报告模块结构
+
+分析报告由 **统计计算** + **4路并发 LLM 分析** 的 LangGraph 子图生成。每个板块包含 `stats`(统计数据)和 `llm_analysis`(LLM 分析结论)。
+
+```mermaid
+flowchart TB
+ subgraph Report["分析报告四大板块"]
+ M1["板块1: 库存总体概览
inventory_overview"]
+ M2["板块2: 销量分析
sales_analysis"]
+ M3["板块3: 库存构成健康度
inventory_health"]
+ M4["板块4: 补货建议生成情况
replenishment_summary"]
+ end
+
+ M1 --> S1[有效库存/资金占用]
+ M1 --> S2[在库/在途/已有计划]
+ M1 --> S3[整体库销比]
+
+ M2 --> R1[月均销量/销售金额]
+ M2 --> R2[有销量/无销量配件数]
+ M2 --> R3[出库/锁定/采购统计]
+
+ M3 --> P1[缺货件统计]
+ M3 --> P2[呆滞件统计]
+ M3 --> P3[低频件统计]
+ M3 --> P4[正常件统计]
+ M3 --> P5[chart_data图表数据]
+
+ M4 --> E1[急需补货统计]
+ M4 --> E2[建议补货统计]
+ M4 --> E3[可选补货统计]
+```
+
+### 8.2 各板块统计计算与LLM分析
+
+| 板块 | 统计计算 | LLM 分析 | 提示词文件 |
+|------|---------|---------|-----------|
+| **库存概览** | 有效库存、资金占用、配件总数、整体库销比 | 库存状况综合评价 | `report_inventory_overview.md` |
+| **销量分析** | 月均销量、出库频次、有/无销量配件数 | 销售趋势洞察 | `report_sales_analysis.md` |
+| **库存健康度** | 缺货/呆滞/低频/正常分类统计(数量/金额/占比) | 健康度风险提示 | `report_inventory_health.md` |
+| **补货建议汇总** | 按优先级(急需/建议/可选)分类统计 | 补货策略建议 | `report_replenishment_summary.md` |
+
+> 四个 LLM 分析节点使用 LangGraph 子图 **并发执行**(fan-out / fan-in),单板块失败不影响其他板块。
+
+### 8.3 并发子图实现
+
+```mermaid
+flowchart LR
+ START --> A[库存概览LLM] --> END2[END]
+ START --> B[销量分析LLM] --> END2
+ START --> C[健康度LLM] --> END2
+ START --> D[补货建议LLM] --> END2
+```
+
+子图采用 `ReportLLMState` TypedDict 定义状态,使用 `Annotated` reducer 合并并发结果:
+- 分析结果:`_merge_dict`(保留非 None)
+- Token 用量:`_sum_int`(累加)
+
+---
+
+## 9. 技术选型
+
+| 组件 | 技术 | 选型理由 |
+|------|------|----------|
+| **编程语言** | Python 3.11+ | 丰富的AI/ML生态 |
+| **Agent框架** | LangChain + LangGraph | 成熟的LLM编排框架,支持并发子图 |
+| **API框架** | FastAPI | 高性能、自动文档 |
+| **数据库** | MySQL | 与主系统保持一致 |
+| **LLM** | 智谱GLM / 豆包 / OpenAI兼容 / Anthropic兼容 | 多模型支持,优先级自动选择 |
+| **前端** | 原生HTML+CSS+JS | 轻量级,无构建依赖 |
+
+### LLM 客户端优先级
+
+| 优先级 | 客户端 | 触发条件 |
+|--------|--------|----------|
+| 1 | `OpenAICompatClient` | `OPENAI_COMPAT_API_KEY` 已配置 |
+| 2 | `AnthropicCompatClient` | `ANTHROPIC_API_KEY` 已配置 |
+| 3 | `GLMClient` | `GLM_API_KEY` 已配置 |
+| 4 | `DoubaoClient` | `DOUBAO_API_KEY` 已配置 |
+
+---
+
+## 10. 部署与运维
+
+### 10.1 部署架构
+
+```mermaid
+flowchart LR
+ subgraph Client["客户端"]
+ Browser[浏览器]
+ end
+
+ subgraph Server["服务器"]
+ Nginx[Nginx
静态资源/反向代理]
+ API[FastAPI
API服务]
+ Scheduler[APScheduler
定时任务]
+ end
+
+ subgraph External["外部服务"]
+ LLM[LLM API]
+ DB[(MySQL)]
+ end
+
+ Browser --> Nginx
+ Nginx --> API
+ API --> LLM
+ API --> DB
+ Scheduler --> API
+```
+
+### 10.2 关键监控指标
+
+| 指标 | 阈值 | 告警方式 |
+|------|------|----------|
+| 任务成功率 | < 95% | 邮件 |
+| LLM响应时间 | > 30s | 日志 |
+| Token消耗 | > 10000/任务 | 日志 |
+| API响应时间 | > 2s | 监控 |
+
+---
+
+## 附录
+
+### A. 术语表
+
+| 术语 | 定义 |
+|------|------|
+| 商家组合 | 多个经销商/门店的逻辑分组 |
+| 库销比 | 库存数量 / 月均销量,衡量库存健康度 |
+| 呆滞件 | 有库存但90天无出库数的配件 |
+| 低频件 | 月均销量<1 或 出库次数<3 或 出库间隔≥30天的配件 |
+| 有效库存 | 在库未锁定 + 在途 + 已有计划 |
+| 资金占用 | (在库未锁定 + 在途) × 成本价 |
+
+### B. 参考文档
+
+- [docs/architecture.md](file:///Users/gunieason/Studio/Code/FeeWee/fw-pms-ai/docs/architecture.md) - 系统架构文档
+- [prompts/part_shop_analysis.md](file:///Users/gunieason/Studio/Code/FeeWee/fw-pms-ai/prompts/part_shop_analysis.md) - 配件分析提示词
+- [prompts/report_inventory_overview.md](file:///Users/gunieason/Studio/Code/FeeWee/fw-pms-ai/prompts/report_inventory_overview.md) - 库存概览提示词
+- [prompts/report_sales_analysis.md](file:///Users/gunieason/Studio/Code/FeeWee/fw-pms-ai/prompts/report_sales_analysis.md) - 销量分析提示词
+- [prompts/report_inventory_health.md](file:///Users/gunieason/Studio/Code/FeeWee/fw-pms-ai/prompts/report_inventory_health.md) - 库存健康度提示词
+- [prompts/report_replenishment_summary.md](file:///Users/gunieason/Studio/Code/FeeWee/fw-pms-ai/prompts/report_replenishment_summary.md) - 补货建议提示词
+
+### C. 版本变更记录
+
+| 版本 | 日期 | 变更说明 |
+|------|------|----------|
+| 1.0.0 | 2026-02-09 | 初始版本 |
+| 2.0.0 | 2026-02-12 | 根据实际实现更新:分析报告重构为四大数据驱动板块、ER图更新、API路径和字段对齐、新增LLM客户端等 |
diff --git a/prompts/analysis_report.md b/prompts/analysis_report.md
deleted file mode 100644
index 7da9790..0000000
--- a/prompts/analysis_report.md
+++ /dev/null
@@ -1,174 +0,0 @@
-# 智能补货建议分析报告
-
-你是一位资深汽车配件采购顾问。AI系统已经基于全量数据生成了补货建议的**多维统计分布数据**,现在需要你站在更宏观的视角,为采购决策者提供**整体性分析**。
-
-> **核心定位**: 统计数据揭示了补货的宏观特征与分布情况,本报告聚焦于"整体策略、风险预警、资金规划"等**决策层面**的洞察。
-
----
-
-## 商家组合信息
-
-| 项目 | 数值 |
-|------|------|
-| 商家组合ID | {dealer_grouping_id} |
-| 商家组合名称 | {dealer_grouping_name} |
-| 报告生成日期 | {statistics_date} |
-
----
-
-## 本期补货建议概览 (统计摘要)
-
-{suggestion_summary}
-
----
-
-## 库存健康度参考
-
-| 状态分类 | 配件数量 | 涉及金额 |
-|----------|----------|----------|
-| 缺货件 | {shortage_cnt} | {shortage_amount} |
-| 呆滞件 | {stagnant_cnt} | {stagnant_amount} |
-| 低频件 | {low_freq_cnt} | {low_freq_amount} |
-
----
-
-## 分析任务
-
-请严格按以下4个模块输出 **JSON格式** 的整体分析报告。
-
-**注意**: 分析应基于提供的统计分布数据(如优先级、价格区间、周转频次等),按以下**宏观维度**展开。
-
-### 模块1: 整体态势研判 (overall_assessment)
-
-从全局视角评估本次补货建议:
-
-1. **补货规模评估**: 结合涉及配件数和总金额,评估本期补货的力度和资金需求规模。
-2. **结构特征分析**: 基于价格区间和周转频次分布,分析补货建议的结构特征(如是否偏向高频低价件,或存在大量低频高价件)。
-3. **时机判断**: 当前是否处于补货的有利时机?需要考虑哪些时间因素(如节假日、促销季、供应商备货周期)?
-
-### 模块2: 风险预警与应对 (risk_alerts)
-
-识别本次补货可能面临的风险并给出应对建议:
-
-1. **供应风险**: 高频或高优先级配件的供应保障是否关键?
-2. **资金风险**: 大额补货配件(≥5000元)的占比是否过高?是否构成资金压力?
-3. **库存结构风险**: 低频配件的补货比例是否合理?是否存在积压风险?
-4. **执行重点**: 针对高优先级或大额补货的配件,建议采取什么复核策略?
-
-### 模块3: 采购策略建议 (procurement_strategy)
-
-提供整体性的采购执行策略:
-
-1. **优先级排序原则**: 结合优先级分布数据,给出资金分配和采购执行的先后顺序建议。
-2. **批量采购机会**: 对于低价高频的配件,是否建议采用批量采购策略以优化成本?
-3. **分批采购建议**: 对于大额或低频配件,是否建议分批次补货以控制风险?
-4. **供应商协调要点**: 针对本期补货的结构特征(如大额占比高或高频占比高),与供应商沟通的侧重点是什么?
-
-### 模块4: 效果预期与建议 (expected_impact)
-
-预估按建议执行后的整体效果:
-
-1. **库存健康度改善**: 补货后整体库存结构预计如何变化?缺货率预计下降多少?
-2. **资金效率预估**: 本期补货的预计投入产出如何?资金周转是否会改善?
-3. **后续关注点**: 补货完成后需要持续关注哪些指标或配件?下一步建议行动是什么?
-
----
-
-## 输出格式
-
-直接输出JSON对象,**不要**包含 ```json 标记:
-
-{{
- "overall_assessment": {{
- "scale_evaluation": {{
- "current_vs_historical": "与历史同期对比结论",
- "possible_reasons": "规模变化的可能原因"
- }},
- "structure_analysis": {{
- "category_distribution": "品类分布特征",
- "price_range_distribution": "价格区间分布特征",
- "turnover_distribution": "周转频次分布特征",
- "imbalance_warning": "是否存在失衡及说明"
- }},
- "timing_judgment": {{
- "is_favorable": true或false,
- "timing_factors": "需要考虑的时间因素",
- "recommendation": "时机相关建议"
- }}
- }},
- "risk_alerts": {{
- "supply_risks": [
- {{
- "risk_type": "风险类型(缺货/涨价/交期延长等)",
- "affected_scope": "影响范围描述",
- "likelihood": "可能性评估(高/中/低)",
- "mitigation": "应对建议"
- }}
- ],
- "capital_risks": {{
- "cash_flow_pressure": "资金压力评估",
- "stagnation_warning": "呆滞风险提示",
- "recommendation": "资金风险应对建议"
- }},
- "market_risks": [
- {{
- "risk_description": "市场风险描述",
- "affected_parts": "影响配件范围",
- "recommendation": "应对建议"
- }}
- ],
- "execution_anomalies": [
- {{
- "anomaly_type": "异常类型",
- "description": "异常描述",
- "review_suggestion": "复核建议"
- }}
- ]
- }},
- "procurement_strategy": {{
- "priority_principle": {{
- "tier1_criteria": "第一优先级标准及说明",
- "tier2_criteria": "第二优先级标准及说明",
- "tier3_criteria": "可延后采购的标准及说明"
- }},
- "batch_opportunities": {{
- "potential_savings": "潜在节省金额或比例",
- "applicable_categories": "适用品类或供应商",
- "execution_suggestion": "具体操作建议"
- }},
- "phased_procurement": {{
- "recommended_parts": "建议分批采购的配件范围",
- "suggested_rhythm": "建议的采购节奏"
- }},
- "supplier_coordination": {{
- "key_communications": "关键沟通事项",
- "timing_suggestions": "沟通时机建议"
- }}
- }},
- "expected_impact": {{
- "inventory_health": {{
- "structure_improvement": "库存结构改善预期",
- "shortage_reduction": "缺货率预计下降幅度"
- }},
- "capital_efficiency": {{
- "investment_amount": 本期补货投入金额,
- "expected_return": "预期收益描述",
- "turnover_improvement": "周转改善预期"
- }},
- "follow_up_actions": {{
- "key_metrics_to_watch": "需持续关注的指标",
- "next_steps": "下一步建议行动"
- }}
- }}
-}}
-
----
-
-## 重要约束
-
-1. **输出必须是合法的JSON对象**
-2. **所有金额单位为元,保留2位小数**
-3. **聚焦宏观分析,不要重复明细中已有的配件级别信息**
-4. **风险和效果预估尽量量化**
-5. **策略建议要具体可执行,避免空泛描述**
-6. **分析基于提供的汇总数据,保持客观理性**
diff --git a/prompts/report_inventory_health.md b/prompts/report_inventory_health.md
new file mode 100644
index 0000000..09a0f0b
--- /dev/null
+++ b/prompts/report_inventory_health.md
@@ -0,0 +1,200 @@
+# 库存健康度分析提示词
+
+你是一位汽车配件库存健康度诊断专家,擅长从库存结构数据中识别问题并提出改善方案。请基于以下健康度统计数据,进行专业的库存健康度诊断。
+
+---
+
+## 统计数据
+
+| 指标 | 数值 |
+|------|------|
+| 配件总种类数 | {total_count} |
+| 库存总金额 | {total_amount} 元 |
+
+### 各类型配件统计
+
+| 类型 | 数量 | 数量占比 | 金额(元) | 金额占比 |
+|------|------|----------|------------|----------|
+| 缺货件 | {shortage_count} | {shortage_count_pct}% | {shortage_amount} | {shortage_amount_pct}% |
+| 呆滞件 | {stagnant_count} | {stagnant_count_pct}% | {stagnant_amount} | {stagnant_amount_pct}% |
+| 低频件 | {low_freq_count} | {low_freq_count_pct}% | {low_freq_amount} | {low_freq_amount_pct}% |
+| 正常件 | {normal_count} | {normal_count_pct}% | {normal_amount} | {normal_amount_pct}% |
+
+---
+
+## 术语说明
+
+- **缺货件**: 有效库存 = 0 且月均销量 >= 1,有需求但无库存
+- **呆滞件**: 有效库存 > 0 且90天出库数 = 0,有库存但无销售
+- **低频件**: 月均销量 < 1 或出库次数 < 3 或出库间隔 >= 30天
+- **正常件**: 不属于以上三类的配件
+
+---
+
+## 当前季节信息
+
+- **当前季节**: {current_season}
+- **统计日期**: {statistics_date}
+
+---
+
+## 季节性因素参考
+
+| 季节 | 健康度评估调整 | 特别关注 |
+|------|--------------|---------|
+| 春季(3-5月) | 呆滞件中可能包含冬季配件,属正常现象 | 关注冬季配件是否及时清理 |
+| 夏季(6-8月) | 制冷配件缺货风险高,需重点关注 | 空调、冷却系统配件缺货影响大 |
+| 秋季(9-11月) | 夏季配件可能转为低频,需提前处理 | 关注夏季配件库存消化 |
+| 冬季(12-2月) | 电瓶、暖风配件缺货影响大 | 春节前缺货损失更大,需提前备货 |
+
+---
+
+## 分析框架与判断标准
+
+### 健康度评分标准
+| 正常件数量占比 | 健康度等级 | 说明 |
+|---------------|-----------|------|
+| > 70% | 健康 | 库存结构良好,继续保持 |
+| 50% - 70% | 亚健康 | 存在优化空间,需关注问题件 |
+| < 50% | 不健康 | 库存结构严重失衡,需立即改善 |
+
+### 各类型问题件风险评估标准
+| 类型 | 数量占比阈值 | 金额占比阈值 | 风险等级 |
+|------|-------------|-------------|---------|
+| 缺货件 | > 10% | - | 高风险(影响销售) |
+| 呆滞件 | > 15% | > 20% | 高风险(资金占用) |
+| 低频件 | > 25% | > 30% | 中风险(周转效率) |
+
+### 资金释放潜力评估
+| 类型 | 可释放比例 | 释放方式 |
+|------|-----------|---------|
+| 呆滞件 | 60%-80% | 促销清仓、退货供应商、调拨其他门店 |
+| 低频件 | 30%-50% | 降价促销、减少补货、逐步淘汰 |
+
+---
+
+## 分析任务
+
+请严格按照以下步骤进行分析,每一步都要展示推理过程:
+
+### 步骤1:健康度评分
+- 读取正常件数量占比
+- 对照健康度评分标准,确定健康度等级
+- 说明判断依据
+
+### 步骤2:问题件诊断
+对每类问题件进行分析:
+
+**缺货件分析:**
+- 对照风险阈值(数量占比>10%),判断风险等级
+- 分析缺货对业务的影响(销售损失、客户流失)
+- 推断可能原因(补货不及时、需求预测不准、供应链问题)
+
+**呆滞件分析:**
+- 对照风险阈值(数量占比>15%或金额占比>20%),判断风险等级
+- 分析呆滞对资金的影响
+- 推断可能原因(采购决策失误、市场变化、产品更新换代)
+
+**低频件分析:**
+- 对照风险阈值(数量占比>25%或金额占比>30%),判断风险等级
+- 分析低频件对SKU效率的影响
+- 推断可能原因(长尾需求、季节性产品、新品导入)
+
+### 步骤3:资金释放机会评估
+- 计算呆滞件可释放资金 = 呆滞件金额 × 可释放比例(60%-80%)
+- 计算低频件可释放资金 = 低频件金额 × 可释放比例(30%-50%)
+- 给出具体的资金释放行动方案
+
+### 步骤4:改善优先级排序
+- 根据风险等级和影响程度,排序问题类型
+- 给出2-3条优先级最高的改善行动
+
+---
+
+## 输出格式
+
+直接输出JSON对象,**不要**包含 ```json 标记:
+
+{{
+ "analysis_process": {{
+ "health_score_diagnosis": {{
+ "normal_ratio": "正常件数量占比(直接读取:{normal_count_pct}%)",
+ "score": "健康/亚健康/不健康",
+ "reasoning": "判断依据:对照标准xxx,当前正常件占比为xxx%,因此判断为xxx"
+ }},
+ "problem_diagnosis": {{
+ "shortage": {{
+ "risk_level": "高/中/低",
+ "threshold_comparison": "对照阈值>10%,当前{shortage_count_pct}%,结论",
+ "business_impact": "对业务的具体影响分析",
+ "possible_causes": ["可能原因1", "可能原因2"]
+ }},
+ "stagnant": {{
+ "risk_level": "高/中/低",
+ "threshold_comparison": "对照阈值(数量>15%或金额>20%),当前数量{stagnant_count_pct}%/金额{stagnant_amount_pct}%,结论",
+ "capital_impact": "对资金的具体影响分析",
+ "possible_causes": ["可能原因1", "可能原因2"]
+ }},
+ "low_freq": {{
+ "risk_level": "高/中/低",
+ "threshold_comparison": "对照阈值(数量>25%或金额>30%),当前数量{low_freq_count_pct}%/金额{low_freq_amount_pct}%,结论",
+ "efficiency_impact": "对SKU效率的具体影响分析",
+ "possible_causes": ["可能原因1", "可能原因2"]
+ }}
+ }},
+ "capital_release_calculation": {{
+ "stagnant_calculation": "呆滞件可释放资金 = {stagnant_amount} × 70% = xxx元(取中间值70%)",
+ "low_freq_calculation": "低频件可释放资金 = {low_freq_amount} × 40% = xxx元(取中间值40%)",
+ "total_releasable": "总可释放资金 = xxx元"
+ }},
+ "seasonal_analysis": {{
+ "current_season": "当前季节",
+ "seasonal_stagnant_items": "呆滞件中是否包含季节性配件(如冬季的空调配件)",
+ "seasonal_shortage_risk": "当季高需求配件的缺货风险评估",
+ "upcoming_season_alert": "下一季节需要关注的配件类型"
+ }}
+ }},
+ "conclusion": {{
+ "health_score": {{
+ "score": "健康/亚健康/不健康",
+ "normal_ratio_evaluation": "正常件占比评估结论(基于分析得出)"
+ }},
+ "problem_diagnosis": {{
+ "stagnant_analysis": "呆滞件问题分析及原因(基于分析得出)",
+ "shortage_analysis": "缺货件问题分析及影响(基于分析得出)",
+ "low_freq_analysis": "低频件问题分析及建议(基于分析得出)"
+ }},
+ "capital_release": {{
+ "stagnant_releasable": "呆滞件可释放资金估算(基于计算得出)",
+ "low_freq_releasable": "低频件可释放资金估算(基于计算得出)",
+ "action_plan": "资金释放行动方案(具体步骤)"
+ }},
+ "priority_actions": [
+ {{
+ "priority": 1,
+ "action": "最优先处理事项",
+ "reason": "优先原因",
+ "expected_effect": "预期效果"
+ }},
+ {{
+ "priority": 2,
+ "action": "次优先处理事项",
+ "reason": "优先原因",
+ "expected_effect": "预期效果"
+ }}
+ ]
+ }}
+}}
+
+---
+
+## 重要约束
+
+1. **输出必须是合法的JSON对象**
+2. **分析必须基于提供的数据,不要编造数据**
+3. **每个结论都必须有明确的推理依据和数据支撑**
+4. **资金释放估算应基于实际数据和给定的释放比例范围**
+5. **score 只能是"健康"、"亚健康"、"不健康"三个值之一**
+6. **priority_actions 数组至少包含2条,最多3条**
+7. **如果数据全为零,请在分析中说明无有效数据,并给出相应建议**
+8. **所有金额计算结果保留两位小数**
diff --git a/prompts/report_inventory_overview.md b/prompts/report_inventory_overview.md
new file mode 100644
index 0000000..211b000
--- /dev/null
+++ b/prompts/report_inventory_overview.md
@@ -0,0 +1,182 @@
+# 库存概览分析提示词
+
+你是一位资深汽车配件库存管理专家,拥有20年以上的汽车后市场库存管理经验。请基于以下库存概览统计数据,进行专业的库存分析。
+
+---
+
+## 统计数据
+
+| 指标 | 数值 |
+|------|------|
+| 配件总种类数 | {part_count} |
+| 有效库存总数量 | {total_valid_storage_cnt} |
+| 有效库存总金额(资金占用) | {total_valid_storage_amount} 元 |
+| 月均销量总数量 | {total_avg_sales_cnt} |
+| 整体库销比 | {overall_ratio} |
+
+### 库存三项构成明细
+
+| 构成项 | 数量 | 金额(元) | 数量占比 | 金额占比 |
+|--------|------|------------|----------|----------|
+| 在库未锁 | {total_in_stock_unlocked_cnt} | {total_in_stock_unlocked_amount} | - | - |
+| 在途 | {total_on_the_way_cnt} | {total_on_the_way_amount} | - | - |
+| 计划数 | {total_has_plan_cnt} | {total_has_plan_amount} | - | - |
+
+---
+
+## 术语说明
+
+- **有效库存**: 在库未锁 + 在途 + 计划数
+- **月均销量**: (90天出库数 + 未关单已锁 + 未关单出库 + 订件) / 3
+- **库销比**: 有效库存总数量 / 月均销量总数量,反映库存周转效率
+
+---
+
+## 当前季节信息
+
+- **当前季节**: {current_season}
+- **统计日期**: {statistics_date}
+
+---
+
+## 季节性因素参考
+
+| 季节 | 需求特征 | 库存策略建议 |
+|------|---------|-------------|
+| 春季(3-5月) | 需求回暖,维修保养高峰前期 | 适当增加库存,为旺季做准备 |
+| 夏季(6-8月) | 空调、冷却系统配件需求旺盛 | 重点备货制冷相关配件,库销比可适当放宽至2.5 |
+| 秋季(9-11月) | 需求平稳,换季保养需求 | 保持正常库存水平,关注轮胎、刹车片等 |
+| 冬季(12-2月) | 电瓶、暖风系统需求增加,春节前备货期 | 提前备货,库销比可适当放宽至2.5-3.0 |
+
+---
+
+## 分析框架与判断标准
+
+### 库销比判断标准
+| 库销比范围 | 判断等级 | 含义 |
+|-----------|---------|------|
+| < 1.0 | 库存不足 | 可能面临缺货风险,需要加快补货 |
+| 1.0 - 2.0 | 合理 | 库存水平健康,周转效率良好 |
+| 2.0 - 3.0 | 偏高 | 库存积压风险,需关注周转 |
+| > 3.0 | 严重积压 | 资金占用过高,需立即优化 |
+| = 999 | 无销量 | 月均销量为零,需特别关注 |
+
+### 库存结构健康标准
+| 构成项 | 健康占比范围 | 风险提示 |
+|--------|-------------|---------|
+| 在库未锁 | 60%-80% | 过高说明周转慢,过低说明库存不足 |
+| 在途 | 10%-25% | 过高说明到货延迟风险,过低说明补货不及时 |
+| 计划数 | 5%-15% | 过高说明计划执行滞后 |
+
+### 资金占用风险等级
+| 条件 | 风险等级 |
+|------|---------|
+| 库销比 > 3.0 或 在库未锁占比 > 85% | high |
+| 库销比 2.0-3.0 或 在库未锁占比 80%-85% | medium |
+| 库销比 < 2.0 且 结构合理 | low |
+
+---
+
+## 分析任务
+
+请严格按照以下步骤进行分析,每一步都要展示推理过程:
+
+### 步骤1:计算关键指标
+首先计算以下指标(请在分析中展示计算过程):
+- 各构成项的数量占比 = 构成项数量 / 有效库存总数量 × 100%
+- 各构成项的金额占比 = 构成项金额 / 有效库存总金额 × 100%
+- 单件平均成本 = 有效库存总金额 / 有效库存总数量
+
+### 步骤2:库销比诊断
+- 对照判断标准,确定当前库销比所处等级
+- 说明该等级的业务含义
+- 与行业经验值(1.5-2.5)进行对比
+
+### 步骤3:库存结构分析
+- 对照健康标准,评估各构成项占比是否合理
+- 识别偏离健康范围的构成项
+- 分析偏离的可能原因
+
+### 步骤4:风险评估
+- 根据风险等级判断条件,确定当前风险等级
+- 列出具体的风险点
+
+### 步骤5:季节性考量
+- 结合当前季节特征,评估库存水平是否适合当前季节
+- 考虑即将到来的季节变化,是否需要提前调整
+
+### 步骤6:形成建议
+- 基于以上分析,提出2-3条具体可操作的改善建议
+- 每条建议需说明预期效果
+- 建议需考虑季节性因素
+
+---
+
+## 输出格式
+
+直接输出JSON对象,**不要**包含 ```json 标记:
+
+{{
+ "analysis_process": {{
+ "calculated_metrics": {{
+ "in_stock_ratio": "在库未锁数量占比(计算过程:xxx / xxx = xx%)",
+ "on_way_ratio": "在途数量占比(计算过程)",
+ "plan_ratio": "计划数占比(计算过程)",
+ "avg_cost": "单件平均成本(计算过程)"
+ }},
+ "ratio_diagnosis": {{
+ "current_value": "当前库销比数值",
+ "level": "不足/合理/偏高/严重积压/无销量",
+ "reasoning": "判断依据:对照标准xxx,当前值xxx,因此判断为xxx",
+ "benchmark_comparison": "与行业经验值1.5-2.5对比的结论"
+ }},
+ "structure_analysis": {{
+ "in_stock_evaluation": "在库未锁占比评估(对照标准60%-80%,当前xx%,结论)",
+ "on_way_evaluation": "在途占比评估(对照标准10%-25%,当前xx%,结论)",
+ "plan_evaluation": "计划数占比评估(对照标准5%-15%,当前xx%,结论)",
+ "abnormal_items": ["偏离健康范围的项目及原因分析"]
+ }},
+ "seasonal_analysis": {{
+ "current_season": "当前季节",
+ "season_demand_feature": "当前季节需求特征",
+ "inventory_fitness": "当前库存水平是否适合本季节(结合季节性因素评估)",
+ "upcoming_season_preparation": "对即将到来季节的准备建议"
+ }}
+ }},
+ "conclusion": {{
+ "capital_assessment": {{
+ "total_evaluation": "总资金占用评估(基于以上分析得出的一句话结论)",
+ "structure_ratio": "各构成部分的资金比例分析结论",
+ "risk_level": "high/medium/low(基于风险等级判断条件得出)"
+ }},
+ "ratio_diagnosis": {{
+ "level": "不足/合理/偏高/严重积压",
+ "analysis": "库销比分析结论",
+ "benchmark": "行业参考值对比结论"
+ }},
+ "recommendations": [
+ {{
+ "action": "具体建议1",
+ "reason": "建议依据",
+ "expected_effect": "预期效果"
+ }},
+ {{
+ "action": "具体建议2",
+ "reason": "建议依据",
+ "expected_effect": "预期效果"
+ }}
+ ]
+ }}
+}}
+
+---
+
+## 重要约束
+
+1. **输出必须是合法的JSON对象**
+2. **分析必须基于提供的数据,不要编造数据**
+3. **每个结论都必须有明确的推理依据**
+4. **建议必须具体可操作,避免空泛的表述**
+5. **risk_level 只能是 high、medium、low 三个值之一**
+6. **如果数据全为零,请在分析中说明无有效数据,并给出相应建议**
+7. **所有百分比计算结果保留两位小数**
diff --git a/prompts/report_replenishment_summary.md b/prompts/report_replenishment_summary.md
new file mode 100644
index 0000000..aeb5a45
--- /dev/null
+++ b/prompts/report_replenishment_summary.md
@@ -0,0 +1,173 @@
+# 补货建议分析提示词
+
+你是一位汽车配件采购策略顾问,擅长制定科学的补货计划和资金分配方案。请基于以下补货建议统计数据,进行专业的补货策略分析。
+
+---
+
+## 统计数据
+
+| 指标 | 数值 |
+|------|------|
+| 补货配件总种类数 | {total_count} |
+| 补货总金额 | {total_amount} 元 |
+
+### 各优先级统计
+
+| 优先级 | 配件种类数 | 金额(元) |
+|--------|-----------|------------|
+| 急需补货(优先级1) | {urgent_count} | {urgent_amount} |
+| 建议补货(优先级2) | {suggested_count} | {suggested_amount} |
+| 可选补货(优先级3) | {optional_count} | {optional_amount} |
+
+---
+
+## 术语说明
+
+- **急需补货(优先级1)**: 库销比 < 0.5 且月均销量 >= 1,库存严重不足,面临断货风险
+- **建议补货(优先级2)**: 库销比 0.5-1.0 且月均销量 >= 1,库存偏低,建议及时补充
+- **可选补货(优先级3)**: 库销比 1.0-目标值 且月均销量 >= 1,库存尚可,可灵活安排
+
+---
+
+## 当前季节信息
+
+- **当前季节**: {current_season}
+- **统计日期**: {statistics_date}
+
+---
+
+## 季节性因素参考
+
+| 季节 | 补货策略调整 | 重点补货品类 |
+|------|-------------|-------------|
+| 春季(3-5月) | 为夏季旺季提前备货 | 空调、冷却系统配件 |
+| 夏季(6-8月) | 制冷配件紧急补货优先级更高 | 空调压缩机、冷凝器、制冷剂 |
+| 秋季(9-11月) | 为冬季备货,减少夏季配件补货 | 电瓶、暖风系统、防冻液 |
+| 冬季(12-2月) | 春节前加快补货节奏 | 电瓶、启动机、暖风配件 |
+
+---
+
+## 分析框架与判断标准
+
+### 紧迫度评估标准
+| 急需补货占比 | 紧迫度等级 | 风险等级 | 建议 |
+|-------------|-----------|---------|------|
+| > 30% | 非常紧迫 | high | 立即启动紧急补货流程 |
+| 15% - 30% | 较紧迫 | medium | 优先处理急需补货 |
+| < 15% | 一般 | low | 按正常流程处理 |
+
+### 资金分配优先级原则
+| 优先级 | 建议预算占比 | 执行时间 |
+|--------|-------------|---------|
+| 急需补货 | 50%-70% | 1-3天内 |
+| 建议补货 | 20%-35% | 1-2周内 |
+| 可选补货 | 10%-15% | 2-4周内 |
+
+### 风险预警阈值
+| 风险类型 | 触发条件 | 预警等级 |
+|---------|---------|---------|
+| 资金压力 | 急需补货金额占比 > 60% | 高 |
+| 过度补货 | 可选补货金额占比 > 40% | 中 |
+
+---
+
+## 分析任务
+
+请严格按照以下步骤进行分析,展示推理过程:
+
+### 步骤1:计算关键指标
+- 各优先级数量占比 = 数量 / 总数量 × 100%
+- 各优先级金额占比 = 金额 / 总金额 × 100%
+
+### 步骤2:紧迫度评估
+- 对照标准确定紧迫度等级和风险等级
+- 判断是否需要立即行动
+
+### 步骤3:资金分配建议
+- 对照建议预算占比,判断当前分布是否合理
+- 给出具体资金分配建议
+
+### 步骤4:执行节奏规划
+- 规划各类补货的执行时间
+
+### 步骤5:风险识别
+- 对照风险预警阈值,识别潜在风险
+
+---
+
+## 输出格式
+
+直接输出JSON对象,**不要**包含 ```json 标记:
+
+{{
+ "analysis_process": {{
+ "calculated_metrics": {{
+ "urgent_count_ratio": "急需补货数量占比(计算:xxx / xxx = xx%)",
+ "urgent_amount_ratio": "急需补货金额占比(计算)",
+ "suggested_count_ratio": "建议补货数量占比(计算)",
+ "suggested_amount_ratio": "建议补货金额占比(计算)",
+ "optional_count_ratio": "可选补货数量占比(计算)",
+ "optional_amount_ratio": "可选补货金额占比(计算)"
+ }},
+ "urgency_diagnosis": {{
+ "urgent_ratio": "急需补货数量占比",
+ "level": "非常紧迫/较紧迫/一般",
+ "reasoning": "判断依据:对照标准xxx,当前占比xxx%,因此判断为xxx"
+ }},
+ "budget_analysis": {{
+ "current_distribution": "当前各优先级金额分布情况",
+ "comparison_with_standard": "与建议预算占比对比分析",
+ "adjustment_needed": "是否需要调整及原因"
+ }},
+ "risk_identification": {{
+ "capital_pressure_check": "资金压力检查(急需占比是否>60%)",
+ "over_replenishment_check": "过度补货检查(可选占比是否>40%)",
+ "identified_risks": ["识别到的风险1", "识别到的风险2"]
+ }},
+ "seasonal_analysis": {{
+ "current_season": "当前季节",
+ "seasonal_priority_items": "当季重点补货品类是否在急需列表中",
+ "timeline_adjustment": "是否需要根据季节调整补货时间(如春节前加快)",
+ "next_season_preparation": "为下一季节需要提前准备的配件"
+ }}
+ }},
+ "conclusion": {{
+ "urgency_assessment": {{
+ "urgent_ratio_evaluation": "急需补货占比评估结论",
+ "risk_level": "high/medium/low",
+ "immediate_action_needed": true或false
+ }},
+ "budget_allocation": {{
+ "recommended_order": "建议资金分配顺序(基于分析得出)",
+ "urgent_budget": "急需补货建议预算(具体金额或比例)",
+ "suggested_budget": "建议补货建议预算",
+ "optional_budget": "可选补货建议预算"
+ }},
+ "execution_plan": {{
+ "urgent_timeline": "急需补货执行时间(1-3天内)",
+ "suggested_timeline": "建议补货执行时间(1-2周内)",
+ "optional_timeline": "可选补货执行时间(2-4周内)"
+ }},
+ "risk_warnings": [
+ {{
+ "risk_type": "风险类型",
+ "description": "风险描述",
+ "mitigation": "应对建议"
+ }}
+ ]
+ }}
+}}
+
+---
+
+## 重要约束
+
+1. **输出必须是合法的JSON对象**
+2. **分析必须基于提供的数据,不要编造数据**
+3. **每个结论都必须有明确的推理依据和数据支撑**
+4. **建议必须具体可操作,包含时间和金额参考**
+5. **risk_level 只能是 high、medium、low 三个值之一**
+6. **immediate_action_needed 必须是布尔值 true 或 false**
+7. **risk_warnings 数组至少包含1条,最多3条**
+8. **如果数据全为零,请在分析中说明无补货建议数据**
+9. **所有百分比计算结果保留两位小数**
diff --git a/prompts/report_sales_analysis.md b/prompts/report_sales_analysis.md
new file mode 100644
index 0000000..0718b84
--- /dev/null
+++ b/prompts/report_sales_analysis.md
@@ -0,0 +1,178 @@
+# 销量分析提示词
+
+你是一位汽车配件销售数据分析师,擅长从销量数据中洞察需求趋势和业务机会。请基于以下销量统计数据,进行专业的销量分析。
+
+---
+
+## 统计数据
+
+| 指标 | 数值 |
+|------|------|
+| 月均销量总数量 | {total_avg_sales_cnt} |
+| 月均销量总金额 | {total_avg_sales_amount} 元 |
+| 有销量配件数 | {has_sales_part_count} |
+| 无销量配件数 | {no_sales_part_count} |
+
+### 销量构成明细
+
+| 构成项 | 数量 | 说明 |
+|--------|------|------|
+| 90天出库数 | {total_out_stock_cnt} | 近90天实际出库,反映正常销售 |
+| 未关单已锁 | {total_storage_locked_cnt} | 已锁定库存但订单未关闭,反映待处理订单 |
+| 未关单出库 | {total_out_stock_ongoing_cnt} | 已出库但订单未关闭,反映在途交付 |
+| 订件 | {total_buy_cnt} | 客户预订的配件数量,反映预订需求 |
+
+---
+
+## 术语说明
+
+- **月均销量**: (90天出库数 + 未关单已锁 + 未关单出库 + 订件) / 3
+- **有销量配件**: 月均销量 > 0 的配件
+- **无销量配件**: 月均销量 = 0 的配件
+- **SKU活跃率**: 有销量配件数 / 总配件数 × 100%
+
+---
+
+## 当前季节信息
+
+- **当前季节**: {current_season}
+- **统计日期**: {statistics_date}
+
+---
+
+## 季节性因素参考
+
+| 季节 | 销量特征 | 关注重点 |
+|------|---------|---------|
+| 春季(3-5月) | 销量逐步回升,保养类配件需求增加 | 关注机油、滤芯等保养件销量变化 |
+| 夏季(6-8月) | 空调、冷却系统配件销量高峰 | 制冷配件销量应明显上升,否则需关注 |
+| 秋季(9-11月) | 销量平稳,换季保养需求 | 轮胎、刹车片等安全件需求增加 |
+| 冬季(12-2月) | 电瓶、暖风配件需求增加,春节前订单高峰 | 订件占比可能上升,属正常现象 |
+
+---
+
+## 分析框架与判断标准
+
+### 销量构成健康标准
+| 构成项 | 健康占比范围 | 异常信号 |
+|--------|-------------|---------|
+| 90天出库数 | > 70% | 占比过低说明正常销售不足,可能存在订单积压 |
+| 未关单已锁 | < 15% | 占比过高说明订单处理效率低,需关注 |
+| 未关单出库 | < 10% | 占比过高说明交付周期长,客户体验受影响 |
+| 订件 | 5%-15% | 过高说明预订需求旺盛但库存不足,过低说明预订渠道不畅 |
+
+### SKU活跃度判断标准
+| 活跃率范围 | 判断等级 | 建议 |
+|-----------|---------|------|
+| > 80% | 优秀 | SKU管理良好,保持现状 |
+| 70%-80% | 良好 | 可适当优化无销量SKU |
+| 50%-70% | 一般 | 需要重点关注SKU精简 |
+| < 50% | 较差 | SKU管理存在严重问题,需立即优化 |
+
+### 需求趋势判断依据
+| 信号 | 趋势判断 |
+|------|---------|
+| 订件占比上升 + 未关单占比上升 | 上升(需求增长但供应跟不上) |
+| 90天出库占比稳定 + 各项占比均衡 | 稳定(供需平衡) |
+| 90天出库占比下降 + 订件占比下降 | 下降(需求萎缩) |
+
+---
+
+## 分析任务
+
+请严格按照以下步骤进行分析,每一步都要展示推理过程:
+
+### 步骤1:计算关键指标
+首先计算以下指标(请在分析中展示计算过程):
+- 各构成项占比 = 构成项数量 / (90天出库数 + 未关单已锁 + 未关单出库 + 订件) × 100%
+- SKU活跃率 = 有销量配件数 / (有销量配件数 + 无销量配件数) × 100%
+- 单件平均销售金额 = 月均销量总金额 / 月均销量总数量
+
+### 步骤2:销量构成分析
+- 对照健康标准,评估各构成项占比是否合理
+- 识别主要销量来源
+- 分析未关单(已锁+出库)对整体销量的影响
+
+### 步骤3:SKU活跃度评估
+- 对照活跃度标准,确定当前活跃率等级
+- 分析无销量配件占比的业务影响
+- 提出SKU优化方向
+
+### 步骤4:季节性分析
+- 结合当前季节特征,评估销量表现是否符合季节预期
+- 分析季节性配件的销量是否正常
+
+### 步骤5:需求趋势判断
+- 根据各构成项的占比关系,判断需求趋势
+- 结合季节因素,说明判断依据
+- 给出短期需求预测(考虑季节变化)
+
+---
+
+## 输出格式
+
+直接输出JSON对象,**不要**包含 ```json 标记:
+
+{{
+ "analysis_process": {{
+ "calculated_metrics": {{
+ "out_stock_ratio": "90天出库占比(计算过程:xxx / xxx = xx%)",
+ "locked_ratio": "未关单已锁占比(计算过程)",
+ "ongoing_ratio": "未关单出库占比(计算过程)",
+ "buy_ratio": "订件占比(计算过程)",
+ "sku_active_rate": "SKU活跃率(计算过程:xxx / xxx = xx%)",
+ "avg_sales_price": "单件平均销售金额(计算过程)"
+ }},
+ "composition_diagnosis": {{
+ "out_stock_evaluation": "90天出库占比评估(对照标准>70%,当前xx%,结论)",
+ "locked_evaluation": "未关单已锁占比评估(对照标准<15%,当前xx%,结论)",
+ "ongoing_evaluation": "未关单出库占比评估(对照标准<10%,当前xx%,结论)",
+ "buy_evaluation": "订件占比评估(对照标准5%-15%,当前xx%,结论)",
+ "abnormal_items": ["偏离健康范围的项目及原因分析"]
+ }},
+ "activity_diagnosis": {{
+ "current_rate": "当前SKU活跃率",
+ "level": "优秀/良好/一般/较差",
+ "reasoning": "判断依据:对照标准xxx,当前值xxx,因此判断为xxx"
+ }},
+ "trend_diagnosis": {{
+ "signals": ["观察到的趋势信号1", "观察到的趋势信号2"],
+ "reasoning": "基于以上信号,判断需求趋势为xxx,因为xxx"
+ }},
+ "seasonal_analysis": {{
+ "current_season": "当前季节",
+ "expected_performance": "本季节预期销量特征",
+ "actual_vs_expected": "实际表现与季节预期对比",
+ "seasonal_items_status": "季节性配件销量状态评估"
+ }}
+ }},
+ "conclusion": {{
+ "composition_analysis": {{
+ "main_driver": "主要销量来源分析(基于占比计算得出)",
+ "pending_orders_impact": "未关单对销量的影响(基于占比计算得出)",
+ "booking_trend": "订件趋势分析(基于占比计算得出)"
+ }},
+ "activity_assessment": {{
+ "active_ratio": "活跃SKU占比评估结论",
+ "optimization_suggestion": "SKU优化建议(基于活跃度等级给出)"
+ }},
+ "demand_trend": {{
+ "direction": "上升/稳定/下降",
+ "evidence": "判断依据(列出具体数据支撑)",
+ "seasonal_factor": "季节因素对趋势的影响",
+ "forecast": "短期需求预测(考虑季节变化)"
+ }}
+ }}
+}}
+
+---
+
+## 重要约束
+
+1. **输出必须是合法的JSON对象**
+2. **分析必须基于提供的数据,不要编造数据**
+3. **每个结论都必须有明确的推理依据和数据支撑**
+4. **建议必须具体可操作,避免空泛的表述**
+5. **direction 只能是"上升"、"稳定"、"下降"三个值之一**
+6. **如果数据全为零,请在分析中说明无有效数据,并给出相应建议**
+7. **所有百分比计算结果保留两位小数**
diff --git a/prompts/sql_agent.md b/prompts/sql_agent.md
index 8b06b04..f9d0cf6 100644
--- a/prompts/sql_agent.md
+++ b/prompts/sql_agent.md
@@ -60,7 +60,7 @@ CREATE TABLE part_ratio (
| 指标 | 公式 | 说明 |
|------|------|------|
-| 有效库存 | `in_stock_unlocked_cnt + on_the_way_cnt + has_plan_cnt + transfer_cnt + gen_transfer_cnt` | 可用库存总量 |
+| 有效库存 | `in_stock_unlocked_cnt + on_the_way_cnt + has_plan_cnt` | 可用库存总量 |
| 月均销量 | `(out_stock_cnt + storage_locked_cnt + out_stock_ongoing_cnt + buy_cnt) / 3` | 基于90天数据计算 |
| 库销比 | `有效库存 / 月均销量` | 当月均销量 > 0 时有效 |
diff --git a/sql/migrate_analysis_report.sql b/sql/migrate_analysis_report.sql
index 0a90acd..acd232d 100644
--- a/sql/migrate_analysis_report.sql
+++ b/sql/migrate_analysis_report.sql
@@ -1,9 +1,9 @@
-- ============================================================================
-- AI 补货建议分析报告表
-- ============================================================================
--- 版本: 2.0.0
--- 更新日期: 2026-02-05
--- 变更说明: 重构报告模块,聚焦补货决策支持(区别于传统库销分析)
+-- 版本: 3.0.0
+-- 更新日期: 2026-02-10
+-- 变更说明: 重构为四大数据驱动板块(库存概览/销量分析/健康度/补货建议)
-- ============================================================================
DROP TABLE IF EXISTS ai_analysis_report;
@@ -15,33 +15,23 @@ CREATE TABLE ai_analysis_report (
dealer_grouping_name VARCHAR(128) COMMENT '商家组合名称',
brand_grouping_id BIGINT COMMENT '品牌组合ID',
report_type VARCHAR(32) DEFAULT 'replenishment' COMMENT '报告类型',
-
- -- 报告各模块 (JSON 结构化存储) - 宏观决策分析
- -- 注:字段名保持兼容,实际存储内容已更新为新模块
- replenishment_insights JSON COMMENT '整体态势研判(规模评估/结构分析/时机判断) - 原overall_assessment',
- urgency_assessment JSON COMMENT '风险预警与应对(供应/资金/市场/执行风险) - 原risk_alerts',
- strategy_recommendations JSON COMMENT '采购策略建议(优先级/批量机会/分批/供应商协调) - 原procurement_strategy',
- execution_guide JSON COMMENT '已废弃,置为NULL',
- expected_outcomes JSON COMMENT '效果预期与建议(库存健康/资金效率/后续行动) - 原expected_impact',
-
- -- 统计信息
- total_suggest_cnt INT DEFAULT 0 COMMENT '总建议数量',
- total_suggest_amount DECIMAL(14,2) DEFAULT 0 COMMENT '总建议金额',
- shortage_risk_cnt INT DEFAULT 0 COMMENT '缺货风险配件数',
- excess_risk_cnt INT DEFAULT 0 COMMENT '过剩风险配件数',
- stagnant_cnt INT DEFAULT 0 COMMENT '呆滞件数量',
- low_freq_cnt INT DEFAULT 0 COMMENT '低频件数量',
-
+
+ -- 四大板块 (JSON 结构化存储,每个字段包含 stats + llm_analysis)
+ inventory_overview JSON COMMENT '库存总体概览(统计数据+LLM分析)',
+ sales_analysis JSON COMMENT '销量分析(统计数据+LLM分析)',
+ inventory_health JSON COMMENT '库存构成健康度(统计数据+图表数据+LLM分析)',
+ replenishment_summary JSON COMMENT '补货建议生成情况(统计数据+LLM分析)',
+
-- LLM 元数据
llm_provider VARCHAR(32) COMMENT 'LLM提供商',
llm_model VARCHAR(64) COMMENT 'LLM模型名称',
llm_tokens INT DEFAULT 0 COMMENT 'LLM Token消耗',
execution_time_ms INT DEFAULT 0 COMMENT '执行耗时(毫秒)',
-
+
statistics_date VARCHAR(16) COMMENT '统计日期',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-
+
INDEX idx_task_no (task_no),
INDEX idx_group_date (group_id, statistics_date),
INDEX idx_dealer_grouping (dealer_grouping_id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI补货建议分析报告表-结构化补货决策支持报告';
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI补货建议分析报告表-重构版';
diff --git a/src/fw_pms_ai/agent/analysis_report_node.py b/src/fw_pms_ai/agent/analysis_report_node.py
index b3c0945..f45176c 100644
--- a/src/fw_pms_ai/agent/analysis_report_node.py
+++ b/src/fw_pms_ai/agent/analysis_report_node.py
@@ -1,412 +1,945 @@
"""
分析报告生成节点
-在补货建议工作流的最后一个节点执行,生成结构化分析报告
+在补货建议工作流的最后一个节点执行,生成结构化分析报告。
+包含四大板块的统计计算函数:库存概览、销量分析、库存健康度、补货建议。
"""
import logging
-import time
-import json
-import os
-from typing import Dict, Any
-from decimal import Decimal
-from datetime import datetime
+from decimal import Decimal, ROUND_HALF_UP
-from langchain_core.messages import HumanMessage
+logger = logging.getLogger(__name__)
-from ..llm import get_llm_client
-from ..models import AnalysisReport
-from ..services.result_writer import ResultWriter
-logger = logging.getLogger(__name__)
+def _to_decimal(value) -> Decimal:
+ """安全转换为 Decimal"""
+ if value is None:
+ return Decimal("0")
+ return Decimal(str(value))
-def _load_prompt(filename: str) -> str:
- """从prompts目录加载提示词文件"""
- prompts_dir = os.path.join(
- os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))),
- "prompts"
+def calculate_inventory_overview(part_ratios: list[dict]) -> dict:
+ """
+ 计算库存总体概览统计数据
+
+ 有效库存 = in_stock_unlocked_cnt + on_the_way_cnt + has_plan_cnt
+ 资金占用 = in_stock_unlocked_cnt + on_the_way_cnt(仅计算实际占用资金的库存)
+
+ Args:
+ part_ratios: PartRatio 字典列表
+
+ Returns:
+ 库存概览统计字典
+ """
+ total_in_stock_unlocked_cnt = Decimal("0")
+ total_in_stock_unlocked_amount = Decimal("0")
+ total_on_the_way_cnt = Decimal("0")
+ total_on_the_way_amount = Decimal("0")
+ total_has_plan_cnt = Decimal("0")
+ total_has_plan_amount = Decimal("0")
+ total_avg_sales_cnt = Decimal("0")
+ # 资金占用合计 = (在库未锁 + 在途) * 成本价
+ total_capital_occupation = Decimal("0")
+
+ for p in part_ratios:
+ cost_price = _to_decimal(p.get("cost_price", 0))
+
+ in_stock = _to_decimal(p.get("in_stock_unlocked_cnt", 0))
+ on_way = _to_decimal(p.get("on_the_way_cnt", 0))
+ has_plan = _to_decimal(p.get("has_plan_cnt", 0))
+
+ total_in_stock_unlocked_cnt += in_stock
+ total_in_stock_unlocked_amount += in_stock * cost_price
+ total_on_the_way_cnt += on_way
+ total_on_the_way_amount += on_way * cost_price
+ total_has_plan_cnt += has_plan
+ total_has_plan_amount += has_plan * cost_price
+
+ # 资金占用 = 在库未锁 + 在途
+ total_capital_occupation += (in_stock + on_way) * cost_price
+
+ # 月均销量
+ out_stock = _to_decimal(p.get("out_stock_cnt", 0))
+ locked = _to_decimal(p.get("storage_locked_cnt", 0))
+ ongoing = _to_decimal(p.get("out_stock_ongoing_cnt", 0))
+ buy = _to_decimal(p.get("buy_cnt", 0))
+ avg_sales = (out_stock + locked + ongoing + buy) / Decimal("3")
+ total_avg_sales_cnt += avg_sales
+
+ total_valid_storage_cnt = (
+ total_in_stock_unlocked_cnt
+ + total_on_the_way_cnt
+ + total_has_plan_cnt
)
- filepath = os.path.join(prompts_dir, filename)
-
- if not os.path.exists(filepath):
- raise FileNotFoundError(f"Prompt文件未找到: {filepath}")
-
- with open(filepath, "r", encoding="utf-8") as f:
- return f.read()
+ total_valid_storage_amount = (
+ total_in_stock_unlocked_amount
+ + total_on_the_way_amount
+ + total_has_plan_amount
+ )
+
+ # 库销比:月均销量为零时标记为特殊值
+ if total_avg_sales_cnt > 0:
+ overall_ratio = total_valid_storage_cnt / total_avg_sales_cnt
+ else:
+ overall_ratio = Decimal("999")
+ return {
+ "total_valid_storage_cnt": total_valid_storage_cnt,
+ "total_valid_storage_amount": total_valid_storage_amount,
+ "total_capital_occupation": total_capital_occupation,
+ "total_in_stock_unlocked_cnt": total_in_stock_unlocked_cnt,
+ "total_in_stock_unlocked_amount": total_in_stock_unlocked_amount,
+ "total_on_the_way_cnt": total_on_the_way_cnt,
+ "total_on_the_way_amount": total_on_the_way_amount,
+ "total_has_plan_cnt": total_has_plan_cnt,
+ "total_has_plan_amount": total_has_plan_amount,
+ "total_avg_sales_cnt": total_avg_sales_cnt,
+ "overall_ratio": overall_ratio,
+ "part_count": len(part_ratios),
+ }
-def _calculate_suggestion_stats(part_results: list) -> dict:
+
+def calculate_sales_analysis(part_ratios: list[dict]) -> dict:
"""
- 基于完整数据计算补货建议统计
-
- 统计维度:
- 1. 总体统计:总数量、总金额
- 2. 优先级分布:高/中/低优先级配件数及金额
- 3. 价格区间分布:低价/中价/高价配件分布
- 4. 周转频次分布:高频/中频/低频配件分布
- 5. 补货规模分布:大额/中额/小额补货配件分布
+ 计算销量分析统计数据
+
+ 月均销量 = (out_stock_cnt + storage_locked_cnt + out_stock_ongoing_cnt + buy_cnt) / 3
+
+ Args:
+ part_ratios: PartRatio 字典列表
+
+ Returns:
+ 销量分析统计字典
"""
- stats = {
- # 总体统计
- "total_parts_cnt": 0,
- "total_suggest_cnt": 0,
- "total_suggest_amount": Decimal("0"),
-
- # 优先级分布: 1=高, 2=中, 3=低
- "priority_high_cnt": 0,
- "priority_high_amount": Decimal("0"),
- "priority_medium_cnt": 0,
- "priority_medium_amount": Decimal("0"),
- "priority_low_cnt": 0,
- "priority_low_amount": Decimal("0"),
-
- # 价格区间分布 (成本价)
- "price_low_cnt": 0,
- "price_low_amount": Decimal("0"),
- "price_medium_cnt": 0,
- "price_medium_amount": Decimal("0"),
- "price_high_cnt": 0,
- "price_high_amount": Decimal("0"),
-
- # 周转频次分布 (月均销量)
- "turnover_high_cnt": 0,
- "turnover_high_amount": Decimal("0"),
- "turnover_medium_cnt": 0,
- "turnover_medium_amount": Decimal("0"),
- "turnover_low_cnt": 0,
- "turnover_low_amount": Decimal("0"),
-
- # 补货金额分布
- "replenish_large_cnt": 0,
- "replenish_large_amount": Decimal("0"),
- "replenish_medium_cnt": 0,
- "replenish_medium_amount": Decimal("0"),
- "replenish_small_cnt": 0,
- "replenish_small_amount": Decimal("0"),
+ total_out_stock_cnt = Decimal("0")
+ total_storage_locked_cnt = Decimal("0")
+ total_out_stock_ongoing_cnt = Decimal("0")
+ total_buy_cnt = Decimal("0")
+ total_avg_sales_amount = Decimal("0")
+ has_sales_part_count = 0
+ no_sales_part_count = 0
+
+ for p in part_ratios:
+ cost_price = _to_decimal(p.get("cost_price", 0))
+
+ out_stock = _to_decimal(p.get("out_stock_cnt", 0))
+ locked = _to_decimal(p.get("storage_locked_cnt", 0))
+ ongoing = _to_decimal(p.get("out_stock_ongoing_cnt", 0))
+ buy = _to_decimal(p.get("buy_cnt", 0))
+
+ total_out_stock_cnt += out_stock
+ total_storage_locked_cnt += locked
+ total_out_stock_ongoing_cnt += ongoing
+ total_buy_cnt += buy
+
+ avg_sales = (out_stock + locked + ongoing + buy) / Decimal("3")
+ total_avg_sales_amount += avg_sales * cost_price
+
+ if avg_sales > 0:
+ has_sales_part_count += 1
+ else:
+ no_sales_part_count += 1
+
+ total_avg_sales_cnt = (
+ total_out_stock_cnt + total_storage_locked_cnt + total_out_stock_ongoing_cnt + total_buy_cnt
+ ) / Decimal("3")
+
+ return {
+ "total_avg_sales_cnt": total_avg_sales_cnt,
+ "total_avg_sales_amount": total_avg_sales_amount,
+ "total_out_stock_cnt": total_out_stock_cnt,
+ "total_storage_locked_cnt": total_storage_locked_cnt,
+ "total_out_stock_ongoing_cnt": total_out_stock_ongoing_cnt,
+ "total_buy_cnt": total_buy_cnt,
+ "has_sales_part_count": has_sales_part_count,
+ "no_sales_part_count": no_sales_part_count,
}
-
- if not part_results:
- return stats
-
- for pr in part_results:
- # 兼容对象和字典两种形式
- if hasattr(pr, "total_suggest_cnt"):
- suggest_cnt = pr.total_suggest_cnt
- suggest_amount = pr.total_suggest_amount
- cost_price = pr.cost_price
- avg_sales = pr.total_avg_sales_cnt
- priority = pr.priority
+
+
+def _classify_part(p: dict) -> str:
+ """
+ 将配件分类为缺货/呆滞/低频/正常
+
+ 分类规则(按优先级顺序判断):
+ - 缺货件: 有效库存 = 0 且 月均销量 >= 1
+ - 呆滞件: 有效库存 > 0 且 90天出库数 = 0
+ - 低频件: 月均销量 < 1 或 出库次数 < 3 或 出库间隔 >= 30天
+ - 正常件: 不属于以上三类
+ """
+ in_stock = _to_decimal(p.get("in_stock_unlocked_cnt", 0))
+ on_way = _to_decimal(p.get("on_the_way_cnt", 0))
+ has_plan = _to_decimal(p.get("has_plan_cnt", 0))
+ valid_storage = in_stock + on_way + has_plan
+
+ out_stock = _to_decimal(p.get("out_stock_cnt", 0))
+ locked = _to_decimal(p.get("storage_locked_cnt", 0))
+ ongoing = _to_decimal(p.get("out_stock_ongoing_cnt", 0))
+ buy = _to_decimal(p.get("buy_cnt", 0))
+ avg_sales = (out_stock + locked + ongoing + buy) / Decimal("3")
+
+ out_times = int(p.get("out_times", 0) or 0)
+ out_duration = int(p.get("out_duration", 0) or 0)
+
+ # 缺货件
+ if valid_storage == 0 and avg_sales >= 1:
+ return "shortage"
+
+ # 呆滞件
+ if valid_storage > 0 and out_stock == 0:
+ return "stagnant"
+
+ # 低频件
+ if avg_sales < 1 or out_times < 3 or out_duration >= 30:
+ return "low_freq"
+
+ return "normal"
+
+
+def calculate_inventory_health(part_ratios: list[dict]) -> dict:
+ """
+ 计算库存构成健康度统计数据
+
+ 将每个配件归类为缺货件/呆滞件/低频件/正常件,统计各类型数量/金额/百分比,
+ 并生成 chart_data 供前端图表使用。
+
+ Args:
+ part_ratios: PartRatio 字典列表
+
+ Returns:
+ 健康度统计字典(含 chart_data)
+ """
+ categories = {
+ "shortage": {"count": 0, "amount": Decimal("0")},
+ "stagnant": {"count": 0, "amount": Decimal("0")},
+ "low_freq": {"count": 0, "amount": Decimal("0")},
+ "normal": {"count": 0, "amount": Decimal("0")},
+ }
+
+ for p in part_ratios:
+ cat = _classify_part(p)
+ cost_price = _to_decimal(p.get("cost_price", 0))
+
+ # 有效库存金额
+ in_stock = _to_decimal(p.get("in_stock_unlocked_cnt", 0))
+ on_way = _to_decimal(p.get("on_the_way_cnt", 0))
+ has_plan = _to_decimal(p.get("has_plan_cnt", 0))
+ valid_storage = in_stock + on_way + has_plan
+ amount = valid_storage * cost_price
+
+ categories[cat]["count"] += 1
+ categories[cat]["amount"] += amount
+
+ total_count = len(part_ratios)
+ total_amount = sum(c["amount"] for c in categories.values())
+
+ # 计算百分比
+ result = {}
+ for cat_name, data in categories.items():
+ count_pct = (data["count"] / total_count * 100) if total_count > 0 else 0.0
+ amount_pct = (float(data["amount"]) / float(total_amount) * 100) if total_amount > 0 else 0.0
+ result[cat_name] = {
+ "count": data["count"],
+ "amount": data["amount"],
+ "count_pct": round(count_pct, 2),
+ "amount_pct": round(amount_pct, 2),
+ }
+
+ result["total_count"] = total_count
+ result["total_amount"] = total_amount
+
+ # chart_data 供前端 Chart.js 使用
+ labels = ["缺货件", "呆滞件", "低频件", "正常件"]
+ cat_keys = ["shortage", "stagnant", "low_freq", "normal"]
+ result["chart_data"] = {
+ "labels": labels,
+ "count_values": [categories[k]["count"] for k in cat_keys],
+ "amount_values": [float(categories[k]["amount"]) for k in cat_keys],
+ }
+
+ return result
+
+
+def calculate_replenishment_summary(part_results: list) -> dict:
+ """
+ 计算补货建议生成情况统计数据
+
+ 按优先级分类统计:
+ - priority=1: 急需补货
+ - priority=2: 建议补货
+ - priority=3: 可选补货
+
+ Args:
+ part_results: 配件汇总结果列表(字典或 ReplenishmentPartSummary 对象)
+
+ Returns:
+ 补货建议统计字典
+ """
+ urgent = {"count": 0, "amount": Decimal("0")}
+ suggested = {"count": 0, "amount": Decimal("0")}
+ optional = {"count": 0, "amount": Decimal("0")}
+
+ for item in part_results:
+ # 兼容字典和对象两种形式
+ if isinstance(item, dict):
+ priority = int(item.get("priority", 0))
+ amount = _to_decimal(item.get("total_suggest_amount", 0))
else:
- suggest_cnt = int(pr.get("total_suggest_cnt", 0))
- suggest_amount = Decimal(str(pr.get("total_suggest_amount", 0)))
- cost_price = Decimal(str(pr.get("cost_price", 0)))
- avg_sales = Decimal(str(pr.get("total_avg_sales_cnt", 0)))
- priority = int(pr.get("priority", 2))
-
- # 总体统计
- stats["total_parts_cnt"] += 1
- stats["total_suggest_cnt"] += suggest_cnt
- stats["total_suggest_amount"] += suggest_amount
-
- # 优先级分布
+ priority = getattr(item, "priority", 0)
+ amount = _to_decimal(getattr(item, "total_suggest_amount", 0))
+
if priority == 1:
- stats["priority_high_cnt"] += 1
- stats["priority_high_amount"] += suggest_amount
+ urgent["count"] += 1
+ urgent["amount"] += amount
elif priority == 2:
- stats["priority_medium_cnt"] += 1
- stats["priority_medium_amount"] += suggest_amount
- else:
- stats["priority_low_cnt"] += 1
- stats["priority_low_amount"] += suggest_amount
-
- # 价格区间分布: <50低价, 50-200中价, >200高价
- if cost_price < 50:
- stats["price_low_cnt"] += 1
- stats["price_low_amount"] += suggest_amount
- elif cost_price <= 200:
- stats["price_medium_cnt"] += 1
- stats["price_medium_amount"] += suggest_amount
- else:
- stats["price_high_cnt"] += 1
- stats["price_high_amount"] += suggest_amount
-
- # 周转频次分布: 月均销量 >=5高频, 1-5中频, <1低频
- if avg_sales >= 5:
- stats["turnover_high_cnt"] += 1
- stats["turnover_high_amount"] += suggest_amount
- elif avg_sales >= 1:
- stats["turnover_medium_cnt"] += 1
- stats["turnover_medium_amount"] += suggest_amount
- else:
- stats["turnover_low_cnt"] += 1
- stats["turnover_low_amount"] += suggest_amount
-
- # 补货金额分布: >=5000大额, 1000-5000中额, <1000小额
- if suggest_amount >= 5000:
- stats["replenish_large_cnt"] += 1
- stats["replenish_large_amount"] += suggest_amount
- elif suggest_amount >= 1000:
- stats["replenish_medium_cnt"] += 1
- stats["replenish_medium_amount"] += suggest_amount
+ suggested["count"] += 1
+ suggested["amount"] += amount
+ elif priority == 3:
+ optional["count"] += 1
+ optional["amount"] += amount
+
+ total_count = urgent["count"] + suggested["count"] + optional["count"]
+ total_amount = urgent["amount"] + suggested["amount"] + optional["amount"]
+
+ return {
+ "urgent": urgent,
+ "suggested": suggested,
+ "optional": optional,
+ "total_count": total_count,
+ "total_amount": total_amount,
+ }
+
+
+# ============================================================
+# LLM 分析函数
+# ============================================================
+
+import os
+import json
+import time
+from langchain_core.messages import SystemMessage, HumanMessage
+
+
+def _load_prompt(filename: str) -> str:
+ """从 prompts 目录加载提示词文件"""
+ prompt_path = os.path.join(
+ os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))),
+ "prompts",
+ filename,
+ )
+ with open(prompt_path, "r", encoding="utf-8") as f:
+ return f.read()
+
+
+def _format_decimal(value) -> str:
+ """将 Decimal 格式化为字符串,用于填充提示词"""
+ if value is None:
+ return "0"
+ return str(round(float(value), 2))
+
+
+def _get_season_from_date(date_str: str) -> str:
+ """
+ 根据日期字符串获取季节
+
+ Args:
+ date_str: 日期字符串,格式如 "2024-01-15" 或 "20240115"
+
+ Returns:
+ 季节名称:春季/夏季/秋季/冬季
+ """
+ from datetime import datetime
+
+ try:
+ # 尝试解析不同格式的日期
+ if "-" in date_str:
+ dt = datetime.strptime(date_str[:10], "%Y-%m-%d")
else:
- stats["replenish_small_cnt"] += 1
- stats["replenish_small_amount"] += suggest_amount
-
- return stats
-
-
-def _calculate_risk_stats(part_ratios: list) -> dict:
- """计算风险统计数据"""
- stats = {
- "shortage_cnt": 0,
- "shortage_amount": Decimal("0"),
- "stagnant_cnt": 0,
- "stagnant_amount": Decimal("0"),
- "low_freq_cnt": 0,
- "low_freq_amount": Decimal("0"),
+ dt = datetime.strptime(date_str[:8], "%Y%m%d")
+ month = dt.month
+ except (ValueError, TypeError):
+ # 解析失败时使用当前月份
+ month = datetime.now().month
+
+ if month in (3, 4, 5):
+ return "春季(3-5月)"
+ elif month in (6, 7, 8):
+ return "夏季(6-8月)"
+ elif month in (9, 10, 11):
+ return "秋季(9-11月)"
+ else:
+ return "冬季(12-2月)"
+
+
+def _parse_llm_json(content: str) -> dict:
+ """
+ 解析 LLM 返回的 JSON 内容
+
+ 尝试直接解析,如果失败则尝试提取 ```json 代码块中的内容。
+ """
+ text = content.strip()
+
+ # 尝试直接解析
+ try:
+ return json.loads(text)
+ except json.JSONDecodeError:
+ pass
+
+ # 尝试提取 ```json ... ``` 代码块
+ import re
+ match = re.search(r"```json\s*(.*?)\s*```", text, re.DOTALL)
+ if match:
+ try:
+ return json.loads(match.group(1))
+ except json.JSONDecodeError:
+ pass
+
+ # 尝试提取 { ... } 块
+ start = text.find("{")
+ end = text.rfind("}")
+ if start != -1 and end != -1 and end > start:
+ try:
+ return json.loads(text[start : end + 1])
+ except json.JSONDecodeError:
+ pass
+
+ # 解析失败
+ raise json.JSONDecodeError("无法从 LLM 响应中解析 JSON", text, 0)
+
+
+def llm_analyze_inventory_overview(stats: dict, statistics_date: str = "", llm_client=None) -> tuple[dict, dict]:
+ """
+ LLM 分析库存概览
+
+ Args:
+ stats: calculate_inventory_overview 的输出
+ statistics_date: 统计日期
+ llm_client: LLM 客户端实例,为 None 时自动获取
+
+ Returns:
+ (llm_analysis_dict, usage_dict)
+ """
+ from ..llm import get_llm_client
+
+ if llm_client is None:
+ llm_client = get_llm_client()
+
+ current_season = _get_season_from_date(statistics_date)
+
+ prompt_template = _load_prompt("report_inventory_overview.md")
+ prompt = prompt_template.format(
+ part_count=stats.get("part_count", 0),
+ total_valid_storage_cnt=_format_decimal(stats.get("total_valid_storage_cnt")),
+ total_valid_storage_amount=_format_decimal(stats.get("total_valid_storage_amount")),
+ total_avg_sales_cnt=_format_decimal(stats.get("total_avg_sales_cnt")),
+ overall_ratio=_format_decimal(stats.get("overall_ratio")),
+ total_in_stock_unlocked_cnt=_format_decimal(stats.get("total_in_stock_unlocked_cnt")),
+ total_in_stock_unlocked_amount=_format_decimal(stats.get("total_in_stock_unlocked_amount")),
+ total_on_the_way_cnt=_format_decimal(stats.get("total_on_the_way_cnt")),
+ total_on_the_way_amount=_format_decimal(stats.get("total_on_the_way_amount")),
+ total_has_plan_cnt=_format_decimal(stats.get("total_has_plan_cnt")),
+ total_has_plan_amount=_format_decimal(stats.get("total_has_plan_amount")),
+ current_season=current_season,
+ statistics_date=statistics_date or "未知",
+ )
+
+ messages = [HumanMessage(content=prompt)]
+ response = llm_client.invoke(messages)
+
+ try:
+ analysis = _parse_llm_json(response.content)
+ except json.JSONDecodeError:
+ logger.warning(f"库存概览 LLM JSON 解析失败,原始响应: {response.content[:200]}")
+ analysis = {"error": "JSON解析失败", "raw": response.content[:200]}
+
+ usage = {
+ "provider": response.usage.provider,
+ "model": response.usage.model,
+ "prompt_tokens": response.usage.prompt_tokens,
+ "completion_tokens": response.usage.completion_tokens,
}
-
- for pr in part_ratios:
- valid_storage = Decimal(str(pr.get("valid_storage_cnt", 0) or 0))
- avg_sales = Decimal(str(pr.get("avg_sales_cnt", 0) or 0))
- out_stock = Decimal(str(pr.get("out_stock_cnt", 0) or 0))
- cost_price = Decimal(str(pr.get("cost_price", 0) or 0))
-
- # 呆滞件: 有库存但90天无出库
- if valid_storage > 0 and out_stock == 0:
- stats["stagnant_cnt"] += 1
- stats["stagnant_amount"] += valid_storage * cost_price
-
- # 低频件: 无库存且月均销量<1
- elif valid_storage == 0 and avg_sales < 1:
- stats["low_freq_cnt"] += 1
-
- # 缺货件: 无库存且月均销量>=1
- elif valid_storage == 0 and avg_sales >= 1:
- stats["shortage_cnt"] += 1
- # 缺货损失估算:月均销量 * 成本价
- stats["shortage_amount"] += avg_sales * cost_price
-
- return stats
+ return analysis, usage
-def _build_suggestion_summary(suggestion_stats: dict) -> str:
+def llm_analyze_sales(stats: dict, statistics_date: str = "", llm_client=None) -> tuple[dict, dict]:
"""
- 基于预计算的统计数据构建结构化补货建议摘要
-
- 摘要包含:
- - 补货总体规模
- - 优先级分布
- - 价格区间分布
- - 周转频次分布
- - 补货金额分布
+ LLM 分析销量
+
+ Args:
+ stats: calculate_sales_analysis 的输出
+ statistics_date: 统计日期
+ llm_client: LLM 客户端实例
+
+ Returns:
+ (llm_analysis_dict, usage_dict)
"""
- if suggestion_stats["total_parts_cnt"] == 0:
- return "暂无补货建议"
-
- lines = []
-
- # 总体规模
- lines.append(f"### 补货总体规模")
- lines.append(f"- 涉及配件种类: {suggestion_stats['total_parts_cnt']}种")
- lines.append(f"- 建议补货总数量: {suggestion_stats['total_suggest_cnt']}件")
- lines.append(f"- 建议补货总金额: {suggestion_stats['total_suggest_amount']:.2f}元")
- lines.append("")
-
- # 优先级分布
- lines.append(f"### 优先级分布")
- lines.append(f"| 优先级 | 配件数 | 金额(元) | 占比 |")
- lines.append(f"|--------|--------|----------|------|")
- total_amount = suggestion_stats['total_suggest_amount'] or Decimal("1")
-
- if suggestion_stats['priority_high_cnt'] > 0:
- pct = suggestion_stats['priority_high_amount'] / total_amount * 100
- lines.append(f"| 高优先级 | {suggestion_stats['priority_high_cnt']} | {suggestion_stats['priority_high_amount']:.2f} | {pct:.1f}% |")
- if suggestion_stats['priority_medium_cnt'] > 0:
- pct = suggestion_stats['priority_medium_amount'] / total_amount * 100
- lines.append(f"| 中优先级 | {suggestion_stats['priority_medium_cnt']} | {suggestion_stats['priority_medium_amount']:.2f} | {pct:.1f}% |")
- if suggestion_stats['priority_low_cnt'] > 0:
- pct = suggestion_stats['priority_low_amount'] / total_amount * 100
- lines.append(f"| 低优先级 | {suggestion_stats['priority_low_cnt']} | {suggestion_stats['priority_low_amount']:.2f} | {pct:.1f}% |")
- lines.append("")
-
- # 价格区间分布
- lines.append(f"### 价格区间分布 (按成本价)")
- lines.append(f"| 价格区间 | 配件数 | 金额(元) | 占比 |")
- lines.append(f"|----------|--------|----------|------|")
- if suggestion_stats['price_low_cnt'] > 0:
- pct = suggestion_stats['price_low_amount'] / total_amount * 100
- lines.append(f"| 低价(<50元) | {suggestion_stats['price_low_cnt']} | {suggestion_stats['price_low_amount']:.2f} | {pct:.1f}% |")
- if suggestion_stats['price_medium_cnt'] > 0:
- pct = suggestion_stats['price_medium_amount'] / total_amount * 100
- lines.append(f"| 中价(50-200元) | {suggestion_stats['price_medium_cnt']} | {suggestion_stats['price_medium_amount']:.2f} | {pct:.1f}% |")
- if suggestion_stats['price_high_cnt'] > 0:
- pct = suggestion_stats['price_high_amount'] / total_amount * 100
- lines.append(f"| 高价(>200元) | {suggestion_stats['price_high_cnt']} | {suggestion_stats['price_high_amount']:.2f} | {pct:.1f}% |")
- lines.append("")
-
- # 周转频次分布
- lines.append(f"### 周转频次分布 (按月均销量)")
- lines.append(f"| 周转频次 | 配件数 | 金额(元) | 占比 |")
- lines.append(f"|----------|--------|----------|------|")
- if suggestion_stats['turnover_high_cnt'] > 0:
- pct = suggestion_stats['turnover_high_amount'] / total_amount * 100
- lines.append(f"| 高频(≥5件/月) | {suggestion_stats['turnover_high_cnt']} | {suggestion_stats['turnover_high_amount']:.2f} | {pct:.1f}% |")
- if suggestion_stats['turnover_medium_cnt'] > 0:
- pct = suggestion_stats['turnover_medium_amount'] / total_amount * 100
- lines.append(f"| 中频(1-5件/月) | {suggestion_stats['turnover_medium_cnt']} | {suggestion_stats['turnover_medium_amount']:.2f} | {pct:.1f}% |")
- if suggestion_stats['turnover_low_cnt'] > 0:
- pct = suggestion_stats['turnover_low_amount'] / total_amount * 100
- lines.append(f"| 低频(<1件/月) | {suggestion_stats['turnover_low_cnt']} | {suggestion_stats['turnover_low_amount']:.2f} | {pct:.1f}% |")
- lines.append("")
-
- # 补货金额分布
- lines.append(f"### 单配件补货金额分布")
- lines.append(f"| 补货规模 | 配件数 | 金额(元) | 占比 |")
- lines.append(f"|----------|--------|----------|------|")
- if suggestion_stats['replenish_large_cnt'] > 0:
- pct = suggestion_stats['replenish_large_amount'] / total_amount * 100
- lines.append(f"| 大额(≥5000元) | {suggestion_stats['replenish_large_cnt']} | {suggestion_stats['replenish_large_amount']:.2f} | {pct:.1f}% |")
- if suggestion_stats['replenish_medium_cnt'] > 0:
- pct = suggestion_stats['replenish_medium_amount'] / total_amount * 100
- lines.append(f"| 中额(1000-5000元) | {suggestion_stats['replenish_medium_cnt']} | {suggestion_stats['replenish_medium_amount']:.2f} | {pct:.1f}% |")
- if suggestion_stats['replenish_small_cnt'] > 0:
- pct = suggestion_stats['replenish_small_amount'] / total_amount * 100
- lines.append(f"| 小额(<1000元) | {suggestion_stats['replenish_small_cnt']} | {suggestion_stats['replenish_small_amount']:.2f} | {pct:.1f}% |")
-
- return "\n".join(lines)
+ from ..llm import get_llm_client
+ if llm_client is None:
+ llm_client = get_llm_client()
-def generate_analysis_report_node(state: dict) -> dict:
+ current_season = _get_season_from_date(statistics_date)
+
+ prompt_template = _load_prompt("report_sales_analysis.md")
+ prompt = prompt_template.format(
+ total_avg_sales_cnt=_format_decimal(stats.get("total_avg_sales_cnt")),
+ total_avg_sales_amount=_format_decimal(stats.get("total_avg_sales_amount")),
+ has_sales_part_count=stats.get("has_sales_part_count", 0),
+ no_sales_part_count=stats.get("no_sales_part_count", 0),
+ total_out_stock_cnt=_format_decimal(stats.get("total_out_stock_cnt")),
+ total_storage_locked_cnt=_format_decimal(stats.get("total_storage_locked_cnt")),
+ total_out_stock_ongoing_cnt=_format_decimal(stats.get("total_out_stock_ongoing_cnt")),
+ total_buy_cnt=_format_decimal(stats.get("total_buy_cnt")),
+ current_season=current_season,
+ statistics_date=statistics_date or "未知",
+ )
+
+ messages = [HumanMessage(content=prompt)]
+ response = llm_client.invoke(messages)
+
+ try:
+ analysis = _parse_llm_json(response.content)
+ except json.JSONDecodeError:
+ logger.warning(f"销量分析 LLM JSON 解析失败,原始响应: {response.content[:200]}")
+ analysis = {"error": "JSON解析失败", "raw": response.content[:200]}
+
+ usage = {
+ "provider": response.usage.provider,
+ "model": response.usage.model,
+ "prompt_tokens": response.usage.prompt_tokens,
+ "completion_tokens": response.usage.completion_tokens,
+ }
+ return analysis, usage
+
+
+def llm_analyze_inventory_health(stats: dict, statistics_date: str = "", llm_client=None) -> tuple[dict, dict]:
"""
- 生成分析报告节点
-
- 输入: part_ratios, llm_suggestions, allocated_details, part_results
- 输出: analysis_report
+ LLM 分析库存健康度
+
+ Args:
+ stats: calculate_inventory_health 的输出
+ statistics_date: 统计日期
+ llm_client: LLM 客户端实例
+
+ Returns:
+ (llm_analysis_dict, usage_dict)
"""
- start_time = time.time()
-
- task_no = state.get("task_no", "")
- group_id = state.get("group_id", 0)
- dealer_grouping_id = state.get("dealer_grouping_id", 0)
- dealer_grouping_name = state.get("dealer_grouping_name", "")
- brand_grouping_id = state.get("brand_grouping_id")
- statistics_date = state.get("statistics_date", "")
-
- part_ratios = state.get("part_ratios", [])
- part_results = state.get("part_results", [])
- allocated_details = state.get("allocated_details", [])
-
- logger.info(f"[{task_no}] 开始生成分析报告: dealer={dealer_grouping_name}")
-
+ from ..llm import get_llm_client
+
+ if llm_client is None:
+ llm_client = get_llm_client()
+
+ current_season = _get_season_from_date(statistics_date)
+
+ prompt_template = _load_prompt("report_inventory_health.md")
+ prompt = prompt_template.format(
+ total_count=stats.get("total_count", 0),
+ total_amount=_format_decimal(stats.get("total_amount")),
+ shortage_count=stats.get("shortage", {}).get("count", 0),
+ shortage_count_pct=stats.get("shortage", {}).get("count_pct", 0),
+ shortage_amount=_format_decimal(stats.get("shortage", {}).get("amount")),
+ shortage_amount_pct=stats.get("shortage", {}).get("amount_pct", 0),
+ stagnant_count=stats.get("stagnant", {}).get("count", 0),
+ stagnant_count_pct=stats.get("stagnant", {}).get("count_pct", 0),
+ stagnant_amount=_format_decimal(stats.get("stagnant", {}).get("amount")),
+ stagnant_amount_pct=stats.get("stagnant", {}).get("amount_pct", 0),
+ low_freq_count=stats.get("low_freq", {}).get("count", 0),
+ low_freq_count_pct=stats.get("low_freq", {}).get("count_pct", 0),
+ low_freq_amount=_format_decimal(stats.get("low_freq", {}).get("amount")),
+ low_freq_amount_pct=stats.get("low_freq", {}).get("amount_pct", 0),
+ normal_count=stats.get("normal", {}).get("count", 0),
+ normal_count_pct=stats.get("normal", {}).get("count_pct", 0),
+ normal_amount=_format_decimal(stats.get("normal", {}).get("amount")),
+ normal_amount_pct=stats.get("normal", {}).get("amount_pct", 0),
+ current_season=current_season,
+ statistics_date=statistics_date or "未知",
+ )
+
+ messages = [HumanMessage(content=prompt)]
+ response = llm_client.invoke(messages)
+
try:
- # 计算风险统计
- risk_stats = _calculate_risk_stats(part_ratios)
-
- # 计算补货建议统计 (基于完整数据)
- suggestion_stats = _calculate_suggestion_stats(part_results)
-
- # 构建结构化建议汇总
- suggestion_summary = _build_suggestion_summary(suggestion_stats)
-
- # 加载 Prompt
- prompt_template = _load_prompt("analysis_report.md")
-
- # 填充 Prompt 变量
- prompt = prompt_template.format(
- dealer_grouping_id=dealer_grouping_id,
- dealer_grouping_name=dealer_grouping_name,
- statistics_date=statistics_date,
- suggestion_summary=suggestion_summary,
- shortage_cnt=risk_stats["shortage_cnt"],
- shortage_amount=f"{risk_stats['shortage_amount']:.2f}",
- stagnant_cnt=risk_stats["stagnant_cnt"],
- stagnant_amount=f"{risk_stats['stagnant_amount']:.2f}",
- low_freq_cnt=risk_stats["low_freq_cnt"],
- low_freq_amount="0.00", # 低频件无库存
- )
-
- # 调用 LLM
+ analysis = _parse_llm_json(response.content)
+ except json.JSONDecodeError:
+ logger.warning(f"健康度 LLM JSON 解析失败,原始响应: {response.content[:200]}")
+ analysis = {"error": "JSON解析失败", "raw": response.content[:200]}
+
+ usage = {
+ "provider": response.usage.provider,
+ "model": response.usage.model,
+ "prompt_tokens": response.usage.prompt_tokens,
+ "completion_tokens": response.usage.completion_tokens,
+ }
+ return analysis, usage
+
+
+def llm_analyze_replenishment_summary(stats: dict, statistics_date: str = "", llm_client=None) -> tuple[dict, dict]:
+ """
+ LLM 分析补货建议
+
+ Args:
+ stats: calculate_replenishment_summary 的输出
+ statistics_date: 统计日期
+ llm_client: LLM 客户端实例
+
+ Returns:
+ (llm_analysis_dict, usage_dict)
+ """
+ from ..llm import get_llm_client
+
+ if llm_client is None:
llm_client = get_llm_client()
- response = llm_client.invoke(
- messages=[HumanMessage(content=prompt)],
- )
-
- # 解析 JSON 响应
- response_text = response.content.strip()
- # 移除可能的 markdown 代码块
- if response_text.startswith("```"):
- lines = response_text.split("\n")
- response_text = "\n".join(lines[1:-1])
-
- report_data = json.loads(response_text)
-
- # 复用已计算的统计数据
- total_suggest_cnt = suggestion_stats["total_suggest_cnt"]
- total_suggest_amount = suggestion_stats["total_suggest_amount"]
-
- execution_time_ms = int((time.time() - start_time) * 1000)
-
- # 创建报告对象
- # 新 prompt 字段名映射到现有数据库字段:
- # overall_assessment -> replenishment_insights
- # risk_alerts -> urgency_assessment
- # procurement_strategy -> strategy_recommendations
- # expected_impact -> expected_outcomes
- # execution_guide 已移除,置为 None
- report = AnalysisReport(
- task_no=task_no,
- group_id=group_id,
- dealer_grouping_id=dealer_grouping_id,
- dealer_grouping_name=dealer_grouping_name,
- brand_grouping_id=brand_grouping_id,
- report_type="replenishment",
- replenishment_insights=report_data.get("overall_assessment"),
- urgency_assessment=report_data.get("risk_alerts"),
- strategy_recommendations=report_data.get("procurement_strategy"),
- execution_guide=None,
- expected_outcomes=report_data.get("expected_impact"),
- total_suggest_cnt=total_suggest_cnt,
- total_suggest_amount=total_suggest_amount,
- shortage_risk_cnt=risk_stats["shortage_cnt"],
- excess_risk_cnt=risk_stats["stagnant_cnt"],
- stagnant_cnt=risk_stats["stagnant_cnt"],
- low_freq_cnt=risk_stats["low_freq_cnt"],
- llm_provider=getattr(llm_client, "provider", ""),
- llm_model=getattr(llm_client, "model", ""),
- llm_tokens=response.usage.total_tokens,
- execution_time_ms=execution_time_ms,
- statistics_date=statistics_date,
- )
-
- # 保存到数据库
- result_writer = ResultWriter()
- try:
- result_writer.save_analysis_report(report)
- finally:
- result_writer.close()
-
- logger.info(
- f"[{task_no}] 分析报告生成完成: "
- f"shortage={risk_stats['shortage_cnt']}, "
- f"stagnant={risk_stats['stagnant_cnt']}, "
- f"time={execution_time_ms}ms"
- )
-
+
+ current_season = _get_season_from_date(statistics_date)
+
+ prompt_template = _load_prompt("report_replenishment_summary.md")
+ prompt = prompt_template.format(
+ total_count=stats.get("total_count", 0),
+ total_amount=_format_decimal(stats.get("total_amount")),
+ urgent_count=stats.get("urgent", {}).get("count", 0),
+ urgent_amount=_format_decimal(stats.get("urgent", {}).get("amount")),
+ suggested_count=stats.get("suggested", {}).get("count", 0),
+ suggested_amount=_format_decimal(stats.get("suggested", {}).get("amount")),
+ optional_count=stats.get("optional", {}).get("count", 0),
+ optional_amount=_format_decimal(stats.get("optional", {}).get("amount")),
+ current_season=current_season,
+ statistics_date=statistics_date or "未知",
+ )
+
+ messages = [HumanMessage(content=prompt)]
+ response = llm_client.invoke(messages)
+
+ try:
+ analysis = _parse_llm_json(response.content)
+ except json.JSONDecodeError:
+ logger.warning(f"补货建议 LLM JSON 解析失败,原始响应: {response.content[:200]}")
+ analysis = {"error": "JSON解析失败", "raw": response.content[:200]}
+
+ usage = {
+ "provider": response.usage.provider,
+ "model": response.usage.model,
+ "prompt_tokens": response.usage.prompt_tokens,
+ "completion_tokens": response.usage.completion_tokens,
+ }
+ return analysis, usage
+
+
+# ============================================================
+# LangGraph 并发子图
+# ============================================================
+
+from typing import TypedDict, Optional, Any, Annotated, Dict
+
+from langgraph.graph import StateGraph, START, END
+
+
+def _merge_dict(left: Optional[dict], right: Optional[dict]) -> Optional[dict]:
+ """合并字典,保留非 None 的值"""
+ if right is not None:
+ return right
+ return left
+
+
+def _sum_int(left: int, right: int) -> int:
+ """累加整数"""
+ return (left or 0) + (right or 0)
+
+
+def _merge_str(left: Optional[str], right: Optional[str]) -> Optional[str]:
+ """合并字符串,保留非 None 的值"""
+ if right is not None:
+ return right
+ return left
+
+
+class ReportLLMState(TypedDict, total=False):
+ """并发 LLM 分析子图的状态"""
+
+ # 输入:四大板块的统计数据(只读,由主函数写入)
+ inventory_overview_stats: Annotated[Optional[dict], _merge_dict]
+ sales_analysis_stats: Annotated[Optional[dict], _merge_dict]
+ inventory_health_stats: Annotated[Optional[dict], _merge_dict]
+ replenishment_summary_stats: Annotated[Optional[dict], _merge_dict]
+
+ # 输入:统计日期(用于季节判断)
+ statistics_date: Annotated[Optional[str], _merge_str]
+
+ # 输出:四大板块的 LLM 分析结果(各节点独立写入)
+ inventory_overview_analysis: Annotated[Optional[dict], _merge_dict]
+ sales_analysis_analysis: Annotated[Optional[dict], _merge_dict]
+ inventory_health_analysis: Annotated[Optional[dict], _merge_dict]
+ replenishment_summary_analysis: Annotated[Optional[dict], _merge_dict]
+
+ # LLM 使用量(累加)
+ total_prompt_tokens: Annotated[int, _sum_int]
+ total_completion_tokens: Annotated[int, _sum_int]
+ llm_provider: Annotated[Optional[str], _merge_dict]
+ llm_model: Annotated[Optional[str], _merge_dict]
+
+
+def _node_inventory_overview(state: ReportLLMState) -> ReportLLMState:
+ """并发节点:库存概览 LLM 分析"""
+ stats = state.get("inventory_overview_stats")
+ statistics_date = state.get("statistics_date", "")
+ if not stats:
+ return {"inventory_overview_analysis": {"error": "无统计数据"}}
+
+ try:
+ analysis, usage = llm_analyze_inventory_overview(stats, statistics_date)
return {
- "analysis_report": report.to_dict(),
- "end_time": time.time(),
+ "inventory_overview_analysis": analysis,
+ "total_prompt_tokens": usage.get("prompt_tokens", 0),
+ "total_completion_tokens": usage.get("completion_tokens", 0),
+ "llm_provider": usage.get("provider", ""),
+ "llm_model": usage.get("model", ""),
}
-
except Exception as e:
- logger.error(f"[{task_no}] 分析报告生成失败: {e}", exc_info=True)
-
- # 返回空报告,不中断整个流程
+ logger.error(f"库存概览 LLM 分析失败: {e}")
+ return {"inventory_overview_analysis": {"error": str(e)}}
+
+
+def _node_sales_analysis(state: ReportLLMState) -> ReportLLMState:
+ """并发节点:销量分析 LLM 分析"""
+ stats = state.get("sales_analysis_stats")
+ statistics_date = state.get("statistics_date", "")
+ if not stats:
+ return {"sales_analysis_analysis": {"error": "无统计数据"}}
+
+ try:
+ analysis, usage = llm_analyze_sales(stats, statistics_date)
+ return {
+ "sales_analysis_analysis": analysis,
+ "total_prompt_tokens": usage.get("prompt_tokens", 0),
+ "total_completion_tokens": usage.get("completion_tokens", 0),
+ "llm_provider": usage.get("provider", ""),
+ "llm_model": usage.get("model", ""),
+ }
+ except Exception as e:
+ logger.error(f"销量分析 LLM 分析失败: {e}")
+ return {"sales_analysis_analysis": {"error": str(e)}}
+
+
+def _node_inventory_health(state: ReportLLMState) -> ReportLLMState:
+ """并发节点:健康度 LLM 分析"""
+ stats = state.get("inventory_health_stats")
+ statistics_date = state.get("statistics_date", "")
+ if not stats:
+ return {"inventory_health_analysis": {"error": "无统计数据"}}
+
+ try:
+ analysis, usage = llm_analyze_inventory_health(stats, statistics_date)
+ return {
+ "inventory_health_analysis": analysis,
+ "total_prompt_tokens": usage.get("prompt_tokens", 0),
+ "total_completion_tokens": usage.get("completion_tokens", 0),
+ "llm_provider": usage.get("provider", ""),
+ "llm_model": usage.get("model", ""),
+ }
+ except Exception as e:
+ logger.error(f"健康度 LLM 分析失败: {e}")
+ return {"inventory_health_analysis": {"error": str(e)}}
+
+
+def _node_replenishment_summary(state: ReportLLMState) -> ReportLLMState:
+ """并发节点:补货建议 LLM 分析"""
+ stats = state.get("replenishment_summary_stats")
+ statistics_date = state.get("statistics_date", "")
+ if not stats:
+ return {"replenishment_summary_analysis": {"error": "无统计数据"}}
+
+ try:
+ analysis, usage = llm_analyze_replenishment_summary(stats, statistics_date)
return {
- "analysis_report": {
- "error": str(e),
- "task_no": task_no,
- },
- "end_time": time.time(),
+ "replenishment_summary_analysis": analysis,
+ "total_prompt_tokens": usage.get("prompt_tokens", 0),
+ "total_completion_tokens": usage.get("completion_tokens", 0),
+ "llm_provider": usage.get("provider", ""),
+ "llm_model": usage.get("model", ""),
}
+ except Exception as e:
+ logger.error(f"补货建议 LLM 分析失败: {e}")
+ return {"replenishment_summary_analysis": {"error": str(e)}}
+
+
+def build_report_llm_subgraph() -> StateGraph:
+ """
+ 构建并发 LLM 分析子图
+
+ 四个 LLM 节点从 START fan-out 并发执行,结果 fan-in 汇总到 END。
+ """
+ graph = StateGraph(ReportLLMState)
+
+ # 添加四个并发节点
+ graph.add_node("inventory_overview_llm", _node_inventory_overview)
+ graph.add_node("sales_analysis_llm", _node_sales_analysis)
+ graph.add_node("inventory_health_llm", _node_inventory_health)
+ graph.add_node("replenishment_summary_llm", _node_replenishment_summary)
+
+ # fan-out: START → 四个节点
+ graph.add_edge(START, "inventory_overview_llm")
+ graph.add_edge(START, "sales_analysis_llm")
+ graph.add_edge(START, "inventory_health_llm")
+ graph.add_edge(START, "replenishment_summary_llm")
+
+ # fan-in: 四个节点 → END
+ graph.add_edge("inventory_overview_llm", END)
+ graph.add_edge("sales_analysis_llm", END)
+ graph.add_edge("inventory_health_llm", END)
+ graph.add_edge("replenishment_summary_llm", END)
+
+ return graph.compile()
+
+
+# ============================================================
+# 主节点函数
+# ============================================================
+
+
+def _serialize_stats(stats: dict) -> dict:
+ """将统计数据中的 Decimal 转换为 float,以便 JSON 序列化"""
+ result = {}
+ for k, v in stats.items():
+ if isinstance(v, Decimal):
+ result[k] = float(v)
+ elif isinstance(v, dict):
+ result[k] = _serialize_stats(v)
+ elif isinstance(v, list):
+ result[k] = [
+ _serialize_stats(item) if isinstance(item, dict) else (float(item) if isinstance(item, Decimal) else item)
+ for item in v
+ ]
+ else:
+ result[k] = v
+ return result
+
+
+def generate_analysis_report_node(state: dict) -> dict:
+ """
+ 分析报告生成主节点
+
+ 串联流程:
+ 1. 统计计算(四大板块)
+ 2. 并发 LLM 分析(LangGraph 子图)
+ 3. 汇总报告
+ 4. 写入数据库
+
+ 单板块 LLM 失败不影响其他板块。
+
+ Args:
+ state: AgentState 字典
+
+ Returns:
+ 更新后的 state 字典
+ """
+ from .state import AgentState
+ from ..models import AnalysisReport
+ from ..services.result_writer import ResultWriter
+
+ logger.info("[AnalysisReport] ========== 开始生成分析报告 ==========")
+ start_time = time.time()
+
+ part_ratios = state.get("part_ratios", [])
+ part_results = state.get("part_results", [])
+
+ # ---- 1. 统计计算 ----
+ logger.info(f"[AnalysisReport] 统计计算: part_ratios={len(part_ratios)}, part_results={len(part_results)}")
+
+ inventory_overview_stats = calculate_inventory_overview(part_ratios)
+ sales_analysis_stats = calculate_sales_analysis(part_ratios)
+ inventory_health_stats = calculate_inventory_health(part_ratios)
+ replenishment_summary_stats = calculate_replenishment_summary(part_results)
+
+ # 序列化统计数据(Decimal → float)
+ io_stats_serialized = _serialize_stats(inventory_overview_stats)
+ sa_stats_serialized = _serialize_stats(sales_analysis_stats)
+ ih_stats_serialized = _serialize_stats(inventory_health_stats)
+ rs_stats_serialized = _serialize_stats(replenishment_summary_stats)
+
+ # ---- 2. 并发 LLM 分析 ----
+ logger.info("[AnalysisReport] 启动并发 LLM 分析子图")
+
+ statistics_date = state.get("statistics_date", "")
+
+ subgraph = build_report_llm_subgraph()
+ llm_state: ReportLLMState = {
+ "inventory_overview_stats": io_stats_serialized,
+ "sales_analysis_stats": sa_stats_serialized,
+ "inventory_health_stats": ih_stats_serialized,
+ "replenishment_summary_stats": rs_stats_serialized,
+ "statistics_date": statistics_date,
+ "inventory_overview_analysis": None,
+ "sales_analysis_analysis": None,
+ "inventory_health_analysis": None,
+ "replenishment_summary_analysis": None,
+ "total_prompt_tokens": 0,
+ "total_completion_tokens": 0,
+ "llm_provider": None,
+ "llm_model": None,
+ }
+
+ try:
+ llm_result = subgraph.invoke(llm_state)
+ except Exception as e:
+ logger.error(f"[AnalysisReport] LLM 子图执行异常: {e}")
+ llm_result = llm_state # 使用初始状态(所有分析为 None)
+
+ # ---- 3. 汇总报告 ----
+ inventory_overview_data = {
+ "stats": io_stats_serialized,
+ "llm_analysis": llm_result.get("inventory_overview_analysis") or {"error": "未生成"},
+ }
+ sales_analysis_data = {
+ "stats": sa_stats_serialized,
+ "llm_analysis": llm_result.get("sales_analysis_analysis") or {"error": "未生成"},
+ }
+ inventory_health_data = {
+ "stats": ih_stats_serialized,
+ "chart_data": ih_stats_serialized.get("chart_data"),
+ "llm_analysis": llm_result.get("inventory_health_analysis") or {"error": "未生成"},
+ }
+ replenishment_summary_data = {
+ "stats": rs_stats_serialized,
+ "llm_analysis": llm_result.get("replenishment_summary_analysis") or {"error": "未生成"},
+ }
+
+ total_tokens = (
+ (llm_result.get("total_prompt_tokens") or 0)
+ + (llm_result.get("total_completion_tokens") or 0)
+ )
+ execution_time_ms = int((time.time() - start_time) * 1000)
+
+ # ---- 4. 写入数据库 ----
+ report = AnalysisReport(
+ task_no=state.get("task_no", ""),
+ group_id=state.get("group_id", 0),
+ dealer_grouping_id=state.get("dealer_grouping_id", 0),
+ dealer_grouping_name=state.get("dealer_grouping_name"),
+ brand_grouping_id=state.get("brand_grouping_id"),
+ inventory_overview=inventory_overview_data,
+ sales_analysis=sales_analysis_data,
+ inventory_health=inventory_health_data,
+ replenishment_summary=replenishment_summary_data,
+ llm_provider=llm_result.get("llm_provider") or "",
+ llm_model=llm_result.get("llm_model") or "",
+ llm_tokens=total_tokens,
+ execution_time_ms=execution_time_ms,
+ statistics_date=state.get("statistics_date", ""),
+ )
+
+ try:
+ writer = ResultWriter()
+ report_id = writer.save_analysis_report(report)
+ writer.close()
+ logger.info(f"[AnalysisReport] 报告已保存: id={report_id}, tokens={total_tokens}, 耗时={execution_time_ms}ms")
+ except Exception as e:
+ logger.error(f"[AnalysisReport] 报告写入数据库失败: {e}")
+
+ # 返回更新后的状态
+ return {
+ "analysis_report": report.to_dict(),
+ "llm_provider": llm_result.get("llm_provider") or state.get("llm_provider", ""),
+ "llm_model": llm_result.get("llm_model") or state.get("llm_model", ""),
+ "llm_prompt_tokens": llm_result.get("total_prompt_tokens") or 0,
+ "llm_completion_tokens": llm_result.get("total_completion_tokens") or 0,
+ "current_node": "generate_analysis_report",
+ "next_node": "end",
+ }
diff --git a/src/fw_pms_ai/agent/sql_agent/agent.py b/src/fw_pms_ai/agent/sql_agent/agent.py
index 0a79343..c130a0d 100644
--- a/src/fw_pms_ai/agent/sql_agent/agent.py
+++ b/src/fw_pms_ai/agent/sql_agent/agent.py
@@ -140,7 +140,7 @@ class SQLAgent:
out_stock_ongoing_cnt, stock_age, out_times, out_duration,
transfer_cnt, gen_transfer_cnt,
part_biz_type, statistics_date,
- (in_stock_unlocked_cnt + on_the_way_cnt + has_plan_cnt + transfer_cnt + gen_transfer_cnt) as valid_storage_cnt,
+ (in_stock_unlocked_cnt + on_the_way_cnt + has_plan_cnt) as valid_storage_cnt,
((out_stock_cnt + storage_locked_cnt + out_stock_ongoing_cnt + buy_cnt) / 3) as avg_sales_cnt
FROM part_ratio
WHERE group_id = %s
@@ -159,7 +159,7 @@ class SQLAgent:
# 优先处理有销量的配件
sql += """ ORDER BY
CASE WHEN ((out_stock_cnt + storage_locked_cnt + out_stock_ongoing_cnt + buy_cnt) / 3) > 0 THEN 0 ELSE 1 END,
- (in_stock_unlocked_cnt + on_the_way_cnt + has_plan_cnt + transfer_cnt + gen_transfer_cnt) / NULLIF((out_stock_cnt + storage_locked_cnt + out_stock_ongoing_cnt + buy_cnt) / 3, 0) ASC,
+ (in_stock_unlocked_cnt + on_the_way_cnt + has_plan_cnt) / NULLIF((out_stock_cnt + storage_locked_cnt + out_stock_ongoing_cnt + buy_cnt) / 3, 0) ASC,
((out_stock_cnt + storage_locked_cnt + out_stock_ongoing_cnt + buy_cnt) / 3) DESC
"""
diff --git a/src/fw_pms_ai/agent/state.py b/src/fw_pms_ai/agent/state.py
index 2387a28..42a9633 100644
--- a/src/fw_pms_ai/agent/state.py
+++ b/src/fw_pms_ai/agent/state.py
@@ -53,8 +53,8 @@ class AgentState(TypedDict, total=False):
dealer_grouping_name: Annotated[str, keep_last]
statistics_date: Annotated[str, keep_last]
- # part_ratio 原始数据
- part_ratios: Annotated[List[dict], merge_dicts]
+ # part_ratio 原始数据(使用 keep_last,因为只在 fetch_part_ratio 节点写入一次)
+ part_ratios: Annotated[List[dict], keep_last]
# SQL Agent 相关
sql_queries: Annotated[List[str], merge_lists]
diff --git a/src/fw_pms_ai/api/routes/tasks.py b/src/fw_pms_ai/api/routes/tasks.py
index 66f3637..49604eb 100644
--- a/src/fw_pms_ai/api/routes/tasks.py
+++ b/src/fw_pms_ai/api/routes/tasks.py
@@ -619,23 +619,14 @@ class AnalysisReportResponse(BaseModel):
group_id: int
dealer_grouping_id: int
dealer_grouping_name: Optional[str] = None
- brand_grouping_id: Optional[int] = None
report_type: str
-
- # JSON 字段,使用 Any 或 Dict 来接收解析后的对象
- replenishment_insights: Optional[Dict[str, Any]] = None
- urgency_assessment: Optional[Dict[str, Any]] = None
- strategy_recommendations: Optional[Dict[str, Any]] = None
- execution_guide: Optional[Dict[str, Any]] = None
- expected_outcomes: Optional[Dict[str, Any]] = None
-
- total_suggest_cnt: int = 0
- total_suggest_amount: float = 0
- shortage_risk_cnt: int = 0
- excess_risk_cnt: int = 0
- stagnant_cnt: int = 0
- low_freq_cnt: int = 0
-
+
+ # 四大板块(统计数据 + LLM 分析)
+ inventory_overview: Optional[Dict[str, Any]] = None
+ sales_analysis: Optional[Dict[str, Any]] = None
+ inventory_health: Optional[Dict[str, Any]] = None
+ replenishment_summary: Optional[Dict[str, Any]] = None
+
llm_provider: Optional[str] = None
llm_model: Optional[str] = None
llm_tokens: int = 0
@@ -664,17 +655,19 @@ async def get_analysis_report(task_no: str):
if not row:
return None
-
- # 辅助函数:解析 JSON 字符串
+
+ # 解析 JSON 字段
def parse_json(value):
- if not value:
+ if value is None:
return None
+ if isinstance(value, dict):
+ return value
if isinstance(value, str):
try:
return json.loads(value)
- except json.JSONDecodeError:
+ except (json.JSONDecodeError, TypeError):
return None
- return value
+ return None
return AnalysisReportResponse(
id=row["id"],
@@ -682,22 +675,11 @@ async def get_analysis_report(task_no: str):
group_id=row["group_id"],
dealer_grouping_id=row["dealer_grouping_id"],
dealer_grouping_name=row.get("dealer_grouping_name"),
- brand_grouping_id=row.get("brand_grouping_id"),
report_type=row.get("report_type", "replenishment"),
-
- replenishment_insights=parse_json(row.get("replenishment_insights")),
- urgency_assessment=parse_json(row.get("urgency_assessment")),
- strategy_recommendations=parse_json(row.get("strategy_recommendations")),
- execution_guide=parse_json(row.get("execution_guide")),
- expected_outcomes=parse_json(row.get("expected_outcomes")),
-
- total_suggest_cnt=row.get("total_suggest_cnt") or 0,
- total_suggest_amount=float(row.get("total_suggest_amount") or 0),
- shortage_risk_cnt=row.get("shortage_risk_cnt") or 0,
- excess_risk_cnt=row.get("excess_risk_cnt") or 0,
- stagnant_cnt=row.get("stagnant_cnt") or 0,
- low_freq_cnt=row.get("low_freq_cnt") or 0,
-
+ inventory_overview=parse_json(row.get("inventory_overview")),
+ sales_analysis=parse_json(row.get("sales_analysis")),
+ inventory_health=parse_json(row.get("inventory_health")),
+ replenishment_summary=parse_json(row.get("replenishment_summary")),
llm_provider=row.get("llm_provider"),
llm_model=row.get("llm_model"),
llm_tokens=row.get("llm_tokens") or 0,
diff --git a/src/fw_pms_ai/models/analysis_report.py b/src/fw_pms_ai/models/analysis_report.py
index ba51bb5..907e1e8 100644
--- a/src/fw_pms_ai/models/analysis_report.py
+++ b/src/fw_pms_ai/models/analysis_report.py
@@ -1,73 +1,59 @@
"""
数据模型 - 分析报告
+四大板块:库存概览、销量分析、库存健康度、补货建议
"""
from dataclasses import dataclass, field
-from decimal import Decimal
from datetime import datetime
-from typing import Optional, Dict, Any
+from typing import Any, Dict, Optional
@dataclass
class AnalysisReport:
- """AI补货建议分析报告"""
-
+ """分析报告数据模型"""
+
task_no: str
group_id: int
dealer_grouping_id: int
-
+
id: Optional[int] = None
dealer_grouping_name: Optional[str] = None
brand_grouping_id: Optional[int] = None
report_type: str = "replenishment"
-
- # 报告各模块 (字典结构)
- replenishment_insights: Optional[Dict[str, Any]] = None
- urgency_assessment: Optional[Dict[str, Any]] = None
- strategy_recommendations: Optional[Dict[str, Any]] = None
- execution_guide: Optional[Dict[str, Any]] = None
- expected_outcomes: Optional[Dict[str, Any]] = None
-
- # 统计信息
- total_suggest_cnt: int = 0
- total_suggest_amount: Decimal = Decimal("0")
- shortage_risk_cnt: int = 0
- excess_risk_cnt: int = 0
- stagnant_cnt: int = 0
- low_freq_cnt: int = 0
-
+
+ # 四大板块
+ inventory_overview: Optional[Dict[str, Any]] = field(default=None)
+ sales_analysis: Optional[Dict[str, Any]] = field(default=None)
+ inventory_health: Optional[Dict[str, Any]] = field(default=None)
+ replenishment_summary: Optional[Dict[str, Any]] = field(default=None)
+
# LLM 元数据
llm_provider: str = ""
llm_model: str = ""
llm_tokens: int = 0
execution_time_ms: int = 0
-
+
statistics_date: str = ""
create_time: Optional[datetime] = None
- def to_dict(self) -> dict:
- """转换为字典"""
+ def to_dict(self) -> Dict[str, Any]:
+ """将报告转换为可序列化的字典"""
return {
+ "id": self.id,
"task_no": self.task_no,
"group_id": self.group_id,
"dealer_grouping_id": self.dealer_grouping_id,
"dealer_grouping_name": self.dealer_grouping_name,
"brand_grouping_id": self.brand_grouping_id,
"report_type": self.report_type,
- "replenishment_insights": self.replenishment_insights,
- "urgency_assessment": self.urgency_assessment,
- "strategy_recommendations": self.strategy_recommendations,
- "execution_guide": self.execution_guide,
- "expected_outcomes": self.expected_outcomes,
- "total_suggest_cnt": self.total_suggest_cnt,
- "total_suggest_amount": float(self.total_suggest_amount),
- "shortage_risk_cnt": self.shortage_risk_cnt,
- "excess_risk_cnt": self.excess_risk_cnt,
- "stagnant_cnt": self.stagnant_cnt,
- "low_freq_cnt": self.low_freq_cnt,
+ "inventory_overview": self.inventory_overview,
+ "sales_analysis": self.sales_analysis,
+ "inventory_health": self.inventory_health,
+ "replenishment_summary": self.replenishment_summary,
"llm_provider": self.llm_provider,
"llm_model": self.llm_model,
"llm_tokens": self.llm_tokens,
"execution_time_ms": self.execution_time_ms,
"statistics_date": self.statistics_date,
+ "create_time": self.create_time.isoformat() if self.create_time else None,
}
diff --git a/src/fw_pms_ai/models/part_ratio.py b/src/fw_pms_ai/models/part_ratio.py
index 3df7039..ce9d717 100644
--- a/src/fw_pms_ai/models/part_ratio.py
+++ b/src/fw_pms_ai/models/part_ratio.py
@@ -46,9 +46,8 @@ class PartRatio:
@property
def valid_storage_cnt(self) -> Decimal:
- """有效库存数量 = 在库未锁 + 在途 + 计划数 + 主动调拨在途 + 自动调拨在途"""
- return (self.in_stock_unlocked_cnt + self.on_the_way_cnt + self.has_plan_cnt +
- Decimal(str(self.transfer_cnt)) + Decimal(str(self.gen_transfer_cnt)))
+ """有效库存数量 = 在库未锁 + 在途 + 计划数"""
+ return self.in_stock_unlocked_cnt + self.on_the_way_cnt + self.has_plan_cnt
@property
def valid_storage_amount(self) -> Decimal:
diff --git a/src/fw_pms_ai/services/result_writer.py b/src/fw_pms_ai/services/result_writer.py
index bba883e..149000c 100644
--- a/src/fw_pms_ai/services/result_writer.py
+++ b/src/fw_pms_ai/services/result_writer.py
@@ -325,8 +325,8 @@ class ResultWriter:
def save_analysis_report(self, report: AnalysisReport) -> int:
"""
- 保存分析报告
-
+ 保存分析报告(四大板块 JSON 结构)
+
Returns:
插入的报告ID
"""
@@ -338,20 +338,18 @@ class ResultWriter:
INSERT INTO ai_analysis_report (
task_no, group_id, dealer_grouping_id, dealer_grouping_name,
brand_grouping_id, report_type,
- replenishment_insights, urgency_assessment, strategy_recommendations,
- execution_guide, expected_outcomes,
- total_suggest_cnt, total_suggest_amount, shortage_risk_cnt,
- excess_risk_cnt, stagnant_cnt, low_freq_cnt,
+ inventory_overview, sales_analysis,
+ inventory_health, replenishment_summary,
llm_provider, llm_model, llm_tokens, execution_time_ms,
statistics_date, create_time
) VALUES (
%s, %s, %s, %s, %s, %s,
- %s, %s, %s, %s, %s,
- %s, %s, %s, %s, %s, %s,
- %s, %s, %s, %s, %s, NOW()
+ %s, %s, %s, %s,
+ %s, %s, %s, %s,
+ %s, NOW()
)
"""
-
+
values = (
report.task_no,
report.group_id,
@@ -359,27 +357,20 @@ class ResultWriter:
report.dealer_grouping_name,
report.brand_grouping_id,
report.report_type,
- json.dumps(report.replenishment_insights, ensure_ascii=False) if report.replenishment_insights else None,
- json.dumps(report.urgency_assessment, ensure_ascii=False) if report.urgency_assessment else None,
- json.dumps(report.strategy_recommendations, ensure_ascii=False) if report.strategy_recommendations else None,
- json.dumps(report.execution_guide, ensure_ascii=False) if report.execution_guide else None,
- json.dumps(report.expected_outcomes, ensure_ascii=False) if report.expected_outcomes else None,
- report.total_suggest_cnt,
- float(report.total_suggest_amount),
- report.shortage_risk_cnt,
- report.excess_risk_cnt,
- report.stagnant_cnt,
- report.low_freq_cnt,
+ json.dumps(report.inventory_overview, ensure_ascii=False) if report.inventory_overview else None,
+ json.dumps(report.sales_analysis, ensure_ascii=False) if report.sales_analysis else None,
+ json.dumps(report.inventory_health, ensure_ascii=False) if report.inventory_health else None,
+ json.dumps(report.replenishment_summary, ensure_ascii=False) if report.replenishment_summary else None,
report.llm_provider,
report.llm_model,
report.llm_tokens,
report.execution_time_ms,
report.statistics_date,
)
-
+
cursor.execute(sql, values)
conn.commit()
-
+
report_id = cursor.lastrowid
logger.info(f"保存分析报告: task_no={report.task_no}, id={report_id}")
return report_id
diff --git a/ui/css/style.css b/ui/css/style.css
index 8420848..cda361d 100644
--- a/ui/css/style.css
+++ b/ui/css/style.css
@@ -2362,3 +2362,390 @@ tbody tr:last-child td {
font-size: 0.75rem;
color: var(--text-muted);
}
+
+
+/* =====================
+ 新报告板块样式
+ ===================== */
+
+/* 报告统计卡片网格 */
+.report-stat-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: var(--spacing-md);
+ margin-bottom: var(--spacing-xl);
+}
+
+.report-stat-cards-4 {
+ grid-template-columns: repeat(4, 1fr);
+}
+
+@media (max-width: 768px) {
+ .report-stat-cards-4 {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+/* 报告统计卡片 */
+.report-stat-card {
+ background: rgba(30, 41, 59, 0.6);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-lg);
+ padding: var(--spacing-lg);
+ transition: all var(--transition-base);
+}
+
+.report-stat-card:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md);
+ border-color: var(--border-color-light);
+}
+
+.report-stat-label {
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ margin-bottom: var(--spacing-xs);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.report-stat-value {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--text-primary);
+ line-height: 1.3;
+}
+
+.report-stat-sub {
+ font-size: 0.85rem;
+ font-weight: 400;
+ color: var(--text-muted);
+}
+
+.report-stat-pct {
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ margin-top: var(--spacing-xs);
+}
+
+/* 健康度卡片颜色变体 */
+.report-stat-card-danger {
+ border-left: 3px solid var(--color-danger);
+}
+.report-stat-card-danger .report-stat-value {
+ color: var(--color-danger-light);
+}
+
+.report-stat-card-warning {
+ border-left: 3px solid var(--color-warning);
+}
+.report-stat-card-warning .report-stat-value {
+ color: var(--color-warning-light);
+}
+
+.report-stat-card-info {
+ border-left: 3px solid var(--color-info);
+}
+.report-stat-card-info .report-stat-value {
+ color: var(--color-info-light);
+}
+
+.report-stat-card-success {
+ border-left: 3px solid var(--color-success);
+}
+.report-stat-card-success .report-stat-value {
+ color: var(--color-success-light);
+}
+
+/* 报告明细表格 */
+.report-detail-table {
+ background: rgba(30, 41, 59, 0.4);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ margin-bottom: var(--spacing-xl);
+}
+
+.report-detail-table table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.report-detail-table th {
+ background: var(--bg-surface);
+ padding: var(--spacing-md) var(--spacing-lg);
+ text-align: left;
+ font-weight: 600;
+ font-size: 0.8rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--text-secondary);
+ border-bottom: 1px solid var(--border-color);
+}
+
+.report-detail-table td {
+ padding: var(--spacing-md) var(--spacing-lg);
+ border-bottom: 1px solid var(--border-color);
+ color: var(--text-primary);
+}
+
+.report-detail-table tbody tr:last-child td {
+ border-bottom: none;
+}
+
+.report-detail-table tbody tr:hover {
+ background: rgba(255, 255, 255, 0.02);
+}
+
+/* 图表容器 */
+.report-charts-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--spacing-xl);
+ margin-bottom: var(--spacing-xl);
+}
+
+@media (max-width: 768px) {
+ .report-charts-row {
+ grid-template-columns: 1fr;
+ }
+}
+
+.report-chart-container {
+ background: rgba(30, 41, 59, 0.4);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-lg);
+ padding: var(--spacing-lg);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.report-chart-title {
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: var(--text-secondary);
+ margin-bottom: var(--spacing-md);
+ text-align: center;
+}
+
+.report-chart-container canvas {
+ max-width: 300px;
+ max-height: 300px;
+}
+
+/* LLM 分析文本区域 */
+.report-analysis-text {
+ background: rgba(30, 41, 59, 0.3);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-lg);
+ padding: var(--spacing-xl);
+}
+
+.analysis-block {
+ margin-bottom: var(--spacing-lg);
+}
+
+.analysis-block:last-child {
+ margin-bottom: 0;
+}
+
+.analysis-block-title {
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: var(--spacing-sm);
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+}
+
+.analysis-block-title svg {
+ width: 18px;
+ height: 18px;
+ color: var(--color-primary);
+}
+
+.analysis-block p {
+ color: var(--text-secondary);
+ line-height: 1.7;
+ margin-bottom: var(--spacing-xs);
+ font-size: 0.9rem;
+}
+
+.analysis-block p:empty {
+ display: none;
+}
+
+.analysis-benchmark {
+ font-style: italic;
+ opacity: 0.8;
+}
+
+.analysis-rec-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.analysis-rec-list li {
+ position: relative;
+ padding-left: 20px;
+ margin-bottom: var(--spacing-sm);
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ line-height: 1.6;
+}
+
+.analysis-rec-list li::before {
+ content: '→';
+ position: absolute;
+ left: 0;
+ color: var(--color-primary);
+}
+
+ol.analysis-rec-list {
+ counter-reset: rec-counter;
+}
+
+ol.analysis-rec-list li {
+ counter-increment: rec-counter;
+}
+
+ol.analysis-rec-list li::before {
+ content: counter(rec-counter) '.';
+ font-weight: 600;
+}
+
+/* 风险标签 */
+.risk-tag {
+ display: inline-flex;
+ align-items: center;
+ padding: 2px 8px;
+ border-radius: var(--radius-full);
+ font-size: 0.7rem;
+ font-weight: 600;
+ margin-left: var(--spacing-sm);
+}
+
+.risk-tag-high {
+ background: rgba(239, 68, 68, 0.15);
+ color: var(--color-danger-light);
+}
+
+.risk-tag-medium {
+ background: rgba(245, 158, 11, 0.15);
+ color: var(--color-warning-light);
+}
+
+.risk-tag-low {
+ background: rgba(16, 185, 129, 0.15);
+ color: var(--color-success-light);
+}
+
+/* 可折叠推理过程 */
+.analysis-process-toggle {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+ padding: var(--spacing-sm) var(--spacing-md);
+ margin-top: var(--spacing-md);
+ background: rgba(99, 102, 241, 0.08);
+ border: 1px solid rgba(99, 102, 241, 0.2);
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+ color: var(--color-primary-light);
+ font-size: 0.85rem;
+ font-weight: 500;
+}
+
+.analysis-process-toggle:hover {
+ background: rgba(99, 102, 241, 0.15);
+}
+
+.analysis-process-toggle svg {
+ width: 16px;
+ height: 16px;
+ transition: transform var(--transition-fast);
+}
+
+.analysis-process-toggle.expanded svg {
+ transform: rotate(180deg);
+}
+
+.analysis-process-content {
+ display: none;
+ margin-top: var(--spacing-md);
+ padding: var(--spacing-md);
+ background: rgba(30, 41, 59, 0.5);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ font-size: 0.85rem;
+}
+
+.analysis-process-content.expanded {
+ display: block;
+}
+
+.process-section {
+ margin-bottom: var(--spacing-md);
+ padding-bottom: var(--spacing-md);
+ border-bottom: 1px solid var(--border-color);
+}
+
+.process-section:last-child {
+ margin-bottom: 0;
+ padding-bottom: 0;
+ border-bottom: none;
+}
+
+.process-section-title {
+ font-size: 0.8rem;
+ font-weight: 600;
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-bottom: var(--spacing-sm);
+}
+
+.process-item {
+ display: flex;
+ margin-bottom: var(--spacing-xs);
+ line-height: 1.5;
+}
+
+.process-item-label {
+ color: var(--text-muted);
+ min-width: 120px;
+ flex-shrink: 0;
+}
+
+.process-item-value {
+ color: var(--text-secondary);
+ word-break: break-word;
+}
+
+.process-item-value.highlight {
+ color: var(--color-primary-light);
+ font-weight: 500;
+}
+
+/* 季节标签 */
+.season-tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 10px;
+ border-radius: var(--radius-full);
+ font-size: 0.75rem;
+ font-weight: 500;
+ background: rgba(59, 130, 246, 0.15);
+ color: var(--color-info-light);
+ margin-left: var(--spacing-sm);
+}
+
+.season-tag svg {
+ width: 12px;
+ height: 12px;
+}
diff --git a/ui/index.html b/ui/index.html
index cae05c6..1774fec 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -17,6 +17,9 @@
+
+
+
diff --git a/ui/js/app.js b/ui/js/app.js
index 1d7e22e..10fba26 100644
--- a/ui/js/app.js
+++ b/ui/js/app.js
@@ -98,7 +98,7 @@ const App = {
/**
- * 渲染分析报告标签页
+ * 渲染分析报告标签页(四大板块:库存概览/销量分析/健康度/补货建议)
*/
async renderReportTab(container, taskNo) {
container.innerHTML = '
${sa.season_demand_feature || ''}
+${sa.inventory_fitness || ''}
+ ${sa.upcoming_season_preparation ? `下季准备: ${sa.upcoming_season_preparation}
` : ''} +${ca.total_evaluation || ''}
+${ca.structure_ratio || ''}
+${rd.analysis || ''}
+${rd.benchmark || ''}
+| 构成项 | +数量 | +金额 | +
|---|---|---|
| 在库未锁 | +${Components.formatNumber(stats.total_in_stock_unlocked_cnt)} | +${Components.formatAmount(stats.total_in_stock_unlocked_amount)} | +
| 在途 | +${Components.formatNumber(stats.total_on_the_way_cnt)} | +${Components.formatAmount(stats.total_on_the_way_amount)} | +
| 计划数 | +${Components.formatNumber(stats.total_has_plan_cnt)} | +${Components.formatAmount(stats.total_has_plan_amount)} | +
${sa.expected_performance || ''}
+${sa.actual_vs_expected || ''}
+ ${sa.seasonal_items_status ? `${sa.seasonal_items_status}
` : ''} +${ca.main_driver || ''}
+${ca.pending_orders_impact || ''}
+${ca.booking_trend || ''}
+${aa.active_ratio || ''}
+${aa.optimization_suggestion || ''}
+${dt.evidence || ''}
+ ${dt.seasonal_factor ? `季节因素: ${dt.seasonal_factor}
` : ''} +${dt.forecast || ''}
+| 构成项 | +总量 | +
|---|---|
| 90天出库数 | +${Components.formatNumber(stats.total_out_stock_cnt)} | +
| 未关单已锁 | +${Components.formatNumber(stats.total_storage_locked_cnt)} | +
| 未关单出库 | +${Components.formatNumber(stats.total_out_stock_ongoing_cnt)} | +
| 订件 | +${Components.formatNumber(stats.total_buy_cnt)} | +