- 前端 React+TypeScript (源码+构建产物) - 后端 Python FastAPI - 数据库迁移 001-015 - Docker部署配置 - 微信ClawBot API频道绑定 - 项目文档 README.md
312 lines
10 KiB
Python
312 lines
10 KiB
Python
"""
|
|
广告位管理 API(管理员)
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File
|
|
from fastapi.responses import FileResponse
|
|
from sqlalchemy.orm import Session
|
|
from ...database import get_db, SessionLocal
|
|
from ...models import AdBanner, AIModel
|
|
from ...auth import get_current_admin
|
|
from ...middleware.log_service import LogService
|
|
import uuid
|
|
import os
|
|
import json
|
|
import httpx
|
|
from datetime import datetime
|
|
|
|
router = APIRouter()
|
|
|
|
# 广告图片存储目录
|
|
ADS_IMAGE_DIR = "/data/share/project/data/ads"
|
|
os.makedirs(ADS_IMAGE_DIR, exist_ok=True)
|
|
|
|
|
|
@router.get("/")
|
|
async def get_ads(
|
|
admin_user=Depends(get_current_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""获取广告轮播图列表"""
|
|
ads = db.query(AdBanner).order_by(AdBanner.sort_order.asc()).all()
|
|
return [
|
|
{
|
|
"id": a.id,
|
|
"title": a.title,
|
|
"image_url": a.image_url,
|
|
"link_app_id": a.link_app_id,
|
|
"link_app_name": a.link_app_name,
|
|
"sort_order": a.sort_order,
|
|
"click_count": a.click_count,
|
|
"status": a.status,
|
|
"created_at": a.created_at.isoformat() if a.created_at else None,
|
|
"updated_at": a.updated_at.isoformat() if a.updated_at else None,
|
|
}
|
|
for a in ads
|
|
]
|
|
|
|
|
|
@router.post("/")
|
|
async def create_ad(
|
|
title: str,
|
|
image_url: str = None,
|
|
link_app_id: str = None,
|
|
link_app_name: str = None,
|
|
sort_order: int = 0,
|
|
status: str = "active",
|
|
admin_user=Depends(get_current_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""创建广告"""
|
|
ad = AdBanner(
|
|
id=str(uuid.uuid4()),
|
|
title=title,
|
|
image_url=image_url,
|
|
link_app_id=link_app_id,
|
|
link_app_name=link_app_name,
|
|
sort_order=sort_order,
|
|
status=status,
|
|
)
|
|
db.add(ad)
|
|
db.commit()
|
|
|
|
LogService.log_audit(
|
|
admin_id=admin_user.id,
|
|
admin_name=admin_user.username,
|
|
action="create_ad",
|
|
target_type="ad_banner",
|
|
target_id=ad.id,
|
|
detail=f"创建广告 {title}",
|
|
)
|
|
|
|
return {"message": "广告已创建", "id": ad.id}
|
|
|
|
|
|
@router.put("/{id}")
|
|
async def update_ad(
|
|
id: str,
|
|
title: str = None,
|
|
image_url: str = None,
|
|
link_app_id: str = None,
|
|
link_app_name: str = None,
|
|
sort_order: int = None,
|
|
status: str = None,
|
|
admin_user=Depends(get_current_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""更新广告"""
|
|
ad = db.query(AdBanner).filter(AdBanner.id == id).first()
|
|
if not ad:
|
|
raise HTTPException(status_code=404, detail="广告不存在")
|
|
|
|
if title is not None:
|
|
ad.title = title
|
|
if image_url is not None:
|
|
ad.image_url = image_url
|
|
if link_app_id is not None:
|
|
ad.link_app_id = link_app_id
|
|
if link_app_name is not None:
|
|
ad.link_app_name = link_app_name
|
|
if sort_order is not None:
|
|
ad.sort_order = sort_order
|
|
if status is not None:
|
|
ad.status = status
|
|
ad.updated_at = datetime.now()
|
|
db.commit()
|
|
|
|
LogService.log_audit(
|
|
admin_id=admin_user.id,
|
|
admin_name=admin_user.username,
|
|
action="update_ad",
|
|
target_type="ad_banner",
|
|
target_id=id,
|
|
detail=f"更新广告 {ad.title}",
|
|
)
|
|
|
|
return {"message": "广告已更新"}
|
|
|
|
|
|
@router.delete("/{id}")
|
|
async def delete_ad(
|
|
id: str,
|
|
admin_user=Depends(get_current_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""删除广告"""
|
|
ad = db.query(AdBanner).filter(AdBanner.id == id).first()
|
|
if not ad:
|
|
raise HTTPException(status_code=404, detail="广告不存在")
|
|
|
|
db.delete(ad)
|
|
db.commit()
|
|
|
|
LogService.log_audit(
|
|
admin_id=admin_user.id,
|
|
admin_name=admin_user.username,
|
|
action="delete_ad",
|
|
target_type="ad_banner",
|
|
target_id=id,
|
|
detail=f"删除广告 {ad.title}",
|
|
)
|
|
|
|
return {"message": "广告已删除"}
|
|
|
|
|
|
@router.put("/{id}/toggle")
|
|
async def toggle_ad(
|
|
id: str,
|
|
status: str,
|
|
admin_user=Depends(get_current_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""切换广告状态"""
|
|
ad = db.query(AdBanner).filter(AdBanner.id == id).first()
|
|
if not ad:
|
|
raise HTTPException(status_code=404, detail="广告不存在")
|
|
|
|
ad.status = status
|
|
ad.updated_at = datetime.now()
|
|
db.commit()
|
|
|
|
LogService.log_audit(
|
|
admin_id=admin_user.id,
|
|
admin_name=admin_user.username,
|
|
action="toggle_ad",
|
|
target_type="ad_banner",
|
|
target_id=id,
|
|
detail=f"切换广告 {ad.title} 状态为 {status}",
|
|
)
|
|
|
|
return {"message": f"广告已{status}"}
|
|
|
|
|
|
@router.post("/upload-image")
|
|
async def upload_ad_image(
|
|
file: UploadFile = File(...),
|
|
admin_user=Depends(get_current_admin),
|
|
):
|
|
"""上传广告图片,保存到 /data/share/project/data/ads/ 目录"""
|
|
# 校验文件类型
|
|
allowed_types = {"image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"}
|
|
if file.content_type not in allowed_types:
|
|
raise HTTPException(status_code=400, detail="仅支持 PNG/JPG/GIF/WebP/SVG 格式的图片")
|
|
|
|
# 生成唯一文件名
|
|
ext = os.path.splitext(file.filename or "image.png")[1] or ".png"
|
|
if ext.lower() not in (".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"):
|
|
ext = ".png"
|
|
filename = f"{uuid.uuid4()}{ext}"
|
|
filepath = os.path.join(ADS_IMAGE_DIR, filename)
|
|
|
|
# 保存文件
|
|
content = await file.read()
|
|
if len(content) > 10 * 1024 * 1024: # 10MB 限制
|
|
raise HTTPException(status_code=400, detail="图片大小不能超过10MB")
|
|
with open(filepath, "wb") as f:
|
|
f.write(content)
|
|
|
|
# 返回可访问的URL
|
|
image_url = f"/app/api/admin/ads/images/{filename}"
|
|
|
|
LogService.log_audit(
|
|
admin_id=admin_user.id,
|
|
admin_name=admin_user.username,
|
|
action="upload_ad_image",
|
|
target_type="ad_banner",
|
|
detail=f"上传广告图片 {filename}",
|
|
)
|
|
|
|
return {"message": "图片上传成功", "image_url": image_url, "filename": filename}
|
|
|
|
|
|
@router.get("/images/{filename}")
|
|
async def get_ad_image(filename: str):
|
|
"""获取广告图片"""
|
|
filepath = os.path.join(ADS_IMAGE_DIR, filename)
|
|
if not os.path.exists(filepath):
|
|
raise HTTPException(status_code=404, detail="图片不存在")
|
|
return FileResponse(filepath)
|
|
|
|
|
|
@router.post("/generate-image")
|
|
async def generate_ad_image(
|
|
prompt: str = Query(default=""),
|
|
admin_user=Depends(get_current_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""AI生成广告图:调用LLM生成prompt,然后生成图片(这里生成渐变色占位图)"""
|
|
# 如果没有传入prompt,使用默认prompt
|
|
final_prompt = prompt.strip() if prompt.strip() else "A modern tech AI platform banner advertisement, sleek interface, gradient blue purple theme, high quality"
|
|
|
|
# 尝试使用默认LLM模型生成更优的prompt
|
|
enhanced_prompt = final_prompt
|
|
try:
|
|
default_model = db.query(AIModel).filter(
|
|
AIModel.is_default == True, AIModel.is_enabled == True
|
|
).first()
|
|
if default_model and default_model.endpoint:
|
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
headers = {}
|
|
if default_model.api_key:
|
|
headers["Authorization"] = f"Bearer {default_model.api_key}"
|
|
resp = await client.post(
|
|
default_model.endpoint,
|
|
json={
|
|
"model": default_model.name,
|
|
"messages": [
|
|
{"role": "system", "content": "你是一个广告创意专家。请根据用户输入的主题,生成一个简短的中文广告图像prompt描述(不超过100字),用于AI图像生成。只需要输出prompt文本本身,不要其他内容。"},
|
|
{"role": "user", "content": f"主题: {final_prompt}"},
|
|
],
|
|
"max_tokens": 200,
|
|
},
|
|
headers=headers,
|
|
)
|
|
if resp.status_code == 200:
|
|
content = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
if content:
|
|
enhanced_prompt = content.strip()[:200]
|
|
except Exception as e:
|
|
# LLM增强失败,使用原始prompt
|
|
pass
|
|
|
|
# 生成一个唯一的图片文件名(使用prompt的hash作为标识)
|
|
import hashlib
|
|
prompt_hash = hashlib.md5(enhanced_prompt.encode()).hexdigest()[:12]
|
|
filename = f"ai_gen_{prompt_hash}.svg"
|
|
filepath = os.path.join(ADS_IMAGE_DIR, filename)
|
|
|
|
# 生成SVG广告图
|
|
svg_content = f'''<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="400" viewBox="0 0 1200 400">
|
|
<defs>
|
|
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1"/>
|
|
<stop offset="50%" style="stop-color:#764ba2;stop-opacity:1"/>
|
|
<stop offset="100%" style="stop-color:#5B5FED;stop-opacity:1"/>
|
|
</linearGradient>
|
|
<linearGradient id="shape" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" style="stop-color:rgba(255,255,255,0.15);stop-opacity:1"/>
|
|
<stop offset="100%" style="stop-color:rgba(255,255,255,0.05);stop-opacity:1"/>
|
|
</linearGradient>
|
|
</defs>
|
|
<rect width="1200" height="400" fill="url(#bg)"/>
|
|
<circle cx="1000" cy="100" r="200" fill="url(#shape)" opacity="0.3"/>
|
|
<circle cx="200" cy="300" r="150" fill="url(#shape)" opacity="0.2"/>
|
|
<rect x="600" y="150" width="300" height="200" rx="20" fill="url(#shape)" opacity="0.15" transform="rotate(-15 750 250)"/>
|
|
<text x="600" y="180" text-anchor="middle" font-family="Arial, sans-serif" font-size="42" font-weight="bold" fill="#ffffff" filter="drop-shadow(0 2px 4px rgba(0,0,0,0.2))">{enhanced_prompt[:40]}</text>
|
|
<text x="600" y="240" text-anchor="middle" font-family="Arial, sans-serif" font-size="20" fill="rgba(255,255,255,0.8)">Powered by AIFlow</text>
|
|
</svg>'''
|
|
|
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
f.write(svg_content)
|
|
|
|
image_url = f"/app/api/admin/ads/images/{filename}"
|
|
|
|
LogService.log_audit(
|
|
admin_id=admin_user.id,
|
|
admin_name=admin_user.username,
|
|
action="generate_ad_image",
|
|
target_type="ad_banner",
|
|
detail=f"AI生成广告图 {filename}, prompt: {enhanced_prompt[:50]}",
|
|
)
|
|
|
|
return {"message": "AI广告图已生成", "image_url": image_url, "prompt": enhanced_prompt}
|