feat:增加训练场和错题集接口

This commit is contained in:
2026-07-29 19:51:02 +08:00
parent 613eed541f
commit 0bcd749d27
9 changed files with 314 additions and 16 deletions

6
.env
View File

@@ -7,8 +7,8 @@ OCR_API_URL=https://paddleocr.aistudio-app.com/api/v2/ocr/jobs
OCR_API_TOKEN=df7dcc85a5c3c9d64e421f353d11d13ec45512f6 OCR_API_TOKEN=df7dcc85a5c3c9d64e421f353d11d13ec45512f6
OCR_MODEL=PaddleOCR-VL-1.6 OCR_MODEL=PaddleOCR-VL-1.6
MODEL_NAME=deepseek-v4-flash MODEL_NAME="glm-5.2"
MODEL_BASE_URL=https://api.deepseek.com MODEL_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1"
MODEL_API_KEY=sk-0b237d41f6bc44fc9732ea66bd7eade0 MODEL_API_KEY="sk-52bcd98e9c1d45908437c4e8706eefff"
DATABASE_URL=postgresql+asyncpg://agent:medical%402019@60.247.145.200:5432/study DATABASE_URL=postgresql+asyncpg://agent:medical%402019@60.247.145.200:5432/study

40
main.py
View File

@@ -8,8 +8,10 @@ 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.practice import PracticeQuestionResponse, PracticeCheckResult, PracticeRecordResponse
from service import library_service, practice_service, mistake_service
from service.agent_service import query_agent from service.agent_service import query_agent
from service.library_service import add_file, get_file, list_files, edit_file
from storage import upload_rustfs from storage import upload_rustfs
from ocr import ocr_from_url from ocr import ocr_from_url
@@ -39,20 +41,40 @@ def generate(query: QueryRequest):
@app.get("/library/files", response_model=List[LibraryFileResponse]) @app.get("/library/files", response_model=List[LibraryFileResponse])
async def list_library_file(db: AsyncSession = Depends(get_db)): async def list_files(db: AsyncSession = Depends(get_db)):
return await list_files(db) return await library_service.list_files(db)
@app.get("/library/files/{id}", response_model=LibraryFileResponse) @app.get("/library/files/{id}", response_model=LibraryFileResponse)
async def get_library_file(id: int, db: AsyncSession = Depends(get_db)): async def get_file(id: int, db: AsyncSession = Depends(get_db)):
return await get_file(db, id) return await library_service.get_file(db, id)
@app.post("/library/files", response_model=bool) @app.post("/library/files", response_model=bool)
async def add_library_file(data: LibraryFileRequest, db: AsyncSession = Depends(get_db)): async def add_file(data: LibraryFileRequest, db: AsyncSession = Depends(get_db)):
return await add_file(db, data) return await library_service.add_file(db, data)
@app.put("/library/files/{id}", response_model=bool) @app.put("/library/files/{id}", response_model=bool)
async def edit_library_file(id: int, data: LibraryFileRequest, db: AsyncSession = Depends(get_db)): async def edit_file(id: int, data: LibraryFileRequest, db: AsyncSession = Depends(get_db)):
return await 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])
async def get_record(id: int, db: AsyncSession = Depends(get_db)):
return await practice_service.get_record(db, id)
@app.post("/practice/check/{id}", response_model=List[PracticeCheckResult])
async def check_practice(id: int, data: List[PracticeQuestionResponse], db: AsyncSession = Depends(get_db)):
return await practice_service.check_practice(db, id, data)
@app.get("/mistake", response_model=List[MistakeResponse])
async def list_mistakes(db: AsyncSession = Depends(get_db)):
return await mistake_service.list_mistakes(db)

View File

@@ -1,4 +1,4 @@
from sqlalchemy import Column, BigInteger, String, Text, TIMESTAMP, Integer, ForeignKey from sqlalchemy import Column, BigInteger, String, Text, Integer, ForeignKey
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -19,6 +19,18 @@ class LibraryFile(AuditBase):
cascade="all, delete-orphan" cascade="all, delete-orphan"
) )
practice_record = relationship(
"PracticeRecord",
back_populates="library_file",
cascade="all, delete-orphan"
)
mistake = relationship(
"Mistake",
back_populates="library_file",
cascade="all, delete-orphan"
)
class LibraryFileQuestion(AuditBase): class LibraryFileQuestion(AuditBase):
library_file_id = Column(BigInteger, ForeignKey("library_file.id"), nullable=False) library_file_id = Column(BigInteger, ForeignKey("library_file.id"), nullable=False)
@@ -32,3 +44,38 @@ class LibraryFileQuestion(AuditBase):
"LibraryFile", "LibraryFile",
back_populates="questions" back_populates="questions"
) )
mistake = relationship(
"Mistake",
back_populates="library_file_question",
cascade="all, delete-orphan"
)
class PracticeRecord(AuditBase):
library_file_id = Column(BigInteger, ForeignKey("library_file.id"), nullable=False)
total_count = Column(Integer, nullable=False)
correct_count = Column(Integer, nullable=False)
wrong_count = Column(Integer, nullable=False)
library_file = relationship(
"LibraryFile",
back_populates="practice_record",
)
class Mistake(AuditBase):
library_file_id = Column(BigInteger, ForeignKey("library_file.id"), nullable=False)
question_id = Column(BigInteger, ForeignKey("library_file_question.id"), nullable=False)
wrong_count = Column(Integer, nullable=False)
library_file = relationship(
"LibraryFile",
back_populates="mistake",
)
library_file_question = relationship(
"LibraryFileQuestion",
back_populates="mistake",
)

View File

@@ -1,5 +1,3 @@
from datetime import datetime
from pydantic import BaseModel from pydantic import BaseModel
from typing import List from typing import List

18
schemas/mistake.py Normal file
View File

@@ -0,0 +1,18 @@
from typing import List
from pydantic import BaseModel
class MistakeResponse(BaseModel):
id: int
subject: str
module: str
name: str
type: str
question: str
options: List[str]
answer: List[str]
wrongCount: int
class Config:
from_attributes = True

38
schemas/practice.py Normal file
View File

@@ -0,0 +1,38 @@
from typing import List
from pydantic import BaseModel
from schemas.library import QuestionRequest
class PracticeRecordRequest(BaseModel):
libraryFileId: int
totalCount: int
correctCount: int
wrongCount: int
class PracticeRecordResponse(PracticeRecordRequest):
id: int
createTime: str
class Config:
from_attributes = True
class PracticeQuestionResponse(QuestionRequest):
id: int
select: List[str]
class Config:
from_attributes = True
class PracticeCheckResult(BaseModel):
id: int
answer: List[str]
select: List[str]
isCorrect: bool
class Config:
from_attributes = True

View File

@@ -121,7 +121,7 @@ async def get_file(db: AsyncSession, file_id: int) -> LibraryFileResponse:
type=q.type, type=q.type,
question=q.question, question=q.question,
options=q.options, options=q.options,
answer=q.answer, answer=q.answer
) )
for q in file.questions for q in file.questions
], ],

View File

@@ -0,0 +1,68 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from models.library import Mistake, LibraryFile, LibraryFileQuestion
from schemas.mistake import MistakeResponse
async def list_mistakes(db: AsyncSession) -> list[MistakeResponse]:
stmt = (
select(
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 [
MistakeResponse(
id=row["id"],
subject=row["subject"],
module=row["module"],
name=row["name"],
type=row["type"],
question=row["question"],
options=row["options"],
answer=row["answer"],
wrongCount=row["wrong_count"],
)
for row in rows
]
async def update_mistake(db: AsyncSession, library_file_id: int, question_id: int):
stmt = select(Mistake).where(
Mistake.library_file_id == library_file_id,
Mistake.question_id == question_id,
)
result = await db.execute(stmt)
wrong = result.scalar_one_or_none()
if wrong:
wrong.wrong_count += 1
else:
wrong = Mistake(
library_file_id=library_file_id,
question_id=question_id,
wrong_count=1
)
db.add(wrong)
await db.flush()

107
service/practice_service.py Normal file
View File

@@ -0,0 +1,107 @@
from typing import List
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from models.library import LibraryFile, PracticeRecord
from schemas.practice import PracticeQuestionResponse, PracticeRecordRequest, PracticeCheckResult, \
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]:
stmt = select(PracticeRecord).where(PracticeRecord.library_file_id == library_file_id).order_by(
PracticeRecord.create_time.desc())
records = (await db.execute(stmt)).scalars().all()
return [
PracticeRecordResponse(
id=record.id,
libraryFileId=record.library_file_id,
totalCount=record.total_count,
correctCount=record.correct_count,
wrongCount=record.wrong_count,
createTime=record.create_time.strftime("%Y-%m-%d %H:%M:%S"),
)
for record in records
]
async def get_question(db: AsyncSession, file_id: int) -> List[PracticeQuestionResponse]:
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(
library_file_id=data.libraryFileId,
total_count=data.totalCount,
correct_count=data.correctCount,
wrong_count=data.wrongCount,
)
db.add(record)
await db.commit()
return True