805 lines
33 KiB
Markdown
805 lines
33 KiB
Markdown
# DataViz Pro — 后端开发指南
|
||
|
||
## 一、技术栈
|
||
|
||
| 层面 | 选型 |
|
||
|------|------|
|
||
| 语言 | Python 3.12+ |
|
||
| Web 框架 | FastAPI |
|
||
| 数据库 | PostgreSQL 16 |
|
||
| ORM | SQLAlchemy 2.0 (async) |
|
||
| 数据库迁移 | Alembic |
|
||
| 文件解析 | openpyxl (xlsx) + xlrd (xls) + pandas (csv/json) |
|
||
| PDF 导出 | WeasyPrint / ReportLab |
|
||
| PPT 导出 | python-pptx |
|
||
| Excel 导出 | openpyxl |
|
||
| HTML 导出 | Jinja2 模板 + 内联 ECharts |
|
||
| 图片导出 | Playwright (服务端渲染 ECharts → 截图) |
|
||
| 容器化 | Docker + Docker Compose |
|
||
| 依赖管理 | Poetry |
|
||
| 代码质量 | Ruff (lint + format) + mypy (类型检查) |
|
||
| 测试 | pytest + pytest-asyncio |
|
||
|
||
## 二、架构总览
|
||
|
||
### 架构风格
|
||
|
||
**DDD + Clean Architecture + 微服务**
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 微服务拆分 │
|
||
│ │
|
||
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐
|
||
│ │ data-service│ │chart-service│ │template-svc │ │export-service│
|
||
│ │ :8001 │ │ :8002 │ │ :8003 │ │ :8004 │
|
||
│ └─────────────┘ └─────────────┘ └──────────────┘ └──────────────┘
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────┐│
|
||
│ │ PostgreSQL :5432 ││
|
||
│ │ data_db │ chart_db │ template_db │ export_db ││
|
||
│ └─────────────────────────────────────────────────────────┘│
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 服务职责
|
||
|
||
| 服务 | 端口 | 职责 | 数据库 |
|
||
|------|------|------|--------|
|
||
| **data-service** | 8001 | 文件上传、解析、数据集 CRUD、字段推断 | data_db |
|
||
| **chart-service** | 8002 | 图表实例 CRUD、字段绑定、样式配置、ECharts option 生成 | chart_db |
|
||
| **template-service** | 8003 | 模板 CRUD、导入导出、内置模板 | template_db |
|
||
| **export-service** | 8004 | 多格式导出(PNG/PDF/PPT/Excel/HTML) | export_db (任务队列) |
|
||
|
||
### 服务间依赖关系
|
||
|
||
```
|
||
data-service ← 独立,不依赖其他服务
|
||
chart-service ← 依赖 data-service(获取数据集结构和数据)
|
||
template-service ← 独立,不依赖其他服务
|
||
export-service ← 依赖 data-service(获取数据)
|
||
← 依赖 chart-service(获取图表配置和 ECharts option)
|
||
|
||
依赖方向为单向树形,无循环依赖
|
||
服务间通信方式:HTTP 内部调用(Docker 内网)
|
||
```
|
||
|
||
## 三、每个微服务内部的 Clean Architecture 分层
|
||
|
||
```
|
||
┌─────────────────────────────────────────────┐
|
||
│ Infrastructure(基础设施层) │
|
||
│ FastAPI Routes · SQLAlchemy · 外部服务客户端 │
|
||
├─────────────────────────────────────────────┤
|
||
│ Interface Adapters(适配层) │
|
||
│ Controllers · Repositories Impl · Presenters│
|
||
├─────────────────────────────────────────────┤
|
||
│ Application(应用层) │
|
||
│ Use Cases · Input/Output Ports · DTOs │
|
||
├─────────────────────────────────────────────┤
|
||
│ Domain(领域层) │
|
||
│ Entities · Value Objects · Domain Services │
|
||
└─────────────────────────────────────────────┘
|
||
|
||
依赖方向:外层 → 内层(绝对不可反向)
|
||
```
|
||
|
||
## 四、Monorepo 目录结构
|
||
|
||
```
|
||
backend/
|
||
├── docker-compose.yml # 一键编排所有服务 + 数据库
|
||
├── pyproject.toml # Monorepo 根配置
|
||
├── shared/ # 跨服务共享代码
|
||
│ ├── __init__.py
|
||
│ ├── base_entity.py # 实体基类(id, created_at, updated_at)
|
||
│ ├── base_repository.py # 仓储接口基类
|
||
│ ├── base_use_case.py # 用例接口基类
|
||
│ ├── exceptions.py # 公共异常定义
|
||
│ ├── types.py # 公共类型(FieldType, ChartType 等枚举)
|
||
│ └── http_client.py # 服务间 HTTP 调用客户端
|
||
│
|
||
├── services/
|
||
│ ├── data-service/ # ========== 数据服务 ==========
|
||
│ │ ├── Dockerfile
|
||
│ │ ├── pyproject.toml
|
||
│ │ ├── alembic/ # 数据库迁移
|
||
│ │ │ ├── alembic.ini
|
||
│ │ │ └── versions/
|
||
│ │ │
|
||
│ │ └── src/
|
||
│ │ ├── domain/ # 领域层
|
||
│ │ │ ├── entities/
|
||
│ │ │ │ ├── dataset.py # DataSet 聚合根
|
||
│ │ │ │ ├── column.py # Column 实体
|
||
│ │ │ │ └── data_row.py # DataRow 值对象
|
||
│ │ │ ├── value_objects/
|
||
│ │ │ │ ├── field_type.py # FieldType 枚举
|
||
│ │ │ │ ├── file_format.py # FileFormat 枚举
|
||
│ │ │ │ └── data_structure.py # DataStructure 枚举
|
||
│ │ │ ├── services/
|
||
│ │ │ │ ├── field_type_inference.py # 字段类型推断规则
|
||
│ │ │ │ └── data_structure_inference.py # 数据结构推断规则
|
||
│ │ │ └── repositories/
|
||
│ │ │ └── dataset_repository.py # 仓储接口(抽象类)
|
||
│ │ │
|
||
│ │ ├── application/ # 应用层
|
||
│ │ │ ├── ports/
|
||
│ │ │ │ ├── input/
|
||
│ │ │ │ │ ├── import_data.py # 导入数据用例接口
|
||
│ │ │ │ │ ├── get_dataset.py # 获取数据集用例接口
|
||
│ │ │ │ │ └── list_datasets.py # 列表数据集用例接口
|
||
│ │ │ │ └── output/
|
||
│ │ │ │ └── file_parser.py # 文件解析端口接口
|
||
│ │ │ ├── usecases/
|
||
│ │ │ │ ├── import_data_usecase.py # 导入数据用例实现
|
||
│ │ │ │ ├── get_dataset_usecase.py # 获取数据集
|
||
│ │ │ │ ├── list_datasets_usecase.py # 列表数据集
|
||
│ │ │ │ └── delete_dataset_usecase.py # 删除数据集
|
||
│ │ │ └── dto/
|
||
│ │ │ ├── import_result.py
|
||
│ │ │ └── dataset_response.py
|
||
│ │ │
|
||
│ │ ├── adapters/ # 适配层
|
||
│ │ │ ├── persistence/
|
||
│ │ │ │ ├── models.py # SQLAlchemy ORM 模型
|
||
│ │ │ │ ├── dataset_repository_impl.py # 仓储实现
|
||
│ │ │ │ └── database.py # 数据库连接配置
|
||
│ │ │ ├── parsers/
|
||
│ │ │ │ ├── xlsx_parser.py # Excel 解析器
|
||
│ │ │ │ ├── csv_parser.py # CSV 解析器
|
||
│ │ │ │ ├── json_parser.py # JSON 解析器
|
||
│ │ │ │ └── parser_factory.py # 解析器工厂
|
||
│ │ │ └── presenters/
|
||
│ │ │ └── dataset_presenter.py # Entity → API Response
|
||
│ │ │
|
||
│ │ └── infrastructure/ # 基础设施层
|
||
│ │ ├── api/
|
||
│ │ │ ├── app.py # FastAPI 应用实例
|
||
│ │ │ ├── routes.py # 路由定义
|
||
│ │ │ └── dependencies.py # 依赖注入
|
||
│ │ ├── config.py # 配置(环境变量)
|
||
│ │ └── main.py # 入口 uvicorn
|
||
│ │
|
||
│ ├── chart-service/ # ========== 图表服务 ==========
|
||
│ │ ├── Dockerfile
|
||
│ │ ├── pyproject.toml
|
||
│ │ ├── alembic/
|
||
│ │ │
|
||
│ │ └── src/
|
||
│ │ ├── domain/
|
||
│ │ │ ├── entities/
|
||
│ │ │ │ ├── chart_instance.py # ChartInstance 聚合根
|
||
│ │ │ │ ├── field_binding.py # FieldBinding 实体
|
||
│ │ │ │ └── style_config.py # StyleConfig 值对象
|
||
│ │ │ ├── value_objects/
|
||
│ │ │ │ └── chart_type.py # ChartType 枚举 + 元数据
|
||
│ │ │ ├── services/
|
||
│ │ │ │ ├── chart_recommendation.py # 图表推荐规则
|
||
│ │ │ │ ├── binding_validation.py # 绑定合法性校验
|
||
│ │ │ │ └── option_builder.py # ECharts option 生成(领域规则)
|
||
│ │ │ └── repositories/
|
||
│ │ │ └── chart_repository.py
|
||
│ │ │
|
||
│ │ ├── application/
|
||
│ │ │ ├── ports/
|
||
│ │ │ │ ├── input/
|
||
│ │ │ │ │ ├── create_chart.py
|
||
│ │ │ │ │ ├── update_chart.py
|
||
│ │ │ │ │ ├── get_chart_option.py # 获取 ECharts option
|
||
│ │ │ │ │ └── recommend_charts.py # 推荐图表
|
||
│ │ │ │ └── output/
|
||
│ │ │ │ └── data_service_client.py # 调用 data-service 的端口
|
||
│ │ │ ├── usecases/
|
||
│ │ │ │ ├── create_chart_usecase.py
|
||
│ │ │ │ ├── update_chart_usecase.py
|
||
│ │ │ │ ├── get_chart_option_usecase.py
|
||
│ │ │ │ └── recommend_charts_usecase.py
|
||
│ │ │ └── dto/
|
||
│ │ │ ├── chart_response.py
|
||
│ │ │ └── echarts_option.py
|
||
│ │ │
|
||
│ │ ├── adapters/
|
||
│ │ │ ├── persistence/
|
||
│ │ │ │ ├── models.py
|
||
│ │ │ │ ├── chart_repository_impl.py
|
||
│ │ │ │ └── database.py
|
||
│ │ │ ├── clients/
|
||
│ │ │ │ └── data_service_client_impl.py # HTTP 调用 data-service
|
||
│ │ │ ├── option_builders/ # ECharts option 适配
|
||
│ │ │ │ ├── bar_builder.py
|
||
│ │ │ │ ├── line_builder.py
|
||
│ │ │ │ ├── pie_builder.py
|
||
│ │ │ │ ├── scatter_builder.py
|
||
│ │ │ │ ├── radar_builder.py
|
||
│ │ │ │ ├── heatmap_builder.py
|
||
│ │ │ │ ├── map_builder.py
|
||
│ │ │ │ ├── wordcloud_builder.py
|
||
│ │ │ │ ├── combo_builder.py
|
||
│ │ │ │ └── builder_factory.py
|
||
│ │ │ └── presenters/
|
||
│ │ │ └── chart_presenter.py
|
||
│ │ │
|
||
│ │ └── infrastructure/
|
||
│ │ ├── api/
|
||
│ │ │ ├── app.py
|
||
│ │ │ ├── routes.py
|
||
│ │ │ └── dependencies.py
|
||
│ │ ├── config.py
|
||
│ │ └── main.py
|
||
│ │
|
||
│ ├── template-service/ # ========== 模板服务 ==========
|
||
│ │ ├── Dockerfile
|
||
│ │ ├── pyproject.toml
|
||
│ │ ├── alembic/
|
||
│ │ │
|
||
│ │ └── src/
|
||
│ │ ├── domain/
|
||
│ │ │ ├── entities/
|
||
│ │ │ │ └── template.py # Template 聚合根
|
||
│ │ │ ├── value_objects/
|
||
│ │ │ │ └── template_type.py # 内置/自定义
|
||
│ │ │ ├── services/
|
||
│ │ │ │ └── template_validation.py # 模板合法性校验
|
||
│ │ │ └── repositories/
|
||
│ │ │ └── template_repository.py
|
||
│ │ │
|
||
│ │ ├── application/
|
||
│ │ │ ├── ports/
|
||
│ │ │ │ ├── input/
|
||
│ │ │ │ │ ├── save_template.py
|
||
│ │ │ │ │ ├── load_template.py
|
||
│ │ │ │ │ ├── list_templates.py
|
||
│ │ │ │ │ └── import_export_template.py
|
||
│ │ │ │ └── output/
|
||
│ │ │ │ └── template_storage.py # 持久化端口
|
||
│ │ │ ├── usecases/
|
||
│ │ │ │ ├── save_template_usecase.py
|
||
│ │ │ │ ├── load_template_usecase.py
|
||
│ │ │ │ ├── list_templates_usecase.py
|
||
│ │ │ │ ├── delete_template_usecase.py
|
||
│ │ │ │ └── import_export_usecase.py
|
||
│ │ │ └── dto/
|
||
│ │ │ └── template_response.py
|
||
│ │ │
|
||
│ │ ├── adapters/
|
||
│ │ │ ├── persistence/
|
||
│ │ │ │ ├── models.py
|
||
│ │ │ │ ├── template_repository_impl.py
|
||
│ │ │ │ └── database.py
|
||
│ │ │ └── presenters/
|
||
│ │ │ └── template_presenter.py
|
||
│ │ │
|
||
│ │ └── infrastructure/
|
||
│ │ ├── api/
|
||
│ │ │ ├── app.py
|
||
│ │ │ ├── routes.py
|
||
│ │ │ └── dependencies.py
|
||
│ │ ├── config.py
|
||
│ │ ├── seed/ # 内置模板种子数据
|
||
│ │ │ └── builtin_templates.json
|
||
│ │ └── main.py
|
||
│ │
|
||
│ └── export-service/ # ========== 导出服务 ==========
|
||
│ ├── Dockerfile
|
||
│ ├── pyproject.toml
|
||
│ ├── alembic/
|
||
│ │
|
||
│ └── src/
|
||
│ ├── domain/
|
||
│ │ ├── entities/
|
||
│ │ │ └── export_task.py # ExportTask 聚合根
|
||
│ │ ├── value_objects/
|
||
│ │ │ ├── export_format.py # PNG/PDF/PPT/Excel/HTML
|
||
│ │ │ └── export_status.py # pending/processing/done/failed
|
||
│ │ ├── services/
|
||
│ │ │ └── export_strategy.py # 导出策略选择规则
|
||
│ │ └── repositories/
|
||
│ │ └── export_task_repository.py
|
||
│ │
|
||
│ ├── application/
|
||
│ │ ├── ports/
|
||
│ │ │ ├── input/
|
||
│ │ │ │ ├── create_export.py
|
||
│ │ │ │ └── get_export_status.py
|
||
│ │ │ └── output/
|
||
│ │ │ ├── data_service_client.py
|
||
│ │ │ ├── chart_service_client.py
|
||
│ │ │ ├── image_renderer.py # 图片渲染端口
|
||
│ │ │ ├── pdf_generator.py # PDF 生成端口
|
||
│ │ │ ├── ppt_generator.py # PPT 生成端口
|
||
│ │ │ ├── excel_generator.py # Excel 生成端口
|
||
│ │ │ └── html_generator.py # HTML 生成端口
|
||
│ │ ├── usecases/
|
||
│ │ │ ├── create_export_usecase.py
|
||
│ │ │ └── get_export_status_usecase.py
|
||
│ │ └── dto/
|
||
│ │ ├── export_request.py
|
||
│ │ └── export_response.py
|
||
│ │
|
||
│ ├── adapters/
|
||
│ │ ├── persistence/
|
||
│ │ │ ├── models.py
|
||
│ │ │ ├── export_task_repository_impl.py
|
||
│ │ │ └── database.py
|
||
│ │ ├── clients/
|
||
│ │ │ ├── data_service_client_impl.py
|
||
│ │ │ └── chart_service_client_impl.py
|
||
│ │ ├── generators/
|
||
│ │ │ ├── playwright_image_renderer.py # Playwright 截图
|
||
│ │ │ ├── weasyprint_pdf_generator.py # PDF
|
||
│ │ │ ├── pptx_generator.py # PPT
|
||
│ │ │ ├── openpyxl_excel_generator.py # Excel
|
||
│ │ │ └── jinja2_html_generator.py # HTML
|
||
│ │ └── presenters/
|
||
│ │ └── export_presenter.py
|
||
│ │
|
||
│ └── infrastructure/
|
||
│ ├── api/
|
||
│ │ ├── app.py
|
||
│ │ ├── routes.py
|
||
│ │ └── dependencies.py
|
||
│ ├── config.py
|
||
│ └── main.py
|
||
│
|
||
└── tests/ # 测试(镜像 services 结构)
|
||
├── data-service/
|
||
│ ├── unit/
|
||
│ │ ├── domain/
|
||
│ │ └── application/
|
||
│ └── integration/
|
||
├── chart-service/
|
||
│ ├── unit/
|
||
│ └── integration/
|
||
├── template-service/
|
||
│ ├── unit/
|
||
│ └── integration/
|
||
└── export-service/
|
||
├── unit/
|
||
└── integration/
|
||
```
|
||
|
||
## 五、DDD 领域模型
|
||
|
||
### 限界上下文映射
|
||
|
||
```
|
||
┌─────────────────┐ ┌─────────────────┐
|
||
│ Data Context │ │ Chart Context │
|
||
│ │ │ │
|
||
│ DataSet (聚合根) │────→│ ChartInstance │
|
||
│ Column │ 引用 │ (聚合根) │
|
||
│ DataRow │ ID │ FieldBinding │
|
||
│ │ │ StyleConfig │
|
||
└─────────────────┘ └─────────────────┘
|
||
│
|
||
┌─────────────────┐ │ 引用 ID
|
||
│Template Context │ │
|
||
│ │ │
|
||
│ Template (聚合根) │←──┘
|
||
│ │
|
||
└─────────────────┘
|
||
┌─────────────────┐
|
||
│ Export Context │
|
||
│ │
|
||
│ ExportTask (聚合根)│
|
||
│ │
|
||
└─────────────────┘
|
||
引用 DataSet ID
|
||
引用 ChartInstance ID
|
||
```
|
||
|
||
### 聚合根设计
|
||
|
||
```python
|
||
# data-service: DataSet 聚合根
|
||
class DataSet:
|
||
id: UUID
|
||
file_name: str
|
||
sheet_name: Optional[str]
|
||
columns: List[Column] # 聚合内实体
|
||
row_count: int
|
||
data_structure: DataStructure # 推断出的数据结构类型
|
||
created_at: datetime
|
||
updated_at: datetime
|
||
|
||
def infer_field_types(self) -> None: ...
|
||
def infer_data_structure(self) -> None: ...
|
||
def get_column_by_name(self, name: str) -> Column: ...
|
||
def validate(self) -> None: ...
|
||
|
||
# chart-service: ChartInstance 聚合根
|
||
class ChartInstance:
|
||
id: UUID
|
||
dataset_id: UUID # 引用,非嵌套
|
||
chart_type: ChartType
|
||
bindings: List[FieldBinding] # 聚合内实体
|
||
style: StyleConfig # 聚合内值对象
|
||
filters: List[FilterRule]
|
||
sort: Optional[SortConfig]
|
||
top_n: Optional[int]
|
||
created_at: datetime
|
||
updated_at: datetime
|
||
|
||
def update_bindings(self, bindings: List[FieldBinding]) -> None: ...
|
||
def update_style(self, style: StyleConfig) -> None: ...
|
||
def validate_bindings(self) -> None: ...
|
||
|
||
# template-service: Template 聚合根
|
||
class Template:
|
||
id: UUID
|
||
name: str
|
||
description: str
|
||
template_type: TemplateType # builtin / custom
|
||
chart_configs: List[dict] # 图表配置 JSON
|
||
layout: List[dict] # 布局配置 JSON
|
||
theme: str
|
||
created_at: datetime
|
||
updated_at: datetime
|
||
|
||
def validate(self) -> None: ...
|
||
def to_export_json(self) -> dict: ...
|
||
@classmethod
|
||
def from_import_json(cls, data: dict) -> 'Template': ...
|
||
|
||
# export-service: ExportTask 聚合根
|
||
class ExportTask:
|
||
id: UUID
|
||
format: ExportFormat
|
||
chart_ids: List[UUID]
|
||
dataset_id: UUID
|
||
status: ExportStatus # pending → processing → done / failed
|
||
file_path: Optional[str]
|
||
error_message: Optional[str]
|
||
created_at: datetime
|
||
completed_at: Optional[datetime]
|
||
|
||
def start_processing(self) -> None: ...
|
||
def complete(self, file_path: str) -> None: ...
|
||
def fail(self, error: str) -> None: ...
|
||
```
|
||
|
||
## 六、Clean Architecture 依赖方向
|
||
|
||
以 data-service 为例:
|
||
|
||
```
|
||
infrastructure/ adapters/ application/ domain/
|
||
(FastAPI, SQLAlchemy) (实现层) (用例层) (领域层)
|
||
|
||
routes.py ──→ dependencies.py ──→ usecases/ ──→ entities/
|
||
│ │ │
|
||
│ │ value_objects/
|
||
│ │ │
|
||
│ │ services/
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ports/input │
|
||
│ ports/output ←────┘
|
||
│ ▲
|
||
│ │(实现接口)
|
||
├──→ persistence/ │
|
||
├──→ parsers/ │
|
||
└──→ presenters/
|
||
|
||
import 方向全部从外向内
|
||
ports/output 定义接口在 application 层,实现在 adapters 层(依赖反转)
|
||
```
|
||
|
||
## 七、依赖注入
|
||
|
||
```python
|
||
# infrastructure/api/dependencies.py
|
||
# 组装所有端口实现,注入到 Use Case
|
||
|
||
from adapters.persistence.database import get_session
|
||
from adapters.persistence.dataset_repository_impl import DataSetRepositoryImpl
|
||
from adapters.parsers.parser_factory import ParserFactory
|
||
from application.usecases.import_data_usecase import ImportDataUseCase
|
||
|
||
async def get_import_data_usecase(
|
||
session: AsyncSession = Depends(get_session),
|
||
) -> ImportDataUseCase:
|
||
repository = DataSetRepositoryImpl(session)
|
||
file_parser = ParserFactory()
|
||
return ImportDataUseCase(
|
||
repository=repository,
|
||
file_parser=file_parser,
|
||
)
|
||
|
||
|
||
# infrastructure/api/routes.py
|
||
from fastapi import APIRouter, Depends, UploadFile
|
||
from infrastructure.api.dependencies import get_import_data_usecase
|
||
|
||
router = APIRouter()
|
||
|
||
@router.post("/datasets/import")
|
||
async def import_data(
|
||
file: UploadFile,
|
||
usecase: ImportDataUseCase = Depends(get_import_data_usecase),
|
||
):
|
||
result = await usecase.execute(file)
|
||
return DataSetPresenter.to_response(result)
|
||
```
|
||
|
||
## 八、API 设计
|
||
|
||
### data-service (:8001)
|
||
|
||
```
|
||
POST /api/v1/datasets/import 上传并解析文件
|
||
GET /api/v1/datasets 列出所有数据集
|
||
GET /api/v1/datasets/{id} 获取数据集详情(含列信息)
|
||
GET /api/v1/datasets/{id}/rows 获取数据行(分页)
|
||
PATCH /api/v1/datasets/{id}/rows/{row} 修改单元格
|
||
DELETE /api/v1/datasets/{id} 删除数据集
|
||
GET /api/v1/datasets/{id}/structure 获取推断的数据结构
|
||
```
|
||
|
||
### chart-service (:8002)
|
||
|
||
```
|
||
POST /api/v1/charts 创建图表实例
|
||
GET /api/v1/charts 列出所有图表
|
||
GET /api/v1/charts/{id} 获取图表配置
|
||
PUT /api/v1/charts/{id} 更新图表配置(绑定+样式)
|
||
DELETE /api/v1/charts/{id} 删除图表
|
||
GET /api/v1/charts/{id}/option 获取 ECharts option JSON
|
||
POST /api/v1/charts/recommend 根据数据集推荐图表类型
|
||
```
|
||
|
||
### template-service (:8003)
|
||
|
||
```
|
||
POST /api/v1/templates 保存模板
|
||
GET /api/v1/templates 列出所有模板(含内置)
|
||
GET /api/v1/templates/{id} 获取模板详情
|
||
PUT /api/v1/templates/{id} 更新模板
|
||
DELETE /api/v1/templates/{id} 删除模板
|
||
POST /api/v1/templates/import 导入模板 JSON
|
||
GET /api/v1/templates/{id}/export 导出模板 JSON
|
||
```
|
||
|
||
### export-service (:8004)
|
||
|
||
```
|
||
POST /api/v1/exports 创建导出任务
|
||
GET /api/v1/exports/{id} 查询导出状态
|
||
GET /api/v1/exports/{id}/download 下载导出文件
|
||
```
|
||
|
||
## 九、服务间通信
|
||
|
||
```python
|
||
# chart-service 调用 data-service 获取数据集
|
||
|
||
# application/ports/output/data_service_client.py(端口接口)
|
||
from abc import ABC, abstractmethod
|
||
|
||
class IDataServiceClient(ABC):
|
||
@abstractmethod
|
||
async def get_dataset(self, dataset_id: UUID) -> DataSetDTO: ...
|
||
|
||
@abstractmethod
|
||
async def get_rows(self, dataset_id: UUID, limit: int, offset: int) -> List[dict]: ...
|
||
|
||
|
||
# adapters/clients/data_service_client_impl.py(适配实现)
|
||
import httpx
|
||
|
||
class DataServiceClientImpl(IDataServiceClient):
|
||
def __init__(self, base_url: str = "http://data-service:8000"):
|
||
self.base_url = base_url
|
||
|
||
async def get_dataset(self, dataset_id: UUID) -> DataSetDTO:
|
||
async with httpx.AsyncClient() as client:
|
||
resp = await client.get(f"{self.base_url}/api/v1/datasets/{dataset_id}")
|
||
resp.raise_for_status()
|
||
return DataSetDTO(**resp.json())
|
||
|
||
async def get_rows(self, dataset_id: UUID, limit: int, offset: int) -> List[dict]:
|
||
async with httpx.AsyncClient() as client:
|
||
resp = await client.get(
|
||
f"{self.base_url}/api/v1/datasets/{dataset_id}/rows",
|
||
params={"limit": limit, "offset": offset},
|
||
)
|
||
resp.raise_for_status()
|
||
return resp.json()["rows"]
|
||
```
|
||
|
||
## 十、数据库设计
|
||
|
||
### data_db
|
||
|
||
```sql
|
||
CREATE TABLE datasets (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
file_name VARCHAR(255) NOT NULL,
|
||
sheet_name VARCHAR(255),
|
||
row_count INTEGER NOT NULL DEFAULT 0,
|
||
data_structure VARCHAR(50), -- single_dim / dual_dim / time_series / geo ...
|
||
raw_data JSONB NOT NULL, -- 原始行数据
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
CREATE TABLE columns (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
dataset_id UUID NOT NULL REFERENCES datasets(id) ON DELETE CASCADE,
|
||
name VARCHAR(255) NOT NULL,
|
||
field_type VARCHAR(50) NOT NULL, -- number / text / date / percentage / geo
|
||
sample_values JSONB,
|
||
ordinal INTEGER NOT NULL -- 列顺序
|
||
);
|
||
```
|
||
|
||
### chart_db
|
||
|
||
```sql
|
||
CREATE TABLE chart_instances (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
dataset_id UUID NOT NULL, -- 引用 data_db.datasets.id(跨库引用,不加外键)
|
||
chart_type VARCHAR(50) NOT NULL,
|
||
bindings JSONB NOT NULL, -- FieldBinding[]
|
||
style JSONB NOT NULL, -- StyleConfig
|
||
filters JSONB DEFAULT '[]',
|
||
sort_config JSONB,
|
||
top_n INTEGER,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
```
|
||
|
||
### template_db
|
||
|
||
```sql
|
||
CREATE TABLE templates (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
name VARCHAR(255) NOT NULL,
|
||
description TEXT,
|
||
template_type VARCHAR(20) NOT NULL DEFAULT 'custom', -- builtin / custom
|
||
chart_configs JSONB NOT NULL, -- 图表配置数组
|
||
layout JSONB NOT NULL, -- 布局配置数组
|
||
theme VARCHAR(50) NOT NULL DEFAULT 'light',
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
```
|
||
|
||
### export_db
|
||
|
||
```sql
|
||
CREATE TABLE export_tasks (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
format VARCHAR(20) NOT NULL, -- png / pdf / ppt / excel / html
|
||
chart_ids UUID[] NOT NULL,
|
||
dataset_id UUID NOT NULL,
|
||
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending / processing / done / failed
|
||
file_path VARCHAR(500),
|
||
error_message TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
completed_at TIMESTAMPTZ
|
||
);
|
||
```
|
||
|
||
## 十一、Docker Compose 编排
|
||
|
||
```yaml
|
||
# docker-compose.yml
|
||
version: "3.9"
|
||
|
||
services:
|
||
postgres:
|
||
image: postgres:16-alpine
|
||
environment:
|
||
POSTGRES_USER: dataviz
|
||
POSTGRES_PASSWORD: dataviz_local
|
||
ports:
|
||
- "5432:5432"
|
||
volumes:
|
||
- pgdata:/var/lib/postgresql/data
|
||
- ./init-databases.sql:/docker-entrypoint-initdb.d/init.sql
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U dataviz"]
|
||
interval: 5s
|
||
retries: 5
|
||
|
||
data-service:
|
||
build: ./services/data-service
|
||
ports:
|
||
- "8001:8000"
|
||
environment:
|
||
DATABASE_URL: postgresql+asyncpg://dataviz:dataviz_local@postgres:5432/data_db
|
||
depends_on:
|
||
postgres:
|
||
condition: service_healthy
|
||
|
||
chart-service:
|
||
build: ./services/chart-service
|
||
ports:
|
||
- "8002:8000"
|
||
environment:
|
||
DATABASE_URL: postgresql+asyncpg://dataviz:dataviz_local@postgres:5432/chart_db
|
||
DATA_SERVICE_URL: http://data-service:8000
|
||
depends_on:
|
||
postgres:
|
||
condition: service_healthy
|
||
data-service:
|
||
condition: service_started
|
||
|
||
template-service:
|
||
build: ./services/template-service
|
||
ports:
|
||
- "8003:8000"
|
||
environment:
|
||
DATABASE_URL: postgresql+asyncpg://dataviz:dataviz_local@postgres:5432/template_db
|
||
depends_on:
|
||
postgres:
|
||
condition: service_healthy
|
||
|
||
export-service:
|
||
build: ./services/export-service
|
||
ports:
|
||
- "8004:8000"
|
||
environment:
|
||
DATABASE_URL: postgresql+asyncpg://dataviz:dataviz_local@postgres:5432/export_db
|
||
DATA_SERVICE_URL: http://data-service:8000
|
||
CHART_SERVICE_URL: http://chart-service:8000
|
||
depends_on:
|
||
postgres:
|
||
condition: service_healthy
|
||
data-service:
|
||
condition: service_started
|
||
chart-service:
|
||
condition: service_started
|
||
|
||
volumes:
|
||
pgdata:
|
||
```
|
||
|
||
```sql
|
||
-- init-databases.sql
|
||
CREATE DATABASE data_db;
|
||
CREATE DATABASE chart_db;
|
||
CREATE DATABASE template_db;
|
||
CREATE DATABASE export_db;
|
||
```
|
||
|
||
## 十二、本地开发启动
|
||
|
||
```bash
|
||
# 一键启动所有服务
|
||
docker-compose up --build
|
||
|
||
# 服务地址
|
||
# data-service → http://localhost:8001/docs
|
||
# chart-service → http://localhost:8002/docs
|
||
# template-service → http://localhost:8003/docs
|
||
# export-service → http://localhost:8004/docs
|
||
|
||
# 单独重启某个服务(开发时)
|
||
docker-compose restart chart-service
|
||
|
||
# 查看日志
|
||
docker-compose logs -f export-service
|
||
```
|
||
|
||
## 十三、前端对接配置
|
||
|
||
```typescript
|
||
// 前端服务地址枚举
|
||
enum ServiceURL {
|
||
Data = 'http://localhost:8001',
|
||
Chart = 'http://localhost:8002',
|
||
Template = 'http://localhost:8003',
|
||
Export = 'http://localhost:8004',
|
||
}
|
||
|
||
// 示例调用
|
||
const response = await fetch(`${ServiceURL.Data}/api/v1/datasets/import`, {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
```
|