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,106 @@
|
||||
"""将 backend/data/books/*.json 导入 word_books / word_book_entries 表。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
BACKEND_DIR = Path(__file__).resolve().parents[1]
|
||||
if str(BACKEND_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(BACKEND_DIR))
|
||||
|
||||
from database import SessionLocal # noqa: E402
|
||||
from models import WordBook, WordBookEntry # noqa: E402
|
||||
from services.book_service import DEPRECATED_SLUGS # noqa: E402
|
||||
|
||||
BOOKS_DIR = BACKEND_DIR / "data" / "books"
|
||||
|
||||
|
||||
def upsert_book(session, data: dict) -> WordBook:
|
||||
book = session.query(WordBook).filter(WordBook.slug == data["slug"]).first()
|
||||
if not book:
|
||||
book = WordBook(slug=data["slug"])
|
||||
session.add(book)
|
||||
book.title = data["title"]
|
||||
book.description = data.get("description")
|
||||
book.level = data.get("level", "general")
|
||||
book.word_count = data.get("word_count", 0)
|
||||
book.unit_count = data.get("unit_count", 0)
|
||||
book.sort_order = data.get("sort_order", 0)
|
||||
book.daily_target = data.get("daily_target", 15)
|
||||
book.master_required_count = data.get("master_required_count", 4)
|
||||
book.weak_wrong_threshold = data.get("weak_wrong_threshold", 2)
|
||||
book.learn_mode = data.get("learn_mode", "sequential")
|
||||
book.is_published = 1
|
||||
session.flush()
|
||||
return book
|
||||
|
||||
|
||||
def import_book_file(session, path: Path) -> tuple[str, int]:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
book = upsert_book(session, data)
|
||||
session.query(WordBookEntry).filter(WordBookEntry.book_id == book.id).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
count = 0
|
||||
for unit in data.get("units", []):
|
||||
unit_no = unit.get("unit", 1)
|
||||
unit_title = unit.get("title")
|
||||
for w in unit.get("words", []):
|
||||
entry = WordBookEntry(
|
||||
book_id=book.id,
|
||||
unit=unit_no,
|
||||
unit_title=unit_title,
|
||||
sort_index=w["sort_index"],
|
||||
lemma_en=w["lemma_en"].strip().lower()[:128],
|
||||
zh=w["zh"].strip()[:256],
|
||||
phonetic=(w.get("phonetic") or "")[:128] or None,
|
||||
example_en=w.get("example_en"),
|
||||
example_cn=w.get("example_cn"),
|
||||
)
|
||||
session.add(entry)
|
||||
count += 1
|
||||
book.word_count = count
|
||||
book.unit_count = len(data.get("units", []))
|
||||
session.commit()
|
||||
return book.slug, count
|
||||
|
||||
|
||||
def import_all(books_dir: Path = BOOKS_DIR) -> None:
|
||||
files = sorted(books_dir.glob("*.json"))
|
||||
files = [
|
||||
f
|
||||
for f in files
|
||||
if f.name != "manifest.json"
|
||||
and f.stem not in DEPRECATED_SLUGS
|
||||
]
|
||||
if not files:
|
||||
print(f"未找到词书 JSON: {books_dir}")
|
||||
return
|
||||
session = SessionLocal()
|
||||
try:
|
||||
for path in files:
|
||||
slug, count = import_book_file(session, path)
|
||||
print(f" 导入 {slug}: {count} 词条")
|
||||
for slug in DEPRECATED_SLUGS:
|
||||
book = session.query(WordBook).filter(WordBook.slug == slug).first()
|
||||
if book and book.is_published:
|
||||
book.is_published = 0
|
||||
session.commit()
|
||||
print(f" 下架旧词书 {slug}")
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="导入词书 JSON 到数据库")
|
||||
parser.add_argument("--dir", type=Path, default=BOOKS_DIR)
|
||||
args = parser.parse_args()
|
||||
import_all(args.dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user