定位

pf-service 是图模工坊的后端中台——所有前端(pf-app / pf-editor / pf-manage)与桌面端(pf-download)的业务请求都经过它;它负责鉴权、模板/订单/商品/相册/素材的读写、合成任务调度与成品下载 API。合成渲染本身已下沉到独立的 pf-compose-service,pf-service 通过 HTTP 调用它、不再内嵌 Puppeteer。

  • 运行时:Bun 1.x(启动快、TS 零编译、原生 Web API)
  • 框架:Elysia 1.4(elysia ^1.4.21,路由 + 校验 + 中间件,TypeBox 驱动)
  • ORM:Drizzle 0.45(drizzle-orm ^0.45.2,drizzle-kit ^0.31.8)
  • 数据库:PostgreSQL 16+(postgres ^3.4.7 postgres.js 驱动)
  • 缓存:Redis 6.x|对象存储:MinIO(Bun 内置 S3Client)
  • 鉴权:JWT (@elysiajs/jwt) + Bearer (@elysiajs/bearer)
  • 端口:开发环境 3000(生产 Docker 内 3001)

源码结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
apps/pf-service/
├── config.json # 运行时配置(端口/DB/Redis/S3/face/compose/jobs)
├── drizzle.config.ts # Drizzle Kit 配置(dialect: postgresql)
├── drizzle/ # 迁移 SQL
├── scripts/ # seed_*.ts / 数据准备脚本
├── test/ # bun test 用例
└── src/
├── index.ts # 启动入口(Elysia 装配 + 启动合成 Job)
├── config/ # 配置加载
├── db/
│ ├── postgres.ts # 连接池(drizzle + postgres.js, max 10)
│ ├── redis.ts / s3.ts # Redis / MinIO 客户端
│ └── schema.ts / seed.ts
├── plugins/ # auth.ts(JWT+Bearer)/ logger.ts
├── routes/
│ ├── pf-app/ # /api/app
│ ├── pf-editor/ # /api/editor
│ ├── pf-manage/ # /api/manage
│ ├── pf-download/ # /api/download
│ └── shared/ # oss / face / font / background(被 editor+app 复用)
├── jobs/
│ └── orderTemplateCompositionJob.ts # 合成轮询 Job(调合成微服务)
├── services/ # composeService / ossStorageService / seedService / ...
└── utils/
├── jobProcessor.ts # 合成主流程(HTTP → compose service)
├── compositionScene.ts # buildPreviewSceneJson
└── serviceResponse.ts # 统一响应封装

启动流程

src/index.ts 装配应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
new Elysia()
.use(cors())
.use(logger)
.use(pfManageRoutes) # /api/manage/*
.use(pfEditorRoutes) # /api/editor/*
.use(pfAppRoutes) # /api/app/*
.use(pfDownloadRoutes) # /api/download/*
.use(swagger({ path: "/docs" }))
.get("/", ...) .get("/health", ...)
.onError(...) # 统一错误兜底,set.status=200 + 结构化响应
.listen(config.server.port)

# 启动后(除非 jobs.composition.enabled=false)
startOrderTemplateCompositionJob(intervalMs) # 默认 10000ms
# SIGINT/SIGTERM → 停 Job → app.stop()

路由分区

后端路由按”调用方”分区,前缀统一带 /api(历史文档里的 /pf-manage、/shared 前缀已废弃):

前缀 调用方 鉴权
/api/manage/* 管理后台 pf-manage JWT(auth 插件)
/api/editor/* 模板编辑器 pf-editor JWT + 复用 shared
/api/app/* 终端用户 pf-app 订单访问令牌(非 JWT)+ 复用 shared
/api/download/* 下载客户端 pf-download JWT
/docs Swagger UI —
/health / 探活 —

shared 打包 oss / face / background-library / font-library,被 editor、app 复用。

鉴权

JWT 中间件封装在 plugins/auth.ts(jwt({ name:"jwt", secret }) + bearer()),由需要鉴权的路由组各自 use,不在根应用挂载:

  • secret 来自 config.auth.jwtSecret(可被 JWT_SECRET 环境变量覆盖)
  • 用户/角色/菜单表与 soybean-admin 标准结构对齐
  • /api/app/* 走订单访问令牌(findOrderForAccess(orderNo, token),一次性令牌,令牌哈希存 orders.orderAccessTokenHash),而非 JWT——终端用户无需登录账号即可凭订单链接编辑
  • GET /font-variants* 等只读接口在 auth 插件里被列为公开白名单

统一响应

utils/serviceResponse.ts:

1
2
3
4
5
6
7
8
9
export const SERVICE_SUCCESS_CODE = "0000";
export const SERVICE_ERROR_CODE = {
badRequest: "400", unauthorized: "401", forbidden: "403",
notFound: "404", conflict: "409",
upstreamUnavailable: "502", upstreamTimeout: "504",
internalError: "500", tokenExpired: "9999",
};
export const success = <T>(data: T, message?: string) => ({ code: SERVICE_SUCCESS_CODE, data, message });
export const failure = (code: string, message: string, data?) => ({ code, data: data ?? null, message });

全局 onError 把异常映射为结构化响应(VALIDATION→400 / NOT_FOUND→404 / 其它→500),并统一附 timestamp(success/failure 本身不带 timestamp,由入口/插件边缘补齐),HTTP 状态恒为 200。

数据访问

1
2
3
4
5
6
import { db } from "../db/postgres";
import { templates } from "../db/schema";
import { and, eq, isNull } from "drizzle-orm";

const rows = await db.select().from(templates)
.where(and(eq(templates.status, "normal"), isNull(templates.deletedAt)));
  • 软删强约束:所有查询带 isNull(deletedAt)
  • 列表瘦身投影:listTemplates / 分页接口只查必要元信息,不返回 sceneJson 大字段,编辑/应用时按 id 单查
  • 分页 + 复合索引:如 GET /api/manage/templates/page(current/size)+ count(),租户维度走 tenantResourceCondition + (tenantId, ...) 复合索引
  • 事务:await db.transaction(async (tx) => { ... })

后台 Job:自动合成

jobs/orderTemplateCompositionJob.ts 定时轮询:

  • 每 config.server.jobs.composition.intervalMs(默认 10s)跑一轮,isRunning 防重入
  • 每轮用 PostgreSQL FOR UPDATE ... SKIP LOCKED 原子抢占最多 MAX_COMPOSITION_CONCURRENCY = 3 条明细(pending、failed 且 attemptCount<3 且到点、或 processing 僵尸 >10min),置 processing
  • asyncPool(3, ...) 并发处理;buildPreviewSceneJson 合并快照 + 用户填值 + 订单变量(buyerName/goodsName/orderDate 等)
  • 实际渲染走 HTTP:utils/jobProcessor.ts::processComposition → services/composeService.ts::requestComposition → POST {composeService.baseUrl}/api/compose,超时 60s(AbortController),期望 { code:"0000", data:{ fileKey, publicUrl, ... } }
  • 失败退避:attemptCount++,nextRetryAt = now + 2^attempt s,≥3 次永久失败;无本地 Puppeteer 兜底(puppeteer 仍在 package.json 但 src/ 未使用)
  • syncParentSubmissionStatus 聚合明细状态回写 submission.status 与 orders.composeStatus

完整机制见 自动合成流水线,渲染细节见 合成微服务。

OSS 预签名(收敛后的契约)

routes/shared/ossRoutes.ts(前缀 /oss):GET /oss/config、GET /oss/upload-url(必填 purpose)、GET /oss/download-url(必填 fileKey)。

services/ossStorageService.ts 里 purpose 必须命中固定的 StoragePurpose 白名单(user-upload / user-crop / auto-fill / template-preview / editor-image / background / background-crop / album-preview / font / composition),未知直接抛错;normalizeObjectKey 拒绝空路径、外链 http(s)://、../. 段并清理控制字符。预签名有效期 86400s,返回的 URL 默认改写为同源 /storage 代理前缀(除非 { raw: true })。

下载 API

routes/pf-download/downloadRoutes.ts(前缀 /api/download,需 JWT + 租户上下文):

接口 说明
GET /orders/page 用户订单分页 + 每单提交统计
GET /shops/options 店铺筛选项
GET /submissions/page 提交分页,可按 status(合成) 与 hasDownloaded 过滤,返回项数/完成数/图片数
POST /submissions/:id/receipt 标记首次下载(非 completed 返回 404/409)
GET /submissions/:id/images 列出可下载成品,每图带 downloadUrl
GET /images/:sid/:iid/:idx/file 预签名取回上游并流式回传(content-disposition: attachment)

下载状态是时间戳/布尔(orders.hasDownloaded / firstDownloadedAt、submissions.downloadedAt),首次写入即固化,与合成状态解耦、无独立 Job。

对外集成

  • 合成服务:services/composeService.ts(requestComposition / checkComposeHealth),配置 composeService.baseUrl(http://localhost:3030)、timeoutMs 60000
  • 人脸服务:routes/shared/faceRoutes.ts 是 thin proxy → faceService.baseUrl + /api/face/detect,把相对图片 URL 补成绝对 MinIO 地址(publicBaseUrl),超时映射 504/502
  • MinIO:db/s3.ts 用 Bun S3Client,前端直传走预签名,成品由合成服务自行 PutObject
  • OpenAPI:@elysiajs/swagger 从路由 schema 生成 /docs(声明 BearerAuth)

开发指令

1
2
3
4
5
6
7
8
9
cd apps/pf-service
bun dev # bun --watch src/index.ts
bun typecheck # tsc --noEmit
bun test # bun 内置测试
bun db:generate # 生成迁移
bun db:push # 同步 schema(dev)
bun db:migrate # 执行迁移
bun db:studio # 可视化
# 根目录:bun lint(oxfmt + oxlint --fix)

注意:pf-service 本身没有 build 脚本——生产由部署脚本用 bun build --compile --target=bun-linux-x64 编译成单文件二进制(见 部署)。

下一步

站内搜索

没有找到内容!