chore: sync local changes (2026-03-12)
@@ -1,36 +0,0 @@
|
||||
# Node modules
|
||||
InfoGenie-frontend/node_modules
|
||||
InfoGenie-frontend/build
|
||||
|
||||
# Python cache
|
||||
InfoGenie-backend/__pycache__
|
||||
InfoGenie-backend/**/__pycache__
|
||||
InfoGenie-backend/*.pyc
|
||||
InfoGenie-backend/**/*.pyc
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Test files
|
||||
InfoGenie-backend/test
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
|
||||
# Backup files
|
||||
*.backup
|
||||
*.bak
|
||||
5
.gitignore
vendored
@@ -1,5 +0,0 @@
|
||||
#项目自忽略
|
||||
.vscode
|
||||
InfoGenie-frontend/node_modules
|
||||
InfoGenie-frontend/build
|
||||
InfoGenie-backend/__pycache__
|
||||
57
Dockerfile
@@ -1,57 +0,0 @@
|
||||
# InfoGenie 统一 Docker 镜像
|
||||
# 多阶段构建:前端构建 + 后端 + Nginx
|
||||
|
||||
# 阶段1: 前端构建
|
||||
FROM node:18-alpine AS frontend-builder
|
||||
|
||||
WORKDIR /frontend
|
||||
COPY InfoGenie-frontend/package*.json ./
|
||||
RUN npm install --legacy-peer-deps
|
||||
COPY InfoGenie-frontend/ ./
|
||||
RUN npm run build
|
||||
|
||||
# 阶段2: 最终镜像
|
||||
FROM python:3.10-slim
|
||||
|
||||
# 安装 Nginx 和必要的工具
|
||||
RUN apt-get update && apt-get install -y \
|
||||
nginx \
|
||||
supervisor \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制后端代码
|
||||
COPY InfoGenie-backend/ ./backend/
|
||||
|
||||
# 安装 Python 依赖
|
||||
RUN pip install --no-cache-dir -r ./backend/requirements.txt gunicorn
|
||||
|
||||
# 复制前端构建产物到 Nginx 目录
|
||||
COPY --from=frontend-builder /frontend/build /usr/share/nginx/html
|
||||
|
||||
# 创建持久化数据目录
|
||||
RUN mkdir -p /app/data/logs
|
||||
|
||||
# 复制 Nginx 配置
|
||||
COPY docker/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY docker/default.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 复制 Supervisor 配置
|
||||
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
# 复制启动脚本
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 2323
|
||||
|
||||
# 设置环境变量
|
||||
ENV FLASK_APP=app.py
|
||||
ENV FLASK_ENV=production
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# 使用 supervisor 管理多进程
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
19
InfoGenie-backend/.dockerignore
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
|
||||
# Python cache
|
||||
__pycache__
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Test files
|
||||
InfoGenie-backend/test
|
||||
|
||||
|
||||
3
InfoGenie-backend/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
#项目自忽略
|
||||
.vscode
|
||||
__pycache__
|
||||
30
InfoGenie-backend/Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
||||
# InfoGenie 后端 Docker 镜像
|
||||
# 仅包含后端服务,使用 Gunicorn
|
||||
|
||||
FROM python:3.10-slim
|
||||
|
||||
# 安装 curl 用于健康检查
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制依赖文件
|
||||
COPY requirements.txt .
|
||||
|
||||
# 安装 Python 依赖
|
||||
RUN pip install --no-cache-dir -r requirements.txt gunicorn
|
||||
|
||||
# 复制后端代码
|
||||
COPY . .
|
||||
|
||||
# 创建持久化数据目录
|
||||
RUN mkdir -p /app/data/logs
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 2323
|
||||
|
||||
# 使用 Gunicorn 启动应用
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:2323", "--workers", "4", "--threads", "2", "--timeout", "120", "--access-logfile", "-", "--error-logfile", "-", "app:app"]
|
||||
@@ -178,7 +178,9 @@ def create_app():
|
||||
|
||||
return app
|
||||
|
||||
# 为 Gunicorn 创建应用实例
|
||||
app = create_app()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = create_app()
|
||||
print("🚀 启动 InfoGenie 后端服务...")
|
||||
app.run(debug=True, host='0.0.0.0', port=5002)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
infogenie:
|
||||
infogenie-backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: infogenie
|
||||
container_name: infogenie-backend
|
||||
restart: always
|
||||
ports:
|
||||
- "2323:2323"
|
||||
volumes:
|
||||
# 持久化数据映射
|
||||
- /shumengya/docker/storage/infogenie/logs:/app/data/logs
|
||||
- /shumengya/docker/storage/infogenie/data:/app/data
|
||||
- /shumengya/docker/infogenie-backend/data:/app/data
|
||||
|
||||
environment:
|
||||
# 从 .env 文件读取环境变量
|
||||
- MONGO_URI=${MONGO_URI}
|
||||
@@ -20,12 +20,15 @@ services:
|
||||
- MAIL_PASSWORD=${MAIL_PASSWORD}
|
||||
- SECRET_KEY=${SECRET_KEY}
|
||||
- FLASK_ENV=production
|
||||
# 生产环境配置
|
||||
- HWT_DOMAIN=.shumengya.top
|
||||
- HWT_SECURE=False # 如果使用 HTTPS 反向代理,设为 False;直接 HTTPS 设为 True
|
||||
env_file:
|
||||
- ./InfoGenie-backend/.env
|
||||
- .env
|
||||
networks:
|
||||
- infogenie-network
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:2323/api/health"]
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:2323/api/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
@@ -266,7 +266,7 @@ def get_qq_avatar_url(email):
|
||||
return None
|
||||
|
||||
# 返回QQ头像API URL
|
||||
return f"http://q1.qlogo.cn/g?b=qq&nk={qq_number}&s=100"
|
||||
return f"https://q1.qlogo.cn/g?b=qq&nk={qq_number}&s=100"
|
||||
|
||||
# 清理过期验证码
|
||||
def cleanup_expired_codes():
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/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❌ 所有连接尝试都失败了")
|
||||
@@ -1,100 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
测试为指定账号增加萌芽币接口 (/api/user/add-coins)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# 加入后端根目录到路径,导入create_app
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app import create_app
|
||||
from modules.auth import generate_token
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
|
||||
def run_test():
|
||||
"""运行加币接口测试,打印真实响应并断言结果"""
|
||||
app = create_app()
|
||||
|
||||
with app.app_context():
|
||||
db = app.mongo.db
|
||||
users = db.userdata
|
||||
|
||||
# 构造一个临时测试用户(真实写库,测试结束删除)
|
||||
test_email = "infogenie.test.addcoins@foxmail.com"
|
||||
users.delete_many({'邮箱': test_email})
|
||||
test_user = {
|
||||
'邮箱': test_email,
|
||||
'用户名': '测试用户_加币',
|
||||
'密码': generate_password_hash('AddCoins123!'),
|
||||
'头像': None,
|
||||
'注册时间': datetime.now().isoformat(),
|
||||
'最后登录': None,
|
||||
'登录次数': 0,
|
||||
'用户状态': 'active',
|
||||
'等级': 0,
|
||||
'经验': 0,
|
||||
'萌芽币': 0,
|
||||
'签到系统': {
|
||||
'连续签到天数': 0,
|
||||
'今日是否已签到': False,
|
||||
'签到时间': datetime.now().strftime('%Y-%m-%d')
|
||||
}
|
||||
}
|
||||
insert_result = users.insert_one(test_user)
|
||||
test_user_id = str(insert_result.inserted_id)
|
||||
|
||||
# 生成有效JWT用于认证
|
||||
token = generate_token({
|
||||
'user_id': test_user_id,
|
||||
'email': test_email,
|
||||
'username': test_user['用户名']
|
||||
})
|
||||
|
||||
client = app.test_client()
|
||||
|
||||
# 第一次加币: +500
|
||||
resp1 = client.post(
|
||||
'/api/user/add-coins',
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
json={'email': test_email, 'amount': 500}
|
||||
)
|
||||
print('第一次加币 状态码:', resp1.status_code)
|
||||
data1 = resp1.get_json()
|
||||
print('第一次加币 响应:')
|
||||
print(json.dumps(data1, ensure_ascii=False, indent=2))
|
||||
assert resp1.status_code == 200
|
||||
assert data1.get('success') is True
|
||||
assert data1['data']['before_coins'] == 0
|
||||
assert data1['data']['added'] == 500
|
||||
assert data1['data']['new_coins'] == 500
|
||||
|
||||
# 第二次加币: +200
|
||||
resp2 = client.post(
|
||||
'/api/user/add-coins',
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
json={'email': test_email, 'amount': 200}
|
||||
)
|
||||
print('第二次加币 状态码:', resp2.status_code)
|
||||
data2 = resp2.get_json()
|
||||
print('第二次加币 响应:')
|
||||
print(json.dumps(data2, ensure_ascii=False, indent=2))
|
||||
assert resp2.status_code == 200
|
||||
assert data2.get('success') is True
|
||||
assert data2['data']['before_coins'] == 500
|
||||
assert data2['data']['added'] == 200
|
||||
assert data2['data']['new_coins'] == 700
|
||||
|
||||
# 清理临时测试用户
|
||||
users.delete_many({'邮箱': test_email})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('🔧 开始测试 /api/user/add-coins 接口...')
|
||||
run_test()
|
||||
print('✅ 测试完成!')
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,81 +0,0 @@
|
||||
#!/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测试完成!")
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,81 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
测试列出所有用户的HTTP接口 (/api/user/list)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# 将后端根目录加入路径,便于导入app
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app import create_app
|
||||
from modules.auth import generate_token
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
|
||||
def run_test():
|
||||
"""运行用户列表接口测试,输出真实数据"""
|
||||
# 使用.env中的真实Mongo配置,不造假
|
||||
app = create_app()
|
||||
|
||||
with app.app_context():
|
||||
db = app.mongo.db
|
||||
users = db.userdata
|
||||
|
||||
# 插入一个测试用户(真实写入后再删除),确保可验证接口输出
|
||||
test_email = "infogenie.test.user@foxmail.com"
|
||||
users.delete_many({'邮箱': test_email})
|
||||
test_user = {
|
||||
'邮箱': test_email,
|
||||
'用户名': '测试用户_列表',
|
||||
'密码': generate_password_hash('TestPass123!'),
|
||||
'头像': None,
|
||||
'注册时间': datetime.now().isoformat(),
|
||||
'最后登录': None,
|
||||
'登录次数': 0,
|
||||
'用户状态': 'active',
|
||||
'等级': 0,
|
||||
'经验': 0,
|
||||
'萌芽币': 0,
|
||||
'签到系统': {
|
||||
'连续签到天数': 0,
|
||||
'今日是否已签到': False,
|
||||
'签到时间': datetime.now().strftime('%Y-%m-%d')
|
||||
}
|
||||
}
|
||||
insert_result = users.insert_one(test_user)
|
||||
test_user_id = str(insert_result.inserted_id)
|
||||
|
||||
# 生成有效JWT,满足认证要求
|
||||
token = generate_token({
|
||||
'user_id': test_user_id,
|
||||
'email': test_email,
|
||||
'username': test_user['用户名']
|
||||
})
|
||||
|
||||
client = app.test_client()
|
||||
resp = client.get('/api/user/list', headers={'Authorization': f'Bearer {token}'})
|
||||
|
||||
print("状态码:", resp.status_code)
|
||||
data = resp.get_json()
|
||||
print("响应内容:")
|
||||
print(json.dumps(data, ensure_ascii=False, indent=2))
|
||||
|
||||
# 基本断言,确保返回真实列表数据且包含刚插入的测试用户
|
||||
assert resp.status_code == 200
|
||||
assert data.get('success') is True
|
||||
assert isinstance(data.get('data'), list)
|
||||
assert any(u.get('email') == test_email for u in data['data'])
|
||||
|
||||
# 清理测试数据
|
||||
users.delete_many({'邮箱': test_email})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('🔎 开始测试 /api/user/list 接口...')
|
||||
run_test()
|
||||
print('✅ 测试完成!')
|
||||
BIN
InfoGenie-frontend/public/icons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
InfoGenie-frontend/public/icons/favicon-16.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
InfoGenie-frontend/public/icons/favicon-32.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
InfoGenie-frontend/public/icons/icon-192-maskable.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
InfoGenie-frontend/public/icons/icon-192.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
InfoGenie-frontend/public/icons/icon-512-maskable.png
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
InfoGenie-frontend/public/icons/icon-512.png
Normal file
|
After Width: | Height: | Size: 256 KiB |
@@ -7,6 +7,15 @@
|
||||
<meta name="description" content="✨ 万象口袋 - 一个多功能的聚合软件应用,提供聚合应用、小游戏、AI模型工具等丰富功能" />
|
||||
<meta name="keywords" content="聚合应用,热搜榜单,小游戏,AI模型,实时资讯,工具集合" />
|
||||
<meta name="author" content="万象口袋" />
|
||||
<meta name="application-name" content="万象口袋" />
|
||||
|
||||
<!-- PWA / App meta -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="apple-mobile-web-app-title" content="万象口袋" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="msapplication-TileColor" content="#667eea" />
|
||||
<meta name="msapplication-TileImage" content="%PUBLIC_URL%/icons/icon-192.png" />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
@@ -19,10 +28,11 @@
|
||||
<meta property="twitter:description" content="🎨一个跨平台的多功能聚合软件应用" />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" href="%PUBLIC_URL%/assets/logo.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/icons/favicon-32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/icons/favicon-16.png" />
|
||||
|
||||
<!-- Apple Touch Icon -->
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/assets/logo.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/icons/apple-touch-icon.png" />
|
||||
|
||||
<!-- Manifest -->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
@@ -352,19 +362,6 @@
|
||||
window.addEventListener('error', function(e) {
|
||||
console.error('应用加载错误:', e.error);
|
||||
});
|
||||
|
||||
// Service Worker 注册
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function() {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then(function(registration) {
|
||||
console.log('SW registered: ', registration);
|
||||
})
|
||||
.catch(function(registrationError) {
|
||||
console.log('SW registration failed: ', registrationError);
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,27 +4,34 @@
|
||||
"description": "🎨 一个多功能的聚合软件应用,提供聚合应用、休闲小游戏、AI模型工具等丰富功能",
|
||||
"icons": [
|
||||
{
|
||||
"src": "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>✨</text></svg>",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/svg+xml"
|
||||
},
|
||||
{
|
||||
"src": "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>✨</text></svg>",
|
||||
"type": "image/svg+xml",
|
||||
"src": "/icons/icon-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>✨</text></svg>",
|
||||
"type": "image/svg+xml",
|
||||
"src": "/icons/icon-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-192-maskable.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-512-maskable.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#667eea",
|
||||
"background_color": "#ffffff",
|
||||
"orientation": "portrait-primary",
|
||||
"scope": "/",
|
||||
"scope": ".",
|
||||
"lang": "zh-CN",
|
||||
"categories": ["utilities", "productivity", "entertainment"],
|
||||
"screenshots": [
|
||||
|
||||
@@ -1,48 +1,150 @@
|
||||
// Service Worker for InfoGenie App
|
||||
const CACHE_NAME = 'infogenie-cache-v1';
|
||||
const urlsToCache = [
|
||||
// Service Worker for InfoGenie App (PWA)
|
||||
// 注意:PWA 必须在 HTTPS(或 localhost)下才能生效
|
||||
|
||||
const CACHE_VERSION = 'v2';
|
||||
const PRECACHE_NAME = `infogenie-precache-${CACHE_VERSION}`;
|
||||
const RUNTIME_NAME = `infogenie-runtime-${CACHE_VERSION}`;
|
||||
|
||||
const CORE_ASSETS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/manifest.json'
|
||||
'/manifest.json',
|
||||
'/icons/icon-192.png',
|
||||
'/icons/icon-512.png',
|
||||
'/icons/icon-192-maskable.png',
|
||||
'/icons/icon-512-maskable.png',
|
||||
'/icons/apple-touch-icon.png',
|
||||
'/icons/favicon-32.png',
|
||||
'/icons/favicon-16.png'
|
||||
];
|
||||
|
||||
// 安装Service Worker
|
||||
async function safeAddAll(cache, urls) {
|
||||
const uniqueUrls = Array.from(new Set(urls)).filter(Boolean);
|
||||
const results = await Promise.allSettled(uniqueUrls.map(url => cache.add(url)));
|
||||
const failures = results.filter(r => r.status === 'rejected');
|
||||
if (failures.length > 0) {
|
||||
console.warn('[SW] Some assets failed to cache:', failures.length);
|
||||
}
|
||||
}
|
||||
|
||||
async function precacheEntrypoints(cache) {
|
||||
try {
|
||||
const res = await fetch('/asset-manifest.json', { cache: 'no-store' });
|
||||
if (!res.ok) return;
|
||||
const manifest = await res.json();
|
||||
|
||||
const filesObj = manifest && typeof manifest === 'object' ? manifest.files : undefined;
|
||||
const files = filesObj && typeof filesObj === 'object' ? Object.values(filesObj) : [];
|
||||
const entrypoints = Array.isArray(manifest.entrypoints) ? manifest.entrypoints : [];
|
||||
|
||||
const urls = [...files, ...entrypoints]
|
||||
.filter(p => typeof p === 'string')
|
||||
.map(p => (p.startsWith('/') ? p : `/${p}`))
|
||||
.filter(p => !p.endsWith('.map'));
|
||||
|
||||
await safeAddAll(cache, urls);
|
||||
} catch (err) {
|
||||
console.warn('[SW] Failed to precache entrypoints:', err);
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
self.skipWaiting();
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then(cache => {
|
||||
console.log('Opened cache');
|
||||
return cache.addAll(urlsToCache);
|
||||
})
|
||||
(async () => {
|
||||
const cache = await caches.open(PRECACHE_NAME);
|
||||
await safeAddAll(cache, CORE_ASSETS);
|
||||
await precacheEntrypoints(cache);
|
||||
})()
|
||||
);
|
||||
});
|
||||
|
||||
// 拦截请求并从缓存中响应
|
||||
self.addEventListener('fetch', event => {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then(response => {
|
||||
// 如果找到缓存的响应,则返回缓存
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
return fetch(event.request);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// 更新Service Worker
|
||||
self.addEventListener('activate', event => {
|
||||
const cacheWhitelist = [CACHE_NAME];
|
||||
event.waitUntil(
|
||||
caches.keys().then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
if (cacheWhitelist.indexOf(cacheName) === -1) {
|
||||
return caches.delete(cacheName);
|
||||
(async () => {
|
||||
const cacheNames = await caches.keys();
|
||||
await Promise.all(
|
||||
cacheNames.map(name => {
|
||||
if (name !== PRECACHE_NAME && name !== RUNTIME_NAME) {
|
||||
return caches.delete(name);
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
);
|
||||
})
|
||||
|
||||
await self.clients.claim();
|
||||
})()
|
||||
);
|
||||
});
|
||||
|
||||
function isNavigationRequest(request) {
|
||||
return request.mode === 'navigate' || request.destination === 'document';
|
||||
}
|
||||
|
||||
function shouldHandleRequest(url, request) {
|
||||
if (request.method !== 'GET') return false;
|
||||
if (url.origin !== self.location.origin) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
const url = new URL(event.request.url);
|
||||
if (!shouldHandleRequest(url, event.request)) return;
|
||||
|
||||
// 不缓存后端 API(如需缓存请在这里加规则)
|
||||
if (url.pathname.startsWith('/api')) return;
|
||||
|
||||
// 页面请求:优先网络,离线回退到缓存
|
||||
if (isNavigationRequest(event.request)) {
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
try {
|
||||
const networkResponse = await fetch(event.request);
|
||||
const cache = await caches.open(RUNTIME_NAME);
|
||||
cache.put(event.request, networkResponse.clone());
|
||||
return networkResponse;
|
||||
} catch (err) {
|
||||
const cached = await caches.match(event.request);
|
||||
return cached || caches.match('/index.html');
|
||||
}
|
||||
})()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 静态资源:stale-while-revalidate
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
const cached = await caches.match(event.request);
|
||||
const cache = await caches.open(RUNTIME_NAME);
|
||||
|
||||
const networkFetch = (async () => {
|
||||
try {
|
||||
const response = await fetch(event.request);
|
||||
if (response && response.ok) {
|
||||
cache.put(event.request, response.clone());
|
||||
}
|
||||
return response;
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
|
||||
if (cached) {
|
||||
event.waitUntil(networkFetch);
|
||||
return cached;
|
||||
}
|
||||
|
||||
const response = await networkFetch;
|
||||
if (response) return response;
|
||||
|
||||
return new Response('', { status: 504, statusText: 'Offline' });
|
||||
})()
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('message', event => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -122,7 +122,7 @@ export const UserProvider = ({ children }) => {
|
||||
if (qqDomains.includes(domain)) {
|
||||
const qqNumber = email.split('@')[0];
|
||||
if (/^\d+$/.test(qqNumber)) {
|
||||
return `http://q1.qlogo.cn/g?b=qq&nk=${qqNumber}&s=100`;
|
||||
return `https://q1.qlogo.cn/g?b=qq&nk=${qqNumber}&s=100`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,13 @@ root.render(
|
||||
);
|
||||
|
||||
// 注册Service Worker
|
||||
if ('serviceWorker' in navigator) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
const publicUrl = (process.env.PUBLIC_URL || '').replace(/\/$/, '');
|
||||
const swUrl = `${publicUrl}/sw.js`;
|
||||
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
console.log('ServiceWorker registration successful with scope: ', registration.scope);
|
||||
})
|
||||
@@ -21,4 +25,8 @@ if ('serviceWorker' in navigator) {
|
||||
console.error('ServiceWorker registration failed: ', error);
|
||||
});
|
||||
});
|
||||
} else if (process.env.NODE_ENV !== 'production' && 'serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||
registrations.forEach(registration => registration.unregister());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -363,7 +363,7 @@ const UserProfilePage = () => {
|
||||
if (qqDomains.includes(domain)) {
|
||||
const qqNumber = email.split('@')[0];
|
||||
if (/^\d+$/.test(qqNumber)) {
|
||||
return `http://q1.qlogo.cn/g?b=qq&nk=${qqNumber}&s=100`;
|
||||
return `https://q1.qlogo.cn/g?b=qq&nk=${qqNumber}&s=100`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
server {
|
||||
listen 2323;
|
||||
server_name _;
|
||||
|
||||
# 前端静态文件根目录
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# 日志配置
|
||||
access_log /app/data/logs/access.log;
|
||||
error_log /app/data/logs/error.log;
|
||||
|
||||
# API 请求转发到后端
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:5002;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_connect_timeout 300s;
|
||||
}
|
||||
|
||||
# 60sapi 静态文件路由
|
||||
location /60sapi/ {
|
||||
alias /usr/share/nginx/html/60sapi/;
|
||||
try_files $uri $uri/ =404;
|
||||
add_header Cache-Control "public, max-age=3600";
|
||||
}
|
||||
|
||||
# smallgame 静态文件路由
|
||||
location /smallgame/ {
|
||||
alias /usr/share/nginx/html/smallgame/;
|
||||
try_files $uri $uri/ =404;
|
||||
add_header Cache-Control "public, max-age=3600";
|
||||
}
|
||||
|
||||
# aimodelapp 静态文件路由
|
||||
location /aimodelapp/ {
|
||||
alias /usr/share/nginx/html/aimodelapp/;
|
||||
try_files $uri $uri/ =404;
|
||||
add_header Cache-Control "public, max-age=3600";
|
||||
}
|
||||
|
||||
# toolbox 静态文件路由
|
||||
location /toolbox/ {
|
||||
alias /usr/share/nginx/html/toolbox/;
|
||||
try_files $uri $uri/ =404;
|
||||
add_header Cache-Control "public, max-age=3600";
|
||||
}
|
||||
|
||||
# 静态资源缓存
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# React Router 支持 - 所有其他请求返回 index.html
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
}
|
||||
|
||||
# 健康检查
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🚀 启动 InfoGenie 服务..."
|
||||
|
||||
# 创建必要的目录
|
||||
mkdir -p /app/data/logs
|
||||
|
||||
# 检查环境变量
|
||||
if [ -z "$MONGO_URI" ]; then
|
||||
echo "⚠️ 警告: MONGO_URI 未设置"
|
||||
fi
|
||||
|
||||
if [ -z "$MAIL_USERNAME" ]; then
|
||||
echo "⚠️ 警告: MAIL_USERNAME 未设置"
|
||||
fi
|
||||
|
||||
echo "✅ 环境变量检查完成"
|
||||
|
||||
# 测试 Nginx 配置
|
||||
echo "🔍 检查 Nginx 配置..."
|
||||
nginx -t
|
||||
|
||||
# 启动 Supervisor
|
||||
echo "🎯 启动服务进程..."
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||
@@ -1,41 +0,0 @@
|
||||
user www-data;
|
||||
worker_processes auto;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
use epoll;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# 日志格式
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /app/data/logs/nginx_access.log main;
|
||||
error_log /app/data/logs/nginx_error.log warn;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
client_max_body_size 20M;
|
||||
|
||||
# Gzip 压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/xml text/javascript
|
||||
application/json application/javascript application/xml+rss
|
||||
application/rss+xml font/truetype font/opentype
|
||||
application/vnd.ms-fontobject image/svg+xml;
|
||||
|
||||
# 包含站点配置
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
user=root
|
||||
logfile=/app/data/logs/supervisord.log
|
||||
pidfile=/var/run/supervisord.pid
|
||||
|
||||
[program:gunicorn]
|
||||
directory=/app/backend
|
||||
command=gunicorn -w 4 -b 127.0.0.1:5002 --timeout 300 --access-logfile /app/data/logs/gunicorn_access.log --error-logfile /app/data/logs/gunicorn_error.log "app:create_app()"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startretries=3
|
||||
stderr_logfile=/app/data/logs/gunicorn_err.log
|
||||
stdout_logfile=/app/data/logs/gunicorn_out.log
|
||||
priority=1
|
||||
|
||||
[program:nginx]
|
||||
command=/usr/sbin/nginx -g "daemon off;"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startretries=3
|
||||
stderr_logfile=/app/data/logs/nginx_err.log
|
||||
stdout_logfile=/app/data/logs/nginx_out.log
|
||||
priority=2
|
||||
89
nginx-frontend.conf
Normal file
@@ -0,0 +1,89 @@
|
||||
# InfoGenie 前端 Nginx 配置
|
||||
# 部署在 192.168.1.100:1313
|
||||
# 域名: infogenie.shumengya.top
|
||||
|
||||
server {
|
||||
listen 1313;
|
||||
listen [::]:1313;
|
||||
server_name infogenie.shumengya.top 192.168.1.100;
|
||||
|
||||
# 网站根目录
|
||||
root /var/www/infogenie;
|
||||
index index.html;
|
||||
|
||||
# 日志配置
|
||||
access_log /var/log/nginx/infogenie-access.log;
|
||||
error_log /var/log/nginx/infogenie-error.log;
|
||||
|
||||
# Gzip 压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/xml text/javascript
|
||||
application/javascript application/xml+rss
|
||||
application/json application/xml;
|
||||
|
||||
# 前端 SPA 路由支持
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
add_header Cache-Control "no-cache";
|
||||
}
|
||||
|
||||
# 静态资源缓存策略
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|svg)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
location ~* \.(css|js)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
|
||||
location ~* \.(woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# 可选:如果需要代理后端 API
|
||||
# 这样前端可以使用相对路径 /api 访问后端
|
||||
location /api {
|
||||
proxy_pass http://192.168.1.233:2323;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
# CORS 配置
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||
add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
|
||||
add_header Access-Control-Expose-Headers 'Content-Length,Content-Range' always;
|
||||
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||
add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
|
||||
add_header Access-Control-Max-Age 1728000;
|
||||
add_header Content-Type 'text/plain; charset=utf-8';
|
||||
add_header Content-Length 0;
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
# 禁止访问隐藏文件
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
|
||||
# 安全头
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
}
|
||||