Files
john 05e173b293 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>
2026-06-08 15:13:59 +08:00

212 lines
6.6 KiB
Python

from datetime import datetime, timezone
from typing import Optional
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
def utc_now_iso() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def calc_mastery_score(correct: int, wrong: int) -> int:
total = correct + wrong
if total == 0:
return 0
return round(correct / total * 100)
class WordService:
def create_word(self, db: Session, user: User, data: dict) -> Word:
book_id = data.get("book_id") or ACCUMULATION_BOOK_ID
existing = (
db.query(Word)
.filter(
Word.user_id == user.id,
Word.book_id == book_id,
Word.source_text == data["source_text"],
Word.target_text == data["target_text"],
)
.first()
)
if existing:
raise HTTPException(status_code=400, detail="该单词已存在")
word = Word(
user_id=user.id,
book_id=book_id,
book_entry_id=data.get("book_entry_id"),
source_text=data["source_text"],
target_text=data["target_text"],
source_lang=data["source_lang"],
target_lang=data["target_lang"],
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)
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,
user: User,
status: Optional[str] = None,
book_id: Optional[int] = None,
) -> list[Word]:
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()
if not word:
raise HTTPException(status_code=404, detail="单词不存在")
return word
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:
word = self.get_word(db, user, word_id)
if "status" in data and data["status"]:
word.status = data["status"]
db.commit()
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()