From e1f8885c6ceecb0fbfd339bf8c540bada8f8793a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=A0=91=E8=90=8C=E8=8A=BD?= <3205788256@qq.com>
Date: Tue, 2 Sep 2025 19:45:50 +0800
Subject: [PATCH 1/3] =?UTF-8?q?60sapi=E6=8E=A5=E5=8F=A3=E6=90=AD=E5=BB=BA?=
=?UTF-8?q?=E5=AE=8C=E6=AF=95=EF=BC=8C=E6=95=B0=E6=8D=AE=E5=BA=93=E8=BF=9E?=
=?UTF-8?q?=E6=8E=A5=E6=B5=8B=E8=AF=95=E6=88=90=E5=8A=9F=EF=BC=8C=E7=99=BB?=
=?UTF-8?q?=E5=BD=95=E6=B3=A8=E5=86=8C=E9=83=A8=E5=88=86=E7=AE=80=E5=8D=95?=
=?UTF-8?q?=E5=AE=8C=E6=88=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 2 +
.vscode/settings.json | 3 +
QQEmailSendAPI.py | 543 +
README.md | 100 +-
backend/app.py | 127 +
backend/config.py | 87 +
backend/md/邮件服务修复说明.md | 92 +
backend/modules/api_60s.py | 419 +
backend/modules/auth.py | 416 +
backend/modules/email_service.py | 276 +
backend/modules/user_management.py | 211 +
backend/requirements.txt | 26 +
backend/test/email_test.py | 34 +
backend/test/mongo_test.py | 49 +
backend/test/test_email.py | 35 +
backend/test/test_email_fix.py | 81 +
backend/test/test_mongo.py | 70 +
.../60sapi/热搜榜单/抖音热搜榜/js/script.js | 30 +-
frontend/60sapi/生成要求模板.txt | 2 +-
frontend/assets/App Logo 设计 (2).png | Bin 0 -> 1619286 bytes
frontend/assets/logo.png | Bin 0 -> 952335 bytes
frontend/react-app/package-lock.json | 20652 ++++++++++++++++
frontend/react-app/package.json | 49 +
.../随机JavaScript趣味题/css/background.css | 190 +
.../随机JavaScript趣味题/css/style.css | 597 +
.../娱乐消遣/随机JavaScript趣味题/index.html | 89 +
.../随机JavaScript趣味题/js/script.js | 565 +
.../随机JavaScript趣味题/接口集合.json | 7 +
.../随机JavaScript趣味题/返回接口.json | 17 +
.../娱乐消遣/随机KFC文案/css/background.css | 81 +
.../60sapi/娱乐消遣/随机KFC文案/css/style.css | 339 +
.../60sapi/娱乐消遣/随机KFC文案/index.html | 46 +
.../60sapi/娱乐消遣/随机KFC文案/js/main.js | 240 +
.../60sapi/娱乐消遣/随机KFC文案/接口集合.json | 7 +
.../60sapi/娱乐消遣/随机KFC文案/返回接口.json | 1 +
.../娱乐消遣/随机一言/css/background.css | 167 +
.../60sapi/娱乐消遣/随机一言/css/style.css | 357 +
.../60sapi/娱乐消遣/随机一言/index.html | 52 +
.../60sapi/娱乐消遣/随机一言/js/script.js | 225 +
.../60sapi/娱乐消遣/随机一言/接口集合.json | 7 +
.../60sapi/娱乐消遣/随机一言/返回接口.json | 8 +
.../娱乐消遣/随机唱歌音频/css/style.css | 251 +
.../60sapi/娱乐消遣/随机唱歌音频/index.html | 67 +
.../60sapi/娱乐消遣/随机唱歌音频/js/script.js | 252 +
.../娱乐消遣/随机唱歌音频/接口集合.json | 7 +
.../娱乐消遣/随机唱歌音频/返回接口.json | 32 +
.../实用功能/EpicGames免费游戏/css/style.css | 330 +
.../实用功能/EpicGames免费游戏/index.html | 63 +
.../实用功能/EpicGames免费游戏/js/script.js | 266 +
.../实用功能/EpicGames免费游戏/接口集合.json | 7 +
.../实用功能/EpicGames免费游戏/返回接口.json | 66 +
.../实用功能/农历信息/css/background.css | 89 +
.../60sapi/实用功能/农历信息/css/style.css | 1105 +
.../60sapi/实用功能/农历信息/index.html | 71 +
.../60sapi/实用功能/农历信息/js/script.js | 485 +
.../60sapi/实用功能/农历信息/接口集合.json | 7 +
.../60sapi/实用功能/农历信息/返回接口.json | 647 +
.../实用功能/实时天气/css/background.css | 145 +
.../60sapi/实用功能/实时天气/css/style.css | 409 +
.../60sapi/实用功能/实时天气/index.html | 140 +
.../60sapi/实用功能/实时天气/js/script.js | 252 +
.../60sapi/实用功能/实时天气/接口集合.json | 7 +
.../60sapi/实用功能/实时天气/返回接口.json | 68 +
.../实用功能/生成二维码/css/background.css | 132 +
.../60sapi/实用功能/生成二维码/css/style.css | 468 +
.../60sapi/实用功能/生成二维码/index.html | 98 +
.../60sapi/实用功能/生成二维码/js/script.js | 417 +
.../60sapi/实用功能/生成二维码/接口集合.json | 7 +
.../60sapi/实用功能/生成二维码/返回接口.json | 10 +
.../实用功能/百度百科词条/css/background.css | 192 +
.../实用功能/百度百科词条/css/style.css | 530 +
.../60sapi/实用功能/百度百科词条/index.html | 83 +
.../60sapi/实用功能/百度百科词条/js/script.js | 324 +
.../实用功能/百度百科词条/接口集合.json | 7 +
.../实用功能/百度百科词条/返回接口.json | 12 +
.../日更资讯/历史上的今天/css/style.css | 388 +
.../60sapi/日更资讯/历史上的今天/index.html | 83 +
.../60sapi/日更资讯/历史上的今天/js/script.js | 295 +
.../日更资讯/历史上的今天/接口集合.json | 7 +
.../日更资讯/历史上的今天/返回接口.json | 102 +
.../日更资讯/必应每日壁纸/css/style.css | 326 +
.../60sapi/日更资讯/必应每日壁纸/index.html | 42 +
.../60sapi/日更资讯/必应每日壁纸/js/script.js | 315 +
.../日更资讯/必应每日壁纸/接口集合.json | 7 +
.../日更资讯/必应每日壁纸/返回接口.json | 15 +
.../日更资讯/每天60s读懂世界/css/style.css | 327 +
.../日更资讯/每天60s读懂世界/index.html | 49 +
.../日更资讯/每天60s读懂世界/js/script.js | 305 +
.../日更资讯/每天60s读懂世界/接口集合.json | 7 +
.../日更资讯/每天60s读懂世界/返回接口.json | 66 +
.../日更资讯/每日国际汇率/css/style.css | 409 +
.../60sapi/日更资讯/每日国际汇率/index.html | 86 +
.../60sapi/日更资讯/每日国际汇率/js/script.js | 520 +
.../日更资讯/每日国际汇率/接口集合.json | 7 +
.../日更资讯/每日国际汇率/返回接口.json | 1 +
.../HackerNews榜单/css/background.css | 106 +
.../热搜榜单/HackerNews榜单/css/style.css | 1037 +
.../60sapi/热搜榜单/HackerNews榜单/index.html | 77 +
.../热搜榜单/HackerNews榜单/js/script.js | 338 +
.../热搜榜单/HackerNews榜单/接口集合.json | 7 +
.../热搜榜单/HackerNews榜单/返回接口.json | 87 +
.../热搜榜单/微博热搜榜/css/background.css | 40 +
.../60sapi/热搜榜单/微博热搜榜/css/style.css | 155 +
.../60sapi/热搜榜单/微博热搜榜/index.html | 34 +
.../60sapi/热搜榜单/微博热搜榜/js/main.js | 94 +
.../60sapi/热搜榜单/微博热搜榜/接口集合.json | 7 +
.../60sapi/热搜榜单/微博热搜榜/返回接口.json | 261 +
.../热搜榜单/抖音热搜榜/css/background.css | 52 +
.../60sapi/热搜榜单/抖音热搜榜/css/style.css | 956 +
.../60sapi/热搜榜单/抖音热搜榜/index.html | 60 +
.../60sapi/热搜榜单/抖音热搜榜/js/script.js | 300 +
.../60sapi/热搜榜单/抖音热搜榜/接口集合.json | 7 +
.../60sapi/热搜榜单/抖音热搜榜/返回接口.json | 496 +
.../热搜榜单/猫眼票房排行榜/css/style.css | 495 +
.../60sapi/热搜榜单/猫眼票房排行榜/index.html | 40 +
.../热搜榜单/猫眼票房排行榜/js/script.js | 249 +
.../热搜榜单/猫眼票房排行榜/接口集合.json | 7 +
.../热搜榜单/猫眼票房排行榜/返回接口.json | 171 +
.../热搜榜单/网易云榜单列表/接口集合.json | 7 +
.../热搜榜单/网易云榜单列表/返回接口.json | 750 +
.../网易云榜单详情/css/background.css | 123 +
.../热搜榜单/网易云榜单详情/css/style.css | 483 +
.../60sapi/热搜榜单/网易云榜单详情/index.html | 69 +
.../热搜榜单/网易云榜单详情/js/script.js | 349 +
.../热搜榜单/网易云榜单详情/接口集合.json | 7 +
.../热搜榜单/网易云榜单详情/返回接口.json | 5612 +++++
.../react-app/public/60sapi/生成要求模板.txt | 8 +
frontend/react-app/public/index.html | 144 +
frontend/react-app/public/manifest.json | 38 +
frontend/react-app/src/App.js | 88 +
frontend/react-app/src/components/Footer.js | 115 +
frontend/react-app/src/components/Header.js | 349 +
.../react-app/src/components/Navigation.js | 126 +
.../react-app/src/contexts/UserContext.js | 118 +
frontend/react-app/src/index.js | 11 +
.../react-app/src/md/前端邮件功能测试指南.md | 118 +
frontend/react-app/src/pages/AiModelPage.js | 287 +
frontend/react-app/src/pages/Api60sPage.js | 427 +
frontend/react-app/src/pages/HomePage.js | 278 +
frontend/react-app/src/pages/LoginPage.js | 593 +
frontend/react-app/src/pages/SmallGamePage.js | 183 +
frontend/react-app/src/styles/global.css | 332 +
frontend/react-app/src/styles/index.css | 219 +
frontend/react-app/src/utils/api.js | 106 +
frontend/react-app/src/utils/helpers.js | 310 +
frontend/setting.json | 8 +
start_backend.bat | 3 +
start_frontend.bat | 3 +
用户数据模板.json | 3 +-
项目架构说明.txt | 8 +-
150 files changed, 53045 insertions(+), 8 deletions(-)
create mode 100644 .vscode/settings.json
create mode 100644 QQEmailSendAPI.py
create mode 100644 backend/app.py
create mode 100644 backend/config.py
create mode 100644 backend/md/邮件服务修复说明.md
create mode 100644 backend/modules/api_60s.py
create mode 100644 backend/modules/auth.py
create mode 100644 backend/modules/email_service.py
create mode 100644 backend/modules/user_management.py
create mode 100644 backend/requirements.txt
create mode 100644 backend/test/email_test.py
create mode 100644 backend/test/mongo_test.py
create mode 100644 backend/test/test_email.py
create mode 100644 backend/test/test_email_fix.py
create mode 100644 backend/test/test_mongo.py
create mode 100644 frontend/assets/App Logo 设计 (2).png
create mode 100644 frontend/assets/logo.png
create mode 100644 frontend/react-app/package-lock.json
create mode 100644 frontend/react-app/package.json
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机JavaScript趣味题/css/background.css
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机JavaScript趣味题/css/style.css
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机JavaScript趣味题/index.html
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机JavaScript趣味题/js/script.js
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机JavaScript趣味题/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机JavaScript趣味题/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机KFC文案/css/background.css
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机KFC文案/css/style.css
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机KFC文案/index.html
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机KFC文案/js/main.js
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机KFC文案/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机KFC文案/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机一言/css/background.css
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机一言/css/style.css
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机一言/index.html
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机一言/js/script.js
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机一言/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机一言/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机唱歌音频/css/style.css
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机唱歌音频/index.html
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机唱歌音频/js/script.js
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机唱歌音频/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/娱乐消遣/随机唱歌音频/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/实用功能/EpicGames免费游戏/css/style.css
create mode 100644 frontend/react-app/public/60sapi/实用功能/EpicGames免费游戏/index.html
create mode 100644 frontend/react-app/public/60sapi/实用功能/EpicGames免费游戏/js/script.js
create mode 100644 frontend/react-app/public/60sapi/实用功能/EpicGames免费游戏/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/实用功能/EpicGames免费游戏/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/实用功能/农历信息/css/background.css
create mode 100644 frontend/react-app/public/60sapi/实用功能/农历信息/css/style.css
create mode 100644 frontend/react-app/public/60sapi/实用功能/农历信息/index.html
create mode 100644 frontend/react-app/public/60sapi/实用功能/农历信息/js/script.js
create mode 100644 frontend/react-app/public/60sapi/实用功能/农历信息/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/实用功能/农历信息/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/实用功能/实时天气/css/background.css
create mode 100644 frontend/react-app/public/60sapi/实用功能/实时天气/css/style.css
create mode 100644 frontend/react-app/public/60sapi/实用功能/实时天气/index.html
create mode 100644 frontend/react-app/public/60sapi/实用功能/实时天气/js/script.js
create mode 100644 frontend/react-app/public/60sapi/实用功能/实时天气/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/实用功能/实时天气/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/实用功能/生成二维码/css/background.css
create mode 100644 frontend/react-app/public/60sapi/实用功能/生成二维码/css/style.css
create mode 100644 frontend/react-app/public/60sapi/实用功能/生成二维码/index.html
create mode 100644 frontend/react-app/public/60sapi/实用功能/生成二维码/js/script.js
create mode 100644 frontend/react-app/public/60sapi/实用功能/生成二维码/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/实用功能/生成二维码/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/实用功能/百度百科词条/css/background.css
create mode 100644 frontend/react-app/public/60sapi/实用功能/百度百科词条/css/style.css
create mode 100644 frontend/react-app/public/60sapi/实用功能/百度百科词条/index.html
create mode 100644 frontend/react-app/public/60sapi/实用功能/百度百科词条/js/script.js
create mode 100644 frontend/react-app/public/60sapi/实用功能/百度百科词条/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/实用功能/百度百科词条/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/日更资讯/历史上的今天/css/style.css
create mode 100644 frontend/react-app/public/60sapi/日更资讯/历史上的今天/index.html
create mode 100644 frontend/react-app/public/60sapi/日更资讯/历史上的今天/js/script.js
create mode 100644 frontend/react-app/public/60sapi/日更资讯/历史上的今天/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/日更资讯/历史上的今天/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/日更资讯/必应每日壁纸/css/style.css
create mode 100644 frontend/react-app/public/60sapi/日更资讯/必应每日壁纸/index.html
create mode 100644 frontend/react-app/public/60sapi/日更资讯/必应每日壁纸/js/script.js
create mode 100644 frontend/react-app/public/60sapi/日更资讯/必应每日壁纸/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/日更资讯/必应每日壁纸/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/日更资讯/每天60s读懂世界/css/style.css
create mode 100644 frontend/react-app/public/60sapi/日更资讯/每天60s读懂世界/index.html
create mode 100644 frontend/react-app/public/60sapi/日更资讯/每天60s读懂世界/js/script.js
create mode 100644 frontend/react-app/public/60sapi/日更资讯/每天60s读懂世界/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/日更资讯/每天60s读懂世界/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/日更资讯/每日国际汇率/css/style.css
create mode 100644 frontend/react-app/public/60sapi/日更资讯/每日国际汇率/index.html
create mode 100644 frontend/react-app/public/60sapi/日更资讯/每日国际汇率/js/script.js
create mode 100644 frontend/react-app/public/60sapi/日更资讯/每日国际汇率/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/日更资讯/每日国际汇率/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/HackerNews榜单/css/background.css
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/HackerNews榜单/css/style.css
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/HackerNews榜单/index.html
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/HackerNews榜单/js/script.js
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/HackerNews榜单/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/HackerNews榜单/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/微博热搜榜/css/background.css
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/微博热搜榜/css/style.css
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/微博热搜榜/index.html
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/微博热搜榜/js/main.js
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/微博热搜榜/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/微博热搜榜/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/抖音热搜榜/css/background.css
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/抖音热搜榜/css/style.css
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/抖音热搜榜/index.html
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/抖音热搜榜/js/script.js
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/抖音热搜榜/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/抖音热搜榜/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/猫眼票房排行榜/css/style.css
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/猫眼票房排行榜/index.html
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/猫眼票房排行榜/js/script.js
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/猫眼票房排行榜/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/猫眼票房排行榜/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/网易云榜单列表/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/网易云榜单列表/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/网易云榜单详情/css/background.css
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/网易云榜单详情/css/style.css
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/网易云榜单详情/index.html
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/网易云榜单详情/js/script.js
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/网易云榜单详情/接口集合.json
create mode 100644 frontend/react-app/public/60sapi/热搜榜单/网易云榜单详情/返回接口.json
create mode 100644 frontend/react-app/public/60sapi/生成要求模板.txt
create mode 100644 frontend/react-app/public/index.html
create mode 100644 frontend/react-app/public/manifest.json
create mode 100644 frontend/react-app/src/App.js
create mode 100644 frontend/react-app/src/components/Footer.js
create mode 100644 frontend/react-app/src/components/Header.js
create mode 100644 frontend/react-app/src/components/Navigation.js
create mode 100644 frontend/react-app/src/contexts/UserContext.js
create mode 100644 frontend/react-app/src/index.js
create mode 100644 frontend/react-app/src/md/前端邮件功能测试指南.md
create mode 100644 frontend/react-app/src/pages/AiModelPage.js
create mode 100644 frontend/react-app/src/pages/Api60sPage.js
create mode 100644 frontend/react-app/src/pages/HomePage.js
create mode 100644 frontend/react-app/src/pages/LoginPage.js
create mode 100644 frontend/react-app/src/pages/SmallGamePage.js
create mode 100644 frontend/react-app/src/styles/global.css
create mode 100644 frontend/react-app/src/styles/index.css
create mode 100644 frontend/react-app/src/utils/api.js
create mode 100644 frontend/react-app/src/utils/helpers.js
create mode 100644 frontend/setting.json
create mode 100644 start_backend.bat
create mode 100644 start_frontend.bat
diff --git a/.gitignore b/.gitignore
index b7faf403..08f22f72 100644
--- a/.gitignore
+++ b/.gitignore
@@ -205,3 +205,5 @@ cython_debug/
marimo/_static/
marimo/_lsp/
__marimo__/
+
+frontend/react-app/node_modules/
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..3b664107
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "git.ignoreLimitWarning": true
+}
\ No newline at end of file
diff --git a/QQEmailSendAPI.py b/QQEmailSendAPI.py
new file mode 100644
index 00000000..0da7ae92
--- /dev/null
+++ b/QQEmailSendAPI.py
@@ -0,0 +1,543 @@
+import smtplib
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+from email.mime.application import MIMEApplication
+from email.header import Header
+import random
+import string
+import json
+import os
+
+# 邮件发送配置
+SENDER_EMAIL = '3205788256@qq.com' # 发件人邮箱
+SENDER_AUTH_CODE = 'szcaxvbftusqddhi' # 授权码
+SMTP_SERVER = 'smtp.qq.com' # QQ邮箱SMTP服务器
+SMTP_PORT = 465 # QQ邮箱SSL端口
+
+# 验证码缓存文件
+VERIFICATION_CACHE_FILE = os.path.join("config", "verification_codes.json")
+
+class QQMailAPI:
+ """QQ邮箱发送邮件API类"""
+
+ def __init__(self, sender_email, authorization_code):
+ """
+ 初始化邮箱配置
+ :param sender_email: 发送方QQ邮箱地址
+ :param authorization_code: QQ邮箱授权码
+ """
+ self.sender_email = sender_email
+ self.authorization_code = authorization_code
+ self.smtp_server = 'smtp.qq.com'
+ self.smtp_port = 465 # SSL端口
+
+ # 发送纯文本邮件
+ def send_text_email(self, receiver_email, subject, content, cc_emails=None):
+ """
+ 发送纯文本邮件
+ :param receiver_email: 接收方邮箱地址(单个)
+ :param subject: 邮件主题
+ :param content: 邮件正文内容
+ :param cc_emails: 抄送邮箱列表
+ :return: 发送成功返回True,失败返回False
+ """
+ try:
+ # 创建邮件对象
+ message = MIMEText(content, 'plain', 'utf-8')
+ message['From'] = Header(self.sender_email, 'utf-8')
+ message['To'] = Header(receiver_email, 'utf-8')
+ message['Subject'] = Header(subject, 'utf-8')
+
+ # 添加抄送
+ if cc_emails:
+ message['Cc'] = Header(",".join(cc_emails), 'utf-8')
+ all_receivers = [receiver_email] + cc_emails
+ else:
+ all_receivers = [receiver_email]
+
+ # 连接SMTP服务器并发送邮件
+ with smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) as server:
+ server.login(self.sender_email, self.authorization_code)
+ server.sendmail(self.sender_email, all_receivers, message.as_string())
+
+ print(f"邮件发送成功:主题='{subject}', 收件人='{receiver_email}'")
+ return True
+ except Exception as e:
+ print(f"邮件发送失败:{str(e)}")
+ return False
+
+ # 发送HTML格式邮件,可带附件
+ def send_html_email(self, receiver_email, subject, html_content, cc_emails=None, attachments=None):
+ """
+ 发送HTML格式邮件,可带附件
+ :param receiver_email: 接收方邮箱地址(单个)
+ :param subject: 邮件主题
+ :param html_content: HTML格式的邮件正文
+ :param cc_emails: 抄送邮箱列表
+ :param attachments: 附件文件路径列表
+ :return: 发送成功返回True,失败返回False
+ """
+ try:
+ # 创建带附件的邮件对象
+ message = MIMEMultipart()
+ message['From'] = Header(self.sender_email, 'utf-8')
+ message['To'] = Header(receiver_email, 'utf-8')
+ message['Subject'] = Header(subject, 'utf-8')
+
+ # 添加抄送
+ if cc_emails:
+ message['Cc'] = Header(",".join(cc_emails), 'utf-8')
+ all_receivers = [receiver_email] + cc_emails
+ else:
+ all_receivers = [receiver_email]
+
+ # 添加HTML正文
+ message.attach(MIMEText(html_content, 'html', 'utf-8'))
+
+ # 添加附件
+ if attachments:
+ for file_path in attachments:
+ try:
+ with open(file_path, 'rb') as file:
+ attachment = MIMEApplication(file.read(), _subtype="octet-stream")
+ attachment.add_header('Content-Disposition', 'attachment', filename=file_path.split("/")[-1])
+ message.attach(attachment)
+ except Exception as e:
+ print(f"添加附件失败 {file_path}: {str(e)}")
+
+ # 连接SMTP服务器并发送邮件
+ with smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) as server:
+ server.login(self.sender_email, self.authorization_code)
+ server.sendmail(self.sender_email, all_receivers, message.as_string())
+
+ print(f"HTML邮件发送成功:主题='{subject}', 收件人='{receiver_email}'")
+ return True
+ except Exception as e:
+ print(f"HTML邮件发送失败:{str(e)}")
+ return False
+
+class EmailVerification:
+
+ #生成指定长度的随机验证码
+ @staticmethod
+ def generate_verification_code(length=6):
+ """
+ 生成指定长度的随机验证码
+
+ 参数:
+ length (int): 验证码长度,默认6位
+
+ 返回:
+ str: 生成的验证码
+ """
+ # 生成包含大写字母和数字的验证码
+ chars = string.ascii_uppercase + string.digits
+ return ''.join(random.choice(chars) for _ in range(length))
+
+ #发送验证码邮件到QQ邮箱
+ @staticmethod
+ def send_verification_email(qq_number, verification_code, email_type="register"):
+ """
+ 发送验证码邮件到QQ邮箱
+
+ 参数:
+ qq_number (str): 接收者QQ号
+ verification_code (str): 验证码
+ email_type (str): 邮件类型,"register" 或 "reset_password"
+
+ 返回:
+ bool: 发送成功返回True,否则返回False
+ str: 成功或错误信息
+ """
+ receiver_email = f"{qq_number}@qq.com"
+
+ # 根据邮件类型设置不同的内容
+ if email_type == "reset_password":
+ email_title = "【萌芽农场】密码重置验证码"
+ email_purpose = "重置萌芽农场游戏账号密码"
+ email_color = "#FF6B35" # 橙红色,表示警告性操作
+ else:
+ email_title = "【萌芽农场】注册验证码"
+ email_purpose = "注册萌芽农场游戏账号"
+ email_color = "#4CAF50" # 绿色,表示正常操作
+
+ # 创建邮件内容
+ message = MIMEText(f'''
+
+
+
+
萌芽农场 - 邮箱验证码
+
亲爱的玩家,您好!
+
您正在{email_purpose},您的验证码是:
+
+ {verification_code}
+
+
该验证码有效期为5分钟,请勿泄露给他人。
+
如果这不是您本人的操作,请忽略此邮件。
+
+ 本邮件由系统自动发送,请勿直接回复。
+
+
+
+
+ ''', 'html', 'utf-8')
+
+ # 修正From头格式,符合QQ邮箱的要求
+ message['From'] = SENDER_EMAIL
+ message['To'] = receiver_email
+ message['Subject'] = Header(email_title, 'utf-8')
+
+ try:
+ # 使用SSL/TLS连接而不是STARTTLS
+ smtp_obj = smtplib.SMTP_SSL(SMTP_SERVER, 465)
+ smtp_obj.login(SENDER_EMAIL, SENDER_AUTH_CODE)
+ smtp_obj.sendmail(SENDER_EMAIL, [receiver_email], message.as_string())
+ smtp_obj.quit()
+ return True, "验证码发送成功"
+ except Exception as e:
+ return False, f"发送验证码失败: {str(e)}"
+
+ #保存验证码到MongoDB(优先)或缓存文件(备用)
+ @staticmethod
+ def save_verification_code(qq_number, verification_code, expiry_time=300, code_type="register"):
+ """
+ 保存验证码到MongoDB(优先)或缓存文件(备用)
+
+ 参数:
+ qq_number (str): QQ号
+ verification_code (str): 验证码
+ expiry_time (int): 过期时间(秒),默认5分钟
+ code_type (str): 验证码类型,"register" 或 "reset_password"
+
+ 返回:
+ bool: 保存成功返回True,否则返回False
+ """
+ import time
+
+ # 优先尝试使用MongoDB
+ try:
+ from SMYMongoDBAPI import SMYMongoDBAPI
+ import os
+ # 根据环境动态选择MongoDB配置
+ if os.path.exists('/.dockerenv') or os.environ.get('PRODUCTION', '').lower() == 'true':
+ environment = "production"
+ else:
+ environment = "test"
+ mongo_api = SMYMongoDBAPI(environment)
+ if mongo_api.is_connected():
+ success = mongo_api.save_verification_code(qq_number, verification_code, expiry_time, code_type)
+ if success:
+ print(f"[验证码系统-MongoDB] 为QQ {qq_number} 保存{code_type}验证码: {verification_code}")
+ return True
+ else:
+ print(f"[验证码系统-MongoDB] 保存失败,尝试使用JSON文件")
+ except Exception as e:
+ print(f"[验证码系统-MongoDB] MongoDB保存失败: {str(e)},尝试使用JSON文件")
+
+ # MongoDB失败,使用JSON文件备用
+ # 创建目录(如果不存在)
+ os.makedirs(os.path.dirname(VERIFICATION_CACHE_FILE), exist_ok=True)
+
+ # 读取现有的验证码数据
+ verification_data = {}
+ if os.path.exists(VERIFICATION_CACHE_FILE):
+ try:
+ with open(VERIFICATION_CACHE_FILE, 'r', encoding='utf-8') as file:
+ verification_data = json.load(file)
+ except Exception as e:
+ print(f"读取验证码文件失败: {str(e)}")
+ verification_data = {}
+
+ # 添加新的验证码
+ expire_at = time.time() + expiry_time
+ current_time = time.time()
+
+ # 创建验证码记录,包含更多信息用于调试
+ verification_data[qq_number] = {
+ "code": verification_code,
+ "expire_at": expire_at,
+ "code_type": code_type,
+ "created_at": current_time,
+ "used": False # 新增:标记验证码是否已使用
+ }
+
+ # 保存到文件
+ try:
+ with open(VERIFICATION_CACHE_FILE, 'w', encoding='utf-8') as file:
+ json.dump(verification_data, file, indent=2, ensure_ascii=False)
+
+ print(f"[验证码系统-JSON] 为QQ {qq_number} 保存{code_type}验证码: {verification_code}, 过期时间: {expire_at}")
+ return True
+ except Exception as e:
+ print(f"保存验证码失败: {str(e)}")
+ return False
+
+ #验证用户输入的验证码(优先使用MongoDB)
+ @staticmethod
+ def verify_code(qq_number, input_code, code_type="register"):
+ """
+ 验证用户输入的验证码(优先使用MongoDB)
+
+ 参数:
+ qq_number (str): QQ号
+ input_code (str): 用户输入的验证码
+ code_type (str): 验证码类型,"register" 或 "reset_password"
+
+ 返回:
+ bool: 验证成功返回True,否则返回False
+ str: 成功或错误信息
+ """
+ import time
+
+ # 优先尝试使用MongoDB
+ try:
+ from SMYMongoDBAPI import SMYMongoDBAPI
+ import os
+ # 根据环境动态选择MongoDB配置
+ if os.path.exists('/.dockerenv') or os.environ.get('PRODUCTION', '').lower() == 'true':
+ environment = "production"
+ else:
+ environment = "test"
+ mongo_api = SMYMongoDBAPI(environment)
+ if mongo_api.is_connected():
+ success, message = mongo_api.verify_verification_code(qq_number, input_code, code_type)
+ print(f"[验证码系统-MongoDB] QQ {qq_number} 验证结果: {success}, 消息: {message}")
+ return success, message
+ except Exception as e:
+ print(f"[验证码系统-MongoDB] MongoDB验证失败: {str(e)},尝试使用JSON文件")
+
+ # MongoDB失败,使用JSON文件备用
+ # 检查缓存文件是否存在
+ if not os.path.exists(VERIFICATION_CACHE_FILE):
+ print(f"[验证码系统-JSON] QQ {qq_number} 验证失败: 缓存文件不存在")
+ return False, "验证码不存在或已过期"
+
+ # 读取验证码数据
+ try:
+ with open(VERIFICATION_CACHE_FILE, 'r', encoding='utf-8') as file:
+ verification_data = json.load(file)
+ except Exception as e:
+ print(f"[验证码系统-JSON] 读取验证码文件失败: {str(e)}")
+ return False, "验证码数据损坏"
+
+ # 检查该QQ号是否有验证码
+ if qq_number not in verification_data:
+ print(f"[验证码系统-JSON] QQ {qq_number} 验证失败: 没有找到验证码记录")
+ return False, "验证码不存在,请重新获取"
+
+ # 获取存储的验证码信息
+ code_info = verification_data[qq_number]
+ stored_code = code_info.get("code", "")
+ expire_at = code_info.get("expire_at", 0)
+ stored_code_type = code_info.get("code_type", "register")
+ is_used = code_info.get("used", False)
+ created_at = code_info.get("created_at", 0)
+
+ print(f"[验证码系统-JSON] QQ {qq_number} 验证码详情: 存储码={stored_code}, 输入码={input_code}, 类型={stored_code_type}, 已使用={is_used}, 创建时间={created_at}")
+
+ # 检查验证码类型是否匹配
+ if stored_code_type != code_type:
+ print(f"[验证码系统-JSON] QQ {qq_number} 验证失败: 验证码类型不匹配,存储类型={stored_code_type}, 请求类型={code_type}")
+ return False, f"验证码类型不匹配,请重新获取{code_type}验证码"
+
+ # 检查验证码是否已被使用
+ if is_used:
+ print(f"[验证码系统-JSON] QQ {qq_number} 验证失败: 验证码已被使用")
+ return False, "验证码已被使用,请重新获取"
+
+ # 检查验证码是否过期
+ current_time = time.time()
+ if current_time > expire_at:
+ # 移除过期的验证码
+ del verification_data[qq_number]
+ with open(VERIFICATION_CACHE_FILE, 'w', encoding='utf-8') as file:
+ json.dump(verification_data, file, indent=2, ensure_ascii=False)
+ print(f"[验证码系统-JSON] QQ {qq_number} 验证失败: 验证码已过期")
+ return False, "验证码已过期,请重新获取"
+
+ # 验证码比较(不区分大小写)
+ if input_code.upper() == stored_code.upper():
+ # 验证成功,标记为已使用而不是删除
+ verification_data[qq_number]["used"] = True
+ verification_data[qq_number]["used_at"] = current_time
+
+ try:
+ with open(VERIFICATION_CACHE_FILE, 'w', encoding='utf-8') as file:
+ json.dump(verification_data, file, indent=2, ensure_ascii=False)
+ print(f"[验证码系统-JSON] QQ {qq_number} 验证成功: 验证码已标记为已使用")
+ return True, "验证码正确"
+ except Exception as e:
+ print(f"[验证码系统-JSON] 标记验证码已使用时失败: {str(e)}")
+ return True, "验证码正确" # 即使标记失败,验证还是成功的
+ else:
+ print(f"[验证码系统-JSON] QQ {qq_number} 验证失败: 验证码不匹配")
+ return False, "验证码错误"
+
+ #清理过期的验证码和已使用的验证码(优先使用MongoDB)
+ @staticmethod
+ def clean_expired_codes():
+ """
+ 清理过期的验证码和已使用的验证码(优先使用MongoDB)
+ """
+ import time
+
+ # 优先尝试使用MongoDB
+ try:
+ from SMYMongoDBAPI import SMYMongoDBAPI
+ import os
+ # 根据环境动态选择MongoDB配置
+ if os.path.exists('/.dockerenv') or os.environ.get('PRODUCTION', '').lower() == 'true':
+ environment = "production"
+ else:
+ environment = "test"
+ mongo_api = SMYMongoDBAPI(environment)
+ if mongo_api.is_connected():
+ expired_count = mongo_api.clean_expired_verification_codes()
+ print(f"[验证码系统-MongoDB] 清理完成,删除了 {expired_count} 个过期验证码")
+ return expired_count
+ except Exception as e:
+ print(f"[验证码系统-MongoDB] MongoDB清理失败: {str(e)},尝试使用JSON文件")
+
+ # MongoDB失败,使用JSON文件备用
+ if not os.path.exists(VERIFICATION_CACHE_FILE):
+ return
+
+ try:
+ with open(VERIFICATION_CACHE_FILE, 'r', encoding='utf-8') as file:
+ verification_data = json.load(file)
+
+ current_time = time.time()
+ removed_keys = []
+
+ # 找出过期的验证码和已使用的验证码(超过1小时)
+ for qq_number, code_info in verification_data.items():
+ expire_at = code_info.get("expire_at", 0)
+ is_used = code_info.get("used", False)
+ used_at = code_info.get("used_at", 0)
+
+ should_remove = False
+
+ # 过期的验证码
+ if current_time > expire_at:
+ should_remove = True
+ print(f"[验证码清理-JSON] 移除过期验证码: QQ {qq_number}")
+
+ # 已使用超过1小时的验证码
+ elif is_used and used_at > 0 and (current_time - used_at) > 3600:
+ should_remove = True
+ print(f"[验证码清理-JSON] 移除已使用的验证码: QQ {qq_number}")
+
+ if should_remove:
+ removed_keys.append(qq_number)
+
+ # 移除标记的验证码
+ for key in removed_keys:
+ del verification_data[key]
+
+ # 保存更新后的数据
+ if removed_keys:
+ with open(VERIFICATION_CACHE_FILE, 'w', encoding='utf-8') as file:
+ json.dump(verification_data, file, indent=2, ensure_ascii=False)
+ print(f"[验证码清理-JSON] 共清理了 {len(removed_keys)} 个验证码")
+
+ except Exception as e:
+ print(f"清理验证码失败: {str(e)}")
+
+ #获取验证码状态(优先使用MongoDB)
+ @staticmethod
+ def get_verification_status(qq_number):
+ """
+ 获取验证码状态(优先使用MongoDB)
+
+ 参数:
+ qq_number (str): QQ号
+
+ 返回:
+ dict: 验证码状态信息
+ """
+ import time
+
+ # 优先尝试使用MongoDB
+ try:
+ from SMYMongoDBAPI import SMYMongoDBAPI
+ import os
+ # 根据环境动态选择MongoDB配置
+ if os.path.exists('/.dockerenv') or os.environ.get('PRODUCTION', '').lower() == 'true':
+ environment = "production"
+ else:
+ environment = "test"
+ mongo_api = SMYMongoDBAPI(environment)
+ if mongo_api.is_connected():
+ verification_codes = mongo_api.get_verification_codes()
+ if verification_codes and qq_number in verification_codes:
+ code_info = verification_codes[qq_number]
+ current_time = time.time()
+
+ return {
+ "status": "found",
+ "code": code_info.get("code", ""),
+ "code_type": code_info.get("code_type", "unknown"),
+ "used": code_info.get("used", False),
+ "expired": current_time > code_info.get("expire_at", 0),
+ "created_at": code_info.get("created_at", 0),
+ "expire_at": code_info.get("expire_at", 0),
+ "used_at": code_info.get("used_at", 0),
+ "source": "mongodb"
+ }
+ else:
+ return {"status": "no_code", "source": "mongodb"}
+ except Exception as e:
+ print(f"[验证码系统-MongoDB] MongoDB状态查询失败: {str(e)},尝试使用JSON文件")
+
+ # MongoDB失败,使用JSON文件备用
+ if not os.path.exists(VERIFICATION_CACHE_FILE):
+ return {"status": "no_cache_file"}
+
+ try:
+ with open(VERIFICATION_CACHE_FILE, 'r', encoding='utf-8') as file:
+ verification_data = json.load(file)
+
+ if qq_number not in verification_data:
+ return {"status": "no_code"}
+
+ code_info = verification_data[qq_number]
+ current_time = time.time()
+
+ return {
+ "status": "found",
+ "code": code_info.get("code", ""),
+ "code_type": code_info.get("code_type", "unknown"),
+ "used": code_info.get("used", False),
+ "expired": current_time > code_info.get("expire_at", 0),
+ "created_at": code_info.get("created_at", 0),
+ "expire_at": code_info.get("expire_at", 0),
+ "used_at": code_info.get("used_at", 0),
+ "source": "json"
+ }
+
+ except Exception as e:
+ return {"status": "error", "message": str(e)}
+
+
+# 测试邮件发送
+if __name__ == "__main__":
+ # 清理过期验证码
+ EmailVerification.clean_expired_codes()
+
+ # 生成验证码
+ test_qq = input("请输入测试QQ号: ")
+ verification_code = EmailVerification.generate_verification_code()
+ print(f"生成的验证码: {verification_code}")
+
+ # 发送测试邮件
+ success, message = EmailVerification.send_verification_email(test_qq, verification_code)
+ print(f"发送结果: {success}, 消息: {message}")
+
+ if success:
+ # 保存验证码
+ EmailVerification.save_verification_code(test_qq, verification_code)
+
+ # 测试验证
+ test_input = input("请输入收到的验证码: ")
+ verify_success, verify_message = EmailVerification.verify_code(test_qq, test_input)
+ print(f"验证结果: {verify_success}, 消息: {verify_message}")
\ No newline at end of file
diff --git a/README.md b/README.md
index ed4dad6e..169baa46 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,102 @@
-# InfoGenie
+# ✨ InfoGenie 神奇万事通
+
+> 🎨 一个多功能的聚合软件应用 💬
+
+## 📋 项目概述
+
+InfoGenie 是一个前后端分离的多功能聚合应用,提供实时数据接口、休闲游戏、AI工具等丰富功能。
+
+### 🏗️ 技术架构
+
+- **前端**: React + Styled Components + React Router
+- **后端**: Python Flask + MongoDB + PyMongo
+- **架构**: 前后端分离,RESTful API
+- **部署**: 支持Docker容器化部署
+
+### 🌟 主要功能
+
+#### 📡 60s API 模块
+- **热搜榜单**: 抖音、微博、猫眼票房、HackerNews等
+- **日更资讯**: 60秒读懂世界、必应壁纸、历史今天、汇率信息
+- **实用功能**: 天气查询、百科搜索、农历信息、二维码生成
+- **娱乐消遣**: 随机一言、音频、趣味题、文案生成
+
+#### 🎮 小游戏模块
+- 经典游戏合集(开发中)
+- 移动端优化
+- 即点即玩
+
+#### 🤖 AI模型模块
+- AI对话助手(开发中)
+- 智能文本生成(开发中)
+- 图像识别分析(规划中)
+- 需要登录验证
+
+## 🚀 快速开始
+
+### 📋 环境要求
+
+- **Python**: 3.8+
+- **Node.js**: 14+
+- **MongoDB**: 4.0+
+
+### 📦 安装依赖
+
+#### 后端依赖
+```bash
+cd backend
+pip install -r requirements.txt
+```
+
+#### 前端依赖
+```bash
+cd frontend/react-app
+npm install
+```
+
+### 🎯 启动服务
+
+#### 方式一:使用启动器(推荐)
+```bash
+# 双击运行 启动器.bat
+# 选择相应的启动选项
+```
+
+#### 方式二:手动启动
+
+**启动后端服务**
+```bash
+cd backend
+python run.py
+# 后端服务: http://localhost:5000
+```
+
+**启动前端服务**
+```bash
+cd frontend/react-app
+npm start
+# 前端服务: http://localhost:3000
+```
+
+## 📞 联系方式
+
+- **开发者**: 神奇万事通
+- **项目地址**: https://github.com/shumengya/InfoGenie
+- **反馈邮箱**: 请通过GitHub Issues反馈
+
+## 📄 许可证
+
+本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情
+
+---
+
+
+
+**✨ 感谢使用 InfoGenie 神奇万事通 ✨**
+
+🎨 *一个多功能的聚合软件应用* 💬
+
+
神奇万事通,一个支持Windows,Android和web的app,聚合了许多神奇有趣的功能,帮助用户一键化解决问题
前端使用React框架,后端使用Python的Flask框架
diff --git a/backend/app.py b/backend/app.py
new file mode 100644
index 00000000..20905b62
--- /dev/null
+++ b/backend/app.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+InfoGenie 后端主应用入口
+Created by: 神奇万事通
+Date: 2025-09-02
+"""
+
+from flask import Flask, jsonify, request, session, send_from_directory
+from flask_cors import CORS
+from flask_pymongo import PyMongo
+import os
+from datetime import datetime, timedelta
+import hashlib
+import secrets
+
+# 导入模块
+from modules.auth import auth_bp
+from modules.api_60s import api_60s_bp
+from modules.user_management import user_bp
+from modules.email_service import init_mail
+
+from config import Config
+
+def create_app():
+ """创建Flask应用实例"""
+ app = Flask(__name__)
+
+ # 加载配置
+ app.config.from_object(Config)
+
+ # 启用CORS跨域支持
+ CORS(app, supports_credentials=True)
+
+ # 初始化MongoDB
+ mongo = PyMongo(app)
+ app.mongo = mongo
+
+ # 初始化邮件服务
+ init_mail(app)
+
+ # 注册蓝图
+ app.register_blueprint(auth_bp, url_prefix='/api/auth')
+ app.register_blueprint(api_60s_bp, url_prefix='/api/60s')
+ app.register_blueprint(user_bp, url_prefix='/api/user')
+
+ # 基础路由
+ @app.route('/')
+ def index():
+ """API根路径"""
+ return jsonify({
+ 'message': '✨ 神奇万事通 API 服务运行中 ✨',
+ 'version': '1.0.0',
+ 'timestamp': datetime.now().isoformat(),
+ 'endpoints': {
+ 'auth': '/api/auth',
+ '60s_api': '/api/60s',
+ 'user': '/api/user'
+ }
+ })
+
+ @app.route('/api/health')
+ def health_check():
+ """健康检查接口"""
+ try:
+ # 检查数据库连接
+ mongo.db.command('ping')
+ db_status = 'connected'
+ except Exception as e:
+ db_status = f'error: {str(e)}'
+
+ return jsonify({
+ 'status': 'running',
+ 'database': db_status,
+ 'timestamp': datetime.now().isoformat()
+ })
+
+ # 60sapi静态文件服务
+ @app.route('/60sapi/')
+ def serve_60sapi_files(filename):
+ """提供60sapi目录下的静态文件服务"""
+ try:
+ # 获取项目根目录
+ project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+ api_directory = os.path.join(project_root, 'frontend', '60sapi')
+
+ # 安全检查:确保文件路径在允许的目录内
+ full_path = os.path.join(api_directory, filename)
+ if not os.path.commonpath([api_directory, full_path]) == api_directory:
+ return jsonify({'error': '非法文件路径'}), 403
+
+ # 检查文件是否存在
+ if not os.path.exists(full_path):
+ return jsonify({'error': '文件不存在'}), 404
+
+ # 获取文件目录和文件名
+ directory = os.path.dirname(full_path)
+ file_name = os.path.basename(full_path)
+
+ return send_from_directory(directory, file_name)
+
+ except Exception as e:
+ return jsonify({'error': f'文件服务错误: {str(e)}'}), 500
+
+ # 错误处理
+ @app.errorhandler(404)
+ def not_found(error):
+ return jsonify({
+ 'error': 'API接口不存在',
+ 'message': '请检查请求路径是否正确'
+ }), 404
+
+ @app.errorhandler(500)
+ def internal_error(error):
+ return jsonify({
+ 'error': '服务器内部错误',
+ 'message': '请稍后重试或联系管理员'
+ }), 500
+
+ return app
+
+if __name__ == '__main__':
+ app = create_app()
+ print("🚀 启动 InfoGenie 后端服务...")
+ print("📡 API地址: http://localhost:5000")
+ print("📚 文档地址: http://localhost:5000/api/health")
+ app.run(debug=True, host='0.0.0.0', port=5000)
diff --git a/backend/config.py b/backend/config.py
new file mode 100644
index 00000000..c9e7ee47
--- /dev/null
+++ b/backend/config.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+InfoGenie 配置文件
+Created by: 神奇万事通
+Date: 2025-09-02
+"""
+
+import os
+from datetime import timedelta
+from dotenv import load_dotenv
+
+# 加载环境变量
+load_dotenv()
+
+class Config:
+ """应用配置类"""
+
+ # 基础配置
+ SECRET_KEY = os.environ.get('SECRET_KEY') or 'infogenie-secret-key-2025'
+
+ # MongoDB 配置
+ MONGO_URI = os.environ.get('MONGO_URI') or 'mongodb://localhost:27017/InfoGenie'
+
+ # Session 配置
+ PERMANENT_SESSION_LIFETIME = timedelta(days=7) # 会话持续7天
+ SESSION_COOKIE_SECURE = False # 开发环境设为False,生产环境设为True
+ SESSION_COOKIE_HTTPONLY = True
+ SESSION_COOKIE_SAMESITE = 'Lax'
+
+ # 邮件配置
+ MAIL_SERVER = 'smtp.qq.com'
+ MAIL_PORT = 465
+ MAIL_USE_SSL = True
+ MAIL_USE_TLS = False
+ MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or 'your-email@qq.com'
+ MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') or 'your-app-password'
+ MAIL_DEFAULT_SENDER = ('InfoGenie 神奇万事通', os.environ.get('MAIL_USERNAME') or 'your-email@qq.com')
+
+ # API 配置
+ API_RATE_LIMIT = '100 per hour' # API调用频率限制
+
+ # 外部API配置
+ EXTERNAL_APIS = {
+ '60s': [
+ 'https://60s.api.shumengya.top',
+ 'https://60s-cf.viki.moe',
+ 'https://60s.viki.moe',
+ 'https://60s.b23.run',
+ 'https://60s.114128.xyz',
+ 'https://60s-cf.114128.xyz'
+ ]
+ }
+
+ # 应用信息
+ APP_INFO = {
+ 'name': '✨ 神奇万事通 ✨',
+ 'description': '🎨 一个多功能的聚合软件应用 💬',
+ 'author': '👨💻 by-神奇万事通',
+ 'version': '1.0.0',
+ 'icp': '📄 蜀ICP备2025151694号'
+ }
+
+class DevelopmentConfig(Config):
+ """开发环境配置"""
+ DEBUG = True
+ TESTING = False
+
+class ProductionConfig(Config):
+ """生产环境配置"""
+ DEBUG = False
+ TESTING = False
+ SESSION_COOKIE_SECURE = True
+
+class TestingConfig(Config):
+ """测试环境配置"""
+ DEBUG = True
+ TESTING = True
+ MONGO_URI = 'mongodb://localhost:27017/InfoGenie_Test'
+
+# 配置字典
+config = {
+ 'development': DevelopmentConfig,
+ 'production': ProductionConfig,
+ 'testing': TestingConfig,
+ 'default': DevelopmentConfig
+}
diff --git a/backend/md/邮件服务修复说明.md b/backend/md/邮件服务修复说明.md
new file mode 100644
index 00000000..547add51
--- /dev/null
+++ b/backend/md/邮件服务修复说明.md
@@ -0,0 +1,92 @@
+# InfoGenie 邮件服务修复说明
+
+## 修复内容
+
+### 问题描述
+原始的 `email_service.py` 中的邮件发送功能存在问题,无法正常发送验证码邮件。
+
+### 修复方案
+参考成功的 `QQEmailSendAPI.py` 实现,对 `email_service.py` 进行了以下修复:
+
+1. **SMTP连接方式优化**
+ - 将 `with smtplib.SMTP_SSL()` 改为直接使用 `smtplib.SMTP_SSL()`
+ - 显式调用 `smtp_obj.quit()` 关闭连接
+
+2. **邮件头设置优化**
+ - 确保 `From` 字段直接使用邮箱地址,不使用 `Header` 包装
+ - 保持与成功实现的一致性
+
+3. **错误处理增强**
+ - 添加了针对 `SMTPAuthenticationError` 的专门处理
+ - 添加了针对 `SMTPConnectError` 的专门处理
+ - 提供更详细的错误信息
+
+4. **调试信息优化**
+ - 添加了适量的日志输出用于问题诊断
+ - 移除了生产环境不安全的验证码返回
+
+## 配置要求
+
+### 环境变量
+确保设置以下环境变量:
+```bash
+MAIL_USERNAME=your-qq-email@qq.com
+MAIL_PASSWORD=your-qq-auth-code
+```
+
+### QQ邮箱授权码
+1. 登录QQ邮箱
+2. 进入设置 -> 账户
+3. 开启SMTP服务
+4. 获取授权码(不是QQ密码)
+
+## 使用方法
+
+### 发送验证码
+```python
+from modules.email_service import send_verification_email
+
+# 发送注册验证码
+result = send_verification_email('user@qq.com', 'register')
+
+# 发送登录验证码
+result = send_verification_email('user@qq.com', 'login')
+```
+
+### 验证验证码
+```python
+from modules.email_service import verify_code
+
+# 验证用户输入的验证码
+result = verify_code('user@qq.com', '123456')
+```
+
+## 测试
+
+运行测试脚本验证功能:
+```bash
+cd backend
+python test/test_email_fix.py
+```
+
+## 支持的邮箱
+
+目前仅支持QQ邮箱系列:
+- @qq.com
+- @vip.qq.com
+- @foxmail.com
+
+## 注意事项
+
+1. **安全性**:验证码不会在API响应中返回,仅通过邮件发送
+2. **有效期**:验证码有效期为5分钟
+3. **尝试次数**:每个验证码最多可尝试验证3次
+4. **频率限制**:建议添加发送频率限制防止滥用
+
+## 修复文件
+
+- `backend/modules/email_service.py` - 主要修复文件
+- `backend/test/test_email_fix.py` - 测试脚本
+- `backend/邮件服务修复说明.md` - 本说明文档
+
+修复完成后,邮件发送功能已正常工作,可以成功发送注册和登录验证码邮件。
\ No newline at end of file
diff --git a/backend/modules/api_60s.py b/backend/modules/api_60s.py
new file mode 100644
index 00000000..8d1ba3ea
--- /dev/null
+++ b/backend/modules/api_60s.py
@@ -0,0 +1,419 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+60s API模块 - 提供各种实时数据接口
+Created by: 神奇万事通
+Date: 2025-09-02
+"""
+
+from flask import Blueprint, jsonify, request
+import requests
+import json
+from datetime import datetime, timedelta
+import random
+import time
+
+api_60s_bp = Blueprint('api_60s', __name__)
+
+# API配置
+API_ENDPOINTS = {
+ '抖音热搜': {
+ 'urls': [
+ 'https://api.vvhan.com/api/hotlist?type=douyin',
+ 'https://tenapi.cn/v2/douyinhot',
+ 'https://api.oioweb.cn/api/common/tebie/dyhot'
+ ],
+ 'cache_time': 600 # 10分钟缓存
+ },
+ '微博热搜': {
+ 'urls': [
+ 'https://api.vvhan.com/api/hotlist?type=weibo',
+ 'https://tenapi.cn/v2/wbhot',
+ 'https://api.oioweb.cn/api/common/tebie/wbhot'
+ ],
+ 'cache_time': 300 # 5分钟缓存
+ },
+ '猫眼票房': {
+ 'urls': [
+ 'https://api.vvhan.com/api/hotlist?type=maoyan',
+ 'https://tenapi.cn/v2/maoyan'
+ ],
+ 'cache_time': 3600 # 1小时缓存
+ },
+ '网易云音乐': {
+ 'urls': [
+ 'https://api.vvhan.com/api/hotlist?type=netease',
+ 'https://tenapi.cn/v2/music'
+ ],
+ 'cache_time': 1800 # 30分钟缓存
+ },
+ 'HackerNews': {
+ 'urls': [
+ 'https://api.vvhan.com/api/hotlist?type=hackernews',
+ 'https://hacker-news.firebaseio.com/v0/topstories.json'
+ ],
+ 'cache_time': 1800 # 30分钟缓存
+ }
+}
+
+# 内存缓存
+cache = {}
+
+def fetch_data_with_fallback(urls, timeout=10):
+ """使用备用URL获取数据"""
+ for url in urls:
+ try:
+ response = requests.get(url, timeout=timeout, headers={
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+ })
+ if response.status_code == 200:
+ return response.json()
+ except Exception as e:
+ print(f"URL {url} 失败: {str(e)}")
+ continue
+ return None
+
+def get_cached_data(key, cache_time):
+ """获取缓存数据"""
+ if key in cache:
+ cached_time, data = cache[key]
+ if datetime.now() - cached_time < timedelta(seconds=cache_time):
+ return data
+ return None
+
+def set_cache_data(key, data):
+ """设置缓存数据"""
+ cache[key] = (datetime.now(), data)
+
+@api_60s_bp.route('/douyin', methods=['GET'])
+def get_douyin_hot():
+ """获取抖音热搜榜"""
+ try:
+ # 检查缓存
+ cached = get_cached_data('douyin', API_ENDPOINTS['抖音热搜']['cache_time'])
+ if cached:
+ return jsonify({
+ 'success': True,
+ 'data': cached,
+ 'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ 'from_cache': True
+ })
+
+ # 获取新数据
+ data = fetch_data_with_fallback(API_ENDPOINTS['抖音热搜']['urls'])
+
+ if data:
+ # 标准化数据格式
+ if 'data' in data:
+ hot_list = data['data']
+ elif isinstance(data, list):
+ hot_list = data
+ else:
+ hot_list = []
+
+ result = {
+ 'title': '抖音热搜榜',
+ 'subtitle': '实时热门话题 · 紧跟潮流趋势',
+ 'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ 'total': len(hot_list),
+ 'list': hot_list[:50] # 最多返回50条
+ }
+
+ # 设置缓存
+ set_cache_data('douyin', result)
+
+ return jsonify({
+ 'success': True,
+ 'data': result,
+ 'from_cache': False
+ })
+ else:
+ return jsonify({
+ 'success': False,
+ 'message': '获取数据失败,所有数据源暂时不可用'
+ }), 503
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'message': f'服务器错误: {str(e)}'
+ }), 500
+
+@api_60s_bp.route('/weibo', methods=['GET'])
+def get_weibo_hot():
+ """获取微博热搜榜"""
+ try:
+ # 检查缓存
+ cached = get_cached_data('weibo', API_ENDPOINTS['微博热搜']['cache_time'])
+ if cached:
+ return jsonify({
+ 'success': True,
+ 'data': cached,
+ 'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ 'from_cache': True
+ })
+
+ # 获取新数据
+ data = fetch_data_with_fallback(API_ENDPOINTS['微博热搜']['urls'])
+
+ if data:
+ if 'data' in data:
+ hot_list = data['data']
+ elif isinstance(data, list):
+ hot_list = data
+ else:
+ hot_list = []
+
+ result = {
+ 'title': '微博热搜榜',
+ 'subtitle': '热门话题 · 实时更新',
+ 'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ 'total': len(hot_list),
+ 'list': hot_list[:50]
+ }
+
+ set_cache_data('weibo', result)
+
+ return jsonify({
+ 'success': True,
+ 'data': result,
+ 'from_cache': False
+ })
+ else:
+ return jsonify({
+ 'success': False,
+ 'message': '获取数据失败,所有数据源暂时不可用'
+ }), 503
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'message': f'服务器错误: {str(e)}'
+ }), 500
+
+@api_60s_bp.route('/maoyan', methods=['GET'])
+def get_maoyan_box_office():
+ """获取猫眼票房排行榜"""
+ try:
+ cached = get_cached_data('maoyan', API_ENDPOINTS['猫眼票房']['cache_time'])
+ if cached:
+ return jsonify({
+ 'success': True,
+ 'data': cached,
+ 'from_cache': True
+ })
+
+ data = fetch_data_with_fallback(API_ENDPOINTS['猫眼票房']['urls'])
+
+ if data:
+ if 'data' in data:
+ box_office_list = data['data']
+ elif isinstance(data, list):
+ box_office_list = data
+ else:
+ box_office_list = []
+
+ result = {
+ 'title': '猫眼票房排行榜',
+ 'subtitle': '实时票房数据',
+ 'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ 'total': len(box_office_list),
+ 'list': box_office_list[:20]
+ }
+
+ set_cache_data('maoyan', result)
+
+ return jsonify({
+ 'success': True,
+ 'data': result,
+ 'from_cache': False
+ })
+ else:
+ return jsonify({
+ 'success': False,
+ 'message': '获取数据失败'
+ }), 503
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'message': f'服务器错误: {str(e)}'
+ }), 500
+
+@api_60s_bp.route('/60s', methods=['GET'])
+def get_60s_news():
+ """获取每天60秒读懂世界"""
+ try:
+ urls = [
+ 'https://60s-cf.viki.moe',
+ 'https://60s.viki.moe',
+ 'https://60s.b23.run'
+ ]
+
+ data = fetch_data_with_fallback(urls)
+
+ if data:
+ return jsonify({
+ 'success': True,
+ 'data': {
+ 'title': '每天60秒读懂世界',
+ 'content': data,
+ 'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ }
+ })
+ else:
+ return jsonify({
+ 'success': False,
+ 'message': '获取数据失败'
+ }), 503
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'message': f'服务器错误: {str(e)}'
+ }), 500
+
+@api_60s_bp.route('/bing-wallpaper', methods=['GET'])
+def get_bing_wallpaper():
+ """获取必应每日壁纸"""
+ try:
+ url = 'https://api.vvhan.com/api/bing'
+ response = requests.get(url, timeout=10)
+
+ if response.status_code == 200:
+ return jsonify({
+ 'success': True,
+ 'data': {
+ 'title': '必应每日壁纸',
+ 'image_url': response.url,
+ 'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ }
+ })
+ else:
+ return jsonify({
+ 'success': False,
+ 'message': '获取壁纸失败'
+ }), 503
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'message': f'服务器错误: {str(e)}'
+ }), 500
+
+@api_60s_bp.route('/weather', methods=['GET'])
+def get_weather():
+ """获取天气信息"""
+ try:
+ city = request.args.get('city', '北京')
+ url = f'https://api.vvhan.com/api/weather?city={city}'
+
+ response = requests.get(url, timeout=10)
+
+ if response.status_code == 200:
+ data = response.json()
+ return jsonify({
+ 'success': True,
+ 'data': data
+ })
+ else:
+ return jsonify({
+ 'success': False,
+ 'message': '获取天气信息失败'
+ }), 503
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'message': f'服务器错误: {str(e)}'
+ }), 500
+
+@api_60s_bp.route('/scan-directories', methods=['GET'])
+def scan_directories():
+ """扫描60sapi目录结构"""
+ try:
+ import os
+
+ # 获取项目根目录
+ project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+ api_directory = os.path.join(project_root, 'frontend', '60sapi')
+
+ if not os.path.exists(api_directory):
+ return jsonify({
+ 'success': False,
+ 'message': '60sapi目录不存在'
+ }), 404
+
+ categories = []
+
+ # 定义分类配置
+ category_config = {
+ '热搜榜单': {'color': '#66bb6a'},
+ '日更资讯': {'color': '#4caf50'},
+ '实用功能': {'color': '#388e3c'},
+ '娱乐消遣': {'color': '#66bb6a'}
+ }
+
+ # 颜色渐变配置
+ gradient_colors = [
+ 'linear-gradient(135deg, #81c784 0%, #66bb6a 100%)',
+ 'linear-gradient(135deg, #a5d6a7 0%, #81c784 100%)',
+ 'linear-gradient(135deg, #c8e6c9 0%, #a5d6a7 100%)',
+ 'linear-gradient(135deg, #66bb6a 0%, #4caf50 100%)',
+ 'linear-gradient(135deg, #4caf50 0%, #388e3c 100%)'
+ ]
+
+ # 扫描目录
+ for category_name in os.listdir(api_directory):
+ category_path = os.path.join(api_directory, category_name)
+
+ if os.path.isdir(category_path) and category_name in category_config:
+ apis = []
+
+ # 扫描分类下的模块
+ for i, module_name in enumerate(os.listdir(category_path)):
+ module_path = os.path.join(category_path, module_name)
+ index_path = os.path.join(module_path, 'index.html')
+
+ if os.path.isdir(module_path) and os.path.exists(index_path):
+ # 读取HTML文件获取标题
+ try:
+ with open(index_path, 'r', encoding='utf-8') as f:
+ html_content = f.read()
+ title_match = html_content.find('')
+ if title_match != -1:
+ title_end = html_content.find('', title_match)
+ if title_end != -1:
+ title = html_content[title_match + 7:title_end].strip()
+ else:
+ title = module_name
+ else:
+ title = module_name
+ except:
+ title = module_name
+
+ apis.append({
+ 'title': title,
+ 'description': f'{module_name}相关功能',
+ 'link': f'/60sapi/{category_name}/{module_name}/index.html',
+ 'status': 'active',
+ 'color': gradient_colors[i % len(gradient_colors)]
+ })
+
+ if apis:
+ categories.append({
+ 'title': category_name,
+ 'color': category_config[category_name]['color'],
+ 'apis': apis
+ })
+
+ return jsonify({
+ 'success': True,
+ 'categories': categories
+ })
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'message': f'扫描目录时出错: {str(e)}'
+ }), 500
diff --git a/backend/modules/auth.py b/backend/modules/auth.py
new file mode 100644
index 00000000..d091ac1d
--- /dev/null
+++ b/backend/modules/auth.py
@@ -0,0 +1,416 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+用户认证模块
+Created by: 神奇万事通
+Date: 2025-09-02
+"""
+
+from flask import Blueprint, request, jsonify, session, current_app
+from werkzeug.security import generate_password_hash, check_password_hash
+import hashlib
+import re
+from datetime import datetime
+from .email_service import send_verification_email, verify_code, is_qq_email, get_qq_avatar_url
+
+auth_bp = Blueprint('auth', __name__)
+
+def validate_qq_email(email):
+ """验证QQ邮箱格式"""
+ return is_qq_email(email)
+
+def validate_password(password):
+ """验证密码格式(6-20位)"""
+ return 6 <= len(password) <= 20
+
+@auth_bp.route('/send-verification', methods=['POST'])
+def send_verification():
+ """发送验证码邮件"""
+ try:
+ data = request.get_json()
+ email = data.get('email', '').strip()
+ verification_type = data.get('type', 'register') # register, login
+
+ # 参数验证
+ if not email:
+ return jsonify({
+ 'success': False,
+ 'message': '邮箱地址不能为空'
+ }), 400
+
+ if not validate_qq_email(email):
+ return jsonify({
+ 'success': False,
+ 'message': '仅支持QQ邮箱(qq.com、vip.qq.com、foxmail.com)'
+ }), 400
+
+ # 获取数据库集合
+ db = current_app.mongo.db
+ users_collection = db.userdata
+
+ # 检查邮箱是否已注册
+ existing_user = users_collection.find_one({'邮箱': email})
+
+ if verification_type == 'register' and existing_user:
+ return jsonify({
+ 'success': False,
+ 'message': '该邮箱已被注册'
+ }), 409
+
+ if verification_type == 'login' and not existing_user:
+ return jsonify({
+ 'success': False,
+ 'message': '该邮箱尚未注册'
+ }), 404
+
+ # 发送验证码
+ result = send_verification_email(email, verification_type)
+
+ if result['success']:
+ return jsonify(result), 200
+ else:
+ return jsonify(result), 500
+
+ except Exception as e:
+ current_app.logger.error(f"发送验证码失败: {str(e)}")
+ return jsonify({
+ 'success': False,
+ 'message': '发送失败,请稍后重试'
+ }), 500
+
+@auth_bp.route('/verify-code', methods=['POST'])
+def verify_verification_code():
+ """验证验证码"""
+ try:
+ data = request.get_json()
+ email = data.get('email', '').strip()
+ code = data.get('code', '').strip()
+
+ # 参数验证
+ if not email or not code:
+ return jsonify({
+ 'success': False,
+ 'message': '邮箱和验证码不能为空'
+ }), 400
+
+ # 验证码校验
+ result = verify_code(email, code)
+
+ if result['success']:
+ return jsonify(result), 200
+ else:
+ return jsonify(result), 400
+
+ except Exception as e:
+ current_app.logger.error(f"验证码校验失败: {str(e)}")
+ return jsonify({
+ 'success': False,
+ 'message': '验证失败,请稍后重试'
+ }), 500
+
+@auth_bp.route('/register', methods=['POST'])
+def register():
+ """用户注册(需要先验证邮箱)"""
+ try:
+ data = request.get_json()
+ email = data.get('email', '').strip()
+ username = data.get('username', '').strip()
+ password = data.get('password', '').strip()
+ code = data.get('code', '').strip()
+
+ # 参数验证
+ if not all([email, username, password, code]):
+ return jsonify({
+ 'success': False,
+ 'message': '所有字段都不能为空'
+ }), 400
+
+ if not validate_qq_email(email):
+ return jsonify({
+ 'success': False,
+ 'message': '仅支持QQ邮箱注册'
+ }), 400
+
+ if not validate_password(password):
+ return jsonify({
+ 'success': False,
+ 'message': '密码长度必须在6-20位之间'
+ }), 400
+
+ # 验证验证码
+ verify_result = verify_code(email, code)
+ if not verify_result['success'] or verify_result.get('type') != 'register':
+ return jsonify({
+ 'success': False,
+ 'message': '验证码无效或已过期'
+ }), 400
+
+ # 获取数据库集合
+ db = current_app.mongo.db
+ users_collection = db.userdata
+
+ # 检查邮箱是否已被注册
+ if users_collection.find_one({'邮箱': email}):
+ return jsonify({
+ 'success': False,
+ 'message': '该邮箱已被注册'
+ }), 409
+
+ # 检查用户名是否已被使用
+ if users_collection.find_one({'用户名': username}):
+ return jsonify({
+ 'success': False,
+ 'message': '该用户名已被使用'
+ }), 409
+
+ # 获取QQ头像
+ avatar_url = get_qq_avatar_url(email)
+
+ # 创建新用户
+ password_hash = generate_password_hash(password)
+ user_data = {
+ '邮箱': email,
+ '用户名': username,
+ '密码': password_hash,
+ '头像': avatar_url,
+ '注册时间': datetime.now().isoformat(),
+ '最后登录': None,
+ '登录次数': 0,
+ '用户状态': 'active'
+ }
+
+ result = users_collection.insert_one(user_data)
+
+ if result.inserted_id:
+ return jsonify({
+ 'success': True,
+ 'message': '注册成功!',
+ 'user': {
+ 'email': email,
+ 'username': username,
+ 'avatar': avatar_url
+ }
+ }), 201
+ else:
+ return jsonify({
+ 'success': False,
+ 'message': '注册失败,请稍后重试'
+ }), 500
+
+ except Exception as e:
+ current_app.logger.error(f"注册失败: {str(e)}")
+ return jsonify({
+ 'success': False,
+ 'message': '注册失败,请稍后重试'
+ }), 500
+
+ if existing_user:
+ return jsonify({
+ 'success': False,
+ 'message': '该账号已被注册'
+ }), 409
+
+ # 创建新用户
+ password_hash = generate_password_hash(password)
+ user_data = {
+ '账号': account,
+ '密码': password_hash,
+ '注册时间': datetime.now().isoformat(),
+ '最后登录': None,
+ '登录次数': 0,
+ '用户状态': 'active'
+ }
+
+ result = users_collection.insert_one(user_data)
+
+ if result.inserted_id:
+ return jsonify({
+ 'success': True,
+ 'message': '注册成功!'
+ }), 201
+ else:
+ return jsonify({
+ 'success': False,
+ 'message': '注册失败,请稍后重试'
+ }), 500
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'message': f'服务器错误: {str(e)}'
+ }), 500
+
+@auth_bp.route('/login', methods=['POST'])
+def login():
+ """用户登录(支持邮箱+验证码或邮箱+密码)"""
+ try:
+ data = request.get_json()
+ email = data.get('email', '').strip()
+ password = data.get('password', '').strip()
+ code = data.get('code', '').strip()
+
+ # 参数验证
+ if not email:
+ return jsonify({
+ 'success': False,
+ 'message': '邮箱地址不能为空'
+ }), 400
+
+ if not validate_qq_email(email):
+ return jsonify({
+ 'success': False,
+ 'message': '仅支持QQ邮箱登录'
+ }), 400
+
+ # 获取数据库集合
+ db = current_app.mongo.db
+ users_collection = db.userdata
+
+ # 查找用户
+ user = users_collection.find_one({'邮箱': email})
+
+ if not user:
+ return jsonify({
+ 'success': False,
+ 'message': '该邮箱尚未注册'
+ }), 404
+
+ # 检查用户状态
+ if user.get('用户状态') != 'active':
+ return jsonify({
+ 'success': False,
+ 'message': '账号已被禁用,请联系管理员'
+ }), 403
+
+ # 验证方式:验证码登录或密码登录
+ if code:
+ # 验证码登录
+ verify_result = verify_code(email, code)
+ if not verify_result['success'] or verify_result.get('type') != 'login':
+ return jsonify({
+ 'success': False,
+ 'message': '验证码无效或已过期'
+ }), 400
+ elif password:
+ # 密码登录
+ if not check_password_hash(user['密码'], password):
+ return jsonify({
+ 'success': False,
+ 'message': '密码错误'
+ }), 401
+ else:
+ return jsonify({
+ 'success': False,
+ 'message': '请输入密码或验证码'
+ }), 400
+
+ # 登录成功,更新用户信息
+ users_collection.update_one(
+ {'邮箱': email},
+ {
+ '$set': {'最后登录': datetime.now().isoformat()},
+ '$inc': {'登录次数': 1}
+ }
+ )
+
+ # 设置会话
+ session['user_id'] = str(user['_id'])
+ session['email'] = email
+ session['username'] = user.get('用户名', '')
+ session.permanent = True
+
+ return jsonify({
+ 'success': True,
+ 'message': '登录成功!',
+ 'user': {
+ 'id': str(user['_id']),
+ 'email': email,
+ 'username': user.get('用户名', ''),
+ 'avatar': user.get('头像', ''),
+ 'login_count': user.get('登录次数', 0) + 1
+ }
+ }), 200
+
+ except Exception as e:
+ current_app.logger.error(f"登录失败: {str(e)}")
+ return jsonify({
+ 'success': False,
+ 'message': '登录失败,请稍后重试'
+ }), 500
+
+ # 登录成功,创建会话
+ session['user_id'] = str(user['_id'])
+ session['account'] = user['账号']
+ session['logged_in'] = True
+
+ # 更新登录信息
+ users_collection.update_one(
+ {'_id': user['_id']},
+ {
+ '$set': {'最后登录': datetime.now().isoformat()},
+ '$inc': {'登录次数': 1}
+ }
+ )
+
+ return jsonify({
+ 'success': True,
+ 'message': '登录成功!',
+ 'user': {
+ 'account': user['账号'],
+ 'last_login': user.get('最后登录'),
+ 'login_count': user.get('登录次数', 0) + 1
+ }
+ }), 200
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'message': f'服务器错误: {str(e)}'
+ }), 500
+
+@auth_bp.route('/logout', methods=['POST'])
+def logout():
+ """用户登出"""
+ try:
+ if 'logged_in' in session:
+ session.clear()
+ return jsonify({
+ 'success': True,
+ 'message': '已成功登出'
+ }), 200
+ else:
+ return jsonify({
+ 'success': False,
+ 'message': '用户未登录'
+ }), 401
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'message': f'服务器错误: {str(e)}'
+ }), 500
+
+@auth_bp.route('/check', methods=['GET'])
+def check_login():
+ """检查登录状态"""
+ try:
+ if session.get('logged_in'):
+ return jsonify({
+ 'success': True,
+ 'logged_in': True,
+ 'user': {
+ 'account': session.get('account'),
+ 'user_id': session.get('user_id')
+ }
+ }), 200
+ else:
+ return jsonify({
+ 'success': True,
+ 'logged_in': False
+ }), 200
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'message': f'服务器错误: {str(e)}'
+ }), 500
diff --git a/backend/modules/email_service.py b/backend/modules/email_service.py
new file mode 100644
index 00000000..13e33591
--- /dev/null
+++ b/backend/modules/email_service.py
@@ -0,0 +1,276 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+邮件发送模块
+负责处理用户注册、登录验证邮件
+"""
+
+import random
+import string
+import smtplib
+from datetime import datetime, timedelta
+from email.mime.text import MIMEText
+from email.header import Header
+from flask import current_app
+import logging
+import os
+
+# 验证码存储(生产环境建议使用Redis)
+verification_codes = {}
+
+def init_mail(app):
+ """初始化邮件配置"""
+ # 使用smtplib直接发送,不需要Flask-Mail
+ pass
+
+def generate_verification_code(length=6):
+ """生成验证码"""
+ return ''.join(random.choices(string.digits, k=length))
+
+def send_verification_email(email, verification_type='register'):
+ """
+ 发送验证邮件
+
+ Args:
+ email: 收件人邮箱
+ verification_type: 验证类型 ('register', 'login', 'reset_password')
+
+ Returns:
+ dict: 发送结果
+ """
+ try:
+ # 验证QQ邮箱格式
+ if not is_qq_email(email):
+ return {
+ 'success': False,
+ 'message': '仅支持QQ邮箱注册登录'
+ }
+
+ # 生成验证码
+ code = generate_verification_code()
+
+ # 存储验证码(5分钟有效期)
+ verification_codes[email] = {
+ 'code': code,
+ 'type': verification_type,
+ 'expires_at': datetime.now() + timedelta(minutes=5),
+ 'attempts': 0
+ }
+
+ # 获取邮件配置 - 使用与QQEmailSendAPI相同的配置
+ sender_email = os.environ.get('MAIL_USERNAME', '3205788256@qq.com')
+ sender_password = os.environ.get('MAIL_PASSWORD', 'szcaxvbftusqddhi')
+
+ # 邮件模板
+ if verification_type == 'register':
+ subject = '【InfoGenie】注册验证码'
+ html_content = f'''
+
+
+
+
+
InfoGenie 神奇万事通
+
欢迎注册InfoGenie
+
+
+
+
验证码
+
+ {code}
+
+
请在5分钟内输入此验证码完成注册
+
+
+
+
+ 如果您没有申请注册,请忽略此邮件
+ 此验证码5分钟内有效,请勿泄露给他人
+
+
+
+
+
+ '''
+ else: # login
+ subject = '【InfoGenie】登录验证码'
+ html_content = f'''
+
+
+
+
+
InfoGenie 神奇万事通
+
安全登录验证
+
+
+
+
登录验证码
+
+ {code}
+
+
请在5分钟内输入此验证码完成登录
+
+
+
+
+ 如果不是您本人操作,请检查账户安全
+ 此验证码5分钟内有效,请勿泄露给他人
+
+
+
+
+
+ '''
+
+ # 创建邮件 - 使用与QQEmailSendAPI相同的方式
+ message = MIMEText(html_content, 'html', 'utf-8')
+ message['From'] = sender_email # 直接使用邮箱地址,不使用Header包装
+ message['To'] = email
+ message['Subject'] = Header(subject, 'utf-8')
+
+ # 发送邮件 - 使用SSL端口465
+ try:
+ # 使用与QQEmailSendAPI相同的连接方式
+ smtp_obj = smtplib.SMTP_SSL('smtp.qq.com', 465)
+ smtp_obj.login(sender_email, sender_password)
+ smtp_obj.sendmail(sender_email, [email], message.as_string())
+ smtp_obj.quit()
+
+ print(f"验证码邮件发送成功: {email}")
+ return {
+ 'success': True,
+ 'message': '验证码已发送到您的邮箱',
+ 'email': email
+ }
+
+ except smtplib.SMTPAuthenticationError as auth_error:
+ print(f"SMTP认证失败: {str(auth_error)}")
+ return {
+ 'success': False,
+ 'message': 'SMTP认证失败,请检查邮箱配置'
+ }
+ except smtplib.SMTPConnectError as conn_error:
+ print(f"SMTP连接失败: {str(conn_error)}")
+ return {
+ 'success': False,
+ 'message': 'SMTP服务器连接失败'
+ }
+ except Exception as smtp_error:
+ print(f"SMTP发送失败: {str(smtp_error)}")
+ return {
+ 'success': False,
+ 'message': f'邮件发送失败: {str(smtp_error)}'
+ }
+
+ except Exception as e:
+ print(f"邮件发送失败: {str(e)}")
+ return {
+ 'success': False,
+ 'message': '邮件发送失败,请稍后重试'
+ }
+
+def verify_code(email, code):
+ """
+ 验证验证码
+
+ Args:
+ email: 邮箱地址
+ code: 验证码
+
+ Returns:
+ dict: 验证结果
+ """
+ if email not in verification_codes:
+ return {
+ 'success': False,
+ 'message': '验证码不存在或已过期'
+ }
+
+ stored_info = verification_codes[email]
+
+ # 检查过期时间
+ if datetime.now() > stored_info['expires_at']:
+ del verification_codes[email]
+ return {
+ 'success': False,
+ 'message': '验证码已过期,请重新获取'
+ }
+
+ # 检查尝试次数
+ if stored_info['attempts'] >= 3:
+ del verification_codes[email]
+ return {
+ 'success': False,
+ 'message': '验证码输入错误次数过多,请重新获取'
+ }
+
+ # 验证码校验
+ if stored_info['code'] != code:
+ stored_info['attempts'] += 1
+ return {
+ 'success': False,
+ 'message': f'验证码错误,还可尝试{3 - stored_info["attempts"]}次'
+ }
+
+ # 验证成功,删除验证码
+ verification_type = stored_info['type']
+ del verification_codes[email]
+
+ return {
+ 'success': True,
+ 'message': '验证码验证成功',
+ 'type': verification_type
+ }
+
+def is_qq_email(email):
+ """
+ 验证是否为QQ邮箱
+
+ Args:
+ email: 邮箱地址
+
+ Returns:
+ bool: 是否为QQ邮箱
+ """
+ if not email or '@' not in email:
+ return False
+
+ domain = email.split('@')[1].lower()
+ qq_domains = ['qq.com', 'vip.qq.com', 'foxmail.com']
+
+ return domain in qq_domains
+
+def get_qq_avatar_url(email):
+ """
+ 根据QQ邮箱获取QQ头像URL
+
+ Args:
+ email: QQ邮箱地址
+
+ Returns:
+ str: QQ头像URL
+ """
+ if not is_qq_email(email):
+ return None
+
+ # 提取QQ号码
+ qq_number = email.split('@')[0]
+
+ # 验证是否为纯数字(QQ号)
+ if not qq_number.isdigit():
+ return None
+
+ # 返回QQ头像API URL
+ return f"http://q1.qlogo.cn/g?b=qq&nk={qq_number}&s=100"
+
+def cleanup_expired_codes():
+ """清理过期的验证码"""
+ current_time = datetime.now()
+ expired_emails = [
+ email for email, info in verification_codes.items()
+ if current_time > info['expires_at']
+ ]
+
+ for email in expired_emails:
+ del verification_codes[email]
+
+ return len(expired_emails)
\ No newline at end of file
diff --git a/backend/modules/user_management.py b/backend/modules/user_management.py
new file mode 100644
index 00000000..9bdb2ae6
--- /dev/null
+++ b/backend/modules/user_management.py
@@ -0,0 +1,211 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+用户管理模块
+Created by: 神奇万事通
+Date: 2025-09-02
+"""
+
+from flask import Blueprint, request, jsonify, session, current_app
+from datetime import datetime
+from bson import ObjectId
+
+user_bp = Blueprint('user', __name__)
+
+def login_required(f):
+ """登录验证装饰器"""
+ def decorated_function(*args, **kwargs):
+ if not session.get('logged_in'):
+ return jsonify({
+ 'success': False,
+ 'message': '请先登录'
+ }), 401
+ return f(*args, **kwargs)
+ decorated_function.__name__ = f.__name__
+ return decorated_function
+
+@user_bp.route('/profile', methods=['GET'])
+@login_required
+def get_profile():
+ """获取用户资料"""
+ try:
+ user_id = session.get('user_id')
+ users_collection = current_app.mongo.db.userdata
+
+ user = users_collection.find_one({'_id': ObjectId(user_id)})
+
+ if not user:
+ return jsonify({
+ 'success': False,
+ 'message': '用户不存在'
+ }), 404
+
+ # 返回用户信息(不包含密码)
+ profile = {
+ 'account': user['账号'],
+ 'register_time': user.get('注册时间'),
+ 'last_login': user.get('最后登录'),
+ 'login_count': user.get('登录次数', 0),
+ 'status': user.get('用户状态', 'active')
+ }
+
+ return jsonify({
+ 'success': True,
+ 'data': profile
+ }), 200
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'message': f'服务器错误: {str(e)}'
+ }), 500
+
+@user_bp.route('/change-password', methods=['POST'])
+@login_required
+def change_password():
+ """修改密码"""
+ try:
+ data = request.get_json()
+ old_password = data.get('old_password', '').strip()
+ new_password = data.get('new_password', '').strip()
+
+ if not old_password or not new_password:
+ return jsonify({
+ 'success': False,
+ 'message': '旧密码和新密码不能为空'
+ }), 400
+
+ if len(new_password) < 6 or len(new_password) > 20:
+ return jsonify({
+ 'success': False,
+ 'message': '新密码长度必须在6-20位之间'
+ }), 400
+
+ user_id = session.get('user_id')
+ users_collection = current_app.mongo.db.userdata
+
+ user = users_collection.find_one({'_id': ObjectId(user_id)})
+
+ if not user:
+ return jsonify({
+ 'success': False,
+ 'message': '用户不存在'
+ }), 404
+
+ from werkzeug.security import check_password_hash, generate_password_hash
+
+ # 验证旧密码
+ if not check_password_hash(user['密码'], old_password):
+ return jsonify({
+ 'success': False,
+ 'message': '原密码错误'
+ }), 401
+
+ # 更新密码
+ new_password_hash = generate_password_hash(new_password)
+
+ result = users_collection.update_one(
+ {'_id': ObjectId(user_id)},
+ {'$set': {'密码': new_password_hash}}
+ )
+
+ if result.modified_count > 0:
+ return jsonify({
+ 'success': True,
+ 'message': '密码修改成功'
+ }), 200
+ else:
+ return jsonify({
+ 'success': False,
+ 'message': '密码修改失败'
+ }), 500
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'message': f'服务器错误: {str(e)}'
+ }), 500
+
+@user_bp.route('/stats', methods=['GET'])
+@login_required
+def get_user_stats():
+ """获取用户统计信息"""
+ try:
+ user_id = session.get('user_id')
+
+ # 这里可以添加更多统计信息,比如API调用次数等
+ stats = {
+ 'login_today': 1, # 今日登录次数
+ 'api_calls_today': 0, # 今日API调用次数
+ 'total_api_calls': 0, # 总API调用次数
+ 'join_days': 1, # 加入天数
+ 'last_activity': datetime.now().isoformat()
+ }
+
+ return jsonify({
+ 'success': True,
+ 'data': stats
+ }), 200
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'message': f'服务器错误: {str(e)}'
+ }), 500
+
+@user_bp.route('/delete', methods=['POST'])
+@login_required
+def delete_account():
+ """删除账户"""
+ try:
+ data = request.get_json()
+ password = data.get('password', '').strip()
+
+ if not password:
+ return jsonify({
+ 'success': False,
+ 'message': '请输入密码确认删除'
+ }), 400
+
+ user_id = session.get('user_id')
+ users_collection = current_app.mongo.db.userdata
+
+ user = users_collection.find_one({'_id': ObjectId(user_id)})
+
+ if not user:
+ return jsonify({
+ 'success': False,
+ 'message': '用户不存在'
+ }), 404
+
+ from werkzeug.security import check_password_hash
+
+ # 验证密码
+ if not check_password_hash(user['密码'], password):
+ return jsonify({
+ 'success': False,
+ 'message': '密码错误'
+ }), 401
+
+ # 删除用户
+ result = users_collection.delete_one({'_id': ObjectId(user_id)})
+
+ if result.deleted_count > 0:
+ # 清除会话
+ session.clear()
+
+ return jsonify({
+ 'success': True,
+ 'message': '账户已成功删除'
+ }), 200
+ else:
+ return jsonify({
+ 'success': False,
+ 'message': '删除失败'
+ }), 500
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'message': f'服务器错误: {str(e)}'
+ }), 500
diff --git a/backend/requirements.txt b/backend/requirements.txt
new file mode 100644
index 00000000..ba9ba09c
--- /dev/null
+++ b/backend/requirements.txt
@@ -0,0 +1,26 @@
+# InfoGenie 后端依赖包
+# Web框架
+Flask==2.3.3
+Flask-CORS==4.0.0
+
+# 数据库
+Flask-PyMongo==2.3.0
+pymongo==4.5.0
+
+# 密码加密
+Werkzeug==2.3.7
+
+# HTTP请求
+requests==2.31.0
+
+# 邮件发送
+Flask-Mail==0.9.1
+
+# 数据处理
+python-dateutil==2.8.2
+
+# 环境变量
+python-dotenv==1.0.0
+
+# 开发工具
+flask-limiter==3.5.0 # API限流
diff --git a/backend/test/email_test.py b/backend/test/email_test.py
new file mode 100644
index 00000000..9b219e6f
--- /dev/null
+++ b/backend/test/email_test.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+测试注册邮件发送
+"""
+
+import requests
+import json
+
+def test_send_verification_email():
+ """测试发送验证码邮件"""
+ url = "http://localhost:5000/api/auth/send-verification"
+
+ test_data = {
+ "email": "3205788256@qq.com", # 使用配置的邮箱
+ "type": "register"
+ }
+
+ try:
+ response = requests.post(url, json=test_data)
+ print(f"状态码: {response.status_code}")
+ print(f"响应: {response.json()}")
+
+ if response.status_code == 200:
+ print("\n✅ 邮件发送成功!请检查邮箱")
+ else:
+ print(f"\n❌ 邮件发送失败: {response.json().get('message', '未知错误')}")
+
+ except Exception as e:
+ print(f"❌ 请求失败: {str(e)}")
+
+if __name__ == "__main__":
+ print("📧 测试注册邮件发送...")
+ test_send_verification_email()
diff --git a/backend/test/mongo_test.py b/backend/test/mongo_test.py
new file mode 100644
index 00000000..26e79874
--- /dev/null
+++ b/backend/test/mongo_test.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+MongoDB连接测试
+"""
+
+from pymongo import MongoClient
+
+def test_connection():
+ # 测试不同的连接配置
+ configs = [
+ "mongodb://shumengya:tyh%4019900420@192.168.1.233:27017/InfoGenie",
+ "mongodb://shumengya:tyh%4019900420@192.168.1.233:27017/InfoGenie?authSource=admin",
+ "mongodb://shumengya:tyh%4019900420@192.168.1.233:27017/InfoGenie?authSource=InfoGenie",
+ "mongodb://shumengya:tyh%4019900420@192.168.1.233:27017/?authSource=admin",
+ ]
+
+ for i, uri in enumerate(configs):
+ print(f"\n测试配置 {i+1}: {uri}")
+ try:
+ client = MongoClient(uri, serverSelectionTimeoutMS=5000)
+ client.admin.command('ping')
+ print("✅ 连接成功!")
+
+ # 测试InfoGenie数据库
+ db = client.InfoGenie
+ collections = db.list_collection_names()
+ print(f"数据库集合: {collections}")
+
+ # 测试userdata集合
+ if 'userdata' in collections:
+ count = db.userdata.count_documents({})
+ print(f"userdata集合文档数: {count}")
+
+ client.close()
+ return uri
+
+ except Exception as e:
+ print(f"❌ 连接失败: {str(e)}")
+
+ return None
+
+if __name__ == "__main__":
+ print("🔧 测试MongoDB连接...")
+ success_uri = test_connection()
+ if success_uri:
+ print(f"\n✅ 成功的连接字符串: {success_uri}")
+ else:
+ print("\n❌ 所有连接尝试都失败了")
diff --git a/backend/test/test_email.py b/backend/test/test_email.py
new file mode 100644
index 00000000..ee645e86
--- /dev/null
+++ b/backend/test/test_email.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+测试邮件发送功能
+"""
+
+import requests
+import json
+
+def test_send_verification():
+ """测试发送验证码"""
+ url = "http://localhost:5000/api/auth/send-verification"
+
+ # 测试数据
+ test_data = {
+ "email": "3205788256@qq.com", # 使用配置中的测试邮箱
+ "type": "register"
+ }
+
+ try:
+ response = requests.post(url, json=test_data)
+ print(f"状态码: {response.status_code}")
+ print(f"响应内容: {response.json()}")
+
+ if response.status_code == 200:
+ print("✅ 邮件发送成功!")
+ else:
+ print("❌ 邮件发送失败")
+
+ except Exception as e:
+ print(f"❌ 请求失败: {str(e)}")
+
+if __name__ == "__main__":
+ print("📧 测试邮件发送功能...")
+ test_send_verification()
diff --git a/backend/test/test_email_fix.py b/backend/test/test_email_fix.py
new file mode 100644
index 00000000..6c7b164d
--- /dev/null
+++ b/backend/test/test_email_fix.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+测试修复后的邮件发送功能
+"""
+
+import sys
+import os
+
+# 添加父目录到路径
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from modules.email_service import send_verification_email, verify_code
+
+def test_email_sending():
+ """
+ 测试邮件发送功能
+ """
+ print("=== 测试邮件发送功能 ===")
+
+ # 测试邮箱(请替换为你的QQ邮箱)
+ test_email = "3205788256@qq.com" # 替换为实际的测试邮箱
+
+ print(f"正在向 {test_email} 发送注册验证码...")
+
+ # 发送注册验证码
+ result = send_verification_email(test_email, 'register')
+
+ print(f"发送结果: {result}")
+
+ if result['success']:
+ print("✅ 邮件发送成功!")
+ if 'code' in result:
+ print(f"验证码: {result['code']}")
+
+ # 测试验证码验证
+ print("\n=== 测试验证码验证 ===")
+ verify_result = verify_code(test_email, result['code'])
+ print(f"验证结果: {verify_result}")
+
+ if verify_result['success']:
+ print("✅ 验证码验证成功!")
+ else:
+ print("❌ 验证码验证失败!")
+ else:
+ print("❌ 邮件发送失败!")
+ print(f"错误信息: {result['message']}")
+
+def test_login_email():
+ """
+ 测试登录验证码邮件
+ """
+ print("\n=== 测试登录验证码邮件 ===")
+
+ test_email = "3205788256@qq.com" # 替换为实际的测试邮箱
+
+ print(f"正在向 {test_email} 发送登录验证码...")
+
+ result = send_verification_email(test_email, 'login')
+
+ print(f"发送结果: {result}")
+
+ if result['success']:
+ print("✅ 登录验证码邮件发送成功!")
+ if 'code' in result:
+ print(f"验证码: {result['code']}")
+ else:
+ print("❌ 登录验证码邮件发送失败!")
+ print(f"错误信息: {result['message']}")
+
+if __name__ == '__main__':
+ print("InfoGenie 邮件服务测试")
+ print("=" * 50)
+
+ # 测试注册验证码
+ test_email_sending()
+
+ # 测试登录验证码
+ test_login_email()
+
+ print("\n测试完成!")
\ No newline at end of file
diff --git a/backend/test/test_mongo.py b/backend/test/test_mongo.py
new file mode 100644
index 00000000..830d1557
--- /dev/null
+++ b/backend/test/test_mongo.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+测试MongoDB连接
+"""
+
+import os
+from pymongo import MongoClient
+from dotenv import load_dotenv
+
+# 加载环境变量
+load_dotenv()
+
+def test_mongodb_connection():
+ """测试MongoDB连接"""
+ try:
+ # 获取连接字符串
+ mongo_uri = os.environ.get('MONGO_URI')
+ print(f"连接字符串: {mongo_uri}")
+
+ # 创建连接
+ client = MongoClient(mongo_uri)
+
+ # 测试连接
+ client.admin.command('ping')
+ print("✅ MongoDB连接成功!")
+
+ # 获取数据库
+ db = client.InfoGenie
+ print(f"数据库: {db.name}")
+
+ # 测试集合访问
+ userdata_collection = db.userdata
+ print(f"用户集合: {userdata_collection.name}")
+
+ # 测试查询(计算文档数量)
+ count = userdata_collection.count_documents({})
+ print(f"用户数据集合中有 {count} 个文档")
+
+ # 关闭连接
+ client.close()
+
+ except Exception as e:
+ print(f"❌ MongoDB连接失败: {str(e)}")
+
+ # 尝试其他认证数据库
+ print("\n尝试使用不同的认证配置...")
+ try:
+ # 尝试不指定认证数据库
+ uri_without_auth = "mongodb://shumengya:tyh%4019900420@192.168.1.233:27017/InfoGenie"
+ client2 = MongoClient(uri_without_auth)
+ client2.admin.command('ping')
+ print("✅ 不使用authSource连接成功!")
+ client2.close()
+ except Exception as e2:
+ print(f"❌ 无authSource也失败: {str(e2)}")
+
+ # 尝试使用InfoGenie作为认证数据库
+ try:
+ uri_with_infogenie_auth = "mongodb://shumengya:tyh%4019900420@192.168.1.233:27017/InfoGenie?authSource=InfoGenie"
+ client3 = MongoClient(uri_with_infogenie_auth)
+ client3.admin.command('ping')
+ print("✅ 使用InfoGenie作为authSource连接成功!")
+ client3.close()
+ except Exception as e3:
+ print(f"❌ InfoGenie authSource也失败: {str(e3)}")
+
+if __name__ == "__main__":
+ print("🔧 测试MongoDB连接...")
+ test_mongodb_connection()
diff --git a/frontend/60sapi/热搜榜单/抖音热搜榜/js/script.js b/frontend/60sapi/热搜榜单/抖音热搜榜/js/script.js
index 5e57415c..03931e5d 100644
--- a/frontend/60sapi/热搜榜单/抖音热搜榜/js/script.js
+++ b/frontend/60sapi/热搜榜单/抖音热搜榜/js/script.js
@@ -1,4 +1,7 @@
-// API接口列表
+// 本地后端API接口
+const LOCAL_API_BASE = 'http://localhost:5000/api/60s';
+
+// API接口列表(备用)
const API_ENDPOINTS = [
"https://60s-cf.viki.moe",
"https://60s.viki.moe",
@@ -9,6 +12,7 @@ const API_ENDPOINTS = [
// 当前使用的API索引
let currentApiIndex = 0;
+let useLocalApi = true;
// DOM元素
const loadingElement = document.getElementById('loading');
@@ -46,6 +50,30 @@ async function loadHotList() {
// 获取数据
async function fetchData() {
+ // 优先尝试本地API
+ if (useLocalApi) {
+ try {
+ const response = await fetch(`${LOCAL_API_BASE}/douyin`, {
+ method: 'GET',
+ headers: {
+ 'Accept': 'application/json',
+ },
+ timeout: 10000
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data.code === 200 && data.data) {
+ return data;
+ }
+ }
+ } catch (error) {
+ console.warn('本地API请求失败,切换到外部API:', error);
+ useLocalApi = false;
+ }
+ }
+
+ // 使用外部API作为备用
for (let i = 0; i < API_ENDPOINTS.length; i++) {
const apiUrl = API_ENDPOINTS[currentApiIndex];
diff --git a/frontend/60sapi/生成要求模板.txt b/frontend/60sapi/生成要求模板.txt
index 0a3addc7..91b7c04d 100644
--- a/frontend/60sapi/生成要求模板.txt
+++ b/frontend/60sapi/生成要求模板.txt
@@ -5,4 +5,4 @@
5.返回接口.json储存了网页api返回的数据格式
6.严格按照用户要求执行,不得随意添加什么注解,如“以下数据来自...”
7.接口集合.json保存了所有已知的后端API接口,一个访问不了尝试自动切换另一个
-8.在css中有关背景的css单独一个css文件,方便我直接迁移
\ No newline at end of file
+8.在css中有关背景的css单独一个css文件,方便我直接迁移
diff --git a/frontend/assets/App Logo 设计 (2).png b/frontend/assets/App Logo 设计 (2).png
new file mode 100644
index 0000000000000000000000000000000000000000..2e50c34cbce4b3177f64644a99c9016702120bfd
GIT binary patch
literal 1619286
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