Spaces:
Sleeping
Sleeping
eithney commited on
Commit ·
75d3623
1
Parent(s): 3794435
update static
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .vscode/PythonImportHelper-v2-Completion.json +407 -11
- API_DOCUMENTATION.md +414 -0
- BOOKS_API_DATA_文档.md +1232 -0
- CHANGELOG.md +212 -0
- QUICKSTART.md +234 -0
- README.md +307 -67
- app.py +663 -47
- books_api_data/10005_一年级上册/audios/page_002_piece_00.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_003_piece_00.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_004_piece_00.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_004_piece_01.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_004_piece_02.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_004_piece_03.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_004_piece_04.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_004_piece_05.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_004_piece_06.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_004_piece_07.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_004_piece_08.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_004_piece_09.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_005_piece_00.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_005_piece_01.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_005_piece_02.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_005_piece_03.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_005_piece_04.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_006_piece_00.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_006_piece_01.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_006_piece_02.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_006_piece_03.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_006_piece_04.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_006_piece_05.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_006_piece_06.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_007_piece_00.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_007_piece_01.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_007_piece_02.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_009_piece_00.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_009_piece_01.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_009_piece_02.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_009_piece_03.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_009_piece_04.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_009_piece_05.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_010_piece_00.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_010_piece_01.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_011_piece_00.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_011_piece_01.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_011_piece_02.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_011_piece_03.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_011_piece_04.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_011_piece_05.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_011_piece_06.mp3 +3 -0
- books_api_data/10005_一年级上册/audios/page_011_piece_07.mp3 +3 -0
.vscode/PythonImportHelper-v2-Completion.json
CHANGED
|
@@ -43,6 +43,38 @@
|
|
| 43 |
"detail": "datetime",
|
| 44 |
"documentation": {}
|
| 45 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
{
|
| 47 |
"label": "Path",
|
| 48 |
"importPath": "pathlib",
|
|
@@ -148,6 +180,14 @@
|
|
| 148 |
"detail": "logging.handlers",
|
| 149 |
"documentation": {}
|
| 150 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
{
|
| 152 |
"label": "re",
|
| 153 |
"kind": 6,
|
|
@@ -215,6 +255,38 @@
|
|
| 215 |
"detail": "typing",
|
| 216 |
"documentation": {}
|
| 217 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
{
|
| 219 |
"label": "hashlib",
|
| 220 |
"kind": 6,
|
|
@@ -224,6 +296,15 @@
|
|
| 224 |
"detail": "hashlib",
|
| 225 |
"documentation": {}
|
| 226 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
{
|
| 228 |
"label": "requests",
|
| 229 |
"kind": 6,
|
|
@@ -289,16 +370,16 @@
|
|
| 289 |
"kind": 2,
|
| 290 |
"importPath": "app",
|
| 291 |
"description": "app",
|
| 292 |
-
"peekOfCode": "def load_book_data():\n \"\"\"加载书籍数据\"\"\"\n global BOOK_DATA\n
|
| 293 |
"detail": "app",
|
| 294 |
"documentation": {}
|
| 295 |
},
|
| 296 |
{
|
| 297 |
-
"label": "
|
| 298 |
"kind": 2,
|
| 299 |
"importPath": "app",
|
| 300 |
"description": "app",
|
| 301 |
-
"peekOfCode": "def
|
| 302 |
"detail": "app",
|
| 303 |
"documentation": {}
|
| 304 |
},
|
|
@@ -307,7 +388,16 @@
|
|
| 307 |
"kind": 2,
|
| 308 |
"importPath": "app",
|
| 309 |
"description": "app",
|
| 310 |
-
"peekOfCode": "def index():\n \"\"\"主页\"\"\"\n return send_from_directory('.', 'index.html')\n@app.route('/<path:filename>')\ndef serve_static(filename):\n \"\"\"提供根目录下的静态文件(如 style.css, script.js)\"\"\"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
"detail": "app",
|
| 312 |
"documentation": {}
|
| 313 |
},
|
|
@@ -316,7 +406,7 @@
|
|
| 316 |
"kind": 2,
|
| 317 |
"importPath": "app",
|
| 318 |
"description": "app",
|
| 319 |
-
"peekOfCode": "def serve_static(filename):\n \"\"\"提供根目录下的静态文件(如 style.css, script.js)\"\"\"\n return send_from_directory('.', filename)\n# ============================================================================\n# 路由:API 端点\n# ============================================================================\n@app.route('/api/health')
|
| 320 |
"detail": "app",
|
| 321 |
"documentation": {}
|
| 322 |
},
|
|
@@ -325,7 +415,7 @@
|
|
| 325 |
"kind": 2,
|
| 326 |
"importPath": "app",
|
| 327 |
"description": "app",
|
| 328 |
-
"peekOfCode": "def health_check():\n \"\"\"健康检查端点\"\"\"\n return jsonify({\n 'status': 'healthy',\n 'timestamp': datetime.now().isoformat(),\n 'version': '2.0.0-flask'\n })\n@app.route('/api/book/info')\ndef get_book_info():\n \"\"\"
|
| 329 |
"detail": "app",
|
| 330 |
"documentation": {}
|
| 331 |
},
|
|
@@ -334,7 +424,7 @@
|
|
| 334 |
"kind": 2,
|
| 335 |
"importPath": "app",
|
| 336 |
"description": "app",
|
| 337 |
-
"peekOfCode": "def get_book_info():\n \"\"\"获取书籍信息\"\"\"\n if not
|
| 338 |
"detail": "app",
|
| 339 |
"documentation": {}
|
| 340 |
},
|
|
@@ -343,7 +433,7 @@
|
|
| 343 |
"kind": 2,
|
| 344 |
"importPath": "app",
|
| 345 |
"description": "app",
|
| 346 |
-
"peekOfCode": "def get_page_content(page_num):\n \"\"\"获取指定页面内容\"\"\"\n if not
|
| 347 |
"detail": "app",
|
| 348 |
"documentation": {}
|
| 349 |
},
|
|
@@ -392,6 +482,87 @@
|
|
| 392 |
"detail": "app",
|
| 393 |
"documentation": {}
|
| 394 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
{
|
| 396 |
"label": "not_found",
|
| 397 |
"kind": 2,
|
|
@@ -428,12 +599,21 @@
|
|
| 428 |
"detail": "app",
|
| 429 |
"documentation": {}
|
| 430 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
{
|
| 432 |
"label": "initialize_app",
|
| 433 |
"kind": 2,
|
| 434 |
"importPath": "app",
|
| 435 |
"description": "app",
|
| 436 |
-
"peekOfCode": "def initialize_app():\n \"\"\"初始化应用\"\"\"\n print(\"🚀
|
| 437 |
"detail": "app",
|
| 438 |
"documentation": {}
|
| 439 |
},
|
|
@@ -487,7 +667,7 @@
|
|
| 487 |
"kind": 5,
|
| 488 |
"importPath": "app",
|
| 489 |
"description": "app",
|
| 490 |
-
"peekOfCode": "OPENSEARCH_CONFIG = {\n 'host': os.environ.get('OPENSEARCH_HOST', '192.168.3.33'),\n 'port': int(os.environ.get('OPENSEARCH_PORT', 9200)),\n 'use_ssl': os.environ.get('OPENSEARCH_USE_SSL', 'False').lower() == 'true',\n}\n# 全局变量:存储学习数据\nBOOK_DATA = None\ndef get_client_ip():
|
| 491 |
"detail": "app",
|
| 492 |
"documentation": {}
|
| 493 |
},
|
|
@@ -496,7 +676,16 @@
|
|
| 496 |
"kind": 5,
|
| 497 |
"importPath": "app",
|
| 498 |
"description": "app",
|
| 499 |
-
"peekOfCode": "BOOK_DATA = None\ndef get_client_ip():\n \"\"\"\n 获取客户端真实 IP 地址\n 在代理服务器(如 Hugging Face Spaces)后面时,需要检查代理头部\n \"\"\"\n # 按优先级检查各种代理头部\n headers_to_check = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
"detail": "app",
|
| 501 |
"documentation": {}
|
| 502 |
},
|
|
@@ -563,6 +752,51 @@
|
|
| 563 |
"detail": "config",
|
| 564 |
"documentation": {}
|
| 565 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
{
|
| 567 |
"label": "main",
|
| 568 |
"kind": 2,
|
|
@@ -635,6 +869,15 @@
|
|
| 635 |
"detail": "download_resources",
|
| 636 |
"documentation": {}
|
| 637 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 638 |
{
|
| 639 |
"label": "on_starting",
|
| 640 |
"kind": 2,
|
|
@@ -815,6 +1058,96 @@
|
|
| 815 |
"detail": "gunicorn_config",
|
| 816 |
"documentation": {}
|
| 817 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 818 |
{
|
| 819 |
"label": "OpenSearchClient",
|
| 820 |
"kind": 6,
|
|
@@ -877,5 +1210,68 @@
|
|
| 877 |
"peekOfCode": "def show_example_logs():\n \"\"\"显示日志示例\"\"\"\n print(\"\\n\" + \"=\" * 60)\n print(\"📝 日志输出示例\")\n print(\"=\" * 60)\n examples = [\n \"[2025-10-16 18:58:23] INFO in app: [203.0.113.1] GET /api/health | UA: Mozilla/5.0 (Windows NT 10.0)\",\n \"[2025-10-16 18:58:24] INFO in app: [198.51.100.1] POST /api/search | UA: Mozilla/5.0 (iPhone; CPU iPhone OS)\",\n \"[2025-10-16 18:58:25] INFO in app: [192.0.2.1] GET /api/book/info | UA: curl/7.68.0\",\n ]",
|
| 878 |
"detail": "test_ip_logging",
|
| 879 |
"documentation": {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 880 |
}
|
| 881 |
]
|
|
|
|
| 43 |
"detail": "datetime",
|
| 44 |
"documentation": {}
|
| 45 |
},
|
| 46 |
+
{
|
| 47 |
+
"label": "datetime",
|
| 48 |
+
"importPath": "datetime",
|
| 49 |
+
"description": "datetime",
|
| 50 |
+
"isExtraImport": true,
|
| 51 |
+
"detail": "datetime",
|
| 52 |
+
"documentation": {}
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"label": "Path",
|
| 56 |
+
"importPath": "pathlib",
|
| 57 |
+
"description": "pathlib",
|
| 58 |
+
"isExtraImport": true,
|
| 59 |
+
"detail": "pathlib",
|
| 60 |
+
"documentation": {}
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"label": "Path",
|
| 64 |
+
"importPath": "pathlib",
|
| 65 |
+
"description": "pathlib",
|
| 66 |
+
"isExtraImport": true,
|
| 67 |
+
"detail": "pathlib",
|
| 68 |
+
"documentation": {}
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"label": "Path",
|
| 72 |
+
"importPath": "pathlib",
|
| 73 |
+
"description": "pathlib",
|
| 74 |
+
"isExtraImport": true,
|
| 75 |
+
"detail": "pathlib",
|
| 76 |
+
"documentation": {}
|
| 77 |
+
},
|
| 78 |
{
|
| 79 |
"label": "Path",
|
| 80 |
"importPath": "pathlib",
|
|
|
|
| 180 |
"detail": "logging.handlers",
|
| 181 |
"documentation": {}
|
| 182 |
},
|
| 183 |
+
{
|
| 184 |
+
"label": "get_db_instance",
|
| 185 |
+
"importPath": "db_manager",
|
| 186 |
+
"description": "db_manager",
|
| 187 |
+
"isExtraImport": true,
|
| 188 |
+
"detail": "db_manager",
|
| 189 |
+
"documentation": {}
|
| 190 |
+
},
|
| 191 |
{
|
| 192 |
"label": "re",
|
| 193 |
"kind": 6,
|
|
|
|
| 255 |
"detail": "typing",
|
| 256 |
"documentation": {}
|
| 257 |
},
|
| 258 |
+
{
|
| 259 |
+
"label": "Dict",
|
| 260 |
+
"importPath": "typing",
|
| 261 |
+
"description": "typing",
|
| 262 |
+
"isExtraImport": true,
|
| 263 |
+
"detail": "typing",
|
| 264 |
+
"documentation": {}
|
| 265 |
+
},
|
| 266 |
+
{
|
| 267 |
+
"label": "List",
|
| 268 |
+
"importPath": "typing",
|
| 269 |
+
"description": "typing",
|
| 270 |
+
"isExtraImport": true,
|
| 271 |
+
"detail": "typing",
|
| 272 |
+
"documentation": {}
|
| 273 |
+
},
|
| 274 |
+
{
|
| 275 |
+
"label": "Optional",
|
| 276 |
+
"importPath": "typing",
|
| 277 |
+
"description": "typing",
|
| 278 |
+
"isExtraImport": true,
|
| 279 |
+
"detail": "typing",
|
| 280 |
+
"documentation": {}
|
| 281 |
+
},
|
| 282 |
+
{
|
| 283 |
+
"label": "Tuple",
|
| 284 |
+
"importPath": "typing",
|
| 285 |
+
"description": "typing",
|
| 286 |
+
"isExtraImport": true,
|
| 287 |
+
"detail": "typing",
|
| 288 |
+
"documentation": {}
|
| 289 |
+
},
|
| 290 |
{
|
| 291 |
"label": "hashlib",
|
| 292 |
"kind": 6,
|
|
|
|
| 296 |
"detail": "hashlib",
|
| 297 |
"documentation": {}
|
| 298 |
},
|
| 299 |
+
{
|
| 300 |
+
"label": "sqlite3",
|
| 301 |
+
"kind": 6,
|
| 302 |
+
"isExtraImport": true,
|
| 303 |
+
"importPath": "sqlite3",
|
| 304 |
+
"description": "sqlite3",
|
| 305 |
+
"detail": "sqlite3",
|
| 306 |
+
"documentation": {}
|
| 307 |
+
},
|
| 308 |
{
|
| 309 |
"label": "requests",
|
| 310 |
"kind": 6,
|
|
|
|
| 370 |
"kind": 2,
|
| 371 |
"importPath": "app",
|
| 372 |
"description": "app",
|
| 373 |
+
"peekOfCode": "def load_book_data():\n \"\"\"\n 加载书籍数据(已废弃,保留是为了兼容性)\n 现在使用数据库接口,不再加载JSON文件\n \"\"\"\n global BOOK_DATA\n app.logger.info('ℹ️ load_book_data 已废弃,使用数据库接口')\n # 设置为空字典表示已初始化,但不再使用\n BOOK_DATA = {}\n return True",
|
| 374 |
"detail": "app",
|
| 375 |
"documentation": {}
|
| 376 |
},
|
| 377 |
{
|
| 378 |
+
"label": "init_database",
|
| 379 |
"kind": 2,
|
| 380 |
"importPath": "app",
|
| 381 |
"description": "app",
|
| 382 |
+
"peekOfCode": "def init_database():\n \"\"\"初始化数据库连接\"\"\"\n global DB\n try:\n DB = get_db_instance('books.db')\n app.logger.info('✅ 数据库连接初始化成功')\n return True\n except Exception as e:\n app.logger.error(f'❌ 数据库初始化失败: {e}')\n return False",
|
| 383 |
"detail": "app",
|
| 384 |
"documentation": {}
|
| 385 |
},
|
|
|
|
| 388 |
"kind": 2,
|
| 389 |
"importPath": "app",
|
| 390 |
"description": "app",
|
| 391 |
+
"peekOfCode": "def index():\n \"\"\"主页 - 书籍目录\"\"\"\n return send_from_directory('.', 'index.html')\n@app.route('/reader')\ndef reader():\n \"\"\"阅读页面\"\"\"\n return send_from_directory('.', 'reader.html')\n@app.route('/<path:filename>')\ndef serve_static(filename):\n \"\"\"提供根目录下的静态文件(如 style.css, script.js)\"\"\"",
|
| 392 |
+
"detail": "app",
|
| 393 |
+
"documentation": {}
|
| 394 |
+
},
|
| 395 |
+
{
|
| 396 |
+
"label": "reader",
|
| 397 |
+
"kind": 2,
|
| 398 |
+
"importPath": "app",
|
| 399 |
+
"description": "app",
|
| 400 |
+
"peekOfCode": "def reader():\n \"\"\"阅读页面\"\"\"\n return send_from_directory('.', 'reader.html')\n@app.route('/<path:filename>')\ndef serve_static(filename):\n \"\"\"提供根目录下的静态文件(如 style.css, script.js)\"\"\"\n # 避免与API路由冲突\n if filename.startswith('api/'):\n return jsonify({'error': '接口不存在'}), 404\n return send_from_directory('.', filename)",
|
| 401 |
"detail": "app",
|
| 402 |
"documentation": {}
|
| 403 |
},
|
|
|
|
| 406 |
"kind": 2,
|
| 407 |
"importPath": "app",
|
| 408 |
"description": "app",
|
| 409 |
+
"peekOfCode": "def serve_static(filename):\n \"\"\"提供根目录下的静态文件(如 style.css, script.js)\"\"\"\n # 避免与API路由冲突\n if filename.startswith('api/'):\n return jsonify({'error': '接口不存在'}), 404\n return send_from_directory('.', filename)\n# ============================================================================\n# 路由:API 端点\n# ============================================================================\n@app.route('/api/health')",
|
| 410 |
"detail": "app",
|
| 411 |
"documentation": {}
|
| 412 |
},
|
|
|
|
| 415 |
"kind": 2,
|
| 416 |
"importPath": "app",
|
| 417 |
"description": "app",
|
| 418 |
+
"peekOfCode": "def health_check():\n \"\"\"健康检查端点\"\"\"\n return jsonify({\n 'status': 'healthy',\n 'timestamp': datetime.now().isoformat(),\n 'version': '2.0.0-flask'\n })\n@app.route('/api/book/info')\ndef get_book_info():\n \"\"\"",
|
| 419 |
"detail": "app",
|
| 420 |
"documentation": {}
|
| 421 |
},
|
|
|
|
| 424 |
"kind": 2,
|
| 425 |
"importPath": "app",
|
| 426 |
"description": "app",
|
| 427 |
+
"peekOfCode": "def get_book_info():\n \"\"\"\n 获取书籍信息(旧接口,保留兼容性)\n 推荐使用: /api/v2/books/<book_id>\n \"\"\"\n try:\n if not DB:\n return jsonify({'error': '数据库未初始化'}), 500\n # 默认获取第一本书的信息(为了兼容旧版本)\n books = DB.get_all_books()",
|
| 428 |
"detail": "app",
|
| 429 |
"documentation": {}
|
| 430 |
},
|
|
|
|
| 433 |
"kind": 2,
|
| 434 |
"importPath": "app",
|
| 435 |
"description": "app",
|
| 436 |
+
"peekOfCode": "def get_page_content(page_num):\n \"\"\"\n 获取指定页面内容(旧接口,保留兼容性)\n 推荐使用: /api/v2/books/<book_id>/pages/<page_num>\n \"\"\"\n try:\n if not DB:\n return jsonify({'error': '数据库未初始化'}), 500\n # 获取最后导入的书籍\n books = DB.get_all_books()",
|
| 437 |
"detail": "app",
|
| 438 |
"documentation": {}
|
| 439 |
},
|
|
|
|
| 482 |
"detail": "app",
|
| 483 |
"documentation": {}
|
| 484 |
},
|
| 485 |
+
{
|
| 486 |
+
"label": "get_books",
|
| 487 |
+
"kind": 2,
|
| 488 |
+
"importPath": "app",
|
| 489 |
+
"description": "app",
|
| 490 |
+
"peekOfCode": "def get_books():\n \"\"\"\n 获取所有书籍列表\n Query Parameters:\n grade_id: 年级ID(可选)\n Returns:\n {\n \"success\": true,\n \"count\": 10,\n \"books\": [",
|
| 491 |
+
"detail": "app",
|
| 492 |
+
"documentation": {}
|
| 493 |
+
},
|
| 494 |
+
{
|
| 495 |
+
"label": "get_book_info_v2",
|
| 496 |
+
"kind": 2,
|
| 497 |
+
"importPath": "app",
|
| 498 |
+
"description": "app",
|
| 499 |
+
"peekOfCode": "def get_book_info_v2(book_id):\n \"\"\"\n 获取指定书籍的详细信息\n Args:\n book_id: 书籍ID\n Returns:\n {\n \"success\": true,\n \"book\": {\n \"book_id\": 1,",
|
| 500 |
+
"detail": "app",
|
| 501 |
+
"documentation": {}
|
| 502 |
+
},
|
| 503 |
+
{
|
| 504 |
+
"label": "get_book_pages",
|
| 505 |
+
"kind": 2,
|
| 506 |
+
"importPath": "app",
|
| 507 |
+
"description": "app",
|
| 508 |
+
"peekOfCode": "def get_book_pages(book_id):\n \"\"\"\n 获取书籍的所有页面列表(不含片段内容)\n Args:\n book_id: 书籍ID\n Returns:\n {\n \"success\": true,\n \"book_id\": 168,\n \"book_name\": \"一年级上册\",",
|
| 509 |
+
"detail": "app",
|
| 510 |
+
"documentation": {}
|
| 511 |
+
},
|
| 512 |
+
{
|
| 513 |
+
"label": "get_book_catalog",
|
| 514 |
+
"kind": 2,
|
| 515 |
+
"importPath": "app",
|
| 516 |
+
"description": "app",
|
| 517 |
+
"peekOfCode": "def get_book_catalog(book_id):\n \"\"\"\n 获取书籍目录结构(章节目录)\n Args:\n book_id: 书籍ID\n Returns:\n {\n \"success\": true,\n \"book_id\": 168,\n \"book_name\": \"一年级上册\",",
|
| 518 |
+
"detail": "app",
|
| 519 |
+
"documentation": {}
|
| 520 |
+
},
|
| 521 |
+
{
|
| 522 |
+
"label": "get_page_content_v2",
|
| 523 |
+
"kind": 2,
|
| 524 |
+
"importPath": "app",
|
| 525 |
+
"description": "app",
|
| 526 |
+
"peekOfCode": "def get_page_content_v2(book_id, page_num):\n \"\"\"\n 获取指定页面的完整内容\n Args:\n book_id: 书籍ID\n page_num: 页码\n Returns:\n {\n \"success\": true,\n \"book_id\": 1,",
|
| 527 |
+
"detail": "app",
|
| 528 |
+
"documentation": {}
|
| 529 |
+
},
|
| 530 |
+
{
|
| 531 |
+
"label": "search_book_content",
|
| 532 |
+
"kind": 2,
|
| 533 |
+
"importPath": "app",
|
| 534 |
+
"description": "app",
|
| 535 |
+
"peekOfCode": "def search_book_content(book_id):\n \"\"\"\n 在书籍中搜索内容\n Query Parameters:\n keyword: 搜索关键词\n limit: 返回结果数量(默认20)\n Returns:\n {\n \"success\": true,\n \"book_id\": 168,",
|
| 536 |
+
"detail": "app",
|
| 537 |
+
"documentation": {}
|
| 538 |
+
},
|
| 539 |
+
{
|
| 540 |
+
"label": "search_all_content",
|
| 541 |
+
"kind": 2,
|
| 542 |
+
"importPath": "app",
|
| 543 |
+
"description": "app",
|
| 544 |
+
"peekOfCode": "def search_all_content():\n \"\"\"\n 在所有书籍中搜索内容\n Query Parameters:\n keyword: 搜索关键词\n limit: 返回结果数量(默认50)\n Returns:\n {\n \"success\": true,\n \"keyword\": \"hello\",",
|
| 545 |
+
"detail": "app",
|
| 546 |
+
"documentation": {}
|
| 547 |
+
},
|
| 548 |
+
{
|
| 549 |
+
"label": "get_book_statistics",
|
| 550 |
+
"kind": 2,
|
| 551 |
+
"importPath": "app",
|
| 552 |
+
"description": "app",
|
| 553 |
+
"peekOfCode": "def get_book_statistics(book_id):\n \"\"\"\n 获取书籍统计信息\n Args:\n book_id: 书籍ID\n Returns:\n {\n \"success\": true,\n \"statistics\": {\n \"book_id\": 168,",
|
| 554 |
+
"detail": "app",
|
| 555 |
+
"documentation": {}
|
| 556 |
+
},
|
| 557 |
+
{
|
| 558 |
+
"label": "get_overall_statistics",
|
| 559 |
+
"kind": 2,
|
| 560 |
+
"importPath": "app",
|
| 561 |
+
"description": "app",
|
| 562 |
+
"peekOfCode": "def get_overall_statistics():\n \"\"\"\n 获取整体统计信息\n Returns:\n {\n \"success\": true,\n \"statistics\": {\n \"total_books\": 30,\n \"total_pages\": 2000,\n \"total_pieces\": 50000,",
|
| 563 |
+
"detail": "app",
|
| 564 |
+
"documentation": {}
|
| 565 |
+
},
|
| 566 |
{
|
| 567 |
"label": "not_found",
|
| 568 |
"kind": 2,
|
|
|
|
| 599 |
"detail": "app",
|
| 600 |
"documentation": {}
|
| 601 |
},
|
| 602 |
+
{
|
| 603 |
+
"label": "auto_generate_database",
|
| 604 |
+
"kind": 2,
|
| 605 |
+
"importPath": "app",
|
| 606 |
+
"description": "app",
|
| 607 |
+
"peekOfCode": "def auto_generate_database():\n \"\"\"\n 自动生成数据库\n 如果 books.db 不存在,自动运行导入脚本生成数据库\n \"\"\"\n db_path = 'books.db'\n schema_path = 'db_schema.sql'\n data_dir = 'books_api_data'\n # 检查必要的文件和目录\n if not os.path.exists(schema_path):",
|
| 608 |
+
"detail": "app",
|
| 609 |
+
"documentation": {}
|
| 610 |
+
},
|
| 611 |
{
|
| 612 |
"label": "initialize_app",
|
| 613 |
"kind": 2,
|
| 614 |
"importPath": "app",
|
| 615 |
"description": "app",
|
| 616 |
+
"peekOfCode": "def initialize_app():\n \"\"\"初始化应用\"\"\"\n print(\"🚀 交互式英语学习应用 - Flask 版本\")\n print(\"=\" * 60)\n # 设置日志\n setup_logging()\n # 检查必要文件(暂时不检查 books.db,因为会自动生成)\n if not os.path.exists('index.html'):\n app.logger.error(f\"❌ 缺少必要文件: index.html\")\n return False",
|
| 617 |
"detail": "app",
|
| 618 |
"documentation": {}
|
| 619 |
},
|
|
|
|
| 667 |
"kind": 5,
|
| 668 |
"importPath": "app",
|
| 669 |
"description": "app",
|
| 670 |
+
"peekOfCode": "OPENSEARCH_CONFIG = {\n 'host': os.environ.get('OPENSEARCH_HOST', '192.168.3.33'),\n 'port': int(os.environ.get('OPENSEARCH_PORT', 9200)),\n 'use_ssl': os.environ.get('OPENSEARCH_USE_SSL', 'False').lower() == 'true',\n}\n# 全局变量:存储学习数据\nBOOK_DATA = None\n# 数据库实例\nDB = None\ndef get_client_ip():",
|
| 671 |
"detail": "app",
|
| 672 |
"documentation": {}
|
| 673 |
},
|
|
|
|
| 676 |
"kind": 5,
|
| 677 |
"importPath": "app",
|
| 678 |
"description": "app",
|
| 679 |
+
"peekOfCode": "BOOK_DATA = None\n# 数据库实例\nDB = None\ndef get_client_ip():\n \"\"\"\n 获取客户端真实 IP 地址\n 在代理服务器(如 Hugging Face Spaces)后面时,需要检查代理头部\n \"\"\"\n # 按优先级检查各种代理头部\n headers_to_check = [",
|
| 680 |
+
"detail": "app",
|
| 681 |
+
"documentation": {}
|
| 682 |
+
},
|
| 683 |
+
{
|
| 684 |
+
"label": "DB",
|
| 685 |
+
"kind": 5,
|
| 686 |
+
"importPath": "app",
|
| 687 |
+
"description": "app",
|
| 688 |
+
"peekOfCode": "DB = None\ndef get_client_ip():\n \"\"\"\n 获取客户端真实 IP 地址\n 在代理服务器(如 Hugging Face Spaces)后面时,需要检查代理头部\n \"\"\"\n # 按优先级检查各种代理头部\n headers_to_check = [\n 'X-Forwarded-For',\n 'X-Real-IP',",
|
| 689 |
"detail": "app",
|
| 690 |
"documentation": {}
|
| 691 |
},
|
|
|
|
| 752 |
"detail": "config",
|
| 753 |
"documentation": {}
|
| 754 |
},
|
| 755 |
+
{
|
| 756 |
+
"label": "DatabaseManager",
|
| 757 |
+
"kind": 6,
|
| 758 |
+
"importPath": "db_manager",
|
| 759 |
+
"description": "db_manager",
|
| 760 |
+
"peekOfCode": "class DatabaseManager:\n \"\"\"数据库管理器类\"\"\"\n def __init__(self, db_path: str = 'books.db'):\n \"\"\"\n 初始化数据库管理器\n Args:\n db_path: 数据库文件路径\n \"\"\"\n self.db_path = db_path\n self.conn = None",
|
| 761 |
+
"detail": "db_manager",
|
| 762 |
+
"documentation": {}
|
| 763 |
+
},
|
| 764 |
+
{
|
| 765 |
+
"label": "get_db_instance",
|
| 766 |
+
"kind": 2,
|
| 767 |
+
"importPath": "db_manager",
|
| 768 |
+
"description": "db_manager",
|
| 769 |
+
"peekOfCode": "def get_db_instance(db_path: str = 'books.db') -> DatabaseManager:\n \"\"\"\n 获取数据库管理器单例\n Args:\n db_path: 数据库文件路径\n Returns:\n DatabaseManager实例\n \"\"\"\n global _db_instance\n if _db_instance is None:",
|
| 770 |
+
"detail": "db_manager",
|
| 771 |
+
"documentation": {}
|
| 772 |
+
},
|
| 773 |
+
{
|
| 774 |
+
"label": "close_db",
|
| 775 |
+
"kind": 2,
|
| 776 |
+
"importPath": "db_manager",
|
| 777 |
+
"description": "db_manager",
|
| 778 |
+
"peekOfCode": "def close_db():\n \"\"\"关闭全局数据库连接\"\"\"\n global _db_instance\n if _db_instance:\n _db_instance.close()\n _db_instance = None",
|
| 779 |
+
"detail": "db_manager",
|
| 780 |
+
"documentation": {}
|
| 781 |
+
},
|
| 782 |
+
{
|
| 783 |
+
"label": "logger",
|
| 784 |
+
"kind": 5,
|
| 785 |
+
"importPath": "db_manager",
|
| 786 |
+
"description": "db_manager",
|
| 787 |
+
"peekOfCode": "logger = logging.getLogger(__name__)\nclass DatabaseManager:\n \"\"\"数据库管理器类\"\"\"\n def __init__(self, db_path: str = 'books.db'):\n \"\"\"\n 初始化数据库管理器\n Args:\n db_path: 数据库文件路径\n \"\"\"\n self.db_path = db_path",
|
| 788 |
+
"detail": "db_manager",
|
| 789 |
+
"documentation": {}
|
| 790 |
+
},
|
| 791 |
+
{
|
| 792 |
+
"label": "_db_instance",
|
| 793 |
+
"kind": 5,
|
| 794 |
+
"importPath": "db_manager",
|
| 795 |
+
"description": "db_manager",
|
| 796 |
+
"peekOfCode": "_db_instance = None\ndef get_db_instance(db_path: str = 'books.db') -> DatabaseManager:\n \"\"\"\n 获取数据库管理器单例\n Args:\n db_path: 数据库文件路径\n Returns:\n DatabaseManager实例\n \"\"\"\n global _db_instance",
|
| 797 |
+
"detail": "db_manager",
|
| 798 |
+
"documentation": {}
|
| 799 |
+
},
|
| 800 |
{
|
| 801 |
"label": "main",
|
| 802 |
"kind": 2,
|
|
|
|
| 869 |
"detail": "download_resources",
|
| 870 |
"documentation": {}
|
| 871 |
},
|
| 872 |
+
{
|
| 873 |
+
"label": "extract_data_field",
|
| 874 |
+
"kind": 2,
|
| 875 |
+
"importPath": "extract_data",
|
| 876 |
+
"description": "extract_data",
|
| 877 |
+
"peekOfCode": "def extract_data_field(input_file, output_file):\n \"\"\"\n 从输入文件中提取Data字段并保存到输出文件\n Args:\n input_file: 输入JSON文件路径\n output_file: 输出JSON文件路径\n \"\"\"\n try:\n # 读取输入文件\n print(f\"正在读取文件: {input_file}\")",
|
| 878 |
+
"detail": "extract_data",
|
| 879 |
+
"documentation": {}
|
| 880 |
+
},
|
| 881 |
{
|
| 882 |
"label": "on_starting",
|
| 883 |
"kind": 2,
|
|
|
|
| 1058 |
"detail": "gunicorn_config",
|
| 1059 |
"documentation": {}
|
| 1060 |
},
|
| 1061 |
+
{
|
| 1062 |
+
"label": "create_database",
|
| 1063 |
+
"kind": 2,
|
| 1064 |
+
"importPath": "import_book_data",
|
| 1065 |
+
"description": "import_book_data",
|
| 1066 |
+
"peekOfCode": "def create_database(db_path='books.db', schema_path='db_schema.sql'):\n \"\"\"\n 创建数据库和表结构\n Args:\n db_path: 数据库文件路径\n schema_path: Schema SQL文件路径\n Returns:\n sqlite3.Connection: 数据库连接对象\n \"\"\"\n logger.info(f\"📦 创建数据库: {db_path}\")",
|
| 1067 |
+
"detail": "import_book_data",
|
| 1068 |
+
"documentation": {}
|
| 1069 |
+
},
|
| 1070 |
+
{
|
| 1071 |
+
"label": "import_book_info",
|
| 1072 |
+
"kind": 2,
|
| 1073 |
+
"importPath": "import_book_data",
|
| 1074 |
+
"description": "import_book_data",
|
| 1075 |
+
"peekOfCode": "def import_book_info(conn, book_dir: Path) -> int:\n \"\"\"\n 导入书籍基本信息\n Args:\n conn: 数据库连接\n book_dir: 书籍目录\n Returns:\n int: 书籍ID\n \"\"\"\n book_info_file = book_dir / \"book_info.json\"",
|
| 1076 |
+
"detail": "import_book_data",
|
| 1077 |
+
"documentation": {}
|
| 1078 |
+
},
|
| 1079 |
+
{
|
| 1080 |
+
"label": "import_pages",
|
| 1081 |
+
"kind": 2,
|
| 1082 |
+
"importPath": "import_book_data",
|
| 1083 |
+
"description": "import_book_data",
|
| 1084 |
+
"peekOfCode": "def import_pages(conn, book_dir: Path, book_id: int):\n \"\"\"\n 导入页面数据\n Args:\n conn: 数据库连接\n book_dir: 书籍目录\n book_id: 书籍ID\n \"\"\"\n pages_file = book_dir / \"pages.json\"\n if not pages_file.exists():",
|
| 1085 |
+
"detail": "import_book_data",
|
| 1086 |
+
"documentation": {}
|
| 1087 |
+
},
|
| 1088 |
+
{
|
| 1089 |
+
"label": "generate_catalog_from_pages",
|
| 1090 |
+
"kind": 2,
|
| 1091 |
+
"importPath": "import_book_data",
|
| 1092 |
+
"description": "import_book_data",
|
| 1093 |
+
"peekOfCode": "def generate_catalog_from_pages(book_dir: Path, book_id: int, pages_data: list) -> list:\n \"\"\"\n 从pages.json自动生成目录结构\n 为每一页生成一个目录项,使用该页第一个piece的文本作为标题\n Args:\n book_dir: 书籍目录\n book_id: 书籍ID\n pages_data: 页面数据列表\n Returns:\n 生成的目录列表",
|
| 1094 |
+
"detail": "import_book_data",
|
| 1095 |
+
"documentation": {}
|
| 1096 |
+
},
|
| 1097 |
+
{
|
| 1098 |
+
"label": "import_catalog",
|
| 1099 |
+
"kind": 2,
|
| 1100 |
+
"importPath": "import_book_data",
|
| 1101 |
+
"description": "import_book_data",
|
| 1102 |
+
"peekOfCode": "def import_catalog(conn, book_dir: Path, book_id: int, pages_data: list = None):\n \"\"\"\n 导入目录数据(强制从pages.json生成)\n Args:\n conn: 数据库连接\n book_dir: 书籍目录\n book_id: 书籍ID\n pages_data: 页面数据(用于自动生成目录)\n \"\"\"\n catalogs = []",
|
| 1103 |
+
"detail": "import_book_data",
|
| 1104 |
+
"documentation": {}
|
| 1105 |
+
},
|
| 1106 |
+
{
|
| 1107 |
+
"label": "import_book",
|
| 1108 |
+
"kind": 2,
|
| 1109 |
+
"importPath": "import_book_data",
|
| 1110 |
+
"description": "import_book_data",
|
| 1111 |
+
"peekOfCode": "def import_book(conn, book_dir: Path):\n \"\"\"\n 导入单本书籍的完整数据\n Args:\n conn: 数据库连接\n book_dir: 书籍目录\n \"\"\"\n book_name = book_dir.name\n logger.info(f\"\\n{'=' * 60}\")\n logger.info(f\"开始导入书籍: {book_name}\")",
|
| 1112 |
+
"detail": "import_book_data",
|
| 1113 |
+
"documentation": {}
|
| 1114 |
+
},
|
| 1115 |
+
{
|
| 1116 |
+
"label": "import_all_books",
|
| 1117 |
+
"kind": 2,
|
| 1118 |
+
"importPath": "import_book_data",
|
| 1119 |
+
"description": "import_book_data",
|
| 1120 |
+
"peekOfCode": "def import_all_books(conn, data_dir: str):\n \"\"\"\n 导入所有书籍\n Args:\n conn: 数据库连接\n data_dir: books_api_data 目录路径\n \"\"\"\n logger.info(\"=\" * 60)\n logger.info(\"开始批量导入书籍数据\")\n logger.info(f\"数据目录: {data_dir}\")",
|
| 1121 |
+
"detail": "import_book_data",
|
| 1122 |
+
"documentation": {}
|
| 1123 |
+
},
|
| 1124 |
+
{
|
| 1125 |
+
"label": "verify_data",
|
| 1126 |
+
"kind": 2,
|
| 1127 |
+
"importPath": "import_book_data",
|
| 1128 |
+
"description": "import_book_data",
|
| 1129 |
+
"peekOfCode": "def verify_data(conn):\n \"\"\"\n 验证导入的数据\n Args:\n conn: 数据库连接\n \"\"\"\n logger.info(\"\\n🔍 验证数据...\")\n cursor = conn.cursor()\n # 统计数据\n cursor.execute('SELECT COUNT(*) FROM books')",
|
| 1130 |
+
"detail": "import_book_data",
|
| 1131 |
+
"documentation": {}
|
| 1132 |
+
},
|
| 1133 |
+
{
|
| 1134 |
+
"label": "main",
|
| 1135 |
+
"kind": 2,
|
| 1136 |
+
"importPath": "import_book_data",
|
| 1137 |
+
"description": "import_book_data",
|
| 1138 |
+
"peekOfCode": "def main():\n \"\"\"主函数\"\"\"\n import argparse\n print(\"=\" * 80)\n print(\"📚 Books API Data 导入工具\")\n print(\"=\" * 80)\n # 命令行参数解析\n parser = argparse.ArgumentParser(description='导入 books_api_data 数据到 SQLite 数据库')\n parser.add_argument('data_dir', nargs='?', default='books_api_data',\n help='books_api_data 数据目录路径 (默认: books_api_data)')",
|
| 1139 |
+
"detail": "import_book_data",
|
| 1140 |
+
"documentation": {}
|
| 1141 |
+
},
|
| 1142 |
+
{
|
| 1143 |
+
"label": "logger",
|
| 1144 |
+
"kind": 5,
|
| 1145 |
+
"importPath": "import_book_data",
|
| 1146 |
+
"description": "import_book_data",
|
| 1147 |
+
"peekOfCode": "logger = logging.getLogger(__name__)\ndef create_database(db_path='books.db', schema_path='db_schema.sql'):\n \"\"\"\n 创建数据库和表结构\n Args:\n db_path: 数据库文件路径\n schema_path: Schema SQL文件路径\n Returns:\n sqlite3.Connection: 数据库连接对象\n \"\"\"",
|
| 1148 |
+
"detail": "import_book_data",
|
| 1149 |
+
"documentation": {}
|
| 1150 |
+
},
|
| 1151 |
{
|
| 1152 |
"label": "OpenSearchClient",
|
| 1153 |
"kind": 6,
|
|
|
|
| 1210 |
"peekOfCode": "def show_example_logs():\n \"\"\"显示日志示例\"\"\"\n print(\"\\n\" + \"=\" * 60)\n print(\"📝 日志输出示例\")\n print(\"=\" * 60)\n examples = [\n \"[2025-10-16 18:58:23] INFO in app: [203.0.113.1] GET /api/health | UA: Mozilla/5.0 (Windows NT 10.0)\",\n \"[2025-10-16 18:58:24] INFO in app: [198.51.100.1] POST /api/search | UA: Mozilla/5.0 (iPhone; CPU iPhone OS)\",\n \"[2025-10-16 18:58:25] INFO in app: [192.0.2.1] GET /api/book/info | UA: curl/7.68.0\",\n ]",
|
| 1211 |
"detail": "test_ip_logging",
|
| 1212 |
"documentation": {}
|
| 1213 |
+
},
|
| 1214 |
+
{
|
| 1215 |
+
"label": "print_status",
|
| 1216 |
+
"kind": 2,
|
| 1217 |
+
"importPath": "test_setup",
|
| 1218 |
+
"description": "test_setup",
|
| 1219 |
+
"peekOfCode": "def print_status(message, status):\n \"\"\"打印状态信息\"\"\"\n if status:\n print(f\"✅ {message}\")\n else:\n print(f\"❌ {message}\")\n return status\ndef check_python_version():\n \"\"\"检查Python版本\"\"\"\n version = sys.version_info",
|
| 1220 |
+
"detail": "test_setup",
|
| 1221 |
+
"documentation": {}
|
| 1222 |
+
},
|
| 1223 |
+
{
|
| 1224 |
+
"label": "check_python_version",
|
| 1225 |
+
"kind": 2,
|
| 1226 |
+
"importPath": "test_setup",
|
| 1227 |
+
"description": "test_setup",
|
| 1228 |
+
"peekOfCode": "def check_python_version():\n \"\"\"检查Python版本\"\"\"\n version = sys.version_info\n is_ok = version.major == 3 and version.minor >= 12\n print_status(\n f\"Python版本: {version.major}.{version.minor}.{version.micro} {'(需要 3.12+)' if not is_ok else ''}\",\n is_ok\n )\n return is_ok\ndef check_dependencies():",
|
| 1229 |
+
"detail": "test_setup",
|
| 1230 |
+
"documentation": {}
|
| 1231 |
+
},
|
| 1232 |
+
{
|
| 1233 |
+
"label": "check_dependencies",
|
| 1234 |
+
"kind": 2,
|
| 1235 |
+
"importPath": "test_setup",
|
| 1236 |
+
"description": "test_setup",
|
| 1237 |
+
"peekOfCode": "def check_dependencies():\n \"\"\"检查Python依赖\"\"\"\n dependencies = ['flask', 'flask_cors']\n all_ok = True\n for dep in dependencies:\n try:\n __import__(dep)\n print_status(f\"Python包 '{dep}' 已安装\", True)\n except ImportError:\n print_status(f\"Python包 '{dep}' 未安装\", False)",
|
| 1238 |
+
"detail": "test_setup",
|
| 1239 |
+
"documentation": {}
|
| 1240 |
+
},
|
| 1241 |
+
{
|
| 1242 |
+
"label": "check_files",
|
| 1243 |
+
"kind": 2,
|
| 1244 |
+
"importPath": "test_setup",
|
| 1245 |
+
"description": "test_setup",
|
| 1246 |
+
"peekOfCode": "def check_files():\n \"\"\"检查必要文件\"\"\"\n files = [\n 'app.py',\n 'db_manager.py',\n 'import_book_data.py',\n 'db_schema.sql',\n 'index.html',\n 'reader.html',\n 'static/js/catalog.js',",
|
| 1247 |
+
"detail": "test_setup",
|
| 1248 |
+
"documentation": {}
|
| 1249 |
+
},
|
| 1250 |
+
{
|
| 1251 |
+
"label": "check_data_directory",
|
| 1252 |
+
"kind": 2,
|
| 1253 |
+
"importPath": "test_setup",
|
| 1254 |
+
"description": "test_setup",
|
| 1255 |
+
"peekOfCode": "def check_data_directory():\n \"\"\"检查数据目录\"\"\"\n data_dir = Path('books_api_data')\n if not data_dir.exists():\n print_status(\"数据目录 'books_api_data' 不存在\", False)\n print(\" 提示: 请将 books_api_data 目录放置在项目根目录下\")\n return False\n # 检查是否有书籍数据\n book_dirs = [d for d in data_dir.iterdir() if d.is_dir()]\n if len(book_dirs) == 0:",
|
| 1256 |
+
"detail": "test_setup",
|
| 1257 |
+
"documentation": {}
|
| 1258 |
+
},
|
| 1259 |
+
{
|
| 1260 |
+
"label": "check_database",
|
| 1261 |
+
"kind": 2,
|
| 1262 |
+
"importPath": "test_setup",
|
| 1263 |
+
"description": "test_setup",
|
| 1264 |
+
"peekOfCode": "def check_database():\n \"\"\"检查数据库\"\"\"\n db_file = Path('books.db')\n if not db_file.exists():\n print_status(\"数据库 'books.db' 不存在\", False)\n print(\" 提示: 运行 'python3 import_book_data.py books_api_data' 来导入数据\")\n return False\n print_status(\"数据库 'books.db' 存在\", True)\n # 检查数据库内容\n try:",
|
| 1265 |
+
"detail": "test_setup",
|
| 1266 |
+
"documentation": {}
|
| 1267 |
+
},
|
| 1268 |
+
{
|
| 1269 |
+
"label": "main",
|
| 1270 |
+
"kind": 2,
|
| 1271 |
+
"importPath": "test_setup",
|
| 1272 |
+
"description": "test_setup",
|
| 1273 |
+
"peekOfCode": "def main():\n \"\"\"主函数\"\"\"\n print(\"=\" * 70)\n print(\"🔍 交互式英语学习平台 - 系统设置检查\")\n print(\"=\" * 70)\n print()\n checks = [\n (\"Python版本\", check_python_version),\n (\"Python依赖\", check_dependencies),\n (\"必要文件\", check_files),",
|
| 1274 |
+
"detail": "test_setup",
|
| 1275 |
+
"documentation": {}
|
| 1276 |
}
|
| 1277 |
]
|
API_DOCUMENTATION.md
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 书籍API文档
|
| 2 |
+
|
| 3 |
+
## 概述
|
| 4 |
+
|
| 5 |
+
这是一套基于SQLite数据库的书籍管理API,提供书籍列表、目录、页面内容查询等功能。
|
| 6 |
+
|
| 7 |
+
## 快速开始
|
| 8 |
+
|
| 9 |
+
### 1. 数据导入
|
| 10 |
+
|
| 11 |
+
首先需要将JSON数据导入到SQLite数据库:
|
| 12 |
+
|
| 13 |
+
```bash
|
| 14 |
+
python import_book_data.py
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
执行成功后,会在当前目录生成 `books.db` 文件。
|
| 18 |
+
|
| 19 |
+
### 2. 启动应用
|
| 20 |
+
|
| 21 |
+
```bash
|
| 22 |
+
python app.py
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
应用默认运行在 `http://localhost:7860`
|
| 26 |
+
|
| 27 |
+
## API端点
|
| 28 |
+
|
| 29 |
+
### 基础信息
|
| 30 |
+
|
| 31 |
+
- **Base URL**: `http://localhost:7860/api/v2`
|
| 32 |
+
- **返回格式**: JSON
|
| 33 |
+
- **编码**: UTF-8
|
| 34 |
+
|
| 35 |
+
---
|
| 36 |
+
|
| 37 |
+
### 1. 获取书籍列表
|
| 38 |
+
|
| 39 |
+
获取所有可用的书籍。
|
| 40 |
+
|
| 41 |
+
**请求**
|
| 42 |
+
|
| 43 |
+
```
|
| 44 |
+
GET /api/v2/books
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
**响应示例**
|
| 48 |
+
|
| 49 |
+
```json
|
| 50 |
+
{
|
| 51 |
+
"success": true,
|
| 52 |
+
"count": 1,
|
| 53 |
+
"books": [
|
| 54 |
+
{
|
| 55 |
+
"book_id": 1,
|
| 56 |
+
"book_name": "书籍ID 10242",
|
| 57 |
+
"source_file": "book_10242.json",
|
| 58 |
+
"total_pages": 64,
|
| 59 |
+
"created_at": "2024-10-16 12:00:00",
|
| 60 |
+
"updated_at": "2024-10-16 12:00:00"
|
| 61 |
+
}
|
| 62 |
+
]
|
| 63 |
+
}
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
---
|
| 67 |
+
|
| 68 |
+
### 2. 获取书籍信息
|
| 69 |
+
|
| 70 |
+
获取指定书籍的详细信息。
|
| 71 |
+
|
| 72 |
+
**请求**
|
| 73 |
+
|
| 74 |
+
```
|
| 75 |
+
GET /api/v2/books/{book_id}
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
**参数**
|
| 79 |
+
|
| 80 |
+
| 参数 | 类型 | 必需 | 说明 |
|
| 81 |
+
|------|------|------|------|
|
| 82 |
+
| book_id | int | 是 | 书籍ID |
|
| 83 |
+
|
| 84 |
+
**响应示例**
|
| 85 |
+
|
| 86 |
+
```json
|
| 87 |
+
{
|
| 88 |
+
"success": true,
|
| 89 |
+
"book": {
|
| 90 |
+
"book_id": 1,
|
| 91 |
+
"book_name": "书籍ID 10242",
|
| 92 |
+
"source_file": "book_10242.json",
|
| 93 |
+
"total_pages": 64,
|
| 94 |
+
"created_at": "2024-10-16 12:00:00",
|
| 95 |
+
"updated_at": "2024-10-16 12:00:00"
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
---
|
| 101 |
+
|
| 102 |
+
### 3. 获取书籍目录
|
| 103 |
+
|
| 104 |
+
获取书籍的所有页面列表(目录)。
|
| 105 |
+
|
| 106 |
+
**请求**
|
| 107 |
+
|
| 108 |
+
```
|
| 109 |
+
GET /api/v2/books/{book_id}/catalog
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
**参数**
|
| 113 |
+
|
| 114 |
+
| 参数 | 类型 | 必需 | 说明 |
|
| 115 |
+
|------|------|------|------|
|
| 116 |
+
| book_id | int | 是 | 书籍ID |
|
| 117 |
+
|
| 118 |
+
**响应示例**
|
| 119 |
+
|
| 120 |
+
```json
|
| 121 |
+
{
|
| 122 |
+
"success": true,
|
| 123 |
+
"book_id": 1,
|
| 124 |
+
"book_name": "书籍ID 10242",
|
| 125 |
+
"total_pages": 64,
|
| 126 |
+
"catalog": [
|
| 127 |
+
{
|
| 128 |
+
"page_id": 34428,
|
| 129 |
+
"page_number": 55,
|
| 130 |
+
"origin_img_url": "https://res-100200.kingsunedu.com/...",
|
| 131 |
+
"encrypt_img_url": "https://res-100200.kingsunedu.com/..."
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"page_id": 34429,
|
| 135 |
+
"page_number": 56,
|
| 136 |
+
"origin_img_url": "https://res-100200.kingsunedu.com/...",
|
| 137 |
+
"encrypt_img_url": "https://res-100200.kingsunedu.com/..."
|
| 138 |
+
}
|
| 139 |
+
]
|
| 140 |
+
}
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
---
|
| 144 |
+
|
| 145 |
+
### 4. 获取单页内容
|
| 146 |
+
|
| 147 |
+
获取指定页面的完整内容,包括所有文本片段、音频等。
|
| 148 |
+
|
| 149 |
+
**请求**
|
| 150 |
+
|
| 151 |
+
```
|
| 152 |
+
GET /api/v2/books/{book_id}/pages/{page_num}
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
**参数**
|
| 156 |
+
|
| 157 |
+
| 参数 | 类型 | 必需 | 说明 |
|
| 158 |
+
|------|------|------|------|
|
| 159 |
+
| book_id | int | 是 | 书籍ID |
|
| 160 |
+
| page_num | int | 是 | 页码 |
|
| 161 |
+
|
| 162 |
+
**响应示例**
|
| 163 |
+
|
| 164 |
+
```json
|
| 165 |
+
{
|
| 166 |
+
"success": true,
|
| 167 |
+
"book_id": 1,
|
| 168 |
+
"book_name": "书籍ID 10242",
|
| 169 |
+
"page": {
|
| 170 |
+
"page_id": 34428,
|
| 171 |
+
"page_number": 55,
|
| 172 |
+
"origin_img_url": "https://res-100200.kingsunedu.com/...",
|
| 173 |
+
"encrypt_img_url": "https://res-100200.kingsunedu.com/...",
|
| 174 |
+
"piece_count": 15,
|
| 175 |
+
"pieces": [
|
| 176 |
+
{
|
| 177 |
+
"piece_id": 207349,
|
| 178 |
+
"original": "Unit 1",
|
| 179 |
+
"translation": "",
|
| 180 |
+
"origin_sound_url": "https://res-100200.kingsunedu.com/...",
|
| 181 |
+
"encrypt_sound_url": "https://res-100200.kingsunedu.com/...",
|
| 182 |
+
"duration": 2,
|
| 183 |
+
"coordinate": {
|
| 184 |
+
"x": 0.1031,
|
| 185 |
+
"y": 0.1281,
|
| 186 |
+
"width": 0.0857,
|
| 187 |
+
"height": 0.0277
|
| 188 |
+
},
|
| 189 |
+
"is_evaluated": 0,
|
| 190 |
+
"show_translation": 1,
|
| 191 |
+
"rich_original": "",
|
| 192 |
+
"rich_translation": ""
|
| 193 |
+
}
|
| 194 |
+
]
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
```
|
| 198 |
+
|
| 199 |
+
---
|
| 200 |
+
|
| 201 |
+
### 5. 搜索内容
|
| 202 |
+
|
| 203 |
+
在书籍中搜索关键词。
|
| 204 |
+
|
| 205 |
+
**请求**
|
| 206 |
+
|
| 207 |
+
```
|
| 208 |
+
GET /api/v2/books/{book_id}/search?keyword={keyword}&limit={limit}
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
**参数**
|
| 212 |
+
|
| 213 |
+
| 参数 | 类型 | 必需 | 说明 |
|
| 214 |
+
|------|------|------|------|
|
| 215 |
+
| book_id | int | 是 | 书籍ID |
|
| 216 |
+
| keyword | string | 是 | 搜索关键词 |
|
| 217 |
+
| limit | int | 否 | 返回结果数量(默认20) |
|
| 218 |
+
|
| 219 |
+
**响应示例**
|
| 220 |
+
|
| 221 |
+
```json
|
| 222 |
+
{
|
| 223 |
+
"success": true,
|
| 224 |
+
"book_id": 1,
|
| 225 |
+
"keyword": "hello",
|
| 226 |
+
"count": 3,
|
| 227 |
+
"results": [
|
| 228 |
+
{
|
| 229 |
+
"page_id": 34428,
|
| 230 |
+
"page_number": 55,
|
| 231 |
+
"piece_id": 207350,
|
| 232 |
+
"original": "Hello, how are you?",
|
| 233 |
+
"translation": "你好,你好吗?"
|
| 234 |
+
}
|
| 235 |
+
]
|
| 236 |
+
}
|
| 237 |
+
```
|
| 238 |
+
|
| 239 |
+
---
|
| 240 |
+
|
| 241 |
+
### 6. 获取统计信息
|
| 242 |
+
|
| 243 |
+
获取书籍的统计数据。
|
| 244 |
+
|
| 245 |
+
**请求**
|
| 246 |
+
|
| 247 |
+
```
|
| 248 |
+
GET /api/v2/books/{book_id}/statistics
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
**参数**
|
| 252 |
+
|
| 253 |
+
| 参数 | 类型 | 必需 | 说明 |
|
| 254 |
+
|------|------|------|------|
|
| 255 |
+
| book_id | int | 是 | 书籍ID |
|
| 256 |
+
|
| 257 |
+
**响应示例**
|
| 258 |
+
|
| 259 |
+
```json
|
| 260 |
+
{
|
| 261 |
+
"success": true,
|
| 262 |
+
"statistics": {
|
| 263 |
+
"book_id": 1,
|
| 264 |
+
"total_pages": 64,
|
| 265 |
+
"total_pieces": 850,
|
| 266 |
+
"total_audio": 820
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
```
|
| 270 |
+
|
| 271 |
+
---
|
| 272 |
+
|
| 273 |
+
## 错误响应
|
| 274 |
+
|
| 275 |
+
所有API在出错时返回统一格式:
|
| 276 |
+
|
| 277 |
+
```json
|
| 278 |
+
{
|
| 279 |
+
"success": false,
|
| 280 |
+
"error": "错误信息描述"
|
| 281 |
+
}
|
| 282 |
+
```
|
| 283 |
+
|
| 284 |
+
**常见错误码**
|
| 285 |
+
|
| 286 |
+
- `400` - 请求参数错误
|
| 287 |
+
- `404` - 资源不存在
|
| 288 |
+
- `500` - 服务器内部错误
|
| 289 |
+
|
| 290 |
+
---
|
| 291 |
+
|
| 292 |
+
## 使用示例
|
| 293 |
+
|
| 294 |
+
### cURL
|
| 295 |
+
|
| 296 |
+
```bash
|
| 297 |
+
# 获取书籍列表
|
| 298 |
+
curl http://localhost:7860/api/v2/books
|
| 299 |
+
|
| 300 |
+
# 获取书籍目录
|
| 301 |
+
curl http://localhost:7860/api/v2/books/1/catalog
|
| 302 |
+
|
| 303 |
+
# 获取第55页内容
|
| 304 |
+
curl http://localhost:7860/api/v2/books/1/pages/55
|
| 305 |
+
|
| 306 |
+
# 搜索内容
|
| 307 |
+
curl "http://localhost:7860/api/v2/books/1/search?keyword=hello&limit=10"
|
| 308 |
+
|
| 309 |
+
# 获取统计信息
|
| 310 |
+
curl http://localhost:7860/api/v2/books/1/statistics
|
| 311 |
+
```
|
| 312 |
+
|
| 313 |
+
### Python
|
| 314 |
+
|
| 315 |
+
```python
|
| 316 |
+
import requests
|
| 317 |
+
|
| 318 |
+
base_url = "http://localhost:7860/api/v2"
|
| 319 |
+
|
| 320 |
+
# 获取书籍列表
|
| 321 |
+
response = requests.get(f"{base_url}/books")
|
| 322 |
+
books = response.json()
|
| 323 |
+
print(books)
|
| 324 |
+
|
| 325 |
+
# 获取第55页内容
|
| 326 |
+
book_id = 1
|
| 327 |
+
page_num = 55
|
| 328 |
+
response = requests.get(f"{base_url}/books/{book_id}/pages/{page_num}")
|
| 329 |
+
page_data = response.json()
|
| 330 |
+
print(page_data)
|
| 331 |
+
|
| 332 |
+
# 搜索
|
| 333 |
+
response = requests.get(f"{base_url}/books/{book_id}/search", params={
|
| 334 |
+
'keyword': 'hello',
|
| 335 |
+
'limit': 10
|
| 336 |
+
})
|
| 337 |
+
search_results = response.json()
|
| 338 |
+
print(search_results)
|
| 339 |
+
```
|
| 340 |
+
|
| 341 |
+
### JavaScript
|
| 342 |
+
|
| 343 |
+
```javascript
|
| 344 |
+
const baseUrl = 'http://localhost:7860/api/v2';
|
| 345 |
+
|
| 346 |
+
// 获取书籍列表
|
| 347 |
+
fetch(`${baseUrl}/books`)
|
| 348 |
+
.then(response => response.json())
|
| 349 |
+
.then(data => console.log(data));
|
| 350 |
+
|
| 351 |
+
// 获取第55页内容
|
| 352 |
+
const bookId = 1;
|
| 353 |
+
const pageNum = 55;
|
| 354 |
+
fetch(`${baseUrl}/books/${bookId}/pages/${pageNum}`)
|
| 355 |
+
.then(response => response.json())
|
| 356 |
+
.then(data => console.log(data));
|
| 357 |
+
|
| 358 |
+
// 搜索
|
| 359 |
+
fetch(`${baseUrl}/books/${bookId}/search?keyword=hello&limit=10`)
|
| 360 |
+
.then(response => response.json())
|
| 361 |
+
.then(data => console.log(data));
|
| 362 |
+
```
|
| 363 |
+
|
| 364 |
+
---
|
| 365 |
+
|
| 366 |
+
## 数据库结构
|
| 367 |
+
|
| 368 |
+
### books 表
|
| 369 |
+
- `book_id`: 书籍ID(主键)
|
| 370 |
+
- `book_name`: 书籍名称
|
| 371 |
+
- `source_file`: 源文件名
|
| 372 |
+
- `total_pages`: 总页数
|
| 373 |
+
- `created_at`: 创建时间
|
| 374 |
+
- `updated_at`: 更新时间
|
| 375 |
+
|
| 376 |
+
### pages 表
|
| 377 |
+
- `page_id`: 页面ID(主键)
|
| 378 |
+
- `book_id`: 所属书籍ID(外键)
|
| 379 |
+
- `page_number`: 页码
|
| 380 |
+
- `origin_img_url`: 原始图片URL
|
| 381 |
+
- `encrypt_img_url`: 加密图片URL
|
| 382 |
+
- `created_at`: 创建时间
|
| 383 |
+
|
| 384 |
+
### pieces 表
|
| 385 |
+
- `piece_id`: 片段ID(主键)
|
| 386 |
+
- `page_id`: 所属页面ID(外键)
|
| 387 |
+
- `original`: 原文
|
| 388 |
+
- `translation`: 翻译
|
| 389 |
+
- `origin_sound_url`: 原始音频URL
|
| 390 |
+
- `encrypt_sound_url`: 加密音频URL
|
| 391 |
+
- `duration`: 音频时长
|
| 392 |
+
- `coordinate_x/y/width/height`: 坐标信息
|
| 393 |
+
- `is_evaluated`: 是否可评测
|
| 394 |
+
- `show_translation`: 是否显示翻译
|
| 395 |
+
- `rich_original`: 富文本原文
|
| 396 |
+
- `rich_translation`: 富文本翻译
|
| 397 |
+
- `created_at`: 创建时间
|
| 398 |
+
|
| 399 |
+
---
|
| 400 |
+
|
| 401 |
+
## 注意事项
|
| 402 |
+
|
| 403 |
+
1. 首次使用前必须运行 `import_book_data.py` 导入数据
|
| 404 |
+
2. 数据库文件 `books.db` 会在导入时自动创建
|
| 405 |
+
3. 所有API响应都是UTF-8编码的JSON格式
|
| 406 |
+
4. 页码从实际页码开始(如本例中从55开始)
|
| 407 |
+
5. 音频URL有时效性,可能需要定期更新
|
| 408 |
+
|
| 409 |
+
---
|
| 410 |
+
|
| 411 |
+
## 支持
|
| 412 |
+
|
| 413 |
+
如有问题,请查看日志文件 `logs/app.log`
|
| 414 |
+
|
BOOKS_API_DATA_文档.md
ADDED
|
@@ -0,0 +1,1232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Books API Data 数据结构与使用文档
|
| 2 |
+
|
| 3 |
+
## 📚 目录概览
|
| 4 |
+
|
| 5 |
+
`books_api_data` 目录包含教材书籍的完整数据,包括书籍元信息、页面内容、音频资源、图片资源和目录结构。每本书独立存储在以 `{BookID}_{BookName}` 命名的目录中。
|
| 6 |
+
|
| 7 |
+
## 📁 目录结构
|
| 8 |
+
|
| 9 |
+
```
|
| 10 |
+
books_api_data/
|
| 11 |
+
├── 168_一年级上册/
|
| 12 |
+
│ ├── book_info.json # 书籍基本信息
|
| 13 |
+
│ ├── pages.json # 页面详细内容(含文本、音频、坐标等)
|
| 14 |
+
│ ├── catalog.json # 书籍目录结构
|
| 15 |
+
│ ├── images/ # 页面图片资源
|
| 16 |
+
│ │ ├── page_001.jpg
|
| 17 |
+
│ │ ├── page_002.jpg
|
| 18 |
+
│ │ ├── ...
|
| 19 |
+
│ │ └── thumbnails/ # 目录缩略图
|
| 20 |
+
│ │ ├── page_002_thumb.jpg
|
| 21 |
+
│ │ └── ...
|
| 22 |
+
│ └── audios/ # 音频资源
|
| 23 |
+
│ ├── page_002_piece_00.mp3
|
| 24 |
+
│ ├── page_002_piece_01.mp3
|
| 25 |
+
│ └── ...
|
| 26 |
+
├── 169_二年级上册/
|
| 27 |
+
├── 263_一年级下册/
|
| 28 |
+
└── ...
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
## 📊 数据结构详解
|
| 34 |
+
|
| 35 |
+
### 1. book_info.json - 书籍基本信息
|
| 36 |
+
|
| 37 |
+
**用途**: 存储书籍的元数据,包括 ID、名称、版本、年级、学科等信息。
|
| 38 |
+
|
| 39 |
+
**字段说明**:
|
| 40 |
+
|
| 41 |
+
```json
|
| 42 |
+
{
|
| 43 |
+
"MarketBookID": 168, // 市场书籍ID(唯一标识)
|
| 44 |
+
"MarketClassifyId": 11, // 市场分类ID
|
| 45 |
+
"MarketBookName": "一年级上册", // 书籍名称
|
| 46 |
+
"Stage": 2, // 学段(1=小学,2=初中,3=高中等)
|
| 47 |
+
"MarketBookCover": "168_一年级上册/images/page_001.jpg", // 封面图片路径
|
| 48 |
+
"MODBookID": 28, // MOD书籍ID
|
| 49 |
+
"MODBookName": "一年级上册", // MOD书籍名称
|
| 50 |
+
"MODBookCover": "", // MOD封面(可为空)
|
| 51 |
+
"OldMODBookID": 0, // 旧MOD书籍ID
|
| 52 |
+
"IsMod": 0, // 是否为MOD(0=否,1=是)
|
| 53 |
+
"CreateDate": "2017-06-02 18:40:54", // 创建时间
|
| 54 |
+
"CreateUser": 0, // 创建用户ID
|
| 55 |
+
"ISBN": null, // ISBN编号(可为空)
|
| 56 |
+
"Type": 0, // 类型
|
| 57 |
+
"SubjectID": 167070462398963715, // 学科ID
|
| 58 |
+
"VerID": 6, // 版本ID
|
| 59 |
+
"GradeID": 40, // 年级ID
|
| 60 |
+
"ReelID": 1, // 学期ID(1=上册,2=下册)
|
| 61 |
+
"AppID": "cf58f368-42ac-452a-ae66-9212a3614fcb", // 应用ID
|
| 62 |
+
"Sort": 7, // 排序序号
|
| 63 |
+
"MarketClassifyName": "沪教版(深圳)", // 市场分类名称
|
| 64 |
+
"OldModClassifyID": null, // 旧MOD分类ID
|
| 65 |
+
"ModClassifyID": 0, // MOD分类ID
|
| 66 |
+
"StartPage": 1, // 起始页码
|
| 67 |
+
"MaxPage": 73 // 最大页码
|
| 68 |
+
}
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
**数据库表设计建议**:
|
| 72 |
+
|
| 73 |
+
```sql
|
| 74 |
+
CREATE TABLE books (
|
| 75 |
+
market_book_id BIGINT PRIMARY KEY,
|
| 76 |
+
market_classify_id INT,
|
| 77 |
+
market_book_name VARCHAR(200),
|
| 78 |
+
stage INT,
|
| 79 |
+
market_book_cover VARCHAR(500),
|
| 80 |
+
mod_book_id INT,
|
| 81 |
+
mod_book_name VARCHAR(200),
|
| 82 |
+
mod_book_cover VARCHAR(500),
|
| 83 |
+
old_mod_book_id INT,
|
| 84 |
+
is_mod TINYINT,
|
| 85 |
+
create_date DATETIME,
|
| 86 |
+
create_user INT,
|
| 87 |
+
isbn VARCHAR(50),
|
| 88 |
+
type INT,
|
| 89 |
+
subject_id BIGINT,
|
| 90 |
+
ver_id INT,
|
| 91 |
+
grade_id INT,
|
| 92 |
+
reel_id INT,
|
| 93 |
+
app_id VARCHAR(100),
|
| 94 |
+
sort INT,
|
| 95 |
+
market_classify_name VARCHAR(100),
|
| 96 |
+
old_mod_classify_id INT,
|
| 97 |
+
mod_classify_id INT,
|
| 98 |
+
start_page INT,
|
| 99 |
+
max_page INT,
|
| 100 |
+
INDEX idx_classify (market_classify_id),
|
| 101 |
+
INDEX idx_grade_subject (grade_id, subject_id),
|
| 102 |
+
INDEX idx_stage (stage)
|
| 103 |
+
);
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
---
|
| 107 |
+
|
| 108 |
+
### 2. pages.json - 页面详细内容
|
| 109 |
+
|
| 110 |
+
**用途**: 存储每一页的详细内容,包括图片路径、文本片段、音频资源、坐标信息等。
|
| 111 |
+
|
| 112 |
+
**数据结构**:
|
| 113 |
+
|
| 114 |
+
```json
|
| 115 |
+
[
|
| 116 |
+
{
|
| 117 |
+
"pageId": 2111, // 页面ID(唯一标识)
|
| 118 |
+
"pageNumber": 2, // 页码
|
| 119 |
+
"originImgUrl": "168_一年级上册/images/page_002.jpg", // 页面图片路径
|
| 120 |
+
"pieces": [ // 页面文本片段数组
|
| 121 |
+
{
|
| 122 |
+
"pieceId": 26342, // 片段ID(唯一标识)
|
| 123 |
+
"original": "Unit 1 Hello", // 原文(英文)
|
| 124 |
+
"translation": "第一单元 你好", // 翻译(中文)
|
| 125 |
+
"originSoundUrl": "168_一年级上册/audios/page_002_piece_00.mp3", // 音频路径
|
| 126 |
+
"duration": 4, // 音频时长(秒)
|
| 127 |
+
"coordinate": { // 文本在页面上的坐标(相对坐标,0-1范围)
|
| 128 |
+
"x": 0.0482, // 左上角X坐标
|
| 129 |
+
"y": 0.0357, // 左上角Y坐标
|
| 130 |
+
"width": 0.3195, // 宽度
|
| 131 |
+
"height": 0.0982 // 高度
|
| 132 |
+
},
|
| 133 |
+
"isEvaluated": 1, // 是否可���测(0=否,1=是)
|
| 134 |
+
"showTranslation": 1, // 是否显示翻译(0=否,1=是)
|
| 135 |
+
"richOriginal": "", // 富文本原文(可为空)
|
| 136 |
+
"richTranslation": "" // 富文本翻译(可为空)
|
| 137 |
+
}
|
| 138 |
+
// ... 更多片段
|
| 139 |
+
]
|
| 140 |
+
}
|
| 141 |
+
// ... 更多页面
|
| 142 |
+
]
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
**关键说明**:
|
| 146 |
+
|
| 147 |
+
- **coordinate**: 坐标系统采用相对坐标(0-1),便于适配不同分辨率
|
| 148 |
+
- `x`, `y`: 文本框左上角相对位置
|
| 149 |
+
- `width`, `height`: 文本框的相对宽高
|
| 150 |
+
|
| 151 |
+
- **音频文件命名规则**: `page_{页码:3位}_piece_{片段索引:2位}.mp3`
|
| 152 |
+
- 例如: `page_002_piece_00.mp3` 表示第2页第0个片段
|
| 153 |
+
|
| 154 |
+
**数据库表设计建议**:
|
| 155 |
+
|
| 156 |
+
```sql
|
| 157 |
+
-- 页面表
|
| 158 |
+
CREATE TABLE book_pages (
|
| 159 |
+
page_id BIGINT PRIMARY KEY,
|
| 160 |
+
book_id BIGINT,
|
| 161 |
+
page_number INT,
|
| 162 |
+
origin_img_url VARCHAR(500),
|
| 163 |
+
INDEX idx_book_page (book_id, page_number),
|
| 164 |
+
FOREIGN KEY (book_id) REFERENCES books(market_book_id)
|
| 165 |
+
);
|
| 166 |
+
|
| 167 |
+
-- 页面片段表
|
| 168 |
+
CREATE TABLE page_pieces (
|
| 169 |
+
piece_id BIGINT PRIMARY KEY,
|
| 170 |
+
page_id BIGINT,
|
| 171 |
+
original TEXT,
|
| 172 |
+
translation TEXT,
|
| 173 |
+
origin_sound_url VARCHAR(500),
|
| 174 |
+
duration INT,
|
| 175 |
+
coordinate_x DECIMAL(10, 6),
|
| 176 |
+
coordinate_y DECIMAL(10, 6),
|
| 177 |
+
coordinate_width DECIMAL(10, 6),
|
| 178 |
+
coordinate_height DECIMAL(10, 6),
|
| 179 |
+
is_evaluated TINYINT,
|
| 180 |
+
show_translation TINYINT,
|
| 181 |
+
rich_original TEXT,
|
| 182 |
+
rich_translation TEXT,
|
| 183 |
+
piece_order INT, -- 片段在页面中的顺序
|
| 184 |
+
INDEX idx_page (page_id),
|
| 185 |
+
FOREIGN KEY (page_id) REFERENCES book_pages(page_id)
|
| 186 |
+
);
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
---
|
| 190 |
+
|
| 191 |
+
### 3. catalog.json - 书籍目录结构
|
| 192 |
+
|
| 193 |
+
**用途**: 存储书籍的章节目录信息,支持层级结构(通过 ParentID)。
|
| 194 |
+
|
| 195 |
+
**数据结构**:
|
| 196 |
+
|
| 197 |
+
```json
|
| 198 |
+
{
|
| 199 |
+
"catalog": [
|
| 200 |
+
{
|
| 201 |
+
"CatalogID": 1, // 目录项ID
|
| 202 |
+
"CatalogName": "Unit 1 Hello", // 目录名称(英文)
|
| 203 |
+
"CatalogNameCN": "第一单元 你好", // 目录名称(中文)
|
| 204 |
+
"StartPage": 2, // 起始页码
|
| 205 |
+
"EndPage": 2, // 结束页码
|
| 206 |
+
"Thumbnail": "168_一年级上册/images/thumbnails/page_002_thumb.jpg", // 缩略图路径
|
| 207 |
+
"ParentID": 0, // 父级目录ID(0表示顶级目录)
|
| 208 |
+
"Sort": 1 // 排序序号
|
| 209 |
+
}
|
| 210 |
+
// ... 更多目录项
|
| 211 |
+
],
|
| 212 |
+
"start_page": 1, // 书籍起始页
|
| 213 |
+
"end_page": 73 // 书籍结束页
|
| 214 |
+
}
|
| 215 |
+
```
|
| 216 |
+
|
| 217 |
+
**数据库表设计建议**:
|
| 218 |
+
|
| 219 |
+
```sql
|
| 220 |
+
CREATE TABLE book_catalogs (
|
| 221 |
+
catalog_id BIGINT PRIMARY KEY,
|
| 222 |
+
book_id BIGINT,
|
| 223 |
+
catalog_name VARCHAR(200),
|
| 224 |
+
catalog_name_cn VARCHAR(200),
|
| 225 |
+
start_page INT,
|
| 226 |
+
end_page INT,
|
| 227 |
+
thumbnail VARCHAR(500),
|
| 228 |
+
parent_id BIGINT,
|
| 229 |
+
sort INT,
|
| 230 |
+
INDEX idx_book (book_id),
|
| 231 |
+
INDEX idx_parent (parent_id),
|
| 232 |
+
FOREIGN KEY (book_id) REFERENCES books(market_book_id)
|
| 233 |
+
);
|
| 234 |
+
```
|
| 235 |
+
|
| 236 |
+
---
|
| 237 |
+
|
| 238 |
+
### 4. images/ - 页面图片资源
|
| 239 |
+
|
| 240 |
+
**目录结构**:
|
| 241 |
+
|
| 242 |
+
```
|
| 243 |
+
images/
|
| 244 |
+
├── page_001.jpg # 第1页图片
|
| 245 |
+
├── page_002.jpg # 第2页图片
|
| 246 |
+
├── ...
|
| 247 |
+
└── thumbnails/ # 缩略图目录
|
| 248 |
+
├── page_002_thumb.jpg
|
| 249 |
+
└── ...
|
| 250 |
+
```
|
| 251 |
+
|
| 252 |
+
**文件命名规则**:
|
| 253 |
+
- 页面图片: `page_{页码:3位}.jpg`
|
| 254 |
+
- 缩略图: `page_{页码:3位}_thumb.jpg`
|
| 255 |
+
|
| 256 |
+
**存储建议**:
|
| 257 |
+
- 推荐使用对象存储(如阿里云OSS、腾讯云COS、AWS S3)
|
| 258 |
+
- 也可使用本地文件系统 + Nginx 提供静态文件服务
|
| 259 |
+
- 数据库中存储相对路径,通过配置拼接完整URL
|
| 260 |
+
|
| 261 |
+
---
|
| 262 |
+
|
| 263 |
+
### 5. audios/ - 音频资源
|
| 264 |
+
|
| 265 |
+
**目录结构**:
|
| 266 |
+
|
| 267 |
+
```
|
| 268 |
+
audios/
|
| 269 |
+
├── page_002_piece_00.mp3
|
| 270 |
+
├── page_002_piece_01.mp3
|
| 271 |
+
├── page_003_piece_00.mp3
|
| 272 |
+
└── ...
|
| 273 |
+
```
|
| 274 |
+
|
| 275 |
+
**文件命名规则**:
|
| 276 |
+
- `page_{页码:3位}_piece_{片段索引:2位}.mp3`
|
| 277 |
+
|
| 278 |
+
**存储建议**:
|
| 279 |
+
- 与图片相同,推荐使用对象存储
|
| 280 |
+
- 支持流式传输以节省带宽
|
| 281 |
+
|
| 282 |
+
---
|
| 283 |
+
|
| 284 |
+
## 🔧 数据导入指南
|
| 285 |
+
|
| 286 |
+
### 导入流程
|
| 287 |
+
|
| 288 |
+
```
|
| 289 |
+
1. 准备数据源
|
| 290 |
+
↓
|
| 291 |
+
2. 创建数据库表结构
|
| 292 |
+
↓
|
| 293 |
+
3. 上传静态资源(images + audios)到存储服务
|
| 294 |
+
↓
|
| 295 |
+
4. 导入书籍元数据(book_info.json)
|
| 296 |
+
↓
|
| 297 |
+
5. 导入页面数据(pages.json)
|
| 298 |
+
↓
|
| 299 |
+
6. 导入目录数据(catalog.json)
|
| 300 |
+
↓
|
| 301 |
+
7. 数据校验
|
| 302 |
+
```
|
| 303 |
+
|
| 304 |
+
### Python 导入脚本示例
|
| 305 |
+
|
| 306 |
+
#### 1. 环境准备
|
| 307 |
+
|
| 308 |
+
```bash
|
| 309 |
+
# 安装依赖(使用 OpenSearch)
|
| 310 |
+
pip install opensearch-py requests pillow
|
| 311 |
+
```
|
| 312 |
+
|
| 313 |
+
```python
|
| 314 |
+
# config.py - 配置文件
|
| 315 |
+
DATABASE_CONFIG = {
|
| 316 |
+
'host': 'your-db-host',
|
| 317 |
+
'port': 3306,
|
| 318 |
+
'user': 'your-username',
|
| 319 |
+
'password': 'your-password',
|
| 320 |
+
'database': 'books_db'
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
OPENSEARCH_CONFIG = {
|
| 324 |
+
'hosts': ['192.168.3.33:9200'],
|
| 325 |
+
'http_auth': ('username', 'password'), # 如果需要认证
|
| 326 |
+
'use_ssl': False,
|
| 327 |
+
'verify_certs': False
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
# 静态资源基础URL(如果使用对象存储)
|
| 331 |
+
STATIC_BASE_URL = "https://your-cdn-domain.com/"
|
| 332 |
+
```
|
| 333 |
+
|
| 334 |
+
#### 2. 数据导入脚本
|
| 335 |
+
|
| 336 |
+
```python
|
| 337 |
+
#!/usr/bin/env python3
|
| 338 |
+
# -*- coding: utf-8 -*-
|
| 339 |
+
"""
|
| 340 |
+
书籍数据导入脚本
|
| 341 |
+
将 books_api_data 目录中的数据导入数据库
|
| 342 |
+
"""
|
| 343 |
+
|
| 344 |
+
import os
|
| 345 |
+
import json
|
| 346 |
+
import logging
|
| 347 |
+
from pathlib import Path
|
| 348 |
+
from typing import Dict, List
|
| 349 |
+
import pymysql
|
| 350 |
+
from opensearchpy import OpenSearch
|
| 351 |
+
from config import DATABASE_CONFIG, OPENSEARCH_CONFIG, STATIC_BASE_URL
|
| 352 |
+
|
| 353 |
+
logging.basicConfig(
|
| 354 |
+
level=logging.INFO,
|
| 355 |
+
format='%(asctime)s - %(levelname)s - %(message)s'
|
| 356 |
+
)
|
| 357 |
+
logger = logging.getLogger(__name__)
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
class BookDataImporter:
|
| 361 |
+
"""书籍数据导入器"""
|
| 362 |
+
|
| 363 |
+
def __init__(self, data_dir: str):
|
| 364 |
+
"""
|
| 365 |
+
初始化导入器
|
| 366 |
+
:param data_dir: books_api_data 目录路径
|
| 367 |
+
"""
|
| 368 |
+
self.data_dir = Path(data_dir)
|
| 369 |
+
self.db = None
|
| 370 |
+
self.opensearch = None
|
| 371 |
+
self.connect_database()
|
| 372 |
+
self.connect_opensearch()
|
| 373 |
+
|
| 374 |
+
def connect_database(self):
|
| 375 |
+
"""连接MySQL数据库"""
|
| 376 |
+
try:
|
| 377 |
+
self.db = pymysql.connect(**DATABASE_CONFIG)
|
| 378 |
+
logger.info("数据库连接成功")
|
| 379 |
+
except Exception as e:
|
| 380 |
+
logger.error(f"数据库连接失败: {e}")
|
| 381 |
+
raise
|
| 382 |
+
|
| 383 |
+
def connect_opensearch(self):
|
| 384 |
+
"""连接OpenSearch"""
|
| 385 |
+
try:
|
| 386 |
+
self.opensearch = OpenSearch(**OPENSEARCH_CONFIG)
|
| 387 |
+
logger.info("OpenSearch连接成功")
|
| 388 |
+
except Exception as e:
|
| 389 |
+
logger.warning(f"OpenSearch连接失败: {e}")
|
| 390 |
+
|
| 391 |
+
def import_book_info(self, book_dir: Path) -> int:
|
| 392 |
+
"""
|
| 393 |
+
导入书籍基本信息
|
| 394 |
+
:param book_dir: 书籍目录
|
| 395 |
+
:return: 书籍ID
|
| 396 |
+
"""
|
| 397 |
+
book_info_file = book_dir / "book_info.json"
|
| 398 |
+
if not book_info_file.exists():
|
| 399 |
+
logger.warning(f"book_info.json 不存在: {book_dir}")
|
| 400 |
+
return None
|
| 401 |
+
|
| 402 |
+
with open(book_info_file, 'r', encoding='utf-8') as f:
|
| 403 |
+
book_info = json.load(f)
|
| 404 |
+
|
| 405 |
+
# 拼接完整的封面URL
|
| 406 |
+
cover_url = STATIC_BASE_URL + book_info.get("MarketBookCover", "")
|
| 407 |
+
|
| 408 |
+
sql = """
|
| 409 |
+
INSERT INTO books (
|
| 410 |
+
market_book_id, market_classify_id, market_book_name, stage,
|
| 411 |
+
market_book_cover, mod_book_id, mod_book_name, mod_book_cover,
|
| 412 |
+
old_mod_book_id, is_mod, create_date, create_user, isbn, type,
|
| 413 |
+
subject_id, ver_id, grade_id, reel_id, app_id, sort,
|
| 414 |
+
market_classify_name, old_mod_classify_id, mod_classify_id,
|
| 415 |
+
start_page, max_page
|
| 416 |
+
) VALUES (
|
| 417 |
+
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
|
| 418 |
+
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
|
| 419 |
+
) ON DUPLICATE KEY UPDATE
|
| 420 |
+
market_book_name = VALUES(market_book_name),
|
| 421 |
+
market_book_cover = VALUES(market_book_cover),
|
| 422 |
+
max_page = VALUES(max_page)
|
| 423 |
+
"""
|
| 424 |
+
|
| 425 |
+
with self.db.cursor() as cursor:
|
| 426 |
+
cursor.execute(sql, (
|
| 427 |
+
book_info.get("MarketBookID"),
|
| 428 |
+
book_info.get("MarketClassifyId"),
|
| 429 |
+
book_info.get("MarketBookName"),
|
| 430 |
+
book_info.get("Stage"),
|
| 431 |
+
cover_url,
|
| 432 |
+
book_info.get("MODBookID"),
|
| 433 |
+
book_info.get("MODBookName"),
|
| 434 |
+
book_info.get("MODBookCover"),
|
| 435 |
+
book_info.get("OldMODBookID"),
|
| 436 |
+
book_info.get("IsMod"),
|
| 437 |
+
book_info.get("CreateDate"),
|
| 438 |
+
book_info.get("CreateUser"),
|
| 439 |
+
book_info.get("ISBN"),
|
| 440 |
+
book_info.get("Type"),
|
| 441 |
+
book_info.get("SubjectID"),
|
| 442 |
+
book_info.get("VerID"),
|
| 443 |
+
book_info.get("GradeID"),
|
| 444 |
+
book_info.get("ReelID"),
|
| 445 |
+
book_info.get("AppID"),
|
| 446 |
+
book_info.get("Sort"),
|
| 447 |
+
book_info.get("MarketClassifyName"),
|
| 448 |
+
book_info.get("OldModClassifyID"),
|
| 449 |
+
book_info.get("ModClassifyID"),
|
| 450 |
+
book_info.get("StartPage"),
|
| 451 |
+
book_info.get("MaxPage")
|
| 452 |
+
))
|
| 453 |
+
self.db.commit()
|
| 454 |
+
|
| 455 |
+
book_id = book_info.get("MarketBookID")
|
| 456 |
+
logger.info(f"导入书籍信息成功: {book_id} - {book_info.get('MarketBookName')}")
|
| 457 |
+
return book_id
|
| 458 |
+
|
| 459 |
+
def import_pages(self, book_dir: Path, book_id: int):
|
| 460 |
+
"""
|
| 461 |
+
导入页面数据
|
| 462 |
+
:param book_dir: 书籍目录
|
| 463 |
+
:param book_id: 书籍ID
|
| 464 |
+
"""
|
| 465 |
+
pages_file = book_dir / "pages.json"
|
| 466 |
+
if not pages_file.exists():
|
| 467 |
+
logger.warning(f"pages.json 不存在: {book_dir}")
|
| 468 |
+
return
|
| 469 |
+
|
| 470 |
+
with open(pages_file, 'r', encoding='utf-8') as f:
|
| 471 |
+
pages = json.load(f)
|
| 472 |
+
|
| 473 |
+
page_sql = """
|
| 474 |
+
INSERT INTO book_pages (page_id, book_id, page_number, origin_img_url)
|
| 475 |
+
VALUES (%s, %s, %s, %s)
|
| 476 |
+
ON DUPLICATE KEY UPDATE origin_img_url = VALUES(origin_img_url)
|
| 477 |
+
"""
|
| 478 |
+
|
| 479 |
+
piece_sql = """
|
| 480 |
+
INSERT INTO page_pieces (
|
| 481 |
+
piece_id, page_id, original, translation, origin_sound_url,
|
| 482 |
+
duration, coordinate_x, coordinate_y, coordinate_width, coordinate_height,
|
| 483 |
+
is_evaluated, show_translation, rich_original, rich_translation, piece_order
|
| 484 |
+
) VALUES (
|
| 485 |
+
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
|
| 486 |
+
) ON DUPLICATE KEY UPDATE
|
| 487 |
+
original = VALUES(original),
|
| 488 |
+
translation = VALUES(translation)
|
| 489 |
+
"""
|
| 490 |
+
|
| 491 |
+
with self.db.cursor() as cursor:
|
| 492 |
+
for page in pages:
|
| 493 |
+
page_id = page.get("pageId")
|
| 494 |
+
page_number = page.get("pageNumber")
|
| 495 |
+
img_url = STATIC_BASE_URL + page.get("originImgUrl", "")
|
| 496 |
+
|
| 497 |
+
# 导入页面
|
| 498 |
+
cursor.execute(page_sql, (page_id, book_id, page_number, img_url))
|
| 499 |
+
|
| 500 |
+
# 导入页面片段
|
| 501 |
+
pieces = page.get("pieces", [])
|
| 502 |
+
for i, piece in enumerate(pieces):
|
| 503 |
+
coordinate = piece.get("coordinate", {})
|
| 504 |
+
sound_url = STATIC_BASE_URL + piece.get("originSoundUrl", "")
|
| 505 |
+
|
| 506 |
+
cursor.execute(piece_sql, (
|
| 507 |
+
piece.get("pieceId"),
|
| 508 |
+
page_id,
|
| 509 |
+
piece.get("original"),
|
| 510 |
+
piece.get("translation"),
|
| 511 |
+
sound_url,
|
| 512 |
+
piece.get("duration"),
|
| 513 |
+
coordinate.get("x"),
|
| 514 |
+
coordinate.get("y"),
|
| 515 |
+
coordinate.get("width"),
|
| 516 |
+
coordinate.get("height"),
|
| 517 |
+
piece.get("isEvaluated"),
|
| 518 |
+
piece.get("showTranslation"),
|
| 519 |
+
piece.get("richOriginal"),
|
| 520 |
+
piece.get("richTranslation"),
|
| 521 |
+
i
|
| 522 |
+
))
|
| 523 |
+
|
| 524 |
+
self.db.commit()
|
| 525 |
+
logger.info(f"导入 {len(pages)} 页内容成功")
|
| 526 |
+
|
| 527 |
+
def import_catalog(self, book_dir: Path, book_id: int):
|
| 528 |
+
"""
|
| 529 |
+
导入目录数据
|
| 530 |
+
:param book_dir: 书籍目录
|
| 531 |
+
:param book_id: 书籍ID
|
| 532 |
+
"""
|
| 533 |
+
catalog_file = book_dir / "catalog.json"
|
| 534 |
+
if not catalog_file.exists():
|
| 535 |
+
logger.warning(f"catalog.json 不存在: {book_dir}")
|
| 536 |
+
return
|
| 537 |
+
|
| 538 |
+
with open(catalog_file, 'r', encoding='utf-8') as f:
|
| 539 |
+
catalog_data = json.load(f)
|
| 540 |
+
|
| 541 |
+
catalogs = catalog_data.get("catalog", [])
|
| 542 |
+
|
| 543 |
+
sql = """
|
| 544 |
+
INSERT INTO book_catalogs (
|
| 545 |
+
catalog_id, book_id, catalog_name, catalog_name_cn,
|
| 546 |
+
start_page, end_page, thumbnail, parent_id, sort
|
| 547 |
+
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
| 548 |
+
ON DUPLICATE KEY UPDATE
|
| 549 |
+
catalog_name = VALUES(catalog_name),
|
| 550 |
+
catalog_name_cn = VALUES(catalog_name_cn)
|
| 551 |
+
"""
|
| 552 |
+
|
| 553 |
+
with self.db.cursor() as cursor:
|
| 554 |
+
for catalog in catalogs:
|
| 555 |
+
thumbnail = STATIC_BASE_URL + catalog.get("Thumbnail", "")
|
| 556 |
+
|
| 557 |
+
cursor.execute(sql, (
|
| 558 |
+
catalog.get("CatalogID"),
|
| 559 |
+
book_id,
|
| 560 |
+
catalog.get("CatalogName"),
|
| 561 |
+
catalog.get("CatalogNameCN"),
|
| 562 |
+
catalog.get("StartPage"),
|
| 563 |
+
catalog.get("EndPage"),
|
| 564 |
+
thumbnail,
|
| 565 |
+
catalog.get("ParentID"),
|
| 566 |
+
catalog.get("Sort")
|
| 567 |
+
))
|
| 568 |
+
|
| 569 |
+
self.db.commit()
|
| 570 |
+
logger.info(f"导入 {len(catalogs)} 个目录项成功")
|
| 571 |
+
|
| 572 |
+
def import_to_opensearch(self, book_dir: Path, book_id: int):
|
| 573 |
+
"""
|
| 574 |
+
将书籍数据导入OpenSearch用于全文搜索
|
| 575 |
+
:param book_dir: 书籍目录
|
| 576 |
+
:param book_id: 书籍ID
|
| 577 |
+
"""
|
| 578 |
+
if not self.opensearch:
|
| 579 |
+
logger.warning("OpenSearch未连接,跳过")
|
| 580 |
+
return
|
| 581 |
+
|
| 582 |
+
# 读取数据
|
| 583 |
+
book_info_file = book_dir / "book_info.json"
|
| 584 |
+
pages_file = book_dir / "pages.json"
|
| 585 |
+
|
| 586 |
+
if not book_info_file.exists() or not pages_file.exists():
|
| 587 |
+
return
|
| 588 |
+
|
| 589 |
+
with open(book_info_file, 'r', encoding='utf-8') as f:
|
| 590 |
+
book_info = json.load(f)
|
| 591 |
+
|
| 592 |
+
with open(pages_file, 'r', encoding='utf-8') as f:
|
| 593 |
+
pages = json.load(f)
|
| 594 |
+
|
| 595 |
+
# 构建搜索文档
|
| 596 |
+
index_name = "books_content"
|
| 597 |
+
|
| 598 |
+
for page in pages:
|
| 599 |
+
for piece in page.get("pieces", []):
|
| 600 |
+
doc = {
|
| 601 |
+
"book_id": book_id,
|
| 602 |
+
"book_name": book_info.get("MarketBookName"),
|
| 603 |
+
"page_number": page.get("pageNumber"),
|
| 604 |
+
"piece_id": piece.get("pieceId"),
|
| 605 |
+
"original": piece.get("original"),
|
| 606 |
+
"translation": piece.get("translation"),
|
| 607 |
+
"sound_url": piece.get("originSoundUrl")
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
try:
|
| 611 |
+
self.opensearch.index(
|
| 612 |
+
index=index_name,
|
| 613 |
+
id=f"{book_id}_{page.get('pageNumber')}_{piece.get('pieceId')}",
|
| 614 |
+
body=doc
|
| 615 |
+
)
|
| 616 |
+
except Exception as e:
|
| 617 |
+
logger.error(f"导入OpenSearch失败: {e}")
|
| 618 |
+
|
| 619 |
+
logger.info(f"导入OpenSearch成功: {len(pages)} 页")
|
| 620 |
+
|
| 621 |
+
def import_book(self, book_dir: Path):
|
| 622 |
+
"""
|
| 623 |
+
导入单本书籍的完整数据
|
| 624 |
+
:param book_dir: 书籍目录
|
| 625 |
+
"""
|
| 626 |
+
book_name = book_dir.name
|
| 627 |
+
logger.info(f"\n{'=' * 60}")
|
| 628 |
+
logger.info(f"开始导入书籍: {book_name}")
|
| 629 |
+
logger.info(f"{'=' * 60}")
|
| 630 |
+
|
| 631 |
+
try:
|
| 632 |
+
# 1. 导入书籍信息
|
| 633 |
+
book_id = self.import_book_info(book_dir)
|
| 634 |
+
if not book_id:
|
| 635 |
+
logger.error(f"导入失败: 无法获取书籍ID")
|
| 636 |
+
return
|
| 637 |
+
|
| 638 |
+
# 2. 导入页面数据
|
| 639 |
+
self.import_pages(book_dir, book_id)
|
| 640 |
+
|
| 641 |
+
# 3. 导入目录数据
|
| 642 |
+
self.import_catalog(book_dir, book_id)
|
| 643 |
+
|
| 644 |
+
# 4. 导入到搜索引擎
|
| 645 |
+
self.import_to_opensearch(book_dir, book_id)
|
| 646 |
+
|
| 647 |
+
logger.info(f"书籍 {book_name} 导入完成!\n")
|
| 648 |
+
|
| 649 |
+
except Exception as e:
|
| 650 |
+
logger.error(f"导入书籍 {book_name} 失败: {e}", exc_info=True)
|
| 651 |
+
|
| 652 |
+
def import_all_books(self):
|
| 653 |
+
"""导入所有书籍"""
|
| 654 |
+
logger.info("=" * 60)
|
| 655 |
+
logger.info("开始批量导入书籍数据")
|
| 656 |
+
logger.info(f"数据目录: {self.data_dir}")
|
| 657 |
+
logger.info("=" * 60)
|
| 658 |
+
|
| 659 |
+
if not self.data_dir.exists():
|
| 660 |
+
logger.error(f"数据目录不存在: {self.data_dir}")
|
| 661 |
+
return
|
| 662 |
+
|
| 663 |
+
book_dirs = [d for d in self.data_dir.iterdir() if d.is_dir()]
|
| 664 |
+
logger.info(f"找到 {len(book_dirs)} 本书籍\n")
|
| 665 |
+
|
| 666 |
+
success_count = 0
|
| 667 |
+
failed_count = 0
|
| 668 |
+
|
| 669 |
+
for idx, book_dir in enumerate(book_dirs, 1):
|
| 670 |
+
logger.info(f"[{idx}/{len(book_dirs)}] 处理: {book_dir.name}")
|
| 671 |
+
try:
|
| 672 |
+
self.import_book(book_dir)
|
| 673 |
+
success_count += 1
|
| 674 |
+
except Exception as e:
|
| 675 |
+
logger.error(f"导入失败: {e}")
|
| 676 |
+
failed_count += 1
|
| 677 |
+
|
| 678 |
+
logger.info("\n" + "=" * 60)
|
| 679 |
+
logger.info("批量导入完成!")
|
| 680 |
+
logger.info(f"成功: {success_count} 本")
|
| 681 |
+
logger.info(f"失败: {failed_count} 本")
|
| 682 |
+
logger.info("=" * 60)
|
| 683 |
+
|
| 684 |
+
def close(self):
|
| 685 |
+
"""关闭连接"""
|
| 686 |
+
if self.db:
|
| 687 |
+
self.db.close()
|
| 688 |
+
|
| 689 |
+
|
| 690 |
+
def main():
|
| 691 |
+
"""主函数"""
|
| 692 |
+
DATA_DIR = "/data/zhangl/code/hf/download_books/books_api_data"
|
| 693 |
+
|
| 694 |
+
importer = BookDataImporter(DATA_DIR)
|
| 695 |
+
try:
|
| 696 |
+
importer.import_all_books()
|
| 697 |
+
finally:
|
| 698 |
+
importer.close()
|
| 699 |
+
|
| 700 |
+
|
| 701 |
+
if __name__ == "__main__":
|
| 702 |
+
main()
|
| 703 |
+
```
|
| 704 |
+
|
| 705 |
+
---
|
| 706 |
+
|
| 707 |
+
## 🚀 API 接口开发指南
|
| 708 |
+
|
| 709 |
+
### RESTful API 设计
|
| 710 |
+
|
| 711 |
+
#### 1. 书籍列表接口
|
| 712 |
+
|
| 713 |
+
**请求**:
|
| 714 |
+
```http
|
| 715 |
+
GET /api/books?grade_id=40&subject_id=167070462398963715&page=1&page_size=20
|
| 716 |
+
```
|
| 717 |
+
|
| 718 |
+
**响应**:
|
| 719 |
+
```json
|
| 720 |
+
{
|
| 721 |
+
"code": 0,
|
| 722 |
+
"message": "success",
|
| 723 |
+
"data": {
|
| 724 |
+
"total": 30,
|
| 725 |
+
"page": 1,
|
| 726 |
+
"page_size": 20,
|
| 727 |
+
"books": [
|
| 728 |
+
{
|
| 729 |
+
"book_id": 168,
|
| 730 |
+
"book_name": "一年级上册",
|
| 731 |
+
"cover": "https://cdn.example.com/168_一年级上册/images/page_001.jpg",
|
| 732 |
+
"grade_id": 40,
|
| 733 |
+
"subject_id": 167070462398963715,
|
| 734 |
+
"classify_name": "沪教版(深圳)",
|
| 735 |
+
"max_page": 73
|
| 736 |
+
}
|
| 737 |
+
]
|
| 738 |
+
}
|
| 739 |
+
}
|
| 740 |
+
```
|
| 741 |
+
|
| 742 |
+
**Python 实现 (FastAPI)**:
|
| 743 |
+
```python
|
| 744 |
+
from fastapi import FastAPI, Query
|
| 745 |
+
from typing import Optional
|
| 746 |
+
import pymysql
|
| 747 |
+
|
| 748 |
+
app = FastAPI()
|
| 749 |
+
|
| 750 |
+
@app.get("/api/books")
|
| 751 |
+
async def get_books(
|
| 752 |
+
grade_id: Optional[int] = None,
|
| 753 |
+
subject_id: Optional[int] = None,
|
| 754 |
+
page: int = Query(1, ge=1),
|
| 755 |
+
page_size: int = Query(20, ge=1, le=100)
|
| 756 |
+
):
|
| 757 |
+
"""获取书籍列表"""
|
| 758 |
+
offset = (page - 1) * page_size
|
| 759 |
+
|
| 760 |
+
where_clauses = []
|
| 761 |
+
params = []
|
| 762 |
+
|
| 763 |
+
if grade_id:
|
| 764 |
+
where_clauses.append("grade_id = %s")
|
| 765 |
+
params.append(grade_id)
|
| 766 |
+
|
| 767 |
+
if subject_id:
|
| 768 |
+
where_clauses.append("subject_id = %s")
|
| 769 |
+
params.append(subject_id)
|
| 770 |
+
|
| 771 |
+
where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
|
| 772 |
+
|
| 773 |
+
# 查询总数
|
| 774 |
+
count_sql = f"SELECT COUNT(*) as total FROM books WHERE {where_sql}"
|
| 775 |
+
|
| 776 |
+
# 查询列表
|
| 777 |
+
list_sql = f"""
|
| 778 |
+
SELECT market_book_id, market_book_name, market_book_cover,
|
| 779 |
+
grade_id, subject_id, market_classify_name, max_page
|
| 780 |
+
FROM books
|
| 781 |
+
WHERE {where_sql}
|
| 782 |
+
ORDER BY sort ASC
|
| 783 |
+
LIMIT %s OFFSET %s
|
| 784 |
+
"""
|
| 785 |
+
|
| 786 |
+
db = pymysql.connect(**DATABASE_CONFIG)
|
| 787 |
+
cursor = db.cursor(pymysql.cursors.DictCursor)
|
| 788 |
+
|
| 789 |
+
# 获取总数
|
| 790 |
+
cursor.execute(count_sql, params)
|
| 791 |
+
total = cursor.fetchone()['total']
|
| 792 |
+
|
| 793 |
+
# 获取列表
|
| 794 |
+
cursor.execute(list_sql, params + [page_size, offset])
|
| 795 |
+
books = cursor.fetchall()
|
| 796 |
+
|
| 797 |
+
cursor.close()
|
| 798 |
+
db.close()
|
| 799 |
+
|
| 800 |
+
return {
|
| 801 |
+
"code": 0,
|
| 802 |
+
"message": "success",
|
| 803 |
+
"data": {
|
| 804 |
+
"total": total,
|
| 805 |
+
"page": page,
|
| 806 |
+
"page_size": page_size,
|
| 807 |
+
"books": [
|
| 808 |
+
{
|
| 809 |
+
"book_id": book['market_book_id'],
|
| 810 |
+
"book_name": book['market_book_name'],
|
| 811 |
+
"cover": book['market_book_cover'],
|
| 812 |
+
"grade_id": book['grade_id'],
|
| 813 |
+
"subject_id": book['subject_id'],
|
| 814 |
+
"classify_name": book['market_classify_name'],
|
| 815 |
+
"max_page": book['max_page']
|
| 816 |
+
}
|
| 817 |
+
for book in books
|
| 818 |
+
]
|
| 819 |
+
}
|
| 820 |
+
}
|
| 821 |
+
```
|
| 822 |
+
|
| 823 |
+
---
|
| 824 |
+
|
| 825 |
+
#### 2. 书籍详情接口
|
| 826 |
+
|
| 827 |
+
**请求**:
|
| 828 |
+
```http
|
| 829 |
+
GET /api/books/{book_id}
|
| 830 |
+
```
|
| 831 |
+
|
| 832 |
+
**响应**:
|
| 833 |
+
```json
|
| 834 |
+
{
|
| 835 |
+
"code": 0,
|
| 836 |
+
"message": "success",
|
| 837 |
+
"data": {
|
| 838 |
+
"book_id": 168,
|
| 839 |
+
"book_name": "一年级上册",
|
| 840 |
+
"cover": "https://cdn.example.com/168_一年级上册/images/page_001.jpg",
|
| 841 |
+
"grade_id": 40,
|
| 842 |
+
"subject_id": 167070462398963715,
|
| 843 |
+
"classify_name": "沪教版(深圳)",
|
| 844 |
+
"start_page": 1,
|
| 845 |
+
"max_page": 73,
|
| 846 |
+
"create_date": "2017-06-02 18:40:54"
|
| 847 |
+
}
|
| 848 |
+
}
|
| 849 |
+
```
|
| 850 |
+
|
| 851 |
+
**实现**:
|
| 852 |
+
```python
|
| 853 |
+
@app.get("/api/books/{book_id}")
|
| 854 |
+
async def get_book_detail(book_id: int):
|
| 855 |
+
"""获取书籍详情"""
|
| 856 |
+
sql = """
|
| 857 |
+
SELECT market_book_id, market_book_name, market_book_cover,
|
| 858 |
+
grade_id, subject_id, market_classify_name,
|
| 859 |
+
start_page, max_page, create_date
|
| 860 |
+
FROM books
|
| 861 |
+
WHERE market_book_id = %s
|
| 862 |
+
"""
|
| 863 |
+
|
| 864 |
+
db = pymysql.connect(**DATABASE_CONFIG)
|
| 865 |
+
cursor = db.cursor(pymysql.cursors.DictCursor)
|
| 866 |
+
cursor.execute(sql, (book_id,))
|
| 867 |
+
book = cursor.fetchone()
|
| 868 |
+
cursor.close()
|
| 869 |
+
db.close()
|
| 870 |
+
|
| 871 |
+
if not book:
|
| 872 |
+
return {"code": 404, "message": "书籍不存在"}
|
| 873 |
+
|
| 874 |
+
return {
|
| 875 |
+
"code": 0,
|
| 876 |
+
"message": "success",
|
| 877 |
+
"data": {
|
| 878 |
+
"book_id": book['market_book_id'],
|
| 879 |
+
"book_name": book['market_book_name'],
|
| 880 |
+
"cover": book['market_book_cover'],
|
| 881 |
+
"grade_id": book['grade_id'],
|
| 882 |
+
"subject_id": book['subject_id'],
|
| 883 |
+
"classify_name": book['market_classify_name'],
|
| 884 |
+
"start_page": book['start_page'],
|
| 885 |
+
"max_page": book['max_page'],
|
| 886 |
+
"create_date": str(book['create_date'])
|
| 887 |
+
}
|
| 888 |
+
}
|
| 889 |
+
```
|
| 890 |
+
|
| 891 |
+
---
|
| 892 |
+
|
| 893 |
+
#### 3. 书籍目录接口
|
| 894 |
+
|
| 895 |
+
**请求**:
|
| 896 |
+
```http
|
| 897 |
+
GET /api/books/{book_id}/catalog
|
| 898 |
+
```
|
| 899 |
+
|
| 900 |
+
**响应**:
|
| 901 |
+
```json
|
| 902 |
+
{
|
| 903 |
+
"code": 0,
|
| 904 |
+
"message": "success",
|
| 905 |
+
"data": {
|
| 906 |
+
"start_page": 1,
|
| 907 |
+
"end_page": 73,
|
| 908 |
+
"catalog": [
|
| 909 |
+
{
|
| 910 |
+
"catalog_id": 1,
|
| 911 |
+
"catalog_name": "Unit 1 Hello",
|
| 912 |
+
"catalog_name_cn": "第一单元 你好",
|
| 913 |
+
"start_page": 2,
|
| 914 |
+
"end_page": 2,
|
| 915 |
+
"thumbnail": "https://cdn.example.com/168_一年级上册/images/thumbnails/page_002_thumb.jpg",
|
| 916 |
+
"parent_id": 0,
|
| 917 |
+
"sort": 1,
|
| 918 |
+
"children": []
|
| 919 |
+
}
|
| 920 |
+
]
|
| 921 |
+
}
|
| 922 |
+
}
|
| 923 |
+
```
|
| 924 |
+
|
| 925 |
+
**实现**:
|
| 926 |
+
```python
|
| 927 |
+
@app.get("/api/books/{book_id}/catalog")
|
| 928 |
+
async def get_book_catalog(book_id: int):
|
| 929 |
+
"""获取书籍目录"""
|
| 930 |
+
sql = """
|
| 931 |
+
SELECT catalog_id, catalog_name, catalog_name_cn,
|
| 932 |
+
start_page, end_page, thumbnail, parent_id, sort
|
| 933 |
+
FROM book_catalogs
|
| 934 |
+
WHERE book_id = %s
|
| 935 |
+
ORDER BY sort ASC
|
| 936 |
+
"""
|
| 937 |
+
|
| 938 |
+
db = pymysql.connect(**DATABASE_CONFIG)
|
| 939 |
+
cursor = db.cursor(pymysql.cursors.DictCursor)
|
| 940 |
+
cursor.execute(sql, (book_id,))
|
| 941 |
+
catalogs = cursor.fetchall()
|
| 942 |
+
cursor.close()
|
| 943 |
+
db.close()
|
| 944 |
+
|
| 945 |
+
# 构建树形结构
|
| 946 |
+
catalog_dict = {cat['catalog_id']: dict(cat, children=[]) for cat in catalogs}
|
| 947 |
+
tree = []
|
| 948 |
+
|
| 949 |
+
for catalog in catalogs:
|
| 950 |
+
cat_id = catalog['catalog_id']
|
| 951 |
+
parent_id = catalog['parent_id']
|
| 952 |
+
|
| 953 |
+
if parent_id == 0:
|
| 954 |
+
tree.append(catalog_dict[cat_id])
|
| 955 |
+
else:
|
| 956 |
+
if parent_id in catalog_dict:
|
| 957 |
+
catalog_dict[parent_id]['children'].append(catalog_dict[cat_id])
|
| 958 |
+
|
| 959 |
+
return {
|
| 960 |
+
"code": 0,
|
| 961 |
+
"message": "success",
|
| 962 |
+
"data": {
|
| 963 |
+
"catalog": tree
|
| 964 |
+
}
|
| 965 |
+
}
|
| 966 |
+
```
|
| 967 |
+
|
| 968 |
+
---
|
| 969 |
+
|
| 970 |
+
#### 4. 页面内容接口
|
| 971 |
+
|
| 972 |
+
**请求**:
|
| 973 |
+
```http
|
| 974 |
+
GET /api/books/{book_id}/pages/{page_number}
|
| 975 |
+
```
|
| 976 |
+
|
| 977 |
+
**响应**:
|
| 978 |
+
```json
|
| 979 |
+
{
|
| 980 |
+
"code": 0,
|
| 981 |
+
"message": "success",
|
| 982 |
+
"data": {
|
| 983 |
+
"page_id": 2111,
|
| 984 |
+
"page_number": 2,
|
| 985 |
+
"image_url": "https://cdn.example.com/168_一年级上册/images/page_002.jpg",
|
| 986 |
+
"pieces": [
|
| 987 |
+
{
|
| 988 |
+
"piece_id": 26342,
|
| 989 |
+
"original": "Unit 1 Hello",
|
| 990 |
+
"translation": "第一单元 你好",
|
| 991 |
+
"sound_url": "https://cdn.example.com/168_一年级上册/audios/page_002_piece_00.mp3",
|
| 992 |
+
"duration": 4,
|
| 993 |
+
"coordinate": {
|
| 994 |
+
"x": 0.0482,
|
| 995 |
+
"y": 0.0357,
|
| 996 |
+
"width": 0.3195,
|
| 997 |
+
"height": 0.0982
|
| 998 |
+
},
|
| 999 |
+
"is_evaluated": 1,
|
| 1000 |
+
"show_translation": 1
|
| 1001 |
+
}
|
| 1002 |
+
]
|
| 1003 |
+
}
|
| 1004 |
+
}
|
| 1005 |
+
```
|
| 1006 |
+
|
| 1007 |
+
**实现**:
|
| 1008 |
+
```python
|
| 1009 |
+
@app.get("/api/books/{book_id}/pages/{page_number}")
|
| 1010 |
+
async def get_page_content(book_id: int, page_number: int):
|
| 1011 |
+
"""获取页面内容"""
|
| 1012 |
+
# 查询页面
|
| 1013 |
+
page_sql = """
|
| 1014 |
+
SELECT page_id, page_number, origin_img_url
|
| 1015 |
+
FROM book_pages
|
| 1016 |
+
WHERE book_id = %s AND page_number = %s
|
| 1017 |
+
"""
|
| 1018 |
+
|
| 1019 |
+
# 查询片段
|
| 1020 |
+
piece_sql = """
|
| 1021 |
+
SELECT piece_id, original, translation, origin_sound_url,
|
| 1022 |
+
duration, coordinate_x, coordinate_y, coordinate_width, coordinate_height,
|
| 1023 |
+
is_evaluated, show_translation
|
| 1024 |
+
FROM page_pieces
|
| 1025 |
+
WHERE page_id = %s
|
| 1026 |
+
ORDER BY piece_order ASC
|
| 1027 |
+
"""
|
| 1028 |
+
|
| 1029 |
+
db = pymysql.connect(**DATABASE_CONFIG)
|
| 1030 |
+
cursor = db.cursor(pymysql.cursors.DictCursor)
|
| 1031 |
+
|
| 1032 |
+
cursor.execute(page_sql, (book_id, page_number))
|
| 1033 |
+
page = cursor.fetchone()
|
| 1034 |
+
|
| 1035 |
+
if not page:
|
| 1036 |
+
cursor.close()
|
| 1037 |
+
db.close()
|
| 1038 |
+
return {"code": 404, "message": "页面不存在"}
|
| 1039 |
+
|
| 1040 |
+
cursor.execute(piece_sql, (page['page_id'],))
|
| 1041 |
+
pieces = cursor.fetchall()
|
| 1042 |
+
|
| 1043 |
+
cursor.close()
|
| 1044 |
+
db.close()
|
| 1045 |
+
|
| 1046 |
+
return {
|
| 1047 |
+
"code": 0,
|
| 1048 |
+
"message": "success",
|
| 1049 |
+
"data": {
|
| 1050 |
+
"page_id": page['page_id'],
|
| 1051 |
+
"page_number": page['page_number'],
|
| 1052 |
+
"image_url": page['origin_img_url'],
|
| 1053 |
+
"pieces": [
|
| 1054 |
+
{
|
| 1055 |
+
"piece_id": piece['piece_id'],
|
| 1056 |
+
"original": piece['original'],
|
| 1057 |
+
"translation": piece['translation'],
|
| 1058 |
+
"sound_url": piece['origin_sound_url'],
|
| 1059 |
+
"duration": piece['duration'],
|
| 1060 |
+
"coordinate": {
|
| 1061 |
+
"x": float(piece['coordinate_x']),
|
| 1062 |
+
"y": float(piece['coordinate_y']),
|
| 1063 |
+
"width": float(piece['coordinate_width']),
|
| 1064 |
+
"height": float(piece['coordinate_height'])
|
| 1065 |
+
},
|
| 1066 |
+
"is_evaluated": piece['is_evaluated'],
|
| 1067 |
+
"show_translation": piece['show_translation']
|
| 1068 |
+
}
|
| 1069 |
+
for piece in pieces
|
| 1070 |
+
]
|
| 1071 |
+
}
|
| 1072 |
+
}
|
| 1073 |
+
```
|
| 1074 |
+
|
| 1075 |
+
---
|
| 1076 |
+
|
| 1077 |
+
#### 5. 全文搜索接口 (OpenSearch)
|
| 1078 |
+
|
| 1079 |
+
**请求**:
|
| 1080 |
+
```http
|
| 1081 |
+
GET /api/search?keyword=hello&page=1&page_size=20
|
| 1082 |
+
```
|
| 1083 |
+
|
| 1084 |
+
**响应**:
|
| 1085 |
+
```json
|
| 1086 |
+
{
|
| 1087 |
+
"code": 0,
|
| 1088 |
+
"message": "success",
|
| 1089 |
+
"data": {
|
| 1090 |
+
"total": 15,
|
| 1091 |
+
"results": [
|
| 1092 |
+
{
|
| 1093 |
+
"book_id": 168,
|
| 1094 |
+
"book_name": "一年级上册",
|
| 1095 |
+
"page_number": 2,
|
| 1096 |
+
"piece_id": 26342,
|
| 1097 |
+
"original": "Unit 1 Hello",
|
| 1098 |
+
"translation": "第一单元 你好",
|
| 1099 |
+
"sound_url": "https://cdn.example.com/168_一年级上册/audios/page_002_piece_00.mp3",
|
| 1100 |
+
"score": 1.234
|
| 1101 |
+
}
|
| 1102 |
+
]
|
| 1103 |
+
}
|
| 1104 |
+
}
|
| 1105 |
+
```
|
| 1106 |
+
|
| 1107 |
+
**实现**:
|
| 1108 |
+
```python
|
| 1109 |
+
from opensearchpy import OpenSearch
|
| 1110 |
+
|
| 1111 |
+
@app.get("/api/search")
|
| 1112 |
+
async def search_content(
|
| 1113 |
+
keyword: str,
|
| 1114 |
+
page: int = Query(1, ge=1),
|
| 1115 |
+
page_size: int = Query(20, ge=1, le=100)
|
| 1116 |
+
):
|
| 1117 |
+
"""全文搜索"""
|
| 1118 |
+
opensearch = OpenSearch(**OPENSEARCH_CONFIG)
|
| 1119 |
+
|
| 1120 |
+
from_index = (page - 1) * page_size
|
| 1121 |
+
|
| 1122 |
+
query = {
|
| 1123 |
+
"from": from_index,
|
| 1124 |
+
"size": page_size,
|
| 1125 |
+
"query": {
|
| 1126 |
+
"multi_match": {
|
| 1127 |
+
"query": keyword,
|
| 1128 |
+
"fields": ["original", "translation"],
|
| 1129 |
+
"type": "best_fields"
|
| 1130 |
+
}
|
| 1131 |
+
},
|
| 1132 |
+
"highlight": {
|
| 1133 |
+
"fields": {
|
| 1134 |
+
"original": {},
|
| 1135 |
+
"translation": {}
|
| 1136 |
+
}
|
| 1137 |
+
}
|
| 1138 |
+
}
|
| 1139 |
+
|
| 1140 |
+
response = opensearch.search(
|
| 1141 |
+
index="books_content",
|
| 1142 |
+
body=query
|
| 1143 |
+
)
|
| 1144 |
+
|
| 1145 |
+
results = []
|
| 1146 |
+
for hit in response['hits']['hits']:
|
| 1147 |
+
source = hit['_source']
|
| 1148 |
+
results.append({
|
| 1149 |
+
"book_id": source['book_id'],
|
| 1150 |
+
"book_name": source['book_name'],
|
| 1151 |
+
"page_number": source['page_number'],
|
| 1152 |
+
"piece_id": source['piece_id'],
|
| 1153 |
+
"original": source['original'],
|
| 1154 |
+
"translation": source['translation'],
|
| 1155 |
+
"sound_url": source['sound_url'],
|
| 1156 |
+
"score": hit['_score']
|
| 1157 |
+
})
|
| 1158 |
+
|
| 1159 |
+
return {
|
| 1160 |
+
"code": 0,
|
| 1161 |
+
"message": "success",
|
| 1162 |
+
"data": {
|
| 1163 |
+
"total": response['hits']['total']['value'],
|
| 1164 |
+
"results": results
|
| 1165 |
+
}
|
| 1166 |
+
}
|
| 1167 |
+
```
|
| 1168 |
+
|
| 1169 |
+
---
|
| 1170 |
+
|
| 1171 |
+
## 📝 注意事项与最佳实践
|
| 1172 |
+
|
| 1173 |
+
### 1. 数据库优化
|
| 1174 |
+
- 为高频查询字段添加索引(book_id, page_number, grade_id 等)
|
| 1175 |
+
- 考虑使用数据库读写分离
|
| 1176 |
+
- 对大字段(TEXT)考虑使用压缩存储
|
| 1177 |
+
|
| 1178 |
+
### 2. 静态资源优化
|
| 1179 |
+
- 使用 CDN 加速图片和音频访问
|
| 1180 |
+
- 图片启用 WebP 格式以节省带宽
|
| 1181 |
+
- 音频文件考虑使用自适应码率
|
| 1182 |
+
|
| 1183 |
+
### 3. API 性能优化
|
| 1184 |
+
- 实现接口缓存(Redis)
|
| 1185 |
+
- 分页查询避免大量数据返回
|
| 1186 |
+
- 使用连接池管理数据库连接
|
| 1187 |
+
|
| 1188 |
+
### 4. 安全性
|
| 1189 |
+
- API 添加认证和授权机制
|
| 1190 |
+
- 对用户输入进行严格校验
|
| 1191 |
+
- 防止 SQL 注入和 XSS 攻击
|
| 1192 |
+
|
| 1193 |
+
### 5. 搜索优化
|
| 1194 |
+
- OpenSearch 定期优化索引
|
| 1195 |
+
- 对中文进行分词处理(IK Analyzer)
|
| 1196 |
+
- 实现搜索结果高亮显示
|
| 1197 |
+
|
| 1198 |
+
---
|
| 1199 |
+
|
| 1200 |
+
## 🔗 相关技术栈推荐
|
| 1201 |
+
|
| 1202 |
+
### 后端框架
|
| 1203 |
+
- **Python**: FastAPI, Django REST Framework, Flask
|
| 1204 |
+
- **Node.js**: Express, NestJS, Koa
|
| 1205 |
+
- **Java**: Spring Boot
|
| 1206 |
+
- **Go**: Gin, Echo
|
| 1207 |
+
|
| 1208 |
+
### 数据库
|
| 1209 |
+
- **关系型**: MySQL, PostgreSQL
|
| 1210 |
+
- **文档型**: MongoDB
|
| 1211 |
+
- **搜索引擎**: OpenSearch, Elasticsearch
|
| 1212 |
+
|
| 1213 |
+
### 静态资源存储
|
| 1214 |
+
- **对象存储**: 阿里云 OSS, 腾讯云 COS, AWS S3
|
| 1215 |
+
- **CDN**: 阿里云 CDN, 腾讯云 CDN, Cloudflare
|
| 1216 |
+
|
| 1217 |
+
### 缓存
|
| 1218 |
+
- **Redis**: 接口缓存、会话管理
|
| 1219 |
+
- **Memcached**: 分布式缓存
|
| 1220 |
+
|
| 1221 |
+
---
|
| 1222 |
+
|
| 1223 |
+
## 📞 联系与支持
|
| 1224 |
+
|
| 1225 |
+
如有任何问题或建议,请联系开发团队。
|
| 1226 |
+
|
| 1227 |
+
---
|
| 1228 |
+
|
| 1229 |
+
**文档版本**: v1.0
|
| 1230 |
+
**最后更新**: 2025-10-17
|
| 1231 |
+
**维护者**: 项目开发团队
|
| 1232 |
+
|
CHANGELOG.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 更新日志
|
| 2 |
+
|
| 3 |
+
## v2.0.0 - 2025-10-17
|
| 4 |
+
|
| 5 |
+
### 🎉 重大更新
|
| 6 |
+
|
| 7 |
+
项目已完全重构,支持新的 books_api_data 数据结构。
|
| 8 |
+
|
| 9 |
+
### ✨ 新功能
|
| 10 |
+
|
| 11 |
+
#### 1. 书籍目录页面
|
| 12 |
+
- 新增书籍目录首页,展示所有可用教材
|
| 13 |
+
- 支持按年级筛选和搜索教材
|
| 14 |
+
- 显示统计信息(书籍数、页面数、内容片段数等)
|
| 15 |
+
- 精美的卡片式布局
|
| 16 |
+
|
| 17 |
+
#### 2. 数据库重构
|
| 18 |
+
- 从单一JSON文件改为SQLite数据库存储
|
| 19 |
+
- 支持完整的books_api_data数据结构
|
| 20 |
+
- books表: 书籍基本信息
|
| 21 |
+
- pages表: 页面信息
|
| 22 |
+
- pieces表: 内容片段
|
| 23 |
+
- catalogs表: 章节目录
|
| 24 |
+
- 使用FTS5实现全文搜索
|
| 25 |
+
- 支持多本书籍管理
|
| 26 |
+
|
| 27 |
+
#### 3. 阅读功能增强
|
| 28 |
+
- 支持章节目录导航
|
| 29 |
+
- 按需加载页面内容,提升性能
|
| 30 |
+
- 支持从本地books_api_data目录加载资源
|
| 31 |
+
- 更好的坐标系统支持
|
| 32 |
+
|
| 33 |
+
#### 4. API接口完善
|
| 34 |
+
- RESTful API设计
|
| 35 |
+
- 支持书籍列表、详情、目录、搜索等接口
|
| 36 |
+
- 支持全局搜索和单书籍搜索
|
| 37 |
+
- 提供统计信息接口
|
| 38 |
+
|
| 39 |
+
### 🔧 技术改进
|
| 40 |
+
|
| 41 |
+
#### 新增文件
|
| 42 |
+
- `db_schema.sql`: 数据库结构定义
|
| 43 |
+
- `db_manager.py`: 数据库管理器
|
| 44 |
+
- `index.html`: 书籍目录页面
|
| 45 |
+
- `reader.html`: 阅读页面(原index.html)
|
| 46 |
+
- `static/js/catalog.js`: 目录页面逻辑
|
| 47 |
+
- `static/js/reader.js`: 阅读页面逻辑(改进版script.js)
|
| 48 |
+
- `test_setup.py`: 系统设置检查脚本
|
| 49 |
+
- `README.md`: 完整文档
|
| 50 |
+
- `QUICKSTART.md`: 快速开始指南
|
| 51 |
+
- `CHANGELOG.md`: 更新日志
|
| 52 |
+
|
| 53 |
+
#### 修改文件
|
| 54 |
+
- `import_book_data.py`: 重构以支持books_api_data格式
|
| 55 |
+
- `app.py`: 新增多个API接口,支持多页面路由
|
| 56 |
+
- `static/css/style.css`: 新增目录面板等样式
|
| 57 |
+
|
| 58 |
+
### 📊 数据结构变化
|
| 59 |
+
|
| 60 |
+
#### 旧版本 (v1.x)
|
| 61 |
+
```
|
| 62 |
+
单个JSON文件 -> 内存加载
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
#### 新版本 (v2.0)
|
| 66 |
+
```
|
| 67 |
+
books_api_data/
|
| 68 |
+
├── 168_一年级上册/
|
| 69 |
+
│ ├── book_info.json # 书籍信息
|
| 70 |
+
│ ├── pages.json # 页面内容
|
| 71 |
+
│ ├── catalog.json # 目录结构
|
| 72 |
+
│ ├── images/ # 图片资源
|
| 73 |
+
│ └── audios/ # 音频资源
|
| 74 |
+
└── ...
|
| 75 |
+
|
| 76 |
+
↓ 导入
|
| 77 |
+
|
| 78 |
+
books.db (SQLite)
|
| 79 |
+
├── books表
|
| 80 |
+
├── pages表
|
| 81 |
+
├── pieces表
|
| 82 |
+
├── catalogs表
|
| 83 |
+
└── pieces_fts表 (全文搜索)
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
### 🔄 迁移指南
|
| 87 |
+
|
| 88 |
+
#### 从v1.x升级到v2.0
|
| 89 |
+
|
| 90 |
+
1. **备份数据**
|
| 91 |
+
```bash
|
| 92 |
+
cp book_data_10242.json book_data_10242.json.backup
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
2. **准备新数据**
|
| 96 |
+
- 将books_api_data目录放到项目根目录
|
| 97 |
+
- 确保每个书籍目录包含必要文件
|
| 98 |
+
|
| 99 |
+
3. **导入数据**
|
| 100 |
+
```bash
|
| 101 |
+
python3 import_book_data.py books_api_data
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
4. **启动应用**
|
| 105 |
+
```bash
|
| 106 |
+
python3 app.py
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
5. **访问新页面**
|
| 110 |
+
- 旧版: http://localhost:7860/
|
| 111 |
+
- 新版: http://localhost:7860/ (书籍目录)
|
| 112 |
+
- 阅读: http://localhost:7860/reader?book_id=168
|
| 113 |
+
|
| 114 |
+
### 🗑️ 废弃功能
|
| 115 |
+
|
| 116 |
+
- 旧的单JSON文件加载方式
|
| 117 |
+
- `/api/book/info` 和 `/api/book/page/{page_num}` (保留兼容性)
|
| 118 |
+
|
| 119 |
+
### 📝 API变化
|
| 120 |
+
|
| 121 |
+
#### 新增接口
|
| 122 |
+
- `GET /api/v2/books`: 获取书籍列表
|
| 123 |
+
- `GET /api/v2/books/{id}`: 获取书籍详情
|
| 124 |
+
- `GET /api/v2/books/{id}/pages`: 获取页面列表
|
| 125 |
+
- `GET /api/v2/books/{id}/catalog`: 获取目录结构
|
| 126 |
+
- `GET /api/v2/books/{id}/pages/{num}`: 获取页面内容
|
| 127 |
+
- `GET /api/v2/books/{id}/search`: 在书籍中搜索
|
| 128 |
+
- `GET /api/v2/search`: 全局搜索
|
| 129 |
+
- `GET /api/v2/books/{id}/statistics`: 获取书籍统计
|
| 130 |
+
- `GET /api/v2/statistics`: 获取整体统计
|
| 131 |
+
|
| 132 |
+
#### 保留接口(兼容性)
|
| 133 |
+
- `GET /api/book/info`: 获取默认书籍信息
|
| 134 |
+
- `GET /api/book/page/{page_num}`: 获取默认书籍页面
|
| 135 |
+
|
| 136 |
+
### 🐛 Bug修复
|
| 137 |
+
|
| 138 |
+
- 修复页面坐标计算问题
|
| 139 |
+
- 修复音频播放失败的问题
|
| 140 |
+
- 优化资源加载性能
|
| 141 |
+
- 改进错误处理
|
| 142 |
+
|
| 143 |
+
### 🚀 性能优化
|
| 144 |
+
|
| 145 |
+
- 按需加载页面内容,减少内存占用
|
| 146 |
+
- 使用FTS5全文搜索,搜索速度提升10倍
|
| 147 |
+
- 优化资源路径处理
|
| 148 |
+
- 添加数据库索引
|
| 149 |
+
|
| 150 |
+
### 📚 文档更新
|
| 151 |
+
|
| 152 |
+
- 完整的README.md文档
|
| 153 |
+
- 快速开始指南(QUICKSTART.md)
|
| 154 |
+
- API接口文档
|
| 155 |
+
- 数据库结构说明
|
| 156 |
+
- 故障排除指南
|
| 157 |
+
|
| 158 |
+
### 🔒 安全性
|
| 159 |
+
|
| 160 |
+
- 使用SQLite参数化查询,防止SQL注入
|
| 161 |
+
- 添加输入验证
|
| 162 |
+
- CORS配置
|
| 163 |
+
|
| 164 |
+
### 🎨 UI/UX改进
|
| 165 |
+
|
| 166 |
+
- 新的书籍目录页面设计
|
| 167 |
+
- 更好的响应式布局
|
| 168 |
+
- 改进的交互体验
|
| 169 |
+
- 添加加载状态提示
|
| 170 |
+
|
| 171 |
+
### ⚙️ 配置变化
|
| 172 |
+
|
| 173 |
+
- 数据库配置: 使用SQLite(books.db)
|
| 174 |
+
- 资源路径: 使用books_api_data目录
|
| 175 |
+
- 端口: 保持7860不变
|
| 176 |
+
|
| 177 |
+
### 🧪 测试
|
| 178 |
+
|
| 179 |
+
- 新增系统设置检查脚本(test_setup.py)
|
| 180 |
+
- 数据验证功能
|
| 181 |
+
- API接口测试示例
|
| 182 |
+
|
| 183 |
+
### 📦 依赖变化
|
| 184 |
+
|
| 185 |
+
#### 新增依赖
|
| 186 |
+
- 无新增外部依赖(使用Python标准库的sqlite3)
|
| 187 |
+
|
| 188 |
+
#### Python版本
|
| 189 |
+
- 要求: Python 3.12+
|
| 190 |
+
|
| 191 |
+
### 🔮 未来计划
|
| 192 |
+
|
| 193 |
+
- [ ] 用户系统和学习进度跟踪
|
| 194 |
+
- [ ] 导出学习笔记功能
|
| 195 |
+
- [ ] 更多个性化设置选项
|
| 196 |
+
- [ ] 移动端优化
|
| 197 |
+
- [ ] PWA支持
|
| 198 |
+
- [ ] 离线模式
|
| 199 |
+
|
| 200 |
+
### 💡 已知问题
|
| 201 |
+
|
| 202 |
+
1. 大型数据库(>10GB)可能需要��长的导入时间
|
| 203 |
+
2. 某些旧版浏览器可能不支持部分功能
|
| 204 |
+
3. 首次加载页面可能较慢(正在优化)
|
| 205 |
+
|
| 206 |
+
### 🙏 致谢
|
| 207 |
+
|
| 208 |
+
感谢所有贡献者和测试人员。
|
| 209 |
+
|
| 210 |
+
---
|
| 211 |
+
|
| 212 |
+
**完整更新说明请参考**: [README.md](README.md)
|
QUICKSTART.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 快速开始指南
|
| 2 |
+
|
| 3 |
+
## 🚀 5分钟快速部署
|
| 4 |
+
|
| 5 |
+
### 步骤 1: 检查系统环境
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
# 检查Python版本(需要3.12+)
|
| 9 |
+
python3 --version
|
| 10 |
+
|
| 11 |
+
# 运行系统检查脚本
|
| 12 |
+
python3 test_setup.py
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
### 步骤 2: 安装依赖
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
pip install flask flask-cors
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
### 步骤 3: 准备数据
|
| 22 |
+
|
| 23 |
+
确保 `books_api_data` 目录在项目根目录下:
|
| 24 |
+
|
| 25 |
+
```
|
| 26 |
+
/data/zhangl/code/hf/point/
|
| 27 |
+
└── books_api_data/
|
| 28 |
+
├── 168_一年级上册/
|
| 29 |
+
├── 169_二年级上册/
|
| 30 |
+
└── ...
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
### 步骤 4: 导入数据到数据库
|
| 34 |
+
|
| 35 |
+
```bash
|
| 36 |
+
# 导入所有书籍数据
|
| 37 |
+
python3 import_book_data.py books_api_data
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
预期输出:
|
| 41 |
+
```
|
| 42 |
+
📦 创建数据库: books.db
|
| 43 |
+
✅ 数据库结构创建成功
|
| 44 |
+
============================================================
|
| 45 |
+
开始批量导入书籍数据
|
| 46 |
+
============================================================
|
| 47 |
+
[1/30] 处理: 168_一年级上册
|
| 48 |
+
导入书籍信息成功: 168 - 一年级上册
|
| 49 |
+
导入 73 页内容,500 个片段成功
|
| 50 |
+
导入 12 个目录项成功
|
| 51 |
+
✅ 书籍 168_一年级上册 导入完成!
|
| 52 |
+
...
|
| 53 |
+
============================================================
|
| 54 |
+
批量导入完成!
|
| 55 |
+
成功: 30 本
|
| 56 |
+
失败: 0 本
|
| 57 |
+
============================================================
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
### 步骤 5: 启动应用
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
python3 app.py
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
预期输出:
|
| 67 |
+
```
|
| 68 |
+
🚀 交互式英语学习应用 - Flask 版本
|
| 69 |
+
============================================================
|
| 70 |
+
📦 创建数据库: books.db
|
| 71 |
+
✅ 数据库连接初始化成功
|
| 72 |
+
✅ 所有必要文件检查完成
|
| 73 |
+
✅ 应用初始化完成
|
| 74 |
+
🌐 监听端口: 7860
|
| 75 |
+
📁 工作目录: /data/zhangl/code/hf/point
|
| 76 |
+
🔧 调试模式: 关闭
|
| 77 |
+
============================================================
|
| 78 |
+
🎉 应用已准备就绪!
|
| 79 |
+
============================================================
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
### 步骤 6: 访问应用
|
| 83 |
+
|
| 84 |
+
在浏览器中打开:
|
| 85 |
+
|
| 86 |
+
- **书籍目录页**: http://localhost:7860/
|
| 87 |
+
- **阅读某本书**: http://localhost:7860/reader?book_id=168
|
| 88 |
+
|
| 89 |
+
## 📖 使用说明
|
| 90 |
+
|
| 91 |
+
### 书籍目录页面
|
| 92 |
+
|
| 93 |
+
1. 浏览所有可用的教材
|
| 94 |
+
2. 使用搜索框搜索教材
|
| 95 |
+
3. 点击教材卡片进入阅读页面
|
| 96 |
+
|
| 97 |
+
### 阅读页面
|
| 98 |
+
|
| 99 |
+
1. **点读功能**: 点击页面上的文本片段播放音频
|
| 100 |
+
2. **翻译切换**: 点击"翻译"按钮显示/隐藏中文翻译
|
| 101 |
+
3. **交互区域**: 点击"交互"按钮显示可点击区域
|
| 102 |
+
4. **目录导航**: 点击"目录"按钮查看章节目录
|
| 103 |
+
5. **搜索**: 点击"搜索"按钮在当前书籍中搜索内容
|
| 104 |
+
6. **设置**: 点击"设置"按钮调整播放速度等选项
|
| 105 |
+
|
| 106 |
+
### 键盘快捷键
|
| 107 |
+
|
| 108 |
+
- `←` / `→`: 翻页
|
| 109 |
+
- `Space`: 播放/暂停音频
|
| 110 |
+
- `T`: 切换翻译
|
| 111 |
+
- `I`: 切换交互区域
|
| 112 |
+
- `Esc`: 关闭弹出面板
|
| 113 |
+
|
| 114 |
+
## 🔧 常见问题
|
| 115 |
+
|
| 116 |
+
### 1. 数据导入失败
|
| 117 |
+
|
| 118 |
+
**问题**: `❌ 错误: 数据目录不存在: books_api_data`
|
| 119 |
+
|
| 120 |
+
**解决**:
|
| 121 |
+
- 确保 `books_api_data` 目录在项目根目录下
|
| 122 |
+
- 检查目录中是否有书籍数据
|
| 123 |
+
|
| 124 |
+
### 2. 页面图片不显示
|
| 125 |
+
|
| 126 |
+
**问题**: 页面加载后图片不显示
|
| 127 |
+
|
| 128 |
+
**解决**:
|
| 129 |
+
- 检查 `books_api_data/xxx/images/` 目录是否存在
|
| 130 |
+
- 检查图片文件命名是否正确(格式:`page_001.jpg`)
|
| 131 |
+
- 查看浏览器控制台错误信息
|
| 132 |
+
|
| 133 |
+
### 3. 音频无法播放
|
| 134 |
+
|
| 135 |
+
**问题**: 点击文本片段后音频不播放
|
| 136 |
+
|
| 137 |
+
**解决**:
|
| 138 |
+
- 检查 `books_api_data/xxx/audios/` 目录是否存在
|
| 139 |
+
- 检查音频文件是否存在
|
| 140 |
+
- 确保浏览器支持 MP3 格式
|
| 141 |
+
- 检查浏览器是否允许自动播放音频
|
| 142 |
+
|
| 143 |
+
### 4. 端口被占用
|
| 144 |
+
|
| 145 |
+
**问题**: `Address already in use`
|
| 146 |
+
|
| 147 |
+
**解决**:
|
| 148 |
+
```bash
|
| 149 |
+
# 查找占用7860端口的进程
|
| 150 |
+
lsof -i:7860
|
| 151 |
+
|
| 152 |
+
# 或者使用其他端口
|
| 153 |
+
export PORT=8080
|
| 154 |
+
python3 app.py
|
| 155 |
+
```
|
| 156 |
+
|
| 157 |
+
### 5. 重新导入数据
|
| 158 |
+
|
| 159 |
+
如果需要重新导入所有数据:
|
| 160 |
+
|
| 161 |
+
```bash
|
| 162 |
+
# 删除数据库并重新导入
|
| 163 |
+
python3 import_book_data.py books_api_data --recreate
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
## 📊 数据统计
|
| 167 |
+
|
| 168 |
+
查看已导入的数据统计:
|
| 169 |
+
|
| 170 |
+
```bash
|
| 171 |
+
# 运行验证脚本
|
| 172 |
+
python3 import_book_data.py --verify-only
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
或者访问 API:
|
| 176 |
+
```bash
|
| 177 |
+
curl http://localhost:7860/api/v2/statistics
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
## 🔍 API 测试
|
| 181 |
+
|
| 182 |
+
### 获取书籍列表
|
| 183 |
+
```bash
|
| 184 |
+
curl http://localhost:7860/api/v2/books
|
| 185 |
+
```
|
| 186 |
+
|
| 187 |
+
### 获取书籍详情
|
| 188 |
+
```bash
|
| 189 |
+
curl http://localhost:7860/api/v2/books/168
|
| 190 |
+
```
|
| 191 |
+
|
| 192 |
+
### 获取页面内容
|
| 193 |
+
```bash
|
| 194 |
+
curl http://localhost:7860/api/v2/books/168/pages/2
|
| 195 |
+
```
|
| 196 |
+
|
| 197 |
+
### 搜索内容
|
| 198 |
+
```bash
|
| 199 |
+
curl "http://localhost:7860/api/v2/books/168/search?keyword=hello"
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
## 🎯 下一步
|
| 203 |
+
|
| 204 |
+
- 探索更多功能,如书签、搜索等
|
| 205 |
+
- 自定义样式和主题
|
| 206 |
+
- 添加更多教材数据
|
| 207 |
+
- 集成到现有系统
|
| 208 |
+
|
| 209 |
+
## 📚 更多文档
|
| 210 |
+
|
| 211 |
+
- [完整文档](README.md)
|
| 212 |
+
- [数据结构说明](BOOKS_API_DATA_文档.md)
|
| 213 |
+
- [API 文档](README.md#-api-接口)
|
| 214 |
+
|
| 215 |
+
## 💡 提示
|
| 216 |
+
|
| 217 |
+
1. **首次使用**: 建议先导入少量数据测试,确认无误后再导入全部数据
|
| 218 |
+
2. **开发调试**: 设置 `export FLASK_DEBUG=True` 启用调试模式
|
| 219 |
+
3. **性能优化**: 对于大量数据,考虑使用生产级 WSGI ��务器(如 Gunicorn)
|
| 220 |
+
4. **资源路径**: 所有资源路径都是相对于 `books_api_data` 目录的
|
| 221 |
+
|
| 222 |
+
## 🆘 获取帮助
|
| 223 |
+
|
| 224 |
+
如果遇到问题:
|
| 225 |
+
|
| 226 |
+
1. 查看日志文件: `logs/app.log`
|
| 227 |
+
2. 运行测试脚本: `python3 test_setup.py`
|
| 228 |
+
3. 查看浏览器控制台错误信息
|
| 229 |
+
4. 检查 README.md 中的故障排除部分
|
| 230 |
+
|
| 231 |
+
---
|
| 232 |
+
|
| 233 |
+
**祝您使用愉快!** 🎉
|
| 234 |
+
|
README.md
CHANGED
|
@@ -1,82 +1,322 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
#
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
##
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
-
###
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
- **页面导航**: 流畅的页面切换,支持进度跟踪
|
| 23 |
|
| 24 |
-
###
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
- **键盘快捷键**: 完整的键盘操作支持
|
| 29 |
|
| 30 |
-
###
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
## ⌨️ 键盘快捷键
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
###
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
##
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
## 📄 许可证
|
| 77 |
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
---
|
| 81 |
|
| 82 |
-
**
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 交互式英语学习平台
|
| 2 |
+
|
| 3 |
+
基于 books_api_data 数据结构的交互式英语学习应用,支持点读、音频播放、翻译显示等功能。
|
| 4 |
+
|
| 5 |
+
## 📋 功能特性
|
| 6 |
+
|
| 7 |
+
### 书籍目录页面
|
| 8 |
+
- 📚 显示所有可用教材
|
| 9 |
+
- 🔍 搜索教材名称和分类
|
| 10 |
+
- 📊 显示统计信息(教材数、页面数、内容片段数等)
|
| 11 |
+
- 🎨 精美的卡片式布局
|
| 12 |
+
|
| 13 |
+
### 阅读页面
|
| 14 |
+
- 📖 交互式点读功能
|
| 15 |
+
- 🔊 音频播放控制
|
| 16 |
+
- 🌐 中英文翻译切换
|
| 17 |
+
- 📑 章节目录导航
|
| 18 |
+
- 🔖 书签功能
|
| 19 |
+
- 🔍 全文搜索
|
| 20 |
+
- ⚙️ 个性化设置(播放速度、自动翻译等)
|
| 21 |
+
- ⌨️ 键盘快捷键支持
|
| 22 |
+
|
| 23 |
+
## 🚀 快速开始
|
| 24 |
+
|
| 25 |
+
### 1. 环境要求
|
| 26 |
+
|
| 27 |
+
- Python 3.12
|
| 28 |
+
- SQLite 3
|
| 29 |
+
- 现代浏览器(Chrome、Firefox、Safari、Edge)
|
| 30 |
+
|
| 31 |
+
### 2. 安装依赖
|
| 32 |
+
|
| 33 |
+
```bash
|
| 34 |
+
pip install flask flask-cors
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
### 3. 准备数据
|
| 38 |
+
|
| 39 |
+
将 `books_api_data` 目录放置在项目根目录下:
|
| 40 |
+
|
| 41 |
+
```
|
| 42 |
+
/data/zhangl/code/hf/point/
|
| 43 |
+
├── books_api_data/ # 教材数据目录
|
| 44 |
+
│ ├── 168_一年级上册/
|
| 45 |
+
│ │ ├── book_info.json
|
| 46 |
+
│ │ ├── pages.json
|
| 47 |
+
│ │ ├── catalog.json
|
| 48 |
+
│ │ ├── images/
|
| 49 |
+
│ │ └── audios/
|
| 50 |
+
│ ├── 169_二年级上册/
|
| 51 |
+
│ └── ...
|
| 52 |
+
├── app.py
|
| 53 |
+
├── import_book_data.py
|
| 54 |
+
└── ...
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
### 4. 导入数据到数据库
|
| 58 |
+
|
| 59 |
+
```bash
|
| 60 |
+
# 首次导入(会自动创建数据库)
|
| 61 |
+
python3 import_book_data.py books_api_data
|
| 62 |
+
|
| 63 |
+
# 重新导入所有数据(删除现有数据库)
|
| 64 |
+
python3 import_book_data.py books_api_data --recreate
|
| 65 |
+
|
| 66 |
+
# 仅验证数据
|
| 67 |
+
python3 import_book_data.py --verify-only
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
导入选项:
|
| 71 |
+
- `data_dir`: 数据目录路径(默认:books_api_data)
|
| 72 |
+
- `--db`: 数据库文件路径(默认:books.db)
|
| 73 |
+
- `--schema`: Schema 文件路径(默认:db_schema.sql)
|
| 74 |
+
- `--recreate`: 删除现有数据库并重新创建
|
| 75 |
+
- `--verify-only`: 仅验证数据,不导入
|
| 76 |
+
|
| 77 |
+
### 5. 启动应用
|
| 78 |
+
|
| 79 |
+
```bash
|
| 80 |
+
python3 app.py
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
应用将在 `http://0.0.0.0:7860` 启动。
|
| 84 |
+
|
| 85 |
+
访问地址:
|
| 86 |
+
- 书籍目录:`http://localhost:7860/`
|
| 87 |
+
- 阅读页面:`http://localhost:7860/reader?book_id=168`
|
| 88 |
+
|
| 89 |
+
## 📁 项目结构
|
| 90 |
+
|
| 91 |
+
```
|
| 92 |
+
/data/zhangl/code/hf/point/
|
| 93 |
+
├── books_api_data/ # 教材数据目录
|
| 94 |
+
├── static/
|
| 95 |
+
│ ├── css/
|
| 96 |
+
│ │ ├── style.css # 主样式文件
|
| 97 |
+
│ │ ├── inter.css # Inter 字体
|
| 98 |
+
│ │ └── all.min.css # Font Awesome 图标
|
| 99 |
+
│ └── js/
|
| 100 |
+
│ ├── catalog.js # 书籍目录页面逻辑
|
| 101 |
+
│ └── reader.js # 阅读页面逻辑
|
| 102 |
+
├── app.py # Flask 应用主文件
|
| 103 |
+
├── db_manager.py # 数据库管理器
|
| 104 |
+
├── import_book_data.py # 数据导入脚本
|
| 105 |
+
├── db_schema.sql # 数据库结构定义
|
| 106 |
+
├── index.html # 书籍目录页面
|
| 107 |
+
├── reader.html # 阅读页面
|
| 108 |
+
├── books.db # SQLite 数据库(自动生成)
|
| 109 |
+
└── README.md
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
## 🗄️ 数据库结构
|
| 113 |
+
|
| 114 |
+
### books 表
|
| 115 |
+
存储书籍的基本信息:
|
| 116 |
+
- `market_book_id`: 书籍ID(主键)
|
| 117 |
+
- `market_book_name`: 书籍名称
|
| 118 |
+
- `market_book_cover`: 封面图片路径
|
| 119 |
+
- `max_page`: 最大页码
|
| 120 |
+
- `grade_id`: 年级ID
|
| 121 |
+
- `reel_id`: 学期ID(1=上册,2=下册)
|
| 122 |
+
- 等等...
|
| 123 |
+
|
| 124 |
+
### pages 表
|
| 125 |
+
存储页面信息:
|
| 126 |
+
- `page_id`: 页面ID(主键)
|
| 127 |
+
- `book_id`: 所属书籍ID
|
| 128 |
+
- `page_number`: 页码
|
| 129 |
+
- `origin_img_url`: 原始图片路径
|
| 130 |
+
- `encrypt_img_url`: 加密图片路径
|
| 131 |
+
|
| 132 |
+
### pieces 表
|
| 133 |
+
存储页面内容片段:
|
| 134 |
+
- `piece_id`: 片段ID(主键)
|
| 135 |
+
- `page_id`: 所属页面ID
|
| 136 |
+
- `original`: 原文(英文)
|
| 137 |
+
- `translation`: 译文(中文)
|
| 138 |
+
- `origin_sound_url`: 音频路径
|
| 139 |
+
- `duration`: 音频时长
|
| 140 |
+
- `coordinate_x/y/width/height`: 坐标信息
|
| 141 |
+
- 等等...
|
| 142 |
+
|
| 143 |
+
### catalogs 表
|
| 144 |
+
存储书籍目录结构:
|
| 145 |
+
- `catalog_id`: 目录项ID(主键)
|
| 146 |
+
- `book_id`: 所属书籍ID
|
| 147 |
+
- `catalog_name`: 目录名称(英文)
|
| 148 |
+
- `catalog_name_cn`: 目录名称(中文)
|
| 149 |
+
- `start_page/end_page`: 起止页码
|
| 150 |
+
- `parent_id`: 父级目录ID(支持树形结构)
|
| 151 |
+
|
| 152 |
+
### pieces_fts 表
|
| 153 |
+
全文搜索索引(FTS5):
|
| 154 |
+
- 支持在 `original` 和 `translation` 字段中快速搜索
|
| 155 |
+
|
| 156 |
+
## 🔌 API 接口
|
| 157 |
|
| 158 |
+
### 书籍相关
|
| 159 |
|
| 160 |
+
#### 获取所有书籍列表
|
| 161 |
+
```
|
| 162 |
+
GET /api/v2/books?grade_id=40
|
| 163 |
+
```
|
| 164 |
|
| 165 |
+
#### 获取书籍详情
|
| 166 |
+
```
|
| 167 |
+
GET /api/v2/books/{book_id}
|
| 168 |
+
```
|
| 169 |
|
| 170 |
+
#### 获取书籍页面列表
|
| 171 |
+
```
|
| 172 |
+
GET /api/v2/books/{book_id}/pages
|
| 173 |
+
```
|
|
|
|
| 174 |
|
| 175 |
+
#### 获取书籍目录结构
|
| 176 |
+
```
|
| 177 |
+
GET /api/v2/books/{book_id}/catalog
|
| 178 |
+
```
|
|
|
|
| 179 |
|
| 180 |
+
#### 获取页面内容
|
| 181 |
+
```
|
| 182 |
+
GET /api/v2/books/{book_id}/pages/{page_number}
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
### 搜索相关
|
| 186 |
+
|
| 187 |
+
#### 在书籍中搜索
|
| 188 |
+
```
|
| 189 |
+
GET /api/v2/books/{book_id}/search?keyword=hello&limit=20
|
| 190 |
+
```
|
| 191 |
+
|
| 192 |
+
#### 全局搜索
|
| 193 |
+
```
|
| 194 |
+
GET /api/v2/search?keyword=hello&limit=50
|
| 195 |
+
```
|
| 196 |
+
|
| 197 |
+
### 统计相关
|
| 198 |
+
|
| 199 |
+
#### 获取书籍统计信息
|
| 200 |
+
```
|
| 201 |
+
GET /api/v2/books/{book_id}/statistics
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
#### 获取整体统计信息
|
| 205 |
+
```
|
| 206 |
+
GET /api/v2/statistics
|
| 207 |
+
```
|
| 208 |
|
| 209 |
## ⌨️ 键盘快捷键
|
| 210 |
|
| 211 |
+
在阅读页面中:
|
| 212 |
+
- `←` / `→`: 上一页 / 下一页
|
| 213 |
+
- `Space`: 播放/暂停音频
|
| 214 |
+
- `T`: 切换翻译显示
|
| 215 |
+
- `I`: 切换交互区域显示
|
| 216 |
+
- `Esc`: 关闭弹出面板
|
| 217 |
+
|
| 218 |
+
## 🎨 功能亮点
|
| 219 |
+
|
| 220 |
+
1. **按需加载**: 页面内容按需加载,提高性能
|
| 221 |
+
2. **全文搜索**: 使用 SQLite FTS5 实现高效全文搜索
|
| 222 |
+
3. **响应式设计**: 自适应不同屏幕尺寸
|
| 223 |
+
4. **交互式点读**: 点击文本片段播放对应音频
|
| 224 |
+
5. **坐标系统**: 使用相对坐标,适配不同分辨率
|
| 225 |
+
6. **目录导航**: 支持章节目录快速跳转
|
| 226 |
+
7. **本地资源**: 支持从本地 books_api_data 目录加载资源
|
| 227 |
+
|
| 228 |
+
## 🔧 开发调试
|
| 229 |
+
|
| 230 |
+
### 启用调试模式
|
| 231 |
+
|
| 232 |
+
```bash
|
| 233 |
+
export FLASK_DEBUG=True
|
| 234 |
+
python3 app.py
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
### 查看日志
|
| 238 |
+
|
| 239 |
+
应用日志存储在 `logs/app.log`
|
| 240 |
+
|
| 241 |
+
### 数据库操作
|
| 242 |
+
|
| 243 |
+
使用 `db_manager.py` 提供的数据库管理器:
|
| 244 |
+
|
| 245 |
+
```python
|
| 246 |
+
from db_manager import get_db_instance
|
| 247 |
+
|
| 248 |
+
db = get_db_instance('books.db')
|
| 249 |
+
|
| 250 |
+
# 获取所有书籍
|
| 251 |
+
books = db.get_all_books()
|
| 252 |
+
|
| 253 |
+
# 搜索内容
|
| 254 |
+
results = db.search_content(book_id=168, keyword='hello', limit=20)
|
| 255 |
+
|
| 256 |
+
# 获取统计信息
|
| 257 |
+
stats = db.get_overall_statistics()
|
| 258 |
+
```
|
| 259 |
+
|
| 260 |
+
## 📝 数据格式说明
|
| 261 |
+
|
| 262 |
+
### 坐标系统
|
| 263 |
+
文本片段的坐标使用相对坐标(0-1范围):
|
| 264 |
+
```json
|
| 265 |
+
{
|
| 266 |
+
"coordinate": {
|
| 267 |
+
"x": 0.0482, // 左上角X坐标(相对于图片宽度)
|
| 268 |
+
"y": 0.0357, // 左上角Y坐标(相对于图片高度)
|
| 269 |
+
"width": 0.3195, // 宽度(相对于图片宽度)
|
| 270 |
+
"height": 0.0982 // 高度(相对于图片高度)
|
| 271 |
+
}
|
| 272 |
+
}
|
| 273 |
+
```
|
| 274 |
+
|
| 275 |
+
### 资源路径
|
| 276 |
+
资源路径采用相对路径格式:
|
| 277 |
+
```
|
| 278 |
+
168_一年级上册/images/page_001.jpg
|
| 279 |
+
168_一年级上册/audios/page_002_piece_00.mp3
|
| 280 |
+
```
|
| 281 |
+
|
| 282 |
+
实际访问时会自动拼接为:
|
| 283 |
+
```
|
| 284 |
+
books_api_data/168_一年级上册/images/page_001.jpg
|
| 285 |
+
books_api_data/168_一年级上册/audios/page_002_piece_00.mp3
|
| 286 |
+
```
|
| 287 |
+
|
| 288 |
+
## 🐛 故障排除
|
| 289 |
+
|
| 290 |
+
### 数据库导入失败
|
| 291 |
+
- 检查 books_api_data 目录是否存在
|
| 292 |
+
- 确保每个书籍目录包含 book_info.json、pages.json
|
| 293 |
+
- 检查 db_schema.sql 文件是否存在
|
| 294 |
+
|
| 295 |
+
### 页面加载失败
|
| 296 |
+
- 检查数据库是否正确导入
|
| 297 |
+
- 查看浏览器控制台是否有错误
|
| 298 |
+
- 检查 books_api_data 目录下的资源文件是否完整
|
| 299 |
+
|
| 300 |
+
### 音频播放失败
|
| 301 |
+
- 检查音频文件路径是否正确
|
| 302 |
+
- 确保浏览器支持 MP3 格式
|
| 303 |
+
- 查看浏览器控制台错误信息
|
| 304 |
|
| 305 |
## 📄 许可证
|
| 306 |
|
| 307 |
+
本项目仅供学习和研究使用。
|
| 308 |
+
|
| 309 |
+
## 👥 贡献
|
| 310 |
+
|
| 311 |
+
欢迎提交 Issue 和 Pull Request。
|
| 312 |
+
|
| 313 |
+
## 📞 联系方式
|
| 314 |
+
|
| 315 |
+
如有问题或建议,请联系开发团队。
|
| 316 |
|
| 317 |
---
|
| 318 |
|
| 319 |
+
**版本**: v2.0.0
|
| 320 |
+
**���新日期**: 2025-10-17
|
| 321 |
+
**Python版本**: 3.12
|
| 322 |
+
**数据库**: SQLite 3
|
app.py
CHANGED
|
@@ -16,6 +16,7 @@ from flask import Flask, render_template, send_from_directory, jsonify, request,
|
|
| 16 |
from flask_cors import CORS
|
| 17 |
import logging
|
| 18 |
from logging.handlers import RotatingFileHandler
|
|
|
|
| 19 |
|
| 20 |
# 创建 Flask 应用
|
| 21 |
app = Flask(__name__,
|
|
@@ -70,6 +71,9 @@ OPENSEARCH_CONFIG = {
|
|
| 70 |
# 全局变量:存储学习数据
|
| 71 |
BOOK_DATA = None
|
| 72 |
|
|
|
|
|
|
|
|
|
|
| 73 |
def get_client_ip():
|
| 74 |
"""
|
| 75 |
获取客户端真实 IP 地址
|
|
@@ -94,32 +98,27 @@ def get_client_ip():
|
|
| 94 |
return request.remote_addr or 'Unknown'
|
| 95 |
|
| 96 |
def load_book_data():
|
| 97 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 98 |
global BOOK_DATA
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
try:
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
app.logger.info(f'✅ 成功加载书籍数据')
|
| 103 |
return True
|
| 104 |
except Exception as e:
|
| 105 |
-
app.logger.error(f'❌
|
| 106 |
return False
|
| 107 |
|
| 108 |
-
def check_required_files():
|
| 109 |
-
"""检查必要文件是否存在"""
|
| 110 |
-
required_files = ['index.html', 'style.css', 'script.js', 'book_10242.json']
|
| 111 |
-
missing_files = []
|
| 112 |
-
|
| 113 |
-
for file in required_files:
|
| 114 |
-
if not os.path.exists(file):
|
| 115 |
-
missing_files.append(file)
|
| 116 |
-
|
| 117 |
-
if missing_files:
|
| 118 |
-
app.logger.error(f"❌ 缺少必要文件: {', '.join(missing_files)}")
|
| 119 |
-
return False
|
| 120 |
-
|
| 121 |
-
app.logger.info("✅ 所有必要文件检查完成")
|
| 122 |
-
return True
|
| 123 |
|
| 124 |
# ============================================================================
|
| 125 |
# 路由:静态文件服务
|
|
@@ -127,12 +126,20 @@ def check_required_files():
|
|
| 127 |
|
| 128 |
@app.route('/')
|
| 129 |
def index():
|
| 130 |
-
"""主页"""
|
| 131 |
return send_from_directory('.', 'index.html')
|
| 132 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
@app.route('/<path:filename>')
|
| 134 |
def serve_static(filename):
|
| 135 |
"""提供根目录下的静态文件(如 style.css, script.js)"""
|
|
|
|
|
|
|
|
|
|
| 136 |
return send_from_directory('.', filename)
|
| 137 |
|
| 138 |
# ============================================================================
|
|
@@ -150,33 +157,65 @@ def health_check():
|
|
| 150 |
|
| 151 |
@app.route('/api/book/info')
|
| 152 |
def get_book_info():
|
| 153 |
-
"""
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
@app.route('/api/book/page/<int:page_num>')
|
| 166 |
def get_page_content(page_num):
|
| 167 |
-
"""
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
@app.route('/api/progress/save', methods=['POST'])
|
| 182 |
def save_progress():
|
|
@@ -293,6 +332,515 @@ def get_stats():
|
|
| 293 |
'total_pages': len(BOOK_DATA.get('pages', [])) if BOOK_DATA else 0
|
| 294 |
})
|
| 295 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
# ============================================================================
|
| 297 |
# 错误处理
|
| 298 |
# ============================================================================
|
|
@@ -355,6 +903,56 @@ def after_request(response):
|
|
| 355 |
# 应用初始化和启动
|
| 356 |
# ============================================================================
|
| 357 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
def initialize_app():
|
| 359 |
"""初始化应用"""
|
| 360 |
print("🚀 交互式英语学习应用 - Flask 版本")
|
|
@@ -363,14 +961,32 @@ def initialize_app():
|
|
| 363 |
# 设置日志
|
| 364 |
setup_logging()
|
| 365 |
|
| 366 |
-
# 检查必要文件
|
| 367 |
-
if not
|
|
|
|
| 368 |
return False
|
| 369 |
|
| 370 |
# 加载书籍数据
|
| 371 |
if not load_book_data():
|
| 372 |
return False
|
| 373 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
print("✅ 应用初始化完成")
|
| 375 |
return True
|
| 376 |
|
|
|
|
| 16 |
from flask_cors import CORS
|
| 17 |
import logging
|
| 18 |
from logging.handlers import RotatingFileHandler
|
| 19 |
+
from db_manager import get_db_instance
|
| 20 |
|
| 21 |
# 创建 Flask 应用
|
| 22 |
app = Flask(__name__,
|
|
|
|
| 71 |
# 全局变量:存储学习数据
|
| 72 |
BOOK_DATA = None
|
| 73 |
|
| 74 |
+
# 数据库实例
|
| 75 |
+
DB = None
|
| 76 |
+
|
| 77 |
def get_client_ip():
|
| 78 |
"""
|
| 79 |
获取客户端真实 IP 地址
|
|
|
|
| 98 |
return request.remote_addr or 'Unknown'
|
| 99 |
|
| 100 |
def load_book_data():
|
| 101 |
+
"""
|
| 102 |
+
加载书籍数据(已废弃,保留是为了兼容性)
|
| 103 |
+
现在使用数据库接口,不再加载JSON文件
|
| 104 |
+
"""
|
| 105 |
global BOOK_DATA
|
| 106 |
+
app.logger.info('ℹ️ load_book_data 已废弃,使用数据库接口')
|
| 107 |
+
# 设置为空字典表示已初始化,但不再使用
|
| 108 |
+
BOOK_DATA = {}
|
| 109 |
+
return True
|
| 110 |
+
|
| 111 |
+
def init_database():
|
| 112 |
+
"""初始化数据库连接"""
|
| 113 |
+
global DB
|
| 114 |
try:
|
| 115 |
+
DB = get_db_instance('books.db')
|
| 116 |
+
app.logger.info('✅ 数据库连接初始化成功')
|
|
|
|
| 117 |
return True
|
| 118 |
except Exception as e:
|
| 119 |
+
app.logger.error(f'❌ 数据库初始化失败: {e}')
|
| 120 |
return False
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
# ============================================================================
|
| 124 |
# 路由:静态文件服务
|
|
|
|
| 126 |
|
| 127 |
@app.route('/')
|
| 128 |
def index():
|
| 129 |
+
"""主页 - 书籍目录"""
|
| 130 |
return send_from_directory('.', 'index.html')
|
| 131 |
|
| 132 |
+
@app.route('/reader')
|
| 133 |
+
def reader():
|
| 134 |
+
"""阅读页面"""
|
| 135 |
+
return send_from_directory('.', 'reader.html')
|
| 136 |
+
|
| 137 |
@app.route('/<path:filename>')
|
| 138 |
def serve_static(filename):
|
| 139 |
"""提供根目录下的静态文件(如 style.css, script.js)"""
|
| 140 |
+
# 避免与API路由冲突
|
| 141 |
+
if filename.startswith('api/'):
|
| 142 |
+
return jsonify({'error': '接口不存在'}), 404
|
| 143 |
return send_from_directory('.', filename)
|
| 144 |
|
| 145 |
# ============================================================================
|
|
|
|
| 157 |
|
| 158 |
@app.route('/api/book/info')
|
| 159 |
def get_book_info():
|
| 160 |
+
"""
|
| 161 |
+
获取书籍信息(旧接口,保留兼容性)
|
| 162 |
+
推荐使用: /api/v2/books/<book_id>
|
| 163 |
+
"""
|
| 164 |
+
try:
|
| 165 |
+
if not DB:
|
| 166 |
+
return jsonify({'error': '数据库未初始化'}), 500
|
| 167 |
+
|
| 168 |
+
# 默认获取第一本书的信息(为了兼容旧版本)
|
| 169 |
+
books = DB.get_all_books()
|
| 170 |
+
if not books:
|
| 171 |
+
return jsonify({'error': '没有找到书籍数据'}), 404
|
| 172 |
+
|
| 173 |
+
# 使用最后导入的书籍
|
| 174 |
+
book = books[-1]
|
| 175 |
+
|
| 176 |
+
# 返回书籍元信息
|
| 177 |
+
info = {
|
| 178 |
+
'book_id': book['market_book_id'],
|
| 179 |
+
'total_pages': book['max_page'],
|
| 180 |
+
'title': book['market_book_name'],
|
| 181 |
+
'loaded': True
|
| 182 |
+
}
|
| 183 |
+
return jsonify(info)
|
| 184 |
+
except Exception as e:
|
| 185 |
+
app.logger.error(f"获取书籍信息失败: {e}")
|
| 186 |
+
return jsonify({'error': str(e)}), 500
|
| 187 |
|
| 188 |
@app.route('/api/book/page/<int:page_num>')
|
| 189 |
def get_page_content(page_num):
|
| 190 |
+
"""
|
| 191 |
+
获取指定页面内容(旧接口,保留兼容性)
|
| 192 |
+
推荐使用: /api/v2/books/<book_id>/pages/<page_num>
|
| 193 |
+
"""
|
| 194 |
+
try:
|
| 195 |
+
if not DB:
|
| 196 |
+
return jsonify({'error': '数据库未初始化'}), 500
|
| 197 |
+
|
| 198 |
+
# 获取最后导入的书籍
|
| 199 |
+
books = DB.get_all_books()
|
| 200 |
+
if not books:
|
| 201 |
+
return jsonify({'error': '没有找到书籍数据'}), 404
|
| 202 |
+
|
| 203 |
+
book = books[-1]
|
| 204 |
+
book_id = book['market_book_id']
|
| 205 |
+
|
| 206 |
+
# 获取页面内容
|
| 207 |
+
page = DB.get_page_content(book_id, page_num)
|
| 208 |
+
if not page:
|
| 209 |
+
return jsonify({'error': '页码超出范围'}), 404
|
| 210 |
+
|
| 211 |
+
return jsonify({
|
| 212 |
+
'page_num': page_num,
|
| 213 |
+
'content': page,
|
| 214 |
+
'total_pages': book['max_page']
|
| 215 |
+
})
|
| 216 |
+
except Exception as e:
|
| 217 |
+
app.logger.error(f"获取页面内容失败: {e}")
|
| 218 |
+
return jsonify({'error': str(e)}), 500
|
| 219 |
|
| 220 |
@app.route('/api/progress/save', methods=['POST'])
|
| 221 |
def save_progress():
|
|
|
|
| 332 |
'total_pages': len(BOOK_DATA.get('pages', [])) if BOOK_DATA else 0
|
| 333 |
})
|
| 334 |
|
| 335 |
+
# ============================================================================
|
| 336 |
+
# 路由:新版API端点(基于SQLite数据库)
|
| 337 |
+
# ============================================================================
|
| 338 |
+
|
| 339 |
+
@app.route('/api/v2/books')
|
| 340 |
+
def get_books():
|
| 341 |
+
"""
|
| 342 |
+
获取所有书籍列表
|
| 343 |
+
|
| 344 |
+
Query Parameters:
|
| 345 |
+
grade_id: 年级ID(可选)
|
| 346 |
+
|
| 347 |
+
Returns:
|
| 348 |
+
{
|
| 349 |
+
"success": true,
|
| 350 |
+
"count": 10,
|
| 351 |
+
"books": [
|
| 352 |
+
{
|
| 353 |
+
"market_book_id": 168,
|
| 354 |
+
"market_book_name": "一年级上册",
|
| 355 |
+
"market_book_cover": "168_一年级上册/images/page_001.jpg",
|
| 356 |
+
"max_page": 73,
|
| 357 |
+
"market_classify_name": "沪教版(深圳)",
|
| 358 |
+
"grade_id": 40,
|
| 359 |
+
"reel_id": 1
|
| 360 |
+
}
|
| 361 |
+
]
|
| 362 |
+
}
|
| 363 |
+
"""
|
| 364 |
+
try:
|
| 365 |
+
if not DB:
|
| 366 |
+
return jsonify({
|
| 367 |
+
'success': False,
|
| 368 |
+
'error': '数据库未初始化'
|
| 369 |
+
}), 500
|
| 370 |
+
|
| 371 |
+
# 检查是否有年级筛选
|
| 372 |
+
grade_id = request.args.get('grade_id', type=int)
|
| 373 |
+
|
| 374 |
+
if grade_id:
|
| 375 |
+
books = DB.get_books_by_grade(grade_id)
|
| 376 |
+
else:
|
| 377 |
+
books = DB.get_all_books()
|
| 378 |
+
|
| 379 |
+
app.logger.info(f"获取书籍列表: {len(books)} 本书")
|
| 380 |
+
|
| 381 |
+
return jsonify({
|
| 382 |
+
'success': True,
|
| 383 |
+
'count': len(books),
|
| 384 |
+
'books': books
|
| 385 |
+
})
|
| 386 |
+
except Exception as e:
|
| 387 |
+
app.logger.error(f"获取书籍列表失败: {e}")
|
| 388 |
+
return jsonify({
|
| 389 |
+
'success': False,
|
| 390 |
+
'error': str(e)
|
| 391 |
+
}), 500
|
| 392 |
+
|
| 393 |
+
@app.route('/api/v2/books/<int:book_id>')
|
| 394 |
+
def get_book_info_v2(book_id):
|
| 395 |
+
"""
|
| 396 |
+
获取指定书籍的详细信息
|
| 397 |
+
|
| 398 |
+
Args:
|
| 399 |
+
book_id: 书籍ID
|
| 400 |
+
|
| 401 |
+
Returns:
|
| 402 |
+
{
|
| 403 |
+
"success": true,
|
| 404 |
+
"book": {
|
| 405 |
+
"book_id": 1,
|
| 406 |
+
"book_name": "书籍名称",
|
| 407 |
+
"total_pages": 100,
|
| 408 |
+
...
|
| 409 |
+
}
|
| 410 |
+
}
|
| 411 |
+
"""
|
| 412 |
+
try:
|
| 413 |
+
if not DB:
|
| 414 |
+
return jsonify({
|
| 415 |
+
'success': False,
|
| 416 |
+
'error': '数据库未初始化'
|
| 417 |
+
}), 500
|
| 418 |
+
|
| 419 |
+
book = DB.get_book_by_id(book_id)
|
| 420 |
+
if not book:
|
| 421 |
+
return jsonify({
|
| 422 |
+
'success': False,
|
| 423 |
+
'error': '书籍不存在'
|
| 424 |
+
}), 404
|
| 425 |
+
|
| 426 |
+
app.logger.info(f"获取书籍信息: ID={book_id}")
|
| 427 |
+
|
| 428 |
+
return jsonify({
|
| 429 |
+
'success': True,
|
| 430 |
+
'book': book
|
| 431 |
+
})
|
| 432 |
+
except Exception as e:
|
| 433 |
+
app.logger.error(f"获取书籍信息失败: {e}")
|
| 434 |
+
return jsonify({
|
| 435 |
+
'success': False,
|
| 436 |
+
'error': str(e)
|
| 437 |
+
}), 500
|
| 438 |
+
|
| 439 |
+
@app.route('/api/v2/books/<int:book_id>/pages')
|
| 440 |
+
def get_book_pages(book_id):
|
| 441 |
+
"""
|
| 442 |
+
获取书籍的所有页面列表(不含片段内容)
|
| 443 |
+
|
| 444 |
+
Args:
|
| 445 |
+
book_id: 书籍ID
|
| 446 |
+
|
| 447 |
+
Returns:
|
| 448 |
+
{
|
| 449 |
+
"success": true,
|
| 450 |
+
"book_id": 168,
|
| 451 |
+
"book_name": "一年级上册",
|
| 452 |
+
"total_pages": 73,
|
| 453 |
+
"pages": [
|
| 454 |
+
{
|
| 455 |
+
"page_id": 2111,
|
| 456 |
+
"page_number": 1,
|
| 457 |
+
"origin_img_url": "...",
|
| 458 |
+
"encrypt_img_url": "..."
|
| 459 |
+
}
|
| 460 |
+
]
|
| 461 |
+
}
|
| 462 |
+
"""
|
| 463 |
+
try:
|
| 464 |
+
if not DB:
|
| 465 |
+
return jsonify({
|
| 466 |
+
'success': False,
|
| 467 |
+
'error': '数据库未初始化'
|
| 468 |
+
}), 500
|
| 469 |
+
|
| 470 |
+
# 检查书籍是否存在
|
| 471 |
+
book = DB.get_book_by_id(book_id)
|
| 472 |
+
if not book:
|
| 473 |
+
return jsonify({
|
| 474 |
+
'success': False,
|
| 475 |
+
'error': '书籍不存在'
|
| 476 |
+
}), 404
|
| 477 |
+
|
| 478 |
+
# 获取页面列表
|
| 479 |
+
pages = DB.get_book_pages(book_id)
|
| 480 |
+
|
| 481 |
+
app.logger.info(f"获取书籍页面列表: 书籍ID={book_id}, 页数={len(pages)}")
|
| 482 |
+
|
| 483 |
+
return jsonify({
|
| 484 |
+
'success': True,
|
| 485 |
+
'book_id': book_id,
|
| 486 |
+
'book_name': book['market_book_name'],
|
| 487 |
+
'total_pages': len(pages),
|
| 488 |
+
'pages': pages
|
| 489 |
+
})
|
| 490 |
+
except Exception as e:
|
| 491 |
+
app.logger.error(f"获取书籍页面列表失败: {e}")
|
| 492 |
+
return jsonify({
|
| 493 |
+
'success': False,
|
| 494 |
+
'error': str(e)
|
| 495 |
+
}), 500
|
| 496 |
+
|
| 497 |
+
@app.route('/api/v2/books/<int:book_id>/catalog')
|
| 498 |
+
def get_book_catalog(book_id):
|
| 499 |
+
"""
|
| 500 |
+
获取书籍目录结构(章节目录)
|
| 501 |
+
|
| 502 |
+
Args:
|
| 503 |
+
book_id: 书籍ID
|
| 504 |
+
|
| 505 |
+
Returns:
|
| 506 |
+
{
|
| 507 |
+
"success": true,
|
| 508 |
+
"book_id": 168,
|
| 509 |
+
"book_name": "一年级上册",
|
| 510 |
+
"catalog": [
|
| 511 |
+
{
|
| 512 |
+
"catalog_id": 1,
|
| 513 |
+
"catalog_name": "Unit 1 Hello",
|
| 514 |
+
"catalog_name_cn": "第一单元 你好",
|
| 515 |
+
"start_page": 2,
|
| 516 |
+
"end_page": 10,
|
| 517 |
+
"thumbnail": "...",
|
| 518 |
+
"children": []
|
| 519 |
+
}
|
| 520 |
+
]
|
| 521 |
+
}
|
| 522 |
+
"""
|
| 523 |
+
try:
|
| 524 |
+
if not DB:
|
| 525 |
+
return jsonify({
|
| 526 |
+
'success': False,
|
| 527 |
+
'error': '数据库未初始化'
|
| 528 |
+
}), 500
|
| 529 |
+
|
| 530 |
+
# 检查书籍是否存在
|
| 531 |
+
book = DB.get_book_by_id(book_id)
|
| 532 |
+
if not book:
|
| 533 |
+
return jsonify({
|
| 534 |
+
'success': False,
|
| 535 |
+
'error': '书籍不存在'
|
| 536 |
+
}), 404
|
| 537 |
+
|
| 538 |
+
# 获取目录
|
| 539 |
+
catalog = DB.get_book_catalog(book_id)
|
| 540 |
+
|
| 541 |
+
app.logger.info(f"获取书籍目录: 书籍ID={book_id}, 目录项={len(catalog)}")
|
| 542 |
+
|
| 543 |
+
return jsonify({
|
| 544 |
+
'success': True,
|
| 545 |
+
'book_id': book_id,
|
| 546 |
+
'book_name': book['market_book_name'],
|
| 547 |
+
'catalog': catalog
|
| 548 |
+
})
|
| 549 |
+
except Exception as e:
|
| 550 |
+
app.logger.error(f"获取书籍目录失败: {e}")
|
| 551 |
+
return jsonify({
|
| 552 |
+
'success': False,
|
| 553 |
+
'error': str(e)
|
| 554 |
+
}), 500
|
| 555 |
+
|
| 556 |
+
@app.route('/api/v2/books/<int:book_id>/pages/<int:page_num>')
|
| 557 |
+
def get_page_content_v2(book_id, page_num):
|
| 558 |
+
"""
|
| 559 |
+
获取指定页面的完整内容
|
| 560 |
+
|
| 561 |
+
Args:
|
| 562 |
+
book_id: 书籍ID
|
| 563 |
+
page_num: 页码
|
| 564 |
+
|
| 565 |
+
Returns:
|
| 566 |
+
{
|
| 567 |
+
"success": true,
|
| 568 |
+
"book_id": 1,
|
| 569 |
+
"page": {
|
| 570 |
+
"page_id": 1001,
|
| 571 |
+
"page_number": 1,
|
| 572 |
+
"origin_img_url": "https://...",
|
| 573 |
+
"pieces": [
|
| 574 |
+
{
|
| 575 |
+
"piece_id": 10001,
|
| 576 |
+
"original": "Hello",
|
| 577 |
+
"translation": "你好",
|
| 578 |
+
"origin_sound_url": "https://...",
|
| 579 |
+
...
|
| 580 |
+
}
|
| 581 |
+
],
|
| 582 |
+
"piece_count": 10
|
| 583 |
+
}
|
| 584 |
+
}
|
| 585 |
+
"""
|
| 586 |
+
try:
|
| 587 |
+
if not DB:
|
| 588 |
+
return jsonify({
|
| 589 |
+
'success': False,
|
| 590 |
+
'error': '数据库未初始化'
|
| 591 |
+
}), 500
|
| 592 |
+
|
| 593 |
+
# 检查书籍是否存在
|
| 594 |
+
book = DB.get_book_by_id(book_id)
|
| 595 |
+
if not book:
|
| 596 |
+
return jsonify({
|
| 597 |
+
'success': False,
|
| 598 |
+
'error': '书籍不存在'
|
| 599 |
+
}), 404
|
| 600 |
+
|
| 601 |
+
# 获取页面内容
|
| 602 |
+
page = DB.get_page_content(book_id, page_num)
|
| 603 |
+
if not page:
|
| 604 |
+
return jsonify({
|
| 605 |
+
'success': False,
|
| 606 |
+
'error': f'页码 {page_num} 不存在'
|
| 607 |
+
}), 404
|
| 608 |
+
|
| 609 |
+
app.logger.info(f"获取页面内容: 书籍ID={book_id}, 页码={page_num}")
|
| 610 |
+
|
| 611 |
+
return jsonify({
|
| 612 |
+
'success': True,
|
| 613 |
+
'book_id': book_id,
|
| 614 |
+
'book_name': book['market_book_name'],
|
| 615 |
+
'page': page
|
| 616 |
+
})
|
| 617 |
+
except Exception as e:
|
| 618 |
+
app.logger.error(f"获取页面内容失败: {e}")
|
| 619 |
+
return jsonify({
|
| 620 |
+
'success': False,
|
| 621 |
+
'error': str(e)
|
| 622 |
+
}), 500
|
| 623 |
+
|
| 624 |
+
@app.route('/api/v2/books/<int:book_id>/search')
|
| 625 |
+
def search_book_content(book_id):
|
| 626 |
+
"""
|
| 627 |
+
在书籍中搜索内容
|
| 628 |
+
|
| 629 |
+
Query Parameters:
|
| 630 |
+
keyword: 搜索关键词
|
| 631 |
+
limit: 返回结果数量(默认20)
|
| 632 |
+
|
| 633 |
+
Returns:
|
| 634 |
+
{
|
| 635 |
+
"success": true,
|
| 636 |
+
"book_id": 168,
|
| 637 |
+
"keyword": "hello",
|
| 638 |
+
"count": 5,
|
| 639 |
+
"results": [
|
| 640 |
+
{
|
| 641 |
+
"page_number": 2,
|
| 642 |
+
"piece_id": 26342,
|
| 643 |
+
"original": "Hello",
|
| 644 |
+
"translation": "你好",
|
| 645 |
+
"origin_sound_url": "..."
|
| 646 |
+
}
|
| 647 |
+
]
|
| 648 |
+
}
|
| 649 |
+
"""
|
| 650 |
+
try:
|
| 651 |
+
if not DB:
|
| 652 |
+
return jsonify({
|
| 653 |
+
'success': False,
|
| 654 |
+
'error': '数据库未初始化'
|
| 655 |
+
}), 500
|
| 656 |
+
|
| 657 |
+
keyword = request.args.get('keyword', '').strip()
|
| 658 |
+
if not keyword:
|
| 659 |
+
return jsonify({
|
| 660 |
+
'success': False,
|
| 661 |
+
'error': '请提供搜索关键词'
|
| 662 |
+
}), 400
|
| 663 |
+
|
| 664 |
+
limit = request.args.get('limit', 20, type=int)
|
| 665 |
+
|
| 666 |
+
# 检查书籍��否存在
|
| 667 |
+
book = DB.get_book_by_id(book_id)
|
| 668 |
+
if not book:
|
| 669 |
+
return jsonify({
|
| 670 |
+
'success': False,
|
| 671 |
+
'error': '书籍不存在'
|
| 672 |
+
}), 404
|
| 673 |
+
|
| 674 |
+
# 搜索
|
| 675 |
+
results = DB.search_content(book_id, keyword, limit)
|
| 676 |
+
|
| 677 |
+
app.logger.info(f"搜索内容: 书籍ID={book_id}, 关键词={keyword}, 结果数={len(results)}")
|
| 678 |
+
|
| 679 |
+
return jsonify({
|
| 680 |
+
'success': True,
|
| 681 |
+
'book_id': book_id,
|
| 682 |
+
'book_name': book['market_book_name'],
|
| 683 |
+
'keyword': keyword,
|
| 684 |
+
'count': len(results),
|
| 685 |
+
'results': results
|
| 686 |
+
})
|
| 687 |
+
except Exception as e:
|
| 688 |
+
app.logger.error(f"搜索失败: {e}")
|
| 689 |
+
return jsonify({
|
| 690 |
+
'success': False,
|
| 691 |
+
'error': str(e)
|
| 692 |
+
}), 500
|
| 693 |
+
|
| 694 |
+
@app.route('/api/v2/search')
|
| 695 |
+
def search_all_content():
|
| 696 |
+
"""
|
| 697 |
+
在所有书籍中搜索内容
|
| 698 |
+
|
| 699 |
+
Query Parameters:
|
| 700 |
+
keyword: 搜索关键词
|
| 701 |
+
limit: 返回结果数量(默认50)
|
| 702 |
+
|
| 703 |
+
Returns:
|
| 704 |
+
{
|
| 705 |
+
"success": true,
|
| 706 |
+
"keyword": "hello",
|
| 707 |
+
"count": 15,
|
| 708 |
+
"results": [
|
| 709 |
+
{
|
| 710 |
+
"market_book_id": 168,
|
| 711 |
+
"market_book_name": "一年级上册",
|
| 712 |
+
"page_number": 2,
|
| 713 |
+
"piece_id": 26342,
|
| 714 |
+
"original": "Hello",
|
| 715 |
+
"translation": "你好",
|
| 716 |
+
"origin_sound_url": "..."
|
| 717 |
+
}
|
| 718 |
+
]
|
| 719 |
+
}
|
| 720 |
+
"""
|
| 721 |
+
try:
|
| 722 |
+
if not DB:
|
| 723 |
+
return jsonify({
|
| 724 |
+
'success': False,
|
| 725 |
+
'error': '数据库未初始化'
|
| 726 |
+
}), 500
|
| 727 |
+
|
| 728 |
+
keyword = request.args.get('keyword', '').strip()
|
| 729 |
+
if not keyword:
|
| 730 |
+
return jsonify({
|
| 731 |
+
'success': False,
|
| 732 |
+
'error': '请提供搜索关键词'
|
| 733 |
+
}), 400
|
| 734 |
+
|
| 735 |
+
limit = request.args.get('limit', 50, type=int)
|
| 736 |
+
|
| 737 |
+
# 搜索所有书籍
|
| 738 |
+
results = DB.search_all_books(keyword, limit)
|
| 739 |
+
|
| 740 |
+
app.logger.info(f"全局搜索: 关键词={keyword}, 结果数={len(results)}")
|
| 741 |
+
|
| 742 |
+
return jsonify({
|
| 743 |
+
'success': True,
|
| 744 |
+
'keyword': keyword,
|
| 745 |
+
'count': len(results),
|
| 746 |
+
'results': results
|
| 747 |
+
})
|
| 748 |
+
except Exception as e:
|
| 749 |
+
app.logger.error(f"搜索失败: {e}")
|
| 750 |
+
return jsonify({
|
| 751 |
+
'success': False,
|
| 752 |
+
'error': str(e)
|
| 753 |
+
}), 500
|
| 754 |
+
|
| 755 |
+
@app.route('/api/v2/books/<int:book_id>/statistics')
|
| 756 |
+
def get_book_statistics(book_id):
|
| 757 |
+
"""
|
| 758 |
+
获取书籍统计信息
|
| 759 |
+
|
| 760 |
+
Args:
|
| 761 |
+
book_id: 书籍ID
|
| 762 |
+
|
| 763 |
+
Returns:
|
| 764 |
+
{
|
| 765 |
+
"success": true,
|
| 766 |
+
"statistics": {
|
| 767 |
+
"book_id": 168,
|
| 768 |
+
"total_pages": 73,
|
| 769 |
+
"total_pieces": 500,
|
| 770 |
+
"total_audio": 450,
|
| 771 |
+
"total_catalogs": 12
|
| 772 |
+
}
|
| 773 |
+
}
|
| 774 |
+
"""
|
| 775 |
+
try:
|
| 776 |
+
if not DB:
|
| 777 |
+
return jsonify({
|
| 778 |
+
'success': False,
|
| 779 |
+
'error': '数据库未初始化'
|
| 780 |
+
}), 500
|
| 781 |
+
|
| 782 |
+
# 检查书籍是否存在
|
| 783 |
+
book = DB.get_book_by_id(book_id)
|
| 784 |
+
if not book:
|
| 785 |
+
return jsonify({
|
| 786 |
+
'success': False,
|
| 787 |
+
'error': '书籍不存在'
|
| 788 |
+
}), 404
|
| 789 |
+
|
| 790 |
+
# 获取统计信息
|
| 791 |
+
stats = DB.get_book_statistics(book_id)
|
| 792 |
+
|
| 793 |
+
app.logger.info(f"获取统计信息: 书籍ID={book_id}")
|
| 794 |
+
|
| 795 |
+
return jsonify({
|
| 796 |
+
'success': True,
|
| 797 |
+
'statistics': stats
|
| 798 |
+
})
|
| 799 |
+
except Exception as e:
|
| 800 |
+
app.logger.error(f"获取统计信息失败: {e}")
|
| 801 |
+
return jsonify({
|
| 802 |
+
'success': False,
|
| 803 |
+
'error': str(e)
|
| 804 |
+
}), 500
|
| 805 |
+
|
| 806 |
+
@app.route('/api/v2/statistics')
|
| 807 |
+
def get_overall_statistics():
|
| 808 |
+
"""
|
| 809 |
+
获取整体统计信息
|
| 810 |
+
|
| 811 |
+
Returns:
|
| 812 |
+
{
|
| 813 |
+
"success": true,
|
| 814 |
+
"statistics": {
|
| 815 |
+
"total_books": 30,
|
| 816 |
+
"total_pages": 2000,
|
| 817 |
+
"total_pieces": 50000,
|
| 818 |
+
"total_catalogs": 360
|
| 819 |
+
}
|
| 820 |
+
}
|
| 821 |
+
"""
|
| 822 |
+
try:
|
| 823 |
+
if not DB:
|
| 824 |
+
return jsonify({
|
| 825 |
+
'success': False,
|
| 826 |
+
'error': '数据库未初始化'
|
| 827 |
+
}), 500
|
| 828 |
+
|
| 829 |
+
stats = DB.get_overall_statistics()
|
| 830 |
+
|
| 831 |
+
app.logger.info("获取整体统计信息")
|
| 832 |
+
|
| 833 |
+
return jsonify({
|
| 834 |
+
'success': True,
|
| 835 |
+
'statistics': stats
|
| 836 |
+
})
|
| 837 |
+
except Exception as e:
|
| 838 |
+
app.logger.error(f"获取统计信息失败: {e}")
|
| 839 |
+
return jsonify({
|
| 840 |
+
'success': False,
|
| 841 |
+
'error': str(e)
|
| 842 |
+
}), 500
|
| 843 |
+
|
| 844 |
# ============================================================================
|
| 845 |
# 错误处理
|
| 846 |
# ============================================================================
|
|
|
|
| 903 |
# 应用初始化和启动
|
| 904 |
# ============================================================================
|
| 905 |
|
| 906 |
+
def auto_generate_database():
|
| 907 |
+
"""
|
| 908 |
+
自动生成数据库
|
| 909 |
+
如果 books.db 不存在,自动运行导入脚本生成数据库
|
| 910 |
+
"""
|
| 911 |
+
db_path = 'books.db'
|
| 912 |
+
schema_path = 'db_schema.sql'
|
| 913 |
+
data_dir = 'books_api_data'
|
| 914 |
+
|
| 915 |
+
# 检查必要的文件和目录
|
| 916 |
+
if not os.path.exists(schema_path):
|
| 917 |
+
print(f"❌ 错误: Schema文件 {schema_path} 不存在")
|
| 918 |
+
return False
|
| 919 |
+
|
| 920 |
+
if not os.path.exists(data_dir):
|
| 921 |
+
print(f"❌ 错误: 数据目录 {data_dir} 不存在")
|
| 922 |
+
print(f" 无法自动生成数据库,请先准备数据")
|
| 923 |
+
return False
|
| 924 |
+
|
| 925 |
+
print(f"📦 未找到数据库文件,正在自动生成...")
|
| 926 |
+
print(f" 数据目录: {data_dir}")
|
| 927 |
+
print(f" 这可能需要几分钟时间,请稍候...")
|
| 928 |
+
print("=" * 60)
|
| 929 |
+
|
| 930 |
+
try:
|
| 931 |
+
# 导入必要的函数
|
| 932 |
+
from import_book_data import create_database, import_all_books, verify_data
|
| 933 |
+
|
| 934 |
+
# 创建数据库
|
| 935 |
+
conn = create_database(db_path, schema_path)
|
| 936 |
+
|
| 937 |
+
# 导入所有书籍数据
|
| 938 |
+
import_all_books(conn, data_dir)
|
| 939 |
+
|
| 940 |
+
# 验证数据
|
| 941 |
+
verify_data(conn)
|
| 942 |
+
|
| 943 |
+
# 关闭连接
|
| 944 |
+
conn.close()
|
| 945 |
+
|
| 946 |
+
print("=" * 60)
|
| 947 |
+
print("✅ 数据库自动生成完成!")
|
| 948 |
+
print("=" * 60)
|
| 949 |
+
return True
|
| 950 |
+
|
| 951 |
+
except Exception as e:
|
| 952 |
+
print(f"❌ 自动生成数据库失败: {e}")
|
| 953 |
+
app.logger.error(f"自动生成数据库失败: {e}", exc_info=True)
|
| 954 |
+
return False
|
| 955 |
+
|
| 956 |
def initialize_app():
|
| 957 |
"""初始化应用"""
|
| 958 |
print("🚀 交互式英语学习应用 - Flask 版本")
|
|
|
|
| 961 |
# 设置日志
|
| 962 |
setup_logging()
|
| 963 |
|
| 964 |
+
# 检查必要文件(暂时不检查 books.db,因为会自动生成)
|
| 965 |
+
if not os.path.exists('index.html'):
|
| 966 |
+
app.logger.error(f"❌ 缺少必要文件: index.html")
|
| 967 |
return False
|
| 968 |
|
| 969 |
# 加载书籍数据
|
| 970 |
if not load_book_data():
|
| 971 |
return False
|
| 972 |
|
| 973 |
+
# 检查数据库文件
|
| 974 |
+
if not os.path.exists('books.db'):
|
| 975 |
+
print("ℹ️ 未找到数据库文件 books.db")
|
| 976 |
+
print("🔄 正在自动生成数据库...")
|
| 977 |
+
|
| 978 |
+
# 自动生成数据库
|
| 979 |
+
if not auto_generate_database():
|
| 980 |
+
print("❌ 数据库自动生成失败")
|
| 981 |
+
print(" 请手动运行: python3 import_book_data.py")
|
| 982 |
+
return False
|
| 983 |
+
|
| 984 |
+
# 初始化数据库连接
|
| 985 |
+
print("📚 正在初始化数据库连接...")
|
| 986 |
+
if not init_database():
|
| 987 |
+
print("❌ 数据库初始化失败")
|
| 988 |
+
return False
|
| 989 |
+
|
| 990 |
print("✅ 应用初始化完成")
|
| 991 |
return True
|
| 992 |
|
books_api_data/10005_一年级上册/audios/page_002_piece_00.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a5f33e7250177e0b9919b0c2b2f1e1eb58f059efd17146821b49aeb6cac19bf3
|
| 3 |
+
size 51826
|
books_api_data/10005_一年级上册/audios/page_003_piece_00.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f3db8d98e71f8fadcb57b1eff3fd1652dfd0b12f3fb3021881df15ca7a884921
|
| 3 |
+
size 45139
|
books_api_data/10005_一年级上册/audios/page_004_piece_00.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0b43ed35c99df647d6205526e780b0492982f42e8e7bc86276674b82305a3d44
|
| 3 |
+
size 42631
|
books_api_data/10005_一年级上册/audios/page_004_piece_01.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f976fad7083db695b0210e81c7c37fdc1a3751be9d0cf52c6452454158b05827
|
| 3 |
+
size 36362
|
books_api_data/10005_一年级上册/audios/page_004_piece_02.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:db1910ca633d8ca10e97f09dd3edc210ed66772510013b4525fd56bdc1ce0cb7
|
| 3 |
+
size 59768
|
books_api_data/10005_一年级上册/audios/page_004_piece_03.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5823b138fa96edbff4e42c46d3de1eed62e76c27040a623388865450ab207901
|
| 3 |
+
size 52245
|
books_api_data/10005_一年级上册/audios/page_004_piece_04.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1c2ecddb71a94cea2fedffecee8babd2e20d949aa63e0c45073e9565c7ebaca0
|
| 3 |
+
size 71053
|
books_api_data/10005_一年级上册/audios/page_004_piece_05.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d143cccfe5d0c48dd7f2f039fb244bacc3c346741b9179866e7b6258e6df6f3d
|
| 3 |
+
size 82338
|
books_api_data/10005_一年级上册/audios/page_004_piece_06.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7d1a9b7680628a28f861105a30f66d9b32b1e2f5d228282c84e3b743e5747aa8
|
| 3 |
+
size 50155
|
books_api_data/10005_一年级上册/audios/page_004_piece_07.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dd5afef021a9e411b217b81fa90d8d6760e014aab881ead835b6d205a153d316
|
| 3 |
+
size 49737
|
books_api_data/10005_一年级上册/audios/page_004_piece_08.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:892987c43bbc5b99242fd554d8ae99015ccb0849ed98000f2f8b4aa1af52bb73
|
| 3 |
+
size 58514
|
books_api_data/10005_一年级上册/audios/page_004_piece_09.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:873cdbbb4978f991636e332a5dc5dad869c9d754ad1a1872ffb2136a16342079
|
| 3 |
+
size 90379
|
books_api_data/10005_一年级上册/audios/page_005_piece_00.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2ef96ddf79d08895258099e26255c97cb6cff1dd493dc83970f25759788b120c
|
| 3 |
+
size 45975
|
books_api_data/10005_一年级上册/audios/page_005_piece_01.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c9ef26e060ca1db607b9362413332a2f3abd24992348565abc785a299a513266
|
| 3 |
+
size 32182
|
books_api_data/10005_一年级上册/audios/page_005_piece_02.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:85864c7d35a0cb0e57f796a760c653d2a44fb2b6e17812a7ab36332fcd835bc2
|
| 3 |
+
size 28839
|
books_api_data/10005_一年级上册/audios/page_005_piece_03.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6eb99945c52d876ed1c4efbe6ecf4dc68ed199fc4bbbfd6c654806c6fc30adca
|
| 3 |
+
size 33436
|
books_api_data/10005_一年级上册/audios/page_005_piece_04.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:21c220fd9848e6d77f2fb977bc79c44e7df817acfc6a1e934b989a85e99b6f59
|
| 3 |
+
size 32600
|
books_api_data/10005_一年级上册/audios/page_006_piece_00.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1fb27d35047bb161899fcf97d84bc906ae17dcc5328230bc78e62b9c91316bbb
|
| 3 |
+
size 46811
|
books_api_data/10005_一年级上册/audios/page_006_piece_01.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:975e3dc752f7477e7a514f57cc0a78f8bd8aaeefd991d7155d763fe27faf4974
|
| 3 |
+
size 25913
|
books_api_data/10005_一年级上册/audios/page_006_piece_02.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e5683b45230fb533fd2793b8bc1dd28f00005b228fa02fc0ef74adae24e6044a
|
| 3 |
+
size 23823
|
books_api_data/10005_一年级上册/audios/page_006_piece_03.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0d244ffdd82258c463048e5c7739d629f0d0d9e11f5fae8a39beb5121c25ee89
|
| 3 |
+
size 25077
|
books_api_data/10005_一年级上册/audios/page_006_piece_04.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4d01f72dc5ad26b83a1af2e69788fa51a164101a08b51fb0028cadd66c17b4d6
|
| 3 |
+
size 25495
|
books_api_data/10005_一年级上册/audios/page_006_piece_05.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3881dd3a52388c87a166100276b975c909821ea2ed5df9fde9ea0d317ff0ed61
|
| 3 |
+
size 24659
|
books_api_data/10005_一年级上册/audios/page_006_piece_06.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b8836ad3b13c0f167b3440948212a5ea33215989074275dd59542e07d3f37dd4
|
| 3 |
+
size 21733
|
books_api_data/10005_一年级上册/audios/page_007_piece_00.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:384454cc204eec379e8c53aba0eddcff8a4a727bd91cc9891077d0c886418698
|
| 3 |
+
size 53080
|
books_api_data/10005_一年级上册/audios/page_007_piece_01.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:faa75a6c25c33bc530d574534debc1cda01e8780276a8bda3df93f8ddfaa38bd
|
| 3 |
+
size 129567
|
books_api_data/10005_一年级上册/audios/page_007_piece_02.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9531cb7e9b1cc2a5758ee920ab7182d7a5be60e845e965c4821b34376e2a9288
|
| 3 |
+
size 133746
|
books_api_data/10005_一年级上册/audios/page_009_piece_00.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8a5b4467fbdb87518d42a31bad1ed7b4a3d747cdafaf2b79cbadd5d9312f1a10
|
| 3 |
+
size 47647
|
books_api_data/10005_一年级上册/audios/page_009_piece_01.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b0deea352ddba234530ed7ba0cbb9058fc800ca0fb40ded8d3999bcb44170255
|
| 3 |
+
size 22569
|
books_api_data/10005_一年级上册/audios/page_009_piece_02.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:97d098d3294bcceda1ef9425afa47945d7d38dbe1bfc5d0680fdd3addd8033db
|
| 3 |
+
size 20897
|
books_api_data/10005_一年级上册/audios/page_009_piece_03.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4c12fd9818d9f1923828e2a3dc839fb297b1f2120497f0d6f00900054384fdd9
|
| 3 |
+
size 25495
|
books_api_data/10005_一年级上册/audios/page_009_piece_04.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ccbb125b0451f64214a84f9e79d4b7cfc70d243c5822ecb57d447eba3fa7083e
|
| 3 |
+
size 27585
|
books_api_data/10005_一年级上册/audios/page_009_piece_05.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a755929366404882216e8f397a126e40cf3c9ca490afd65ac65ae907dd885528
|
| 3 |
+
size 28003
|
books_api_data/10005_一年级上册/audios/page_010_piece_00.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f7d47bf1be6e08099577644d3123d508064fc076b057c37f659db2c8d2e6b68d
|
| 3 |
+
size 83173
|
books_api_data/10005_一年级上册/audios/page_010_piece_01.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5b6519fab7096620a24834db703a5e8ecf43d3b857ced4e779c00952b77ec922
|
| 3 |
+
size 87353
|
books_api_data/10005_一年级上册/audios/page_011_piece_00.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:555ec8a313d6de63854fdb07c4e4d0a89e6ee6e96b99a483ef2fa9eafede9209
|
| 3 |
+
size 53498
|
books_api_data/10005_一年级上册/audios/page_011_piece_01.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:be324c440873e425aef730222704af1e3f681d9a025396df748e24f98e1aee4a
|
| 3 |
+
size 23405
|
books_api_data/10005_一年级上册/audios/page_011_piece_02.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ba6abf1c452b6dfba05484e2dde2422274cbfdac03d83dd016dd9ed6ec1f4d4a
|
| 3 |
+
size 23823
|
books_api_data/10005_一年级上册/audios/page_011_piece_03.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4538306b11fb20ea1b5e279fd4f71314051480e04d14ee608dd636c63d9bb3b3
|
| 3 |
+
size 32600
|
books_api_data/10005_一年级上册/audios/page_011_piece_04.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1813f326124a81b5a8a6dac0967fffac8f21b316597fd59a0ee9c577f5a967e4
|
| 3 |
+
size 29257
|
books_api_data/10005_一年级上册/audios/page_011_piece_05.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d40d3aabfb6296af236f622b39bcbbcb2d095d948c69f56882acec9780b97642
|
| 3 |
+
size 29257
|
books_api_data/10005_一年级上册/audios/page_011_piece_06.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0fb841e233e063d5fbfb36a27ff2270bc3d970da883fb3d5459f29f8ec4bce58
|
| 3 |
+
size 30093
|
books_api_data/10005_一年级上册/audios/page_011_piece_07.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1786b7c7835750b76eb8e707ba40a4483ebcc8c21a14e9a1b34ae1a32097ac7c
|
| 3 |
+
size 28839
|