Ship native iOS app, Wiki/TTS backend, and skeleton loading UX.
Replace the WebView shell with SwiftUI screens, add account-scoped Wiki and TTS APIs with adaptive review and photo scan support, and keep web/iOS pages usable while data loads asynchronously. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,7 @@ from fastapi import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models import QuizRecord, Word, User
|
||||
from services.wiki_service import wiki_service
|
||||
|
||||
ACCUMULATION_BOOK_ID = 0
|
||||
|
||||
@@ -52,14 +53,34 @@ class WordService:
|
||||
wrong_count=0,
|
||||
consecutive_correct_count=0,
|
||||
mastery_score=0,
|
||||
difficulty=5.0,
|
||||
stability_hours=24.0,
|
||||
next_review_at=utc_now_iso(),
|
||||
review_due_date=datetime.now(timezone.utc).strftime("%Y-%m-%d"),
|
||||
created_at=utc_now_iso(),
|
||||
)
|
||||
db.add(word)
|
||||
db.commit()
|
||||
db.refresh(word)
|
||||
wiki_service.enqueue_word(user.id, word.id)
|
||||
return word
|
||||
|
||||
def _list_words_query(
|
||||
self,
|
||||
db: Session,
|
||||
user: User,
|
||||
status: Optional[str] = None,
|
||||
book_id: Optional[int] = None,
|
||||
):
|
||||
q = db.query(Word).filter(Word.user_id == user.id)
|
||||
if book_id is None or book_id == ACCUMULATION_BOOK_ID:
|
||||
q = q.filter(Word.book_id == ACCUMULATION_BOOK_ID)
|
||||
else:
|
||||
q = q.filter(Word.book_id == book_id)
|
||||
if status:
|
||||
q = q.filter(Word.status == status)
|
||||
return q
|
||||
|
||||
def list_words(
|
||||
self,
|
||||
db: Session,
|
||||
@@ -67,14 +88,33 @@ class WordService:
|
||||
status: Optional[str] = None,
|
||||
book_id: Optional[int] = None,
|
||||
) -> list[Word]:
|
||||
q = db.query(Word).filter(Word.user_id == user.id)
|
||||
if book_id is None:
|
||||
q = q.filter(Word.book_id == ACCUMULATION_BOOK_ID)
|
||||
elif book_id > 0:
|
||||
q = q.filter(Word.book_id == book_id)
|
||||
if status:
|
||||
q = q.filter(Word.status == status)
|
||||
return q.order_by(Word.created_at.desc()).all()
|
||||
return (
|
||||
self._list_words_query(db, user, status, book_id)
|
||||
.order_by(Word.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
def list_words_page(
|
||||
self,
|
||||
db: Session,
|
||||
user: User,
|
||||
*,
|
||||
status: Optional[str] = None,
|
||||
book_id: Optional[int] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> tuple[list[Word], int]:
|
||||
page = max(1, page)
|
||||
page_size = max(1, min(page_size, 100))
|
||||
q = self._list_words_query(db, user, status, book_id)
|
||||
total = q.count()
|
||||
items = (
|
||||
q.order_by(Word.created_at.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
.all()
|
||||
)
|
||||
return items, total
|
||||
|
||||
def get_word(self, db: Session, user: User, word_id: int) -> Word:
|
||||
word = db.query(Word).filter(Word.id == word_id, Word.user_id == user.id).first()
|
||||
@@ -84,11 +124,14 @@ class WordService:
|
||||
|
||||
def delete_word(self, db: Session, user: User, word_id: int) -> None:
|
||||
word = self.get_word(db, user, word_id)
|
||||
wiki_service.remove_word(db, user, word)
|
||||
db.query(QuizRecord).filter(QuizRecord.word_id == word.id).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
db.flush()
|
||||
db.delete(word)
|
||||
db.flush()
|
||||
wiki_service.refresh_index(db, user)
|
||||
db.commit()
|
||||
|
||||
def update_word(self, db: Session, user: User, word_id: int, data: dict) -> Word:
|
||||
@@ -99,5 +142,70 @@ class WordService:
|
||||
db.refresh(word)
|
||||
return word
|
||||
|
||||
def batch_create_words(self, db: Session, user: User, items: list[dict]) -> dict:
|
||||
created: list[Word] = []
|
||||
skipped = 0
|
||||
book_id = ACCUMULATION_BOOK_ID
|
||||
|
||||
for data in items:
|
||||
source_text = str(data.get("source_text", "")).strip()
|
||||
target_text = str(data.get("target_text", "")).strip()
|
||||
if not source_text or not target_text:
|
||||
continue
|
||||
|
||||
item_book_id = data.get("book_id") or book_id
|
||||
existing = (
|
||||
db.query(Word)
|
||||
.filter(
|
||||
Word.user_id == user.id,
|
||||
Word.book_id == item_book_id,
|
||||
Word.source_text == source_text,
|
||||
Word.target_text == target_text,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
word = Word(
|
||||
user_id=user.id,
|
||||
book_id=item_book_id,
|
||||
book_entry_id=data.get("book_entry_id"),
|
||||
source_text=source_text,
|
||||
target_text=target_text,
|
||||
source_lang=data.get("source_lang") or "en",
|
||||
target_lang=data.get("target_lang") or "zh",
|
||||
phonetic=data.get("phonetic"),
|
||||
example_en=data.get("example_en"),
|
||||
example_cn=data.get("example_cn"),
|
||||
status="new",
|
||||
correct_count=0,
|
||||
wrong_count=0,
|
||||
consecutive_correct_count=0,
|
||||
mastery_score=0,
|
||||
difficulty=5.0,
|
||||
stability_hours=24.0,
|
||||
next_review_at=utc_now_iso(),
|
||||
review_due_date=datetime.now(timezone.utc).strftime("%Y-%m-%d"),
|
||||
created_at=utc_now_iso(),
|
||||
)
|
||||
db.add(word)
|
||||
created.append(word)
|
||||
|
||||
if not created and skipped == 0:
|
||||
raise HTTPException(status_code=400, detail="没有可添加的单词")
|
||||
|
||||
db.commit()
|
||||
for word in created:
|
||||
db.refresh(word)
|
||||
wiki_service.enqueue_word(user.id, word.id)
|
||||
|
||||
return {
|
||||
"created": len(created),
|
||||
"skipped": skipped,
|
||||
"items": created,
|
||||
}
|
||||
|
||||
|
||||
word_service = WordService()
|
||||
|
||||
Reference in New Issue
Block a user