创新实训(四)——WitNova数据库设计及实体关系建模与基于FastAPI的数据库方法实现
在项目中,数据库的设计与实体关系建模是关键环节,对整个平台的稳定运行和功能实现起着决定性作用。本次任务我对项目的需求和功能进行了进一步的分析,并完成了初步的数据库设计,并在后端实现了各个数据库对应的方法,完善了用户权限的审核。
一、数据库设计分析
对我们的AI智能辩论教练项目进行分析,按照数据库设计的思路,系统核心功能可以大体分为以下几点:
-
用户登录与画像管理
-
智能陪练与仿真辩论
-
自动评分与能力提升建议
-
观点检索与结构化语料使用
-
社区互动(话题、评论)
为支撑这些业务场景,初步设计了一套数据库结构,采用第三范式(3NF)设计数据表,确保数据不冗余、逻辑独立,并建模了以下基础实体:
-
User:用户(辩手/管理员) -
DebateSession:每场辩论训练记录 -
DebateTurn:一次辩论中的每一轮发言 -
Report:每场辩论的评分反馈与个性化训练建议报告
-
ArgumentCorpus:结构化语料库 -
CommunityPost:社区发帖 -
Comment:帖子评论
二、实体关系建模
根据上述数据库基础实体,构建实体之间的关系,以上实体对应的E-R图如下图所示

其中ArgumentCorpus结构化语料库由小组另一位负责RAG的同学建立,与其他实体耦合度较低,因此暂时不放在E-R图中。
三、数据库相关代码实现
在实际的工程中,主数据库使用关系型数据库MySQL,结构化存储各种数据。
接下来我将以debate_session辩论会话实体来展示在FastAPI中数据模型和接口方法的定义与实现。
1. 数据模型定义
首先明确debate_session实体中可能出现的各个属性的含义
-
记录每场辩论的主题(topic)、立场(position)与结果(result)
-
关联发起辩论的用户(user_id)
-
自动记录辩论发起时间(created_at)
基于以上设计目标,我们使用 SQLModel(结合了 Pydantic + SQLAlchemy 的现代ORM框架)定义了数据模型。
首先,在后端的models模块下创建新文件debate_session_models.py,在里面定义不同的数据模型,这种定义方式对我来说很新颖,因此我思考并查阅资料,了解了这样设计的目的。
为什么要定义不同的数据模型?
在传统 Web 开发中(比如Spring Boot + Hibernate、Django ORM),我们通常会这样设计数据模型:
定义一张数据库表(Entity)
直接用数据库表对象(比如 Java 的 Entity,Python 的 Model)来完成所有 CRUD(增删改查)操作
请求创建、更新、返回时,往往直接复用这一张表的数据模型
这种方式虽然简洁,但也带来几个问题:
不够灵活:创建和更新时字段要求不同,却只能用同一个模型
容易安全隐患:接口返回时容易泄露敏感字段(比如密码哈希)
无法精准验证:每个操作的输入输出格式很难做到严格控制
而在 SQLModel 中,它推崇一种新的实践模式:为每种不同的使用场景单独定义专属的数据模型(Pydantic Model)
比如针对一张
debate_sessions表,它会专门为不同的用途定义响应的数据模型,例如更新模型、创建模型、返回模型、数据库表模型等,这样的好处非常明显:
不同场景不同模型,精准校验,避免出错
返回时可以控制隐藏敏感字段,提升安全性
易于维护,未来表结构变更时影响面小
针对这种模式,我设计了debate_session实体可能会用到的各种数据模型。其在数据库表中模型中,要定义 user_id 外键关联到 users 表,实现辩论会话与用户的绑定,同时也需要在User中定义与DebateSession的关联
# app/models/debate_models.py
import uuid
from datetime import datetime
from sqlmodel import SQLModel, Field, Relationship
from app.models.user_models import User
#基础模型,包含其他模型都应该有的属性
class DebateSessionBase(SQLModel):
topic: str
position: str # pro / con
result: str | None = None # win / lose / draw
#创建模型
class DebateSessionCreate(DebateSessionBase):
pass
#更新模型
class DebateSessionUpdate(SQLModel):
topic: str | None = None
position: str | None = None
result: str | None = None
#数据库表模型,与实际的数据库表格属性对应
class DebateSession(DebateSessionBase, table=True):
__tablename__ = "debate_sessions"
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
user_id: uuid.UUID = Field(foreign_key="users.id", nullable=False)
created_at: datetime = Field(default_factory=datetime.utcnow)
user: User | None = Relationship(back_populates="debate_sessions")
#返回模型(用于API返回)
class DebateSessionPublic(DebateSessionBase):
id: uuid.UUID
user_id: uuid.UUID
created_at: datetime
#返回列表模型
class DebateSessionsPublic(SQLModel):
data: list[DebateSessionPublic]
count: int
2. CRUD接口方法实现
(1)用户权限控制
为保证用户的信息和隐私安全,只有在确认用户身份和权限的时候,才能针对每个用户进行相应的操作。在第二篇博客中已经完成了登录的实现和token的返回,接下来需要在后面的每次请求中,都加入该用户的token,后端对token进行解码和权限认证后,方可调用相应接口并响应请求。
在deps.py中定义解析token并返回当前用户的方法get_current_user,以下是传入的参数:
-
SessionDep:表示数据库会话对象Session,通过Depends(get_db)自动注入数据库连接。 -
TokenDep:表示当前请求头中携带的Bearer Token(JWT),由Depends(reusable_oauth2)提取
在该方法中对token进行解码并找到对应的用户,作为方法的返回值。在后面需要进行用户身份认证的方法中,只需要传入别名CurrentUser,FastAPI 就会自动获取 JWT Token、解码 token、查询用户并注入 User 实例,极大地提升了代码的可读性与复用性
SessionDep = Annotated[Session, Depends(get_db)]
TokenDep = Annotated[str, Depends(reusable_oauth2)]
def get_current_user(session: SessionDep, token: TokenDep) -> User:
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
)
token_data = TokenPayload(**payload)
except (InvalidTokenError, ValidationError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials",
)
user = session.get(User, UUID(token_data.sub))
if not user:
raise HTTPException(status_code=404, detail="User not found")
# if not user.is_active:
# raise HTTPException(status_code=400, detail="Inactive user")
return user
CurrentUser = Annotated[User, Depends(get_current_user)]
对于前端,在登录时将后端返回的token存入到了cookie中,因此在后续需要身份验证的请求中,需要把token添加到请求头中。首先,从 cookie 读取 token,在请求拦截器中加入携带token请求头Authorization。
//从cookie中获取 token
const getTokenFromCookie = () => {
const match = document.cookie.match(/authToken=([^;]*)/);
return match ? match[1] : null;
};
//在请求拦截器中添加token
axios.interceptors.request.use(config => {
const token = getTokenFromCookie();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
可以看到请求头已经被正确设置

(2)具体方法实现
接口文件位于 app/api/routes/debate.py,按照标准 RESTful 风格实现了对DebateSession增删改查,其中完成了对数据库会话的增删改查操作,以及对出现的错误的判断,并返回清晰的错误状态码和报错信息。
import uuid
from datetime import datetime
from typing import Any
from fastapi import APIRouter, HTTPException
from sqlmodel import func, select
from app.api.deps import CurrentUser, SessionDep
from app.models.debate_session_models import (
DebateSession,
DebateSessionCreate,
DebateSessionUpdate,
DebateSessionPublic,
DebateSessionsPublic,
)
router = APIRouter(prefix="/debates", tags=["debates"])
@router.get("/", response_model=DebateSessionsPublic)
def read_debate_sessions(
session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100
) -> Any:
if current_user.is_superuser:
count = session.exec(select(func.count()).select_from(DebateSession)).one()
items = session.exec(select(DebateSession).offset(skip).limit(limit)).all()
else:
count = session.exec(
select(func.count()).select_from(DebateSession).where(DebateSession.user_id == current_user.id)
).one()
items = session.exec(
select(DebateSession)
.where(DebateSession.user_id == current_user.id)
.offset(skip)
.limit(limit)
).all()
return DebateSessionsPublic(data=items, count=count)
@router.get("/{id}", response_model=DebateSessionPublic)
def read_debate_session(session: SessionDep, current_user: CurrentUser, id: uuid.UUID) -> Any:
debate = session.get(DebateSession, id)
if not debate:
raise HTTPException(status_code=404, detail="Debate session not found")
if not current_user.is_superuser and debate.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
return debate
@router.post("/", response_model=DebateSessionPublic)
def create_debate_session(
*, session: SessionDep, current_user: CurrentUser, item_in: DebateSessionCreate
) -> Any:
debate = DebateSession.model_validate(item_in, update={"user_id": current_user.id})
session.add(debate)
session.commit()
session.refresh(debate)
return debate
@router.put("/{id}", response_model=DebateSessionPublic)
def update_debate_session(
*, session: SessionDep, current_user: CurrentUser, id: uuid.UUID, item_in: DebateSessionUpdate
) -> Any:
debate = session.get(DebateSession, id)
if not debate:
raise HTTPException(status_code=404, detail="Debate session not found")
if debate.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
update_dict = item_in.model_dump(exclude_unset=True)
debate.sqlmodel_update(update_dict)
session.add(debate)
session.commit()
session.refresh(debate)
return debate
@router.delete("/{id}")
def delete_debate_session(
session: SessionDep, current_user: CurrentUser, id: uuid.UUID
) -> dict:
debate = session.get(DebateSession, id)
if not debate:
raise HTTPException(status_code=404, detail="Debate session not found")
if not current_user.is_superuser and debate.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
session.delete(debate)
session.commit()
return {"message": "Debate session deleted successfully"}
在前端对接口进行测试,发现可以正确完成用户身份的验证,并返回对应的数据。

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)