Compare commits

..

27 Commits

Author SHA1 Message Date
7786e5f507 修改前端 项目改名 2025-09-19 22:03:59 +08:00
98c6371c4e 继续更新 2025-09-17 22:03:31 +08:00
887f6fb5d6 update 2025-09-17 21:15:24 +08:00
5613cdd6c9 修复AI工具调用报错 2025-09-17 18:29:54 +08:00
dd3f702887 校赛提交 2025-09-16 22:17:20 +08:00
174d8a3bc5 前端美化 2025-09-16 18:39:10 +08:00
17691af8d1 继续完善 2025-09-16 12:57:36 +08:00
249e434b72 简单修改 2025-09-16 09:14:04 +08:00
dcfa89e63c 优化结果 2025-09-15 19:08:47 +08:00
72084a8782 整理 清理项目架构 2025-09-09 13:54:00 +08:00
2b5a6740d4 重新整理项目结构 2025-09-09 13:45:04 +08:00
44a4f1bae1 更新前端静态网页获取方式,放弃使用后端获取api 2025-09-09 10:47:51 +08:00
6889ca37e5 添加更多的API接口功能 2025-09-08 22:08:09 +08:00
227ccf9c29 修改说明 2025-09-08 11:27:03 +08:00
261fb6edb3 修复 2025-09-07 22:01:47 +08:00
c2b2e84416 修复 2025-09-07 22:00:47 +08:00
yangyaoxiang666
0a8b20e450 Merge branch 'main' of https://github.com/shumengya/InfoGenie 2025-09-06 09:54:54 +08:00
yangyaoxiang666
bd4c7439be 热搜榜单
热搜榜单
2025-09-06 09:54:49 +08:00
6f850caad1 网页框架部署成功 2025-09-04 16:03:09 +08:00
Chance_Li
71a648fdf4 新的实用更新 2025-09-04 14:31:44 +08:00
yangyaoxiang666
b8456c437a Merge branch 'main' of https://github.com/shumengya/InfoGenie 2025-09-04 14:07:47 +08:00
yangyaoxiang666
6829a16f96 热门推荐
热门推荐
2025-09-04 14:07:42 +08:00
eb13979d4d 部分修复 2025-09-04 13:38:14 +08:00
HcpY
cca1a969c5 提交修复部分api 2025-09-04 13:36:00 +08:00
HcpY
ebd66145c8 Merge branch 'main' of https://github.com/shumengya/InfoGenie 2025-09-04 13:24:01 +08:00
Chance_Li
e62a2d7127 实用功能更新 2025-09-03 12:55:23 +08:00
XsX05
dd43157e09 随机发病,运势,冷笑话,段子 2025-09-02 22:13:07 +08:00
497 changed files with 46003 additions and 40568 deletions

214
.gitignore vendored Normal file → Executable file
View File

@@ -1,209 +1,5 @@
# Byte-compiled / optimized / DLL files #项目自忽略
__pycache__/ .vscode
*.py[codz] InfoGenie-frontend/node_modules
*$py.class InfoGenie-frontend/build
InfoGenie-backend/__pycache__
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock
#pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
frontend/react-app/node_modules/

3
.vscode/settings.json vendored Normal file → Executable file
View File

@@ -1,3 +1,4 @@
{ {
"git.ignoreLimitWarning": true "git.ignoreLimitWarning": true,
"terminal.integrated.defaultProfile.windows": "Command Prompt"
} }

Submodule InfoGenie deleted from dd43157e09

View File

@@ -0,0 +1,50 @@
# Git
.git
.gitignore
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
.venv/
pip-log.txt
pip-delete-this-directory.txt
.tox/
.coverage
.coverage.*
.pytest_cache/
# 环境变量文件
.env
.env.production
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# 日志文件
*.log
# 测试文件(可选,如果不想包含在镜像中)
test/
# 文档文件(可选)
*.md
LICENSE
# 启动脚本Windows
*.bat
# 其他临时文件
*.tmp
.cache/

16
InfoGenie-backend/.env Executable file
View File

@@ -0,0 +1,16 @@
# InfoGenie 环境变量配置文件
# 请勿将此文件提交到版本控制系统
# 邮件配置
# 请将下面的邮箱地址替换为您的实际QQ邮箱
MAIL_USERNAME=3205788256@qq.com
MAIL_PASSWORD=szcaxvbftusqddhi
# 数据库配置
MONGO_URI=mongodb://shumengya:tyh%4019900420@47.108.90.0:27018/InfoGenie?authSource=admin
# 应用密钥
SECRET_KEY=infogenie-secret-key-2025
# 环境配置
FLASK_ENV=development

View File

@@ -0,0 +1,14 @@
# 生产环境配置
# MongoDB配置
MONGO_URI=mongodb://用户名:密码@主机地址:端口/InfoGenie?authSource=admin
# 邮件配置
MAIL_USERNAME=your-email@qq.com
MAIL_PASSWORD=your-app-password
# 应用密钥
SECRET_KEY=infogenie-production-secret-key-2025
# 会话安全配置
HWT_SECURE=True

View File

@@ -0,0 +1,32 @@
# 使用官方Python镜像作为基础镜像
FROM python:3.10-slim
# 设置工作目录
WORKDIR /app
# 安装系统依赖(如果需要)
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# 复制requirements.txt并安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制应用代码
COPY . .
# 创建非root用户安全最佳实践
RUN useradd --create-home --shell /bin/bash app \
&& chown -R app:app /app
USER app
# 暴露端口
EXPOSE 5002
# 设置环境变量
ENV FLASK_APP=app.py
ENV FLASK_ENV=production
# 启动命令
CMD ["python", "app.py"]

View File

@@ -0,0 +1,13 @@
{
"deepseek": {
"api_key": "sk-832f8e5250464de08a31523c7fd71295",
"api_base": "https://api.deepseek.com",
"model": ["deepseek-chat","deepseek-reasoner"]
},
"kimi": {
"api_key": "sk-zdg9NBpTlhOcDDpoWfaBKu0KNDdGv18SipORnL2utawja0bE",
"api_base": "https://api.moonshot.cn",
"model": ["kimi-k2-0905-preview","kimi-k2-0711-preview"]
}
}

75
backend/app.py → InfoGenie-backend/app.py Normal file → Executable file
View File

@@ -2,11 +2,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
InfoGenie 后端主应用入口 InfoGenie 后端主应用入口
Created by: 神奇万事通 Created by: 万象口袋
Date: 2025-09-02 Date: 2025-09-02
""" """
from flask import Flask, jsonify, request, session, send_from_directory from flask import Flask, jsonify, request, send_from_directory
from flask_cors import CORS from flask_cors import CORS
from flask_pymongo import PyMongo from flask_pymongo import PyMongo
import os import os
@@ -16,12 +16,13 @@ import secrets
# 导入模块 # 导入模块
from modules.auth import auth_bp from modules.auth import auth_bp
from modules.api_60s import api_60s_bp
from modules.user_management import user_bp from modules.user_management import user_bp
from modules.email_service import init_mail from modules.email_service import init_mail
from modules.aimodelapp import aimodelapp_bp
from config import Config from config import Config
# 创建Flask应用
def create_app(): def create_app():
"""创建Flask应用实例""" """创建Flask应用实例"""
app = Flask(__name__) app = Flask(__name__)
@@ -29,7 +30,7 @@ def create_app():
# 加载配置 # 加载配置
app.config.from_object(Config) app.config.from_object(Config)
# 启用CORS跨域支持 # 启用CORS跨域支持(允许所有源)
CORS(app, supports_credentials=True) CORS(app, supports_credentials=True)
# 初始化MongoDB # 初始化MongoDB
@@ -41,21 +42,23 @@ def create_app():
# 注册蓝图 # 注册蓝图
app.register_blueprint(auth_bp, url_prefix='/api/auth') 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.register_blueprint(user_bp, url_prefix='/api/user')
app.register_blueprint(aimodelapp_bp, url_prefix='/api/aimodelapp')
# 基础路由 # 基础路由
@app.route('/') @app.route('/')
def index(): def index():
"""API根路径""" """API根路径"""
return jsonify({ return jsonify({
'message': '神奇万事通 API 服务运行中 ✨', 'message': '万象口袋 API 服务运行中 ✨',
'version': '1.0.0', 'version': '1.0.0',
'timestamp': datetime.now().isoformat(), 'timestamp': datetime.now().isoformat(),
'endpoints': { 'endpoints': {
'auth': '/api/auth', 'auth': '/api/auth',
'60s_api': '/api/60s', '60s_api': '/api/60s',
'user': '/api/user' 'user': '/api/user',
'smallgame': '/api/smallgame',
'aimodelapp': '/api/aimodelapp'
} }
}) })
@@ -102,6 +105,60 @@ def create_app():
except Exception as e: except Exception as e:
return jsonify({'error': f'文件服务错误: {str(e)}'}), 500 return jsonify({'error': f'文件服务错误: {str(e)}'}), 500
# smallgame静态文件服务
@app.route('/smallgame/<path:filename>')
def serve_smallgame_files(filename):
"""提供smallgame目录下的静态文件服务"""
try:
# 获取项目根目录
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
game_directory = os.path.join(project_root, 'frontend', 'smallgame')
# 安全检查:确保文件路径在允许的目录内
full_path = os.path.join(game_directory, filename)
if not os.path.commonpath([game_directory, full_path]) == game_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
# aimodelapp静态文件服务
@app.route('/aimodelapp/<path:filename>')
def serve_aimodelapp_files(filename):
"""提供aimodelapp目录下的静态文件服务"""
try:
# 获取项目根目录
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ai_directory = os.path.join(project_root, 'frontend', 'public', 'aimodelapp')
# 安全检查:确保文件路径在允许的目录内
full_path = os.path.join(ai_directory, filename)
if not os.path.commonpath([ai_directory, full_path]) == ai_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) @app.errorhandler(404)
def not_found(error): def not_found(error):
@@ -122,6 +179,4 @@ def create_app():
if __name__ == '__main__': if __name__ == '__main__':
app = create_app() app = create_app()
print("🚀 启动 InfoGenie 后端服务...") print("🚀 启动 InfoGenie 后端服务...")
print("📡 API地址: http://localhost:5000") app.run(debug=True, host='0.0.0.0', port=5002)
print("📚 文档地址: http://localhost:5000/api/health")
app.run(debug=True, host='0.0.0.0', port=5000)

118
InfoGenie-backend/build_docker.sh Executable file
View File

@@ -0,0 +1,118 @@
#!/bin/bash
# InfoGenie 后端 Docker 镜像构建脚本
# Created by: 万象口袋
# Date: 2025-09-16
set -e
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 配置
IMAGE_NAME="infogenie-backend"
IMAGE_TAG="latest"
DOCKERFILE_PATH="."
# 函数:打印信息
print_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 检查Docker是否安装
check_docker() {
if ! command -v docker &> /dev/null; then
print_error "Docker 未安装,请先安装 Docker"
exit 1
fi
print_info "Docker 版本: $(docker --version)"
}
# 检查Dockerfile是否存在
check_dockerfile() {
if [ ! -f "Dockerfile" ]; then
print_error "Dockerfile 不存在"
exit 1
fi
print_info "找到 Dockerfile"
}
# 构建Docker镜像
build_image() {
print_info "开始构建 Docker 镜像: ${IMAGE_NAME}:${IMAGE_TAG}"
# 构建镜像
docker build \
--no-cache \
-t ${IMAGE_NAME}:${IMAGE_TAG} \
-f ${DOCKERFILE_PATH}/Dockerfile \
${DOCKERFILE_PATH}
if [ $? -eq 0 ]; then
print_info "Docker 镜像构建成功!"
print_info "镜像信息:"
docker images ${IMAGE_NAME}:${IMAGE_TAG}
else
print_error "Docker 镜像构建失败"
exit 1
fi
}
# 显示使用说明
show_usage() {
echo ""
print_info "构建完成! 使用方法:"
echo ""
echo "1. 运行容器 (需要MongoDB):"
echo " docker run -d \\"
echo " --name infogenie-backend \\"
echo " -p 5002:5002 \\"
echo " -e MONGO_URI=mongodb://host.docker.internal:27017/InfoGenie \\"
echo " -e SECRET_KEY=your-secret-key \\"
echo " -e MAIL_USERNAME=your-email@qq.com \\"
echo " -e MAIL_PASSWORD=your-app-password \\"
echo " ${IMAGE_NAME}:${IMAGE_TAG}"
echo ""
echo "2. 使用 Docker Compose (推荐):"
echo " 创建 docker-compose.yml 文件并运行:"
echo " docker-compose up -d"
echo ""
echo "3. 查看日志:"
echo " docker logs infogenie-backend"
echo ""
echo "4. 停止容器:"
echo " docker stop infogenie-backend"
echo " docker rm infogenie-backend"
}
# 主函数
main() {
print_info "InfoGenie 后端 Docker 镜像构建脚本"
print_info "=================================="
# 检查环境
check_docker
check_dockerfile
# 构建镜像
build_image
# 显示使用说明
show_usage
print_info "构建脚本执行完成!"
}
# 执行主函数
main "$@"

30
backend/config.py → InfoGenie-backend/config.py Normal file → Executable file
View File

@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
InfoGenie 配置文件 InfoGenie 配置文件
Created by: 神奇万事通 Created by: 万象口袋
Date: 2025-09-02 Date: 2025-09-02
""" """
@@ -22,11 +22,14 @@ class Config:
# MongoDB 配置 # MongoDB 配置
MONGO_URI = os.environ.get('MONGO_URI') or 'mongodb://localhost:27017/InfoGenie' MONGO_URI = os.environ.get('MONGO_URI') or 'mongodb://localhost:27017/InfoGenie'
# Session 配置 # hwt 配置
PERMANENT_SESSION_LIFETIME = timedelta(days=7) # 会话持续7天 HWT_LIFETIME = timedelta(days=7) # hwt持续7天
SESSION_COOKIE_SECURE = False # 开发环境设为False生产环境设为True HWT_SECURE = False # 开发环境设为False生产环境设为True
SESSION_COOKIE_HTTPONLY = True HWT_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax' HWT_SAMESITE = 'Lax'
HWT_DOMAIN = None # 开发环境设为None生产环境设为具体域名
HWT_PATH = '/'
HWT_REFRESH_EACH_REQUEST = True # 每次请求刷新hwt过期时间
# 邮件配置 # 邮件配置
MAIL_SERVER = 'smtp.qq.com' MAIL_SERVER = 'smtp.qq.com'
@@ -35,7 +38,7 @@ class Config:
MAIL_USE_TLS = False MAIL_USE_TLS = False
MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or 'your-email@qq.com' MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or 'your-email@qq.com'
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') or 'your-app-password' 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') MAIL_DEFAULT_SENDER = ('InfoGenie 万象口袋', os.environ.get('MAIL_USERNAME') or 'your-email@qq.com')
# API 配置 # API 配置
API_RATE_LIMIT = '100 per hour' # API调用频率限制 API_RATE_LIMIT = '100 per hour' # API调用频率限制
@@ -43,20 +46,15 @@ class Config:
# 外部API配置 # 外部API配置
EXTERNAL_APIS = { EXTERNAL_APIS = {
'60s': [ '60s': [
'https://60s.api.shumengya.top', '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 = { APP_INFO = {
'name': '神奇万事通', 'name': '万象口袋',
'description': '🎨 一个多功能的聚合软件应用 💬', 'description': '🎨 一个多功能的聚合软件应用 💬',
'author': '👨‍💻 by-神奇万事通', 'author': '👨‍💻 by-万象口袋',
'version': '1.0.0', 'version': '1.0.0',
'icp': '📄 蜀ICP备2025151694号' 'icp': '📄 蜀ICP备2025151694号'
} }
@@ -70,7 +68,7 @@ class ProductionConfig(Config):
"""生产环境配置""" """生产环境配置"""
DEBUG = False DEBUG = False
TESTING = False TESTING = False
SESSION_COOKIE_SECURE = True HWT_SECURE = True
class TestingConfig(Config): class TestingConfig(Config):
"""测试环境配置""" """测试环境配置"""

View File

@@ -0,0 +1,53 @@
version: '3.8'
services:
# InfoGenie 后端服务
infogenie-backend:
build:
context: .
dockerfile: Dockerfile
ports:
- "5002:5002"
environment:
- FLASK_ENV=production
- SECRET_KEY=${SECRET_KEY:-infogenie-secret-key-2025}
- MONGO_URI=mongodb://mongodb:27017/InfoGenie
- MAIL_USERNAME=${MAIL_USERNAME:-your-email@qq.com}
- MAIL_PASSWORD=${MAIL_PASSWORD:-your-app-password}
- HWT_SECURE=false
depends_on:
- mongodb
networks:
- infogenie-network
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5002/api/health"]
interval: 30s
timeout: 10s
retries: 3
# MongoDB 数据库
mongodb:
image: mongo:6.0
ports:
- "27017:27017"
environment:
- MONGO_INITDB_DATABASE=InfoGenie
volumes:
- mongodb_data:/data/db
- ./mongo-init:/docker-entrypoint-initdb.d
networks:
- infogenie-network
restart: unless-stopped
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 30s
timeout: 10s
retries: 3
volumes:
mongodb_data:
networks:
infogenie-network:
driver: bridge

Binary file not shown.

View File

@@ -0,0 +1,954 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
AI模型应用服务模块
Created by: 万象口袋
Date: 2025-01-15
"""
from flask import Blueprint, request, jsonify, current_app
import requests
import json
import os
from datetime import datetime
from bson import ObjectId
from functools import wraps
# 创建蓝图
aimodelapp_bp = Blueprint('aimodelapp', __name__)
# AI功能萌芽币消耗配置
AI_COST = 100 # 每次调用AI功能消耗的萌芽币数量
# 验证用户萌芽币余额装饰器
def verify_user_coins(f):
"""验证用户萌芽币余额并在调用AI功能后扣除相应数量的萌芽币"""
@wraps(f)
def decorated(*args, **kwargs):
try:
# 获取用户认证信息
token = request.headers.get('Authorization')
if not token:
return jsonify({
'success': False,
'message': '未提供认证信息',
'error_code': 'auth_required'
}), 401
if token.startswith('Bearer '):
token = token[7:]
# 解析JWT token
import jwt
try:
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
user_id = payload['user_id']
except Exception as jwt_error:
print(f"JWT解析错误: {str(jwt_error)}")
return jsonify({
'success': False,
'message': '无效的认证信息',
'error_code': 'invalid_token'
}), 401
# 查询用户萌芽币余额
users_collection = current_app.mongo.db.userdata
user = users_collection.find_one({'_id': ObjectId(user_id)})
if not user:
return jsonify({
'success': False,
'message': '用户不存在',
'error_code': 'user_not_found'
}), 404
# 检查萌芽币余额
current_coins = user.get('萌芽币', 0)
if current_coins < AI_COST:
return jsonify({
'success': False,
'message': f'萌芽币余额不足!当前余额: {current_coins}, 需要: {AI_COST}',
'error_code': 'insufficient_coins',
'current_coins': current_coins,
'required_coins': AI_COST
}), 402
# 先扣除萌芽币,确保无论服务是否成功都会扣费
deduct_result = users_collection.update_one(
{'_id': ObjectId(user_id)},
{'$inc': {'萌芽币': -AI_COST}}
)
if deduct_result.modified_count < 1:
print(f"警告: 用户 {user_id} 萌芽币扣除失败")
# 为请求添加用户信息,以便在函数内部使用
request.current_user = {
'user_id': user_id,
'username': user.get('用户名', ''),
'email': user.get('邮箱', '')
}
# 保存API调用类型
api_type = request.path.split('/')[-1]
# 添加使用记录
usage_record = {
'api_type': api_type,
'timestamp': datetime.now().isoformat(),
'cost': AI_COST
}
# 更新用户的AI使用历史记录
users_collection.update_one(
{'_id': ObjectId(user_id)},
{'$push': {'ai_usage_history': usage_record}}
)
# 调用原函数
result = f(*args, **kwargs)
return result
except Exception as e:
print(f"验证萌芽币时发生错误: {str(e)}")
return jsonify({
'success': False,
'message': '处理请求时出错',
'error': str(e)
}), 500
return decorated
#加载AI配置文件
def load_ai_config():
"""加载AI配置文件"""
try:
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'ai_config.json')
with open(config_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"加载AI配置失败: {e}")
return None
#调用DeepSeek API带重试机制
def call_deepseek_api(messages, model="deepseek-chat", max_retries=3):
"""调用DeepSeek API带重试机制"""
config = load_ai_config()
if not config or 'deepseek' not in config:
return None, "AI配置加载失败"
deepseek_config = config['deepseek']
headers = {
'Authorization': f'Bearer {deepseek_config["api_key"]}',
'Content-Type': 'application/json'
}
data = {
'model': model,
'messages': messages,
'temperature': 0.7,
'max_tokens': 2000
}
import time
for attempt in range(max_retries):
try:
# 增加超时时间到90秒
response = requests.post(
f"{deepseek_config['api_base']}/chat/completions",
headers=headers,
json=data,
timeout=90
)
if response.status_code == 200:
result = response.json()
return result['choices'][0]['message']['content'], None
else:
error_msg = f"API调用失败: {response.status_code} - {response.text}"
if attempt < max_retries - 1:
print(f"{attempt + 1}次尝试失败,等待重试: {error_msg}")
time.sleep(2 ** attempt) # 指数退避
continue
return None, error_msg
except requests.exceptions.Timeout:
error_msg = "API请求超时"
if attempt < max_retries - 1:
print(f"{attempt + 1}次尝试超时,等待重试")
time.sleep(2 ** attempt) # 指数退避
continue
return None, f"{error_msg}(已重试{max_retries}次)"
except Exception as e:
error_msg = f"API调用异常: {str(e)}"
if attempt < max_retries - 1:
print(f"{attempt + 1}次尝试异常,等待重试: {error_msg}")
time.sleep(2 ** attempt) # 指数退避
continue
return None, f"{error_msg}(已重试{max_retries}次)"
#调用Kimi API
def call_kimi_api(messages, model="kimi-k2-0905-preview"):
"""调用Kimi API"""
config = load_ai_config()
if not config or 'kimi' not in config:
return None, "AI配置加载失败"
kimi_config = config['kimi']
headers = {
'Authorization': f'Bearer {kimi_config["api_key"]}',
'Content-Type': 'application/json'
}
data = {
'model': model,
'messages': messages,
'temperature': 0.7,
'max_tokens': 2000
}
try:
response = requests.post(
f"{kimi_config['api_base']}/v1/chat/completions",
headers=headers,
json=data,
timeout=30
)
if response.status_code == 200:
result = response.json()
return result['choices'][0]['message']['content'], None
else:
return None, f"API调用失败: {response.status_code} - {response.text}"
except Exception as e:
return None, f"API调用异常: {str(e)}"
#统一的AI聊天接口
@aimodelapp_bp.route('/chat', methods=['POST'])
@verify_user_coins
def ai_chat():
"""统一的AI聊天接口"""
try:
data = request.get_json()
if not data:
return jsonify({'error': '请求数据为空'}), 400
# 获取请求参数
messages = data.get('messages', [])
model_provider = data.get('provider', 'deepseek') # 默认使用deepseek
model_name = data.get('model', 'deepseek-chat') # 默认模型
if not messages:
return jsonify({'error': '消息内容不能为空'}), 400
# 根据提供商调用对应的API
if model_provider == 'deepseek':
content, error = call_deepseek_api(messages, model_name)
elif model_provider == 'kimi':
content, error = call_kimi_api(messages, model_name)
else:
return jsonify({'error': f'不支持的AI提供商: {model_provider}'}), 400
if error:
return jsonify({'error': error}), 500
return jsonify({
'success': True,
'content': content,
'provider': model_provider,
'model': model_name,
'timestamp': datetime.now().isoformat()
})
except Exception as e:
return jsonify({'error': f'服务器错误: {str(e)}'}), 500
#姓名分析专用接口
@aimodelapp_bp.route('/name-analysis', methods=['POST'])
@verify_user_coins
def name_analysis():
"""姓名分析专用接口"""
try:
data = request.get_json()
name = data.get('name', '').strip()
if not name:
return jsonify({'error': '姓名不能为空'}), 400
# 构建姓名分析的专业提示词
prompt = f"""你是一位专业的姓名学专家和语言学家,请对输入的姓名进行全面分析。请直接输出分析结果,不要包含任何思考过程或<think>标签。
姓名:{name}
请按照以下格式严格输出分析结果:
【稀有度评分】
评分X%
评价:[对稀有度的详细说明,包括姓氏和名字的常见程度分析]
【音韵评价】
评分X%
评价:[对音韵美感的分析,包括声调搭配、读音流畅度、音律和谐度等]
【含义解读】
[详细分析姓名的寓意内涵,包括:
1. 姓氏的历史渊源和文化背景
2. 名字各字的含义和象征
3. 整体姓名的寓意组合
4. 可能体现的父母期望或文化内涵
5. 与传统文化、诗词典故的关联等]
要求:
1. 评分必须是1-100的整数百分比要有明显区分度避免雷同
2. 分析要专业、客观、有依据,评分要根据实际情况有所差异
3. 含义解读要详细深入至少150字
4. 严格按照上述格式输出,不要添加思考过程、<think>标签或其他内容
5. 如果是生僻字或罕见姓名,要特别说明
6. 直接输出最终结果,不要显示推理过程"""
messages = [
{"role": "user", "content": prompt}
]
# 使用DeepSeek进行分析
content, error = call_deepseek_api(messages)
if error:
return jsonify({'error': error}), 500
return jsonify({
'success': True,
'analysis': content,
'name': name,
'timestamp': datetime.now().isoformat()
})
except Exception as e:
return jsonify({'error': f'姓名分析失败: {str(e)}'}), 500
#变量命名助手接口
@aimodelapp_bp.route('/variable-naming', methods=['POST'])
@verify_user_coins
def variable_naming():
"""变量命名助手接口"""
try:
data = request.get_json()
description = data.get('description', '').strip()
language = data.get('language', 'javascript').lower()
if not description:
return jsonify({'error': '变量描述不能为空'}), 400
# 构建变量命名的提示词
prompt = f"""你是一个专业的变量命名助手。请根据以下描述为变量生成合适的名称:
描述:{description}
请为每种命名规范生成3个变量名建议
1. camelCase (驼峰命名法)
2. PascalCase (帕斯卡命名法)
3. snake_case (下划线命名法)
4. kebab-case (短横线命名法)
5. CONSTANT_CASE (常量命名法)
要求:
- 变量名要准确反映功能和用途
- 严格遵循各自的命名规范
- 避免使用缩写,除非是广泛认知的缩写
- 名称要简洁但具有描述性
- 考虑代码的可读性和维护性
请按以下JSON格式返回
{{
"suggestions": {{
"camelCase": [
{{"name": "变量名1", "description": "解释说明1"}},
{{"name": "变量名2", "description": "解释说明2"}},
{{"name": "变量名3", "description": "解释说明3"}}
],
"PascalCase": [
{{"name": "变量名1", "description": "解释说明1"}},
{{"name": "变量名2", "description": "解释说明2"}},
{{"name": "变量名3", "description": "解释说明3"}}
],
"snake_case": [
{{"name": "变量名1", "description": "解释说明1"}},
{{"name": "变量名2", "description": "解释说明2"}},
{{"name": "变量名3", "description": "解释说明3"}}
],
"kebab-case": [
{{"name": "变量名1", "description": "解释说明1"}},
{{"name": "变量名2", "description": "解释说明2"}},
{{"name": "变量名3", "description": "解释说明3"}}
],
"CONSTANT_CASE": [
{{"name": "变量名1", "description": "解释说明1"}},
{{"name": "变量名2", "description": "解释说明2"}},
{{"name": "变量名3", "description": "解释说明3"}}
]
}}
}}
只返回JSON格式的结果不要包含其他文字。"""
messages = [
{"role": "user", "content": prompt}
]
# 使用DeepSeek进行分析
content, error = call_deepseek_api(messages)
if error:
return jsonify({'error': error}), 500
# 解析AI返回的JSON格式数据
try:
# 尝试直接解析JSON
ai_response = json.loads(content)
suggestions = ai_response.get('suggestions', {})
except json.JSONDecodeError:
# 如果直接解析失败尝试提取JSON部分
import re
json_match = re.search(r'\{[\s\S]*\}', content)
if json_match:
try:
ai_response = json.loads(json_match.group())
suggestions = ai_response.get('suggestions', {})
except json.JSONDecodeError:
return jsonify({'error': 'AI返回的数据格式无法解析'}), 500
else:
return jsonify({'error': 'AI返回的数据中未找到有效的JSON格式'}), 500
return jsonify({
'success': True,
'suggestions': suggestions,
'description': description,
'language': language,
'timestamp': datetime.now().isoformat()
})
except Exception as e:
return jsonify({'error': f'变量命名失败: {str(e)}'}), 500
#AI写诗助手接口
@aimodelapp_bp.route('/poetry', methods=['POST'])
@verify_user_coins
def poetry_assistant():
"""AI写诗助手接口"""
try:
data = request.get_json()
theme = data.get('theme', '').strip()
style = data.get('style', '现代诗').strip()
mood = data.get('mood', '').strip()
if not theme:
return jsonify({'error': '诗歌主题不能为空'}), 400
# 构建写诗的提示词
prompt = f"""你是一位才华横溢的诗人,请根据以下要求创作一首诗歌。
主题:{theme}
风格:{style}
情感基调:{mood if mood else '自由发挥'}
创作要求:
1. 紧扣主题,情感真挚
2. 语言优美,意境深远
3. 符合指定的诗歌风格
4. 长度适中,朗朗上口
5. 如果是古体诗,注意平仄和韵律
请直接输出诗歌作品,不需要额外的解释或分析。"""
messages = [
{"role": "user", "content": prompt}
]
# 使用DeepSeek进行创作
content, error = call_deepseek_api(messages)
if error:
return jsonify({'error': error}), 500
return jsonify({
'success': True,
'poem': content,
'theme': theme,
'style': style,
'mood': mood,
'timestamp': datetime.now().isoformat()
})
except Exception as e:
return jsonify({'error': f'诗歌创作失败: {str(e)}'}), 500
#AI语言翻译接口
@aimodelapp_bp.route('/translation', methods=['POST'])
@verify_user_coins
def translation():
"""AI语言翻译接口"""
try:
data = request.get_json()
source_text = data.get('source_text', '').strip()
target_language = data.get('target_language', 'zh-CN').strip()
if not source_text:
return jsonify({'error': '翻译内容不能为空'}), 400
# 语言映射
language_map = {
'zh-CN': '中文(简体)',
'zh-TW': '中文(繁体)',
'en': '英语',
'ja': '日语',
'ko': '韩语',
'fr': '法语',
'de': '德语',
'es': '西班牙语',
'it': '意大利语',
'pt': '葡萄牙语',
'ru': '俄语',
'ar': '阿拉伯语',
'hi': '印地语',
'th': '泰语',
'vi': '越南语'
}
target_language_name = language_map.get(target_language, target_language)
# 构建翻译的专业提示词
prompt = f"""你是一位专业的翻译专家,精通多种语言的翻译工作。请将以下文本翻译成{target_language_name}
原文:{source_text}
翻译要求:
1. 【信】- 忠实原文,准确传达原意,不遗漏、不添加、不歪曲
2. 【达】- 译文通顺流畅,符合目标语言的表达习惯和语法规范
3. 【雅】- 用词优美得体,风格与原文相符,具有良好的可读性
特别注意:
- 自动检测源语言,无需用户指定
- 保持原文的语气、情感色彩和文体风格
- 对于专业术语,提供准确的对应翻译
- 对于文化特色词汇,在保持原意的基础上进行适当的本土化处理
- 如果是单词或短语,提供多个常用含义的翻译
- 如果是句子,确保语法正确、表达自然
请按以下JSON格式返回翻译结果
{{
"detected_language": "检测到的源语言名称",
"target_language": "{target_language_name}",
"translation": "翻译结果",
"alternative_translations": [
"备选翻译1",
"备选翻译2",
"备选翻译3"
],
"explanation": "翻译说明(包括语境、用法、注意事项等)",
"pronunciation": "目标语言的发音指导(如适用)"
}}
只返回JSON格式的结果不要包含其他文字。"""
messages = [
{"role": "user", "content": prompt}
]
# 使用DeepSeek进行翻译
content, error = call_deepseek_api(messages)
if error:
return jsonify({'error': error}), 500
return jsonify({
'success': True,
'translation_result': content,
'source_text': source_text,
'target_language': target_language,
'timestamp': datetime.now().isoformat()
})
except Exception as e:
return jsonify({'error': f'翻译失败: {str(e)}'}), 500
#现代文转文言文接口
@aimodelapp_bp.route('/classical_conversion', methods=['POST'])
@verify_user_coins
def classical_conversion():
"""现代文转文言文接口"""
try:
data = request.get_json()
modern_text = data.get('modern_text', '').strip()
style = data.get('style', '古雅').strip()
article_type = data.get('article_type', '散文').strip()
if not modern_text:
return jsonify({'error': '现代文内容不能为空'}), 400
# 构建文言文转换的专业提示词
prompt = f"""你是一位精通古代文言文的文学大师,擅长将现代文转换为优美的文言文。请将以下现代文转换为文言文。
现代文:{modern_text}
转换要求:
1. 风格:{style}
2. 文体:{article_type}
3. 保持原文的核心意思和情感色彩
4. 使用恰当的文言文语法和词汇
5. 注重音韵美感和文字的雅致
6. 根据不同风格调整用词和句式
风格说明:
- 古雅:典雅庄重,用词考究,句式工整
- 简洁:言简意赅,删繁就简,朴实无华
- 华丽:辞藻华美,对仗工整,音韵和谐
- 朴实:平实自然,通俗易懂,贴近生活
文体特点:
- 散文:行文自由,情理并茂
- 诗歌:讲究韵律,意境深远
- 议论文:逻辑严密,论证有力
- 记叙文:叙事生动,描写细腻
- 书信:格式规范,情真意切
- 公文:庄重严肃,用词准确
请按以下JSON格式返回转换结果
{{
"classical_text": "转换后的文言文",
"translation_notes": "转换说明,包括重要词汇的选择理由和语法特点",
"style_analysis": "风格分析,说明如何体现所选风格特点",
"difficulty_level": "难度等级(初级/中级/高级)",
"key_phrases": [
{{
"modern": "现代词汇",
"classical": "对应文言文词汇",
"explanation": "转换说明"
}}
],
"cultural_elements": "文化内涵说明,包含的典故、意象等"
}}
只返回JSON格式的结果不要包含其他文字。"""
messages = [
{"role": "user", "content": prompt}
]
# 使用DeepSeek进行文言文转换
content, error = call_deepseek_api(messages)
if error:
return jsonify({'error': error}), 500
return jsonify({
'success': True,
'conversion_result': content,
'modern_text': modern_text,
'style': style,
'article_type': article_type,
'timestamp': datetime.now().isoformat()
})
except Exception as e:
return jsonify({'error': f'文言文转换失败: {str(e)}'}), 500
#AI表情制作器接口
@aimodelapp_bp.route('/expression-maker', methods=['POST'])
@verify_user_coins
def expression_maker():
"""AI表情制作器接口"""
try:
data = request.get_json()
text = data.get('text', '').strip()
style = data.get('style', 'mixed').strip()
if not text:
return jsonify({'error': '文字内容不能为空'}), 400
# 风格映射
style_prompts = {
'mixed': '混合使用Emoji表情和颜文字',
'emoji': '仅使用Emoji表情符号',
'kaomoji': '仅使用颜文字(日式表情符号)',
'cute': '使用可爱风格的表情符号',
'cool': '使用酷炫风格的表情符号'
}
style_description = style_prompts.get(style, style_prompts['mixed'])
# 构建表情制作的提示词
prompt = f"""你是一个专业的表情符号专家,擅长为文字内容生成合适的表情符号。请根据以下文字内容生成相应的表情符号:
文字内容:{text}
表情风格:{style_description}
请为这个文字内容生成表情符号,要求:
1. 准确表达文字的情感和含义
2. 符合指定的表情风格
3. 提供多样化的选择
4. 包含使用场景说明
请按以下分类生成表情符号:
1. Emoji表情使用Unicode表情符号
2. 颜文字使用ASCII字符组成的表情
3. 组合表情(多个符号组合使用)
每个分类提供5个不同的表情选项每个选项包含
- 表情符号本身
- 适用场景说明
- 情感强度(轻微/中等/强烈)
请按以下JSON格式返回
{{
"expressions": {{
"emoji": [
{{
"symbol": "😊",
"description": "适用场景和情感说明",
"intensity": "中等",
"usage": "使用建议"
}}
],
"kaomoji": [
{{
"symbol": "(^_^)",
"description": "适用场景和情感说明",
"intensity": "轻微",
"usage": "使用建议"
}}
],
"combination": [
{{
"symbol": "🎉✨",
"description": "适用场景和情感说明",
"intensity": "强烈",
"usage": "使用建议"
}}
]
}},
"summary": {{
"emotion_analysis": "对输入文字的情感分析",
"recommended_usage": "推荐的使用场景",
"style_notes": "风格特点说明"
}}
}}
只返回JSON格式的结果不要包含其他文字。"""
messages = [
{"role": "user", "content": prompt}
]
# 使用DeepSeek进行分析
content, error = call_deepseek_api(messages)
if error:
return jsonify({'error': error}), 500
# 解析AI返回的JSON格式数据
try:
# 尝试直接解析JSON
ai_response = json.loads(content)
expressions = ai_response.get('expressions', {})
summary = ai_response.get('summary', {})
except json.JSONDecodeError:
# 如果直接解析失败尝试提取JSON部分
import re
json_match = re.search(r'\{[\s\S]*\}', content)
if json_match:
try:
ai_response = json.loads(json_match.group())
expressions = ai_response.get('expressions', {})
summary = ai_response.get('summary', {})
except json.JSONDecodeError:
return jsonify({'error': 'AI返回的数据格式无法解析'}), 500
else:
return jsonify({'error': 'AI返回的数据中未找到有效的JSON格式'}), 500
return jsonify({
'success': True,
'expressions': expressions,
'summary': summary,
'text': text,
'style': style,
'timestamp': datetime.now().isoformat()
})
except Exception as e:
return jsonify({'error': f'表情制作失败: {str(e)}'}), 500
#Linux命令生成接口
@aimodelapp_bp.route('/linux-command', methods=['POST'])
@verify_user_coins
def linux_command_generator():
"""Linux命令生成接口"""
try:
data = request.get_json()
task_description = data.get('task_description', '').strip()
difficulty_level = data.get('difficulty_level', 'beginner').strip()
if not task_description:
return jsonify({'error': '任务描述不能为空'}), 400
# 构建Linux命令生成的专业提示词
prompt = f"""你是一位Linux系统专家请根据用户的任务描述生成相应的Linux命令。
任务描述:{task_description}
用户水平:{difficulty_level}
请为这个任务生成合适的Linux命令要求
1. 命令准确可用符合Linux标准
2. 根据用户水平提供适当的复杂度
3. 提供多种实现方式(如果有的话)
4. 包含安全提示和注意事项
5. 解释每个命令的作用和参数
用户水平说明:
- beginner初学者提供基础命令详细解释
- intermediate中级提供常用命令和选项
- advanced高级提供高效命令和高级用法
请按以下JSON格式返回
{{
"commands": [
{{
"command": "具体的Linux命令",
"description": "命令的详细说明",
"safety_level": "safe/caution/dangerous",
"explanation": "命令各部分的解释",
"example_output": "预期的命令输出示例",
"alternatives": ["替代命令1", "替代命令2"]
}}
],
"safety_warnings": ["安全提示1", "安全提示2"],
"prerequisites": ["前置条件1", "前置条件2"],
"related_concepts": ["相关概念1", "相关概念2"]
}}
只返回JSON格式的结果不要包含其他文字。"""
messages = [
{"role": "user", "content": prompt}
]
# 使用DeepSeek进行命令生成
content, error = call_deepseek_api(messages)
if error:
return jsonify({'error': error}), 500
return jsonify({
'success': True,
'command_result': content,
'task_description': task_description,
'difficulty_level': difficulty_level,
'timestamp': datetime.now().isoformat()
})
except Exception as e:
return jsonify({'error': f'Linux命令生成失败: {str(e)}'}), 500
#获取用户萌芽币余额
@aimodelapp_bp.route('/coins', methods=['GET'])
def get_user_coins():
"""获取用户萌芽币余额"""
try:
# 获取用户认证信息
token = request.headers.get('Authorization')
if not token:
return jsonify({
'success': False,
'message': '未提供认证信息',
'error_code': 'auth_required'
}), 401
if token.startswith('Bearer '):
token = token[7:]
# 解析JWT token
import jwt
try:
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
user_id = payload['user_id']
except jwt.ExpiredSignatureError:
return jsonify({
'success': False,
'message': 'Token已过期请重新登录',
'error_code': 'token_expired'
}), 401
except Exception as e:
return jsonify({
'success': False,
'message': f'无效的认证信息: {str(e)}',
'error_code': 'invalid_token'
}), 401
# 查询用户萌芽币余额
users_collection = current_app.mongo.db.userdata
user = users_collection.find_one({'_id': ObjectId(user_id)})
if not user:
return jsonify({
'success': False,
'message': '用户不存在',
'error_code': 'user_not_found'
}), 404
# 返回萌芽币信息
current_coins = user.get('萌芽币', 0)
username = user.get('用户名', '用户')
# 增加额外有用信息
ai_usage_history = []
if 'ai_usage_history' in user:
ai_usage_history = user['ai_usage_history'][-5:] # 最近5条使用记录
return jsonify({
'success': True,
'data': {
'coins': current_coins,
'ai_cost': AI_COST,
'can_use_ai': current_coins >= AI_COST,
'username': username,
'usage_count': len(ai_usage_history),
'recent_usage': ai_usage_history
},
'message': f'当前萌芽币余额: {current_coins}'
}), 200
except Exception as e:
return jsonify({
'success': False,
'message': '处理请求时出错',
'error': str(e)
}), 500
#获取可用的AI模型列表
@aimodelapp_bp.route('/models', methods=['GET'])
def get_available_models():
"""获取可用的AI模型列表"""
try:
config = load_ai_config()
if not config:
return jsonify({'error': 'AI配置加载失败'}), 500
models = {}
for provider, provider_config in config.items():
if 'model' in provider_config:
models[provider] = provider_config['model']
return jsonify({
'success': True,
'models': models,
'default_provider': 'deepseek',
'default_model': 'deepseek-chat'
})
except Exception as e:
return jsonify({'error': f'获取模型列表失败: {str(e)}'}), 500

View File

@@ -2,27 +2,75 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
用户认证模块 用户认证模块
Created by: 神奇万事通 Created by: 万象口袋
Date: 2025-09-02 Date: 2025-09-02
""" """
from flask import Blueprint, request, jsonify, session, current_app from flask import Blueprint, request, jsonify, current_app
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
import hashlib import hashlib
import re import re
from datetime import datetime import jwt
from datetime import datetime, timedelta
from functools import wraps
from .email_service import send_verification_email, verify_code, is_qq_email, get_qq_avatar_url from .email_service import send_verification_email, verify_code, is_qq_email, get_qq_avatar_url
auth_bp = Blueprint('auth', __name__) auth_bp = Blueprint('auth', __name__)
#生成JWT token
def generate_token(user_data):
"""生成JWT token"""
payload = {
'user_id': user_data['user_id'],
'email': user_data['email'],
'username': user_data['username'],
'exp': datetime.utcnow() + timedelta(days=7), # 7天过期
'iat': datetime.utcnow()
}
return jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256')
#验证JWT token
def verify_token(token):
"""验证JWT token"""
try:
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
return {'success': True, 'data': payload}
except jwt.ExpiredSignatureError:
return {'success': False, 'message': 'Token已过期'}
except jwt.InvalidTokenError:
return {'success': False, 'message': 'Token无效'}
#JWT token验证装饰器
def token_required(f):
"""JWT token验证装饰器"""
@wraps(f)
def decorated(*args, **kwargs):
token = request.headers.get('Authorization')
if not token:
return jsonify({'success': False, 'message': '缺少认证token'}), 401
if token.startswith('Bearer '):
token = token[7:]
result = verify_token(token)
if not result['success']:
return jsonify({'success': False, 'message': result['message']}), 401
request.current_user = result['data']
return f(*args, **kwargs)
return decorated
#验证QQ邮箱格式
def validate_qq_email(email): def validate_qq_email(email):
"""验证QQ邮箱格式""" """验证QQ邮箱格式"""
return is_qq_email(email) return is_qq_email(email)
#验证密码格式
def validate_password(password): def validate_password(password):
"""验证密码格式6-20位""" """验证密码格式6-20位"""
return 6 <= len(password) <= 20 return 6 <= len(password) <= 20
#发送验证码邮件
@auth_bp.route('/send-verification', methods=['POST']) @auth_bp.route('/send-verification', methods=['POST'])
def send_verification(): def send_verification():
"""发送验证码邮件""" """发送验证码邮件"""
@@ -78,6 +126,7 @@ def send_verification():
'message': '发送失败,请稍后重试' 'message': '发送失败,请稍后重试'
}), 500 }), 500
#验证验证码
@auth_bp.route('/verify-code', methods=['POST']) @auth_bp.route('/verify-code', methods=['POST'])
def verify_verification_code(): def verify_verification_code():
"""验证验证码""" """验证验证码"""
@@ -108,6 +157,7 @@ def verify_verification_code():
'message': '验证失败,请稍后重试' 'message': '验证失败,请稍后重试'
}), 500 }), 500
#用户注册
@auth_bp.route('/register', methods=['POST']) @auth_bp.route('/register', methods=['POST'])
def register(): def register():
"""用户注册(需要先验证邮箱)""" """用户注册(需要先验证邮箱)"""
@@ -176,7 +226,15 @@ def register():
'注册时间': datetime.now().isoformat(), '注册时间': datetime.now().isoformat(),
'最后登录': None, '最后登录': None,
'登录次数': 0, '登录次数': 0,
'用户状态': 'active' '用户状态': 'active',
'等级': 0,
'经验': 0,
'萌芽币': 0,
'签到系统': {
'连续签到天数': 0,
'今日是否已签到': False,
'签到时间': '2025-01-01'
}
} }
result = users_collection.insert_one(user_data) result = users_collection.insert_one(user_data)
@@ -204,42 +262,7 @@ def register():
'message': '注册失败,请稍后重试' 'message': '注册失败,请稍后重试'
}), 500 }), 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']) @auth_bp.route('/login', methods=['POST'])
def login(): def login():
"""用户登录(支持邮箱+验证码或邮箱+密码)""" """用户登录(支持邮箱+验证码或邮箱+密码)"""
@@ -313,15 +336,18 @@ def login():
} }
) )
# 设置会话 # 生成JWT token
session['user_id'] = str(user['_id']) user_data = {
session['email'] = email 'user_id': str(user['_id']),
session['username'] = user.get('用户名', '') 'email': email,
session.permanent = True 'username': user.get('用户名', '')
}
token = generate_token(user_data)
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '登录成功!', 'message': '登录成功!',
'token': token,
'user': { 'user': {
'id': str(user['_id']), 'id': str(user['_id']),
'email': email, 'email': email,
@@ -339,9 +365,10 @@ def login():
}), 500 }), 500
# 登录成功,创建会话 # 登录成功,创建会话
session['user_id'] = str(user['_id']) hwt = getattr(request, 'hwt', {})
session['account'] = user['账号'] hwt['user_id'] = str(user['_id'])
session['logged_in'] = True hwt['account'] = user['账号']
hwt['logged_in'] = True
# 更新登录信息 # 更新登录信息
users_collection.update_one( users_collection.update_one(
@@ -368,21 +395,16 @@ def login():
'message': f'服务器错误: {str(e)}' 'message': f'服务器错误: {str(e)}'
}), 500 }), 500
#用户登出
@auth_bp.route('/logout', methods=['POST']) @auth_bp.route('/logout', methods=['POST'])
def logout(): def logout():
"""用户登出""" """用户登出"""
try: try:
if 'logged_in' in session: # JWT是无状态的客户端删除token即可
session.clear() return jsonify({
return jsonify({ 'success': True,
'success': True, 'message': '已成功登出'
'message': '已成功登出' }), 200
}), 200
else:
return jsonify({
'success': False,
'message': '用户未登录'
}), 401
except Exception as e: except Exception as e:
return jsonify({ return jsonify({
@@ -390,17 +412,31 @@ def logout():
'message': f'服务器错误: {str(e)}' 'message': f'服务器错误: {str(e)}'
}), 500 }), 500
#检查登录状态
@auth_bp.route('/check', methods=['GET']) @auth_bp.route('/check', methods=['GET'])
def check_login(): def check_login():
"""检查登录状态""" """检查登录状态"""
try: try:
if session.get('logged_in'): token = request.headers.get('Authorization')
if not token:
return jsonify({
'success': True,
'logged_in': False
}), 200
if token.startswith('Bearer '):
token = token[7:]
result = verify_token(token)
if result['success']:
user_data = result['data']
return jsonify({ return jsonify({
'success': True, 'success': True,
'logged_in': True, 'logged_in': True,
'user': { 'user': {
'account': session.get('account'), 'id': user_data['user_id'],
'user_id': session.get('user_id') 'email': user_data['email'],
'username': user_data['username']
} }
}), 200 }), 200
else: else:

View File

@@ -18,15 +18,18 @@ import os
# 验证码存储生产环境建议使用Redis # 验证码存储生产环境建议使用Redis
verification_codes = {} verification_codes = {}
# 初始化日志
def init_mail(app): def init_mail(app):
"""初始化邮件配置""" """初始化邮件配置"""
# 使用smtplib直接发送不需要Flask-Mail # 使用smtplib直接发送不需要Flask-Mail
pass pass
# 生成验证码
def generate_verification_code(length=6): def generate_verification_code(length=6):
"""生成验证码""" """生成验证码"""
return ''.join(random.choices(string.digits, k=length)) return ''.join(random.choices(string.digits, k=length))
# 发送验证邮件
def send_verification_email(email, verification_type='register'): def send_verification_email(email, verification_type='register'):
""" """
发送验证邮件 发送验证邮件
@@ -69,7 +72,7 @@ def send_verification_email(email, verification_type='register'):
<body> <body>
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;"> <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="text-align: center; margin-bottom: 30px;"> <div style="text-align: center; margin-bottom: 30px;">
<h1 style="color: #66bb6a; margin: 0;">InfoGenie 神奇万事通</h1> <h1 style="color: #66bb6a; margin: 0;">InfoGenie 万象口袋</h1>
<p style="color: #666; font-size: 14px; margin: 5px 0;">欢迎注册InfoGenie</p> <p style="color: #666; font-size: 14px; margin: 5px 0;">欢迎注册InfoGenie</p>
</div> </div>
@@ -98,7 +101,7 @@ def send_verification_email(email, verification_type='register'):
<body> <body>
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;"> <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="text-align: center; margin-bottom: 30px;"> <div style="text-align: center; margin-bottom: 30px;">
<h1 style="color: #66bb6a; margin: 0;">InfoGenie 神奇万事通</h1> <h1 style="color: #66bb6a; margin: 0;">InfoGenie 万象口袋</h1>
<p style="color: #666; font-size: 14px; margin: 5px 0;">安全登录验证</p> <p style="color: #666; font-size: 14px; margin: 5px 0;">安全登录验证</p>
</div> </div>
@@ -168,6 +171,7 @@ def send_verification_email(email, verification_type='register'):
'message': '邮件发送失败,请稍后重试' 'message': '邮件发送失败,请稍后重试'
} }
# 验证验证码
def verify_code(email, code): def verify_code(email, code):
""" """
验证验证码 验证验证码
@@ -221,6 +225,7 @@ def verify_code(email, code):
'type': verification_type 'type': verification_type
} }
# 验证QQ邮箱格式
def is_qq_email(email): def is_qq_email(email):
""" """
验证是否为QQ邮箱 验证是否为QQ邮箱
@@ -239,6 +244,7 @@ def is_qq_email(email):
return domain in qq_domains return domain in qq_domains
# 获取QQ头像URL
def get_qq_avatar_url(email): def get_qq_avatar_url(email):
""" """
根据QQ邮箱获取QQ头像URL 根据QQ邮箱获取QQ头像URL
@@ -262,6 +268,7 @@ def get_qq_avatar_url(email):
# 返回QQ头像API URL # 返回QQ头像API URL
return f"http://q1.qlogo.cn/g?b=qq&nk={qq_number}&s=100" return f"http://q1.qlogo.cn/g?b=qq&nk={qq_number}&s=100"
# 清理过期验证码
def cleanup_expired_codes(): def cleanup_expired_codes():
"""清理过期的验证码""" """清理过期的验证码"""
current_time = datetime.now() current_time = datetime.now()

View File

@@ -0,0 +1,408 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
用户管理模块
Created by: 万象口袋
Date: 2025-09-02
"""
from flask import Blueprint, request, jsonify, current_app
from datetime import datetime
from bson import ObjectId
import jwt
from functools import wraps
user_bp = Blueprint('user', __name__)
# 验证JWT token
def verify_token(token):
"""验证JWT token"""
try:
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
return {'success': True, 'data': payload}
except jwt.ExpiredSignatureError:
return {'success': False, 'message': 'Token已过期'}
except jwt.InvalidTokenError:
return {'success': False, 'message': 'Token无效'}
# 登录验证装饰器支持JWT token和hwt
def login_required(f):
"""登录验证装饰器支持JWT token和hwt"""
@wraps(f)
def decorated_function(*args, **kwargs):
# 优先检查JWT token
token = request.headers.get('Authorization')
if token:
if token.startswith('Bearer '):
token = token[7:]
result = verify_token(token)
if result['success']:
request.current_user = result['data']
return f(*args, **kwargs)
# 回退到hwt验证
hwt = getattr(request, 'hwt', {})
if not hwt.get('logged_in'):
return jsonify({
'success': False,
'message': '请先登录'
}), 401
return f(*args, **kwargs)
return decorated_function
return decorated_function
# 获取用户资料
@user_bp.route('/profile', methods=['GET'])
@login_required
def get_profile():
"""获取用户资料"""
try:
hwt = getattr(request, 'hwt', {})
user_id = hwt.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
hwt = getattr(request, 'hwt', {})
user_id = hwt.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:
hwt = getattr(request, 'hwt', {})
user_id = hwt.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('/game-data', methods=['GET'])
@login_required
def get_user_game_data():
"""获取用户游戏数据"""
try:
# 优先从JWT token获取用户ID
if hasattr(request, 'current_user'):
user_id = request.current_user['user_id']
else:
hwt = getattr(request, 'hwt', {})
user_id = hwt.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
# 返回用户游戏数据
game_data = {
'level': user.get('等级', 0),
'experience': user.get('经验', 0),
'coins': user.get('萌芽币', 0),
'checkin_system': user.get('签到系统', {
'连续签到天数': 0,
'今日是否已签到': False,
'签到时间': '2025-01-01'
})
}
return jsonify({
'success': True,
'data': game_data
}), 200
except Exception as e:
return jsonify({
'success': False,
'message': f'服务器错误: {str(e)}'
}), 500
# 每日签到
@user_bp.route('/checkin', methods=['POST'])
@login_required
def daily_checkin():
"""每日签到"""
try:
# 优先从JWT token获取用户ID
if hasattr(request, 'current_user'):
user_id = request.current_user['user_id']
else:
hwt = getattr(request, 'hwt', {})
user_id = hwt.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
# 获取当前日期
today = datetime.now().strftime('%Y-%m-%d')
# 获取签到系统数据
checkin_system = user.get('签到系统', {
'连续签到天数': 0,
'今日是否已签到': False,
'签到时间': '2025-01-01'
})
# 检查今日是否已签到
if checkin_system.get('今日是否已签到', False) and checkin_system.get('签到时间') == today:
return jsonify({
'success': False,
'message': '今日已签到,请明天再来!'
}), 400
# 计算连续签到天数
last_checkin_date = checkin_system.get('签到时间', '2025-01-01')
consecutive_days = checkin_system.get('连续签到天数', 0)
# 检查是否连续签到
if last_checkin_date:
try:
last_date = datetime.strptime(last_checkin_date, '%Y-%m-%d')
today_date = datetime.strptime(today, '%Y-%m-%d')
days_diff = (today_date - last_date).days
if days_diff == 1:
# 连续签到
consecutive_days += 1
elif days_diff > 1:
# 断签,重新开始
consecutive_days = 1
else:
# 同一天,不应该发生
consecutive_days = consecutive_days
except:
consecutive_days = 1
else:
consecutive_days = 1
# 签到奖励
coin_reward = 300
exp_reward = 200
# 获取当前用户数据
current_coins = user.get('萌芽币', 0)
current_exp = user.get('经验', 0)
current_level = user.get('等级', 0)
# 计算新的经验和等级
new_exp = current_exp + exp_reward
new_level = current_level
# 等级升级逻辑100 × 1.2^(等级)
while True:
exp_needed = int(100 * (1.2 ** new_level))
if new_exp >= exp_needed:
new_exp -= exp_needed
new_level += 1
else:
break
# 更新用户数据
update_data = {
'萌芽币': current_coins + coin_reward,
'经验': new_exp,
'等级': new_level,
'签到系统': {
'连续签到天数': consecutive_days,
'今日是否已签到': True,
'签到时间': today
}
}
result = users_collection.update_one(
{'_id': ObjectId(user_id)},
{'$set': update_data}
)
if result.modified_count > 0:
level_up = new_level > current_level
return jsonify({
'success': True,
'message': '签到成功!',
'data': {
'coin_reward': coin_reward,
'exp_reward': exp_reward,
'consecutive_days': consecutive_days,
'level_up': level_up,
'new_level': new_level,
'new_coins': current_coins + coin_reward,
'new_exp': new_exp
}
}), 200
else:
return jsonify({
'success': False,
'message': '签到失败,请稍后重试'
}), 500
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
hwt = getattr(request, 'hwt', {})
user_id = hwt.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:
# 清除会话
hwt = getattr(request, 'hwt', {})
hwt.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

View File

@@ -10,6 +10,9 @@ pymongo==4.5.0
# 密码加密 # 密码加密
Werkzeug==2.3.7 Werkzeug==2.3.7
# JWT认证
PyJWT==2.8.0
# HTTP请求 # HTTP请求
requests==2.31.0 requests==2.31.0

View File

@@ -0,0 +1,2 @@
@echo off
python app.py

View File

@@ -0,0 +1,2 @@
#!/bin/bash
python3 app.py

View File

View File

View File

View File

View File

@@ -0,0 +1,166 @@
# InfoGenie后端项目专业技术总结
## 项目架构概述
InfoGenie后端采用了**模块化、松耦合**的设计理念基于Flask框架构建RESTful API服务实现了前后端完全分离的现代Web应用架构。整体架构遵循了**单一职责原则**和**关注点分离原则**各模块独立封装通过清晰定义的API接口进行交互。
## 核心技术栈
### 基础框架
- **Web框架**: Flask 2.3.3(轻量、灵活、可扩展)
- **API设计**: RESTful架构资源导向、无状态通信
- **数据库**: MongoDB适用于文档型数据存储通过Flask-PyMongo 2.3.0集成)
- **认证机制**: JWT TokenPyJWT 2.8.0支持7天有效期
### 中间件与辅助工具
- **CORS支持**: Flask-CORS 4.0.0(解决跨域资源共享问题)
- **密码安全**: Werkzeug 2.3.7(提供高强度密码哈希功能)
- **邮件服务**: 基于SMTP协议的邮件发送使用smtplib直接实现无依赖Flask-Mail
- **环境配置**: python-dotenv 1.0.0(分离配置与代码,增强安全性)
- **API限流**: Flask-Limiter 3.5.0防止API滥用提高系统稳定性
## 架构设计亮点
### 1. 应用工厂模式
项目采用**应用工厂模式**Factory Pattern创建Flask应用实例便于测试和多环境部署
```python
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
# 初始化各种扩展和注册蓝图
return app
```
### 2. 蓝图模块化设计
采用Flask蓝图Blueprint实现功能模块化提高代码复用性和可维护性
- `auth_bp`: 用户认证模块
- `user_bp`: 用户管理模块
- `aimodelapp_bp`: AI模型应用模块
### 3. 装饰器模式
大量使用装饰器模式实现横切关注点Cross-cutting Concerns如认证、权限验证、萌芽币消费等
```python
@verify_user_coins
def ai_function_endpoint():
# 业务逻辑
```
### 4. 统一响应格式
实现了一致的API响应格式便于前端处理
```json
{
"success": true|false,
"data": {},
"message": "操作信息",
"timestamp": "ISO格式时间戳"
}
```
## 安全设计分析
### 1. 多层次认证体系
- **JWT Token认证**: 无状态认证机制,适合分布式部署
- **验证码邮箱认证**: 双因素认证提高安全性
- **QQ邮箱格式验证**: 限制注册邮箱类型,减少垃圾注册
### 2. 数据安全措施
- **密码哈希存储**: 使用Werkzeug提供的高强度哈希算法
- **敏感配置外部化**: 通过环境变量注入敏感配置
- **路径遍历防护**: 静态文件服务实现了路径限制检查
```python
if not os.path.commonpath([base_directory, full_path]) == base_directory:
return jsonify({'error': '非法文件路径'}), 403
```
### 3. 请求安全控制
- **API限流**: 防止暴力攻击和资源耗尽
- **CORS限制**: 生产环境可配置严格的跨域策略
- **请求参数验证**: 严格验证所有客户端输入
## 业务模块分析
### 1. 认证模块auth.py
实现了基于JWT的无状态认证系统通过邮箱验证码进行用户身份确认支持注册、登录和会话管理。设计重点包括
- 验证码5分钟有效期机制
- JWT token 7天有效期管理
- 认证装饰器实现代码复用
### 2. 用户管理模块user_management.py
负责用户资料、签到系统、萌芽币管理等核心业务功能,实现了:
- 用户资料CRUD操作
- 每日签到奖励系统(经验值和萌芽币)
- 用户等级动态计算逻辑
### 3. AI模型应用模块aimodelapp.py
集成多种AI服务DeepSeek、Kimi并实现统一接口调用特点
- 萌芽币消费装饰器模式每次调用消耗100萌芽币
- AI调用带重试机制提高系统稳定性
- 多模型提供商支持(提高可用性和容错性)
### 4. 邮件服务模块email_service.py
负责验证码邮件发送、QQ邮箱格式验证等功能特点
- 直接使用smtplib实现减少依赖
- HTML格式邮件模板支持
- 验证码管理机制内存存储生产环境建议使用Redis
## 数据库设计
采用MongoDB文档型数据库主要集合为`userdata`存储用户相关所有数据。MongoDB的选择优势
- **灵活的数据结构**: 适合存储复杂且不断演化的用户数据
- **文档自包含**: 减少关联查询,提高读取性能
- **水平扩展能力**: 支持未来系统规模扩展需求
用户数据模型设计合理,包含核心字段:
```json
{
"邮箱": "user@qq.com",
"用户名": "用户名",
"密码": "哈希密码",
"头像": "QQ头像URL",
"注册时间": "ISO时间格式",
"萌芽币": 1500,
"签到系统": {
"连续签到天数": 7,
"今日是否已签到": true
}
}
```
## 部署与运维
### 多环境配置支持
实现了开发、测试和生产环境的配置分离:
```python
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'testing': TestingConfig,
'default': DevelopmentConfig
}
```
### Docker化部署
提供了完整的Docker化部署方案
- Dockerfile定义应用容器
- docker-compose.yml配置多容器协作
- 支持环境变量注入敏感配置
## 技术亮点与优化空间
### 亮点
1. **模块化设计**: 通过Flask蓝图实现功能解耦
2. **装饰器封装**: 横切关注点(cross-cutting concerns)集中处理
3. **统一错误处理**: 全局一致的错误响应机制
4. **AI服务抽象**: 屏蔽不同AI提供商的实现差异
### 优化空间
1. **缓存机制**: 可引入Redis缓存验证码、热点数据等
2. **异步处理**: 邮件发送、AI调用等耗时操作可改为异步执行
3. **日志系统**: 增强日志记录和监控能力
4. **单元测试**: 增加自动化测试覆盖率
## 结论
InfoGenie后端项目展现了良好的软件工程实践采用模块化设计、RESTful API架构和多层次安全控制构建了一个可扩展、可维护的后端系统。该项目不仅满足了当前的业务需求还为未来功能扩展和性能优化预留了空间。
特别是在AI功能集成方面通过抽象接口和装饰器模式实现了业务逻辑与技术实现的分离体现了良好的软件设计原则。萌芽币消费系统的实现也展示了面向业务模型的领域设计能力。

View File

@@ -0,0 +1,13 @@
{
"账号":"3205788256",
"邮箱":"3205788256@qq.com",
"密码":"0123456789",
"等级":0,
"经验":0,
"萌芽币":0,
"签到系统":{
"连续签到天数":0,
"今日是否已签到":false,
"签到时间":"2025-01-01"
}
}

View File

@@ -0,0 +1,4 @@
@echo off
npm run build
npx serve -s build
pause

5
InfoGenie-frontend/env.backup Executable file
View File

@@ -0,0 +1,5 @@
# 生产环境API配置
REACT_APP_API_URL=https://infogenie.api.shumengya.top
# 生产环境API配置
REACT_APP_API_URL=http://127.0.0.1:5002

View File

View File

@@ -1,11 +1,12 @@
{ {
"name": "infogenie-frontend", "name": "infogenie-frontend",
"version": "1.0.0", "version": "1.0.0",
"description": "✨ 神奇万事通 - 前端React应用", "description": "✨ 万象口袋 - 前端React应用",
"keywords": ["react", "api", "mobile-first", "responsive"], "keywords": ["react", "api", "mobile-first", "responsive"],
"author": "神奇万事通", "author": "万象口袋",
"license": "MIT", "license": "MIT",
"private": true, "private": true,
"homepage": "/",
"dependencies": { "dependencies": {
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
@@ -44,6 +45,5 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
}, }
"proxy": "http://localhost:5000"
} }

View File

@@ -4,11 +4,13 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>随机JavaScript趣味题</title> <title>随机JavaScript趣味题</title>
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
<link rel="dns-prefetch" href="https://60s.api.shumengya.top">
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/background.css"> <link rel="stylesheet" href="css/background.css">
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.9.0/styles/github.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<script src="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script>
</head> </head>
<body> <body>
<div class="container"> <div class="container">

View File

@@ -2,19 +2,17 @@
class JSQuizApp { class JSQuizApp {
constructor() { constructor() {
this.apiEndpoints = [ this.apiEndpoints = [
'https://60s-cf.viki.moe', 'https://60s.api.shumengya.top',
'https://60s.viki.moe',
'https://60s.b23.run',
'https://60s.114128.xyz',
'https://60s-cf.114128.xyz'
]; ];
this.currentApiIndex = 0; this.currentApiIndex = 0;
this.currentQuestion = null; this.currentQuestion = null;
this.selectedOption = null; this.selectedOption = null;
this.isAnswered = false; this.isAnswered = false;
this.loadStartTime = null;
this.initElements(); this.initElements();
this.bindEvents(); this.bindEvents();
this.preloadResources();
this.loadQuestion(); this.loadQuestion();
} }
@@ -41,6 +39,17 @@ class JSQuizApp {
}; };
} }
// 预加载资源
preloadResources() {
// 预连接API服务器
this.apiEndpoints.forEach(endpoint => {
const link = document.createElement('link');
link.rel = 'preconnect';
link.href = endpoint;
document.head.appendChild(link);
});
}
// 绑定事件 // 绑定事件
bindEvents() { bindEvents() {
this.elements.submitBtn.addEventListener('click', () => this.submitAnswer()); this.elements.submitBtn.addEventListener('click', () => this.submitAnswer());
@@ -84,6 +93,7 @@ class JSQuizApp {
// 加载题目 // 加载题目
async loadQuestion() { async loadQuestion() {
this.loadStartTime = Date.now();
this.showLoading(); this.showLoading();
this.resetQuestion(); this.resetQuestion();
@@ -92,15 +102,20 @@ class JSQuizApp {
while (attempts < maxAttempts) { while (attempts < maxAttempts) {
try { try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch(this.getCurrentApiUrl(), { const response = await fetch(this.getCurrentApiUrl(), {
method: 'GET', method: 'GET',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
timeout: 10000 signal: controller.signal
}); });
clearTimeout(timeoutId);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`); throw new Error(`HTTP ${response.status}: ${response.statusText}`);
} }
@@ -109,6 +124,8 @@ class JSQuizApp {
if (data.code === 200 && data.data) { if (data.code === 200 && data.data) {
this.currentQuestion = data.data; this.currentQuestion = data.data;
const loadTime = Date.now() - this.loadStartTime;
console.log(`题目加载完成,耗时: ${loadTime}ms`);
this.displayQuestion(); this.displayQuestion();
return; return;
} else { } else {

View File

@@ -17,8 +17,8 @@ class KFCGenerator {
// 加载API接口列表 // 加载API接口列表
async loadApiEndpoints() { async loadApiEndpoints() {
try { try {
const response = await fetch('./接口集合.json'); // 直接硬编码API端点避免CORS问题
this.apiEndpoints = await response.json(); this.apiEndpoints = ["https://60s.api.shumengya.top"];
} catch (error) { } catch (error) {
console.error('加载API接口列表失败:', error); console.error('加载API接口列表失败:', error);
this.showToast('加载接口配置失败', 'error'); this.showToast('加载接口配置失败', 'error');

View File

@@ -0,0 +1,8 @@
{
"code": 200,
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841",
"data": {
"index": 78,
"kfc": "我叫夯大力 立冬给我准备了糖炒栗子了没有 没准备的自动绝交 再 v 我 50 吃疯狂星期四 然后再给我点杯奶茶 再给我两万块钱 懂吗你们"
}
}

View File

@@ -4,11 +4,7 @@ class HitokotoApp {
constructor() { constructor() {
// API接口列表 // API接口列表
this.apiEndpoints = [ this.apiEndpoints = [
"https://60s-cf.viki.moe", "https://60s.api.shumengya.top"
"https://60s.viki.moe",
"https://60s.b23.run",
"https://60s.114128.xyz",
"https://60s-cf.114128.xyz"
]; ];
this.currentEndpointIndex = 0; this.currentEndpointIndex = 0;
@@ -117,7 +113,8 @@ class HitokotoApp {
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时 const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
try { try {
const response = await fetch(`${endpoint}/v2/hitokoto?encoding=text`, { // 移除URL中的encoding=text参数确保返回JSON格式
const response = await fetch(`${endpoint}/v2/hitokoto`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',

View File

@@ -0,0 +1 @@
["https://60s.api.shumengya.top"]

View File

@@ -0,0 +1 @@
["https://60s.api.shumengya.top"]

View File

@@ -17,7 +17,7 @@
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/changya`); this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/changya`);
} catch (e) { } catch (e) {
// 如果无法加载接口集合,使用默认接口 // 如果无法加载接口集合,使用默认接口
this.endpoints = ['https://60s.viki.moe/v2/changya']; this.endpoints = ['https://60s.api.shumengya.top/v2/changya'];
} }
}, },
// 获取当前接口URL // 获取当前接口URL

View File

@@ -8,7 +8,7 @@ document.addEventListener('DOMContentLoaded', () => {
const jokeCard = document.getElementById('joke-card'); const jokeCard = document.getElementById('joke-card');
// API // API
const apiBaseUrls = ["https://60s.api.shumengya.top", "https://60s-cf.viki.moe", "https://60s.viki.moe"]; const apiBaseUrls = ["https://60s.api.shumengya.top"];
const apiPath = "/v2/duanzi"; const apiPath = "/v2/duanzi";
let currentApiIndex = 0; let currentApiIndex = 0;

View File

@@ -0,0 +1 @@
["https://60s.api.shumengya.top"]

View File

@@ -14,11 +14,6 @@ document.addEventListener('DOMContentLoaded', () => {
const apiBaseUrls = [ const apiBaseUrls = [
"https://60s.api.shumengya.top", "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"
]; ];
const apiPath = "/v2/luck"; const apiPath = "/v2/luck";

View File

@@ -13,7 +13,7 @@
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/epic`); this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/epic`);
} catch (e) { } catch (e) {
// 如果无法加载接口集合,使用默认接口 // 如果无法加载接口集合,使用默认接口
this.endpoints = ['https://60s-api.viki.moe/v2/epic']; this.endpoints = ['https://60s.api.shumengya.top/v2/epic'];
} }
}, },
// 获取当前接口URL // 获取当前接口URL

View File

@@ -0,0 +1,233 @@
/* 动态背景样式 */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
z-index: -2;
}
body::after {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
z-index: -1;
animation: backgroundMove 20s ease-in-out infinite;
}
@keyframes backgroundMove {
0%, 100% {
background:
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
}
25% {
background:
radial-gradient(circle at 60% 30%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 30% 70%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
}
50% {
background:
radial-gradient(circle at 80% 60%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 20% 30%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 60% 70%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
}
75% {
background:
radial-gradient(circle at 40% 90%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 70% 10%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 20% 60%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
}
}
/* 浮动粒子效果 */
.particles {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: -1;
}
.particle {
position: absolute;
width: 4px;
height: 4px;
background: rgba(255, 255, 255, 0.5);
border-radius: 50%;
animation: float 15s infinite linear;
}
.particle:nth-child(1) {
left: 10%;
animation-delay: 0s;
animation-duration: 12s;
}
.particle:nth-child(2) {
left: 20%;
animation-delay: 2s;
animation-duration: 18s;
}
.particle:nth-child(3) {
left: 30%;
animation-delay: 4s;
animation-duration: 15s;
}
.particle:nth-child(4) {
left: 40%;
animation-delay: 6s;
animation-duration: 20s;
}
.particle:nth-child(5) {
left: 50%;
animation-delay: 8s;
animation-duration: 14s;
}
.particle:nth-child(6) {
left: 60%;
animation-delay: 10s;
animation-duration: 16s;
}
.particle:nth-child(7) {
left: 70%;
animation-delay: 12s;
animation-duration: 22s;
}
.particle:nth-child(8) {
left: 80%;
animation-delay: 14s;
animation-duration: 13s;
}
.particle:nth-child(9) {
left: 90%;
animation-delay: 16s;
animation-duration: 19s;
}
.particle:nth-child(10) {
left: 15%;
animation-delay: 18s;
animation-duration: 17s;
}
@keyframes float {
0% {
transform: translateY(100vh) rotate(0deg);
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
transform: translateY(-100px) rotate(360deg);
opacity: 0;
}
}
/* 网格背景效果 */
.grid-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
linear-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
background-size: 50px 50px;
z-index: -1;
opacity: 0.3;
animation: gridMove 30s linear infinite;
}
@keyframes gridMove {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(50px, 50px);
}
}
/* 光晕效果 */
.glow-effect {
position: fixed;
top: 50%;
left: 50%;
width: 300px;
height: 300px;
background: radial-gradient(circle, rgba(74, 144, 226, 0.2) 0%, transparent 70%);
border-radius: 50%;
transform: translate(-50%, -50%);
z-index: -1;
animation: pulse 4s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.5;
}
50% {
transform: translate(-50%, -50%) scale(1.2);
opacity: 0.8;
}
}
/* 响应式背景调整 */
@media (max-width: 768px) {
.grid-background {
background-size: 30px 30px;
}
.glow-effect {
width: 200px;
height: 200px;
}
.particle {
width: 3px;
height: 3px;
}
}
@media (max-width: 480px) {
.grid-background {
background-size: 20px 20px;
}
.glow-effect {
width: 150px;
height: 150px;
}
.particle {
width: 2px;
height: 2px;
}
}

View File

@@ -0,0 +1,445 @@
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
min-height: 100vh;
overflow-x: hidden;
}
/* 容器样式 */
.container {
min-height: 100vh;
display: flex;
flex-direction: column;
position: relative;
z-index: 1;
}
/* 头部样式 */
.header {
text-align: center;
padding: 3rem 2rem 2rem;
background: linear-gradient(135deg, rgba(74, 144, 226, 0.1), rgba(80, 200, 120, 0.1));
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.header h1 {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(135deg, #4a90e2, #50c878);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header h1 i {
margin-right: 0.5rem;
background: linear-gradient(135deg, #4a90e2, #50c878);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
font-size: 1.1rem;
color: #666;
font-weight: 300;
}
/* 主要内容区域 */
.main-content {
flex: 1;
padding: 2rem;
max-width: 800px;
margin: 0 auto;
width: 100%;
}
/* 查询按钮区域 */
.query-section {
text-align: center;
margin-bottom: 2rem;
}
.query-btn {
background: linear-gradient(135deg, #4a90e2, #50c878);
color: white;
border: none;
padding: 1rem 2rem;
font-size: 1.1rem;
font-weight: 600;
border-radius: 50px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(74, 144, 226, 0.3);
display: inline-flex;
align-items: center;
gap: 0.5rem;
min-width: 200px;
justify-content: center;
}
.query-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(74, 144, 226, 0.4);
background: linear-gradient(135deg, #3a7bc8, #40a868);
}
.query-btn:active {
transform: translateY(0);
}
.query-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
/* 加载动画 */
.loading {
text-align: center;
padding: 2rem;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #4a90e2;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading p {
color: #666;
font-size: 1rem;
}
/* IP信息卡片 */
.ip-info {
animation: fadeInUp 0.6s ease;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.ip-card {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.ip-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 2px solid #f0f0f0;
}
.ip-header i {
font-size: 1.5rem;
color: #4a90e2;
}
.ip-header h2 {
font-size: 1.5rem;
font-weight: 600;
color: #333;
}
.ip-display {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
margin-bottom: 1.5rem;
padding: 1.5rem;
background: linear-gradient(135deg, rgba(74, 144, 226, 0.1), rgba(80, 200, 120, 0.1));
border-radius: 15px;
border: 2px solid rgba(74, 144, 226, 0.2);
}
.ip-address {
font-size: 2rem;
font-weight: 700;
font-family: 'Courier New', monospace;
color: #2c3e50;
background: linear-gradient(135deg, #4a90e2, #50c878);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.copy-btn {
background: #4a90e2;
color: white;
border: none;
padding: 0.5rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 1rem;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.copy-btn:hover {
background: #3a7bc8;
transform: scale(1.1);
}
.ip-details {
display: grid;
gap: 1rem;
}
.detail-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
background: rgba(248, 249, 250, 0.8);
border-radius: 10px;
transition: all 0.3s ease;
}
.detail-item:hover {
background: rgba(74, 144, 226, 0.1);
transform: translateX(5px);
}
.detail-item i {
color: #4a90e2;
width: 20px;
text-align: center;
}
.detail-item .label {
font-weight: 600;
color: #555;
min-width: 80px;
}
.detail-item .value {
color: #333;
font-weight: 500;
}
/* IP地址说明区域 */
.ip-explanation {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.ip-explanation h3 {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
font-size: 1.3rem;
color: #333;
}
.ip-explanation h3 i {
color: #4a90e2;
}
.ip-explanation p {
color: #666;
line-height: 1.8;
margin-bottom: 1.5rem;
}
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.feature-item {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1rem;
background: rgba(248, 249, 250, 0.8);
border-radius: 12px;
transition: all 0.3s ease;
}
.feature-item:hover {
background: rgba(74, 144, 226, 0.1);
transform: translateY(-2px);
}
.feature-item i {
color: #4a90e2;
font-size: 1.5rem;
margin-top: 0.2rem;
}
.feature-item h4 {
font-size: 1rem;
font-weight: 600;
color: #333;
margin-bottom: 0.3rem;
}
.feature-item p {
font-size: 0.9rem;
color: #666;
line-height: 1.5;
margin: 0;
}
/* 错误信息 */
.error-message {
text-align: center;
padding: 2rem;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 99, 99, 0.3);
animation: fadeInUp 0.6s ease;
}
.error-message i {
font-size: 3rem;
color: #ff6b6b;
margin-bottom: 1rem;
}
.error-message p {
color: #666;
font-size: 1.1rem;
margin-bottom: 1.5rem;
}
.retry-btn {
background: #ff6b6b;
color: white;
border: none;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
border-radius: 25px;
cursor: pointer;
transition: all 0.3s ease;
}
.retry-btn:hover {
background: #ff5252;
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
}
/* 页脚 */
.footer {
text-align: center;
padding: 2rem;
background: rgba(248, 249, 250, 0.8);
backdrop-filter: blur(10px);
border-top: 1px solid rgba(255, 255, 255, 0.2);
color: #666;
font-size: 0.9rem;
}
/* 响应式设计 */
@media (max-width: 768px) {
.header {
padding: 2rem 1rem 1.5rem;
}
.header h1 {
font-size: 2rem;
}
.main-content {
padding: 1rem;
}
.ip-card, .ip-explanation {
padding: 1.5rem;
}
.ip-address {
font-size: 1.5rem;
}
.ip-display {
flex-direction: column;
gap: 1rem;
}
.features {
grid-template-columns: 1fr;
}
.detail-item {
flex-direction: column;
align-items: flex-start;
gap: 0.3rem;
}
.detail-item .label {
min-width: auto;
}
}
@media (max-width: 480px) {
.header h1 {
font-size: 1.8rem;
}
.query-btn {
padding: 0.875rem 1.5rem;
font-size: 1rem;
min-width: 180px;
}
.ip-address {
font-size: 1.3rem;
}
.ip-card, .ip-explanation {
padding: 1rem;
}
}

View File

@@ -0,0 +1,139 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>公网IP地址查询</title>
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/background.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<div class="container">
<!-- 头部 -->
<header class="header">
<h1><i class="fas fa-globe"></i> 公网IP地址查询</h1>
<p class="subtitle">快速获取您的公网IP地址信息</p>
</header>
<!-- 主要内容区域 -->
<main class="main-content">
<!-- 查询按钮区域 -->
<div class="query-section">
<button id="queryBtn" class="query-btn">
<i class="fas fa-search"></i>
<span>查询我的IP地址</span>
</button>
</div>
<!-- 加载动画 -->
<div id="loading" class="loading" style="display: none;">
<div class="spinner"></div>
<p>正在获取IP地址信息...</p>
</div>
<!-- IP信息展示区域 -->
<div id="ip-info" class="ip-info" style="display: none;">
<div class="ip-card">
<div class="ip-header">
<i class="fas fa-network-wired"></i>
<h2>您的公网IP地址</h2>
</div>
<div class="ip-display">
<span id="ip-address" class="ip-address">---.---.---.---</span>
<button id="copyBtn" class="copy-btn" title="复制IP地址">
<i class="fas fa-copy"></i>
</button>
</div>
<div class="ip-details">
<div class="detail-item">
<i class="fas fa-clock"></i>
<span class="label">查询时间:</span>
<span id="query-time" class="value">--</span>
</div>
<div class="detail-item">
<i class="fas fa-server"></i>
<span class="label">数据来源:</span>
<span class="value">60s.viki.moe</span>
</div>
<div class="detail-item">
<i class="fas fa-map-marker-alt"></i>
<span class="label">位置信息:</span>
<span id="location" class="value">--</span>
</div>
<div class="detail-item">
<i class="fas fa-building"></i>
<span class="label">网络服务商:</span>
<span id="isp" class="value">--</span>
</div>
<div class="detail-item">
<i class="fas fa-flag"></i>
<span class="label">国家:</span>
<span id="country" class="value">--</span>
</div>
<div class="detail-item">
<i class="fas fa-map"></i>
<span class="label">地区:</span>
<span id="region" class="value">--</span>
</div>
<div class="detail-item">
<i class="fas fa-city"></i>
<span class="label">城市:</span>
<span id="city" class="value">--</span>
</div>
<div class="detail-item">
<i class="fas fa-clock"></i>
<span class="label">时区:</span>
<span id="timezone" class="value">--</span>
</div>
</div>
</div>
<!-- IP地址信息说明 -->
<div class="ip-explanation">
<h3><i class="fas fa-info-circle"></i> 什么是公网IP地址</h3>
<p>公网IP地址是您的设备在互联网上的唯一标识符由您的网络服务提供商(ISP)分配。通过这个地址,互联网上的其他设备可以找到并与您的设备通信。</p>
<div class="features">
<div class="feature-item">
<i class="fas fa-shield-alt"></i>
<div>
<h4>隐私保护</h4>
<p>了解您的IP地址有助于保护网络隐私</p>
</div>
</div>
<div class="feature-item">
<i class="fas fa-map-marker-alt"></i>
<div>
<h4>地理位置</h4>
<p>IP地址可以大致确定您的地理位置</p>
</div>
</div>
<div class="feature-item">
<i class="fas fa-cogs"></i>
<div>
<h4>网络配置</h4>
<p>用于网络故障排除和配置</p>
</div>
</div>
</div>
</div>
</div>
<!-- 错误信息 -->
<div id="error-message" class="error-message" style="display: none;">
<i class="fas fa-exclamation-triangle"></i>
<p>获取IP地址失败请检查网络连接或稍后重试</p>
<button id="retryBtn" class="retry-btn">重试</button>
</div>
</main>
<!-- 页脚 -->
<footer class="footer">
<p>&copy; 2024 公网IP地址查询工具 | 数据来源: 60s.viki.moe</p>
</footer>
</div>
<script src="js/script.js"></script>
</body>
</html>

View File

@@ -0,0 +1,343 @@
// 公网IP地址查询应用
class IPQueryApp {
constructor() {
this.apiEndpoint = 'https://60s.api.shumengya.top/v2/ip';
this.init();
}
// 初始化应用
init() {
this.bindEvents();
this.createParticles();
this.createBackgroundElements();
console.log('IP查询应用初始化完成');
}
// 绑定事件
bindEvents() {
const queryBtn = document.getElementById('queryBtn');
const retryBtn = document.getElementById('retryBtn');
const copyBtn = document.getElementById('copyBtn');
if (queryBtn) {
queryBtn.addEventListener('click', () => this.queryIP());
}
if (retryBtn) {
retryBtn.addEventListener('click', () => this.queryIP());
}
if (copyBtn) {
copyBtn.addEventListener('click', () => this.copyIP());
}
// 页面加载完成后自动查询一次
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => this.queryIP(), 500);
});
}
// 创建浮动粒子
createParticles() {
const particlesContainer = document.createElement('div');
particlesContainer.className = 'particles';
document.body.appendChild(particlesContainer);
for (let i = 0; i < 10; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
particlesContainer.appendChild(particle);
}
}
// 创建背景元素
createBackgroundElements() {
// 创建网格背景
const gridBackground = document.createElement('div');
gridBackground.className = 'grid-background';
document.body.appendChild(gridBackground);
// 创建光晕效果
const glowEffect = document.createElement('div');
glowEffect.className = 'glow-effect';
document.body.appendChild(glowEffect);
}
// 显示加载状态
showLoading() {
const loading = document.getElementById('loading');
const ipInfo = document.getElementById('ipInfo');
const errorMessage = document.getElementById('errorMessage');
const queryBtn = document.getElementById('queryBtn');
if (loading) loading.style.display = 'block';
if (ipInfo) ipInfo.style.display = 'none';
if (errorMessage) errorMessage.style.display = 'none';
if (queryBtn) {
queryBtn.disabled = true;
queryBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 查询中...';
}
}
// 隐藏加载状态
hideLoading() {
const loading = document.getElementById('loading');
const queryBtn = document.getElementById('queryBtn');
if (loading) loading.style.display = 'none';
if (queryBtn) {
queryBtn.disabled = false;
queryBtn.innerHTML = '<i class="fas fa-search"></i> 查询我的IP';
}
}
// 显示错误信息
showError(message) {
const errorMessage = document.getElementById('error-message');
const ipInfo = document.getElementById('ip-info');
if (errorMessage) {
errorMessage.style.display = 'block';
const errorText = errorMessage.querySelector('p');
if (errorText) errorText.textContent = message || '获取IP信息失败请稍后重试';
}
if (ipInfo) ipInfo.style.display = 'none';
this.hideLoading();
}
// 查询IP地址
async queryIP() {
try {
this.showLoading();
console.log('开始查询IP地址...');
const response = await fetch(this.apiEndpoint, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
console.log('API响应状态:', response.status);
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
console.log('API返回数据:', data);
if (data.code === 200 && data.data) {
this.displayIPInfo(data.data);
} else {
throw new Error(data.message || '获取IP信息失败');
}
} catch (error) {
console.error('查询IP失败:', error);
this.showError(error.message);
}
}
// 显示IP信息
displayIPInfo(data) {
const ipInfo = document.getElementById('ip-info');
const errorMessage = document.getElementById('error-message');
// 更新IP地址显示
const ipAddressElement = document.getElementById('ip-address');
if (ipAddressElement && data.ip) {
ipAddressElement.textContent = data.ip;
}
// 更新查询时间
const queryTimeElement = document.getElementById('query-time');
if (queryTimeElement) {
const now = new Date();
queryTimeElement.textContent = now.toLocaleString('zh-CN');
}
// 更新详细信息 - 只显示API提供的数据
if (data.location) this.updateDetailItem('location', data.location);
else this.hideDetailItem('location');
if (data.isp) this.updateDetailItem('isp', data.isp);
else this.hideDetailItem('isp');
if (data.country) this.updateDetailItem('country', data.country);
else this.hideDetailItem('country');
if (data.region) this.updateDetailItem('region', data.region);
else this.hideDetailItem('region');
if (data.city) this.updateDetailItem('city', data.city);
else this.hideDetailItem('city');
if (data.timezone) this.updateDetailItem('timezone', data.timezone);
else this.hideDetailItem('timezone');
// 显示IP信息隐藏错误信息
if (ipInfo) ipInfo.style.display = 'block';
if (errorMessage) errorMessage.style.display = 'none';
this.hideLoading();
console.log('IP信息显示完成');
}
// 更新详细信息项
updateDetailItem(id, value) {
const element = document.getElementById(id);
if (element) {
element.textContent = value;
// 显示对应的详细信息行
const detailRow = element.closest('.detail-item');
if (detailRow) {
detailRow.style.display = 'flex';
}
}
}
// 隐藏详细信息项
hideDetailItem(id) {
const element = document.getElementById(id);
if (element) {
// 隐藏整个详细信息行
const detailRow = element.closest('.detail-item');
if (detailRow) {
detailRow.style.display = 'none';
}
}
}
// 复制IP地址
async copyIP() {
const ipAddressElement = document.getElementById('ip-address');
const copyBtn = document.getElementById('copyBtn');
if (!ipAddressElement || !ipAddressElement.textContent) {
this.showToast('没有可复制的IP地址', 'error');
return;
}
try {
await navigator.clipboard.writeText(ipAddressElement.textContent);
// 更新按钮状态
if (copyBtn) {
const originalHTML = copyBtn.innerHTML;
copyBtn.innerHTML = '<i class="fas fa-check"></i>';
copyBtn.style.background = '#50c878';
setTimeout(() => {
copyBtn.innerHTML = originalHTML;
copyBtn.style.background = '#4a90e2';
}, 1500);
}
this.showToast('IP地址已复制到剪贴板', 'success');
console.log('IP地址已复制:', ipAddressElement.textContent);
} catch (error) {
console.error('复制失败:', error);
this.showToast('复制失败,请手动选择复制', 'error');
}
}
// 显示提示消息
showToast(message, type = 'info') {
// 移除已存在的toast
const existingToast = document.querySelector('.toast');
if (existingToast) {
existingToast.remove();
}
// 创建新的toast
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.innerHTML = `
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
<span>${message}</span>
`;
// 添加toast样式
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'success' ? '#50c878' : type === 'error' ? '#ff6b6b' : '#4a90e2'};
color: white;
padding: 1rem 1.5rem;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
z-index: 1000;
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 500;
animation: slideInRight 0.3s ease;
max-width: 300px;
`;
// 添加动画样式
const style = document.createElement('style');
style.textContent = `
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
`;
document.head.appendChild(style);
document.body.appendChild(toast);
// 3秒后自动移除
setTimeout(() => {
toast.style.animation = 'slideOutRight 0.3s ease';
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
if (style.parentNode) {
style.remove();
}
}, 300);
}, 3000);
}
// 格式化时间
formatTime(timestamp) {
const date = new Date(timestamp);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
}
// 初始化应用
const app = new IPQueryApp();
// 导出到全局作用域(用于调试)
window.IPQueryApp = app;

View File

@@ -0,0 +1,3 @@
[
"https://60s.api.shumengya.top/v2/ip"
]

View File

@@ -0,0 +1,17 @@
{
"code": 200,
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841",
"data": {
"ip": "2401:b60:16:83::"
}
}
// 注意此API只返回IP地址不包含以下信息
// - location (位置信息)
// - isp (网络服务商)
// - country (国家)
// - region (地区)
// - city (城市)
// - timezone (时区)
//
// 使API

Some files were not shown because too many files have changed in this diff Show More