feat: AI生成图持久化记录(AiRenderRecord模型+图片落盘+记录列表/图片/删除端点+前端生成记录抽屉, 三模式统一)
This commit is contained in:
@@ -2,21 +2,29 @@
|
||||
|
||||
- quick 模式: /v1/images/generations 纯提示词生成
|
||||
- photo 模式: /v1/images/edits 以用户照片为参考图,把窗型合成进照片
|
||||
- blueprint 模式: /v1/images/edits 以手绘草图/2D 图纸为底图重绘工程大样图
|
||||
- 生成结果持久化: 图片落盘 runtime/ai_renders/{company}/{id}.png + ai_render_records 元数据
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import FileResponse
|
||||
from pathlib import Path
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from huamenchuang.core.config import settings
|
||||
from huamenchuang.core.deps import CurrentUser, DbSession
|
||||
from huamenchuang.core.response import fail, ok
|
||||
from huamenchuang.core.response import fail, ok, paginated
|
||||
from huamenchuang.models.extend import AiRenderRecord
|
||||
from huamenchuang.services.ai_prompt import build_prompt, describe_window, pick_size
|
||||
|
||||
router = APIRouter(prefix="/ai-render", tags=["AI 炫图"])
|
||||
|
||||
RENDER_DIR = Path(__file__).resolve().parents[3] / "runtime" / "ai_renders"
|
||||
|
||||
|
||||
class AiRenderBody(BaseModel):
|
||||
template_id: int | None = None
|
||||
@@ -182,7 +190,75 @@ async def generate(body: AiRenderBody, user: CurrentUser, db: DbSession):
|
||||
await asyncio.sleep(stage_wait)
|
||||
if not b64:
|
||||
return fail(last_err or "生成失败")
|
||||
return ok(data={"image_b64": b64, "prompt": prompt, "size": size,
|
||||
|
||||
# 持久化: 图片落盘 + 元数据入库(三模式统一记录)
|
||||
from huamenchuang.models.extend import AiRenderRecord
|
||||
rec = AiRenderRecord(company_id=user.company_id, user_id=user.id,
|
||||
user_name=user.nickname, mode=body.mode,
|
||||
template_id=body.template_id or 0, scene=body.scene,
|
||||
prompt=prompt, width_mm=int(info["width_mm"] or 0),
|
||||
height_mm=int(info["height_mm"] or 0))
|
||||
if body.template_id:
|
||||
from huamenchuang.models.windoor_template import WindoorTemplate
|
||||
t = await db.get(WindoorTemplate, body.template_id)
|
||||
if t:
|
||||
rec.template_name = t.name or ""
|
||||
db.add(rec)
|
||||
await db.flush()
|
||||
rec_dir = RENDER_DIR / str(user.company_id)
|
||||
rec_dir.mkdir(parents=True, exist_ok=True)
|
||||
(rec_dir / f"{rec.id}.png").write_bytes(base64.b64decode(b64))
|
||||
rec.file_path = f"{user.company_id}/{rec.id}.png"
|
||||
await db.flush()
|
||||
|
||||
return ok(data={"record_id": rec.id, "image_b64": b64, "prompt": prompt, "size": size,
|
||||
"features": {"width_mm": info["width_mm"], "height_mm": info["height_mm"],
|
||||
"summary": info["summary"]}},
|
||||
message="生成成功")
|
||||
|
||||
|
||||
# ---------------- 生成记录 ----------------
|
||||
|
||||
@router.get("/records")
|
||||
async def list_records(user: CurrentUser, db: DbSession, page: int = 1, page_size: int = 12,
|
||||
mode: str | None = None):
|
||||
"""AI 生图记录列表(本公司,按时间倒序)。"""
|
||||
conds = [AiRenderRecord.company_id == user.company_id]
|
||||
if mode:
|
||||
conds.append(AiRenderRecord.mode == mode)
|
||||
where = *conds,
|
||||
total = (await db.execute(select(func.count()).select_from(AiRenderRecord).where(*where))).scalar_one()
|
||||
stmt = (select(AiRenderRecord).where(*where).order_by(AiRenderRecord.id.desc())
|
||||
.offset((page - 1) * page_size).limit(page_size))
|
||||
items = list((await db.execute(stmt)).scalars())
|
||||
return ok(data=paginated([{
|
||||
"id": r.id, "mode": r.mode, "template_id": r.template_id, "template_name": r.template_name,
|
||||
"scene": r.scene, "width_mm": r.width_mm, "height_mm": r.height_mm,
|
||||
"user_name": r.user_name, "created_at": r.created_at.strftime("%Y-%m-%d %H:%M") if r.created_at else "",
|
||||
"url": f"/api/ai-render/records/{r.id}/image",
|
||||
} for r in items], page, page_size, total))
|
||||
|
||||
|
||||
@router.get("/records/{rid}/image")
|
||||
async def record_image(rid: int, user: CurrentUser, db: DbSession):
|
||||
"""记录原图(公司隔离)。"""
|
||||
rec = await db.get(AiRenderRecord, rid)
|
||||
if rec is None or rec.company_id != user.company_id:
|
||||
return fail("记录不存在")
|
||||
file = RENDER_DIR / rec.file_path
|
||||
if not file.exists():
|
||||
return fail("图片文件已丢失")
|
||||
return FileResponse(file, media_type="image/png",
|
||||
filename=f"ai-{rec.mode}-{rec.id}.png")
|
||||
|
||||
|
||||
@router.delete("/records/{rid}")
|
||||
async def delete_record(rid: int, user: CurrentUser, db: DbSession):
|
||||
rec = await db.get(AiRenderRecord, rid)
|
||||
if rec is None or rec.company_id != user.company_id:
|
||||
return fail("记录不存在")
|
||||
file = RENDER_DIR / rec.file_path
|
||||
if file.exists():
|
||||
file.unlink()
|
||||
await db.delete(rec)
|
||||
return ok(message="记录已删除")
|
||||
|
||||
@@ -21,6 +21,7 @@ from huamenchuang.models.contract import (
|
||||
WarehouseItem,
|
||||
)
|
||||
from huamenchuang.models.extend import (
|
||||
AiRenderRecord,
|
||||
Article,
|
||||
CompanyParam,
|
||||
Device,
|
||||
@@ -38,6 +39,7 @@ from huamenchuang.models.windoor_template import WindoorCatelog, WindoorTemplate
|
||||
|
||||
__all__ = [
|
||||
"AfterSale",
|
||||
"AiRenderRecord",
|
||||
"Announcement",
|
||||
"Article",
|
||||
"CompanyParam",
|
||||
|
||||
@@ -162,3 +162,23 @@ class SmsCode(Base):
|
||||
scene: Mapped[str] = mapped_column(String(16), default="login") # login/reset/bind
|
||||
used: Mapped[int] = mapped_column(default=0)
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
|
||||
class AiRenderRecord(Base):
|
||||
"""AI 生图记录(quick/photo/blueprint 云端生成结果持久化,图片落盘 runtime/ai_renders/)。"""
|
||||
|
||||
__tablename__ = "ai_render_records"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
company_id: Mapped[int] = mapped_column(default=1, index=True)
|
||||
user_id: Mapped[int] = mapped_column(default=0, index=True)
|
||||
user_name: Mapped[str] = mapped_column(String(64), default="")
|
||||
mode: Mapped[str] = mapped_column(String(16), default="quick") # quick/photo/blueprint
|
||||
template_id: Mapped[int] = mapped_column(default=0)
|
||||
template_name: Mapped[str] = mapped_column(String(64), default="")
|
||||
scene: Mapped[str] = mapped_column(String(32), default="")
|
||||
prompt: Mapped[str] = mapped_column(Text, default="")
|
||||
file_path: Mapped[str] = mapped_column(String(255), default="") # 相对 runtime/ai_renders/
|
||||
width_mm: Mapped[int] = mapped_column(default=0)
|
||||
height_mm: Mapped[int] = mapped_column(default=0)
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
Reference in New Issue
Block a user