feat: AI生成图持久化记录(AiRenderRecord模型+图片落盘+记录列表/图片/删除端点+前端生成记录抽屉, 三模式统一)

This commit is contained in:
HHY
2026-10-07 04:08:27 -07:00
parent f9456b5633
commit ac55932969
7 changed files with 197 additions and 4 deletions
+78 -2
View File
@@ -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="记录已删除")
+2
View File
@@ -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",
+20
View File
@@ -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)