1
This commit is contained in:
@@ -0,0 +1,590 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, UploadFile, File, Form, Depends, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from app.config import settings
|
||||
from app.models import init_database, close_database
|
||||
from app.utils.redis import init_redis, close_redis
|
||||
from app.api.v1 import api_router
|
||||
from app.middleware.logging import RequestLoggingMiddleware
|
||||
from app.middleware.anti_crawler import AntiCrawlerMiddleware
|
||||
from app.middleware.rate_limit import RateLimitMiddleware
|
||||
from app.middleware.request_encrypt import RequestEncryptMiddleware
|
||||
from app.services.log_config import decrypt_data
|
||||
|
||||
logging.basicConfig(level=logging.INFO if settings.DEBUG else logging.WARNING)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
from app.models import async_session
|
||||
|
||||
# Ensure upload directory exists
|
||||
os.makedirs(settings.UPLOAD_LOCAL_PATH, exist_ok=True)
|
||||
await init_database()
|
||||
await init_redis()
|
||||
await _seed_data()
|
||||
|
||||
# Start task queue (handles both video and image generation)
|
||||
from app.services.video_queue import task_queue
|
||||
await task_queue.recover()
|
||||
queue_task = asyncio.create_task(task_queue.run())
|
||||
|
||||
app.state.db_session_factory = async_session
|
||||
|
||||
yield
|
||||
|
||||
task_queue.stop()
|
||||
await queue_task
|
||||
await close_database()
|
||||
await close_redis()
|
||||
|
||||
|
||||
async def _seed_data():
|
||||
"""Insert initial data on first run."""
|
||||
from app.models import async_session
|
||||
from app.models.user import User
|
||||
from app.models.system_config import SystemConfig
|
||||
from app.models.video_engine import VideoEngine
|
||||
from app.models.image_engine import ImageEngine
|
||||
from app.models.credit_ratio import CreditRatio
|
||||
from app.models.model_config import ModelConfig
|
||||
from app.services.auth import hash_password
|
||||
from app.utils.id_gen import generate_id
|
||||
from sqlalchemy import select
|
||||
|
||||
async with async_session() as db:
|
||||
# Check if admin exists
|
||||
result = await db.execute(select(User).where(User.is_admin == True).limit(1))
|
||||
admin = result.scalar_one_or_none()
|
||||
if not admin:
|
||||
admin_user = User(
|
||||
id=generate_id(),
|
||||
username="admin",
|
||||
email="admin@videogen.ai",
|
||||
phone="13800000000",
|
||||
hashed_password=hash_password("123456"),
|
||||
credits=10000,
|
||||
is_admin=True,
|
||||
user_type="admin",
|
||||
)
|
||||
db.add(admin_user)
|
||||
|
||||
# Seed demo user
|
||||
result = await db.execute(select(User).where(User.username == "demo"))
|
||||
demo = result.scalar_one_or_none()
|
||||
if not demo:
|
||||
demo_user = User(
|
||||
id=generate_id(),
|
||||
username="demo",
|
||||
email="demo@videogen.ai",
|
||||
phone="13888888888",
|
||||
hashed_password=hash_password("123456"),
|
||||
credits=2680,
|
||||
is_admin=False,
|
||||
user_type="frontend",
|
||||
)
|
||||
db.add(demo_user)
|
||||
|
||||
# Seed system configs
|
||||
configs = [
|
||||
("site_name", "民众普康", "网站名称"),
|
||||
("site_logo", "", "网站Logo URL"),
|
||||
("seo_title", "民众普康 - AI视频生成平台", "SEO标题"),
|
||||
("seo_description", "专业的AI视频生成服务", "SEO描述"),
|
||||
("seo_keywords", "AI视频,视频生成,人工智能", "SEO关键词"),
|
||||
# Agreement configs
|
||||
("user_agreement_url", "", "用户协议PDF"),
|
||||
("privacy_policy_url", "", "隐私政策PDF"),
|
||||
# Payment configs
|
||||
("payment_wechat_enabled", "false", "微信支付启用"),
|
||||
("payment_wechat_mch_id", "", "微信商户号"),
|
||||
("payment_wechat_api_key", "", "微信API密钥"),
|
||||
("payment_alipay_enabled", "false", "支付宝启用"),
|
||||
("payment_alipay_app_id", "", "支付宝AppID"),
|
||||
("payment_alipay_private_key", "", "支付宝私钥"),
|
||||
# Text credit config
|
||||
("text_credits_per_1000_tokens", "1", "每1000 token消耗文本积分"),
|
||||
]
|
||||
for key, value, desc in configs:
|
||||
existing = await db.execute(
|
||||
select(SystemConfig).where(SystemConfig.key == key)
|
||||
)
|
||||
if not existing.scalar_one_or_none():
|
||||
db.add(
|
||||
SystemConfig(
|
||||
id=generate_id(), key=key, value=value, description=desc
|
||||
)
|
||||
)
|
||||
|
||||
# Seed video engine
|
||||
existing_engine = await db.execute(
|
||||
select(VideoEngine).where(VideoEngine.provider == "ark")
|
||||
)
|
||||
if not existing_engine.scalars().first():
|
||||
db.add(
|
||||
VideoEngine(
|
||||
id=generate_id(),
|
||||
name="Seedance 2.0",
|
||||
provider="ark",
|
||||
api_base="https://ark.cn-beijing.volces.com/api/v3",
|
||||
api_key="",
|
||||
model_name="doubao-seedance-2-0-260128",
|
||||
supported_ratios='["16:9","4:3","1:1","3:4","9:16","21:9"]',
|
||||
supported_resolutions='["480p","720p","1080p"]',
|
||||
supported_durations='[4,5,6,7,8,9,10,11,12,13,14,15]',
|
||||
max_duration=15,
|
||||
is_active=True,
|
||||
priority=10,
|
||||
)
|
||||
)
|
||||
existing_fast_engine = await db.execute(
|
||||
select(VideoEngine).where(VideoEngine.model_name == "doubao-seedance-2-0-fast-260128")
|
||||
)
|
||||
if not existing_fast_engine.scalars().first():
|
||||
db.add(
|
||||
VideoEngine(
|
||||
id=generate_id(),
|
||||
name="Seedance 2.0 fast",
|
||||
provider="ark",
|
||||
api_base="https://ark.cn-beijing.volces.com/api/v3",
|
||||
api_key="",
|
||||
model_name="doubao-seedance-2-0-fast-260128",
|
||||
supported_ratios='["16:9","4:3","1:1","3:4","9:16","21:9"]',
|
||||
supported_resolutions='["480p","720p","1080p"]',
|
||||
supported_durations='[4,5,6,7,8,9,10,11,12,13,14,15]',
|
||||
max_duration=15,
|
||||
is_active=True,
|
||||
priority=10,
|
||||
)
|
||||
)
|
||||
|
||||
# Seed image engine
|
||||
existing_img_engine = await db.execute(
|
||||
select(ImageEngine).where(ImageEngine.provider == "ark").limit(1)
|
||||
)
|
||||
if not existing_img_engine.scalar_one_or_none():
|
||||
db.add(
|
||||
ImageEngine(
|
||||
id=generate_id(),
|
||||
name="豆包文生图",
|
||||
provider="ark",
|
||||
api_base="https://ark.cn-beijing.volces.com/api/v3",
|
||||
api_key="",
|
||||
model_name="doubao-seedream-5-0-260128",
|
||||
supported_models='["doubao-seedream-5-0-260128"]',
|
||||
supported_sizes='{"2K":{"1:1":"2048×2048","4:3":"2304×1728","3:4":"1728×2304","16:9":"2560×1440","9:16":"1600×2848","3:2":"2496×1664","2:3":"1664×2496","21:9":"3024×1296"},"4K":{"1:1":"4096×4096","4:3":"4608×3456","3:4":"3520×4704","16:9":"5404×3040","9:16":"3040×5504","3:2":"4992×3328","2:3":"3328×4992","21:9":"6197×2656"}}',
|
||||
default_size="2K",
|
||||
generate_url="https://ark.cn-beijing.volces.com/api/v3/images/generations",
|
||||
is_active=True,
|
||||
priority=10,
|
||||
)
|
||||
)
|
||||
|
||||
# Seed default Ark model config
|
||||
existing_sdk_model = await db.execute(
|
||||
select(ModelConfig).where(ModelConfig.provider == "sdk").limit(1)
|
||||
)
|
||||
if not existing_sdk_model.scalar_one_or_none():
|
||||
db.add(
|
||||
ModelConfig(
|
||||
id=generate_id(),
|
||||
name="火山引擎 Ark",
|
||||
provider="sdk",
|
||||
api_base="https://ark.cn-beijing.volces.com/api/v3",
|
||||
api_key="",
|
||||
model_name="doubao-seed-2-0-lite-260215",
|
||||
weight=1,
|
||||
max_tokens=4096,
|
||||
temperature=0.7,
|
||||
is_active=True,
|
||||
priority=10,
|
||||
)
|
||||
)
|
||||
|
||||
# Seed credit ratios - use first model config if available
|
||||
model_result = await db.execute(select(ModelConfig).limit(1))
|
||||
model = model_result.scalar_one_or_none()
|
||||
if model:
|
||||
existing_ratio = await db.execute(
|
||||
select(CreditRatio).where(CreditRatio.model_config_id == model.id).limit(1)
|
||||
)
|
||||
if not existing_ratio.scalars().first():
|
||||
for gen_type, resolution, ratio_val, base, per_sec in [
|
||||
("video", "480p", 1.0, 60, 2),
|
||||
("video", "720p", 1.0, 80, 2),
|
||||
("video", "1080p", 1.5, 120, 3),
|
||||
("image", "2K", 1.0, 4, 0),
|
||||
("image", "4K", 1.0, 6, 0),
|
||||
]:
|
||||
db.add(
|
||||
CreditRatio(
|
||||
id=generate_id(),
|
||||
model_config_id=model.id,
|
||||
gen_type=gen_type,
|
||||
resolution=resolution,
|
||||
ratio=ratio_val,
|
||||
base_credits=base,
|
||||
per_second_credits=per_sec,
|
||||
)
|
||||
)
|
||||
|
||||
# Seed menu configs
|
||||
from app.models.menu_config import MenuConfig
|
||||
|
||||
default_menus = [
|
||||
("/projects", "我的项目", "HomeOutlined", 0, "frontend"),
|
||||
("/records", "生成记录", "PlayCircleOutlined", 1, "frontend"),
|
||||
("/credits", "积分中心", "WalletOutlined", 2, "frontend"),
|
||||
("/conversation", "ai对话", "StarOutlined", 3, "frontend"),
|
||||
]
|
||||
for path, label, icon, order, target in default_menus:
|
||||
existing = await db.execute(
|
||||
select(MenuConfig).where(MenuConfig.path == path)
|
||||
)
|
||||
if not existing.scalar_one_or_none():
|
||||
db.add(
|
||||
MenuConfig(
|
||||
id=generate_id(),
|
||||
path=path,
|
||||
label=label,
|
||||
icon=icon,
|
||||
sort_order=order,
|
||||
is_active=True,
|
||||
menu_type="page",
|
||||
menu_target=target,
|
||||
is_default=True,
|
||||
)
|
||||
)
|
||||
|
||||
# Seed admin group menus first, then page menus with parent_id
|
||||
admin_groups = [
|
||||
("模型配置", "RobotOutlined", 6),
|
||||
("系统设置", "SettingOutlined", 99),
|
||||
]
|
||||
group_ids = {}
|
||||
for label, icon, order in admin_groups:
|
||||
existing = await db.execute(
|
||||
select(MenuConfig).where(
|
||||
MenuConfig.label == label,
|
||||
MenuConfig.menu_type == "group",
|
||||
MenuConfig.menu_target == "admin",
|
||||
)
|
||||
)
|
||||
group = existing.scalar_one_or_none()
|
||||
if not group:
|
||||
gid = generate_id()
|
||||
db.add(
|
||||
MenuConfig(
|
||||
id=gid,
|
||||
path="",
|
||||
label=label,
|
||||
icon=icon,
|
||||
sort_order=order,
|
||||
is_active=True,
|
||||
menu_type="group",
|
||||
menu_target="admin",
|
||||
)
|
||||
)
|
||||
group_ids[label] = gid
|
||||
else:
|
||||
group_ids[label] = group.id
|
||||
|
||||
# (path, label, icon, sort_order, parent_group_label or None)
|
||||
admin_menus = [
|
||||
("/", "数据概览", "DashboardOutlined", 0, None),
|
||||
("/users", "用户管理", "UserOutlined", 1, None),
|
||||
("/credit-records", "交易流水", "WalletOutlined", 2, None),
|
||||
("/generation-records", "生成记录", "VideoCameraOutlined", 3, None),
|
||||
("/recharge-packages", "充值套餐", "GiftOutlined", 4, None),
|
||||
("/notifications", "消息推送", "BellOutlined", 5, None),
|
||||
("/video-engines", "视频引擎", "PlayCircleOutlined", 0, "模型配置"),
|
||||
("/models", "模型配置", "RobotOutlined", 1, "模型配置"),
|
||||
("/image-engines", "图片模型", "PictureOutlined", 2, "模型配置"),
|
||||
("/credit-ratios", "积分比例", "CalculatorOutlined", 3, "模型配置"),
|
||||
("/payment", "支付配置", "DollarOutlined", 1, "系统设置"),
|
||||
("/industries", "行业配置", "AppstoreOutlined", 2, "系统设置"),
|
||||
("/menu-configs", "菜单配置", "SettingOutlined", 3, "系统设置"),
|
||||
("/settings", "系统设置", "SettingOutlined", 4, "系统设置"),
|
||||
("/operation-logs", "操作日志", "HistoryOutlined", 5, "系统设置"),
|
||||
]
|
||||
for path, label, icon, order, parent_group in admin_menus:
|
||||
existing = await db.execute(
|
||||
select(MenuConfig).where(
|
||||
MenuConfig.path == path,
|
||||
MenuConfig.menu_target == "admin",
|
||||
)
|
||||
)
|
||||
if not existing.scalar_one_or_none():
|
||||
db.add(
|
||||
MenuConfig(
|
||||
id=generate_id(),
|
||||
path=path,
|
||||
label=label,
|
||||
icon=icon,
|
||||
sort_order=order,
|
||||
is_active=True,
|
||||
menu_type="page",
|
||||
menu_target="admin",
|
||||
parent_id=group_ids.get(parent_group),
|
||||
)
|
||||
)
|
||||
|
||||
# Seed recharge packages
|
||||
from app.models.recharge_package import RechargePackage
|
||||
|
||||
default_packages = [
|
||||
("体验包", 500, 49, 0, "首次体验推荐", "normal", 0),
|
||||
("进阶包", 2000, 168, 200, "最受欢迎", "normal", 1),
|
||||
("专业包", 5000, 388, 500, "高性价比", "normal", 2),
|
||||
("企业包", 20000, 1280, 2000, "团队首选", "normal", 3),
|
||||
]
|
||||
for name, credits, price, bonus, desc, ptype, order in default_packages:
|
||||
existing = await db.execute(
|
||||
select(RechargePackage).where(RechargePackage.name == name)
|
||||
)
|
||||
if not existing.scalar_one_or_none():
|
||||
db.add(
|
||||
RechargePackage(
|
||||
id=generate_id(),
|
||||
name=name,
|
||||
credits=credits,
|
||||
price=price,
|
||||
bonus_credits=bonus,
|
||||
description=desc,
|
||||
package_type=ptype,
|
||||
is_gift=False,
|
||||
is_active=True,
|
||||
sort_order=order,
|
||||
)
|
||||
)
|
||||
|
||||
# Seed industry configs
|
||||
from app.models.industry_config import IndustryConfig
|
||||
|
||||
default_industries = [
|
||||
("ecommerce", "电商", "ShoppingCartOutlined", "电商行业的视频生成模板", 0),
|
||||
("social", "社交", "TeamOutlined", "社交行业的视频生成模板", 1),
|
||||
]
|
||||
for key, label, icon, desc, order in default_industries:
|
||||
existing = await db.execute(
|
||||
select(IndustryConfig).where(IndustryConfig.key == key)
|
||||
)
|
||||
if not existing.scalar_one_or_none():
|
||||
db.add(
|
||||
IndustryConfig(
|
||||
id=generate_id(),
|
||||
key=key,
|
||||
label=label,
|
||||
icon=icon,
|
||||
description=desc,
|
||||
skills="[]",
|
||||
is_active=True,
|
||||
sort_order=order,
|
||||
)
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
application = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
version=settings.APP_VERSION,
|
||||
lifespan=lifespan,
|
||||
docs_url="/internal/api-docs",
|
||||
redoc_url="/internal/api-redoc",
|
||||
)
|
||||
|
||||
# Middleware (outermost first)
|
||||
application.add_middleware(RequestLoggingMiddleware)
|
||||
application.add_middleware(AntiCrawlerMiddleware)
|
||||
application.add_middleware(RateLimitMiddleware)
|
||||
application.add_middleware(RequestEncryptMiddleware)
|
||||
application.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.CORS_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
expose_headers=["X-Encrypted"],
|
||||
)
|
||||
|
||||
# Routes
|
||||
application.include_router(api_router, prefix="/api")
|
||||
|
||||
# Static files for uploads
|
||||
upload_dir = os.path.abspath(settings.UPLOAD_LOCAL_PATH)
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
application.mount("/uploads", StaticFiles(directory=upload_dir), name="uploads")
|
||||
|
||||
# Static files for generated videos
|
||||
storage_dir = os.path.abspath(settings.STORAGE_LOCAL_PATH)
|
||||
os.makedirs(storage_dir, exist_ok=True)
|
||||
application.mount("/videos", StaticFiles(directory=storage_dir), name="videos")
|
||||
|
||||
# Static files for generated images
|
||||
storage_image_dir = os.path.abspath(settings.STORAGE_IMAGE_LOCAL_PATH)
|
||||
os.makedirs(storage_image_dir, exist_ok=True)
|
||||
application.mount("/images", StaticFiles(directory=storage_image_dir), name="images")
|
||||
|
||||
@application.get("/internal/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
@application.get("/internal/decrypt-data", response_class=HTMLResponse)
|
||||
async def decrypt_data_page():
|
||||
html_content = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>数据解密工具</title>
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; }
|
||||
h1 { color: #1a1a2e; }
|
||||
textarea { width: 100%; height: 150px; padding: 12px; border: 1px solid #e2e8f0; border-radius: 8px; font-family: monospace; font-size: 14px; }
|
||||
button { background: #6366f1; color: white; border: none; padding: 12px 24px; border-radius: 8px; cursor: pointer; font-size: 16px; }
|
||||
button:hover { background: #4f46e5; }
|
||||
.result { margin-top: 20px; padding: 16px; background: #f8fafc; border-radius: 8px; }
|
||||
.result pre { white-space: pre-wrap; word-break: break-all; font-size: 14px; color: #334155; }
|
||||
.error { color: #dc2626; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>数据解密工具</h1>
|
||||
<p>用于解密日志中的加密数据</p>
|
||||
<textarea id="encryptedInput" placeholder="请输入加密的数据..."></textarea>
|
||||
<br><br>
|
||||
<button onclick="decrypt()">解密</button>
|
||||
<div class="result" id="result"></div>
|
||||
<script>
|
||||
async function decrypt() {
|
||||
const input = document.getElementById('encryptedInput').value.trim();
|
||||
const resultDiv = document.getElementById('result');
|
||||
|
||||
if (!input) {
|
||||
resultDiv.innerHTML = '<p class="error">请输入加密数据</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/decrypt', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ encrypted_data: input })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
resultDiv.innerHTML = '<pre>' + JSON.stringify(data.decrypted_data, null, 2) + '</pre>';
|
||||
} else {
|
||||
resultDiv.innerHTML = '<p class="error">解密失败: ' + data.error + '</p>';
|
||||
}
|
||||
} catch (e) {
|
||||
resultDiv.innerHTML = '<p class="error">请求失败: ' + e.message + '</p>';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return HTMLResponse(content=html_content)
|
||||
|
||||
@application.post("/api/decrypt")
|
||||
async def api_decrypt(encrypted_data: dict):
|
||||
try:
|
||||
data = encrypted_data.get("encrypted_data", "")
|
||||
if not data:
|
||||
return {"success": False, "error": "缺少加密数据"}
|
||||
|
||||
decrypted = decrypt_data(data)
|
||||
if decrypted == {} and data:
|
||||
return {"success": False, "error": "解密失败,数据格式不正确"}
|
||||
|
||||
return {"success": True, "decrypted_data": decrypted}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@application.post("/api/admin/upload-pdf")
|
||||
async def upload_pdf(
|
||||
file: UploadFile = File(...),
|
||||
config_key: str = Form(...),
|
||||
):
|
||||
"""Upload a PDF file and save URL to system config."""
|
||||
from app.dependencies import get_db
|
||||
from app.models.system_config import SystemConfig
|
||||
from app.models.base import async_session
|
||||
from sqlalchemy import select
|
||||
|
||||
if not file.filename or not file.filename.endswith('.pdf'):
|
||||
raise HTTPException(status_code=400, detail="仅支持PDF文件")
|
||||
|
||||
# Save file
|
||||
safe_name = f"{config_key}.pdf"
|
||||
file_path = os.path.join(settings.UPLOAD_LOCAL_PATH, safe_name)
|
||||
content = await file.read()
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(content)
|
||||
|
||||
url = f"/uploads/{safe_name}"
|
||||
|
||||
# Update system config
|
||||
async with async_session() as db:
|
||||
result = await db.execute(
|
||||
select(SystemConfig).where(SystemConfig.key == config_key)
|
||||
)
|
||||
config = result.scalar_one_or_none()
|
||||
if config:
|
||||
config.value = url
|
||||
else:
|
||||
db.add(SystemConfig(
|
||||
id=f"cfg_{config_key}",
|
||||
key=config_key,
|
||||
value=url,
|
||||
description="用户协议" if "agreement" in config_key else "隐私政策",
|
||||
))
|
||||
await db.commit()
|
||||
|
||||
return {"url": url}
|
||||
|
||||
@application.get("/internal/", response_class=HTMLResponse)
|
||||
async def index():
|
||||
return """<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head><meta charset="utf-8"><title>民众普康</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,sans-serif;max-width:600px;margin:80px auto;text-align:center;color:#1a1a2e;background:#f5f6fa}
|
||||
h1{font-size:28px;margin-bottom:8px}
|
||||
h1 span{color:#6366f1}
|
||||
p{color:#64748b;margin:4px 0}
|
||||
a{color:#6366f1;text-decoration:none;font-weight:600}
|
||||
a:hover{text-decoration:underline}
|
||||
.cards{display:flex;gap:16px;justify-content:center;margin-top:32px}
|
||||
.card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:20px 28px;text-align:center}
|
||||
.card h3{margin:0 0 4px;font-size:16px}
|
||||
.card p{font-size:13px;margin:0}
|
||||
.badge{display:inline-block;background:#6366f1;color:#fff;font-size:11px;padding:2px 8px;border-radius:6px;margin-bottom:12px}
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="badge">Backend Running</div>
|
||||
<h1>民众普康<span>.AI</span></h1>
|
||||
<p>后端 API 服务</p>
|
||||
<p style="font-size:13px;margin-top:12px">版本 v""" + settings.APP_VERSION + """</p>
|
||||
<div class="cards">
|
||||
<div class="card"><h3><a href="/internal/api-docs">API 文档</a></h3><p>Swagger UI 交互式文档</p></div>
|
||||
<div class="card"><h3><a href="/internal/api-redoc">ReDoc</a></h3><p>ReDoc 格式文档</p></div>
|
||||
<div class="card"><h3><a href="/internal/health">健康检查</a></h3><p>服务状态</p></div>
|
||||
<div class="card"><h3><a href="/internal/decrypt-data">解密数据</a></h3><p>数据解密</p></div>
|
||||
</div>
|
||||
</body></html>"""
|
||||
|
||||
return application
|
||||
|
||||
|
||||
app = create_app()
|
||||
Reference in New Issue
Block a user