游乐游手机版
首页/AI教程/文章详情

图片生成任务到用户隔离:AIGC后端与PostgreSQL建模实践

时间:2026-08-05 22:59
基于AIGCCreativeStudio实践,后端采用Express+TypeScript与PostgreSQL17,通过users、generation_tasks、images三表模型实现任务状态机、图片本地存储及受认证访问,确保用户隔离与资源安全。

从图片生成任务到用户隔离:AIGC Creative Studio 后端与 PostgreSQL 建模实践

很多AIGC项目的第一版,跑得都挺快——前端填个Prompt,后端调一次模型接口,把URL返回给页面,搞定。可一旦想继续往下做,问题就一个个冒出来了:生成需要几十秒怎么办?链接过期了怎么办?用户A能不能看到用户B的图片?编辑后的图片存在哪里?服务重启后任务还在不在?换台新电脑,怎么快速把开发环境跑起来?

从图片生成任务到用户隔离:AIGC Creative Studio 后端与 PostgreSQL 建模实践

这篇文章基于AIGC Creative Studio的真实代码展开。项目本身不是生产级SaaS,而是一个用于学习完整AIGC全栈链路、也能作为面试案例讲解的本地项目。文中会明确区分“当前已实现的能力”和“生产环境仍应补齐的能力”。

一、项目的后端目标与边界

后端要解决的不是“返回一张图片”,而是维护下面这个闭环:

用户登录↓
提交生成参数↓
创建本地任务(pending)并立即返回 taskId↓
后台调用图片 Provider,更新 processing / succeeded / failed↓
下载 Provider 临时图片,保存到本地文件系统↓
任务和图片元数据写入 PostgreSQL↓
用户在生成库查询、下载、编辑或删除自己的图片

当前后端技术选型如下:

目标实现
HTTP APIExpress + TypeScript
数据库PostgreSQL 17 + pg
密码bcryptjs 哈希
登录态JWT + HttpOnly Cookie,同时兼容 Bearer Token
真实生图Provider 接口 + DashScope Wanx 实现
图片二进制server/storage/images/
元数据PostgreSQL usersgeneration_tasksimages
本地环境Docker Compose PostgreSQL

这里有一个重要边界:浏览器直接导入的本地素材目前保存于 IndexedDB,不上传服务端;服务端 images 表管理的是 AI 生成图片以及保存回生成库的编辑 PNG。

二、不要让 index.ts 同时承担应用配置和端口监听

后端入口拆成两个文件:

server/src/app.ts
创建 Express app,注册中间件和路由

server/src/index.ts
加载 .env,读取 PORT,调用 app.listen()

这样拆分的价值在于测试。Supertest 可以直接请求导出的 app,无需占用 3001 端口:

import request from 'supertest'
import { app } from '../app.js'

const response = await request(app).get('/api/health')

app.ts 负责装配:

app.use(cors({ origin: true, credentials: true }))
app.use(express.json())
app.use('/api/auth', authRouter)
app.use('/api/generations', generationsRouter)

图片读取没有直接暴露 express.static,而是走受保护接口:

GET /api/images/:filename

它先验证登录用户,再检查该文件是否属于该用户,最后才从受限目录读取二进制。这避免了“知道文件名就能看图”的资源越权问题。

三、数据库连接:连接池、环境变量与快速失败

数据库连接集中在 server/src/database/database.ts

let databasePool: Pool | null = null

export const getDatabasePool = (): Pool => {
  if (!databasePool) {
    databasePool = new Pool({ connectionString: getDatabaseUrl() })
  }
  return databasePool
}

这里使用延迟初始化连接池,而不是每次请求创建一个 PostgreSQL 客户端。DATABASE_URL 缺失时会立即抛错,而不是让业务路由在运行中间出现难以定位的空连接错误。

本地 Docker 开发配置:

services:
  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: aigc
      POSTGRES_PASSWORD: aigc_dev_password
      POSTGRES_DB: aigc_studio
    ports:
      - "5432:5432"

后端本机 .env 使用:

DATABASE_URL=postgresql://aigc:aigc_dev_password@localhost:5432/aigc_studio
PORT=3001
JWT_SECRET=至少32位的本机随机字符串

面试时可以主动说明:仓库中的 Docker 密码仅用于本地学习;真实数据库连接串、JWT 密钥和 DashScope API Key 都在 .env 中,并被 .gitignore 排除,不能提交到 Git。

四、三张表如何关联

当前 Schema 的核心是三张表:usersgeneration_tasksimages

users (1)
└── generation_tasks (N)
    └── images (N)
        └── source_image_id → images.id(编辑作品的来源)

1. users:认证主体

CREATE TABLE users (
    id UUID PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    display_name VARCHAR(100) NOT NULL,
    password_hash TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
  • id 是内部用户主键;
  • email 有唯一约束,避免重复注册;
  • password_hash 保存 bcrypt 哈希,绝不保存明文密码;
  • 创建、更新时间使用 TIMESTAMPTZ,减少时区歧义。

2. generation_tasks:一次生成请求,而不是一张图片

CREATE TABLE generation_tasks (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    status VARCHAR(20) NOT NULL CHECK (status IN ('pending', 'processing', 'succeeded', 'failed')),
    prompt TEXT NOT NULL,
    negative_prompt TEXT,
    aspect_ratio VARCHAR(5) NOT NULL,
    image_count SMALLINT NOT NULL,
    seed INTEGER,
    style VARCHAR(20) NOT NULL,
    error_code VARCHAR(100),
    error_message TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    completed_at TIMESTAMPTZ
);

为什么任务表不直接把图片 URL 写成一个字符串?因为“生成请求”和“生成结果”并不是一对一:一次任务可能多图,也可能失败、取消或后续增加编辑作品。任务表保存请求参数、状态和错误;图片表保存每个图片实体。

user_id 外键采用 ON DELETE CASCADE,删除用户时可级联删除其任务。项目还建立了:

CREATE INDEX generation_tasks_user_created_at_index
ON generation_tasks (user_id, created_at DESC);

它对应生成库最常见的访问模式:按当前用户读取最新任务。

3. images:图片元数据、来源关系和稳定顺序

CREATE TABLE images (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    generation_task_id UUID REFERENCES generation_tasks(id) ON DELETE SET NULL,
    kind VARCHAR(20) NOT NULL CHECK (kind IN ('generated', 'edited', 'imported')),
    storage_key TEXT NOT NULL,
    mime_type VARCHAR(100) NOT NULL,
    source_image_id UUID REFERENCES images(id) ON DELETE SET NULL,
    position INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

字段设计中有三个值得注意的点:

  1. storage_key 不是外部临时 URL,而是服务端本地文件名;API 对外构造 /api/images/{storage_key}
  2. source_image_id 是图片自关联。编辑作品可追溯到原图,但原图被删除后使用 SET NULL,不强制删除编辑作品;
  3. position 表示同一任务中的稳定显示顺序。

第三点非常容易忽略。前端下载和编辑使用 imageIndex。如果仅按 created_at, id 排序,批量生成的图片可能有相同时间,而 UUID 排序又是随机的,下一次读取时“第 0 张图”可能变了。项目因此在写入时按数组下标保存 position,读取时 ORDER BY position ASC,并创建唯一索引:

CREATE UNIQUE INDEX images_generation_task_position_index
ON images (generation_task_id, position);

五、任务状态机:先返回 taskId,再后台执行

创建任务接口是:

POST /api/generations

请求校验完成后,服务端先创建 pending 任务并返回 HTTP 202:

{
  "success": true,
  "message": "Generation request accepted",
  "data": {
    "taskId": "UUID",
    "status": "pending",
    "request": { }
  }
}

然后后台状态按以下路径推进:

pending → processing → succeeded
                      ↘ failed

这比让 HTTP 请求一直等待 Provider 返回更合理:浏览器不会被长请求阻塞;用户可以切换页面;前端只需轮询 GET /api/generations/:taskId 即可获取最新状态。

当前项目通过 ImageGenerationProvider 进行平台隔离:

interface ImageGenerationProvider {
  readonly name: string
  generate(input: GenerateImageInput): Promise<GenerateImageResult>
}

Wanx Provider 处理比例到实际尺寸映射、异步任务查询、超时、网络错误和安全错误转换。业务路由并不需要知道 DashScope 的具体 HTTP 协议。

首次真实调用还会强制按 1 张图片处理,避免前端传入多图时产生不可控成本。这是一种很适合学习项目和云模型接入初期的成本保护策略。

六、Provider 成功后,为什么还要下载图片

Provider 返回的图片 URL 往往是临时地址。若直接写入数据库,过期后历史图片库会失效。

因此 Provider 成功后的流程是:

外部临时 URL
→ Node.js 原生 fetch 下载二进制
→ 写入 server/storage/images/{taskId}-{index}.png
→ 图片表保存 storage_key
→ 对外暴露受认证保护的 /api/images/{filename}

本地存储模块会严格校验文件名:只接受 .png,要求文件名等于 basename(filename),并确认 resolve 后路径仍在 storage/images 目录下。这样下载、删除和读取接口都不能被客户端传入的路径穿越攻击利用。

值得注意的是:服务端不会接受客户端传入任意图片 URL 再去 fetch。编辑、下载和删除都只通过已保存的任务与图片索引定位资源,从源头避免 SSRF 风险。

七、登录态与资源授权

认证流程分为三步:

  1. 注册或登录时,用 bcrypt 比较或生成密码哈希;
  2. 使用 JWT_SECRET 创建包含 sub(用户 ID)和邮箱的 JWT;
  3. 将 token 放入 HttpOnly Cookie,并在 JSON 响应中返回 token 以兼容 Bearer 请求。

认证中间件优先读取:

Authorization: Bearer 

没有 Bearer Token 时,再解析 Cookie 中的 aigc_access_token。验证成功后把用户信息挂到 request.authUser

任务查询的核心不是“先按 taskId 查出来再判断”,而是在 Repository 查询条件中带上 user_id

WHERE id = $1 AND user_id = $2

因此即使用户猜到另一个 taskId,也只能得到 404。图片访问同样通过:

SELECT 1 FROM images WHERE storage_key = $1 AND user_id = $2 LIMIT 1

确认归属后才读文件。这样数据库授权与文件授权保持同一条用户边界。

前端编辑器加载受保护图片时也有一个容易踩坑的细节:图片 API 和 Vite 开发服务器通常端口不同。Canvas 使用 new Image() 加载时必须设置:

image.crossOrigin = 'use-credentials'

anonymous 不会携带 Cookie,会导致受保护的 /api/images/... 返回 401,最终 Canvas 只会触发 onerror。后端 CORS 需要允许 credentials,登录 Cookie 也要按本地/生产环境正确设置 sameSitesecure

八、Repository:为什么读取需要一致性快照

生成任务和图片分别存于两张表。若先查任务、再查图片,后台恰好在两次查询之间完成生成,就可能出现“状态还是 processing,但结果中已有图片”的混合响应。

当前 Repository 将两次读取放进 PostgreSQL 的只读一致性事务:

await client.query('BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY')
const taskResult = await client.query(/* generation_tasks */)
const imageResult = await client.query(/* images */)
await client.query('COMMIT')

这让一次 API 响应对应同一数据库快照。任务与图片的写入也使用事务:先锁定已有任务并检查 owner,再 upsert 任务记录,更新图片元数据,最后提交或回滚。

九、图片编辑作品如何回存

编辑器导出当前 Canvas 为 PNG Blob。保存到生成库时,前端向后端发送:

POST /api/generations/:taskId/images/:imageIndex/edits
Content-Type: image/png
Body: PNG 二进制

后端校验顺序包括:

  • 当前用户是否拥有任务;
  • 任务是否已经 succeeded
  • 图片索引是否为非负整数且确实存在;
  • Content-Type 是否为 image/png
  • 请求体是否为空、是否符合 PNG 文件头。

PNG 文件头是:

89 50 4E 47 0D 0A 1A 0A

通过后使用 crypto.randomUUID() 生成编辑文件名,保存本地文件,并将 kind='edited'source_image_idposition 等元数据写回数据库。原图并不会被覆盖。

十、如何在新电脑快速复现

项目不提交数据库实际数据和图片文件,只提交代码、docker-compose.ymlschema.sql.env.example。新电脑启动流程:

git clone 
Set-Location aigc-creative-studio
npm ci
Set-Location server
npm ci
Set-Location ..
docker compose up -d postgres
Copy-Item ..env.example ..env
Copy-Item .server.env.example .server.env
Get-Content .serversqlschema.sql | docker compose exec -T postgres psql -U aigc -d aigc_studio

之后分别启动 server 和前端,重新注册账号即可。schema.sql 使用 CREATE ... IF NOT EXISTS 与必要的 ALTER ... IF NOT EXISTS,既能创建空数据库,也能补齐例如 images.position 这样的后续字段。

十一、面试全栈工程师时,建议主动讲清的点

1. 为什么不用“同步调用模型并直接返回图片”?

回答重点:生图是长耗时异步任务。先返回 taskId 可以降低 HTTP 超时风险,让前端切页后继续轮询,也便于后续替换为队列、SSE 或 WebSocket。

2. 为什么任务和图片要分表?

回答重点:任务表示请求生命周期,图片表示结果实体。一对多建模支持多图、失败记录、编辑作品、自关联来源和后续扩展。

3. 为什么图片要有 position?

回答重点:前端编辑和下载使用 imageIndex;数据库默认返回顺序不可靠,UUID 和同一时间戳都不能表达用户看到的顺序。position 将 UI 数组下标变成持久化业务约束。

4. 如何避免用户越权访问图片?

回答重点:每次任务和图片查询都带 user_id 条件;图片文件不是公开静态目录,读取前先检查数据库归属;无权限返回 404。

5. 如何避免 Provider 临时链接过期?

回答重点:成功后立即由服务端下载图片,保存本地文件,数据库只保存内部 storage key,对外通过受保护 API 访问。

6. Canvas 特效如何保证能导出?

回答重点:不只做 CSS 显示效果。所有滤镜和动画都以 Canvas 像素为结果源,PNG 通过 toBlob,WebM 通过 captureStream,因此预览和导出一致。

7. 如何测试而不消耗真实模型额度?

回答重点:Provider 测试 Mock 全局 fetch;轮询和超时使用 fake timers;Supertest 直接请求 app;CI 使用占位 Key、禁用真实生成、不会请求外部模型平台。

十二、当前实现的真实局限,以及下一步演进

面试中不要把本地项目包装成生产系统。当前版本仍有明确边界:

  • 图片保存在本地文件系统,未接对象存储和 CDN;
  • Provider 调用由当前 Node 进程后台执行,未接 Redis、消息队列或分布式任务系统;
  • 同一任务的编辑/删除在单 Node 进程内串行化,多实例部署仍应使用乐观锁、版本号 CAS 或数据库级事务变更;
  • 数据库测试目前主要 Mock Repository,生产前应增加独立 PostgreSQL 集成测试;
  • 登录是账号密码模式,未接 OAuth、验证码、限流、审计与密码找回;
  • Docker Compose 密码仅适用于本地开发,生产必须使用密钥管理和隔离网络。

比较合理的下一步不是立即增加更多页面,而是:补 PostgreSQL 集成测试、为任务引入版本控制、将图片文件迁移到对象存储、将生图调用迁移到持久化任务队列。

结语

一个 AIGC 全栈项目的难点,往往不在于发出一次模型请求,而在于把任务、结果、权限、文件、编辑和本地开发环境连接成稳定闭环。

在 AIGC Creative Studio 中,我把这条链路拆成了可解释的层次:Express 路由负责协议和授权、Provider 负责外部平台差异、Repository 负责 PostgreSQL 一致性、Storage 负责文件安全、Canvas 负责编辑结果一致性。对学习项目而言,这比堆砌更多效果更有价值;对全栈面试而言,这些取舍也比“我调用过某个模型 API”更能体现工程能力。

来源:https://juejin.cn/post/7670094721702576168
上一篇动态代码拖累SEO?用Gofair纯静态页面剔除冗余代码 下一篇WorkBuddy使用一个月避坑指南:5个常见问题及解决方案
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
WorkBuddy使用一个月避坑指南:5个常见问题及解决方案
AI教程 · 2026-08-05

WorkBuddy使用一个月避坑指南:5个常见问题及解决方案

使用WorkBuddy一个月,踩过指令模糊、未指定输出格式、反复打断任务、积分过期、未验证结果五个坑。对应解法:明确文件路径、动作、维度、格式和文件名;指定输出格式;耐心等待;优先使用快过期积分;抽查验证汇总逻辑。

动态代码拖累SEO?用Gofair纯静态页面剔除冗余代码
AI教程 · 2026-08-05

动态代码拖累SEO?用Gofair纯静态页面剔除冗余代码

静态页面加载速度快,搜索引擎爬取效率高,优于动态建站。某孕产妇用品企业改用Gofair静态建站,五天多关键词冲至谷歌首页。SEO效果需通过关键词反查验证,流量数据易被干扰。未来静态页面策略将更主流。

WorkBuddy AI工作台实操教程 零基础搞定周报与数据分析
AI教程 · 2026-08-05

WorkBuddy AI工作台实操教程 零基础搞定周报与数据分析

使用WorkBuddy时需下达清晰指令,包括文件路径、输出格式和完整需求。典型场景如周报生成、Excel数据清洗与可视化,需注意指定去重列和输出格式,避免打断大文件处理。定时任务可自动化抓取新闻,轻量模型和Ask模式可节省积分。

CC压缩机制之toolResultBudget源码实现原理技术深度解读
AI教程 · 2026-08-05

CC压缩机制之toolResultBudget源码实现原理技术深度解读

toolResultBudget机制在每次模型请求前自动执行,检查单个API-levelusermessage中tool_result总量是否超过200K字符,若超则将最大的工具结果落盘并替换为预览,以降低上下文噪音。该机制位于压缩流水线最前端,在microcompact之前执行,确保后续压缩更高效。