Sfoglia il codice sorgente

增加生成过程中间日志

zsh 2 settimane fa
parent
commit
13d77cc002

+ 2 - 1
.gitignore

@@ -6,4 +6,5 @@ algo/.venv/
 /logs
 algo/py312
 __pycache__/
-*.pyc
+*.pyc
+/task-logs

+ 486 - 80
backend/README.md

@@ -1,12 +1,12 @@
-# Financial Report Generation Backend
+# FinRep Backend 智能报告生成后端服务
 
-## 项目简介
+## 项目概述
 
-本系统为智能报告生成平台的后端服务,采用 Spring Boot 框架实现,遵循 DDD(领域驱动设计)架构模式
+FinRep Backend 是一个基于 DDD(领域驱动设计)架构的智能报告生成后端服务,提供从任务创建、大纲生成、数据准备到最终报告生成的完整流程
 
 ## 技术栈
 
-- Java 17
+- Java 17+
 - Spring Boot 3.2.5
 - MyBatis-Plus 3.5.6
 - MySQL 8.0+
@@ -15,64 +15,420 @@
 - Maven
 - Lombok
 - MapStruct
+- FastJSON2
+- Swagger/OpenAPI 3.0
 - Resilience4j
 - Flyway
 
-## 模块说明
-
-### finrep-parent
-父 POM 模块,统一管理依赖版本和插件配置。
-
-### finrep-common
-公共模块,包含:
-- 上下文管理
-- 异常处理
-- 工具类
-- 常量定义
-
-### finrep-domain
-领域模块,包含:
-- 领域模型
-- 领域服务
-- 值对象
-- 领域事件
-
-### finrep-application
-应用层模块,包含:
-- 应用服务
-- 编排器
-- 端口接口
-- 命令/查询
-- 策略
-
-### finrep-infrastructure
-基础设施模块,包含:
-- 持久化实现
-- 外部适配器
-- 配置类
-- 可观测性
-
-### finrep-web
-Web 层模块,包含:
-- REST 控制器
-- DTO 定义
-- 安全配置
-- SSE 支持
-- 异常处理
-
-### finrep-worker
-Worker 模块,包含:
-- MQ 消费者
-- 消息处理器
-- 定时任务
-- 分布式锁
-
-### finrep-contract
-契约模块,包含:
-- OpenAPI 规范
-- 接口契约文档
-
-## 快速开始
+## 项目架构
+
+本项目采用 DDD 分层架构,包含以下模块:
+
+```
+backend/
+├── finrep-parent/        # Maven 父模块,统一管理依赖版本
+├── finrep-common/         # 通用工具和配置
+├── finrep-contract/       # 对外契约定义
+├── finrep-domain/         # 领域层(核心业务逻辑)
+├── finrep-application/    # 应用层(用例编排)
+├── finrep-infrastructure/ # 基础设施层(外部依赖适配)
+├── finrep-web/           # 接口层(REST API)
+└── finrep-worker/        # 后台工作模块
+```
+
+### 模块职责
+
+| 模块 | 职责 |
+|------|------|
+| `finrep-parent` | 定义所有子模块的依赖版本和插件配置 |
+| `finrep-common` | 上下文管理、异常处理、工具类、常量定义 |
+| `finrep-contract` | OpenAPI 规范、接口契约文档 |
+| `finrep-domain` | 领域模型、领域服务、领域事件、值对象 |
+| `finrep-application` | 应用服务、编排器、命令/查询服务、策略 |
+| `finrep-infrastructure` | 数据库访问、外部服务调用、消息队列实现 |
+| `finrep-web` | REST 控制器、JWT 认证、SSE 推送、DTO 定义 |
+| `finrep-worker` | MQ 消费者、消息处理器、定时任务、分布式锁 |
+
+## 领域模型
+
+### 1. Task(任务)
+- **职责**:表示一个报告生成任务的全生命周期
+- **状态**:DRAFT, RUNNING, COMPLETED, FAILED, CANCELLED
+- **类型**:REPORT_GENERATION, DATA_ANALYSIS
+- **关键属性**:任务ID、任务名称、当前阶段、进度、模型配置、创建者
+
+### 2. Outline(大纲)
+- **职责**:定义报告的结构和章节组织
+- **类型**:
+  - LEVEL1(一级大纲)- 顶层章节结构
+  - LEVEL2(二级大纲)- 详细章节细分
+- **关键属性**:大纲节点树、章节列表、知识单元关联、写作逻辑
+
+### 3. DataPackage(数据包)
+- **职责**:为报告生成准备数据,包含自动数据和手动数据
+- **状态**:CREATED, FETCHING, READY, CONFIRMED
+- **数据源**:DATABASE, API, MANUAL, FILE
+- **关键属性**:知识单元ID、自动数据、手动数据、合并数据
+
+### 4. Report(报告)
+- **职责**:最终生成的报告内容
+- **状态**:CREATED, GENERATING, COMPLETED, FAILED
+- **格式**:DOCX, PDF, HTML
+- **关键属性**:报告标题、章节列表、完整文本、元数据、版本
+
+### 5. KnowledgeUnit(知识单元)
+- **职责**:封装领域知识和写作规范
+- **类型**:TOPIC(主题)、STRUCTURE(结构)、FORMAT(格式)
+- **作用域**:GLOBAL, TENANT, ORGANIZATION
+- **关键属性**:名称、类型、内容、作用域、版本
+
+## API 接口文档
+
+### 1. 任务管理(TaskController)
+
+**基础路径**: `/api/v1/tasks`
+
+| 方法 | 路径 | 描述 |
+|------|------|------|
+| POST | `/` | 创建任务并开始执行 |
+| GET | `/{taskId}` | 获取任务详情 |
+| GET | `/` | 分页获取租户的任务列表 |
+| PUT | `/{taskId}/status` | 更新任务状态 |
+| PUT | `/{taskId}/progress` | 更新任务进度 |
+| POST | `/{taskId}/start` | 开始任务 |
+| POST | `/{taskId}/complete` | 完成任务 |
+| POST | `/{taskId}/fail` | 标记任务失败 |
+| POST | `/{taskId}/cancel` | 取消任务 |
+| DELETE | `/{taskId}` | 删除任务 |
+| PUT | `/{taskId}` | 更新任务参数 |
+
+**请求示例**(创建任务):
+```json
+{
+  "taskName":"test",
+  "parameters":{
+    "report_type": "项目融资",
+    "agreement_amount": "35",
+    "enterprise_type": "单一企业",
+    "group_business_segments": [],
+    "industry_type": "制造业",
+    "has_independent_report": false,
+    "independent_report_types": [],
+    "candidate_financing_tools": ["流动资金贷款"],
+    "recommended_financing_tools": ["流动资金贷款"],
+    "other_requirements": "本次无专项独立调查报告;主服务报告需在「企业基本情况」「企业财务情况」等章节完整论证主体与财务基础,并在此基础上展开融资方案设计。",
+    "chapter_candidates": [
+      {
+        "chapter_id": "pf-l1-01",
+        "chapter_name": "企业基本情况分析",
+        "重要性(无独立报告)": "必定",
+        "重要性(有独立报告)": "低",
+        "适用条件": "无独立报告时需在主报告完整呈现本一级章",
+        "知识边界说明": "无独立企业调查报告时,主报告承担主体基本面论证"
+      },
+      {
+        "chapter_id": "pf-l1-02",
+        "chapter_name": "企业财务情况分析",
+        "重要性(无独立报告)": "必定",
+        "重要性(有独立报告)": "低",
+        "适用条件": "无独立报告时需在主报告完整呈现本一级章",
+        "知识边界说明": "无独立财务分析报告时,主报告承担财务论证与指标支撑"
+      },
+      {
+        "chapter_id": "pf-l1-03",
+        "chapter_name": "融资方案设计",
+        "重要性(无独立报告)": "必定",
+        "重要性(有独立报告)": "必定",
+        "适用条件": "项目融资主报告核心输出,须保留为独立一级章",
+        "知识边界说明": "综合前述结论形成可执行融资结构,不可省略"
+      }
+    ]
+  }
+
+}
+```
+
+**响应示例**:
+```json
+{
+  "code": "0000",
+  "message": "操作成功",
+  "data": {
+    "outlineId": "cb5aea76475b4056a6c617db1e51baf3",
+    "taskId": "90596e26243649d0b93796fae7ea9da2",
+    "tenantId": "default",
+    "title": null,
+    "type": "LEVEL1",
+    "rootNode": null,
+    "sections": "[{\"attributes\":{\"paragraph_count_enum\":\"P2\",\"presentation_enum\":\"S1\"},\"description\":\"因无独立调查报告,需在主报告中完整呈现企业基本面论证\",\"sectionId\":\"pf-l1-01\",\"sortOrder\":0,\"title\":\"企业基本情况分析\"},{\"attributes\":{\"paragraph_count_enum\":\"P2\",\"presentation_enum\":\"S1\"},\"description\":\"因无独立财务分析报告,需在主报告中完整呈现财务论证与指标支撑\",\"sectionId\":\"pf-l1-02\",\"sortOrder\":1,\"title\":\"企业财务情况分析\"},{\"attributes\":{\"paragraph_count_enum\":\"P3\",\"presentation_enum\":\"S1\"},\"description\":\"作为项目融资主报告核心输出,须保留为独立一级章并展开可执行融资结构\",\"sectionId\":\"pf-l1-03\",\"sortOrder\":2,\"title\":\"融资方案设计\"}]",
+    "nodes": null,
+    "knowledgeUnitIds": null,
+    "rawContent": "{\"chapter_results\":[{\"chapter_id\":\"pf-l1-01\",\"chapter_name\":\"企业基本情况分析\",\"presentation_enum\":\"S1\",\"paragraph_count_enum\":\"P2\",\"reason\":\"因无独立调查报告,需在主报告中完整呈现企业基本面论证\"},{\"chapter_id\":\"pf-l1-02\",\"chapter_name\":\"企业财务情况分析\",\"presentation_enum\":\"S1\",\"paragraph_count_enum\":\"P2\",\"reason\":\"因无独立财务分析报告,需在主报告中完整呈现财务论证与指标支撑\"},{\"chapter_id\":\"pf-l1-03\",\"chapter_name\":\"融资方案设计\",\"presentation_enum\":\"S1\",\"paragraph_count_enum\":\"P3\",\"reason\":\"作为项目融资主报告核心输出,须保留为独立一级章并展开可执行融资结构\"}],\"overall_logic\":\"报告以企业基本情况分析为基础,结合财务情况分析形成对主体和财务的全面论证,最终在融资方案设计中实现对流动资金贷款的结构化推荐,确保逻辑闭环与方案可执行性。\"}",
+    "writingLogic": "报告以企业基本情况分析为基础,结合财务情况分析形成对主体和财务的全面论证,最终在融资方案设计中实现对流动资金贷款的结构化推荐,确保逻辑闭环与方案可执行性。",
+    "modelConfigId": null,
+    "confirmed": false,
+    "confirmerId": null,
+    "confirmerName": null,
+    "confirmTime": null,
+    "version": 1,
+    "createTime": "2026-04-09T14:46:27.833412",
+    "updateTime": "2026-04-09T14:46:27.833428",
+    "nodeList": [],
+    "sectionList": [
+      {
+        "sectionId": "pf-l1-01",
+        "sectionNumber": null,
+        "title": "企业基本情况分析",
+        "description": "因无独立调查报告,需在主报告中完整呈现企业基本面论证",
+        "knowledgeUnitId": null,
+        "knowledgeUnitName": null,
+        "dataBinding": null,
+        "type": null,
+        "sortOrder": 0,
+        "attributes": {
+          "paragraph_count_enum": "P2",
+          "presentation_enum": "S1"
+        }
+      },
+      {
+        "sectionId": "pf-l1-02",
+        "sectionNumber": null,
+        "title": "企业财务情况分析",
+        "description": "因无独立财务分析报告,需在主报告中完整呈现财务论证与指标支撑",
+        "knowledgeUnitId": null,
+        "knowledgeUnitName": null,
+        "dataBinding": null,
+        "type": null,
+        "sortOrder": 1,
+        "attributes": {
+          "paragraph_count_enum": "P2",
+          "presentation_enum": "S1"
+        }
+      },
+      {
+        "sectionId": "pf-l1-03",
+        "sectionNumber": null,
+        "title": "融资方案设计",
+        "description": "作为项目融资主报告核心输出,须保留为独立一级章并展开可执行融资结构",
+        "knowledgeUnitId": null,
+        "knowledgeUnitName": null,
+        "dataBinding": null,
+        "type": null,
+        "sortOrder": 2,
+        "attributes": {
+          "paragraph_count_enum": "P3",
+          "presentation_enum": "S1"
+        }
+      }
+    ]
+  },
+  "traceId": null,
+  "timestamp": "2026-04-09T14:46:28.556412"
+}
+```
+
+### 2. 大纲管理(OutlineController)
+
+**基础路径**: `/api/v1/outlines`
+
+| 方法 | 路径 | 描述 |
+|------|------|------|
+| POST | `/{outlineId}/confirm-level1` | 确认一级大纲并生成二级大纲 |
+| POST | `/{outlineId}/confirm-level2` | 确认二级大纲并开始数据准备 |
+| DELETE | `/{outlineId}` | 删除大纲 |
+
+
+### 3. 数据包管理(DataPackageController)
+
+**基础路径**: `/api/v1/data`
+
+| 方法 | 路径 | 描述 |
+|------|------|------|
+| POST | `/` | 创建数据包 |
+| GET | `/{packageId}` | 获取数据包详情 |
+| GET | `/task/{taskId}` | 获取任务的所有数据包 |
+| PUT | `/{packageId}/auto-data` | 更新自动数据 |
+| PUT | `/{packageId}/manual-data` | 更新手动数据 |
+| POST | `/{packageId}/confirm` | 确认数据包 |
+| POST | `/task/{taskId}/confirm-all` | 批量确认任务的所有数据包 |
+| POST | `/{taskId}/generate` | 批量确认数据包并生成报告 |
+| DELETE | `/{packageId}` | 删除数据包 |
+
+
+**响应示例**(生成报告):
+```json
+{
+  "code": "0000",
+  "message": "报告生成完成",
+  "data": [
+    {
+      "chapterId": "pf-l1-01",
+      "chapterName": "企业基本情况分析",
+      "l2": {
+        "chapterName": "企业基本情况分析",
+        "chapterNo": "1",
+        "chapterStructure": [
+          {
+            "nodeName": "企业基本情况",
+            "nodeNo": "1.1",
+            "content": "企业成立于2016年3月18日,注册资本为8000万元人民币,法定代表人为张某某。公司主营业务涵盖智能制造装备的研发、生产与销售,同时提供工业软件与系统集成服务。目前,企业员工规模约为520人,主要生产基地位于苏州工业园区和无锡高新区。近三年来,企业持续聚焦于汽车及电子行业的自动化产线解决方案,致力于为相关领域客户提供系统化、专业化的技术支撑与服务。"
+          },
+          {
+            "nodeName": "企业股权结构",
+            "nodeNo": "1.2",
+            "content": "企业股权结构及控制关系是判断其主体稳定性和治理有效性的重要依据。从股权层级来看,公司由单一控股股东主导,其持股比例为52.3%,在公司治理中具有显著的控制地位。该控股股东为某产业投资有限公司,具备较强的资本实力与行业背景,能够对企业的战略方向和重大决策产生实质性影响。此外,企业还设有员工持股平台和地方国资投资平台作为主要股东,分别持有12.1%和8.6%的股份,体现了多元化股权构成的特点,有助于增强企业治理的多元性与稳定性。\n\n在治理结构方面,公司董事会共设7个席位,其中独立董事占3席,表明公司在决策机制上具备一定的独立性和专业性。同时,公司已设立审计委员会,反映出其在内部监督和财务合规管理方面的制度安排较为完善,为公司稳健运营提供了基础保障。整体来看,企业当前的股权与控制结构在形式上具备一定的规范性和合理性,为后续财务分析与融资方案设计提供了必要的基础信息支撑。"
+          },
+          {
+            "nodeName": "企业主营业务分析",
+            "nodeNo": "1.3",
+            "content": "该企业收入结构呈现多元化特征,主要来源于产线集成与交付、维保与技术服务以及核心零部件销售三大板块。其中,产线集成与交付占比较为突出,达到58%,反映出企业在系统性解决方案提供方面具备较强的业务能力;维保与技术服务占比为22%,表明其在客户服务与持续运营支持方面具有一定基础;核心零部件销售占比为20%,体现出企业在关键部件供应方面的市场参与度。从下游行业分布来看,企业客户主要集中在汽车制造与消费电子领域,分别占比约40%和35%,其余约25%的收入来自其他行业,显示出一定的客户结构多样性。\n\n在订单与交付方面,企业当前在手订单金额约为6.8亿元,表明其具备较为稳定的业务承接能力。同时,典型交付周期介于4至9个月之间,反映出项目实施周期较长,需在融资安排中充分考虑资金周转节奏与项目进度匹配的问题。整体来看,企业的业务模式与客户结构具有一定的稳定性与可延续性,为后续财务分析及融资方案设计提供了必要的业务背景支撑。"
+          },
+          {
+            "nodeName": "企业经营情况分析",
+            "nodeNo": "1.4",
+            "content": "企业当前生产经营规模主要体现在产能与产出的匹配程度上。根据现有数据,华东装配基地的年产成套线体能力为45条,近一年的产能利用率为约78%,表明其在实际运营中基本保持在合理区间内。从产出情况看,上年度共交付成套线体38条,关键部件出货量约为12万台套,反映出企业在产品供给方面具备一定规模基础。同时,企业客户结构呈现较为集中的特征,最近一年前五大客户贡献的收入占比达到46%,体现出客户集中度较高的现状。上述指标共同构成了企业当前经营状况的基本数据框架。"
+          },
+          {
+            "nodeName": "企业融资能力分析",
+            "nodeNo": "1.5",
+            "content": "企业作为以项目制集成交付为主营业务的高新技术企业,具备稳定的业务模式和持续增长的服务收入结构,展现出较强的市场适应能力和盈利能力。其信用记录良好,近三年无公开披露的债务违约情况,表明企业在信用管理方面具备良好的自律机制和风险控制能力。在主体资质方面,企业已获得相关行业准入资质,并被认定为高新技术企业,这为其在融资过程中提供了有力的资质支撑与政策优势。同时,企业与多家银行保持授信合作关系,显示出其在金融资源获取方面的基础条件较为稳固,为后续融资安排奠定了良好的合作基础。上述主体条件与能力边界,构成了企业开展流动资金贷款融资的基本前提,也为后续财务分析及融资方案设计提供了可靠的依据。"
+          }
+        ]
+      }
+    },
+    {
+      "chapterId": "pf-l1-02",
+      "chapterName": "企业财务情况分析",
+      "l2": {
+        "chapterName": "企业财务情况分析",
+        "chapterNo": "2",
+        "chapterStructure": [
+          {
+            "nodeName": "财务概况与关键报表摘要",
+            "nodeNo": "2.1",
+            "content": "企业最近一期资产负债表显示,总资产为18.6亿元,总负债为10.2亿元,所有者权益合计为8.4亿元。从资产与负债结构看,企业资产规模较为稳健,资本结构中所有者权益占比约为45.1%,反映出企业具备一定的财务稳定性。\n\n从利润表情况看,企业在最近一年实现营业收入12.4亿元,营业成本为9.1亿元,净利润为0.95亿元。该年度内,企业主营业务保持一定规模的收入贡献,同时实现了正向的盈利结果。\n\n现金流量方面,企业最近一年经营活动产生的现金流量净额为1.1亿元,投资活动现金流量净额为-0.62亿元,筹资活动现金流量净额为0.28亿元。上述数据表明,企业经营性现金流保持稳定,投资支出对现金流形成一定压力,而融资活动则为企业提供了部分资金支持。"
+          },
+          {
+            "nodeName": "偿债能力、现金流与财务风险要点",
+            "nodeNo": "2.2",
+            "content": "从当前分析对象的财务结构与运营特征来看,其在偿债能力、流动性状况及潜在风险方面呈现出一定的结构性特点。从杠杆水平来看,截至最近一期数据,该主体的资产负债率为54.8%,处于中等偏上水平,反映出其在资本结构中对债务融资的依赖程度较高,需关注其整体负债压力与再融资能力。同时,流动比率与速动比率分别为1.22和0.91,表明其短期偿债能力尚可维持,但速动比率略低于1,提示其流动资产中非速动资产占比相对较高,可能对短期资金调度形成一定制约。\n\n在现金流覆盖方面,近两个年度经营活动产生的现金流量净额分别为1.05亿元与1.10亿元,显示出其主营业务具备一定的现金创造能力。然而,同期利息支出分别为0.31亿元与0.33亿元,表明其经营性现金流对利息支出的覆盖能力较为充足,但仍需关注未来利率变动及融资成本上升可能带来的影响。此外,近两年因产线扩建导致资本性支出持续为负,反映出企业正处于扩张阶段,投资活动对现金流形成较大压力,需结合其长期发展战略评估相关投入的合理性与回报周期。\n\n从风险因素来看,客户集中度较高,前五大客户占比偏高,若单一行业或主要客户出现波动,可能对企业的收入稳定性产生影响。同时,资本性支出的持续负值也表明企业在短期内面临较大的资金支出压力,需关注其资金链的可持续性以及未来融资安排的匹配性。上述因素共同构成了企业在当前阶段所面临的财务与经营环境,为后续融资工具选择与期限结构设计提供了基础依据。"
+          }
+        ]
+      }
+    },
+    {
+      "chapterId": "pf-l1-03",
+      "chapterName": "融资方案设计",
+      "l2": {
+        "chapterName": "融资方案设计",
+        "chapterNo": "3",
+        "chapterStructure": [
+          {
+            "nodeName": "项目概况与建设/运营要点",
+            "nodeNo": "3.1",
+            "content": "华东智能制造产线升级项目由某智能制造科技有限公司负责实施,项目建设周期为18个月,目前处于设备安装与调试阶段。项目主要建设内容包括新增两条柔性装配线、升级仓储物流系统及引入制造执行系统(MES),旨在提升生产效率与智能化水平。根据规划,项目预计于2026年第四季度进入试运营阶段。该项目建设与运营情况为后续融资方案的制定提供了基础依据,相关融资需求将围绕项目实际资金使用计划进行安排。"
+          },
+          {
+            "nodeName": "融资规模、资金用途与融资动因",
+            "nodeNo": "3.2",
+            "content": "在当前企业经营与财务状况的综合研判基础上,拟通过发行不超过5亿元的3年期中期票据,作为本次融资安排的核心工具,旨在有效匹配企业资金需求结构与期限特征。该融资规模及期限设定充分考虑了企业现阶段流动资金缺口、债务到期结构以及项目建设进度等关键因素,具有较强的适配性与实施基础。\n\n从资金用途来看,拟募集资金中约2.5亿元将用于补充日常经营所需的流动资金,以应对订单增长带来的原材料备货压力及应收账款周期延长所形成的营运资金占用;约1.5亿元将专项用于偿还即将到期的存量债务,确保债务结构平稳过渡,避免因集中到期而引发流动性风险;剩余约1亿元将用于支付项目建设尾款及相关运营支出,保障项目收尾阶段的资金需求。上述资金配置逻辑清晰,符合企业实际经营节奏与财务规划。\n\n在融资动因方面,企业当前面临订单持续增长带来的营运资金压力,同时存在债务集中到期的流动性管理挑战。通过中期票据融资,不仅能够有效缓解短期资金压力,还能为后续融资安排预留空间,形成较为稳定的融资节奏。整体安排体现了对企业发展阶段与财务健康度的综合考量,具备良好的可操作性与可持续性。"
+          }
+        ]
+      }
+    }
+  ],
+  "traceId": null,
+  "timestamp": "2026-04-09T14:47:16.504889"
+}
+```
+
+### 4. 报告管理(ReportController)
+
+**基础路径**: `/api/v1/reports`
+
+| 方法 | 路径 | 描述 |
+|------|------|------|
+| POST | `/` | 创建报告 |
+| GET | `/{reportId}` | 获取报告详情 |
+| GET | `/task{taskId}` | 根据任务ID获取报告 |
+| GET | `/{reportId}/full-text` | 获取报告完整文本 |
+| PUT | `/{reportId}/sections/{sectionId}` | 更新报告段落 |
+| POST | `/{reportId}/complete` | 完成报告报告 |
+| GET | `/{reportId}/export/docx` | 导出 DOCX 报告 |
+| GET | `/{reportId}/export/pdf` | 导出 PDF 报告 |
+| DELETE | `/{reportId}` | 删除报告 |
+
+
+## 业务流程
+
+### 完整报告生成流程
+
+```
+1. 创建任务 (POST /api/v1/tasks)
+   ↓
+   任务状态: DRAFT → RUNNING
+   当前阶段: OUTLINE_GENERATION
+
+2. 生成一级大纲(自动)
+   ↓
+   调用 Python Agent 生成一级大纲结构
+
+3. 确认一级大纲 (POST /api/v1/outlines/{id}/confirm-level1)
+   ↓
+   任务状态: RUNNING
+   当前阶段: LEVEL2_OUTLINE_GENERATION
+
+4. 生成二级大纲(自动)
+   ↓
+   基于一级大纲生成详细的二级大纲
+
+5. 确认二级大纲 (POST /api/v1/outlines/{id}/confirm-level2)
+   ↓
+   任务状态: RUNNING
+   当前阶段: DATA_PREPARATION
+
+6. 准备数据包(数据获取)
+   ↓
+   从数据源自动获取数据
+   用户手动补充数据
+
+7. 确认数据包 (POST /api/v1/data/{taskId}/generate)
+   ↓
+   任务状态: RUNNING
+   当前阶段: REPORT_GENERATION
+
+8. 生成报告(自动)
+   ↓
+   调用 Python Agent 生成报告内容
+
+9. 导出报告 (GET /api/v1/reports/{id}/export/docx)
+   ↓
+   任务状态: COMPLETED
+```
+
+## 安全认证
+
+### JWT 认证机制
+
+- 所有 API 接口需要在请求头中携带 JWT Token
+- Token 通过 `Authorization: Bearer {token}` 传递
+- 通过 `TenantContextFilter` 自动提取租户和用户信息
+
+### 上下文信息
+
+**TenantContext**:
+- tenantId: 租户ID
+- orgId: 组织ID
+
+**UserContext**:
+- userId: 用户ID
+- realName: 用户姓名
+
+### 安全配置
+
+相关类:`SecurityConfig`, `JwtAuthenticationFilter`, `TenantContextFilter`
+
+## 开发指南
 
 ### 环境要求
 
@@ -89,21 +445,13 @@ cd backend
 mvn clean package -DskipTests
 ```
 
-### 启动 Web 服务
+### 启动服务
 
 ```bash
+# 启动 Web 服务
 cd finrep-web
 mvn spring-boot:run
-```
-
-### 启动 Worker 服务
 
-```bash
-cd finrep-worker
-mvn spring-boot:run
-```
-
-## 开发指南
 
 ### 代码规范
 
@@ -114,14 +462,14 @@ mvn spring-boot:run
 
 ### 分层规范
 
-- Web 层:处理 HTTP 请求/响应,参数校验
-- Application 层:业务编排,用例实现
-- Domain 层:核心业务逻辑,领域模型
-- Infrastructure 层:技术实现,外部集成
+- **Web 层**:处理 HTTP 请求/响应,参数校验
+- **Application 层**:业务编排,用例实现
+- **Domain 层**:核心业务逻辑,领域模型
+- **Infrastructure 层**:技术实现,外部集成
 
 ### 提交规范
 
-```
+````text
 feat: 新功能
 fix: 修复问题
 docs: 文档更新
@@ -129,17 +477,75 @@ style: 代码格式
 refactor: 重构
 test: 测试相关
 chore: 构建/工具链相关
+````
+
+## 配置说明
+
+### application.yml 主要配置项
+
+```yaml
+server:
+  port: 8080
+
+spring:
+  datasource:
+    url: jdbc:mysql://localhost:3306/finrep
+    username: root
+    password: password
+    driver-class-name: com.mysql.cj.jdbc.Driver
+
+  redis:
+    host: localhost
+    port: 6379
+
+  rabbitmq:
+    host: localhost
+    port: 5672
+
+jwt:
+  secret: your-secret-key
+  expiration: 86400
+
+python-agent:
+  url: http://localhost:5000/api
+  timeout: 30000
 ```
 
-## 部署指南
+## 依赖服务
+
+| 服务 | 用途 | 必需 |
+|------|------|------|
+| MySQL 8.0+ | 数据库 | 是 |
+| Redis | 缓存 | 否 |
+| RabbitMQ | 消息队列 | 否 |
+| Python Agent | AI 模型调用服务 | 是 |
+| OSS/MinIO | 文件存储 | 是 |
+
+## 可观测性
+
+### 日志
+
+- 使用 Slf4j + Logback
+- 日志级别:DEBUG, INFO, WARN, ERROR
+- 日志格式:包含 traceId 用于链路追踪
+
+### 指标
+
+- Spring Boot Actuator
+- Prometheus 指标暴露
+- 健康检查端点:`/actuator/health`
+
+### 分布式追踪
+
+- TraceID 传递
+- 跨服务追踪支持
 
-详见 [docs/deployment.md](docs/deployment.md)
 
-## API 文档
+## 版本历史
 
-启动服务后访问:
-- Swagger UI: http://localhost:8080/swagger-ui.html
-- OpenAPI JSON: http://localhost:8080/v3/api-docs
+| 版本 | 日期 | 说明 |
+|------|------|------|
+| 1.0.0 | 2024-04-09 | 初始版本,实现基础报告生成流程 |
 
 ## 许可证
 

+ 1 - 1
backend/finrep-application/src/main/java/com/yuxin/finrep/application/port/in/DataPackageApplicationService.java

@@ -65,7 +65,7 @@ public interface DataPackageApplicationService {
 
     /**
      * 批量确认任务的所有数据包并生成报告
-     * @return 生成的完整文章内容
+     * @return 生成的完整文章内容(JSON字符串)
      */
     String confirmAllDataPackagesAndGenerateReport(String taskId, String confirmerId, String confirmerName);
 }

+ 34 - 0
backend/finrep-common/src/main/java/com/yuxin/finrep/common/context/TaskContext.java

@@ -0,0 +1,34 @@
+package com.yuxin.finrep.common.context;
+
+/**
+ * 任务上下文
+ * 用于在当前线程中存储任务相关信息
+ */
+public class TaskContext {
+
+    private static final ThreadLocal<String> TASK_ID = new ThreadLocal<>();
+
+    private TaskContext() {
+    }
+
+    /**
+     * 设置任务ID
+     */
+    public static void setTaskId(String taskId) {
+        TASK_ID.set(taskId);
+    }
+
+    /**
+     * 获取任务ID
+     */
+    public static String getTaskId() {
+        return TASK_ID.get();
+    }
+
+    /**
+     * 清除任务ID
+     */
+    public static void clear() {
+        TASK_ID.remove();
+    }
+}

+ 250 - 0
backend/finrep-common/src/main/java/com/yuxin/finrep/common/util/TaskLogger.java

@@ -0,0 +1,250 @@
+package com.yuxin.finrep.common.util;
+
+import com.alibaba.fastjson2.JSON;
+import com.yuxin.finrep.common.constant.Constant;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 任务日志工具类
+ * 用于记录任务执行过程中的关键节点返回结果
+ */
+@Slf4j
+public class TaskLogger {
+
+    private static final String LOG_DIR = "task-logs";
+    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
+
+    /**
+     * 日志目录
+     */
+    private static String logDirectory = LOG_DIR;
+
+    /**
+     * 设置日志目录
+     */
+    public static void setLogDirectory(String dir) {
+        logDirectory = dir;
+    }
+
+    /**
+     * 初始化日志目录
+     */
+    static {
+        initLogDirectory();
+    }
+
+    /**
+     * 初始化日志目录
+     */
+    private static void initLogDirectory() {
+        try {
+            Path logPath = Paths.get(logDirectory);
+            if (!Files.exists(logPath)) {
+                Files.createDirectories(logPath);
+                log.info("任务日志目录创建成功: {}", logPath.toAbsolutePath());
+            }
+        } catch (IOException e) {
+            log.error("创建任务日志目录失败", e);
+        }
+    }
+
+    /**
+     * 记录接口调用日志
+     *
+     * @param taskId        任务ID
+     * @param interfaceName 接口名称
+     * @param step          步骤描述
+     * @param responseData  响应数据
+     */
+    public static void logInterface(String taskId, String interfaceName, String step, Object responseData) {
+        String logContent = buildLogContent(taskId, interfaceName, step, responseData);
+        writeToFile(taskId, logContent);
+    }
+
+    /**
+     * 记录接口调用日志(简化版)
+     *
+     * @param taskId        任务ID
+     * @param interfaceName 接口名称
+     * @param responseData  响应数据
+     */
+    public static void logInterface(String taskId, String interfaceName, Object responseData) {
+        logInterface(taskId, interfaceName, null, responseData);
+    }
+
+    /**
+     * 记录接口调用日志(带JSON格式化)
+     *
+     * @param taskId        任务ID
+     * @param interfaceName 接口名称
+     * @param step          步骤描述
+     * @param responseData  响应数据
+     */
+    public static void logInterfaceJson(String taskId, String interfaceName, String step, Object responseData) {
+        try {
+            // 使用 PrettyFormat 保留 JSON 格式化和换行
+            String jsonStr = JSON.toJSONString(responseData, com.alibaba.fastjson2.JSONWriter.Feature.PrettyFormat);
+            String logContent = buildLogContent(taskId, interfaceName, step, jsonStr);
+            writeToFile(taskId, logContent);
+        } catch (Exception e) {
+            log.error("格式化JSON失败", e);
+            logInterface(taskId, interfaceName, step, responseData);
+        }
+    }
+
+    /**
+     * 构建日志内容
+     */
+    private static String buildLogContent(String taskId, String interfaceName, String step, Object data) {
+        StringBuilder sb = new StringBuilder();
+        String timestamp = LocalDateTime.now().format(DATE_FORMATTER);
+
+        sb.append("========================================\n");
+        sb.append("时间: ").append(timestamp).append("\n");
+        sb.append("任务ID: ").append(taskId).append("\n");
+        sb.append("接口: ").append(interfaceName).append("\n");
+
+        if (StringUtil.isNotBlank(step)) {
+            sb.append("步骤: ").append(step).append("\n");
+        }
+
+        sb.append("----------------------------------------\n");
+        sb.append("返回数据:\n");
+
+        if (data != null) {
+            sb.append(data.toString());
+        } else {
+            sb.append("(null)");
+        }
+
+        sb.append("\n");
+        sb.append("========================================\n\n");
+
+        return sb.toString();
+    }
+
+    /**
+     * 写入文件
+     */
+    private static void writeToFile(String taskId, String content) {
+        // 过滤掉文件名中的非法字符
+        String safeTaskId = taskId.replaceAll("[^a-zA-Z0-9_-]", "_");
+        String fileName = safeTaskId + ".log";
+        String filePath = logDirectory + File.separator + fileName;
+
+        try (FileWriter writer = new FileWriter(filePath, true)) {
+            writer.write(content);
+            log.debug("任务日志写入成功: {}", filePath);
+        } catch (IOException e) {
+            log.error("写入任务日志失败: {}", filePath, e);
+        }
+    }
+
+    /**
+     * 记录任务开始
+     */
+    public static void logTaskStart(String taskId, String taskName, String description) {
+        String logContent = buildTaskStartLog(taskId, taskName, description);
+        writeToFile(taskId, logContent);
+    }
+
+    /**
+     * 构建任务开始日志
+     */
+    private static String buildTaskStartLog(String taskId, String taskName, String description) {
+        StringBuilder sb = new StringBuilder();
+        String timestamp = LocalDateTime.now().format(DATE_FORMATTER);
+
+        sb.append("========================================\n");
+        sb.append("========================================\n");
+        sb.append("           任务执行日志开始\n");
+        sb.append("========================================\n");
+        sb.append("========================================\n\n");
+        sb.append("开始时间: ").append(timestamp).append("\n");
+        sb.append("任务ID: ").append(taskId).append("\n");
+        sb.append("任务名称: ").append(taskName).append("\n");
+
+        if (StringUtil.isNotBlank(description)) {
+            sb.append("任务描述: ").append(description).append("\n");
+        }
+
+        sb.append("\n");
+        return sb.toString();
+    }
+
+    /**
+     * 记录任务完成
+     */
+    public static void logTaskComplete(String taskId) {
+        String logContent = buildTaskCompleteLog(taskId);
+        writeToFile(taskId, logContent);
+    }
+
+    /**
+     * 构建任务完成日志
+     */
+    private static String buildTaskCompleteLog(String taskId) {
+        StringBuilder sb = new StringBuilder();
+        String timestamp = LocalDateTime.now().format(DATE_FORMATTER);
+
+        sb.append("\n");
+        sb.append("========================================\n");
+        sb.append("========================================\n");
+        sb.append("完成时间: ").append(timestamp).append("\n");
+        sb.append("任务ID: ").append(taskId).append("\n");
+        sb.append("状态: ").append("已完成").append("\n");
+        sb.append("========================================\n");
+        sb.append("           任务执行日志结束\n");
+        sb.append("========================================\n");
+        sb.append("========================================\n\n");
+
+        return sb.toString();
+    }
+
+    /**
+     * 记录任务失败
+     */
+    public static void logTaskFailed(String taskId, String reason) {
+        String logContent = buildTaskFailedLog(taskId, reason);
+        writeToFile(taskId, logContent);
+    }
+
+    /**
+     * 构建任务失败日志
+     */
+    private static String buildTaskFailedLog(String taskId, String reason) {
+        StringBuilder sb = new StringBuilder();
+        String timestamp = LocalDateTime.now().format(DATE_FORMATTER);
+
+        sb.append("\n");
+        sb.append("========================================\n");
+        sb.append("========================================\n");
+        sb.append("失败时间: ").append(timestamp).append("\n");
+        sb.append("任务ID: ").append(taskId).append("\n");
+        sb.append("状态: ").append("失败").append("\n");
+        sb.append("失败原因: ").append(reason).append("\n");
+        sb.append("========================================\n");
+        sb.append("           任务执行日志结束\n");
+        sb.append("========================================\n");
+        sb.append("========================================\n\n");
+
+        return sb.toString();
+    }
+
+    /**
+     * 获取日志文件路径
+     */
+    public static String getLogFilePath(String taskId) {
+        String safeTaskId = taskId.replaceAll("[^a-zA-Z0-9_-]", "_");
+        return logDirectory + File.separator + safeTaskId + ".log";
+    }
+}

+ 72 - 2
backend/finrep-web/src/main/java/com/yuxin/finrep/web/rest/controller/DataPackageController.java

@@ -1,8 +1,12 @@
 package com.yuxin.finrep.web.rest.controller;
 
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
 import com.yuxin.finrep.application.port.in.DataPackageApplicationService;
 import com.yuxin.finrep.common.context.TenantContext;
 import com.yuxin.finrep.common.context.UserContext;
+import com.yuxin.finrep.common.util.TaskLogger;
 import com.yuxin.finrep.domain.dataprep.DataPackage;
 import com.yuxin.finrep.web.rest.dto.*;
 import io.swagger.v3.oas.annotations.Operation;
@@ -13,6 +17,7 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -149,7 +154,7 @@ public class DataPackageController {
      */
     @PostMapping("/{taskId}/generate")
     @Operation(summary = "批量确认数据包并生成报告", description = "批量确认任务的所有数据包并开始生成报告")
-    public CommonResponse<String> confirmDataPackagesAndGenerateReport(
+    public CommonResponse<List<ReportChapter>> confirmDataPackagesAndGenerateReport(
             @Parameter(description = "任务ID") @PathVariable String taskId) {
         String confirmerId = UserContext.getUserId();
         String confirmerName = UserContext.getRealName();
@@ -157,8 +162,73 @@ public class DataPackageController {
         log.info("批量确认数据包并生成报告: taskId={}, confirmerId={}", taskId, confirmerId);
 
         String fullArticle = dataPackageApplicationService.confirmAllDataPackagesAndGenerateReport(taskId, confirmerId, confirmerName);
+        List<ReportChapter> reportChapters = parseReportChapters(fullArticle);
 
-        return CommonResponse.success("报告生成完成", fullArticle);
+        // 记录接口调用日志
+        TaskLogger.logInterfaceJson(taskId, "confirmDataPackagesAndGenerateReport", "批量确认数据包并生成报告", reportChapters);
+
+        // 记录任务完成日志
+        TaskLogger.logTaskComplete(taskId);
+
+        return CommonResponse.success("报告生成完成", reportChapters);
+    }
+
+    /**
+     * 解析JSON字符串为报告章节列表
+     */
+    private List<ReportChapter> parseReportChapters(String jsonStr) {
+        List<ReportChapter> chapters = new ArrayList<>();
+        try {
+            JSONArray jsonArray = JSON.parseArray(jsonStr);
+            if (jsonArray == null || jsonArray.isEmpty()) {
+                return chapters;
+            }
+
+            for (int i = 0; i < jsonArray.size(); i++) {
+                JSONObject chapterObj = jsonArray.getJSONObject(i);
+                if (chapterObj == null) {
+                    continue;
+                }
+
+                ReportChapter chapter = ReportChapter.builder()
+                        .chapterId(chapterObj.getString("chapter_id"))
+                        .chapterName(chapterObj.getString("chapter_name"))
+                        .build();
+
+                JSONObject l2Obj = chapterObj.getJSONObject("l2");
+                if (l2Obj != null) {
+                    L2Chapter l2 = L2Chapter.builder()
+                            .chapterName(l2Obj.getString("chapter_name"))
+                            .chapterNo(l2Obj.getString("chapter_no"))
+                            .build();
+
+                    JSONArray structureArray = l2Obj.getJSONArray("chapter_structure");
+                    if (structureArray != null) {
+                        List<ChapterStructureNode> nodes = new ArrayList<>();
+                        for (int j = 0; j < structureArray.size(); j++) {
+                            JSONObject nodeObj = structureArray.getJSONObject(j);
+                            if (nodeObj != null) {
+                                ChapterStructureNode node = ChapterStructureNode.builder()
+                                        .nodeName(nodeObj.getString("node_name"))
+                                        .nodeNo(nodeObj.getString("node_no"))
+                                        .content(nodeObj.getString("content"))
+                                        .build();
+                                nodes.add(node);
+                            }
+                        }
+                        l2.setChapterStructure(nodes);
+                    }
+
+                    chapter.setL2(l2);
+                }
+
+                chapters.add(chapter);
+            }
+        } catch (Exception e) {
+            log.error("解析报告章节JSON失败: {}", e.getMessage(), e);
+        }
+
+        return chapters;
     }
 
     /**

+ 13 - 3
backend/finrep-web/src/main/java/com/yuxin/finrep/web/rest/controller/OutlineController.java

@@ -6,6 +6,7 @@ import com.yuxin.finrep.application.port.in.OutlineApplicationService;
 import com.yuxin.finrep.application.port.in.dto.ChapterCandidateDto;
 import com.yuxin.finrep.common.context.TenantContext;
 import com.yuxin.finrep.common.context.UserContext;
+import com.yuxin.finrep.common.util.TaskLogger;
 import com.yuxin.finrep.domain.outline.Outline;
 import com.yuxin.finrep.domain.outline.OutlineNode;
 import com.yuxin.finrep.domain.outline.OutlineSection;
@@ -52,7 +53,12 @@ public class OutlineController {
         log.info("确认大纲并生成二级大纲: outlineId={}, confirmerId={}", outlineId, confirmerId);
 
         Outline outline = outlineApplicationService.confirmOutlineAndGenerateLevel2(outlineId, confirmerId, confirmerName);
-        return CommonResponse.success(convertToResponse(outline));
+        OutlineResponse response = convertToResponse(outline);
+
+        // 记录接口调用日志
+        TaskLogger.logInterfaceJson(outline.getTaskId(), "confirmOutlineAndGenerateLevel2", "确认一级大纲并生成二级大纲", response);
+
+        return CommonResponse.success(response);
     }
 
     /**
@@ -68,7 +74,12 @@ public class OutlineController {
         log.info("确认二级大纲并准备数据: outlineId={}, confirmerId={}", outlineId, confirmerId);
 
         Outline outline = outlineApplicationService.confirmLevel2OutlineAndPrepareData(outlineId, confirmerId, confirmerName);
-        return CommonResponse.success(convertToResponse(outline));
+        OutlineResponse response = convertToResponse(outline);
+
+        // 记录接口调用日志
+        TaskLogger.logInterfaceJson(outline.getTaskId(), "confirmLevel2OutlineAndPrepareData", "确认二级大纲并准备数据", response);
+
+        return CommonResponse.success(response);
     }
 
     /**
@@ -107,7 +118,6 @@ public class OutlineController {
                 .title(outline.getTitle())
                 .type(outline.getType() != null ? outline.getType().getCode() : null)
                 .rootNode(rootNodeResponse)
-                .sections(sectionResponses)
                 .level2Sections(level2Sections)
                 .knowledgeUnitIds(outline.getKnowledgeUnitIds())
                 .modelConfigId(outline.getModelConfigId())

+ 8 - 1
backend/finrep-web/src/main/java/com/yuxin/finrep/web/rest/controller/TaskController.java

@@ -1,8 +1,10 @@
 package com.yuxin.finrep.web.rest.controller;
 
 import com.yuxin.finrep.application.port.in.TaskApplicationService;
+import com.yuxin.finrep.common.context.TaskContext;
 import com.yuxin.finrep.common.context.TenantContext;
 import com.yuxin.finrep.common.context.UserContext;
+import com.yuxin.finrep.common.util.TaskLogger;
 import com.yuxin.finrep.domain.outline.Outline;
 import com.yuxin.finrep.domain.task.Task;
 import com.yuxin.finrep.web.rest.dto.*;
@@ -44,9 +46,14 @@ public class TaskController {
                 tenantId, orgId, request.getTaskName(), request.getDescription(),
                 request.getModelConfigId(), creatorId, creatorName, request.getParameters()
         );
-        Outline outline = taskApplicationService.startTask(task.getTaskId());
 
+        // 记录任务开始日志
+        TaskLogger.logTaskStart(task.getTaskId(), task.getTaskName(), task.getDescription());
+
+        Outline outline = taskApplicationService.startTask(task.getTaskId());
 
+        // 记录接口调用日志
+        TaskLogger.logInterfaceJson(task.getTaskId(), "createTask", "创建任务并生成一级大纲", outline);
 
         return CommonResponse.success(outline);
     }

+ 31 - 0
backend/finrep-web/src/main/java/com/yuxin/finrep/web/rest/dto/ChapterStructureNode.java

@@ -0,0 +1,31 @@
+package com.yuxin.finrep.web.rest.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 章节结构节点DTO
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ChapterStructureNode {
+
+    /**
+     * 节点名称
+     */
+    private String nodeName;
+
+    /**
+     * 节点编号
+     */
+    private String nodeNo;
+
+    /**
+     * 节点内容
+     */
+    private String content;
+}

+ 33 - 0
backend/finrep-web/src/main/java/com/yuxin/finrep/web/rest/dto/L2Chapter.java

@@ -0,0 +1,33 @@
+package com.yuxin.finrep.web.rest.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * 二级章节DTO
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class L2Chapter {
+
+    /**
+     * 章节名称
+     */
+    private String chapterName;
+
+    /**
+     * 章节编号
+     */
+    private String chapterNo;
+
+    /**
+     * 章节结构
+     */
+    private List<ChapterStructureNode> chapterStructure;
+}

+ 31 - 0
backend/finrep-web/src/main/java/com/yuxin/finrep/web/rest/dto/ReportChapter.java

@@ -0,0 +1,31 @@
+package com.yuxin.finrep.web.rest.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 报告章节DTO
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ReportChapter {
+
+    /**
+     * 章节ID
+     */
+    private String chapterId;
+
+    /**
+     * 章节名称
+     */
+    private String chapterName;
+
+    /**
+     * 二级章节信息
+     */
+    private L2Chapter l2;
+}