dv/docs/backend-architecture-guide.md

33 KiB
Raw Permalink Blame History

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

聚合根设计

# 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 层(依赖反转)

七、依赖注入

# 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     下载导出文件

九、服务间通信

# 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

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

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

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

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 编排

# 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:
-- init-databases.sql
CREATE DATABASE data_db;
CREATE DATABASE chart_db;
CREATE DATABASE template_db;
CREATE DATABASE export_db;

十二、本地开发启动

# 一键启动所有服务
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

十三、前端对接配置

// 前端服务地址枚举
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,
});