eithney commited on
Commit
75d3623
·
1 Parent(s): 3794435

update static

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .vscode/PythonImportHelper-v2-Completion.json +407 -11
  2. API_DOCUMENTATION.md +414 -0
  3. BOOKS_API_DATA_文档.md +1232 -0
  4. CHANGELOG.md +212 -0
  5. QUICKSTART.md +234 -0
  6. README.md +307 -67
  7. app.py +663 -47
  8. books_api_data/10005_一年级上册/audios/page_002_piece_00.mp3 +3 -0
  9. books_api_data/10005_一年级上册/audios/page_003_piece_00.mp3 +3 -0
  10. books_api_data/10005_一年级上册/audios/page_004_piece_00.mp3 +3 -0
  11. books_api_data/10005_一年级上册/audios/page_004_piece_01.mp3 +3 -0
  12. books_api_data/10005_一年级上册/audios/page_004_piece_02.mp3 +3 -0
  13. books_api_data/10005_一年级上册/audios/page_004_piece_03.mp3 +3 -0
  14. books_api_data/10005_一年级上册/audios/page_004_piece_04.mp3 +3 -0
  15. books_api_data/10005_一年级上册/audios/page_004_piece_05.mp3 +3 -0
  16. books_api_data/10005_一年级上册/audios/page_004_piece_06.mp3 +3 -0
  17. books_api_data/10005_一年级上册/audios/page_004_piece_07.mp3 +3 -0
  18. books_api_data/10005_一年级上册/audios/page_004_piece_08.mp3 +3 -0
  19. books_api_data/10005_一年级上册/audios/page_004_piece_09.mp3 +3 -0
  20. books_api_data/10005_一年级上册/audios/page_005_piece_00.mp3 +3 -0
  21. books_api_data/10005_一年级上册/audios/page_005_piece_01.mp3 +3 -0
  22. books_api_data/10005_一年级上册/audios/page_005_piece_02.mp3 +3 -0
  23. books_api_data/10005_一年级上册/audios/page_005_piece_03.mp3 +3 -0
  24. books_api_data/10005_一年级上册/audios/page_005_piece_04.mp3 +3 -0
  25. books_api_data/10005_一年级上册/audios/page_006_piece_00.mp3 +3 -0
  26. books_api_data/10005_一年级上册/audios/page_006_piece_01.mp3 +3 -0
  27. books_api_data/10005_一年级上册/audios/page_006_piece_02.mp3 +3 -0
  28. books_api_data/10005_一年级上册/audios/page_006_piece_03.mp3 +3 -0
  29. books_api_data/10005_一年级上册/audios/page_006_piece_04.mp3 +3 -0
  30. books_api_data/10005_一年级上册/audios/page_006_piece_05.mp3 +3 -0
  31. books_api_data/10005_一年级上册/audios/page_006_piece_06.mp3 +3 -0
  32. books_api_data/10005_一年级上册/audios/page_007_piece_00.mp3 +3 -0
  33. books_api_data/10005_一年级上册/audios/page_007_piece_01.mp3 +3 -0
  34. books_api_data/10005_一年级上册/audios/page_007_piece_02.mp3 +3 -0
  35. books_api_data/10005_一年级上册/audios/page_009_piece_00.mp3 +3 -0
  36. books_api_data/10005_一年级上册/audios/page_009_piece_01.mp3 +3 -0
  37. books_api_data/10005_一年级上册/audios/page_009_piece_02.mp3 +3 -0
  38. books_api_data/10005_一年级上册/audios/page_009_piece_03.mp3 +3 -0
  39. books_api_data/10005_一年级上册/audios/page_009_piece_04.mp3 +3 -0
  40. books_api_data/10005_一年级上册/audios/page_009_piece_05.mp3 +3 -0
  41. books_api_data/10005_一年级上册/audios/page_010_piece_00.mp3 +3 -0
  42. books_api_data/10005_一年级上册/audios/page_010_piece_01.mp3 +3 -0
  43. books_api_data/10005_一年级上册/audios/page_011_piece_00.mp3 +3 -0
  44. books_api_data/10005_一年级上册/audios/page_011_piece_01.mp3 +3 -0
  45. books_api_data/10005_一年级上册/audios/page_011_piece_02.mp3 +3 -0
  46. books_api_data/10005_一年级上册/audios/page_011_piece_03.mp3 +3 -0
  47. books_api_data/10005_一年级上册/audios/page_011_piece_04.mp3 +3 -0
  48. books_api_data/10005_一年级上册/audios/page_011_piece_05.mp3 +3 -0
  49. books_api_data/10005_一年级上册/audios/page_011_piece_06.mp3 +3 -0
  50. 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 try:\n with open('book_10242.json', 'r', encoding='utf-8') as f:\n BOOK_DATA = json.load(f)\n app.logger.info(f' 成功加载书籍数据')\n return True\n except Exception as e:\n app.logger.error(f'❌ 加载书籍数据失败: {e}')",
293
  "detail": "app",
294
  "documentation": {}
295
  },
296
  {
297
- "label": "check_required_files",
298
  "kind": 2,
299
  "importPath": "app",
300
  "description": "app",
301
- "peekOfCode": "def check_required_files():\n \"\"\"检查必要文件是否存在\"\"\"\n required_files = ['index.html', 'style.css', 'script.js', 'book_10242.json']\n missing_files = []\n for file in required_files:\n if not os.path.exists(file):\n missing_files.append(file)\n if missing_files:\n app.logger.error(f\"缺少必要文件: {', '.join(missing_files)}\")\n return False",
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)\"\"\"\n return send_from_directory('.', filename)\n# ============================================================================\n# 路由���API 端点\n# ============================================================================",
 
 
 
 
 
 
 
 
 
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')\ndef health_check():\n \"\"\"健康检查端点\"\"\"\n return jsonify({",
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 BOOK_DATA:\n return jsonify({'error': '书籍数据未加载'}), 500\n # 返回籍元信息(不含具体页面内容)\n info = {\n 'total_pages': len(BOOK_DATA.get('pages', [])),\n 'title': BOOK_DATA.get('title', ''),\n 'loaded': True\n }",
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 BOOK_DATA:\n return jsonify({'error': '书籍数据未加载'}), 500\n pages = BOOK_DATA.get('pages', [])\n if page_num < 0 or page_num >= len(pages):\n return jsonify({'error': '页码超出范围'}), 404\n return jsonify({\n 'page_num': page_num,\n 'content': pages[page_num],",
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(\"🚀 ���互式英语学习应用 - Flask 版本\")\n print(\"=\" * 60)\n # 设置日志\n setup_logging()\n # 检查必要文件\n if not check_required_files():\n return False\n # 加载书籍数据",
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():\n \"\"\"\n 获取客户端真实 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 = [\n 'X-Forwarded-For',\n 'X-Real-IP',",
 
 
 
 
 
 
 
 
 
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
- title: 交互式英语学习应用
3
- emoji: 📚
4
- colorFrom: blue
5
- colorTo: green
6
- sdk: docker
7
- app_port: 7860
8
- pinned: false
9
- license: mit
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
- | `T` | 切换翻译显示 |
43
- | `I` | 显示/隐藏交互区域 |
44
- | `P` | 播放整页内容 |
45
- | `R` | 重复播放当前音频 |
46
- | `B` | 添加/删除书签 |
47
- | `F` | 打开搜索功 |
48
-
49
- ## 🎮 使用指南
50
-
51
- ### 基本操作
52
- 1. **页面切换**: 使用导航按钮或键盘箭头键
53
- 2. **点读学习**: 点击页面上的任意文片段
54
- 3. **翻译显示**: 点击翻译按钮或按T键切换
55
- 4. **音频控制**: 使用底部音频播放控制器
56
-
57
- ### 搜索功能
58
- 1. 点击搜索图标或按F键
59
- 2. 输入要搜索的英文或中文内容
60
- 3. 点击搜索结果直接跳转到对应页面
61
-
62
- ### 书签管理
63
- 1. 在想要标记的页面点击书签按钮或按B键
64
- 2. 点击书签图标查看所有书签
65
- 3. 点击书签项目快速跳转
66
- 4. 使删除按钮移除不需要的书签
67
-
68
- ## 🔧 技术栈
69
-
70
- - **前端**: HTML5, CSS3, Vanilla JavaScript
71
- - **字体**: FZKT_GBK.woff (方正楷体)
72
- - **数据**: JSON格式的学习内容
73
- - **音频**: Web Audio API
74
- - **存储**: localStorage
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
  ## 📄 许可证
77
 
78
- MIT License
 
 
 
 
 
 
 
 
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
- with open('book_10242.json', 'r', encoding='utf-8') as f:
101
- BOOK_DATA = json.load(f)
102
- app.logger.info(f'✅ 成功加载书籍数据')
103
  return True
104
  except Exception as e:
105
- app.logger.error(f'❌ 加载书籍数据失败: {e}')
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
- if not BOOK_DATA:
155
- return jsonify({'error': '书籍数据未加载'}), 500
156
-
157
- # 返回书籍元信息(不含具体页面内容)
158
- info = {
159
- 'total_pages': len(BOOK_DATA.get('pages', [])),
160
- 'title': BOOK_DATA.get('title', ''),
161
- 'loaded': True
162
- }
163
- return jsonify(info)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
  @app.route('/api/book/page/<int:page_num>')
166
  def get_page_content(page_num):
167
- """获取指定页面内容"""
168
- if not BOOK_DATA:
169
- return jsonify({'error': '书籍数据未加载'}), 500
170
-
171
- pages = BOOK_DATA.get('pages', [])
172
- if page_num < 0 or page_num >= len(pages):
173
- return jsonify({'error': '页码超出范围'}), 404
174
-
175
- return jsonify({
176
- 'page_num': page_num,
177
- 'content': pages[page_num],
178
- 'total_pages': len(pages)
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 check_required_files():
 
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