Add word-book practice, iOS app shell, and fix embedded WebView blank screen.
Ship dual-track learning (daily accumulation vs textbook),沪教/商务词书 APIs and UI, native iOS wrapper with bundled H5, and production book import on deploy. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from auth import get_current_user
|
||||
from database import get_db
|
||||
from models import User, WordBookEntry
|
||||
from schemas import (
|
||||
ActiveBookOut,
|
||||
BookPracticeSettingsOut,
|
||||
BookPracticeSettingsUpdate,
|
||||
SetActiveBookRequest,
|
||||
WordBookDetailOut,
|
||||
WordBookOut,
|
||||
WordBookProgressOut,
|
||||
WordBookUnitOut,
|
||||
)
|
||||
from services.book_service import book_service
|
||||
|
||||
router = APIRouter(prefix="/api/books", tags=["books"])
|
||||
|
||||
|
||||
def _active_out(db: Session, user: User, book) -> ActiveBookOut:
|
||||
progress = book_service.book_progress(db, user, book.id)
|
||||
settings = book_service.practice_settings_dict(db, user, book)
|
||||
return ActiveBookOut(
|
||||
book=WordBookOut.model_validate(book),
|
||||
progress=WordBookProgressOut(**progress),
|
||||
settings=BookPracticeSettingsOut(**settings),
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=list[WordBookOut])
|
||||
def list_books(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
return book_service.list_books(db)
|
||||
|
||||
|
||||
@router.get("/settings", response_model=BookPracticeSettingsOut)
|
||||
def get_book_practice_settings(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
book = book_service.get_active_book(db, current_user)
|
||||
if not book:
|
||||
from fastapi import HTTPException
|
||||
|
||||
raise HTTPException(status_code=400, detail="请先在词书设置中选择词书")
|
||||
data = book_service.practice_settings_dict(db, current_user, book)
|
||||
return BookPracticeSettingsOut(**data)
|
||||
|
||||
|
||||
@router.patch("/settings", response_model=ActiveBookOut)
|
||||
def update_book_practice_settings(
|
||||
data: BookPracticeSettingsUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
book = book_service.get_active_book(db, current_user)
|
||||
if data.book_id:
|
||||
book = book_service.activate_book(db, current_user, data.book_id)
|
||||
elif not book:
|
||||
from fastapi import HTTPException
|
||||
|
||||
raise HTTPException(status_code=400, detail="请先选择词书")
|
||||
payload = data.model_dump(exclude_unset=True, exclude={"book_id"})
|
||||
if payload:
|
||||
book_service.update_practice_settings(db, current_user, book.id, payload)
|
||||
return _active_out(db, current_user, book)
|
||||
|
||||
|
||||
@router.get("/active", response_model=ActiveBookOut)
|
||||
def get_active_book(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
book = book_service.get_active_book(db, current_user)
|
||||
if not book:
|
||||
return ActiveBookOut(book=None, progress=None, settings=None)
|
||||
return _active_out(db, current_user, book)
|
||||
|
||||
|
||||
@router.post("/active", response_model=ActiveBookOut)
|
||||
def set_active_book(
|
||||
data: SetActiveBookRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
book = book_service.activate_book(db, current_user, data.book_id)
|
||||
return _active_out(db, current_user, book)
|
||||
|
||||
|
||||
@router.get("/{book_id}", response_model=WordBookDetailOut)
|
||||
def get_book_detail(
|
||||
book_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
book = book_service.get_book(db, book_id)
|
||||
rows = (
|
||||
db.query(
|
||||
WordBookEntry.unit,
|
||||
WordBookEntry.unit_title,
|
||||
func.count(WordBookEntry.id),
|
||||
)
|
||||
.filter(WordBookEntry.book_id == book.id)
|
||||
.group_by(WordBookEntry.unit, WordBookEntry.unit_title)
|
||||
.order_by(WordBookEntry.unit.asc())
|
||||
.all()
|
||||
)
|
||||
units = [WordBookUnitOut(unit=r[0], title=r[1], word_count=r[2]) for r in rows]
|
||||
return WordBookDetailOut(
|
||||
**WordBookOut.model_validate(book).model_dump(),
|
||||
units=units,
|
||||
)
|
||||
@@ -1,4 +1,6 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from auth import get_current_user
|
||||
@@ -12,10 +14,11 @@ router = APIRouter(prefix="/api/coach", tags=["coach"])
|
||||
|
||||
@router.get("/session", response_model=CoachSessionResponse)
|
||||
def coach_session(
|
||||
track: Literal["accumulation", "book"] = Query("accumulation"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
return memory_coach_service.start_session(db, current_user)
|
||||
return memory_coach_service.start_session(db, current_user, track=track)
|
||||
|
||||
|
||||
@router.post("/turn", response_model=CoachTurnResponse)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from typing import Literal, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from auth import get_current_user
|
||||
@@ -17,19 +19,21 @@ router = APIRouter(prefix="/api/quiz", tags=["quiz"])
|
||||
|
||||
@router.get("/daily", response_model=DailyQuizResponse)
|
||||
def daily_quiz(
|
||||
track: Literal["accumulation", "book"] = Query("accumulation"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
result = quiz_service.get_daily_quiz(db, current_user)
|
||||
result = quiz_service.get_daily_quiz(db, current_user, track=track)
|
||||
return DailyQuizResponse(**result)
|
||||
|
||||
|
||||
@router.get("/spell", response_model=DailyQuizResponse)
|
||||
def spell_quiz(
|
||||
track: Literal["accumulation", "book"] = Query("accumulation"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
result = quiz_service.get_spell_quiz(db, current_user)
|
||||
result = quiz_service.get_spell_quiz(db, current_user, track=track)
|
||||
return DailyQuizResponse(**result)
|
||||
|
||||
|
||||
@@ -53,7 +57,8 @@ def submit_answer(
|
||||
|
||||
@router.get("/stats", response_model=QuizStatsResponse)
|
||||
def quiz_stats(
|
||||
track: Literal["accumulation", "book"] = Query("accumulation"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
return quiz_service.get_stats(db, current_user)
|
||||
return quiz_service.get_stats(db, current_user, track=track)
|
||||
|
||||
@@ -34,18 +34,20 @@ def create_word(
|
||||
@router.get("", response_model=list[WordOut])
|
||||
def list_words(
|
||||
status: Optional[str] = None,
|
||||
book_id: Optional[int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
return word_service.list_words(db, current_user, status)
|
||||
return word_service.list_words(db, current_user, status, book_id=book_id)
|
||||
|
||||
|
||||
@router.get("/memory-viz", response_model=MemoryVisualizationResponse)
|
||||
def memory_visualization(
|
||||
book_id: int = 0,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
return memory_visual_service.get_visualization(db, current_user)
|
||||
return memory_visual_service.get_visualization(db, current_user, book_id=book_id)
|
||||
|
||||
|
||||
@router.get("/{word_id}/memory", response_model=WordMemoryDetailResponse)
|
||||
|
||||
Reference in New Issue
Block a user