feat:更新练习题接口

This commit is contained in:
2026-07-30 13:07:26 +08:00
parent 50a27323cc
commit 9c9ac3ab1c
9 changed files with 83 additions and 266 deletions

View File

@@ -21,6 +21,7 @@ study_prompt = """
- JSON 必须是合法格式,可被程序直接解析。 - JSON 必须是合法格式,可被程序直接解析。
4. **返回格式(严格遵循)** 4. **返回格式(严格遵循)**
- select字段是用户实际选择的永远为空数组
[ [
{ {
"id": 1, "id": 1,
@@ -32,7 +33,8 @@ study_prompt = """
"选项C内容", "选项C内容",
"选项D内容" "选项D内容"
], ],
"answer": ["A"] "answer": ["A"],
"select": []
}, },
{ {
"id": 2, "id": 2,
@@ -44,7 +46,8 @@ study_prompt = """
"选项C内容", "选项C内容",
"选项D内容" "选项D内容"
], ],
"answer": ["A", "C"] "answer": ["A", "C"],
"select": []
} }
] ]
""" """

25
main.py
View File

@@ -8,8 +8,8 @@ from starlette.responses import StreamingResponse
from database import get_db from database import get_db
from schemas.agent import QueryRequest from schemas.agent import QueryRequest
from schemas.library import LibraryFileRequest, LibraryFileResponse from schemas.library import LibraryFileRequest, LibraryFileResponse
from schemas.mistake import MistakeResponse from schemas.mistake import MistakeResponse, MistakeRequest
from schemas.practice import PracticeQuestionResponse, PracticeCheckResult, PracticeRecordResponse from schemas.practice import PracticeRecordResponse, PracticeRecordRequest
from service import library_service, practice_service, mistake_service from service import library_service, practice_service, mistake_service
from service.agent_service import query_agent from service.agent_service import query_agent
from storage import upload_rustfs from storage import upload_rustfs
@@ -60,21 +60,26 @@ async def edit_file(id: int, data: LibraryFileRequest, db: AsyncSession = Depend
return await library_service.edit_file(db, id, data) return await library_service.edit_file(db, id, data)
@app.get("/practice/files/{id}", response_model=List[PracticeQuestionResponse])
async def get_question(id: int, db: AsyncSession = Depends(get_db)):
return await practice_service.get_question(db, id)
@app.get("/practice/record/{id}", response_model=List[PracticeRecordResponse]) @app.get("/practice/record/{id}", response_model=List[PracticeRecordResponse])
async def get_record(id: int, db: AsyncSession = Depends(get_db)): async def get_record(id: int, db: AsyncSession = Depends(get_db)):
return await practice_service.get_record(db, id) return await practice_service.get_record(db, id)
@app.post("/practice/check/{id}", response_model=List[PracticeCheckResult]) @app.post("/practice/record", response_model=bool)
async def check_practice(id: int, data: List[PracticeQuestionResponse], db: AsyncSession = Depends(get_db)): async def add_record(data: PracticeRecordRequest, db: AsyncSession = Depends(get_db)):
return await practice_service.check_practice(db, id, data) return await practice_service.add_record(db, data)
@app.get("/mistake", response_model=List[MistakeResponse]) @app.get("/mistake", response_model=List[MistakeResponse])
async def list_mistakes(db: AsyncSession = Depends(get_db)): async def list_mistakes(db: AsyncSession = Depends(get_db)):
return await mistake_service.list_mistakes(db) return await mistake_service.list_mistakes(db)
@app.post("/mistake", response_model=bool)
async def list_mistakes(data: list[MistakeRequest], db: AsyncSession = Depends(get_db)):
return await mistake_service.add_mistakes(db, data)
@app.delete("/mistake/{id}", response_model=bool)
async def remove_mistake(id: int, db: AsyncSession = Depends(get_db)):
return await mistake_service.remove_mistake(db, id)

View File

@@ -13,44 +13,12 @@ class LibraryFile(AuditBase):
size = Column(Integer, nullable=False) size = Column(Integer, nullable=False)
content = Column(Text, nullable=False) content = Column(Text, nullable=False)
questions = relationship(
"LibraryFileQuestion",
back_populates="library_file",
cascade="all, delete-orphan"
)
practice_record = relationship( practice_record = relationship(
"PracticeRecord", "PracticeRecord",
back_populates="library_file", back_populates="library_file",
cascade="all, delete-orphan" cascade="all, delete-orphan"
) )
mistake = relationship(
"Mistake",
back_populates="library_file",
cascade="all, delete-orphan"
)
class LibraryFileQuestion(AuditBase):
library_file_id = Column(BigInteger, ForeignKey("library_file.id"), nullable=False)
type = Column(String(45), nullable=False)
question = Column(Text, nullable=False)
options = Column(JSONB, nullable=False)
answer = Column(JSONB, nullable=False)
library_file = relationship(
"LibraryFile",
back_populates="questions"
)
mistake = relationship(
"Mistake",
back_populates="library_file_question",
cascade="all, delete-orphan"
)
class PracticeRecord(AuditBase): class PracticeRecord(AuditBase):
library_file_id = Column(BigInteger, ForeignKey("library_file.id"), nullable=False) library_file_id = Column(BigInteger, ForeignKey("library_file.id"), nullable=False)
@@ -64,18 +32,12 @@ class PracticeRecord(AuditBase):
back_populates="practice_record", back_populates="practice_record",
) )
class Mistake(AuditBase): class Mistake(AuditBase):
library_file_id = Column(BigInteger, ForeignKey("library_file.id"), nullable=False) subject = Column(String(45), nullable=False)
question_id = Column(BigInteger, ForeignKey("library_file_question.id"), nullable=False) module = Column(String(45), nullable=False)
name = Column(String(45), nullable=False)
wrong_count = Column(Integer, nullable=False) type = Column(String(45), nullable=False)
question = Column(Text, nullable=False)
library_file = relationship( options = Column(JSONB, nullable=False)
"LibraryFile", answer = Column(JSONB, nullable=False)
back_populates="mistake",
)
library_file_question = relationship(
"LibraryFileQuestion",
back_populates="mistake",
)

View File

@@ -1,19 +1,4 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import List
class QuestionRequest(BaseModel):
type: str
question: str
options: List[str]
answer: List[str]
class QuestionResponse(QuestionRequest):
id: int
class Config:
from_attributes = True
class LibraryFileRequest(BaseModel): class LibraryFileRequest(BaseModel):
@@ -24,13 +9,10 @@ class LibraryFileRequest(BaseModel):
type: str type: str
content: str content: str
questions: List[QuestionRequest] = []
class LibraryFileResponse(LibraryFileRequest): class LibraryFileResponse(LibraryFileRequest):
id: int id: int
uploadTime: str uploadTime: str
questions: List[QuestionResponse] = []
class Config: class Config:
from_attributes = True from_attributes = True

View File

@@ -3,8 +3,7 @@ from typing import List
from pydantic import BaseModel from pydantic import BaseModel
class MistakeResponse(BaseModel): class MistakeRequest(BaseModel):
id: int
subject: str subject: str
module: str module: str
name: str name: str
@@ -12,7 +11,13 @@ class MistakeResponse(BaseModel):
question: str question: str
options: List[str] options: List[str]
answer: List[str] answer: List[str]
wrongCount: int
class Config:
from_attributes = True
class MistakeResponse(MistakeRequest):
id: int
class Config: class Config:
from_attributes = True from_attributes = True

View File

@@ -2,8 +2,6 @@ from typing import List
from pydantic import BaseModel from pydantic import BaseModel
from schemas.library import QuestionRequest
class PracticeRecordRequest(BaseModel): class PracticeRecordRequest(BaseModel):
libraryFileId: int libraryFileId: int
@@ -20,14 +18,6 @@ class PracticeRecordResponse(PracticeRecordRequest):
from_attributes = True from_attributes = True
class PracticeQuestionResponse(QuestionRequest):
id: int
select: List[str]
class Config:
from_attributes = True
class PracticeCheckResult(BaseModel): class PracticeCheckResult(BaseModel):
id: int id: int
answer: List[str] answer: List[str]

View File

@@ -1,9 +1,8 @@
from sqlalchemy import select, delete from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from models.library import LibraryFile, LibraryFileQuestion from models.library import LibraryFile
from schemas.library import LibraryFileRequest, LibraryFileResponse, QuestionResponse from schemas.library import LibraryFileRequest, LibraryFileResponse
async def list_files(db: AsyncSession) -> list[LibraryFileResponse]: async def list_files(db: AsyncSession) -> list[LibraryFileResponse]:
@@ -20,7 +19,6 @@ async def list_files(db: AsyncSession) -> list[LibraryFileResponse]:
size=file.size, size=file.size,
content=file.content, content=file.content,
uploadTime=file.create_time.strftime("%Y-%m-%d %H:%M:%S"), uploadTime=file.create_time.strftime("%Y-%m-%d %H:%M:%S"),
questions=[],
) )
for file in files for file in files
] ]
@@ -28,7 +26,7 @@ async def list_files(db: AsyncSession) -> list[LibraryFileResponse]:
async def add_file(db: AsyncSession, data: LibraryFileRequest) -> bool: async def add_file(db: AsyncSession, data: LibraryFileRequest) -> bool:
""" """
创建题库文件及其题目 创建题库文件
""" """
file = LibraryFile( file = LibraryFile(
subject=data.subject, subject=data.subject,
@@ -40,18 +38,6 @@ async def add_file(db: AsyncSession, data: LibraryFileRequest) -> bool:
) )
db.add(file) db.add(file)
await db.flush()
for q in data.questions:
question = LibraryFileQuestion(
library_file_id=file.id,
type=q.type,
question=q.question,
options=q.options,
answer=q.answer
)
db.add(question)
await db.commit() await db.commit()
return True return True
@@ -59,7 +45,7 @@ async def add_file(db: AsyncSession, data: LibraryFileRequest) -> bool:
async def edit_file(db: AsyncSession, file_id: int, data: LibraryFileRequest) -> bool: async def edit_file(db: AsyncSession, file_id: int, data: LibraryFileRequest) -> bool:
""" """
编辑题库文件及其题目 编辑题库文件
""" """
result = await db.execute( result = await db.execute(
@@ -76,33 +62,13 @@ async def edit_file(db: AsyncSession, file_id: int, data: LibraryFileRequest) ->
file.type = data.type file.type = data.type
file.content = data.content file.content = data.content
await db.execute(
delete(LibraryFileQuestion)
.where(LibraryFileQuestion.library_file_id == file.id)
)
# 4. 插入新题目
for q in data.questions:
question = LibraryFileQuestion(
library_file_id=file.id,
type=q.type,
question=q.question,
options=q.options,
answer=q.answer,
)
db.add(question)
await db.commit() await db.commit()
return True return True
async def get_file(db: AsyncSession, file_id: int) -> LibraryFileResponse: async def get_file(db: AsyncSession, file_id: int) -> LibraryFileResponse:
stmt = ( stmt = (select(LibraryFile).where(LibraryFile.id == file_id))
select(LibraryFile)
.options(selectinload(LibraryFile.questions))
.where(LibraryFile.id == file_id)
)
result = await db.execute(stmt) result = await db.execute(stmt)
file = result.scalar_one_or_none() file = result.scalar_one_or_none()
@@ -115,14 +81,4 @@ async def get_file(db: AsyncSession, file_id: int) -> LibraryFileResponse:
size=file.size, size=file.size,
content=file.content, content=file.content,
uploadTime=file.create_time.strftime("%Y-%m-%d %H:%M:%S"), uploadTime=file.create_time.strftime("%Y-%m-%d %H:%M:%S"),
questions=[
QuestionResponse(
id=q.id,
type=q.type,
question=q.question,
options=q.options,
answer=q.answer
)
for q in file.questions
],
) )

View File

@@ -1,68 +1,51 @@
from sqlalchemy import select from sqlalchemy import select, delete
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from models.library import Mistake, LibraryFile, LibraryFileQuestion from models.library import Mistake
from schemas.mistake import MistakeResponse from schemas.mistake import MistakeResponse, MistakeRequest
async def list_mistakes(db: AsyncSession) -> list[MistakeResponse]: async def list_mistakes(db: AsyncSession) -> list[MistakeResponse]:
stmt = ( stmt = select(Mistake).order_by(Mistake.id.desc())
select( mistakes = (await db.execute(stmt)).scalars().all()
Mistake.id,
LibraryFile.subject,
LibraryFile.module,
LibraryFile.name,
LibraryFileQuestion.type,
LibraryFileQuestion.question,
LibraryFileQuestion.options,
LibraryFileQuestion.answer,
Mistake.wrong_count,
)
.join(
LibraryFileQuestion,
Mistake.question_id == LibraryFileQuestion.id,
)
.join(
LibraryFile,
Mistake.library_file_id == LibraryFile.id,
)
)
stmt = stmt.order_by(Mistake.create_time.desc())
result = await db.execute(stmt)
rows = result.mappings().all()
return [ return [
MistakeResponse( MistakeResponse(
id=row["id"], id=mistake.id,
subject=row["subject"], subject=mistake.subject,
module=row["module"], module=mistake.module,
name=row["name"], name=mistake.name,
type=row["type"], type=mistake.type,
question=row["question"], question=mistake.question,
options=row["options"], options=mistake.options,
answer=row["answer"], answer=mistake.answer
wrongCount=row["wrong_count"],
) )
for row in rows for mistake in mistakes
] ]
async def update_mistake(db: AsyncSession, library_file_id: int, question_id: int): async def add_mistakes(db: AsyncSession, data: list[MistakeRequest]):
stmt = select(Mistake).where( db.add_all(
Mistake.library_file_id == library_file_id, [
Mistake.question_id == question_id, Mistake(
subject=item.subject,
module=item.module,
name=item.name,
type=item.type,
question=item.question,
options=item.options,
answer=item.answer,
)
for item in data
]
) )
result = await db.execute(stmt)
wrong = result.scalar_one_or_none()
if wrong: await db.commit()
wrong.wrong_count += 1 return True
else:
wrong = Mistake(
library_file_id=library_file_id,
question_id=question_id,
wrong_count=1
)
db.add(wrong)
await db.flush()
async def remove_mistake(db: AsyncSession, mistake_id: int) -> bool:
await db.execute(delete(Mistake).where(Mistake.id == mistake_id))
await db.commit()
return True

View File

@@ -2,18 +2,13 @@ from typing import List
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from models.library import LibraryFile, PracticeRecord from models.library import PracticeRecord
from schemas.practice import PracticeQuestionResponse, PracticeRecordRequest, PracticeCheckResult, \ from schemas.practice import PracticeRecordRequest, PracticeRecordResponse
PracticeRecordResponse
from service.library_service import get_file
from service.mistake_service import update_mistake
async def get_record(db: AsyncSession, library_file_id: int) -> List[PracticeRecordResponse]: async def get_record(db: AsyncSession, library_file_id: int) -> List[PracticeRecordResponse]:
stmt = select(PracticeRecord).where(PracticeRecord.library_file_id == library_file_id).order_by( stmt = select(PracticeRecord).where(PracticeRecord.library_file_id == library_file_id).order_by(PracticeRecord.create_time.desc())
PracticeRecord.create_time.desc())
records = (await db.execute(stmt)).scalars().all() records = (await db.execute(stmt)).scalars().all()
return [ return [
@@ -29,71 +24,7 @@ async def get_record(db: AsyncSession, library_file_id: int) -> List[PracticeRec
] ]
async def get_question(db: AsyncSession, file_id: int) -> List[PracticeQuestionResponse]: async def add_record(db: AsyncSession, data: PracticeRecordRequest):
stmt = (
select(LibraryFile)
.options(selectinload(LibraryFile.questions))
.where(LibraryFile.id == file_id)
)
result = await db.execute(stmt)
file = result.scalar_one_or_none()
return [
PracticeQuestionResponse(
id=q.id,
type=q.type,
question=q.question,
options=q.options,
answer=[],
select=[]
)
for q in file.questions
]
async def check_practice(db: AsyncSession,
library_file_id: int,
questions: List[PracticeQuestionResponse]) -> List[PracticeCheckResult]:
library_file = await get_file(db, library_file_id)
db_questions = library_file.questions
check_results = []
for user_q in questions:
db_q = next((q for q in db_questions if q.id == user_q.id), None)
if not db_q:
continue
is_correct = set(user_q.select) == set(db_q.answer)
if not is_correct:
await update_mistake(
db=db,
library_file_id=library_file_id,
question_id=user_q.id,
)
check_results.append(PracticeCheckResult(
id=user_q.id,
answer=db_q.answer,
select=user_q.select,
isCorrect=is_correct,
))
correct_count = sum(1 for r in check_results if r.isCorrect)
await add_practice_record(
db,
PracticeRecordRequest(
libraryFileId=library_file_id,
totalCount=len(check_results),
correctCount=correct_count,
wrongCount=len(check_results) - correct_count,
),
)
return check_results
async def add_practice_record(db: AsyncSession, data: PracticeRecordRequest):
record = PracticeRecord( record = PracticeRecord(
library_file_id=data.libraryFileId, library_file_id=data.libraryFileId,
total_count=data.totalCount, total_count=data.totalCount,