init
This commit is contained in:
12
html-generator/backend/.env
Normal file
12
html-generator/backend/.env
Normal file
@@ -0,0 +1,12 @@
|
||||
# 应用配置
|
||||
APP_NAME="HTML Generator API"
|
||||
API_PREFIX="/api"
|
||||
|
||||
# 前端配置
|
||||
FRONTEND_BASE_URL="http://localhost:3000"
|
||||
|
||||
# 允许的跨域来源
|
||||
ALLOWED_ORIGINS=["*"]
|
||||
|
||||
# 静态文件目录
|
||||
STATIC_DIR="../frontend/public/static"
|
||||
12
html-generator/backend/.env.example
Normal file
12
html-generator/backend/.env.example
Normal file
@@ -0,0 +1,12 @@
|
||||
# 应用配置
|
||||
APP_NAME="HTML Generator API"
|
||||
API_PREFIX="/api"
|
||||
|
||||
# 前端配置
|
||||
FRONTEND_BASE_URL="http://localhost:3000"
|
||||
|
||||
# 允许的跨域来源
|
||||
ALLOWED_ORIGINS=["*"]
|
||||
|
||||
# 静态文件目录
|
||||
STATIC_DIR="../frontend/public/static"
|
||||
0
html-generator/backend/app/__init__.py
Normal file
0
html-generator/backend/app/__init__.py
Normal file
BIN
html-generator/backend/app/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
html-generator/backend/app/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
html-generator/backend/app/__pycache__/config.cpython-313.pyc
Normal file
BIN
html-generator/backend/app/__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
BIN
html-generator/backend/app/__pycache__/database.cpython-313.pyc
Normal file
BIN
html-generator/backend/app/__pycache__/database.cpython-313.pyc
Normal file
Binary file not shown.
BIN
html-generator/backend/app/__pycache__/main.cpython-313.pyc
Normal file
BIN
html-generator/backend/app/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
22
html-generator/backend/app/config.py
Normal file
22
html-generator/backend/app/config.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
backend_dir = Path(__file__).resolve().parent.parent
|
||||
load_dotenv(backend_dir / ".env")
|
||||
|
||||
|
||||
class Settings:
|
||||
app_name = "HTML Generator API"
|
||||
api_prefix = "/api"
|
||||
backend_dir = backend_dir
|
||||
data_dir = backend_dir / "data"
|
||||
database_path = data_dir / "html_generator.db"
|
||||
database_url = f"sqlite:///{database_path.as_posix()}"
|
||||
allowed_origins = ["*"]
|
||||
frontend_base_url = os.environ.get("FRONTEND_BASE_URL", "http://localhost:3000")
|
||||
static_dir = backend_dir / "../frontend/public/static"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
23
html-generator/backend/app/database.py
Normal file
23
html-generator/backend/app/database.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from collections.abc import Generator
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import declarative_base, sessionmaker
|
||||
|
||||
from app.config import settings
|
||||
|
||||
settings.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
engine = create_engine(
|
||||
settings.database_url,
|
||||
connect_args={"check_same_thread": False},
|
||||
)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def get_db() -> Generator:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
43
html-generator/backend/app/main.py
Normal file
43
html-generator/backend/app/main.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import logging
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.config import settings
|
||||
from app.database import Base, engine, SessionLocal
|
||||
from app.models import HTMLFile
|
||||
from app.routers import html
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# 删除过期记录
|
||||
db = SessionLocal()
|
||||
try:
|
||||
deleted_count = HTMLFile.delete_expired_records(db)
|
||||
if deleted_count > 0:
|
||||
logger.info(f"Deleted {deleted_count} expired HTML file records")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
app = FastAPI(title=settings.app_name)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.allowed_origins,
|
||||
allow_credentials=False,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(html.router, prefix=settings.api_prefix)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def health_check() -> dict[str, str]:
|
||||
return {"message": "HTML Generator API is running"}
|
||||
30
html-generator/backend/app/models.py
Normal file
30
html-generator/backend/app/models.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from sqlalchemy import DateTime, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, Session
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class HTMLFile(Base):
|
||||
__tablename__ = "html_files"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
unique_id: Mapped[str] = mapped_column(
|
||||
String(32), unique=True, index=True, nullable=False
|
||||
)
|
||||
filename: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=datetime.utcnow, nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def delete_expired_records(cls, db: Session, days: int = 5) -> int:
|
||||
"""删除超过指定天数的记录"""
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
||||
deleted = db.query(cls).filter(cls.created_at < cutoff_date).delete()
|
||||
db.commit()
|
||||
return deleted
|
||||
0
html-generator/backend/app/routers/__init__.py
Normal file
0
html-generator/backend/app/routers/__init__.py
Normal file
86
html-generator/backend/app/routers/html.py
Normal file
86
html-generator/backend/app/routers/html.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import os
|
||||
import secrets
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import settings
|
||||
from app.database import get_db
|
||||
from app.models import HTMLFile
|
||||
from app.schemas import HTMLGenerateRequest, HTMLGenerateResponse
|
||||
|
||||
router = APIRouter(prefix="/html", tags=["html"])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def generate_unique_id() -> str:
|
||||
return secrets.token_urlsafe(16)
|
||||
|
||||
|
||||
@router.post("/generate", response_model=HTMLGenerateResponse, status_code=status.HTTP_201_CREATED)
|
||||
def generate_html(request: HTMLGenerateRequest, db: Session = Depends(get_db)):
|
||||
try:
|
||||
# 先删除过期记录
|
||||
deleted_count = HTMLFile.delete_expired_records(db)
|
||||
if deleted_count > 0:
|
||||
logger.info(f"Deleted {deleted_count} expired HTML file records")
|
||||
|
||||
# 生成唯一 ID
|
||||
unique_id = generate_unique_id()
|
||||
|
||||
# 确保静态文件目录存在
|
||||
static_dir = settings.static_dir.resolve()
|
||||
static_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 生成 HTML 文件路径
|
||||
html_filename = f"{unique_id}.html"
|
||||
html_path = static_dir / html_filename
|
||||
|
||||
# 写入 HTML 内容
|
||||
with open(html_path, "w", encoding="utf-8") as f:
|
||||
f.write(request.html_content)
|
||||
|
||||
# 保存到数据库
|
||||
html_file = HTMLFile(
|
||||
unique_id=unique_id,
|
||||
filename=html_filename,
|
||||
)
|
||||
db.add(html_file)
|
||||
db.commit()
|
||||
db.refresh(html_file)
|
||||
|
||||
# 生成完整链接
|
||||
html_url = f"{settings.frontend_base_url}/static/{html_filename}"
|
||||
|
||||
return HTMLGenerateResponse(
|
||||
message="HTML 文件生成成功",
|
||||
unique_id=unique_id,
|
||||
url=html_url
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"生成 HTML 文件失败: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"生成 HTML 文件失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{unique_id}")
|
||||
def get_html_file(unique_id: str, db: Session = Depends(get_db)):
|
||||
html_file = db.query(HTMLFile).filter(HTMLFile.unique_id == unique_id).first()
|
||||
|
||||
if not html_file:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="HTML 文件不存在"
|
||||
)
|
||||
|
||||
# 生成完整链接
|
||||
html_url = f"{settings.frontend_base_url}/static/{html_file.filename}"
|
||||
|
||||
return {
|
||||
"message": "HTML 文件查询成功",
|
||||
"unique_id": html_file.unique_id,
|
||||
"url": html_url
|
||||
}
|
||||
23
html-generator/backend/app/schemas.py
Normal file
23
html-generator/backend/app/schemas.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class HTMLGenerateRequest(BaseModel):
|
||||
html_content: str
|
||||
|
||||
|
||||
class HTMLGenerateResponse(BaseModel):
|
||||
message: str
|
||||
unique_id: str
|
||||
url: str
|
||||
|
||||
|
||||
class HTMLFileResponse(BaseModel):
|
||||
id: int
|
||||
unique_id: str
|
||||
filename: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
18
html-generator/backend/package.json
Normal file
18
html-generator/backend/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "html-generator-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "HTML Generator Backend",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
}
|
||||
}
|
||||
4
html-generator/backend/requirements.txt
Normal file
4
html-generator/backend/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0.post1
|
||||
python-dotenv==1.0.0
|
||||
sqlalchemy==2.0.23
|
||||
70
html-generator/backend/server.js
Normal file
70
html-generator/backend/server.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const dotenv = require('dotenv');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 加载环境变量
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const port = 8000;
|
||||
|
||||
// 中间件
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// 生成唯一 ID
|
||||
function generateUniqueId() {
|
||||
return Math.random().toString(36).substring(2, 18) + Math.random().toString(36).substring(2, 18);
|
||||
}
|
||||
|
||||
// 确保静态文件目录存在
|
||||
const staticDir = path.resolve(__dirname, '../frontend/public/static');
|
||||
if (!fs.existsSync(staticDir)) {
|
||||
fs.mkdirSync(staticDir, { recursive: true });
|
||||
}
|
||||
|
||||
// API 路由
|
||||
app.post('/api/html/generate', (req, res) => {
|
||||
try {
|
||||
const { html_content } = req.body;
|
||||
|
||||
if (!html_content) {
|
||||
return res.status(400).json({ error: 'HTML 内容不能为空' });
|
||||
}
|
||||
|
||||
// 生成唯一 ID
|
||||
const uniqueId = generateUniqueId();
|
||||
|
||||
// 生成 HTML 文件路径
|
||||
const htmlFilename = `${uniqueId}.html`;
|
||||
const htmlPath = path.join(staticDir, htmlFilename);
|
||||
|
||||
// 写入 HTML 内容
|
||||
fs.writeFileSync(htmlPath, html_content, 'utf-8');
|
||||
|
||||
// 生成完整链接
|
||||
const frontendBaseUrl = process.env.FRONTEND_BASE_URL || 'http://localhost:3000';
|
||||
const htmlUrl = `${frontendBaseUrl}/static/${htmlFilename}`;
|
||||
|
||||
res.status(201).json({
|
||||
message: 'HTML 文件生成成功',
|
||||
unique_id: uniqueId,
|
||||
url: htmlUrl
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('生成 HTML 文件失败:', error);
|
||||
res.status(500).json({ error: `生成 HTML 文件失败: ${error.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// 健康检查
|
||||
app.get('/', (req, res) => {
|
||||
res.json({ message: 'HTML Generator API is running' });
|
||||
});
|
||||
|
||||
// 启动服务器
|
||||
app.listen(port, () => {
|
||||
console.log(`服务器运行在 http://localhost:${port}`);
|
||||
});
|
||||
87
html-generator/backend/server.py
Normal file
87
html-generator/backend/server.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import http.server
|
||||
import socketserver
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
PORT = 8000
|
||||
|
||||
# 生成唯一 ID
|
||||
def generate_unique_id():
|
||||
return ''.join(random.choices(string.ascii_letters + string.digits, k=16))
|
||||
|
||||
# 确保静态文件目录存在
|
||||
static_dir = os.path.join(os.path.dirname(__file__), '../frontend/public/static')
|
||||
if not os.path.exists(static_dir):
|
||||
os.makedirs(static_dir, exist_ok=True)
|
||||
|
||||
class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
if self.path == '/api/html/generate':
|
||||
# 读取请求体
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
post_data = self.rfile.read(content_length)
|
||||
|
||||
try:
|
||||
# 解析 JSON 数据
|
||||
data = json.loads(post_data)
|
||||
html_content = data.get('html_content', '')
|
||||
|
||||
if not html_content:
|
||||
self.send_response(400)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({'error': 'HTML 内容不能为空'}).encode('utf-8'))
|
||||
return
|
||||
|
||||
# 生成唯一 ID
|
||||
unique_id = generate_unique_id()
|
||||
|
||||
# 生成 HTML 文件路径
|
||||
html_filename = f"{unique_id}.html"
|
||||
html_path = os.path.join(static_dir, html_filename)
|
||||
|
||||
# 写入 HTML 内容
|
||||
with open(html_path, 'w', encoding='utf-8') as f:
|
||||
f.write(html_content)
|
||||
|
||||
# 生成完整链接
|
||||
frontend_base_url = 'http://localhost:3000'
|
||||
html_url = f"{frontend_base_url}/static/{html_filename}"
|
||||
|
||||
# 返回响应
|
||||
self.send_response(201)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.end_headers()
|
||||
response = {
|
||||
'message': 'HTML 文件生成成功',
|
||||
'unique_id': unique_id,
|
||||
'url': html_url
|
||||
}
|
||||
self.wfile.write(json.dumps(response).encode('utf-8'))
|
||||
except Exception as e:
|
||||
self.send_response(500)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({'error': f'生成 HTML 文件失败: {str(e)}'}).encode('utf-8'))
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == '/':
|
||||
# 健康检查
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({'message': 'HTML Generator API is running'}).encode('utf-8'))
|
||||
else:
|
||||
# 静态文件服务
|
||||
super().do_GET()
|
||||
|
||||
if __name__ == "__main__":
|
||||
with socketserver.TCPServer(("", PORT), MyHTTPRequestHandler) as httpd:
|
||||
print(f"服务器运行在 http://localhost:{PORT}")
|
||||
httpd.serve_forever()
|
||||
Reference in New Issue
Block a user