目录
1. 项目结构概述
为了保持项目的模块化和可维护性,建议将 API 相关的代码组织在 api
目录下,并将路由分散到不同的文件中。以下是一个建议的项目结构示例:
src/
├── backend/
│ ├── bisheng/
│ │ ├── api/
│ │ │ ├── __init__.py
│ │ │ └── flow_router.py
│ │ ├── autobuild/
│ │ │ ├── __init__.py
│ │ │ ├── manager.py
│ │ │ └── utils.py
│ │ ├── cache/
│ │ ├── chat/
│ │ ├── database/
│ │ │ ├── models/
│ │ │ │ ├── __init__.py
│ │ │ │ └── flow.py
│ │ │ ├── base.py
│ │ ├── processing/
│ │ ├── utils/
│ │ ├── main.py
│ │ └── requirements.txt
在这个结构中,flow_router.py
将包含与 Flow
相关的 API 路由。
2. 定义 API 模型
首先,确保您的 FlowCreate
模型在 Pydantic 中正确定义,以便于 FastAPI 进行数据验证和序列化。根据您提供的 FlowCreate
模型,您可能需要在 flow.py
中进行如下定义:
src/backend/bisheng/database/models/flow.py
from datetime import datetime
from enum import Enum
from typing import Dict, List, Optional
from uuid import UUID, uuid4
from pydantic import BaseModel, validator
from sqlmodel import Field, SQLModel
from sqlalchemy import Column, DateTime, String, text
class FlowStatus(Enum):
OFFLINE = 1
ONLINE = 2
class FlowType(Enum):
FLOW = 1
ASSISTANT = 5
WORKFLOW = 10
class FlowBase(BaseModel):
name: str
user_id: Optional[int]
description: Optional[str]
data: Optional[Dict] = None
logo: Optional[str]
status: Optional[int] = 1
flow_type: Optional[int] = 1
update_time: Optional[datetime] = Field(
default=None,
sa_column=Column(
DateTime,
nullable=True,
server_default=text('CURRENT_TIMESTAMP'),
onupdate=text('CURRENT_TIMESTAMP')
)
)
create_time: Optional[datetime] = Field(
default=None,
sa_column=Column(
DateTime, nullable=False, index=True, server_default=text('CURRENT_TIMESTAMP')
)
)
guide_word: Optional[str] = Field(
default=None,
sa_column=Column(String(length=1000))
)
@validator('data')
def validate_json(cls, v):
if not v:
return v
if not isinstance(v, dict):
raise ValueError('Flow must be a valid JSON')
if 'nodes' not in v or 'edges' not in v:
raise ValueError('Flow must have nodes and edges')
return v
class FlowCreate(FlowBase):
flow_id: Optional[UUID] = None
# 其他模型定义保持不变
注意:
- 使用
BaseModel
作为FlowBase
的基类,以便 Pydantic 进行数据验证。 - 确保
FlowCreate
包含所有必要的字段。
3. 创建路由和端点
在 api
目录下创建一个新的路由文件 flow_router.py
,其中包含读取 JSON 文件并实例化 FlowCreate
对象的 API 端点。
src/backend/bisheng/api/flow_router.py
from fastapi import APIRouter, HTTPException, Depends
from pydantic import ValidationError
from typing import List
import json
import os
from uuid import UUID
from bisheng.database.models.flow import FlowCreate
from bisheng.api.services.user_service import UserPayload # 假设有用户服务
from bisheng.auth import get_current_user # 假设有用户认证依赖
router = APIRouter()
@router.get("/flows/from_json", response_model=List[FlowCreate])
async def get_flows_from_json(
file_path: str = "flows.json",
user: UserPayload = Depends(get_current_user)
):
"""
读取本地 JSON 文件并将其中的信息实例化为 FlowCreate 对象。
- **file_path**: 本地 JSON 文件的路径,默认为项目根目录下的 flows.json
- **user**: 当前登录用户的信息
"""
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail=f"File {file_path} not found.")
try:
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
except json.JSONDecodeError as e:
raise HTTPException(status_code=400, detail=f"Invalid JSON format: {e}")
if not isinstance(data, list):
raise HTTPException(status_code=400, detail="JSON data must be a list of flows.")
flows = []
for item in data:
try:
flow_create = FlowCreate(**item)
flows.append(flow_create)
except ValidationError as ve:
raise HTTPException(status_code=422, detail=f"Validation error: {ve}")
return flows
说明:
-
路由定义:
- 路径:
/flows/from_json
- 方法:
GET
- 返回类型:
List[FlowCreate]
- 依赖:用户认证(假设使用
get_current_user
进行认证)
- 路径:
-
参数:
file_path
:可选参数,指定 JSON 文件的路径,默认为项目根目录下的flows.json
-
逻辑流程:
- 检查文件是否存在。
- 读取并解析 JSON 文件。
- 验证 JSON 数据格式(应为列表)。
- 将每个 JSON 对象实例化为
FlowCreate
对象,处理验证错误。 - 返回
FlowCreate
对象列表。
-
错误处理:
- 文件不存在:返回
404 Not Found
- JSON 格式错误:返回
400 Bad Request
- 数据验证错误:返回
422 Unprocessable Entity
- 文件不存在:返回
4. 实现 JSON 读取和实例化逻辑
上述代码已经涵盖了读取 JSON 文件和实例化 FlowCreate
对象的逻辑。以下是对关键部分的详细解释:
读取 JSON 文件
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
- 使用
open
函数以读取模式打开指定路径的 JSON 文件。 - 使用
json.load
解析文件内容。
验证 JSON 数据格式
if not isinstance(data, list):
raise HTTPException(status_code=400, detail="JSON data must be a list of flows.")
- 确保 JSON 数据是一个列表,每个元素代表一个
Flow
对象。
实例化 FlowCreate
对象
flows = []
for item in data:
try:
flow_create = FlowCreate(**item)
flows.append(flow_create)
except ValidationError as ve:
raise HTTPException(status_code=422, detail=f"Validation error: {ve}")
- 遍历 JSON 数据列表。
- 使用 Pydantic 的模型验证功能,将每个字典实例化为
FlowCreate
对象。 - 如果验证失败,抛出
422 Unprocessable Entity
错误。
5. 集成到 FastAPI 应用
在 main.py
中集成新的路由,并确保所有依赖项正确导入。
src/backend/bisheng/main.py
from fastapi import FastAPI
from bisheng.api.flow_router import router as flow_router
app = FastAPI(
title="Bisheng API",
description="API for managing flows and other functionalities.",
version="1.0.0"
)
# 包含其他路由
app.include_router(flow_router, prefix="/api", tags=["Flows"])
# 其他初始化代码,如事件处理器
@app.on_event("startup")
async def startup_event():
# 初始化缓存管理器、数据库连接等
pass
@app.on_event("shutdown")
async def shutdown_event():
# 关闭缓存管理器、数据库连接等
pass
说明:
- 使用
include_router
将flow_router
集成到 FastAPI 应用中,前缀为/api
,标签为Flows
。 - 根据项目需求,添加其他路由和初始化逻辑。
6. 示例 JSON 文件
为了测试 API 端点,您需要准备一个符合 FlowCreate
模型结构的 JSON 文件。以下是一个示例:
flows.json
[
{
"name": "Flow 1",
"user_id": 123,
"description": "Description for Flow 1",
"data": {
"nodes": [
{"id": "node1", "type": "start"},
{"id": "node2", "type": "process"}
],
"edges": [
{"source": "node1", "target": "node2"}
]
},
"logo": "path/to/logo1.png",
"status": 2,
"flow_type": 1,
"guide_word": "Guide for Flow 1"
},
{
"name": "Flow 2",
"user_id": 456,
"description": "Description for Flow 2",
"data": {
"nodes": [
{"id": "nodeA", "type": "start"},
{"id": "nodeB", "type": "end"}
],
"edges": [
{"source": "nodeA", "target": "nodeB"}
]
},
"logo": "path/to/logo2.png",
"status": 1,
"flow_type": 5,
"guide_word": "Guide for Flow 2"
}
]
说明:
- 每个对象代表一个
FlowCreate
实例。 data
字段包含nodes
和edges
,符合FlowBase
的验证要求。
7. 测试与调试
7.1. 运行 FastAPI 应用
确保您已经安装了所有依赖项,然后运行 FastAPI 应用:
uvicorn bisheng.main:app --reload
7.2. 使用 Swagger UI 测试 API
FastAPI 自动生成了 Swagger UI,可以通过访问 http://localhost:8000/docs
进行测试。
- 打开浏览器,访问
http://localhost:8000/docs
- 找到
/api/flows/from_json
端点 - 点击
GET
按钮,输入file_path
参数(如果使用默认值,可以留空) - 点击
Execute
查看响应
7.3. 使用命令行工具测试
您也可以使用 curl
或其他命令行工具进行测试:
curl -X GET "http://localhost:8000/api/flows/from_json" -H "accept: application/json"
如果需要指定文件路径:
curl -X GET "http://localhost:8000/api/flows/from_json?file_path=path/to/your_flows.json" -H "accept: application/json"
7.4. 单元测试
使用 pytest
编写单元测试,确保 API 端点按预期工作。
安装 pytest
和 httpx
:
pip install pytest httpx
创建测试文件 test_flow_router.py
src/backend/bisheng/api/test_flow_router.py
import pytest
from fastapi.testclient import TestClient
from bisheng.main import app
from unittest.mock import patch
import json
client = TestClient(app)
@pytest.fixture
def sample_flows_json(tmp_path):
data = [
{
"name": "Test Flow 1",
"user_id": 1,
"description": "A test flow",
"data": {
"nodes": [{"id": "n1", "type": "start"}, {"id": "n2", "type": "end"}],
"edges": [{"source": "n1", "target": "n2"}]
},
"logo": "logo1.png",
"status": 1,
"flow_type": 1,
"guide_word": "Guide 1"
}
]
file = tmp_path / "test_flows.json"
with open(file, "w", encoding="utf-8") as f:
json.dump(data, f)
return str(file)
@patch("builtins.open")
def test_get_flows_from_json_success(mock_open, sample_flows_json):
# Mock the open function to read the sample JSON data
mock_open.return_value.__enter__.return_value.read.return_value = json.dumps([
{
"name": "Test Flow 1",
"user_id": 1,
"description": "A test flow",
"data": {
"nodes": [{"id": "n1", "type": "start"}, {"id": "n2", "type": "end"}],
"edges": [{"source": "n1", "target": "n2"}]
},
"logo": "logo1.png",
"status": 1,
"flow_type": 1,
"guide_word": "Guide 1"
}
])
# Mock user authentication
with patch("bisheng.api.flow_router.get_current_user") as mock_user:
mock_user.return_value = {
"user_id": 1,
"user_name": "test_user"
}
response = client.get("/api/flows/from_json", params={"file_path": sample_flows_json})
assert response.status_code == 200
flows = response.json()
assert len(flows) == 1
assert flows[0]["name"] == "Test Flow 1"
assert flows[0]["user_id"] == 1
def test_get_flows_from_json_file_not_found():
with patch("os.path.exists") as mock_exists:
mock_exists.return_value = False
response = client.get("/api/flows/from_json", params={"file_path": "non_existent.json"})
assert response.status_code == 404
assert response.json() == {"detail": "File non_existent.json not found."}
@patch("builtins.open")
def test_get_flows_from_json_invalid_json(mock_open):
# Mock the open function to return invalid JSON
mock_open.return_value.__enter__.return_value.read.return_value = "Invalid JSON"
with patch("bisheng.api.flow_router.get_current_user") as mock_user:
mock_user.return_value = {
"user_id": 1,
"user_name": "test_user"
}
response = client.get("/api/flows/from_json", params={"file_path": "flows.json"})
assert response.status_code == 400
assert "Invalid JSON format" in response.json()["detail"]
@patch("builtins.open")
def test_get_flows_from_json_validation_error(mock_open):
# Mock the open function to return JSON with missing required fields
mock_open.return_value.__enter__.return_value.read.return_value = json.dumps([
{
"user_id": 1,
"description": "A test flow without a name",
"data": {
"nodes": [{"id": "n1", "type": "start"}, {"id": "n2", "type": "end"}],
"edges": [{"source": "n1", "target": "n2"}]
},
"logo": "logo1.png",
"status": 1,
"flow_type": 1,
"guide_word": "Guide 1"
}
])
with patch("bisheng.api.flow_router.get_current_user") as mock_user:
mock_user.return_value = {
"user_id": 1,
"user_name": "test_user"
}
response = client.get("/api/flows/from_json", params={"file_path": "flows.json"})
assert response.status_code == 422
assert "Validation error" in response.json()["detail"][0]["msg"]
说明:
sample_flows_json
:创建一个临时的 JSON 文件供测试使用。test_get_flows_from_json_success
:测试成功读取并实例化FlowCreate
对象。test_get_flows_from_json_file_not_found
:测试文件不存在时的错误处理。test_get_flows_from_json_invalid_json
:测试无效 JSON 格式时的错误处理。test_get_flows_from_json_validation_error
:测试数据验证错误时的错误处理。
运行测试:
pytest src/backend/bisheng/api/test_flow_router.py
8. 总结与优化建议
8.1. 模块化与代码组织
- 独立路由:将不同功能的路由组织在不同的文件中,如
flow_router.py
专门处理与Flow
相关的 API。 - 服务与依赖:将业务逻辑与路由分离,使用服务层(如
FlowDao
)进行数据库操作,保持路由的简洁。
8.2. 错误处理
- 详细错误信息:提供明确的错误信息,帮助前端更好地处理错误。
- 日志记录:在异常处理部分记录详细的日志,便于调试和监控。
8.3. 安全性
- 认证与授权:确保所有 API 端点都进行适当的用户认证和授权,防止未授权访问。
- 输入验证:使用 Pydantic 模型进行严格的输入验证,防止恶意数据注入。
8.4. 性能优化
- 异步操作:确保所有 I/O 操作(如文件读取)都采用异步方法,以提升性能。
- 缓存机制:对于频繁读取的 JSON 文件,可以考虑使用缓存机制,减少磁盘 I/O。
8.5. 可扩展性
- 动态文件路径:允许客户端指定 JSON 文件路径,但需确保路径安全,防止路径遍历攻击。
- 分页与过滤:对于返回大量数据的情况,考虑添加分页和过滤功能,提升响应效率。
8.6. 文档与测试
- 自动文档:利用 FastAPI 的自动文档功能,确保 API 文档实时更新,便于开发和使用。
- 全面测试:编写全面的单元测试和集成测试,确保 API 端点的可靠性和稳定性。
完整代码示例
以下是将上述各部分集成在一起的完整代码示例,帮助您快速实现所需功能。
src/backend/bisheng/database/models/flow.py
from datetime import datetime
from enum import Enum
from typing import Dict, Optional
from uuid import UUID, uuid4
from pydantic import BaseModel, validator
from sqlmodel import Field, SQLModel
from sqlalchemy import Column, DateTime, String, text
class FlowStatus(Enum):
OFFLINE = 1
ONLINE = 2
class FlowType(Enum):
FLOW = 1
ASSISTANT = 5
WORKFLOW = 10
class FlowBase(BaseModel):
name: str
user_id: Optional[int]
description: Optional[str]
data: Optional[Dict] = None
logo: Optional[str]
status: Optional[int] = 1
flow_type: Optional[int] = 1
update_time: Optional[datetime] = Field(
default=None,
sa_column=Column(
DateTime,
nullable=True,
server_default=text('CURRENT_TIMESTAMP'),
onupdate=text('CURRENT_TIMESTAMP')
)
)
create_time: Optional[datetime] = Field(
default=None,
sa_column=Column(
DateTime, nullable=False, index=True, server_default=text('CURRENT_TIMESTAMP')
)
)
guide_word: Optional[str] = Field(
default=None,
sa_column=Column(String(length=1000))
)
@validator('data')
def validate_json(cls, v):
if not v:
return v
if not isinstance(v, dict):
raise ValueError('Flow must be a valid JSON')
if 'nodes' not in v or 'edges' not in v:
raise ValueError('Flow must have nodes and edges')
return v
class FlowCreate(FlowBase):
flow_id: Optional[UUID] = None
# 其他模型定义保持不变
src/backend/bisheng/api/flow_router.py
from fastapi import APIRouter, HTTPException, Depends
from pydantic import ValidationError
from typing import List
import json
import os
from uuid import UUID
from bisheng.database.models.flow import FlowCreate
from bisheng.api.services.user_service import UserPayload # 假设有用户服务
from bisheng.auth import get_current_user # 假设有用户认证依赖
router = APIRouter()
@router.get("/flows/from_json", response_model=List[FlowCreate])
async def get_flows_from_json(
file_path: str = "flows.json",
user: UserPayload = Depends(get_current_user)
):
"""
读取本地 JSON 文件并将其中的信息实例化为 FlowCreate 对象。
- **file_path**: 本地 JSON 文件的路径,默认为项目根目录下的 flows.json
- **user**: 当前登录用户的信息
"""
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail=f"File {file_path} not found.")
try:
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
except json.JSONDecodeError as e:
raise HTTPException(status_code=400, detail=f"Invalid JSON format: {e}")
if not isinstance(data, list):
raise HTTPException(status_code=400, detail="JSON data must be a list of flows.")
flows = []
for item in data:
try:
flow_create = FlowCreate(**item)
flows.append(flow_create)
except ValidationError as ve:
raise HTTPException(status_code=422, detail=f"Validation error: {ve}")
return flows
src/backend/bisheng/main.py
from fastapi import FastAPI
from bisheng.api.flow_router import router as flow_router
app = FastAPI(
title="Bisheng API",
description="API for managing flows and other functionalities.",
version="1.0.0"
)
# 包含其他路由
app.include_router(flow_router, prefix="/api", tags=["Flows"])
# 其他初始化代码,如事件处理器
@app.on_event("startup")
async def startup_event():
# 初始化缓存管理器、数据库连接等
pass
@app.on_event("shutdown")
async def shutdown_event():
# 关闭缓存管理器、数据库连接等
pass
flows.json
(示例 JSON 文件)
[
{
"name": "Flow 1",
"user_id": 123,
"description": "Description for Flow 1",
"data": {
"nodes": [
{"id": "node1", "type": "start"},
{"id": "node2", "type": "process"}
],
"edges": [
{"source": "node1", "target": "node2"}
]
},
"logo": "path/to/logo1.png",
"status": 2,
"flow_type": 1,
"guide_word": "Guide for Flow 1"
},
{
"name": "Flow 2",
"user_id": 456,
"description": "Description for Flow 2",
"data": {
"nodes": [
{"id": "nodeA", "type": "start"},
{"id": "nodeB", "type": "end"}
],
"edges": [
{"source": "nodeA", "target": "nodeB"}
]
},
"logo": "path/to/logo2.png",
"status": 1,
"flow_type": 5,
"guide_word": "Guide for Flow 2"
}
]
扩展:将 FlowCreate
实例保存到数据库
如果您的需求不仅仅是读取 JSON 文件并返回数据,而是还需要将这些数据保存到数据库,可以在 API 端点中集成数据库操作。以下是如何实现这一扩展的指导:
1. 修改 FlowCreate
模型
确保 FlowCreate
模型包含所有必要的字段,并可以转换为数据库模型。
2. 集成数据库会话
在 API 端点中,使用数据库会话将 FlowCreate
实例保存到数据库。
3. 修改路由逻辑
以下是修改后的路由逻辑示例:
src/backend/bisheng/api/flow_router.py
from fastapi import APIRouter, HTTPException, Depends
from pydantic import ValidationError
from typing import List
import json
import os
from uuid import UUID
from bisheng.database.models.flow import FlowCreate, FlowDao
from bisheng.api.services.user_service import UserPayload # 假设有用户服务
from bisheng.auth import get_current_user # 假设有用户认证依赖
router = APIRouter()
@router.post("/flows/from_json", response_model=List[FlowCreate])
async def create_flows_from_json(
file_path: str = "flows.json",
user: UserPayload = Depends(get_current_user)
):
"""
读取本地 JSON 文件并将其中的信息实例化为 FlowCreate 对象,并保存到数据库。
- **file_path**: 本地 JSON 文件的路径,默认为项目根目录下的 flows.json
- **user**: 当前登录用户的信息
"""
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail=f"File {file_path} not found.")
try:
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
except json.JSONDecodeError as e:
raise HTTPException(status_code=400, detail=f"Invalid JSON format: {e}")
if not isinstance(data, list):
raise HTTPException(status_code=400, detail="JSON data must be a list of flows.")
flows = []
for item in data:
try:
flow_create = FlowCreate(**item)
# 实例化数据库模型
flow = FlowDao(
name=flow_create.name,
user_id=flow_create.user_id,
description=flow_create.description,
data=flow_create.data,
logo=flow_create.logo,
status=flow_create.status,
flow_type=flow_create.flow_type,
guide_word=flow_create.guide_word
)
# 保存到数据库
FlowDao.create_flow(flow, flow_type=flow_create.flow_type)
flows.append(flow_create)
except ValidationError as ve:
raise HTTPException(status_code=422, detail=f"Validation error: {ve}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Internal server error: {e}")
return flows
说明:
- 将 HTTP 方法从
GET
改为POST
,因为涉及到数据的创建操作。 - 在成功验证和实例化
FlowCreate
对象后,使用FlowDao
将其保存到数据库。 - 处理可能的数据库异常,返回适当的错误信息。
3. 更新 FlowDao
确保 FlowDao
的 create_flow
方法能够正确处理新实例的保存。
src/backend/bisheng/database/models/flow.py
from sqlmodel import SQLModel, Field
from typing import Dict, Optional
from uuid import UUID, uuid4
class Flow(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
name: str
user_id: Optional[int] = Field(default=None, index=True)
description: Optional[str] = Field(default=None)
data: Optional[Dict] = Field(default=None, sa_column=Column(JSON))
logo: Optional[str] = Field(default=None)
status: Optional[int] = Field(default=1)
flow_type: Optional[int] = Field(default=1)
guide_word: Optional[str] = Field(default=None)
update_time: Optional[datetime] = Field(
default=None,
sa_column=Column(
DateTime,
nullable=True,
server_default=text('CURRENT_TIMESTAMP'),
onupdate=text('CURRENT_TIMESTAMP')
)
)
create_time: Optional[datetime] = Field(
default=None,
sa_column=Column(
DateTime, nullable=False, index=True, server_default=text('CURRENT_TIMESTAMP')
)
)
# 其他方法保持不变
说明:
- 确保
Flow
模型与FlowCreate
的字段对应。