定位
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.7postgres.js 驱动) - 缓存:Redis 6.x|对象存储:MinIO(Bun 内置
S3Client) - 鉴权:JWT (
@elysiajs/jwt) + Bearer (@elysiajs/bearer) - 端口:开发环境
3000(生产 Docker 内3001)
源码结构
1 | apps/pf-service/ |
启动流程
src/index.ts 装配应用:
1 | new Elysia() |
路由分区
后端路由按”调用方”分区,前缀统一带 /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 | export const SERVICE_SUCCESS_CODE = "0000"; |
全局 onError 把异常映射为结构化响应(VALIDATION→400 / NOT_FOUND→404 / 其它→500),并统一附 timestamp(success/failure 本身不带 timestamp,由入口/插件边缘补齐),HTTP 状态恒为 200。
数据访问
1 | import { db } from "../db/postgres"; |
- 软删强约束:所有查询带
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用 BunS3Client,前端直传走预签名,成品由合成服务自行 PutObject - OpenAPI:
@elysiajs/swagger从路由 schema 生成/docs(声明BearerAuth)
开发指令
1 | cd apps/pf-service |
注意:
pf-service本身没有build脚本——生产由部署脚本用bun build --compile --target=bun-linux-x64编译成单文件二进制(见 部署)。